/**
 * Tests that if a query has an extractable limit, we send a search command to mongot with that
 * information in the docsRequested field.
 * All tests are skipped if featureFlagSearchBatchSizeTuning is enabled, since this file only tests
 * the docsRequested options, whereas that flag enables the batchSize option.
 * TODO SERVER-92576 Remove this test when featureFlagSearchBatchSizeLimit is removed.
 * @tags: [requires_fcv_71]
 */
import {FeatureFlagUtil} from "jstests/libs/feature_flag_util.js";
import {checkSbeRestrictedOrFullyEnabled} from "jstests/libs/query/sbe_util.js";
import {getUUIDFromListCollections} from "jstests/libs/uuid_util.js";
import {
    getDefaultProtocolVersionForPlanShardedSearch,
    mockPlanShardedSearchResponse,
    mongotCommandForQuery,
    MongotMock
} from "jstests/with_mongot/mongotmock/lib/mongotmock.js";
import {
    ShardingTestWithMongotMock
} from "jstests/with_mongot/mongotmock/lib/shardingtest_with_mongotmock.js";

const dbName = "test";
const collName = "search_docsrequested";
const chunkBoundary = 8;
const protocolVersion = getDefaultProtocolVersionForPlanShardedSearch();

const docs = [
    {"_id": 1, "title": "cakes"},
    {"_id": 2, "title": "cookies and cakes"},
    {"_id": 3, "title": "vegetables"},
    {"_id": 4, "title": "oranges"},
    {"_id": 5, "title": "cakes and oranges"},
    {"_id": 6, "title": "cakes and apples"},
    {"_id": 7, "title": "apples"},
    {"_id": 8, "title": "cakes and xyz"},
    {"_id": 9, "title": "cakes and blueberries"},
    {"_id": 10, "title": "cakes and strawberries"},
    {"_id": 11, "title": "cakes and raspberries"},
    {"_id": 12, "title": "cakes and cakes"},
    {"_id": 13, "title": "cakes and elderberries"},
    {"_id": 14, "title": "cakes and carrots"},
    {"_id": 15, "title": "cakes and more cakes"},
    {"_id": 16, "title": "cakes and even more cakes"},
];

const foreignCollectionDocs = [
    {"_id": 1, "fruit": "raspberries"},
    {"_id": 2, "fruit": "blueberries"},
    {"_id": 3, "fruit": "strawberries"},
    {"_id": 4, "fruit": "gooseberries"},
    {"_id": 5, "fruit": "mangos"},
];
const foreignCollName = "fruits";
const foreignChunkBoundary = 3;

const searchQuery = {
    query: "cakes",
    path: "title"
};

// All the documents that would be returned by the search query above.
let relevantDocs = [];
let relevantSearchDocs = [];
let relevantSearchDocsShard0 = [];
let relevantSearchDocsShard1 = [];
let searchScore = 0.300;
for (let i = 0; i < docs.length; i++) {
    if (docs[i]["title"].includes(searchQuery.query)) {
        relevantDocs.push(docs[i]);

        // Standalone case.
        relevantSearchDocs.push({_id: docs[i]._id, $searchScore: searchScore});

        // Sharded environment case.
        if (docs[i]._id < chunkBoundary) {
            relevantSearchDocsShard0.push({_id: docs[i]._id, $searchScore: searchScore});
        } else {
            relevantSearchDocsShard1.push({_id: docs[i]._id, $searchScore: searchScore});
        }

        // The documents with lower _id will have a higher search score.
        searchScore = searchScore - 0.001;
    }
}
assert.eq(13, relevantSearchDocs.length);
assert.eq(4, relevantSearchDocsShard0.length);
assert.eq(9, relevantSearchDocsShard1.length);

// Mongot may return slightly more documents than mongod requests as an optimization for the case
// when $idLookup filters out some of them.
function calcNumDocsMongotShouldReturn(extractedLimit) {
    return Math.max(Math.ceil(1.064 * extractedLimit), 10);
}

function buildHistoryStandalone(coll, collUUID, extractedLimit, mongotConn) {
    let mongotReturnedDocs = calcNumDocsMongotShouldReturn(extractedLimit);
    {
        const cursorId = NumberLong(123);
        const history = [
            {
                expectedCommand: mongotCommandForQuery({
                    query: searchQuery,
                    collName: collName,
                    db: dbName,
                    collectionUUID: collUUID,
                    cursorOptions: {docsRequested: NumberInt(extractedLimit)}
                }),
                response: {
                    cursor: {
                        id: NumberLong(0),
                        ns: coll.getFullName(),
                        nextBatch: relevantSearchDocs.slice(0, mongotReturnedDocs),
                    },
                    ok: 1
                }
            },
        ];
        assert.commandWorked(
            mongotConn.adminCommand({setMockResponses: 1, cursorId: cursorId, history: history}));
    }
}

