/*
 * Basic tests for moveRange.
 *
 * @tags: [
 *    assumes_balancer_off
 * ]
 */
import {FeatureFlagUtil} from "jstests/libs/feature_flag_util.js";
import {ShardingTest} from "jstests/libs/shardingtest.js";
import {chunkBoundsUtil} from "jstests/sharding/libs/chunk_bounds_util.js";
import {findChunksUtil} from "jstests/sharding/libs/find_chunks_util.js";

const st = new ShardingTest({mongos: 1, shards: 2, chunkSize: 1});
const kDbName = 'db';

const mongos = st.s0;
const shard0 = st.shard0.shardName;
const shard1 = st.shard1.shardName;

assert.commandWorked(mongos.adminCommand({enableSharding: kDbName, primaryShard: shard0}));

function getRandomShardKeyValue(ns, skPattern, filter) {
    const isHashedShardKey = Object.values(skPattern).includes('hashed');
    const coll = mongos.getCollection(ns);

    // Get a random document from the collection
    let doc = coll.aggregate([{$match: filter}, {$sample: {size: 1}}]).next();

    // Delete fields not making part of the shard key
    for (let key in doc) {
        if (!(key in skPattern)) {
            delete doc[key];
        }
    }

    if (isHashedShardKey) {
        doc.a = convertShardKeyToHashed(doc.a);
    }

    return doc;
}

// Tests for moveRange that will call move&split. We call moveRange with minBound on the positive
// values and moveRange with maxBound on negative values to ensure that the chunk chosen is big
// enough to be split by moveRange.
function testMoveRangeWithBigChunk(mongos, ns, skPattern, minBound) {
    // Get a random existing shard key value, `moveRange` will be called on the owning chunk
    let filter = minBound ? {a: {$gte: 0}} : {a: {$lt: 0}};
    let randomSK = getRandomShardKeyValue(ns, skPattern, filter);

    // Get bounds and shard of the chunk owning `randomSK`
    const chunksBefore = findChunksUtil.findChunksByNs(mongos.getDB('config'), ns).toArray();
    const shardChunkBounds = chunkBoundsUtil.findShardChunkBounds(chunksBefore);
    const {shard, bounds} =
        chunkBoundsUtil.findShardAndChunkBoundsForShardKey(st, shardChunkBounds, randomSK);

    const donor = shard.shardName;
    const recipient = donor == shard0 ? shard1 : shard0;

    // Count chunks belonging to donor and recipient shards BEFORE moveRange
    const nChunksOnDonorBefore = chunksBefore.filter(chunk => chunk.shard == donor).length;
    const nChunksOnRecipientBefore = chunksBefore.filter(chunk => chunk.shard == recipient).length;

    if (minBound) {
        assert.commandWorked(
            mongos.adminCommand({moveRange: ns, min: randomSK, toShard: recipient}));
    } else {
        assert.commandWorked(
            mongos.adminCommand({moveRange: ns, max: randomSK, toShard: recipient}));
    }

    // Count chunks belonging to donor and recipient shards AFTER moveRange
    const chunksAfter = findChunksUtil.findChunksByNs(mongos.getDB('config'), ns).toArray();
    const nChunksOnDonorAfter = chunksAfter.filter(chunk => chunk.shard == donor).length;
    const nChunksOnRecipientAfter = chunksAfter.filter(chunk => chunk.shard == recipient).length;

    let nExpectedChunksOnRecipientAfter = nChunksOnRecipientBefore + 1;
    // For moveRange with a maxBound, the number of chunks on recipient and donor doesn't change
    // if a shardKey that is a lower-bound of a pre-existing chunk was selected.
    if (!minBound && chunkBoundsUtil.eq(bounds[0], randomSK)) {
        nExpectedChunksOnRecipientAfter = nChunksOnRecipientBefore;
    }

    assert.eq(nExpectedChunksOnRecipientAfter,
              nChunksOnRecipientAfter,
              "The number of chunks on the recipient shard did not increase following a moveRange");
    assert(nChunksOnDonorAfter == nChunksOnDonorBefore ||
               nChunksOnDonorAfter == nChunksOnDonorBefore + 1,
           "Unexpected number of chunks on the donor shard after triggering a split + move");
}

