/**
 * Test all the possible succeed/fail cases around sharded collections renaming.
 */

load('jstests/sharding/libs/catalog_cache_loader_helpers.js');

/**
 * Initialize a "from" sharded collection with 2 chunks - on 2 different nodes - each containing 1
 * document. Rename to `toNs` with the provided options and get sure it succeeds/fails as expected.
 */
function testRename(st, dbName, toNs, dropTarget, mustFail) {
    const db = st.getDB(dbName);
    const mongos = st.s0;

    const fromNs = dbName + '.from';
    assert.commandWorked(
        mongos.adminCommand({enablesharding: dbName, primaryShard: st.shard0.shardName}));
    assert.commandWorked(mongos.adminCommand({shardCollection: fromNs, key: {x: 1}}));

    const fromColl = mongos.getCollection(fromNs);
    fromColl.insert({x: 0});
    fromColl.insert({x: 2});
    assert.commandWorked(mongos.adminCommand({split: fromNs, middle: {x: 1}}));

    const fromUUID = getUUIDFromConfigCollections(mongos, fromNs);
    const aChunk = mongos.getDB('config').chunks.findOne({uuid: fromUUID});
    assert.commandWorked(mongos.adminCommand({
        moveChunk: fromNs,
        bounds: [aChunk.min, aChunk.max],
        to: st.shard1.shardName,
    }));

    const res = fromColl.renameCollection(toNs.split('.')[1], dropTarget);
    if (mustFail) {
        assert.commandFailed(res);
        return;
    }

    assert.commandWorked(res);

    const toUUID = getUUIDFromConfigCollections(mongos, toNs);
    const chunks = mongos.getDB('config').chunks.find({uuid: toUUID});
    const chunk0 = chunks.next();
    const chunk1 = chunks.next();

    assert(!chunks.hasNext(), 'Target collection expected to have exactly 2 chunks');
    assert(chunk0.shard != chunk1.shard, 'Chunks expected to be on different shards');

    const toColl = mongos.getCollection(toNs);
    assert.eq(db.to.find({x: 0}).itcount(), 1, 'Expected exactly one document on the shard');
    assert.eq(toColl.find({x: 2}).itcount(), 1, 'Expected exactly one document on the shard');

    // Infer whether the chunks cache collection names are based by collection UUID or collection
    // namespace and get the actual chunks cache collection name of the target collection
    const toConfigCollDoc = mongos.getDB('config').collections.findOne({_id: toNs});
    const toChunksCollName = getCachedChunksCollectionName(toConfigCollDoc);

    // Validate the correctness of the collections metadata in the catalog cache on shards
    for (let db of [st.shard0.getDB('config'), st.shard1.getDB('config')]) {
        // Validate that the source collection metadata has been cleaned up
        assert.eq(db['cache.collections'].countDocuments({_id: fromNs}), 0);
        if (!supportLongCollectionName(toConfigCollDoc)) {
            assert(!db['cache.chunks.' + fromNs].exists());
        }

        // Validate that the target collection metadata has been downloaded
        assert.eq(db['cache.collections'].countDocuments({_id: toNs}), 1);
        assert(db[toChunksCollName].exists());
    }
}

// Never use the third shard, but leave it in order to indirectly check that rename participants
// properly handle the following cases:
// - Locally unknown source collection to rename
// - Locally unknown target collection to drop
const st = new ShardingTest({shards: 3, mongos: 1, other: {enableBalancer: false}});

const mongos = st.s0;

// Rename to non-existing target collection must succeed
{
    const dbName = 'testRenameToNewCollection';
    const toNs = dbName + '.to';
    testRename(st, dbName, toNs, false /* dropTarget */, false /* mustFail */);
}

// Rename to existing sharded target collection with dropTarget=true must succeed
{
    const dbName = 'testRenameToExistingShardedCollection';
    const toNs = dbName + '.to';
    assert.commandWorked(
        mongos.adminCommand({enablesharding: dbName, primaryShard: st.shard0.shardName}));
    assert.commandWorked(mongos.adminCommand({shardCollection: toNs, key: {a: 1}}));

    const toColl = mongos.getCollection(toNs);
    toColl.insert({a: 0});
    toColl.insert({a: 2});
    assert.commandWorked(mongos.adminCommand({split: toNs, middle: {a: 1}}));

    const toUUID = getUUIDFromConfigCollections(mongos, toNs);
    const aChunk = mongos.getDB('config').chunks.findOne({uuid: toUUID});
    assert.commandWorked(mongos.adminCommand({
        moveChunk: toNs,
        bounds: [aChunk.min, aChunk.max],
        to: st.shard1.shardName,
    }));

    testRename(st, dbName, toNs, true /* dropTarget */, false /* mustFail */);
}

// Rename to existing unsharded target collection with dropTarget=true must succeed
{
    const dbName = 'testRenameToExistingUnshardedCollection';
    const toNs = dbName + '.to';
    assert.commandWorked(
        mongos.adminCommand({enablesharding: dbName, primaryShard: st.shard0.shardName}));
    const toColl = mongos.getCollection(toNs);
    toColl.insert({a: 0});

    testRename(st, dbName, toNs, true /* dropTarget */, false /* mustFail */);
}