function buildHistoryShardedEnv(coll, collUUID, extractedLimit, stWithMock) {
    let mongotReturnedDocs = calcNumDocsMongotShouldReturn(extractedLimit);
    {
        const cursorId = NumberLong(123);
        const metaId = NumberLong(2);

        // Set history for shard 0.
        const history0 = [
            {
                expectedCommand: mongotCommandForQuery({
                    query: searchQuery,
                    collName: collName,
                    db: dbName,
                    collectionUUID: collUUID,
                    protocolVersion: protocolVersion,
                    cursorOptions: {docsRequested: NumberInt(extractedLimit)}
                }),
                response: {
                    cursors: [
                        {
                            cursor: {
                                id: NumberLong(0),
                                type: "results",
                                ns: coll.getFullName(),
                                nextBatch: relevantSearchDocsShard0.slice(0, mongotReturnedDocs),
                            },
                            ok: 1
                        },
                        {
                            cursor: {
                                id: NumberLong(0),
                                ns: coll.getFullName(),
                                type: "meta",
                                nextBatch: [{metaVal: 1}],
                            },
                            ok: 1
                        }
                    ]

                }
            },
        ];
        const s0Mongot = stWithMock.getMockConnectedToHost(stWithMock.st.rs0.getPrimary());
        s0Mongot.setMockResponses(history0, cursorId, metaId);

        // Set history for shard 1.
        const history1 = [
            {
                expectedCommand: mongotCommandForQuery({
                    query: searchQuery,
                    collName: collName,
                    db: dbName,
                    collectionUUID: collUUID,
                    protocolVersion: protocolVersion,
                    cursorOptions: {docsRequested: NumberInt(extractedLimit)}
                }),
                response: {
                    cursors: [
                        {
                            cursor: {
                                id: NumberLong(0),
                                type: "results",
                                ns: coll.getFullName(),
                                nextBatch: relevantSearchDocsShard1.slice(0, mongotReturnedDocs),
                            },
                            ok: 1
                        },
                        {
                            cursor: {
                                id: NumberLong(0),
                                ns: coll.getFullName(),
                                type: "meta",
                                nextBatch: [{metaVal: 2}],
                            },
                            ok: 1
                        }
                    ]
                }
            },
        ];
        const s1Mongot = stWithMock.getMockConnectedToHost(stWithMock.st.rs1.getPrimary());
        s1Mongot.setMockResponses(history1, cursorId, metaId);

        mockPlanShardedSearchResponse(
            collName, searchQuery, dbName, undefined /*sortSpec*/, stWithMock);
    }
}

function runAndAssert(
    pipeline, extractedLimit, expectedResults, coll, collUUID, standaloneConn, stConn) {
    // Only one of standaloneConn and stConn can be non-null.
    if (standaloneConn != null) {
        assert(stConn == null);
        buildHistoryStandalone(coll, collUUID, extractedLimit, standaloneConn);
    } else {
        assert(standaloneConn == null);
        buildHistoryShardedEnv(coll, collUUID, extractedLimit, stConn);
    }
    let cursor = coll.aggregate(pipeline);
    assert.eq(expectedResults, cursor.toArray());
}

// The extractable limit optimization cannot be done if there is a stage between $search and
// $limit that would change the number of documents, such as $match. Thus, there should be no
// 'docsRequested' field in the command sent to mongot in this case.
function expectNoDocsRequestedInCommand(coll, collUUID, mongotConn, stWithMock) {
    let pipeline = [{$search: searchQuery}, {$match: {title: {$regex: "more cakes"}}}, {$limit: 1}];
    let cursorId = NumberLong(123);

    // Only one of mongotConn and stWithMock can be non-null.
    if (mongotConn != null) {
        assert(stWithMock == null);
        const history = [
            {
                expectedCommand: {
                    search: coll.getName(),
                    collectionUUID: collUUID,
                    query: searchQuery,
                    $db: dbName,
                },
                response: {
                    cursor: {
                        id: NumberLong(0),
                        ns: coll.getFullName(),
                        nextBatch: [
                            {_id: 15, $searchScore: 0.789},
                            {_id: 16, $searchScore: 0.123},
                        ]
                    },
                    ok: 1
                }
            },
        ];
        assert.commandWorked(
            mongotConn.adminCommand({setMockResponses: 1, cursorId: cursorId, history: history}));
    } else {
        assert(mongotConn == null);
        const metaId = NumberLong(2);
        // Set history for shard 0.
        const history0 = [
            {
                expectedCommand: {
                    search: coll.getName(),
                    collectionUUID: collUUID,
                    query: searchQuery,
                    $db: dbName,
                    intermediate: protocolVersion
                },
                response: {
                    cursors: [
                        {
                            cursor: {
                                id: NumberLong(0),
                                type: "results",
                                ns: coll.getFullName(),
                                nextBatch: [],
                            },
                            ok: 1
                        },
                        {
                            cursor: {
                                id: NumberLong(0),
                                ns: coll.getFullName(),
                                type: "meta",
                                nextBatch: [{metaVal: 1}],
                            },
                            ok: 1
                        }
                    ]
                }
            },
        ];
        const s0Mongot = stWithMock.getMockConnectedToHost(stWithMock.st.rs0.getPrimary());
        s0Mongot.setMockResponses(history0, cursorId, metaId);

        // Set history for shard 1.
        const history1 = [
            {
                expectedCommand: {
                    search: coll.getName(),
                    collectionUUID: collUUID,
                    query: searchQuery,
                    $db: dbName,
                    intermediate: protocolVersion
                },
                response: {
                    cursors: [
                        {
                            cursor: {
                                id: NumberLong(0),
                                type: "results",
                                ns: coll.getFullName(),
                                nextBatch: [
                                    {_id: 15, $searchScore: 0.789},
                                    {_id: 16, $searchScore: 0.123},
                                ]
                            },
                            ok: 1
                        },
                        {
                            cursor: {
                                id: NumberLong(0),
                                ns: coll.getFullName(),
                                type: "meta",
                                nextBatch: [{metaVal: 1}],
                            },
                            ok: 1
                        }
                    ]
                }
            },
        ];
        const s1Mongot = stWithMock.getMockConnectedToHost(stWithMock.st.rs1.getPrimary());
        s1Mongot.setMockResponses(history1, cursorId, metaId);

        mockPlanShardedSearchResponse(
            collName, searchQuery, dbName, undefined /*sortSpec*/, stWithMock);
    }
    let cursor = coll.aggregate(pipeline);
    const expected = [
        {"_id": 15, "title": "cakes and more cakes"},
    ];
    assert.eq(expected, cursor.toArray());
}

