/**
 * Tests basic support for sampling read queries against a sharded collection on a sharded cluster.
 *
 * @tags: [requires_fcv_70]
 */
import {ShardingTest} from "jstests/libs/shardingtest.js";
import {QuerySamplingUtil} from "jstests/sharding/analyze_shard_key/libs/query_sampling_util.js";

// Make the periodic jobs for refreshing sample rates and writing sampled queries and diffs have a
// period of 1 second to speed up the test.
const queryAnalysisWriterIntervalSecs = 1;
const queryAnalysisSamplerConfigurationRefreshSecs = 1;
const st = new ShardingTest({
    shards: 3,
    rs: {
        nodes: 2,
        setParameter: {
            queryAnalysisSamplerConfigurationRefreshSecs,
            queryAnalysisWriterIntervalSecs,
            logComponentVerbosity: tojson({sharding: 2})
        }
    },
    mongosOptions: {setParameter: {queryAnalysisSamplerConfigurationRefreshSecs}}
});

const dbName = "testDb";
const collName = "testColl";
const ns = dbName + "." + collName;
const mongosDB = st.s.getDB(dbName);
const mongosColl = mongosDB.getCollection(collName);

// Make the collection have three chunks:
// shard0: [MinKey, 0]
// shard1: [0, 1000]
// shard1: [1000, MaxKey]
assert.commandWorked(st.s.adminCommand({enableSharding: dbName, primaryShard: st.shard0.name}));
assert.commandWorked(st.s.adminCommand({shardCollection: ns, key: {x: 1}}));
assert.commandWorked(st.s.adminCommand({split: ns, middle: {x: 0}}));
assert.commandWorked(st.s.adminCommand({split: ns, middle: {x: 1000}}));
assert.commandWorked(st.s.adminCommand({moveChunk: ns, find: {x: 0}, to: st.shard1.shardName}));
assert.commandWorked(st.s.adminCommand({moveChunk: ns, find: {x: 1000}, to: st.shard2.name}));
const collectionUuid = QuerySamplingUtil.getCollectionUuid(mongosDB, collName);

assert.commandWorked(
    st.s.adminCommand({configureQueryAnalyzer: ns, mode: "full", samplesPerSecond: 1000}));
QuerySamplingUtil.waitForActiveSamplingShardedCluster(st, ns, collectionUuid);

const expectedSampledQueryDocs = [];

// Make each read below have a unique filter and use that to look up the corresponding
// config.sampledQueries document later.
function runCmd(makeCmdObjFunc, filter, shardNames, explain, expectFilterCaptured = true) {
    const collation = QuerySamplingUtil.generateRandomCollation();
    const originalCmdObj = makeCmdObjFunc(filter, collation);
    const cmdName = Object.keys(originalCmdObj)[0];

    assert.commandWorked(mongosDB.runCommand(explain ? {explain: originalCmdObj} : originalCmdObj));
    // 'explain' queries should not get sampled.
    if (!explain) {
        const expectedFilter = expectFilterCaptured ? filter : {};
        expectedSampledQueryDocs.push({
            filter: {"cmd.filter": expectedFilter},
            cmdName,
            cmdObj: {filter: expectedFilter, collation},
            shardNames
        });
    }
}

{
    // Run find commands.
    const makeCmdObjFunc = (filter, collation) => {
        return {
            find: collName,
            filter,
            collation,
        };
    };

    const filter0 = {x: 1};
    const shardNames0 = [st.rs1.name];
    runCmd(makeCmdObjFunc, filter0, shardNames0, false /* explain */);

    const filter1 = {x: {$gte: 2}};
    const shardNames1 = [st.rs1.name, st.rs2.name];
    runCmd(makeCmdObjFunc, filter1, shardNames1, false /* explain */);

    const filter2 = {x: 3};
    const shardNames2 = [];
    runCmd(makeCmdObjFunc, filter2, shardNames2, true /* explain */);

    const filter3 = {x: {$gte: 4}};
    const shardNames3 = [];
    runCmd(makeCmdObjFunc, filter3, shardNames3, true /* explain */);
}

{
    // Run count commands.
    const makeCmdObjFunc = (filter, collation) => {
        return {
            count: collName,
            query: filter,
            collation,
        };
    };

    const filter0 = {x: 5};
    const shardNames0 = [st.rs1.name];
    runCmd(makeCmdObjFunc, filter0, shardNames0, false /* explain */);

    const filter1 = {x: {$gte: 6}};
    const shardNames1 = [st.rs1.name, st.rs2.name];
    runCmd(makeCmdObjFunc, filter1, shardNames1, false /* explain */);

    const filter2 = {x: 7};
    const shardNames2 = [];
    runCmd(makeCmdObjFunc, filter2, shardNames2, true /* explain */);

    const filter3 = {x: {$gte: 8}};
    const shardNames3 = [];
    runCmd(makeCmdObjFunc, filter3, shardNames3, true /* explain */);
}

