/**
 * Tests aggregate expression counters.
 * @tags: [
 * ]
 */

import {assertDocumentValidationFailure} from "jstests/libs/doc_validation_utils.js";

const mongod = MongoRunner.runMongod();
const db = mongod.getDB(jsTest.name());
const collName = jsTest.name();
const coll = db[collName];
coll.drop();

for (let i = 0; i < 3; i++) {
    assert.commandWorked(coll.insert({
        _id: i,
        x: i,
        "a$b": "foo",
        "a.b": "bar",
    }));
}

/**
 * Execute the `command` and compare the operator counters with the expected values.
 * @param {*} command to be executed.
 * @param {*} expectedCounters  expected operator counters.
 */
function checkCounters(command, expectedCounters) {
    const origCounters = db.serverStatus().metrics.operatorCounters.expressions;

    command();

    const newCounters = db.serverStatus().metrics.operatorCounters.expressions;
    let actualCounters = {};
    for (let ec in origCounters) {
        const diff = newCounters[ec] - origCounters[ec];
        if (diff !== 0) {
            actualCounters[ec] = diff;
        }
    }

    assert.docEq(expectedCounters, actualCounters);
}

/**
 * Check operator counters in the `find`.
 * @param {*} query to be executed.
 * @param {*} expectedCounters - expected operator counters.
 * @param {*} expectedCount - expected number of records returned by the `find` function.
 */
function checkFindCounters(query, expectedCounters, expectedCount) {
    checkCounters(() => assert.eq(expectedCount, coll.find(query).itcount()), expectedCounters);
}

/**
 * Check operator counters in the `aggregate`.
 * @param {*} pipeline to be executed.
 * @param {*} expectedCounters - expected operator counters.
 * @param {*} expectedCount - expected number of records returned by the `find` function.
 */
function checkAggregationCounters(pipeline, expectedCounters, expectedCount) {
    checkCounters(() => assert.eq(expectedCount, coll.aggregate(pipeline).itcount()),
                  expectedCounters);
}

// Find.
checkFindCounters({$expr: {$eq: [{$getField: "a$b"}, "foo"]}}, {"$getField": 1, "$eq": 1}, 3);

// Update.
checkCounters(() => assert.commandWorked(coll.update(
                  {_id: 0, $expr: {$eq: [{$getField: {field: "a$b", input: "$$ROOT"}}, "foo"]}},
                  {$set: {y: 10}})),
              {"$getField": 1, "$eq": 1});

checkCounters(
    () => assert.commandWorked(coll.update(
        {_id: 1}, [{$replaceWith: {$setField: {field: "a.b", input: "$$ROOT", value: "qqq"}}}])),
    {"$setField": 1});

checkCounters(() => assert.commandWorked(db.runCommand({
    update: coll.getName(),
    updates: [{
        q: {_id: 1},
        u: [{$replaceWith: {$setField: {field: "a.b", input: "$$ROOT", value: "zzz"}}}],
        upsert: false
    }]
})),
              {"$setField": 1});

// Delete.
checkCounters(() => assert.commandWorked(db.runCommand({
    delete: coll.getName(),
    deletes: [{q: {"_id": {$gt: 1}, $expr: {$eq: [{$getField: "a$b"}, "foo"]}}, limit: 1}]
})),
              {"$getField": 1, "$eq": 1});

// In aggregation pipeline.
let pipeline = [{$project: {_id: 1, test: {$getField: "a$b"}}}];
checkAggregationCounters(pipeline, {"$getField": 1}, 2);

pipeline = [{$match: {_id: 1, $expr: {$eq: [{$getField: "a$b"}, "foo"]}}}];
checkAggregationCounters(pipeline, {"$getField": 1, "$eq": 1}, 1);

pipeline =
    [{$match: {_id: 1, $expr: {$eq: [{$getField: {field: "a$b", input: {"a$b": "b"}}}, "b"]}}}];
checkAggregationCounters(pipeline, {"$getField": 1, "$eq": 1}, 1);

pipeline = [{
    $project: {_id: 1, test: {$setField: {field: {$const: "a.b"}, input: "$$ROOT", value: "barrr"}}}
}];
checkAggregationCounters(pipeline, {"$setField": 1, "$const": 1}, 2);

// With sub-pipeline.
const testColl = db.operator_counters_expressions2;

function initTestColl(i) {
    testColl.drop();
    assert.commandWorked(testColl.insert({
        _id: i,
        x: i,
        "a$b": "bar",
    }));
}

initTestColl(5);
pipeline = [
    {$project: {x: 1, _id: 0, test: {$getField: "a$b"}}},
    {
        $unionWith: {
            coll: testColl.getName(),
            pipeline: [{$project: {x: 1, _id: 0, test: {$getField: "a$b"}}}]
        }
    }
];
checkAggregationCounters(pipeline, {"$getField": 1}, 3);