// Rename to existing unsharded target collection with dropTarget=false must fail
{
    const dbName = 'testRenameToUnshardedCollectionWithoutDropTarget';
    const toNs = dbName + '.to';
    assert.commandWorked(
        mongos.adminCommand({enablesharding: dbName, primaryShard: st.shard0.shardName}));
    const toColl = mongos.getCollection(toNs);
    toColl.insert({a: 0});

    testRename(st, dbName, toNs, false /* dropTarget */, true /* mustFail */);
}

// Rename to existing sharded target collection with dropTarget=false must fail
{
    const dbName = 'testRenameToShardedCollectionWithoutDropTarget';
    const toNs = dbName + '.to';
    assert.commandWorked(
        mongos.adminCommand({enablesharding: dbName, primaryShard: st.shard0.shardName}));
    assert.commandWorked(mongos.adminCommand({shardCollection: toNs, key: {a: 1}}));
    const toColl = mongos.getCollection(toNs);
    toColl.insert({a: 0});

    testRename(st, dbName, toNs, false /* dropTarget */, true /* mustFail */);
}

// Rename unsharded collection to sharded target collection with dropTarget=true must succeed
{
    const dbName = 'testRenameUnshardedToShardedTargetCollection';
    const fromNs = dbName + '.from';
    const toNs = dbName + '.to';
    assert.commandWorked(
        mongos.adminCommand({enablesharding: dbName, primaryShard: st.shard0.shardName}));
    assert.commandWorked(mongos.adminCommand({shardCollection: toNs, key: {a: 1}}));

    const toColl = mongos.getCollection(toNs);
    toColl.insert({a: 0});
    toColl.insert({a: 2});
    assert.commandWorked(mongos.adminCommand({split: toNs, middle: {a: 1}}));
    const toUUID = getUUIDFromConfigCollections(mongos, toNs);
    const aChunk = mongos.getDB('config').chunks.findOne({uuid: toUUID});
    assert.commandWorked(mongos.adminCommand(
        {moveChunk: toNs, bounds: [aChunk.min, aChunk.max], to: st.shard1.shardName}));

    const fromColl = mongos.getCollection(fromNs);
    fromColl.insert({x: 0});

    assert.commandWorked(fromColl.renameCollection(toNs.split('.')[1], true /* dropTarget */));

    // Source collection just has documents with field `x`
    assert.eq(toColl.find({x: {$exists: true}}).itcount(), 1, 'Expected one source document');
    // Source collection just has documents with field `a`
    assert.eq(toColl.find({a: {$exists: true}}).itcount(), 0, 'Expected no target documents');
}

// Successful rename must pass tags from source to the target collection
{
    const dbName = 'testRenameFromTaggedCollection';
    const db = st.getDB(dbName);
    const fromNs = dbName + '.from';
    const toNs = dbName + '.to';

    assert.commandWorked(
        mongos.adminCommand({enablesharding: dbName, primaryShard: st.shard0.shardName}));
    assert.commandWorked(mongos.adminCommand({addShardToZone: st.shard0.shardName, zone: 'x'}));
    assert.commandWorked(mongos.adminCommand({addShardToZone: st.shard1.shardName, zone: 'y'}));
    assert.commandWorked(
        mongos.adminCommand({updateZoneKeyRange: fromNs, min: {x: 0}, max: {x: 2}, zone: 'x'}));
    assert.commandWorked(
        mongos.adminCommand({updateZoneKeyRange: fromNs, min: {x: 2}, max: {x: 4}, zone: 'y'}));
    assert.commandWorked(mongos.adminCommand({shardCollection: fromNs, key: {x: 1}}));

    var fromTags = mongos.getDB('config').tags.find({ns: fromNs}).toArray();

    const fromColl = mongos.getCollection(fromNs);
    fromColl.insert({x: 1});

    assert.commandWorked(fromColl.renameCollection(toNs.split('.')[1], false /* dropTarget */));

    const toTags = mongos.getDB('config').tags.find({ns: toNs}).toArray();
    assert.eq(toTags.length, 2, "Expected 2 tags associated to the target collection");

    function deleteDifferentTagFields(tag, index, array) {
        delete tag['_id'];
        delete tag['ns'];
    }
    fromTags.forEach(deleteDifferentTagFields);
    toTags.forEach(deleteDifferentTagFields);

    // Compare field by field because keys can potentially be in different order
    for (field in Object.keys(fromTags[0])) {
        assert.eq(fromTags[0][field],
                  toTags[0][field],
                  "Expected source tags to be passed to target collection");
        assert.eq(fromTags[1][field],
                  toTags[1][field],
                  "Expected source tags to be passed to target collection");
    }

    fromTags = mongos.getDB('config').tags.find({ns: fromNs}).toArray();
    assert.eq(fromTags.length, 0, "Expected no tags associated to the source collection");
}

// Rename to target collection with tags must fail
{
    const dbName = 'testRenameToTaggedCollection';
    const fromNs = dbName + '.from';
    const toNs = dbName + '.to';
    assert.commandWorked(mongos.adminCommand({addShardToZone: st.shard0.shardName, zone: 'x'}));
    assert.commandWorked(
        mongos.adminCommand({updateZoneKeyRange: toNs, min: {x: 0}, max: {x: 10}, zone: 'x'}));

    assert.commandWorked(mongos.adminCommand({enablesharding: dbName}));
    assert.commandWorked(mongos.adminCommand({shardCollection: fromNs, key: {x: 1}}));

    const fromColl = mongos.getCollection(fromNs);
    fromColl.insert({x: 1});
    assert.commandFailed(fromColl.renameCollection(toNs.split('.')[1], false /* dropTarget*/));
}

st.stop();