// Perform a $search query where $$SEARCH_META is referenced after a $limit stage.
function searchMetaAfterLimit(coll, collUUID, stWithMock) {
    let st = stWithMock.st;
    let limit = 3;
    let mongotReturnedDocs = calcNumDocsMongotShouldReturn(limit);

    let pipeline =
        [{$search: searchQuery}, {$limit: limit}, {$project: {_id: 1, meta: "$$SEARCH_META"}}];
    let expected = relevantDocs.slice(0, limit);
    // Modify the expected documents to reflect the $project stage in the pipeline.
    for (let i = 0; i < expected.length; i++) {
        expected[i] = {_id: expected[i]["_id"], meta: {value: 0}};
    }

    // Set history for shard 0.
    {
        const resultsID = NumberLong(11);
        const metaID = NumberLong(12);
        const historyResults = [
            {
                expectedCommand: mongotCommandForQuery({
                    query: searchQuery,
                    collName: collName,
                    db: dbName,
                    collectionUUID: collUUID,
                    protocolVersion: protocolVersion,
                    cursorOptions: {docsRequested: NumberInt(limit)}
                }),
                response: {
                    ok: 1,
                    cursors: [
                        {
                            cursor: {
                                id: NumberLong(0),
                                type: "results",
                                ns: coll.getFullName(),
                                nextBatch: relevantSearchDocsShard0.slice(0, mongotReturnedDocs)
                            },
                            ok: 1
                        },
                        {
                            cursor: {
                                id: NumberLong(0),
                                ns: coll.getFullName(),
                                type: "meta",
                                nextBatch: [{value: 0}],
                            },
                            ok: 1
                        }
                    ]
                }
            },
        ];
        const mongot = stWithMock.getMockConnectedToHost(st.rs0.getPrimary());
        mongot.setMockResponses(historyResults, resultsID, metaID);
    }

    // Set history for shard 1.
    {
        const resultsID = NumberLong(21);
        const metaID = NumberLong(22);
        const historyResults = [
            {
                expectedCommand: mongotCommandForQuery({
                    query: searchQuery,
                    collName: collName,
                    db: dbName,
                    collectionUUID: collUUID,
                    protocolVersion: protocolVersion,
                    cursorOptions: {docsRequested: NumberInt(limit)}
                }),
                response: {
                    ok: 1,
                    cursors: [
                        {
                            cursor: {
                                id: NumberLong(0),
                                type: "results",
                                ns: coll.getFullName(),
                                nextBatch: relevantSearchDocsShard1.slice(0, mongotReturnedDocs)

                            },
                            ok: 1
                        },
                        {
                            cursor: {
                                id: NumberLong(0),
                                ns: coll.getFullName(),
                                type: "meta",
                                nextBatch: [{value: 0}],
                            },
                            ok: 1
                        }
                    ]
                }
            },
        ];
        const mongot = stWithMock.getMockConnectedToHost(st.rs1.getPrimary());
        mongot.setMockResponses(historyResults, resultsID, metaID);
    }

    // Set history for mongos.
    {
        const mergingPipelineHistory = [{
            expectedCommand: {
                planShardedSearch: collName,
                query: searchQuery,
                $db: dbName,
                searchFeatures: {shardedSort: 1}
            },
            response: {
                ok: 1,
                protocolVersion: NumberInt(1),
                // This does not represent an actual merging pipeline. The merging pipeline is
                // arbitrary, it just must only generate one document.
                metaPipeline: [{$limit: 1}]
            }
        }];
        const mongot = stWithMock.getMockConnectedToHost(stWithMock.st.s);
        mongot.setMockResponses(mergingPipelineHistory, 1);
    }

    let cursor = coll.aggregate(pipeline);
    assert.eq(expected, cursor.toArray());
}

