/**
 * Test that chunks and documents are moved correctly after zone changes.
 */
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";
import {
    assertChunksOnShards,
    assertDocsOnShards,
    assertShardTags,
    moveZoneToShard,
    runBalancer,
} from "jstests/sharding/libs/zone_changes_util.js";

/**
 * Adds each shard to the corresponding zone in zoneTags, and makes the zone range equal
 * to the chunk range of the shard. Assumes that there are no chunk holes on each shard.
 */
function addShardsToZonesAndAssignZoneRanges(st, ns, shardChunkBounds, shardTags) {
    let zoneChunks = {};
    for (let [shardName, chunkBounds] of Object.entries(shardChunkBounds)) {
        let zoneName = shardTags[shardName][0];
        let rangeMin = {x: MaxKey};
        let rangeMax = {x: MinKey};
        for (let bounds of chunkBounds) {
            if (chunkBoundsUtil.lt(bounds[0], rangeMin)) {
                rangeMin = bounds[0];
            }
            if (chunkBoundsUtil.gte(bounds[1], rangeMax)) {
                rangeMax = bounds[1];
            }
        }
        zoneChunks[zoneName] = chunkBounds;
        assert.commandWorked(st.s.adminCommand({addShardToZone: shardName, zone: zoneName}));
        assert.commandWorked(st.s.adminCommand(
            {updateZoneKeyRange: ns, min: rangeMin, max: rangeMax, zone: zoneName}));
    }
    return zoneChunks;
}

/**
 * Returns the highest chunk bounds out of the given chunk bounds. Assumes that the
 * chunks do not overlap.
 */
function findHighestChunkBounds(chunkBounds) {
    let highestBounds = chunkBounds[0];
    for (let i = 1; i < chunkBounds.length; i++) {
        if (chunkBoundsUtil.lt(highestBounds, chunkBounds[i])) {
            highestBounds = chunkBounds[i];
        }
    }
    return highestBounds;
}

const st = new ShardingTest({shards: 3, other: {chunkSize: 1}});
let primaryShard = st.shard0;
let dbName = "test";
let testDB = st.s.getDB(dbName);
let configDB = st.s.getDB("config");
let coll = testDB.hashed;
let ns = coll.getFullName();
let shardKey = {x: "hashed"};

assert.commandWorked(
    st.s.adminCommand({enableSharding: dbName, primaryShard: primaryShard.shardName}));

jsTest.log("Shard the collection. The command creates one chunk on each of the shards by default.");
assert.commandWorked(st.s.adminCommand({shardCollection: ns, key: shardKey}));

jsTest.log("Insert docs (one per chunk) and check that they end up on the right shards.");
const bigString = 'X'.repeat(1024 * 1024);  // 1MB
let docs = [
    {x: -25, s: bigString},
    {x: -18, s: bigString},
    {x: -5, s: bigString},
    {x: -1, s: bigString},
    {x: 5, s: bigString},
    {x: 10, s: bigString}
];
// TODO SERVER-81884: update once 8.0 becomes last LTS.
if (FeatureFlagUtil.isPresentAndEnabled(testDB,
                                        "OneChunkPerShardEmptyCollectionWithHashedShardKey")) {
    // Make sure that there is one chunk dedicated for each inserted document
    assert.commandWorked(
        st.s.adminCommand({split: ns, middle: {x: convertShardKeyToHashed(docs[1].x)}}));
    assert.commandWorked(
        st.s.adminCommand({split: ns, middle: {x: convertShardKeyToHashed(docs[3].x)}}));
    assert.commandWorked(
        st.s.adminCommand({split: ns, middle: {x: convertShardKeyToHashed(docs[5].x)}}));
}
assert.commandWorked(coll.insert(docs));

let chunkDocs = findChunksUtil.findChunksByNs(configDB, ns).sort({min: 1}).toArray();
let shardChunkBounds = chunkBoundsUtil.findShardChunkBounds(chunkDocs);

let docChunkBounds = [];
let minHash = MaxKey;
docs.forEach(function(doc) {
    let hash = convertShardKeyToHashed(doc.x);
    let {shard, bounds} =
        chunkBoundsUtil.findShardAndChunkBoundsForShardKey(st, shardChunkBounds, {x: hash});
    assert.eq(1, shard.getCollection(ns).count(doc));
    docChunkBounds.push(bounds);
    if (bsonWoCompare(hash, minHash) < 0) {
        minHash = hash;
    }
});
assert.eq(docs.length, (new Set(docChunkBounds)).size);
assert.eq(docs.length, findChunksUtil.countChunksForNs(configDB, ns));

