/**
 * 1. Test that rollback can successfully recover committed prepared transactions that were prepared
 * before the stable timestamp and committed between the stable timestamp and the common point.
 * 2. Test that rollback can successfully recover aborted prepared transactions that were prepared
 * and aborted between the stable timestamp and the common point.
 *
 * @tags: [
 *   uses_prepare_transaction,
 *   uses_transactions,
 * ]
 */
(function() {
"use strict";

load("jstests/core/txns/libs/prepare_helpers.js");
load("jstests/replsets/libs/rollback_test.js");

const dbName = "test";
const collName = "recover_committed_aborted_prepared_transactions";

const rollbackTest = new RollbackTest(dbName);
let primary = rollbackTest.getPrimary();

// Create collection we're using beforehand.
let testDB = primary.getDB(dbName);
let testColl = testDB.getCollection(collName);

assert.commandWorked(testDB.runCommand({create: collName}));

// Start two different sessions on the primary.
let session1 = primary.startSession({causalConsistency: false});
const sessionID1 = session1.getSessionId();
const session2 = primary.startSession({causalConsistency: false});

let sessionDB1 = session1.getDatabase(dbName);
let sessionColl1 = sessionDB1.getCollection(collName);

const sessionDB2 = session2.getDatabase(dbName);
const sessionColl2 = sessionDB2.getCollection(collName);

assert.commandWorked(sessionColl1.insert({_id: 1}));

rollbackTest.awaitLastOpCommitted();

// Prepare a transaction on the first session which will be committed eventually.
session1.startTransaction();
assert.commandWorked(sessionColl1.insert({_id: 2}));
const prepareTimestamp = PrepareHelpers.prepareTransaction(session1);

// Prevent the stable timestamp from moving beyond the following prepared transactions so
// that when we replay the oplog from the stable timestamp, we correctly recover them.
assert.commandWorked(
    primary.adminCommand({configureFailPoint: 'disableSnapshotting', mode: 'alwaysOn'}));

// The following transactions will be prepared before the common point, so they must be in
// prepare after rollback recovery.

// Prepare another transaction on the second session which will be aborted.
session2.startTransaction();
assert.commandWorked(sessionColl2.insert({_id: 3}));
const prepareTimestamp2 = PrepareHelpers.prepareTransaction(session2, {w: 1});

// Commit the first transaction.
assert.commandWorked(PrepareHelpers.commitTransaction(session1, prepareTimestamp));

// Abort the second transaction.
assert.commandWorked(session2.abortTransaction_forTesting());

// Check that we have two transactions in the transactions table.
assert.eq(primary.getDB('config')['transactions'].find().itcount(), 2);

// The following write will be rolled back.
rollbackTest.transitionToRollbackOperations();
assert.commandWorked(testColl.insert({_id: 4}));

rollbackTest.transitionToSyncSourceOperationsBeforeRollback();
rollbackTest.transitionToSyncSourceOperationsDuringRollback();

try {
    rollbackTest.transitionToSteadyStateOperations();
} finally {
    assert.commandWorked(
        primary.adminCommand({configureFailPoint: 'disableSnapshotting', mode: 'off'}));
}

// Make sure there are two transactions in the transactions table.
assert.eq(primary.getDB('config')['transactions'].find().itcount(), 2);

// Make sure we can see the first two writes and the insert from the first prepared transaction.
// Make sure we cannot see the insert from the second prepared transaction or the writes after
// transitionToRollbackOperations.
assert.sameMembers(testColl.find().toArray(), [{_id: 1}, {_id: 2}]);
assert.sameMembers(sessionColl1.find().toArray(), [{_id: 1}, {_id: 2}]);

assert.eq(testColl.count(), 2);
assert.eq(sessionColl1.count(), 2);

// Get the correct members after the topology changes.
primary = rollbackTest.getPrimary();
testDB = primary.getDB(dbName);
testColl = testDB.getCollection(collName);
const rst = rollbackTest.getTestFixture();
const secondaries = rst.getSecondaries();

// Make sure we can successfully run a prepared transaction on the same first session after
// going through rollback. This ensures that the session state has properly been restored.
session1 = PrepareHelpers.createSessionWithGivenId(primary, sessionID1, {causalConsistency: false});
sessionDB1 = session1.getDatabase(dbName);
sessionColl1 = sessionDB1.getCollection(collName);
// The next transaction on this session should have a txnNumber of 1. We explicitly set this
// since createSessionWithGivenId does not restore the current txnNumber in the shell.
session1.setTxnNumber_forTesting(1);

session1.startTransaction();
assert.commandWorked(sessionColl1.insert({_id: 5}));
const prepareTimestamp3 = PrepareHelpers.prepareTransaction(session1);
// Make sure we can successfully retry the commitTransaction command after rollback.
assert.commandWorked(PrepareHelpers.commitTransaction(session1, prepareTimestamp3));

session1.startTransaction();
assert.commandWorked(sessionColl1.insert({_id: 6}));
PrepareHelpers.prepareTransaction(session1);
assert.commandWorked(session1.abortTransaction_forTesting());
// Retrying the abortTransaction command should fail with a NoSuchTransaction error.
assert.commandFailedWithCode(sessionDB1.adminCommand({
    abortTransaction: 1,
    txnNumber: NumberLong(session1.getTxnNumber_forTesting()),
    autocommit: false,
}),
                             ErrorCodes.NoSuchTransaction);

// Make sure we can see the insert after committing the prepared transaction.
assert.sameMembers(testColl.find().toArray(), [{_id: 1}, {_id: 2}, {_id: 5}]);
assert.eq(testColl.count(), 3);

rollbackTest.stop();
}());