initTestColl(5);
pipeline = [
    {$lookup: {
        from: testColl.getName(),
        pipeline: [{$project: {x: 1, _id: 0, test: {$getField: "a$b"}}}],
        as: "joinedField"
}}];
checkAggregationCounters(pipeline, {"$getField": 1}, 2);

initTestColl(1);
let mergePipeline =
    [{$project: {test: {$setField: {field: "a$b", input: "$$ROOT", value: "merged"}}}}];
pipeline = [{
    $merge:
        {into: testColl.getName(), on: "_id", whenMatched: mergePipeline, whenNotMatched: "insert"}
}];
checkCounters(() => {
    coll.aggregate(pipeline).itcount();
    assert.eq(2, testColl.find().itcount());
}, {"$setField": 1});

// Expressions in view pipeline.
db.view.drop();
let viewPipeline = [{$match: {$expr: {$eq: [{$getField: "a.b"}, "bar"]}}}];
assert.commandWorked(
    db.runCommand({create: "view", viewOn: coll.getName(), pipeline: viewPipeline}));
checkCounters(() => db.view.find().itcount(), {"$getField": 1, "$eq": 1});

// Expressions in document validator.
checkCounters(() => {
    initTestColl(1);
    assert.commandWorked(db.runCommand(
        {"collMod": testColl.getName(), "validator": {$expr: {$eq: [{$getField: "a$b"}, "new"]}}}));
}, {"$getField": 1, "$eq": 1});

// Expression counter is not incremented for each validated document.
checkCounters(() => {
    assert.commandWorked(testColl.insert({
        _id: 2,
        x: 2,
        "a$b": "new",
    }));
    assertDocumentValidationFailure(testColl.insert({
        _id: 3,
        x: 3,
        "a$b": "invalid",
    }),
                                    testColl);
}, {});

// $cond
pipeline = [{$project: {item: 1, discount: {$cond: {if: {$gte: ["$x", 1]}, then: 10, else: 0}}}}];
checkAggregationCounters(pipeline, {"$cond": 1, "$gte": 1}, 2);

// $ifNull
pipeline = [{$project: {description: {$ifNull: ["$description", "Unspecified"]}}}];
checkAggregationCounters(pipeline, {"$ifNull": 1}, 2);

// $divide, $switch
let query = {
    $expr: {
        $eq: [
            {
                $switch: {
                    branches: [{case: {$gt: ["$x", 0]}, then: {$divide: ["$x", 2]}}],
                    default: {$subtract: [100, "$x"]}
                }
            },
            100
        ]
    }
};
checkFindCounters(query, {"$divide": 1, "$subtract": 1, "$eq": 1, "$gt": 1, "$switch": 1}, 1);

// $cmp, $exp, $abs, $range
pipeline = [{
    $project: {
        cmpField: {$cmp: ["$x", 250]},
        expField: {$exp: "$x"},
        absField: {$abs: "$x"},
        rangeField: {$range: [0, "$x", 25]}
    }
}];
checkAggregationCounters(pipeline, {"$abs": 1, "$cmp": 1, "$exp": 1, "$range": 1}, 2);

// $or
pipeline = [{$match: {$expr: {$or: [{$eq: ["$_id", 0]}, {$eq: ["$x", 1]}]}}}];
checkAggregationCounters(pipeline, {"$eq": 2, "$or": 1}, 2);

// $dateFromParts
pipeline =
    [{$project: {date: {$dateFromParts: {'year': 2021, 'month': 10, 'day': {$add: ['$x', 10]}}}}}];
checkAggregationCounters(pipeline, {"$add": 1, "$dateFromParts": 1}, 2);

// $concat
pipeline = [{$project: {mystring: {$concat: [{$getField: "a$b"}, {$getField: "a.b"}]}}}];
checkAggregationCounters(pipeline, {"$concat": 1, "$getField": 2}, 2);

// $toDouble
pipeline = [{$project: {doubleval: {$toDouble: "$_id"}}}];
checkAggregationCounters(pipeline, {"$toDouble": 1}, 2);

// $setIntersection
pipeline = [{$project: {intersection: {$setIntersection: [[1, 2, 3], [3, 2]]}}}];
checkAggregationCounters(pipeline, {"$setIntersection": 1}, 2);

// Expressions in bulk operations.
const bulkColl = db.operator_counters_expressions3;
for (let i = 0; i < 3; i++) {
    assert.commandWorked(bulkColl.insert({_id: i, x: i}));
}
const bulkOp = bulkColl.initializeUnorderedBulkOp();
bulkOp.find({$expr: {$eq: ["$x", 2]}}).update({$set: {x: 10}});
bulkOp.find({$expr: {$lt: ["$x", 1]}}).remove();
checkCounters(() => assert.commandWorked(bulkOp.execute()), {"$eq": 1, "$lt": 1});

MongoRunner.stopMongod(mongod);