{
    // Run distinct commands.
    const makeCmdObjFunc = (filter, collation) => {
        return {
            distinct: collName,
            key: "x",
            query: filter,
            collation,
        };
    };

    const filter0 = {x: 9};
    const shardNames0 = [st.rs1.name];
    runCmd(makeCmdObjFunc, filter0, shardNames0, false /* explain */);

    const filter1 = {x: {$gte: 10}};
    const shardNames1 = [st.rs1.name, st.rs2.name];
    runCmd(makeCmdObjFunc, filter1, shardNames1, false /* explain */);

    const filter2 = {x: 11};
    const shardNames2 = [];
    runCmd(makeCmdObjFunc, filter2, shardNames2, true /* explain */);

    const filter3 = {x: {$gte: 12}};
    const shardNames3 = [];
    runCmd(makeCmdObjFunc, filter3, shardNames3, true /* explain */);
}

{
    // Run aggregate commands with filter in the first stage ($match).
    const makeCmdObjFunc = (filter, collation) => {
        return {aggregate: collName, pipeline: [{$match: filter}], collation, cursor: {}};
    };

    const filter0 = {x: 13};
    const shardNames0 = [st.rs1.name];
    runCmd(makeCmdObjFunc, filter0, shardNames0, false /* explain */);

    const filter1 = {x: {$gte: 14}};
    const shardNames1 = [st.rs1.name, st.rs2.name];
    runCmd(makeCmdObjFunc, filter1, shardNames1, false /* explain */);

    const filter2 = {x: 15};
    const shardNames2 = [];
    runCmd(makeCmdObjFunc, filter2, shardNames2, true /* explain */);

    const filter3 = {x: {$gte: 16}};
    const shardNames3 = [];
    runCmd(makeCmdObjFunc, filter3, shardNames3, true /* explain */);
}

{
    // Run aggregate commands with filter in the first stage ($geoNear).
    const makeCmdObjFunc = (filter, collation) => {
        const geoNearStage = {
            $geoNear: {near: {type: "Point", coordinates: [1, 1]}, distanceField: "dist"}
        };
        if (filter !== null) {
            geoNearStage.$geoNear.query = filter;
        }
        return {aggregate: collName, pipeline: [geoNearStage], collation, cursor: {}};
    };

    assert.commandWorked(mongosColl.createIndex({x: "2dsphere"}));

    const filter0 = {x: 17};
    const shardNames0 = [st.rs1.name];
    runCmd(makeCmdObjFunc, filter0, shardNames0, false /* explain */);

    const filter1 = {x: {$gte: 18}};
    const shardNames1 = [st.rs1.name, st.rs2.name];
    runCmd(makeCmdObjFunc, filter1, shardNames1, false /* explain */);

    const filter2 = {x: 19};
    const shardNames2 = [];
    runCmd(makeCmdObjFunc, filter2, shardNames2, true /* explain */);

    const filter3 = {x: {$gte: 20}};
    const shardNames3 = [];
    runCmd(makeCmdObjFunc, filter3, shardNames3, true /* explain */);
}

{
    // Run aggregate commands with filter in a non-first but moveable stage ($match).
    const makeCmdObjFunc = (filter, collation) => {
        return {
            aggregate: collName,
            pipeline: [{$sort: {x: -1}}, {$match: filter}],
            collation,
            cursor: {}
        };
    };

    const filter0 = {x: 21};
    const shardNames0 = [st.rs1.name];
    runCmd(makeCmdObjFunc, filter0, shardNames0, false /* explain */);

    const filter1 = {x: {$gte: 22}};
    const shardNames1 = [st.rs1.name, st.rs2.name];
    runCmd(makeCmdObjFunc, filter1, shardNames1, false /* explain */);

    const filter2 = {x: 23};
    const shardNames2 = [];
    runCmd(makeCmdObjFunc, filter2, shardNames2, true /* explain */);

    const filter3 = {x: {$gte: 24}};
    const shardNames3 = [];
    runCmd(makeCmdObjFunc, filter3, shardNames3, true /* explain */);
}

{
    // Run aggregate commands with filter in a non-first and non-moveable stage ($match).
    const makeCmdObjFunc = (filter, collation) => {
        return {
            aggregate: collName,
            pipeline: [{$_internalInhibitOptimization: {}}, {$match: filter}],
            collation,
            cursor: {}
        };
    };

    // The filter can't be moved up so the commands below don't have an initial filter.
    const expectFilterCaptured = false;

    const filter0 = {x: 25};
    const shardNames0 = [st.rs0.name, st.rs1.name, st.rs2.name];
    runCmd(makeCmdObjFunc, filter0, shardNames0, false /* explain */, expectFilterCaptured);

    const filter1 = {x: 26};
    const shardNames1 = [];
    runCmd(makeCmdObjFunc, filter1, shardNames1, true /* explain */, expectFilterCaptured);

    const filter2 = {x: {$gte: 27}};
    const shardNames2 = [];
    runCmd(makeCmdObjFunc, filter2, shardNames2, true /* explain */, expectFilterCaptured);
}

const cmdNames = ["find", "count", "distinct", "aggregate"];
QuerySamplingUtil.assertSoonSampledQueryDocumentsAcrossShards(
    st, ns, collectionUuid, cmdNames, expectedSampledQueryDocs);

assert.commandWorked(st.s.adminCommand({configureQueryAnalyzer: ns, mode: "off"}));

st.stop();