/**
 * Tests to verify that hashed indexes obey collation rules.
 *
 * @tags: [
 *   assumes_unsharded_collection,
 * ]
 */
import {arrayEq} from "jstests/aggregation/extras/utils.js";
import {assertStagesForExplainOfCommand} from "jstests/libs/query/analyze_plan.js";

const coll = db.hashed_index_collation;
coll.drop();
const collation = {
    locale: "en_US",
    strength: 1
};

/**
 * Runs find command with the 'filter' and 'projection' provided in the input, then validates
 * that the output returned matches 'expectedOutput'. Also runs explain() command on the same find
 * command, validates that all the 'expectedStages' are present in the plan returned and all the
 * 'stagesNotExpected' are not present in the plan.
 */
function validateFindCmdOutputAndPlan(
    {filter, projection = {}, expectedOutput, expectedStages, stagesNotExpected}) {
    const cmdObj =
        {find: coll.getName(), filter: filter, projection: projection, collation: collation};
    const res = assert.commandWorked(coll.runCommand(cmdObj));
    const ouputArray = new DBCommandCursor(coll.getDB(), res).toArray();

    // We ignore the order since hashed index order is not predictable.
    assert(arrayEq(expectedOutput, ouputArray), ouputArray);

    assertStagesForExplainOfCommand({
        coll: coll,
        cmdObj: cmdObj,
        expectedStages: expectedStages,
        stagesNotExpected: stagesNotExpected
    });
}

// Verify that index creation works for compound hashed index with collation, when hashed field is a
// prefix.
assert.commandWorked(
    coll.createIndex({"a.b": "hashed", "a.c": 1, "a.e": -1}, {collation: collation}));

// Insert a series of documents whose fieldnames and values differ only by case.
assert.commandWorked(coll.insert({_id: 0, a: {b: "string", c: "STRING", e: 5}}));
assert.commandWorked(coll.insert({_id: 1, a: {b: "STRING", c: "string", e: 5}}));
assert.commandWorked(coll.insert({_id: 2, A: {B: "string", C: "STRING", E: 5}}));
assert.commandWorked(coll.insert({_id: 3, A: {B: "StrinG", C: "sTRINg", E: 5}}));

// Verify that hashed field can use index in the presence of collation. Also verify that only
// the document's values, not the field names, adhere to the case-insensitive collation.
validateFindCmdOutputAndPlan({
    filter: {"a.b": "string", "a.c": "string"},
    expectedStages: ["IXSCAN", "FETCH"],
    expectedOutput: [
        {_id: 0, a: {b: "string", c: "STRING", e: 5}},
        {_id: 1, a: {b: "STRING", c: "string", e: 5}}
    ]
});

// Verify that the field names doesn't adhere to the case-insensitive collation and uses collection
// scan if the case doesn't match.
validateFindCmdOutputAndPlan({
    filter: {"A.B": "string"},
    expectedStages: ["COLLSCAN"],
    expectedOutput: [
        {_id: 2, A: {B: "string", C: "STRING", E: 5}},
        {_id: 3, A: {B: "StrinG", C: "sTRINg", E: 5}}
    ]
});

// Verify that $or query with collation returns correct results.
validateFindCmdOutputAndPlan({
    filter: {$or: [{"a.b": "string_1"}, {"a.b": "string", "a.c": "string"}]},
    expectedStages: ["OR"],
    stagesNotExpected: ["COLLSCAN"],  // Verify that both the OR stages use index scan.
    expectedOutput: [
        {_id: 0, a: {b: "string", c: "STRING", e: 5}},
        {_id: 1, a: {b: "STRING", c: "string", e: 5}}
    ]
});

/**
 * When hashed field is not a prefix.
 */
assert.commandWorked(coll.dropIndexes());
assert.commandWorked(
    coll.createIndex({"a.b": 1, "a.c": "hashed", "a.e": -1}, {collation: collation}));

// Hashed indexes with collation can be covered, if the query predicate restrict strings from being
// returned.
validateFindCmdOutputAndPlan({
    filter: {"a.b": "string", "a.e": {$type: "number"}},
    projection: {"a.e": 1, _id: 0},
    expectedStages: ["IXSCAN"],
    stagesNotExpected: ["FETCH"],
    expectedOutput: [{a: {e: 5}}, {a: {e: 5}}]
});

// Hashed indexes with collation cannot be covered, if the query predicate doesn't restrict strings
// from being returneds.
validateFindCmdOutputAndPlan({
    filter: {"a.b": "string"},
    projection: {"a.e": 1, _id: 0},
    expectedStages: ["IXSCAN", "FETCH"],
    expectedOutput: [{a: {e: 5}}, {a: {e: 5}}]
});
