/**
 * Test to confirm that mongoS's special handling of replacement updates with an exact query on _id
 * behaves as expected in the case where a collection's shard key includes _id:
 *
 * - For update replacements, mongoS combines the _id from the query with the replacement document
 * to target the query towards a single shard, rather than scattering to all shards.
 * - For upsert replacements, which always require an exact shard key match, mongoS combines the _id
 * from the query with the replacement document to produce a complete shard key.
 *
 * These special cases are allowed because mongoD always propagates the _id of an existing document
 * into its replacement, and in the case of an upsert will use the value of _id from the query
 * filter.
 *
 * @tags: [
 *   requires_find_command,
 *   uses_multi_shard_transactions,
 *   uses_transactions,
 * ]
 */
(function() {
load("jstests/libs/profiler.js");  // For profilerHas*OrThrow helper functions.

// Test deliberately inserts orphans outside of migrations.
TestData.skipCheckOrphans = true;

const st = new ShardingTest({shards: 2, mongos: 1, other: {enableBalancer: false}});

const mongosDB = st.s0.getDB(jsTestName());
const mongosColl = mongosDB.test;

const shard0DB = st.shard0.getDB(jsTestName());
const shard1DB = st.shard1.getDB(jsTestName());

assert.commandWorked(mongosDB.dropDatabase());

// Enable sharding on the test DB and ensure its primary is shard0.
assert.commandWorked(mongosDB.adminCommand({enableSharding: mongosDB.getName()}));
st.ensurePrimaryShard(mongosDB.getName(), st.shard0.shardName);

// Enables profiling on both shards so that we can verify the targeting behaviour.
function restartProfiling() {
    for (let shardDB of [shard0DB, shard1DB]) {
        shardDB.setProfilingLevel(0);
        shardDB.system.profile.drop();
        shardDB.setProfilingLevel(2);
    }
}

function setUpData() {
    // Write a single document to shard0 and verify that it is present.
    mongosColl.insert({_id: -100, a: -100, msg: "not_updated"});
    assert.docEq(shard0DB.test.find({_id: -100}).toArray(),
                 [{_id: -100, a: -100, msg: "not_updated"}]);

    // Write a document with the same key directly to shard1. This simulates an orphaned
    // document, or the duplicate document which temporarily exists during a chunk migration.
    shard1DB.test.insert({_id: -100, a: -100, msg: "not_updated"});

    // Clear and restart the profiler on both shards.
    restartProfiling();
}

function runReplacementUpdateTestsForHashedShardKey() {
    setUpData();

    // Perform a replacement update whose query is an exact match on _id and whose replacement
    // document contains the remainder of the shard key. Despite the fact that the replacement
    // document does not contain the entire shard key, we expect that mongoS will extract the
    // _id from the query and combine it with the replacement doc to target a single shard.
    let writeRes = assert.commandWorked(
        mongosColl.update({_id: -100}, {a: -100, msg: "update_extracted_id_from_query"}));

    // Verify that the update did not modify the orphan document.
    assert.docEq(shard1DB.test.find({_id: -100}).toArray(),
                 [{_id: -100, a: -100, msg: "not_updated"}]);
    assert.eq(writeRes.nMatched, 1);
    assert.eq(writeRes.nModified, 1);

    // Verify that the update only targeted shard0 and that the resulting document appears as
    // expected.
    assert.docEq(mongosColl.find({_id: -100}).toArray(),
                 [{_id: -100, a: -100, msg: "update_extracted_id_from_query"}]);
    profilerHasSingleMatchingEntryOrThrow({
        profileDB: shard0DB,
        filter: {op: "update", "command.u.msg": "update_extracted_id_from_query"}
    });
    profilerHasZeroMatchingEntriesOrThrow({
        profileDB: shard1DB,
        filter: {op: "update", "command.u.msg": "update_extracted_id_from_query"}
    });

    // Perform an upsert replacement whose query is an exact match on _id and whose replacement
    // doc contains the remainder of the shard key. The _id taken from the query should be used
    // both in targeting the update and in generating the new document.
    writeRes = assert.commandWorked(mongosColl.update(
        {_id: 101}, {a: 101, msg: "upsert_extracted_id_from_query"}, {upsert: true}));
    assert.eq(writeRes.nUpserted, 1);

    // Verify that the update only targeted shard1, and that the resulting document appears as
    // expected. At this point in the test we expect shard1 to be stale, because it was the
    // destination shard for the first moveChunk; we therefore explicitly check the profiler for
    // a successful update, i.e. one which did not report a stale config exception.
    assert.docEq(mongosColl.find({_id: 101}).toArray(),
                 [{_id: 101, a: 101, msg: "upsert_extracted_id_from_query"}]);
    assert.docEq(shard1DB.test.find({_id: 101}).toArray(),
                 [{_id: 101, a: 101, msg: "upsert_extracted_id_from_query"}]);
    profilerHasZeroMatchingEntriesOrThrow({
        profileDB: shard0DB,
        filter: {op: "update", "command.u.msg": "upsert_extracted_id_from_query"}
    });
    profilerHasSingleMatchingEntryOrThrow({
        profileDB: shard1DB,
        filter: {
            op: "update",
            "command.u.msg": "upsert_extracted_id_from_query",
            errName: {$exists: false}
        }
    });
}

function runReplacementUpdateTestsForCompoundShardKey() {
    setUpData();

    // Perform a replacement update whose query is an exact match on _id and whose replacement
    // document contains the remainder of the shard key. Despite the fact that the replacement
    // document does not contain the entire shard key, we expect that mongoS will extract the
    // _id from the query and combine it with the replacement doc to target a single shard.
    let writeRes = assert.commandWorked(
        mongosColl.update({_id: -100}, {a: -100, msg: "update_extracted_id_from_query"}));

    // Verify that the update did not modify the orphan document.
    assert.docEq(shard1DB.test.find({_id: -100}).toArray(),
                 [{_id: -100, a: -100, msg: "not_updated"}]);
    assert.eq(writeRes.nMatched, 1);
    assert.eq(writeRes.nModified, 1);

    // Verify that the update only targeted shard0 and that the resulting document appears as
    // expected.
    assert.docEq(mongosColl.find({_id: -100}).toArray(),
                 [{_id: -100, a: -100, msg: "update_extracted_id_from_query"}]);
    profilerHasSingleMatchingEntryOrThrow({
        profileDB: shard0DB,
        filter: {op: "update", "command.u.msg": "update_extracted_id_from_query"}
    });
    profilerHasZeroMatchingEntriesOrThrow({
        profileDB: shard1DB,
        filter: {op: "update", "command.u.msg": "update_extracted_id_from_query"}
    });

    // An upsert whose query doesn't have full shard key will fail.
    assert.commandFailedWithCode(
        mongosColl.update(
            {_id: 101}, {a: 101, msg: "upsert_extracted_id_from_query"}, {upsert: true}),
        ErrorCodes.ShardKeyNotFound);

    // Verify that the document did not perform any writes.
    assert.docEq(mongosColl.find({_id: 101}).itcount(), 0);

    // Verify that an update whose query contains an exact match on _id but whose replacement
    // doc does not contain all other shard key fields will be targeted as if the missing shard
    // key values are null, but will write the replacement document as-is.

    // Need to start a session to change the shard key.
    const session = st.s.startSession({retryWrites: true});
    const sessionDB = session.getDatabase(jsTestName());
    const sessionColl = sessionDB.test;

    sessionColl.insert({_id: -99, a: null, msg: "not_updated"});

    assert.commandWorked(
        sessionColl.update({_id: -99}, {_id: -99, msg: "update_missing_shard_key_field"}));

    assert.docEq(sessionColl.find({_id: -99}).toArray(),
                 [{_id: -99, msg: "update_missing_shard_key_field"}]);

    // Verify that an upsert whose query contains an exact match on _id but whose replacement
    // document does not contain all other shard key fields will work properly.
    assert.commandWorked(
        sessionColl.update({_id: -100, a: -100}, {msg: "upsert_targeting_worked"}, {upsert: true}));
    assert.eq(mongosColl.find({_id: -100, a: -100}).itcount(), 0);
    assert.eq(mongosColl.find({msg: "upsert_targeting_worked"}).itcount(), 1);
}

// Shard the test collection on {_id: 1, a: 1}, split it into two chunks, and migrate one of
// these to the second shard.
st.shardColl(mongosColl, {_id: 1, a: 1}, {_id: 0, a: 0}, {_id: 1, a: 1}, mongosDB.getName(), true);

// Run the replacement behaviour tests that are relevant to a compound key that includes _id.
runReplacementUpdateTestsForCompoundShardKey();

// Drop and reshard the collection on {_id: "hashed"}, which will autosplit across both shards.
assert(mongosColl.drop());
mongosDB.adminCommand({shardCollection: mongosColl.getFullName(), key: {_id: "hashed"}});

// Run the replacement behaviour tests relevant to a collection sharded on {_id: "hashed"}.
runReplacementUpdateTestsForHashedShardKey();

st.stop();
})();