/**
 * Test that explain of $lookup and $unionWith works correctly when the involved collections are
 * sharded.
 *
 * This test was originally designed to reproduce SERVER-71636.
 */
import {getAggPlanStage, getAggPlanStages} from "jstests/libs/query/analyze_plan.js";
import {ShardingTest} from "jstests/libs/shardingtest.js";

const dbName = "test";

const st = new ShardingTest({shards: 2});
const db = st.s.getDB(dbName);

const outerColl = db["outer"];
const innerColl = db["inner"];

(function createOuterColl() {
    outerColl.drop();
    assert.commandWorked(outerColl.insert([{_id: 1, x: "foo"}, {_id: 3, x: "foo"}]));
}());

function createInnerColl() {
    innerColl.drop();
    assert.commandWorked(
        innerColl.insert([{_id: 1, x: "foo", y: "a"}, {_id: 2, x: "foo", y: "z"}]));
}
createInnerColl();

function explainStage(stage, stageName) {
    const pipeline = [stage];
    let explain = outerColl.explain("executionStats").aggregate(pipeline);
    let stageExplain = getAggPlanStage(explain, stageName);
    assert.neq(stageExplain, null, explain);
    return stageExplain;
}

// Explain of $lookup when neither collection is sharded.
const lookupStage =
        {
            $lookup: {
                from: innerColl.getName(),
                let: {
                    myX: "$x",
                },
                pipeline: [
                    {$match: {$expr: {$eq: ["$x", "$$myX"]}}},
                ],
                as: "as"
            }
        };
let stageExplain = explainStage(lookupStage, "$lookup");
assert.eq(stageExplain.nReturned, 2, stageExplain);
// The two documents in the inner collection are scanned twice, leading to four docs examined in
// total.
assert.eq(stageExplain.collectionScans, 2, stageExplain);
assert.eq(stageExplain.totalDocsExamined, 4, stageExplain);
assert.eq(stageExplain.totalKeysExamined, 0, stageExplain);
assert.eq(stageExplain.indexesUsed, [], stageExplain);

const unionWithStage = {
    $unionWith: {
        coll: innerColl.getName(),
        pipeline: [
            {$match: {$expr: {$eq: ["$x", "foo"]}}},
        ]
    }
};

const nestedUnionWithStage = {
    $unionWith: {coll: innerColl.getName(), pipeline: [unionWithStage]}
};

// Explain of $unionWith when neither collection is sharded.
stageExplain = explainStage(unionWithStage, "$unionWith");
assert.eq(stageExplain.nReturned, 4, stageExplain);

// Explain of nested $unionWith when neither collection is sharded.
stageExplain = explainStage(nestedUnionWithStage, "$unionWith");
assert.eq(stageExplain.nReturned, 6, stageExplain);

// Shard the inner collection.
assert.commandWorked(innerColl.createIndex({y: 1, x: 1}));
st.shardColl(innerColl.getName(),
             {y: 1, x: 1} /* shard key */,
             {y: "b", x: "b"} /* split at */,
             {y: "c", x: "c"} /* move */,
             dbName,
             true);

// Explain of $lookup when outer collection is unsharded and inner collection is sharded.
stageExplain = explainStage(lookupStage, "$lookup");
assert.eq(stageExplain.nReturned, 2, stageExplain);
// Now that the inner collection is sharded, the execution of the $lookup requires dispatching
// commands across the wire for the inner collection. The runtime stats currently do not reflect the
// work done by these dispatched subcommands. We could improve this in the future to more accurately
// reflect docs examined, keys examined, collection scans, etc accrued when executing the
// subpipeline.
assert.eq(stageExplain.totalDocsExamined, 0, stageExplain);
assert.eq(stageExplain.totalKeysExamined, 0, stageExplain);
assert.eq(stageExplain.collectionScans, 0, stageExplain);
assert.eq(stageExplain.indexesUsed, [], stageExplain);

// Explain of $unionWith when outer collection is unsharded and inner collection is sharded.
stageExplain = explainStage(unionWithStage, "$unionWith");
assert.eq(stageExplain.nReturned, 4, stageExplain);
// The $unionWith explain format currently shows explains for the inner pipeline from both
// targeted shards.
assert(stageExplain.$unionWith.hasOwnProperty("pipeline"), stageExplain);
const pipelineExplain = stageExplain.$unionWith.pipeline;
assert(pipelineExplain.hasOwnProperty("shards"), stageExplain);
const shardNames = Object.keys(pipelineExplain.shards);
assert.eq(shardNames.length, 2, stageExplain);
// Each shard should have returned one document.
assert.eq(pipelineExplain.shards[shardNames[0]].executionStats.nReturned, 1, stageExplain);
assert.eq(pipelineExplain.shards[shardNames[1]].executionStats.nReturned, 1, stageExplain);