function buildHistorySearchWithinLookupStandalone(db, mongotConn, searchLookupQuery, numBerries) {
    let foreignColl = db.getCollection(foreignCollName);
    assert.commandWorked(foreignColl.insertMany(foreignCollectionDocs));
    let foreignCollUUID = getUUIDFromListCollections(db, foreignCollName);

    {
        const history = [
            {
                expectedCommand: {
                    search: foreignCollName,
                    collectionUUID: foreignCollUUID,
                    query: searchLookupQuery,
                    $db: dbName,
                    cursorOptions: {docsRequested: numBerries},
                },
                response: {
                    cursor: {
                        id: NumberLong(0),
                        ns: foreignColl.getFullName(),
                        nextBatch: [
                            {"_id": 1, "$searchScore": 0.300},
                            {"_id": 2, "$searchScore": 0.299},
                            {"_id": 3, "$searchScore": 0.298},
                            {"_id": 4, "$searchScore": 0.297},
                            // We set mongotmock to return 4 documents here because of the
                            // oversubscription that mongot would do (mongot would want to return 10
                            // documents in this case because 3 * 1.064 < 10, but there are only 4
                            // that satisfy the query so we return those 4).
                        ],
                    },
                    ok: 1
                }
            },
        ];
        // Only one response is needed, as $lookup executes $search once and caches the response.
        assert.commandWorked(mongotConn.adminCommand(
            {setMockResponses: 1, cursorId: NumberLong(123), history: history}));
    }
}

function buildHistorySearchWithinLookupShardedEnv(db, stWithMock, searchLookupQuery, numBerries) {
    let st = stWithMock.st;

    let foreignColl = db.getCollection(foreignCollName);
    assert.commandWorked(foreignColl.insertMany(foreignCollectionDocs));
    let foreignCollUUID =
        getUUIDFromListCollections(st.rs0.getPrimary().getDB(dbName), foreignCollName);

    // Shard the foreign collection for the $lookup test and move the higher chunk to shard1.
    st.shardColl(
        foreignColl, {_id: 1}, {_id: foreignChunkBoundary}, {_id: foreignChunkBoundary + 1});

    const planShardedSearchHistory = [{
        expectedCommand: {
            planShardedSearch: foreignCollName,
            query: searchLookupQuery,
            $db: dbName,
            searchFeatures: {shardedSort: 1}
        },
        response: {ok: 1, protocolVersion: NumberInt(1), metaPipeline: [{$limit: 1}]}
    }];

    function history(cursorId, docsToReturn) {
        // We will set mongotmock to return 2 of the documents given in the first batch, and the
        // rest as a response to the expected getMore.
        let docsInFirstBatch = 2;
        assert(docsToReturn.length >= docsInFirstBatch);

        return [
            {
                expectedCommand: {
                    search: foreignCollName,
                    collectionUUID: foreignCollUUID,
                    query: searchLookupQuery,
                    $db: dbName,
                    cursorOptions: {docsRequested: numBerries},
                    intermediate: protocolVersion
                },
                response: {
                    cursors: [
                        {
                            cursor: {
                                id: NumberLong(cursorId),
                                type: "results",
                                ns: foreignColl.getFullName(),
                                nextBatch: docsToReturn.slice(0, 2),
                            },
                            ok: 1
                        },
                        {
                            cursor: {
                                id: NumberLong(0),
                                ns: foreignColl.getFullName(),
                                type: "meta",
                                nextBatch: [{metaVal: 1}],
                            },
                            ok: 1
                        }
                    ]
                }
            },
            {
                expectedCommand: {
                    getMore: NumberLong(cursorId),
                    collection: foreignCollName,
                    cursorOptions: {docsRequested: numBerries - docsInFirstBatch}
                },
                response: {
                    cursor: {
                        id: NumberLong(0),
                        ns: foreignColl.getFullName(),
                        nextBatch: docsToReturn.slice(2),
                    },
                    ok: 1
                }
            },

        ];
    }

    const s0Mongot = stWithMock.getMockConnectedToHost(stWithMock.st.rs0.getPrimary());
    const s1Mongot = stWithMock.getMockConnectedToHost(stWithMock.st.rs1.getPrimary());

    // We need a new cursorId for each setMockResponses below because calling setMockResponses with
    // the same cursorId twice overwrites the first mock response.
    let cursorId = 123;
    let metaId = 2;

    // Each shard will invoke PSS during execution of $search in $lookup. Only one response is
    // necessary since $search is executed once and cached.

    // Mock the responses for the commands resulting from parsing the local collection documents on
    // shard0.
    s0Mongot.setMockResponses(planShardedSearchHistory, NumberLong(cursorId++));

    // Mock the responses for the commands resulting from parsing the local collection
    // documents on shard1.
    s1Mongot.setMockResponses(planShardedSearchHistory, NumberLong(cursorId++));

    // As part of $lookup execution, each shard will send a search command to itself and to the
    // other shard. Only occurs once per shard as $search is executed once and cached.
    for (let i = 0; i < 2; i++) {
        s0Mongot.setMockResponses(
            history(cursorId, [{_id: 1, $searchScore: 0.3}, {_id: 2, $searchScore: 0.299}]),
            NumberLong(cursorId++),
            NumberLong(metaId++));

        s1Mongot.setMockResponses(
            history(cursorId, [{_id: 3, $searchScore: 0.298}, {_id: 4, $searchScore: 0.297}]),
            NumberLong(cursorId++),
            NumberLong(metaId++));
    }

    stWithMock.getMockConnectedToHost(stWithMock.st.rs0.getPrimary()).disableOrderCheck();
    stWithMock.getMockConnectedToHost(stWithMock.st.rs1.getPrimary()).disableOrderCheck();
}