jsTest.log(
    "Assign each shard a zone, make each zone range equal to the chunk range for the shard, " +
    "and store the chunks for each zone.");
let shardTags = {
    [st.shard0.shardName]: ["zoneA"],
    [st.shard1.shardName]: ["zoneB"],
    [st.shard2.shardName]: ["zoneC"]
};
let zoneChunkBounds = addShardsToZonesAndAssignZoneRanges(st, ns, shardChunkBounds, shardTags);
assertShardTags(configDB, shardTags);

jsTest.log("Test shard's zone changes...");

jsTest.log(
    "Check that removing a zone from a shard causes its chunks and documents to move to other" +
    " shards that the zone belongs to.");
moveZoneToShard(st, "zoneA", st.shard0, st.shard1);
shardTags = {
    [st.shard0.shardName]: [],
    [st.shard1.shardName]: ["zoneB", "zoneA"],
    [st.shard2.shardName]: ["zoneC"]
};
assertShardTags(configDB, shardTags);

runBalancer(st, zoneChunkBounds["zoneA"].length);
shardChunkBounds = {
    [st.shard0.shardName]: [],
    [st.shard1.shardName]: [...zoneChunkBounds["zoneB"], ...zoneChunkBounds["zoneA"]],
    [st.shard2.shardName]: zoneChunkBounds["zoneC"]
};
assertChunksOnShards(configDB, ns, shardChunkBounds);
assertDocsOnShards(st, ns, shardChunkBounds, docs, shardKey);

jsTest.log("Check that the balancer balances chunks within zones.");
assert.commandWorked(st.s.adminCommand({addShardToZone: st.shard0.shardName, zone: "zoneB"}));
shardTags = {
    [st.shard0.shardName]: ["zoneB"],
    [st.shard1.shardName]: ["zoneB", "zoneA"],
    [st.shard2.shardName]: ["zoneC"]
};
assertShardTags(configDB, shardTags);

const numChunksToMove = zoneChunkBounds["zoneB"].length - 1;
runBalancer(st, numChunksToMove);
shardChunkBounds = {
    [st.shard0.shardName]: zoneChunkBounds["zoneB"].slice(0, numChunksToMove),
    [st.shard1.shardName]: [
        ...zoneChunkBounds["zoneA"],
        ...zoneChunkBounds["zoneB"].slice(numChunksToMove, zoneChunkBounds["zoneB"].length)
    ],
    [st.shard2.shardName]: zoneChunkBounds["zoneC"]
};
assertChunksOnShards(configDB, ns, shardChunkBounds);
assertDocsOnShards(st, ns, shardChunkBounds, docs, shardKey);

jsTest.log("Make another zone change, and check that the chunks and docs are on the right shards.");
assert.commandWorked(st.s.adminCommand({removeShardFromZone: st.shard0.shardName, zone: "zoneB"}));
moveZoneToShard(st, "zoneC", st.shard2, st.shard0);
moveZoneToShard(st, "zoneA", st.shard1, st.shard2);
shardTags = {
    [st.shard0.shardName]: ["zoneC"],
    [st.shard1.shardName]: ["zoneB"],
    [st.shard2.shardName]: ["zoneA"]
};
assertShardTags(configDB, shardTags);

runBalancer(st,
            numChunksToMove + zoneChunkBounds["zoneA"].length + zoneChunkBounds["zoneC"].length);
shardChunkBounds = {
    [st.shard0.shardName]: zoneChunkBounds["zoneC"],
    [st.shard1.shardName]: zoneChunkBounds["zoneB"],
    [st.shard2.shardName]: zoneChunkBounds["zoneA"]
};
assertChunksOnShards(configDB, ns, shardChunkBounds);
assertDocsOnShards(st, ns, shardChunkBounds, docs, shardKey);

jsTest.log("Test chunk's zone changes...");