// Explain of nested $unionWith when outer collection is unsharded and inner collection is sharded.
stageExplain = explainStage(nestedUnionWithStage, "$unionWith");
assert.eq(stageExplain.nReturned, 6, stageExplain);

// Shard the outer collection.
st.shardColl(outerColl.getName(),
             {_id: 1} /* shard key */,
             {_id: 2} /* split at */,
             {_id: 3} /* move */,
             dbName,
             true);

// A variant of 'explainStage()' when the stage is expected to appear twice because it runs on
// two shards.
function explainStageTwoShards(stage, stageName) {
    const pipeline = [stage];
    let explain = outerColl.explain("executionStats").aggregate(pipeline);
    let stageExplain = getAggPlanStages(explain, stageName);
    assert.eq(stageExplain.length, 2, stageExplain);
    return stageExplain;
}

// Explain of $lookup when inner and outer collections are both sharded.
stageExplain = explainStageTwoShards(lookupStage, "$lookup");
for (let explain of stageExplain) {
    assert.eq(explain.nReturned, 1, stageExplain);
    // As above, the inner collection is sharded. We don't currently ship execution stats across the
    // wire alongside the query results themselves. As a result, the docs examined, total keys
    // examined, etc. will currently always be reported as zero when the inner collection is
    // sharded. We could improve this in the future to report the stats more accurately.
    assert.eq(explain.totalDocsExamined, 0, stageExplain);
    assert.eq(explain.totalKeysExamined, 0, stageExplain);
    assert.eq(explain.collectionScans, 0, stageExplain);
    assert.eq(explain.indexesUsed, [], stageExplain);
}

// Asserts that 'explain' is for a split pipeline with an empty shards part and a merger part with
// two stages. Asserts that the first merging stage is a $mergeCursors and then returns the second
// stage in the merging pipeline.
function getStageFromMergerPart(explain) {
    assert(explain.hasOwnProperty("splitPipeline"));
    assert(explain.splitPipeline.hasOwnProperty("shardsPart"));
    assert.eq(explain.splitPipeline.shardsPart, [], explain);
    assert(explain.splitPipeline.hasOwnProperty("mergerPart"));
    let mergerPart = explain.splitPipeline.mergerPart;
    assert.eq(mergerPart.length, 2, explain);
    assert(mergerPart[0].hasOwnProperty("$mergeCursors"), explain);
    return mergerPart[1];
}

function assertStageDoesNotHaveRuntimeStats(stageExplain) {
    assert(!stageExplain.hasOwnProperty("nReturned"), stageExplain);
    assert(!stageExplain.hasOwnProperty("totalDocsExamined"), stageExplain);
    assert(!stageExplain.hasOwnProperty("totalKeysExamined"), stageExplain);
    assert(!stageExplain.hasOwnProperty("collectionScans"), stageExplain);
    assert(!stageExplain.hasOwnProperty("indexesUsed"), stageExplain);
}

// Explain of $unionWith when inner and outer collections are both sharded. We expect the $unionWith
// to be part of the merging pipeline rather than pushed down to the shards.
let explain = outerColl.explain("executionStats").aggregate([unionWithStage]);
stageExplain = getStageFromMergerPart(explain);
assert(stageExplain.hasOwnProperty("$unionWith"), explain);
assertStageDoesNotHaveRuntimeStats(stageExplain);

// Nested $unionWith when inner and outer collections are sharded.
explain = outerColl.explain("executionStats").aggregate([nestedUnionWithStage]);
stageExplain = getStageFromMergerPart(explain);
assert(stageExplain.hasOwnProperty("$unionWith"), explain);
assertStageDoesNotHaveRuntimeStats(stageExplain);

// Drop and recreate the inner collection. Re-test when the outer collection is sharded but the
// inner collection is unsharded.
createInnerColl();

// Explain of $lookup when outer collection is sharded and inner collection is unsharded. In this
// case we expect the $lookup operation to execute on the primary shard as part of the merging
// pipeline.
explain = outerColl.explain("executionStats").aggregate([lookupStage]);
stageExplain = getStageFromMergerPart(explain);
assert(stageExplain.hasOwnProperty("$lookup"), explain);
assertStageDoesNotHaveRuntimeStats(stageExplain);

// Explain of $unionWith when the outer collection is sharded and the inner collection is unsharded.
explain = outerColl.explain("executionStats").aggregate([unionWithStage]);
stageExplain = getStageFromMergerPart(explain);
assert(stageExplain.hasOwnProperty("$unionWith"), explain);
assertStageDoesNotHaveRuntimeStats(stageExplain);

// Nested $unionWith when the outer collection is sharded and the inner collection is unsharded.
explain = outerColl.explain("executionStats").aggregate([nestedUnionWithStage]);
stageExplain = getStageFromMergerPart(explain);
assert(stageExplain.hasOwnProperty("$unionWith"), explain);
assertStageDoesNotHaveRuntimeStats(stageExplain);

st.stop();
