/**
 * Collection of helper functions for testing the $merge aggregation stage.
 */

load("jstests/libs/fixture_helpers.js");  // For isSharded.

function withEachKindOfWriteStage(targetColl, callback) {
    callback({$out: targetColl.getName()});
    callback({$merge: {into: targetColl.getName()}});
}

/**
 * Executes the callback function with each valid combination of 'whenMatched' and 'whenNotMatched'
 * modes (as named arguments). Note that one mode is a pipeline.
 */
function withEachMergeMode(callback) {
    callback({whenMatchedMode: "replace", whenNotMatchedMode: "insert"});
    callback({whenMatchedMode: "replace", whenNotMatchedMode: "fail"});
    callback({whenMatchedMode: "replace", whenNotMatchedMode: "discard"});

    callback({whenMatchedMode: "merge", whenNotMatchedMode: "insert"});
    callback({whenMatchedMode: "merge", whenNotMatchedMode: "fail"});
    callback({whenMatchedMode: "merge", whenNotMatchedMode: "discard"});

    callback({whenMatchedMode: "fail", whenNotMatchedMode: "insert"});

    callback({whenMatchedMode: "keepExisting", whenNotMatchedMode: "insert"});

    callback({whenMatchedMode: [], whenNotMatchedMode: "insert"});
    callback({whenMatchedMode: [], whenNotMatchedMode: "fail"});
    callback({whenMatchedMode: [], whenNotMatchedMode: "discard"});
}

function assertMergeFailsForAllModesWithCode(
    {source, target, onFields, options, prevStages = [], errorCodes}) {
    withEachMergeMode(({whenMatchedMode, whenNotMatchedMode}) => {
        const mergeStage = {
            into: {db: target.getDB().getName(), coll: target.getName()},
            whenMatched: whenMatchedMode,
            whenNotMatched: whenNotMatchedMode
        };
        if (onFields) {
            mergeStage.on = onFields;
        }
        const pipeline = prevStages.concat([{$merge: mergeStage}]);

        // In sharded passthrough suites, the error code may be different depending on where we
        // extract the "on" fields.
        const cmd = {aggregate: source.getName(), pipeline: pipeline, cursor: {}};
        assert.commandFailedWithCode(source.getDB().runCommand(Object.merge(cmd, options)),
                                     errorCodes);
    });
}

function assertMergeFailsWithoutUniqueIndex({source, target, onFields, options, prevStages}) {
    assertMergeFailsForAllModesWithCode(
        {source, target, onFields, options, prevStages, errorCodes: [51183, 51190]});
}

function assertMergeSucceedsWithExpectedUniqueIndex(
    {source, target, onFields, options, prevStages = []}) {
    withEachMergeMode(({whenMatchedMode, whenNotMatchedMode}) => {
        // Skip the combination of merge modes which will fail depending on the contents of the
        // source and target collection, as this will cause the assertion below to trip.
        if (whenMatchedMode == "fail" || whenNotMatchedMode == "fail")
            return;

        const mergeStage = {
            into: {db: target.getDB().getName(), coll: target.getName()},
            whenMatched: whenMatchedMode,
            whenNotMatched: whenNotMatchedMode
        };

        // Do not include the "on" fields in the command if the caller did not specify it.
        if (onFields) {
            mergeStage.on = onFields;
        }
        const pipeline = prevStages.concat([{$merge: mergeStage}]);

        assert.commandWorked(target.remove({}));
        assert.doesNotThrow(() => source.aggregate(pipeline, options));
    });
}

// Helper to drop a collection without using the shell helper, and thus avoiding the implicit
// recreation in the sharded collections passthrough suites.
function dropWithoutImplicitRecreate(collName) {
    db.runCommand({drop: collName});
}