/**
 * Test expected behavior for hidden indexes. A hidden index is invisible to the query planner so
 * it will not be used in planning. It is handled in the same way as other indexes by the index
 * catalog and for TTL purposes.
 * @tags: [
 *   # CollMod is not retryable.
 *   requires_non_retryable_commands,
 *   not_allowed_with_signed_security_token,
 *   requires_capped,
 * ]
 */

import {assertDropAndRecreateCollection} from "jstests/libs/collection_drop_recreate.js";
import {FixtureHelpers} from "jstests/libs/fixture_helpers.js";
import {IndexCatalogHelpers} from "jstests/libs/index_catalog_helpers.js";
import {getNumberOfIndexScans} from "jstests/libs/query/analyze_plan.js";

const collName = "hidden_index";
let coll = assertDropAndRecreateCollection(db, collName);

function numOfUsedIndexes(explain) {
    return getNumberOfIndexScans(explain);
}

function validateHiddenIndexBehaviour(
    {query = {}, projection = {}, index_type = 1, wildcard = false}) {
    let index_name;
    if (wildcard)
        index_name = "a.$**_" + index_type;
    else
        index_name = "a_" + index_type;

    if (wildcard)
        assert.commandWorked(coll.createIndex({"a.$**": index_type}));
    else
        assert.commandWorked(coll.createIndex({"a": index_type}));

    let idxSpec = IndexCatalogHelpers.findByName(coll.getIndexes(), index_name);
    assert.eq(idxSpec.hidden, undefined);

    // Assert the plan is using an index scan.
    let explain = assert.commandWorked(coll.find(query, projection).explain());
    assert.gt(numOfUsedIndexes(explain), 0);

    assert.commandWorked(coll.hideIndex(index_name));
    idxSpec = IndexCatalogHelpers.findByName(coll.getIndexes(), index_name);
    assert(idxSpec.hidden);
    if (index_type === "text") {
        assert.commandFailedWithCode(coll.runCommand("find", {filter: query}, {hint: {a: 1}}), 291);
        assert.commandWorked(coll.dropIndexes());
        return;
    }

    // Assert the plan is not using an index scan.
    explain = assert.commandWorked(coll.find(query, projection).explain());
    assert.eq(numOfUsedIndexes(explain), 0);

    assert.commandWorked(coll.unhideIndex(index_name));
    idxSpec = IndexCatalogHelpers.findByName(coll.getIndexes(), index_name);
    assert.eq(idxSpec.hidden, undefined);

    // Assert the plan is using an index scan.
    explain = assert.commandWorked(coll.find(query, projection).explain());
    assert.gt(numOfUsedIndexes(explain), 0);

    assert.commandWorked(coll.dropIndex(index_name));

    if (wildcard)
        assert.commandWorked(coll.createIndex({"a.$**": index_type}, {hidden: true}));
    else
        assert.commandWorked(coll.createIndex({"a": index_type}, {hidden: true}));

    idxSpec = IndexCatalogHelpers.findByName(coll.getIndexes(), index_name);
    assert(idxSpec.hidden);
    explain = assert.commandWorked(coll.find(query, projection).explain());
    assert.eq(numOfUsedIndexes(explain), 0);

    if (wildcard)
        assert.commandFailedWithCode(coll.createIndex({"a.$**": index_type}, {hidden: false}),
                                     ErrorCodes.IndexOptionsConflict);
    else
        assert.commandFailedWithCode(coll.createIndex({"a": index_type}, {hidden: false}),
                                     ErrorCodes.IndexOptionsConflict);

    idxSpec = IndexCatalogHelpers.findByName(coll.getIndexes(), index_name);
    assert(idxSpec.hidden);

    assert.commandWorked(coll.dropIndexes());
}

// Normal index testing.
validateHiddenIndexBehaviour({query: {a: 1}, index_type: 1});

// GEO index testing.
validateHiddenIndexBehaviour({
    query: {
        a: {
            $geoWithin:
                {$geometry: {type: "Polygon", coordinates: [[[0, 0], [3, 6], [6, 1], [0, 0]]]}}
        }
    },
    index_type: "2dsphere"
});

// Fts index.
validateHiddenIndexBehaviour({query: {$text: {$search: "java"}}, index_type: "text"});

// Wildcard index.
validateHiddenIndexBehaviour({query: {"a.f": 1}, index_type: 1, wildcard: true});

// Hidden index on capped collection.
if (!FixtureHelpers.isMongos(db) && !TestData.testingReplicaSetEndpoint) {
    coll = assertDropAndRecreateCollection(db, collName, {capped: true, size: 100});
    validateHiddenIndexBehaviour({query: {a: 1}, index_type: 1});
    coll = assertDropAndRecreateCollection(db, collName);
}
// Test that index 'hidden' status can be found in listIndexes command.
assert.commandWorked(coll.createIndex({lsIdx: 1}, {hidden: true}));
let res = assert.commandWorked(db.runCommand({"listIndexes": collName}));
let idxSpec = IndexCatalogHelpers.findByName(res.cursor.firstBatch, "lsIdx_1");
assert.eq(idxSpec.hidden, true);

// Can't hide any index in a system collection.
const systemColl = db.getSiblingDB("admin").system.version;
assert.commandWorked(systemColl.createIndex({a: 1}));
// The collMod command throws ShardingStateNotInitialized on DDL coordinator implementation and
// BadValue on old implementation.
assert.commandFailedWithCode(systemColl.hideIndex("a_1"), [
    ErrorCodes.ShardingStateNotInitialized,
    ErrorCodes.BadValue,
]);
assert.commandFailedWithCode(systemColl.createIndex({a: 1}, {hidden: true}), 2);

// Can't hide the '_id' index.
assert.commandFailed(coll.hideIndex("_id_"));

// Can't 'hint' a hidden index.
assert.commandWorked(coll.createIndex({"a": 1}, {"hidden": true}));
assert.commandFailedWithCode(coll.runCommand("find", {hint: {a: 1}}), 2);

// We can change ttl index and hide info at the same time.
assert.commandWorked(coll.dropIndexes());
assert.commandWorked(coll.createIndex({"tm": 1}, {expireAfterSeconds: 10}));
idxSpec = IndexCatalogHelpers.findByName(coll.getIndexes(), "tm_1");
assert.eq(idxSpec.hidden, undefined);
assert.eq(idxSpec.expireAfterSeconds, 10);

db.runCommand({
    "collMod": coll.getName(),
    "index": {"name": "tm_1", "expireAfterSeconds": 1, "hidden": true}
});
idxSpec = IndexCatalogHelpers.findByName(coll.getIndexes(), "tm_1");
assert(idxSpec.hidden);
assert.eq(idxSpec.expireAfterSeconds, 1);

//
// Ensure that "hidden: false" won't be added to index specification.
//
assert.commandWorked(
    db.runCommand({createIndexes: collName, indexes: [{key: {y: 1}, name: "y", hidden: false}]}));
idxSpec = IndexCatalogHelpers.findByName(coll.getIndexes(), "y");
assert.eq(idxSpec.hidden, undefined);

assert.commandWorked(coll.hideIndex("y"));
idxSpec = IndexCatalogHelpers.findByName(coll.getIndexes(), "y");
assert(idxSpec.hidden);

// Ensure that unhiding the hidden index won't add 'hidden: false' to the index spec as well.
assert.commandWorked(coll.unhideIndex("y"));
idxSpec = IndexCatalogHelpers.findByName(coll.getIndexes(), "y");
assert.eq(idxSpec.hidden, undefined);
