/**
 * Tests to verify that the current op output shows all the sub-pipelines of a $unionWith. In this
 * test we also validate that current op shows the expected stages and comment.
 */
(function() {
"use strict";

load("jstests/libs/fixture_helpers.js");  // For FixtureHelpers.
load("jstests/libs/curop_helpers.js");    // For waitForCurOpByFailPoint.

const st = new ShardingTest({shards: 2});
const testDB = st.s.getDB(jsTestName());
const shardedColl1 = testDB.shardedColl1;
const shardedColl2 = testDB.shardedColl2;
const unshardedColl = testDB.unsharded;
const shard0DB = st.shard0.getDB(jsTestName());
const shard1DB = st.shard1.getDB(jsTestName());

assert.commandWorked(st.s0.adminCommand({enableSharding: testDB.getName()}));
st.ensurePrimaryShard(testDB.getName(), st.shard0.shardName);

function setupShardColl(shardedColl) {
    // Shard shardedColl on {x:1}, split it at {x:0}, and move chunk {x:1} to shard1.
    st.shardColl(shardedColl, {x: 1}, {x: 0}, {x: 1});

    // Insert one document on each shard.
    assert.commandWorked(shardedColl.insert({x: 1, _id: 1}));
    assert.commandWorked(shardedColl.insert({x: -1, _id: 0}));
}

setupShardColl(shardedColl1);
setupShardColl(shardedColl2);
assert.commandWorked(unshardedColl.insert({x: 1, _id: 1}));

const kFailPointName = "waitAfterCommandFinishesExecution";
function setPostCommandFailpointOnShards({mode, options}) {
    FixtureHelpers.runCommandOnEachPrimary({
        db: testDB.getSiblingDB("admin"),
        cmdObj: {configureFailPoint: kFailPointName, data: options, mode: mode}
    });
}

function runTest({command, expectedRunningOps, collToPause}) {
    setPostCommandFailpointOnShards(
        {mode: "alwaysOn", options: {ns: collToPause.getFullName(), commands: ['aggregate']}});

    const commentObj = {
        testName: jsTestName() + "_comment",
        commentField: "comment_aggregate",
        uuid: UUID().hex()
    };
    command["comment"] = commentObj;

    const parallelFunction = `
        const sourceDB = db.getSiblingDB(jsTestName());
        let cmdRes = sourceDB.runCommand(${tojson(command)});
        assert.commandWorked(cmdRes);
    `;

    // Run the 'command' in a parallel shell.
    let unionShell = startParallelShell(parallelFunction, st.s.port);

    const filter = {"command.aggregate": collToPause.getName(), "command.comment": commentObj};
    const deepestUnion = expectedRunningOps[expectedRunningOps.length - 1];
    // Wait for the parallel shell to hit the failpoint and verify that the 'comment' field is
    // present in $currentOp.
    assert.soon(() => {
        const results =
            waitForCurOpByFailPoint(testDB, collToPause.getFullName(), kFailPointName, filter);
        return results.length == deepestUnion.count;
    });

    // Verify that MongoS has an operation running for the base aggregation.
    filter['command.aggregate'] = expectedRunningOps[0].coll.getName();
    const mongosOp = testDB.getSiblingDB("admin")
                         .aggregate([{$currentOp: {localOps: true}}, {$match: filter}])
                         .toArray();
    assert.eq(mongosOp.length, 1, mongosOp);
    for (let expectedOp of expectedRunningOps) {
        let filterForOp = {};
        filterForOp['command.aggregate'] = expectedOp.coll.getName();
        for (let stage of expectedOp.stages) {
            filterForOp['command.pipeline.' + stage] = {$exists: true};
        }
        filterForOp['command.comment'] = commentObj;

        assert.eq(testDB.getSiblingDB("admin")
                      .aggregate([{$currentOp: {localOps: false}}, {$match: filterForOp}])
                      .toArray()
                      .length,
                  expectedOp.count,
                  testDB.getSiblingDB("admin")
                      .aggregate([{$currentOp: {localOps: false}}, {$match: filterForOp}])
                      .toArray());
    }

    // Unset the failpoint to unblock the command and join with the parallel shell.
    setPostCommandFailpointOnShards({mode: "off", options: {}});
    unionShell();
}

// Test that the current op shows all the sub-pipelines when a sharded collection unions with
// another sharded collection. Also validate that the 'comment' is attached to all the related
// operations.
runTest({
    command: {
        aggregate: shardedColl1.getName(),
        pipeline: [
            {$match: {p: null}},
            {
                $unionWith: {
                    coll: shardedColl2.getName(),
                    pipeline: [{$group: {_id: "$x"}}, {$project: {_id: 1}}]
                }
            }
        ],
        cursor: {}
    },
    // We expect to see the merging half of the pipeline still running because the $unionWith hasn't
    // finished, even though we should have exhaused the input cursors by the time we're looking at
    // the union sub-pipeline.
    expectedRunningOps: [
        {coll: shardedColl1, count: 1, stages: ['$mergeCursors', '$unionWith']},
        {coll: shardedColl2, count: 2, stages: ['$group']}
    ],
    collToPause: shardedColl2
});

// Test that the current op shows all the sub-pipelines when a sharded collection unions with an
// unsharded collection. Also validate that the 'comment' is attached to all the related
// operations.
runTest({
    command: {
        aggregate: shardedColl1.getName(),
        pipeline: [
            {$match: {p: null}},
            {$unionWith: {coll: unshardedColl.getName(), pipeline: [{$group: {_id: "$x"}}]}}
        ],
        cursor: {}
    },
    expectedRunningOps: [
        {coll: shardedColl1, count: 1, stages: ['$mergeCursors', '$unionWith']},
        {coll: unshardedColl, count: 1, stages: ['$group']}
    ],
    collToPause: unshardedColl
});

// Test that the current op shows all the sub-pipelines when a sharded collection unions with an
// unsharded collection which in-turn unions with another sharded collection. Also validate that
// the 'comment' is attached to all the related operations.
runTest({
    command: {
        aggregate: shardedColl1.getName(),
        pipeline: [
            {$match: {p: null}},
            {
                $unionWith: {
                    coll: unshardedColl.getName(),
                    pipeline: [{
                        $unionWith:
                            {coll: shardedColl2.getName(), pipeline: [{$group: {_id: "$x"}}]}
                    }]
                }
            }
        ],
        cursor: {}
    },
    expectedRunningOps: [
        {coll: shardedColl1, count: 1, stages: ['$mergeCursors', '$unionWith']},
        {coll: unshardedColl, count: 1, stages: ['$unionWith']},
        {coll: shardedColl2, count: 2, stages: ['$group']}
    ],
    collToPause: shardedColl2
});

// Test that the current op shows all the sub-pipelines when an unsharded collection unions with a
// sharded collection which in-turn unions with another sharded collection. Also validate that the
// 'comment' is attached to all the related operations.
runTest({
    command: {
        aggregate: unshardedColl.getName(),
        pipeline: [
            {$match: {p: null}},
            {
                $unionWith: {
                    coll: shardedColl1.getName(),
                    pipeline: [{
                        $unionWith:
                            {coll: shardedColl2.getName(), pipeline: [{$group: {_id: "$x"}}]}
                    }]
                }
            }
        ],
        cursor: {}
    },
    expectedRunningOps: [
        {coll: unshardedColl, count: 1, stages: ['$unionWith']},
        // Note that we don't expect any operation on 'shardedColl1' since the failpoint being used
        // will only block commands on the "deepest" namespace, which is 'shardedColl2' for this
        // command. The cursors on 'shardedColl1' will have been exhausted by the time the command
        // on 'shardedColl2' is started. Note there will still be a $mergeCursors stage around that
        // exhausted those cursors, but it will not get its own current op entry because it's a
        // sub-pipeline on the same host and operation as the pipeline for unshardedColl.
        {coll: shardedColl2, count: 2, stages: ['$group']}
    ],
    collToPause: shardedColl2
});

st.stop();
})();