function test(collName, skPattern) {
    const ns = kDbName + '.' + collName;

    assert.commandWorked(mongos.adminCommand({shardCollection: ns, key: skPattern}));

    let aChunk = findChunksUtil.findOneChunkByNs(mongos.getDB('config'), ns, {shard: shard0});
    assert(aChunk);
    jsTest.log("Testing invalid commands");
    // Fail if one of the bounds is not a valid shard key
    assert.commandFailed(mongos.adminCommand(
        {moveRange: ns, min: aChunk.min, max: {invalidShardKey: 10}, toShard: shard1}));

    // Fail if the `to` shard does not exists
    assert.commandFailed(mongos.adminCommand(
        {moveRange: ns, min: aChunk.min, max: aChunk.max, toShard: 'WrongShard'}));

    // Test that `moveRange` with min & max bounds works
    jsTest.log("Testing moveRange with both bounds");
    assert.commandWorked(
        mongos.adminCommand({moveRange: ns, min: aChunk.min, max: aChunk.max, toShard: shard1}));

    assert.eq(0, mongos.getDB('config').chunks.countDocuments({_id: aChunk._id, shard: shard0}));
    assert.eq(1, mongos.getDB('config').chunks.countDocuments({_id: aChunk._id, shard: shard1}));

    // Test that `moveRange` only with min bound works (translates to `moveChunk` because chunk too
    // small to be split)
    jsTest.log("Testing moveRange with only min bound");
    assert.commandWorked(mongos.adminCommand({moveRange: ns, min: aChunk.min, toShard: shard0}));

    assert.eq(1, mongos.getDB('config').chunks.countDocuments({_id: aChunk._id, shard: shard0}));
    assert.eq(0, mongos.getDB('config').chunks.countDocuments({_id: aChunk._id, shard: shard1}));

    // Test that `moveRange` only with max bound works (translates to `moveChunk` because chunk too
    // small to be split)
    jsTest.log("Testing moveRange with only max bound");
    assert.commandWorked(mongos.adminCommand({moveRange: ns, max: aChunk.max, toShard: shard1}));

    assert.eq(0, mongos.getDB('config').chunks.countDocuments({_id: aChunk._id, shard: shard0}));
    assert.eq(1, mongos.getDB('config').chunks.countDocuments({_id: aChunk._id, shard: shard1}));

    // Insert 10MB >0 and <0 in order to create multiple big chunks (chunkSize is set to 1MB)
    jsTest.log("Inserting data to create large chunks");
    const bigString = "X".repeat(1024 * 1024 / 4);  // 1 MB
    const coll = mongos.getCollection(ns);
    let bulk = coll.initializeUnorderedBulkOp();
    for (let i = -10; i < 10; i++) {
        bulk.insert({a: i, b: i, str: bigString});
    }
    assert.commandWorked(bulk.execute());

    // Test moving large chunk with only min bound
    jsTest.log("Testing moveChunk with only min bound and large chunk");
    testMoveRangeWithBigChunk(mongos, ns, skPattern, true /* minBound */);

    // Test moving large chunk with only max bound
    jsTest.log("Testing moveChunk with only max bound and large chunk");
    testMoveRangeWithBigChunk(mongos, ns, skPattern, false /* maxBound */);
}

// Test running running moveRange on an unsplittable collection will fail
if (FeatureFlagUtil.isPresentAndEnabled(mongos, "TrackUnshardedCollectionsUponCreation")) {
    const collName = "unsplittable_collection";
    const ns = kDbName + '.' + collName;

    jsTest.log("Testing on unsplittable namespace");
    assert.commandWorked(
        mongos.getDB(kDbName).runCommand({createUnsplittableCollection: collName}));
    assert.commandFailedWithCode(
        mongos.adminCommand({moveRange: ns, min: {_id: 0}, toShard: shard0}),
        ErrorCodes.NamespaceNotSharded);
}

test('nonHashedShardKey', {a: 1});

test('nonHashedCompundShardKey', {a: 1, b: 1});

test('hashedShardKey', {a: 'hashed'});

st.stop();
