/**
 * Tests passing hint to the update command:
 *   - A bad argument to the hint option should raise an error.
 *   - The hint option should support both the name of the index, and the object spec of the
 *     index.
 *
 * @tags: [assumes_unsharded_collection, requires_multi_updates, requires_non_retryable_writes]
 */

import {getPlanStage} from "jstests/libs/query/analyze_plan.js";

function assertCommandUsesIndex(command, expectedHintKeyPattern) {
    const out = assert.commandWorked(coll.runCommand({explain: command}));
    const planStage = getPlanStage(out, "IXSCAN");
    assert.neq(null, planStage);
    assert.eq(planStage.keyPattern, expectedHintKeyPattern, tojson(planStage));
}

const coll = db.jstests_update_hint;

function normalIndexTest() {
    // Hint using a key pattern.
    coll.drop();
    assert.commandWorked(coll.insert({x: 1, y: 1}));
    assert.commandWorked(coll.createIndex({x: 1}));
    assert.commandWorked(coll.createIndex({y: -1}));

    // Hint using index key pattern.
    let updateCmd = {
        update: coll.getName(),
        updates: [{q: {x: 1}, u: {$set: {y: 1}}, hint: {x: 1}}]
    };
    assertCommandUsesIndex(updateCmd, {x: 1});

    // Hint using an index name.
    updateCmd = {update: coll.getName(), updates: [{q: {x: 1}, u: {$set: {y: 1}}, hint: 'y_-1'}]};
    assertCommandUsesIndex(updateCmd, {y: -1});

    // Passing a hint should not use the idhack fast-path.
    updateCmd = {update: coll.getName(), updates: [{q: {_id: 1}, u: {$set: {y: 1}}, hint: 'y_-1'}]};
    assertCommandUsesIndex(updateCmd, {y: -1});
}

function sparseIndexTest() {
    // Create a sparse index with 2 documents.
    coll.drop();
    assert.commandWorked(coll.insert([{x: 1}, {x: 1}, {x: 1, s: 0}, {x: 1, s: 0}]));
    assert.commandWorked(coll.createIndex({x: 1}));
    assert.commandWorked(coll.createIndex({s: 1}, {sparse: true}));

    // Hint should be respected, even on incomplete indexes.
    let updateCmd = {
        update: coll.getName(),
        updates: [{q: {_id: 1}, u: {$set: {y: 1}}, hint: {s: 1}}]
    };
    assertCommandUsesIndex(updateCmd, {s: 1});

    // Update hinting a sparse index updates only the document in the sparse index.
    updateCmd = {
        update: coll.getName(),
        updates: [{q: {}, u: {$set: {s: 1}}, hint: {s: 1}, multi: true}]
    };
    assert.commandWorked(coll.runCommand(updateCmd));
    assert.eq(2, coll.count({s: 1}));

    // Update hinting a sparse index with upsert option can result in an insert even if the
    // correct behaviour would be to update an existing document.
    assert.commandWorked(coll.insert({x: 2}));
    updateCmd = {
        update: coll.getName(),
        updates: [{q: {x: 2}, u: {$set: {x: 1}}, hint: {s: 1}, upsert: true}]
    };
    let res = assert.commandWorked(coll.runCommand(updateCmd));
    assert.eq(res.upserted.length, 1);
}

function shellHelpersTest() {
    coll.drop();
    assert.commandWorked(coll.insert([{x: 1}, {x: 1, s: 0}, {x: 1, s: 0}]));
    assert.commandWorked(coll.createIndex({x: 1}));
    assert.commandWorked(coll.createIndex({s: 1}, {sparse: true}));

    // Test shell helpers using a hinted sparse index should only update documents that exist in
    // the sparse index.
    let res = coll.update({x: 1}, {$set: {y: 2}}, {hint: {s: 1}, multi: true});
    assert.eq(res.nMatched, 2);

    // Insert document that will not be in the sparse index. Update hinting sparse index should
    // result in upsert.
    assert.commandWorked(coll.insert({x: 2}));
    res = coll.updateOne({x: 2}, {$set: {y: 2}}, {hint: {s: 1}, upsert: true});
    assert(res.upsertedId);
    res = coll.updateMany({x: 1}, {$set: {y: 2}}, {hint: {s: 1}});
    assert.eq(res.matchedCount, 2);

    // Test bulk writes.
    let bulk = coll.initializeUnorderedBulkOp();
    bulk.find({x: 1}).hint({s: 1}).update({$set: {y: 1}});
    res = bulk.execute();
    assert.eq(res.nMatched, 2);
    bulk = coll.initializeUnorderedBulkOp();
    bulk.find({x: 2}).hint({s: 1}).upsert().updateOne({$set: {y: 1}});
    res = bulk.execute();
    assert.eq(res.nUpserted, 1);
    bulk = coll.initializeUnorderedBulkOp();
    bulk.find({x: 2}).hint({s: 1}).upsert().replaceOne({$set: {y: 1}});
    res = bulk.execute();
    assert.eq(res.nUpserted, 1);

    res = coll.bulkWrite([{
        updateOne: {
            filter: {x: 2},
            update: {$set: {y: 2}},
            hint: {s: 1},
            upsert: true,
        }
    }]);
    assert.eq(res.upsertedCount, 1);

    res = coll.bulkWrite([{
        updateMany: {
            filter: {x: 1},
            update: {$set: {y: 2}},
            hint: {s: 1},
        }
    }]);
    assert.eq(res.matchedCount, 2);

    res = coll.bulkWrite([{
        replaceOne: {
            filter: {x: 2},
            replacement: {x: 2, y: 3},
            hint: {s: 1},
            upsert: true,
        }
    }]);
    assert.eq(res.upsertedCount, 1);
}

function failedHintTest() {
    coll.drop();
    assert.commandWorked(coll.insert({x: 1}));
    assert.commandWorked(coll.createIndex({x: 1}));

    // Command should fail with incorrectly formatted hints.
    let updateCmd = {update: coll.getName(), updates: [{q: {_id: 1}, u: {$set: {y: 1}}, hint: 1}]};
    assert.commandFailedWithCode(coll.runCommand(updateCmd), ErrorCodes.FailedToParse);
    updateCmd = {update: coll.getName(), updates: [{q: {_id: 1}, u: {$set: {y: 1}}, hint: true}]};
    assert.commandFailedWithCode(coll.runCommand(updateCmd), ErrorCodes.FailedToParse);

    // Command should fail with hints to non-existent indexes.
    updateCmd = {
        update: coll.getName(),
        updates: [{q: {_id: 1}, u: {$set: {y: 1}}, hint: {badHint: 1}}]
    };
    assert.commandFailedWithCode(coll.runCommand(updateCmd), ErrorCodes.BadValue);
}

normalIndexTest();
sparseIndexTest();
shellHelpersTest();
failedHintTest();