/**
 * Test that the $facet stage fails cleanly without consuming too much memory if the size of the
 * facet's output document is large.
 *
 * This test was designed to reproduce SERVER-40317.
 */
(function() {
"use strict";

load("jstests/libs/fixture_helpers.js");  // For isSharded.

const collName = "facet_memory_consumption";
const coll = db[collName];
const kFacetOutputTooLargeCode = 4031700;
coll.drop();

// Do not run the rest of the tests if the foreign collection is implicitly sharded but the flag to
// allow $lookup/$graphLookup into a sharded collection is disabled.
const getShardedLookupParam = db.adminCommand({getParameter: 1, featureFlagShardedLookup: 1});
const isShardedLookupEnabled = getShardedLookupParam.hasOwnProperty("featureFlagShardedLookup") &&
    getShardedLookupParam.featureFlagShardedLookup.value;
if (FixtureHelpers.isSharded(coll) && !isShardedLookupEnabled) {
    return;
}

// A document that is slightly less than 1MB.
const doc = {
    str: "x".repeat(1024 * 1024 - 100)
};

// Insert it into the collection twice.
assert.commandWorked(coll.insert(doc));
assert.commandWorked(coll.insert(doc));

// Creates a pipeline that chains Cartesian product pipelines to create a pipeline returning
// 2^exponent documents (assuming that there 2 documents in the 'collName' collection).
function cartesianProductPipeline(exponent) {
    let productPipeline = [];
    for (let i = 0; i < exponent - 1; ++i) {
        productPipeline = productPipeline.concat([
            {$lookup: {from: collName, pipeline: [{$match: {}}], as: "join"}},
            {$unwind: "$join"},
            {$project: {str: 1}},
        ]);
    }
    return productPipeline;
}

(function succeedsWhenWithinMemoryLimit() {
    // This pipeline uses $facet to return one document that is just slightly less than the 16MB,
    // which is within the document size limit.
    const result = coll.aggregate([{$facet: {product: cartesianProductPipeline(4)}}]).toArray();
    const resultSize = Object.bsonsize(result);

    // As a sanity check, make sure that the resulting document is somewhere around 16MB in size.
    assert.gt(resultSize, 15 * 1024 * 1024, result);
    assert.lt(resultSize, 16 * 1024 * 1024, result);
}());

(function failsWhenResultDocumentExeedsMaxBSONSize() {
    // This pipeline uses $facet to create a document that is larger than the 16MB max document
    // size.
    const result = assert.throws(
        () => coll.aggregate([{$facet: {product: cartesianProductPipeline(6)}}]).toArray());
    assert.eq(result.code, ErrorCodes.BSONObjectTooLarge);
}());

(function succeedsWhenIntermediateDocumentExceedsMaxBSONSizeWithUnwind() {
    // This pipeline uses $facet to create an intermediate document that is larger than the 16MB
    // max document size but smaller than the 100MB allowed for an intermediate document. The
    // $unwind stage breaks the large document into a bunch of small documents, which is legal.
    const result =
        coll.aggregate([{$facet: {product: cartesianProductPipeline(6)}}, {$unwind: "$product"}])
            .toArray();
    assert.eq(64, result.length, result);
}());

(function failsWhenFacetOutputDocumentTooLarge() {
    // This pipeline uses $facet to create a document that is larger than the 100MB maximum size for
    // an intermediate document. Even with the $unwind stage, the pipeline should fail, this time
    // with error code 31034.
    const result = assert.throws(
        () => coll.aggregate(
                      [{$facet: {product: cartesianProductPipeline(10)}}, {$unwind: "$product"}])
                  .toArray());
    assert.eq(result.code, kFacetOutputTooLargeCode);
}());
}());
