// Tests for $merge with an existing target collection.
(function() {
"use strict";

load("jstests/aggregation/extras/merge_helpers.js");  // For withEachMergeMode.
load("jstests/aggregation/extras/utils.js");          // For assertErrorCode.

const st = new ShardingTest({shards: 2, rs: {nodes: 1}});

const mongosDB = st.s0.getDB("source_db");
const sourceColl = mongosDB["source_coll"];
const outputCollSameDb = mongosDB[jsTestName() + "_merge"];

function testMerge(sourceColl, targetColl, shardedSource, shardedTarget) {
    jsTestLog(`Testing $merge from ${sourceColl.getFullName()} ` +
              `(${shardedSource ? "sharded" : "unsharded"}) to ${targetColl.getFullName()} ` +
              `(${shardedTarget ? "sharded" : "unsharded"})`);
    sourceColl.drop();
    targetColl.drop();
    assert.commandWorked(targetColl.runCommand("create"));

    if (shardedSource) {
        st.shardColl(sourceColl, {_id: 1}, {_id: 0}, {_id: 1}, sourceColl.getDB().getName());
    }

    if (shardedTarget) {
        st.shardColl(targetColl, {_id: 1}, {_id: 0}, {_id: 1}, targetColl.getDB().getName());
    }

    for (let i = -5; i < 5; i++) {
        assert.commandWorked(sourceColl.insert({_id: i}));
    }
    withEachMergeMode(({whenMatchedMode, whenNotMatchedMode}) => {
        // Test without documents in target collection.
        assert.commandWorked(targetColl.remove({}));
        if (whenNotMatchedMode == "fail") {
            // Test whenNotMatchedMode: "fail" to an existing collection.
            assertErrorCode(sourceColl,
                            [{
                                $merge: {
                                    into: {
                                        db: targetColl.getDB().getName(),
                                        coll: targetColl.getName(),
                                    },
                                    whenMatched: whenMatchedMode,
                                    whenNotMatched: whenNotMatchedMode
                                }
                            }],
                            13113);
        } else {
            assert.doesNotThrow(() => sourceColl.aggregate([{
                $merge: {
                    into: {
                        db: targetColl.getDB().getName(),
                        coll: targetColl.getName(),
                    },
                    whenMatched: whenMatchedMode,
                    whenNotMatched: whenNotMatchedMode
                }
            }]));
            assert.eq(whenNotMatchedMode == "discard" ? 0 : 10, targetColl.find().itcount());
        }

        // Test with documents in target collection. Every document in the source collection is
        // present in the target, plus some additional documents that doesn't match.
        assert.commandWorked(targetColl.remove({}));
        for (let i = -10; i < 5; i++) {
            assert.commandWorked(targetColl.insert({_id: i}));
        }

        if (whenMatchedMode == "fail") {
            // Test whenMatched: "fail" to an existing collection with unique key conflicts.
            assertErrorCode(sourceColl,
                            [{
                                $merge: {
                                    into: {
                                        db: targetColl.getDB().getName(),
                                        coll: targetColl.getName(),
                                    },
                                    whenMatched: whenMatchedMode,
                                    whenNotMatched: whenNotMatchedMode
                                }
                            }],
                            ErrorCodes.DuplicateKey);
        } else {
            assert.doesNotThrow(() => sourceColl.aggregate([{
                $merge: {
                    into: {
                        db: targetColl.getDB().getName(),
                        coll: targetColl.getName(),
                    },
                    whenMatched: whenMatchedMode,
                    whenNotMatched: whenNotMatchedMode
                }
            }]));
        }
        assert.eq(15, targetColl.find().itcount());
    });

    // Legacy $out is only supported to the same database.
    if (sourceColl.getDB() === targetColl.getDB()) {
        if (shardedTarget) {
            // Test that legacy $out fails if the target collection is sharded.
            assertErrorCode(sourceColl, [{$out: targetColl.getName()}], 28769);
        } else {
            // Test that legacy $out will drop the target collection and replace with the
            // contents of the source collection.
            sourceColl.aggregate([{$out: targetColl.getName()}]);
            assert.eq(10, targetColl.find().itcount());
        }
    }
}

//
// Tests for $merge where the output collection is in the same database as the source
// collection.
//

// Test with unsharded source and sharded target collection.
testMerge(sourceColl, outputCollSameDb, false, true);

// Test with sharded source and sharded target collection.
testMerge(sourceColl, outputCollSameDb, true, true);

// Test with sharded source and unsharded target collection.
testMerge(sourceColl, outputCollSameDb, true, false);

// Test with unsharded source and unsharded target collection.
testMerge(sourceColl, outputCollSameDb, false, false);

//
// Tests for $merge to a database that differs from the source collection's database.
//
const foreignDb = st.s0.getDB("foreign_db");
const outputCollDiffDb = foreignDb["output_coll"];

// Test with sharded source and sharded target collection.
testMerge(sourceColl, outputCollDiffDb, true, true);

// Test with unsharded source and unsharded target collection.
testMerge(sourceColl, outputCollDiffDb, false, false);

// Test with unsharded source and sharded target collection.
testMerge(sourceColl, outputCollDiffDb, false, true);

// Test with sharded source and unsharded target collection.
testMerge(sourceColl, outputCollDiffDb, true, false);

st.stop();
}());
