/**
 * Tests the behavior of the createIndex and dropIndex events without various command parameters.
 *
 * @tags: [
 *   requires_fcv_60,
 *   assumes_unsharded_collection,
 *   assumes_against_mongod_not_mongos,
 * ]
 */
import {ChangeStreamTest} from "jstests/libs/query/change_stream_util.js";

const testDB = db.getSiblingDB(jsTestName());

const dbName = testDB.getName();
const collName = jsTestName();
const ns = {
    db: dbName,
    coll: collName
};

const cst = new ChangeStreamTest(testDB);

function assertNextChangeEvent(cursor, expectedEvent) {
    cst.assertNextChangesEqual({cursor: cursor, expectedChanges: expectedEvent});
}

function runTest(startChangeStream, pipeline, insertDataBeforeCreateIndex) {
    const cst = new ChangeStreamTest(testDB);

    assert.commandWorked(testDB.runCommand({create: collName}));
    if (insertDataBeforeCreateIndex) {
        testDB[collName].insert({a: 1, b: "j", c: "k", d: "l", e: "m", f: [55.5, 42.3]});
    }

    function testCreateIndexAndDropIndex(key, options, opDescKey = key, dropKey) {
        let name = "";
        if (options.hasOwnProperty("name")) {
            // If the "name" option was explicitly specified, use that
            name = options.name;
            dropKey = name;
        } else {
            // Otherwise, determine what "name" should be based on "key"
            let arr = [];
            for (let property in key) {
                arr.push(property);
                arr.push(key[property]);
            }
            name = arr.join("_");
        }

        if (dropKey == undefined) {
            dropKey = name;
        }

        let cursor = startChangeStream();
        let opDesc = {indexes: [Object.assign({v: 2, key: opDescKey, name: name}, options)]};

        assert.commandWorked(testDB[collName].createIndex(key, options));
        if (insertDataBeforeCreateIndex && pipeline[0].$changeStream.showSystemEvents) {
            assertNextChangeEvent(
                cursor, {operationType: "startIndexBuild", ns: ns, operationDescription: opDesc});
        }
        assertNextChangeEvent(
            cursor, {operationType: "createIndexes", ns: ns, operationDescription: opDesc});

        assert.commandWorked(testDB[collName].dropIndexes([dropKey]));
        assertNextChangeEvent(cursor,
                              {operationType: "dropIndexes", ns: ns, operationDescription: opDesc});
    }

    // Test createIndex() with various option followed by dropIndexes("*").
    let options = {
        hidden: true,
        partialFilterExpression: {a: {$gte: 0}},
        expireAfterSeconds: 86400,
        storageEngine: {wiredTiger: {}}
    };
    testCreateIndexAndDropIndex({a: 1}, options);

    // Test createIndex() with a non-simple collation followed by dropIndex(). We include all
    // fields in the collation spec so that we don't rely on any default settings.
    options = {
        collation: {
            locale: "en_US",
            caseLevel: false,
            caseFirst: "off",
            strength: 3,
            numericOrdering: false,
            alternate: "non-ignorable",
            maxVariable: "punct",
            normalization: false,
            backwards: false,
            version: "57.1"
        }
    };
    testCreateIndexAndDropIndex({e: 1}, options, {e: 1}, "*");

    // Test createIndex() for a wildcard index on all fields with the wildcardProjection option,
    // followed by dropIndex().
    options = {name: "wi", wildcardProjection: {a: true, b: true, _id: false}};
    testCreateIndexAndDropIndex({"$**": 1}, options);

    // Test createIndex() for a text index with various options, followed by dropIndex().
    options = {
        name: "text",
        weights: {e: 1},
        default_language: "english",
        language_override: "language",
        textIndexVersion: 3
    };
    testCreateIndexAndDropIndex({e: "text"}, options, {_fts: "text", _ftsx: 1});

    // Test createIndex() for a 2d index with various options, followed by dropIndex().
    options = {name: "2d", min: -150.0, max: 150.0, bits: 26};
    testCreateIndexAndDropIndex({f: "2d"}, options);

    // Test createIndex() for a 2dsphere index with various options, followed by dropIndex().
    options = {name: "2dsphere", "2dsphereIndexVersion": 3};
    testCreateIndexAndDropIndex({f: "2dsphere"}, options);

    // Test createIndexes() to create two sparse indexes (with one index being a compound index),
    // followed by dropIndexes().
    let cursor = startChangeStream();
    let opDesc1 = {indexes: [{v: 2, key: {b: 1, c: -1}, name: "b_1_c_-1", sparse: true}]};
    let opDesc2 = {indexes: [{v: 2, key: {d: "hashed"}, name: "d_hashed", sparse: true}]};
    if (!insertDataBeforeCreateIndex) {
        // If the collection was empty before calling createIndexes(), then there will be a separate
        // change stream event for each index.
        assert.commandWorked(
            testDB[collName].createIndexes([{b: 1, c: -1}, {d: "hashed"}], {sparse: true}));
        cst.assertNextChangesEqualUnordered({
            cursor: cursor,
            expectedChanges: [
                {operationType: "createIndexes", ns: ns, operationDescription: opDesc1},
                {operationType: "createIndexes", ns: ns, operationDescription: opDesc2}
            ]
        });
    } else {
        // If the collection was not empty before calling createIndexes(), then there will be a
        // single change stream event that covers both indexes.
        assert.commandWorked(
            testDB[collName].createIndexes([{b: 1, c: -1}, {d: "hashed"}], {sparse: true}));
        if (pipeline[0].$changeStream.showSystemEvents) {
            assertNextChangeEvent(cursor, {
                operationType: "startIndexBuild",
                ns: ns,
                operationDescription: {indexes: [opDesc1.indexes[0], opDesc2.indexes[0]]},
            });
        }
        assertNextChangeEvent(cursor, {
            operationType: "createIndexes",
            ns: ns,
            operationDescription: {indexes: [opDesc1.indexes[0], opDesc2.indexes[0]]}
        });
    }
    assert.commandWorked(testDB[collName].dropIndexes(["b_1_c_-1", "d_hashed"]));
    cst.assertNextChangesEqualUnordered({
        cursor: cursor,
        expectedChanges: [
            {operationType: "dropIndexes", ns: ns, operationDescription: opDesc1},
            {operationType: "dropIndexes", ns: ns, operationDescription: opDesc2}
        ]
    });

    testDB[collName].drop();
}

const runTests = function(pipeline) {
    // Run the test using a whole-db change stream on an empty collection.
    runTest((() => cst.startWatchingChanges({pipeline, collection: 1})), pipeline, false);

    // Run the test using a single change stream on an empty collection.
    runTest((() => cst.startWatchingChanges({pipeline, collection: collName})), pipeline, false);

    // Run the test using a whole-db collection change stream on a non-empty collection.
    runTest((() => cst.startWatchingChanges({pipeline, collection: 1})), pipeline, true);

    // Run the test using a single collection change stream on a non-empty collection.
    runTest((() => cst.startWatchingChanges({pipeline, collection: collName})), pipeline, true);
};
runTests([{$changeStream: {showExpandedEvents: true}}]);
runTests([{$changeStream: {showExpandedEvents: true, showSystemEvents: true}}]);
