/**
 * The purpose of this test is to verify that simple CRUD operations are rolled back
 * successfully in multiversion replica sets. This test induces communication between
 * the rollback node and the sync source during rollback. This is done in order to
 * exercise rollback via refetch in the case that refetch is necessary.
 */

'use strict';

load("jstests/replsets/libs/rollback_test.js");
load("jstests/libs/collection_drop_recreate.js");

/**
 * Executes and validates rollback between a pair of nodes with the given versions.
 *
 * @param {string} testName the name of the test being run
 * @param {string} rollbackNodeVersion the desired version for the rollback node
 * @param {string} syncSourceVersion the desired version for the sync source
 *
 */
function testMultiversionRollback(testName, rollbackNodeVersion, syncSourceVersion) {
    jsTestLog("Started multiversion rollback test for versions: {rollbackNode: " +
              rollbackNodeVersion + ", syncSource: " + syncSourceVersion + "}.");

    let dbName = testName;

    let CommonOps = (node) => {
        // Insert four documents on both nodes.
        assert.commandWorked(node.getDB(dbName)["bothNodesKeep"].insert({a: 1}));
        assert.commandWorked(node.getDB(dbName)["rollbackNodeDeletes"].insert({b: 1}));
        assert.commandWorked(node.getDB(dbName)["rollbackNodeUpdates"].insert({c: 1}));
        assert.commandWorked(node.getDB(dbName)["bothNodesUpdate"].insert({d: 1}));
    };

    let RollbackOps = (node) => {
        // Perform operations only on the rollback node:
        //   1. Delete a document.
        //   2. Update a document only on this node.
        //   3. Update a document on both nodes.
        // All three documents will be refetched during rollback.
        assert.commandWorked(node.getDB(dbName)["rollbackNodeDeletes"].remove({b: 1}));
        assert.commandWorked(node.getDB(dbName)["rollbackNodeUpdates"].update({c: 1}, {c: 0}));
        assert.commandWorked(node.getDB(dbName)["bothNodesUpdate"].update({d: 1}, {d: 0}));
    };

    let SyncSourceOps = (node) => {
        // Perform operations only on the sync source:
        //   1. Make a conflicting write on one of the documents the rollback node updates.
        //   2. Insert a new document.
        assert.commandWorked(node.getDB(dbName)["bothNodesUpdate"].update({d: 1}, {d: 2}));
        assert.commandWorked(node.getDB(dbName)["syncSourceInserts"].insert({e: 1}));
    };

    // Set up replica set.
    let replSet = setupReplicaSet(testName, rollbackNodeVersion, syncSourceVersion);

    // Set up Rollback Test.
    let rollbackTest = new RollbackTest(testName, replSet);

    CommonOps(rollbackTest.getPrimary());

    // Perform operations that will be rolled back.
    let rollbackNode = rollbackTest.transitionToRollbackOperations();
    RollbackOps(rollbackNode);

    // Perform different operations only on the sync source.
    let syncSource = rollbackTest.transitionToSyncSourceOperationsBeforeRollback();
    SyncSourceOps(syncSource);

    // Wait for rollback to finish.
    rollbackTest.transitionToSyncSourceOperationsDuringRollback();
    rollbackTest.transitionToSteadyStateOperations();

    rollbackTest.stop();
}

/**
 * Sets up a multiversion replica set.
 *
 * Note that, regardless of which node in the rollbackNode-syncSource pair requires
 * which version, there is only one possible way to start up such a cluster:
 *
 * 1. Start up the first two nodes with the higher of the two versions.
 * 2. Set the FCV to the lower version in order to be able to include the third node.
 * 3. Bring up the third node and add it to the set, with the lower binary version.
 * 4. This always results in a higher-version primary and a lower-version secondary,
 *    so if a test case specifies the lower version on the rollback node (i.e. the
 *    opposite setup), this function will force the current primary and secondary
 *    to switch roles.
 *
 * This function returns a replica set with the intended rollback node as the primary.
 *
 * @param {string} testName the name of the test being run
 * @param {string} rollbackNodeVersion the desired version for the rollback node
 * @param {string} syncSourceVersion the desired version for the sync source
 */
