/**
 * Tests that using the "failNonIntentLocksIfWaitNeeded" failpoint allows us to immediately
 * fail DDL operations blocked behind prepare, as we know they will not be able to acquire locks
 * anyway. The DDL ops will fail here because they won't be able to get a MODE_X lock on the
 * global or database resources.
 *
 * @tags: [uses_transactions, uses_prepare_transaction]
 */

import {PrepareHelpers} from "jstests/core/txns/libs/prepare_helpers.js";
import {IndexCatalogHelpers} from "jstests/libs/index_catalog_helpers.js";
import {ReplSetTest} from "jstests/libs/replsettest.js";

const rst = new ReplSetTest({nodes: 1});
rst.startSet();
rst.initiate();

const dbName = "test";
const collName = "ddl_ops_after_prepare_lock_failpoint";
const indexName = "test_index";

const primary = rst.getPrimary();
const testDB = primary.getDB(dbName);
const testColl = testDB.getCollection(collName);

// Create the collection we will be working with.
assert.commandWorked(testDB.runCommand({create: collName, writeConcern: {w: "majority"}}));

// Also build an index (on the same collection) which we will later attempt to drop.
assert.commandWorked(
    testDB.runCommand({createIndexes: collName, indexes: [{key: {"num": 1}, name: indexName}]}));

const session = primary.startSession({causalConsistency: false});
const sessionDB = session.getDatabase(dbName);
const sessionColl = sessionDB.getCollection(collName);

session.startTransaction();
assert.commandWorked(sessionColl.insert({_id: 42}));

PrepareHelpers.prepareTransaction(session);

assert.commandWorked(
    primary.adminCommand({configureFailPoint: "failNonIntentLocksIfWaitNeeded", mode: "alwaysOn"}));

/**
 * Tests that conflicting DDL ops fail immediately.
 */

// Collection names for DDL ops that will fail.
const collToDrop = collName;
const collToRenameFrom = collName;
const collToRenameTo = "rename_collection_to_fail";
const indexToCreate = "create_index_to_fail";
const indexToDrop = indexName;

let testDDLOps = () => {
    // Also attempt to delete our original collection (it is in conflict anyway, but should
    // fail to acquire the db lock in the first place).
    assert.throws(function() {
        testDB.getCollection(collToDrop).drop();
    });
    assert(testDB.getCollectionNames().includes(collToDrop));

    // Same goes for trying to rename it.
    assert.commandFailedWithCode(
        testDB.getCollection(collToRenameFrom).renameCollection(collToRenameTo),
        ErrorCodes.LockTimeout);
    assert(testDB.getCollectionNames().includes(collToRenameFrom));
    assert(!testDB.getCollectionNames().includes(collToRenameTo));

    assert.commandFailedWithCode(testDB.adminCommand({
        renameCollection: testDB.getCollection(collToRenameFrom).getFullName(),
        to: testDB.getSiblingDB('test2').getCollection(collToRenameTo).getFullName(),
    }),
                                 ErrorCodes.LockTimeout);

    // Attempt to add a new index to that collection.
    assert.commandFailedWithCode(
        testDB.runCommand(
            {createIndexes: collName, indexes: [{key: {"b": 1}, name: indexToCreate}]}),
        ErrorCodes.LockTimeout);
    assert.eq(null, IndexCatalogHelpers.findByName(testColl.getIndexes(), indexToCreate));

    // Try dropping the index we created originally. This should also fail.
    assert.commandFailedWithCode(testDB.runCommand({dropIndexes: collName, index: indexToDrop}),
                                 ErrorCodes.LockTimeout);
    assert.neq(null, IndexCatalogHelpers.findByName(testColl.getIndexes(), indexToDrop));
};

/**
 * Tests that CRUD operations on the same collection succeed.
 */

const docToInsert = {
    num: 100
};
const docToUpdateFrom = docToInsert;
const docToUpdateTo = {
    num: 101
};
const docToRemove = docToUpdateTo;

let testCRUDOps = (collConn) => {
    // Having an extra document in the collection is necessary to avoid prepare conflicts when
    // deleting adjacent documents. See SERVER-40167.
    assert.commandWorked(collConn.insert({num: 1}));

    assert.commandWorked(collConn.insert(docToInsert));
    assert.eq(100, collConn.findOne(docToInsert).num);

    // This will not encounter a prepare conflict because there is an index on "num" that
    // eliminates the need for using a collection scan.
    assert.commandWorked(collConn.update(docToUpdateFrom, docToUpdateTo));
    assert.eq(101, collConn.findOne(docToUpdateTo).num);

    assert.commandWorked(collConn.remove(docToRemove));
    assert.eq(null, collConn.findOne(docToUpdateFrom));
    assert.eq(null, collConn.findOne(docToUpdateTo));
};

// First test DDL ops (should fail).
testDDLOps();

// Then test operations outside of transactions (should succeed).
testCRUDOps(testColl);

// Also test operations as part of a transaction (should succeed).
testCRUDOps(
    primary.startSession({causalConsistency: false}).getDatabase(dbName).getCollection(collName));

assert.commandWorked(
    primary.adminCommand({configureFailPoint: "failNonIntentLocksIfWaitNeeded", mode: "off"}));

assert.commandWorked(session.abortTransaction_forTesting());
rst.stopSet();