// Find the chunk with the highest bounds in zoneA.
let originalZoneARange = chunkBoundsUtil.computeRange(zoneChunkBounds["zoneA"]);
let targetChunkBounds = findHighestChunkBounds(zoneChunkBounds["zoneA"]);
assert(chunkBoundsUtil.containsKey(targetChunkBounds[0], ...originalZoneARange));
assert(chunkBoundsUtil.eq(targetChunkBounds[1], originalZoneARange[1]));
let remainingZoneAChunkBounds = zoneChunkBounds["zoneA"].filter(
    (chunkBounds) => !chunkBoundsUtil.eq(targetChunkBounds, chunkBounds));

jsTest.log(
    "Change the zone ranges so that the chunk that used to belong to zoneA now belongs to zoneB.");
assert.commandWorked(st.s.adminCommand(
    {updateZoneKeyRange: ns, min: originalZoneARange[0], max: originalZoneARange[1], zone: null}));
assert.commandWorked(st.s.adminCommand({
    updateZoneKeyRange: ns,
    min: originalZoneARange[0],
    max: targetChunkBounds[0],
    zone: "zoneA"
}));
assert.commandWorked(st.s.adminCommand({
    updateZoneKeyRange: ns,
    min: targetChunkBounds[0],
    max: originalZoneARange[1],
    zone: "zoneB"
}));

jsTest.log("Check that the chunk moves from zoneA to zoneB after the zone range change.");
runBalancer(st, 1);
shardChunkBounds = {
    [st.shard0.shardName]: zoneChunkBounds["zoneC"],
    [st.shard1.shardName]: [targetChunkBounds, ...zoneChunkBounds["zoneB"]],
    [st.shard2.shardName]: remainingZoneAChunkBounds
};
assertChunksOnShards(configDB, ns, shardChunkBounds);
assertDocsOnShards(st, ns, shardChunkBounds, docs, shardKey);

jsTest.log(
    "Change the zone ranges so that the chunk that used to belong to zoneB now belongs to zoneC.");
assert.commandWorked(st.s.adminCommand(
    {updateZoneKeyRange: ns, min: targetChunkBounds[0], max: targetChunkBounds[1], zone: null}));
assert.commandWorked(st.s.adminCommand(
    {updateZoneKeyRange: ns, min: targetChunkBounds[0], max: targetChunkBounds[1], zone: "zoneC"}));

jsTest.log("Check that the chunk moves from zoneB to zoneC after the zone range change.");
runBalancer(st, 1);
shardChunkBounds = {
    [st.shard0.shardName]: [targetChunkBounds, ...zoneChunkBounds["zoneC"]],
    [st.shard1.shardName]: zoneChunkBounds["zoneB"],
    [st.shard2.shardName]: remainingZoneAChunkBounds
};
assertChunksOnShards(configDB, ns, shardChunkBounds);
assertDocsOnShards(st, ns, shardChunkBounds, docs, shardKey);

jsTest.log("Make the chunk not aligned with zone ranges.");
let splitPoint = (targetChunkBounds[1].x === MaxKey)
    ? {x: NumberLong(targetChunkBounds[0].x + 5000)}
    : {x: NumberLong(targetChunkBounds[1].x - 5000)};
assert(chunkBoundsUtil.containsKey(splitPoint, ...targetChunkBounds));
assert.commandWorked(st.s.adminCommand(
    {updateZoneKeyRange: ns, min: targetChunkBounds[0], max: targetChunkBounds[1], zone: null}));
assert.commandWorked(st.s.adminCommand(
    {updateZoneKeyRange: ns, min: targetChunkBounds[0], max: splitPoint, zone: "zoneC"}));
assert.commandWorked(st.s.adminCommand(
    {updateZoneKeyRange: ns, min: splitPoint, max: targetChunkBounds[1], zone: "zoneA"}));

jsTest.log(
    "Check that the balancer splits the chunk and that all chunks and docs are on the right shards.");
runBalancer(st, 1);
shardChunkBounds = {
    [st.shard0.shardName]: [[targetChunkBounds[0], splitPoint], ...zoneChunkBounds["zoneC"]],
    [st.shard1.shardName]: zoneChunkBounds["zoneB"],
    [st.shard2.shardName]: [[splitPoint, targetChunkBounds[1]], ...remainingZoneAChunkBounds]
};
assertChunksOnShards(configDB, ns, shardChunkBounds);
assertDocsOnShards(st, ns, shardChunkBounds, docs, shardKey);

st.stop();