function testSearchWithinLookup(db, coll, mongotConn, stWithMock) {
    // The $lookup subpipeline produces one document of the form {three_berries: {berries: [...]}}
    // where the contents of the array are the first 3 fruits (3 from the limit stage) in the
    // foreign collection documents that contain the substring "berries".
    // Each document in the local collection has this document produced from the subpipeline after
    // the $lookup stage. The $unwind stages unwind the array in that document. The $match stage
    // filters out documents where the title field does not contain any of the first 3 berries as a
    // substring. Thus, we are left with documents in the local collection where the titles contain
    // one of the first 3 berries from the foreign collection documents.
    const searchLookupQuery = {query: "berries", path: "fruit"};
    const numBerries = 3;
    const expected = [
        {"title": "cakes and blueberries"},
        {"title": "cakes and strawberries"},
        {"title": "cakes and raspberries"},
    ];
    const pipeline = [
        {
            $lookup: {
                from: foreignCollName,
                pipeline: [
                    {$search: searchLookupQuery},
                    {$limit: numBerries},
                    {$group: {_id: null, berries: {$addToSet: "$fruit"}}},
                    {$project: {_id: 0}}
                ],
                as: "three_berries"
            }
        },
        {$unwind: {path: "$three_berries"}},
        {$unwind: {path: "$three_berries.berries"}},
        {$match: {$expr: {$ne: [{$indexOfCP: ["$title", "$three_berries.berries"]}, -1]}}},
        {$project: {_id: 0, title: 1}}
    ];

    // Exactly one of mongotConn and stWithMock is expected to be null.
    if (mongotConn != null) {
        assert(stWithMock == null);
        buildHistorySearchWithinLookupStandalone(db, mongotConn, searchLookupQuery, numBerries);
    } else {
        assert(mongotConn == null);
        buildHistorySearchWithinLookupShardedEnv(db, stWithMock, searchLookupQuery, numBerries);
    }

    let cursor = coll.aggregate(pipeline);
    assert.eq(expected, cursor.toArray());
}

// Perform a $search query where a getMore is required.
function getMoreCaseBuildHistoryStandalone(coll, collUUID, mongotConn, limitVal, orphanDocs) {
    const cursorId = NumberLong(123);

    // This tests that mongod will getMore thrice as necessary to obtain all the relevent documents.
    // There are 13 total documents that should be returned by the search query and the limit in the
    // query is 15. We define the first batch returned by mongot to be 10 of the documents actually
    // in the collection and 6 documents that will not go through the $idLookup stage because they
    // don't exist in the collection (this simulates the case where the mongot index of the data is
    // stale and some documents have been deleted from the collection but the index has not yet been
    // updated to reflect this). 6 comes from the oversubscription that mongot will do (limitVal
    // * 1.064, rounded up, minus the 10 real documents). This will cause mongod to getMore with 5
    // documents since only 10 in the first batch were valid. To this, mongot will return 10
    // documents (as that is the minumum number of documents mongot returns for a query with an
    // extractable limit), but all of these will be more orphan documents that aren't in the
    // collection. mongod will send another getMore for 5 documents and to this mongot will return a
    // batch with only 1 valid document (to exercise the case where mongot returns fewer documents
    // than requested, but there are still more to be returned. This could happen if the batch
    // exceeds the 16MB limit.) mongod will send another getMore for 4 documents and to this mongot
    // will send the remaining 2 documents in the collection that satisfy the search query.
    const batch1 = relevantSearchDocs.slice(0, 10).concat(orphanDocs.slice(0, 6));
    const batch2 = orphanDocs.slice(6);
    const batch3 = relevantSearchDocs.slice(10, 11);
    const batch4 = relevantSearchDocs.slice(11);

    const history = [
        {
            expectedCommand: mongotCommandForQuery({
                query: searchQuery,
                collName: collName,
                db: dbName,
                collectionUUID: collUUID,
                cursorOptions: {docsRequested: NumberInt(limitVal)}
            }),
            response: {
                cursor: {
                    id: cursorId,
                    ns: coll.getFullName(),
                    nextBatch: batch1,
                },
                ok: 1
            }
        },
        {
            expectedCommand:
                {getMore: cursorId, collection: coll.getName(), cursorOptions: {docsRequested: 5}},
            response: {
                ok: 1,
                cursor: {
                    id: cursorId,
                    ns: coll.getFullName(),
                    nextBatch: batch2,
                },
            }
        },
        {
            expectedCommand:
                {getMore: cursorId, collection: coll.getName(), cursorOptions: {docsRequested: 5}},
            response: {
                ok: 1,
                cursor: {
                    id: cursorId,
                    ns: coll.getFullName(),
                    nextBatch: batch3,
                },
            }
        },
        {
            expectedCommand:
                {getMore: cursorId, collection: coll.getName(), cursorOptions: {docsRequested: 4}},
            response: {
                ok: 1,
                cursor: {
                    id: NumberLong(0),  // We have exhausted the cursor.
                    ns: coll.getFullName(),
                    nextBatch: batch4,
                },
            }
        },
    ];
    assert.commandWorked(
        mongotConn.adminCommand({setMockResponses: 1, cursorId: cursorId, history: history}));
}

