/**
 * Verifies that haystack indexes cannot be created on 4.9+ binaries, but are maintained
 * correctly in mixed version clusters.
 *
 * TODO SERVER-51871: This test can be deleted once 5.0 becomes last-lts.
 */
(function() {
"use strict";
load("jstests/multiVersion/libs/multi_rs.js");      // For 'upgradeSecondaries()/upgradePrimary()'
load('jstests/noPassthrough/libs/index_build.js');  // For 'assertIndexes()'
load("jstests/libs/fixture_helpers.js");            // For 'isSharded()'

const dbName = "test";
const collName = jsTestName();
const geoIndexKey = {
    loc: "geoHaystack",
    x: 1
};
const geoIndexName = "geo";
const indexList = ["_id_", geoIndexName];
const nonGeoIndexList = ["_id_"];

function insertInitialDocuments(coll) {
    const documentList =
        [{_id: 0, loc: [1, 2], x: 'foo', y: 2}, {_id: 1, loc: [1.5, 1.5], x: 'bar', y: 1}];
    assert.commandWorked(coll.insert(documentList));
}

/**
 * Calls 'validate()' on 'coll' and verifies that the documents which are inserted into 'coll'
 * produce the correct number of index keys for the geoHaystack index in this test.
 */
function validateAndAssertCorrectIndexKeys(coll) {
    const validateResult = assert.commandWorked(coll.validate({full: true}));
    let validateOutput;
    if (FixtureHelpers.isSharded(coll)) {
        assert(validateResult.hasOwnProperty("raw"));
        validateOutput = validateResult["raw"];
    } else {
        validateOutput = validateResult;
    }

    // There should be as many index keys as there are documents.
    const expectedNumKeys = coll.find().itcount();
    let keys = 0;
    if (FixtureHelpers.isSharded(coll)) {
        for (const shard of Object.keys(validateOutput)) {
            keys += validateOutput[shard]["keysPerIndex"][geoIndexName];
            assert.eq(0, validateOutput[shard]["errors"].length);
            assert.eq(true, validateOutput[shard]["valid"]);
        }
    } else {
        keys = validateOutput["keysPerIndex"][geoIndexName];
        assert.eq(0, validateOutput["errors"].length);
        assert.eq(true, validateOutput["valid"]);
    }

    assert.eq(expectedNumKeys, keys);
}

// Verify that haystack indexes cannot be created on a standalone in the latest version regardless
// of the FCV.
function runStandaloneTest() {
    const mongo = MongoRunner.runMongod({binVersion: "latest"});
    const testDB = mongo.getDB(dbName);
    const coll = testDB[collName];
    for (const fcv of [lastLTSFCV, latestFCV]) {
        assert.commandWorked(mongo.adminCommand({setFeatureCompatibilityVersion: fcv}));
        assert.commandFailedWithCode(
            coll.createIndex(geoIndexKey, {name: geoIndexName, bucketSize: 1}),
            ErrorCodes.CannotCreateIndex);
    }
    MongoRunner.stopMongod(mongo);
}

// Verify that haystack indexes are maintained properly on a mixed version replica set.
function runReplicaSetTest() {
    // Set up a mixed version replica-set: both nodes will be initialized to the last-lts
    // binary version (4.4), but the secondary will be initialized to the latest binary version.
    const rst = new ReplSetTest({nodes: [{binVersion: "last-lts"}, {binVersion: "latest"}]});
    rst.startSet();
    rst.initiate();
    let primaryDB = rst.getPrimary().getDB(dbName);
    let primaryColl = primaryDB[collName];
    insertInitialDocuments(primaryColl);
    rst.awaitReplication();

    // Creating a haystack index should still work.
    assert.commandWorked(primaryDB.runCommand({
        "createIndexes": collName,
        indexes: [{key: geoIndexKey, name: geoIndexName, bucketSize: 1}],
        writeConcern: {w: 2}
    }));

    // The haystack index should replicate correctly to the secondary.
    const secondaryDB = rst.getSecondary().getDB(dbName);
    const secondaryColl = secondaryDB[collName];
    IndexBuildTest.assertIndexes(secondaryColl, indexList.length, indexList);

    // Verify that documents which are inserted after the index is built produce valid index keys.
    assert.commandWorked(
        primaryColl.insert([{_id: 4, loc: [4, 4], x: "baz"}, {_id: 5, loc: [5, 5], x: "baz"}],
                           {writeConcern: {w: 2}}));
    validateAndAssertCorrectIndexKeys(primaryColl);

    // Upgrade the primary and attempt to re-create the index after the upgrade.
    assert.commandWorked(
        primaryDB.runCommand({"dropIndexes": collName, index: geoIndexName, writeConcern: {w: 2}}));
    rst.upgradePrimary(rst.getPrimary(), {binVersion: "latest"});
    rst.awaitNodesAgreeOnPrimary();

    // Even though we haven't bumped the FCV, index creation should still fail on a primary in
    // the latest version.
    primaryDB = rst.getPrimary().getDB(dbName);
    primaryColl = primaryDB[collName];
    assert.commandFailedWithCode(
        primaryColl.createIndex(geoIndexKey, {name: geoIndexName, bucketSize: 1}),
        ErrorCodes.CannotCreateIndex);

    rst.stopSet();
}

// Verify that haystack indexes are maintained properly in a mixed version sharded cluster.
function runShardingTest() {
    // Set up a mixed version sharded cluster, where shard0's nodes are initialized to the last-lts
    // binary version (4.4) and shard1's nodes are initialized to the latest binary version.
    const st = new ShardingTest({
        shards: {
            rs0: {nodes: [{binVersion: "last-lts"}, {binVersion: "last-lts"}]},
            rs1: {nodes: [{binVersion: "latest"}, {binVersion: "latest"}]}
        },
        other: {mongosOptions: {binVersion: "last-lts"}}
    });

    // Test that indexes are maintained properly during chunk migration. More precisely, verify
    // that when a chunk from a shard consisting of 4.4 nodes with a haystack index is moved to a
    // shard consisting of nodes in the latest binary version, the haystack index is built
    // correctly on the shard in the latest binary version.
    const mongos = st.s;
    const ns = dbName + "." + collName;

    // Create a sharded collection with two chunks: [MinKey, 1), [1, MaxKey], both on shard0.
    assert.commandWorked(mongos.adminCommand({enableSharding: dbName}));
    assert.commandWorked(mongos.adminCommand({movePrimary: dbName, to: st.shard0.shardName}));
    assert.commandWorked(mongos.adminCommand({shardCollection: ns, key: {_id: 1}}));
    assert.commandWorked(mongos.adminCommand({split: ns, middle: {_id: 1}}));

    // Insert some documents and create a haystack index.
    const db = mongos.getDB(dbName);
    const coll = db[collName];
    insertInitialDocuments(coll);
    assert.commandWorked(coll.createIndex(geoIndexKey, {name: geoIndexName, bucketSize: 1}));

    // Wait for shard0 to finish replicating its documents and building the index.
    st.rs0.awaitReplication();
    validateAndAssertCorrectIndexKeys(coll);

    // Move the [1, MaxKey] chunk to shard1.
    assert.commandWorked(mongos.adminCommand(
        {moveChunk: ns, find: {_id: 1}, to: st.shard1.shardName, _waitForDelete: true}));
    st.rs1.awaitReplication();

    // Verify that shard1 has the haystack index after the chunk was moved.
    const shard1primary = st.rs1.getPrimary();
    const shard1DB = shard1primary.getDB(dbName);
    const shard1Coll = shard1DB[collName];
    IndexBuildTest.assertIndexes(shard1Coll, indexList.length, indexList);

    // Verify that inserting documents into a shard consisting of nodes in the latest version with
    // an existing haystack index will create the correct index keys for the index.
    assert.commandWorked(
        coll.insert([{_id: 4, loc: [4, 4], x: "baz"}, {_id: 5, loc: [5, 5], x: "blah"}],
                    {writeConcern: {w: 2}}));

    // Creating a new haystack index against a sharded cluster with at least one shard upgraded to
    // the latest version should fail.
    assert.commandWorked(
        db.runCommand({"dropIndexes": collName, index: geoIndexName, writeConcern: {w: 2}}));
    assert.commandFailedWithCode(coll.createIndex(geoIndexKey, {name: geoIndexName, bucketSize: 1}),
                                 ErrorCodes.CannotCreateIndex);

    // Though the command failed, the haystack index will still be created on shard0 since it is in
    // version 4.4.
    const shard0DB = st.rs0.getPrimary().getDB(dbName);
    const shard0coll = shard0DB[collName];
    IndexBuildTest.assertIndexes(shard0coll, indexList.length, indexList);

    // Since shard1 is in the latest version, it will not have the geoHaystack index.
    IndexBuildTest.assertIndexes(shard1Coll, nonGeoIndexList.length, nonGeoIndexList);

    // Though the 'dropIndexes' command will fail because shard1 does not have the haystack
    // index, it should still remove the haystack index on shard0.
    assert.commandFailedWithCode(
        mongos.getDB(dbName).runCommand(
            {"dropIndexes": collName, index: geoIndexName, writeConcern: {w: 2}}),
        ErrorCodes.IndexNotFound);
    IndexBuildTest.assertIndexes(shard0coll, nonGeoIndexList.length, nonGeoIndexList);
    st.stop();
}

runStandaloneTest();
runReplicaSetTest();
runShardingTest();
})();