function setupReplicaSet(testName, rollbackNodeVersion, syncSourceVersion) {
    jsTestLog(
        `[${testName}] Beginning cluster setup with versions: {rollbackNode: ${rollbackNodeVersion},
            syncSource: ${syncSourceVersion}}.`);

    let sortedVersions =
        [rollbackNodeVersion, syncSourceVersion].sort(MongoRunner.compareBinVersions);
    let lowerVersion = MongoRunner.getBinVersionFor(sortedVersions[0]);
    let higherVersion = MongoRunner.getBinVersionFor(sortedVersions[1]);

    jsTestLog(`[${testName}] Starting up first two nodes with version: ${higherVersion}`);
    var initialNodes = {n1: {binVersion: higherVersion}, n2: {binVersion: higherVersion}};

    // Start up a two-node cluster first. This cluster contains two data bearing nodes, but the
    // second node will be priority: 0 to ensure that it will never become primary. This, in
    // addition to stopping/restarting server replication should make the node exhibit similar
    // behavior to an arbiter.
    var rst = new ReplSetTest(
        {name: testName, nodes: initialNodes, useBridge: true, settings: {chainingAllowed: false}});
    rst.startSet();
    rst.initiateWithHighElectionTimeout();

    // Wait for both nodes to be up.
    waitForState(rst.nodes[0], ReplSetTest.State.PRIMARY);
    waitForState(rst.nodes[1], ReplSetTest.State.SECONDARY);

    const initialPrimary = rst.getPrimary();

    // Set FCV to accommodate third node.
    jsTestLog(
        `[${testName} - ${initialPrimary.host}] Setting FCV to ${lowerVersion} on the primary.`);
    assert.commandWorked(
        initialPrimary.adminCommand({setFeatureCompatibilityVersion: lowerVersion}));

    jsTestLog(`[${testName}] Bringing up third node with version ${lowerVersion}`);
    rst.add({binVersion: lowerVersion});
    rst.reInitiate();

    let config = rst.getReplSetConfigFromNode();
    config.members[1].priority = 0;
    reconfig(rst, config, true);

    jsTestLog(
        `[${testName} - ${rst.nodes[2].host}] Waiting for the newest node to become a secondary.`);
    rst.awaitSecondaryNodes();

    let primary = rst.nodes[0];
    let secondary = rst.nodes[2];
    let tiebreakerNode = rst.nodes[1];

    // Make sure we still have the right node as the primary.
    assert.eq(rst.getPrimary(), primary);

    // Also make sure the other two nodes are in their expected states.
    assert.eq(ReplSetTest.State.SECONDARY,
              tiebreakerNode.adminCommand({replSetGetStatus: true}).myState);
    assert.eq(ReplSetTest.State.SECONDARY,
              secondary.adminCommand({replSetGetStatus: true}).myState);

    jsTestLog(`[${testName}] Cluster now running with versions: {primary: ${higherVersion},
            secondary: ${lowerVersion}, tiebreakerNode: ${higherVersion}}.`);

    // Some test cases require that the primary (future rollback node) is running the lower
    // version, which at this point is always on the secondary, so we elect that node instead.
    if (rollbackNodeVersion === lowerVersion) {
        jsTestLog(
            `[${testName}] Test case requires opposite versions for primary and secondary. Swapping roles.`);

        // Force the current secondary to become the primary.
        rst.stepUp(secondary);

        let newPrimary = secondary;
        secondary = primary;
        primary = newPrimary;

        jsTestLog(`[${testName}] Cluster now running with versions: {primary: ${lowerVersion},
            secondary: ${higherVersion}, tiebreakerNode: ${higherVersion}}.`);
    }

    return rst;
}