function getMoreCaseBuildHistoryShardedEnv(coll, collUUID, stWithMock, limitVal, orphanDocs) {
    const cursorId = NumberLong(123);
    const metaId = NumberLong(2);

    // This is a similar situation to the standlone case.

    // For the first batch, mongot will return 10 total real documents to the shards (3 for shard0
    // and 7 for shard1) and 6 total documents that are not in the collection (3 for each shard).
    const batch1shard0 = relevantSearchDocsShard0.slice(0, 3).concat(orphanDocs.slice(0, 3));
    const batch1shard1 = relevantSearchDocsShard1.slice(0, 7).concat(orphanDocs.slice(3, 6));

    // The amount of documents that each shard will request in the getMore will be the difference
    // between the limit in the pipeline and the number of valid documents each shard got back from
    // the first batch.
    const docsRequestedShard0 = limitVal - 3;
    const docsRequestedShard1 = limitVal - 7;

    // For the second batch, mongot will return 5 orphan documents to each shard.
    const batch2shard0 = orphanDocs.slice(6, 11);
    const batch2shard1 = orphanDocs.slice(11);

    // For the third batch, mongot will return 1 of the documents on shard 1.
    const batch3shard0 = [];
    const batch3shard1 = relevantSearchDocsShard1.slice(7, 8);

    // For the third batch, mongot will return the last remaining document on shard0 and the last
    // remaining document on shard1.
    const batch4shard0 = relevantSearchDocsShard0.slice(3);
    const batch4shard1 = relevantSearchDocsShard1.slice(8);

    // Set history for shard 0.
    const history0 = [
        {
            expectedCommand: mongotCommandForQuery({
                query: searchQuery,
                collName: collName,
                db: dbName,
                collectionUUID: collUUID,
                protocolVersion: protocolVersion,
                cursorOptions: {docsRequested: limitVal}
            }),
            response: {
                cursors: [
                    {
                        cursor: {
                            id: cursorId,
                            type: "results",
                            ns: coll.getFullName(),
                            nextBatch: batch1shard0,
                        },
                        ok: 1
                    },
                    {
                        cursor: {
                            id: NumberLong(0),
                            ns: coll.getFullName(),
                            type: "meta",
                            nextBatch: [{metaVal: 1}],
                        },
                        ok: 1
                    }
                ]
            }
        },
        {
            expectedCommand: {
                getMore: cursorId,
                collection: coll.getName(),
                cursorOptions: {docsRequested: docsRequestedShard0}
            },
            response: {
                ok: 1,
                cursor: {
                    id: cursorId,
                    ns: coll.getFullName(),
                    nextBatch: batch2shard0,
                },
            }
        },
        {
            expectedCommand: {
                getMore: cursorId,
                collection: coll.getName(),
                cursorOptions: {docsRequested: docsRequestedShard0}
            },
            response: {
                ok: 1,
                cursor: {
                    id: cursorId,
                    ns: coll.getFullName(),
                    nextBatch: batch3shard0,
                },
            }
        },
        {
            expectedCommand: {
                getMore: cursorId,
                collection: coll.getName(),
                // Since the previous batch returned no documents for this shard, this docsRequested
                // value will be the same as the previous.
                cursorOptions: {docsRequested: docsRequestedShard0}
            },
            response: {
                ok: 1,
                cursor: {
                    id: NumberLong(0),  // We have exhausted the cursor.
                    ns: coll.getFullName(),
                    nextBatch: batch4shard0,
                },
            }
        },
    ];
    const s0Mongot = stWithMock.getMockConnectedToHost(stWithMock.st.rs0.getPrimary());
    s0Mongot.setMockResponses(history0, cursorId, metaId);

    // Set history for shard 1.
    const history1 = [
        {
            expectedCommand: mongotCommandForQuery({
                query: searchQuery,
                collName: collName,
                db: dbName,
                collectionUUID: collUUID,
                protocolVersion: protocolVersion,
                cursorOptions: {docsRequested: NumberInt(limitVal)}
            }),
            response: {
                cursors: [
                    {
                        cursor: {
                            id: cursorId,
                            type: "results",
                            ns: coll.getFullName(),
                            nextBatch: batch1shard1,
                        },
                        ok: 1
                    },
                    {
                        cursor: {
                            id: NumberLong(0),
                            ns: coll.getFullName(),
                            type: "meta",
                            nextBatch: [{metaVal: 1}],
                        },
                        ok: 1
                    }
                ]

            }
        },
        {
            expectedCommand: {
                getMore: cursorId,
                collection: coll.getName(),
                cursorOptions: {docsRequested: docsRequestedShard1}
            },
            response: {
                ok: 1,
                cursor: {
                    id: cursorId,
                    ns: coll.getFullName(),
                    nextBatch: batch2shard1,
                },
            }
        },
        {
            expectedCommand: {
                getMore: cursorId,
                collection: coll.getName(),
                cursorOptions: {docsRequested: docsRequestedShard1}
            },
            response: {
                ok: 1,
                cursor: {
                    id: cursorId,
                    ns: coll.getFullName(),
                    nextBatch: batch3shard1,
                },
            }
        },
        {
            expectedCommand: {
                getMore: cursorId,
                collection: coll.getName(),
                // Since the previous batch returned one valid document for this shard, this
                // docsRequested value will be one less than the previous.
                cursorOptions: {docsRequested: docsRequestedShard1 - 1}
            },
            response: {
                ok: 1,
                cursor: {
                    id: NumberLong(0),  // We have exhausted the cursor.
                    ns: coll.getFullName(),
                    nextBatch: batch4shard1,
                },
            }
        },
    ];
    const s1Mongot = stWithMock.getMockConnectedToHost(stWithMock.st.rs1.getPrimary());
    s1Mongot.setMockResponses(history1, cursorId, metaId);

    mockPlanShardedSearchResponse(
        collName, searchQuery, dbName, undefined /*sortSpec*/, stWithMock);
}

// Perform a $search query where a getMore is required.
function getMoreCase(coll, collUUID, standaloneConn, stConn) {
    const limitVal = 15;
    const pipeline = [{$search: searchQuery}, {$limit: limitVal}];

    // Construct 16 fake documents that aren't in the collection for mongot to return (mimicking an
    // out-of-date index) that will not pass the idLookup stage.
    let orphanDocs = [];
    for (let i = 20; i < 36; i++) {
        orphanDocs.push({_id: i, $searchScore: 0.3});
    }

    // Exactly one of standaloneConn and stConn is execpted to be null.
    if (standaloneConn != null) {
        assert(stConn == null);
        getMoreCaseBuildHistoryStandalone(coll, collUUID, standaloneConn, limitVal, orphanDocs);
    } else {
        assert(standaloneConn == null);
        getMoreCaseBuildHistoryShardedEnv(coll, collUUID, stConn, limitVal, orphanDocs);
    }

    let cursor = coll.aggregate(pipeline);
    // All relevant documents are expected to be returned by the query.
    assert.eq(relevantDocs, cursor.toArray());
}

function runTest(db, collUUID, standaloneConn, stConn) {
    let coll = db.getCollection(collName);
    function runSearchQueries(limitVal, otherLimitVal, skipVal) {
        // Perform a $search query with a limit stage.
        let pipeline = [{$search: searchQuery}, {$limit: limitVal}];
        // The extracted limit here comes from the limit value in the pipeline.
        let expected = relevantDocs.slice(0, limitVal);
        runAndAssert(pipeline, limitVal, expected, coll, collUUID, standaloneConn, stConn);

        // Perform a $search query with a $skip followed by $limit.
        pipeline = [{$search: searchQuery}, {$skip: skipVal}, {$limit: limitVal}];
        // The extracted limit here comes from the sum of the limit and skip values in the pipeline.
        expected = relevantDocs.slice(skipVal).slice(0, limitVal);
        runAndAssert(
            pipeline, limitVal + skipVal, expected, coll, collUUID, standaloneConn, stConn);

        // Perform a $search query with multiple limit stages.
        pipeline = [{$search: searchQuery}, {$limit: limitVal}, {$limit: otherLimitVal}];
        // The extracted limit here comes from the minimum of the two limit values in the pipeline.
        expected = relevantDocs.slice(0, Math.min(limitVal, otherLimitVal));
        runAndAssert(pipeline,
                     Math.min(limitVal, otherLimitVal),
                     expected,
                     coll,
                     collUUID,
                     standaloneConn,
                     stConn);

        // Perform a $search query with a limit and multiple skip stages.
        pipeline = [{$search: searchQuery}, {$skip: skipVal}, {$skip: skipVal}, {$limit: limitVal}];
        // The extracted limit here comes from the value of the limit plus the values of the two
        // skip stages in the pipeline.
        expected = relevantDocs.slice(skipVal + skipVal).slice(0, limitVal);
        runAndAssert(pipeline,
                     skipVal + skipVal + limitVal,
                     expected,
                     coll,
                     collUUID,
                     standaloneConn,
                     stConn);

        // Perform a $search query with multiple limit stages and multiple skip stages.
        pipeline = [
            {$search: searchQuery},
            {$skip: skipVal},
            {$skip: skipVal},
            {$limit: limitVal},
            {$limit: otherLimitVal}
        ];
        // The extracted limit here comes from the minimum of the two limit values plus the values
        // of the two skip stages in the pipeline.
        expected =
            relevantDocs.slice(skipVal + skipVal).slice(0, Math.min(limitVal, otherLimitVal));
        runAndAssert(pipeline,
                     skipVal + skipVal + Math.min(limitVal, otherLimitVal),
                     expected,
                     coll,
                     collUUID,
                     standaloneConn,
                     stConn);
    }

    // Run the search queries with limit and skip values such that mongod will extract a user limit
    // of less than 10, which means we will exercise the branch where mongot returns a minimum of 10
    // documents.
    runSearchQueries(3 /* limitVal */, 2 /* otherLimitVal */, 1 /* skipVal */);

    // Run the search queries with limit and skip values such that mongod will extract a user limit
    // of greater than 10, which means we will exercise the branch where mongot the extracted limit
    // multiplied by the oversubscription factor.
    runSearchQueries(11 /* limitVal */, 10 /* otherLimitVal */, 1 /* skipVal */);

    expectNoDocsRequestedInCommand(coll, collUUID, standaloneConn, stConn);

    // SERVER-80648 $search in SBE doesn't support the batch size optimization, so skip the tests.
    if (!(checkSbeRestrictedOrFullyEnabled(db) &&
          FeatureFlagUtil.isPresentAndEnabled(db.getMongo(), 'SearchInSbe'))) {
        // Tests that getMore has a correct cursorOptions field.
        getMoreCase(coll, collUUID, standaloneConn, stConn);

        testSearchWithinLookup(db, coll, standaloneConn, stConn);
    }

    // Test that the docsRequested field makes it to the shards in a sharded environment when
    // $$SEARCH_META is referenced in the query.
    if (stConn != null) {
        searchMetaAfterLimit(coll, collUUID, stConn);
    }
}

function setupAndRunTestStandalone() {
    const mongotmock = new MongotMock();
    mongotmock.start();
    const mongotConn = mongotmock.getConnection();
    const conn = MongoRunner.runMongod({setParameter: {mongotHost: mongotConn.host}});
    let db = conn.getDB(dbName);
    if (FeatureFlagUtil.isPresentAndEnabled(db.getMongo(), 'SearchBatchSizeTuning')) {
        jsTestLog("Skipping the test because it only applies when batchSize isn't enabled.");
    } else {
        let coll = db.getCollection(collName);
        // Insert documents.
        assert.commandWorked(coll.insertMany(docs));

        let collUUID = getUUIDFromListCollections(db, collName);
        runTest(db, collUUID, mongotConn, null /* stConn */);
    }

    MongoRunner.stopMongod(conn);
    mongotmock.stop();
}

function setupAndRunTestShardedEnv() {
    const stWithMock = new ShardingTestWithMongotMock({
        name: "search_docsrequested",
        shards: {
            rs0: {nodes: 2},
            rs1: {nodes: 2},
        },
        mongos: 1,
        other: {
            rsOptions: {setParameter: {enableTestCommands: 1}},
        }
    });
    stWithMock.start();
    let st = stWithMock.st;
    let mongos = st.s;
    let db = mongos.getDB(dbName);
    if (FeatureFlagUtil.isPresentAndEnabled(db.getMongo(), 'SearchBatchSizeTuning')) {
        jsTestLog("Skipping the test because it only applies when batchSize isn't enabled.");
    } else {
        assert.commandWorked(mongos.getDB("admin").runCommand(
            {enableSharding: dbName, primaryShard: st.shard0.name}));

        let coll = db.getCollection(collName);

        // Insert documents.
        assert.commandWorked(coll.insertMany(docs));

        // Shard the collection, split it at {_id: chunkBoundary}, and move the higher chunk to
        // shard1.
        st.shardColl(coll, {_id: 1}, {_id: chunkBoundary}, {_id: chunkBoundary + 1});

        let collUUID = getUUIDFromListCollections(st.rs0.getPrimary().getDB(dbName), collName);
        runTest(db, collUUID, null /* standaloneConn */, stWithMock);
    }

    stWithMock.stop();
}

// Test standalone.
setupAndRunTestStandalone();

// Test sharded cluster.
setupAndRunTestShardedEnv();
