/**
 * Tests the behavior of projection for dotted paths, including edge cases where the path only
 * sometimes exists.
 *
 * @tags: [
 *   # We could potentially need to resume an index build in the event of a stepdown, which is not
 *   # yet implemented.
 *   does_not_support_stepdowns,
 *   not_allowed_with_signed_security_token,
 *   featureFlagSbeFull,
 * ]
 */

const coll = db.projection_semantics;
coll.drop();

let globalIdCounter = 0;

// Tests that when 'projection' is applied to 'input', we get 'expectedOutput'.
// Tests that this remains true if indexes are added, or if we use aggregation instead of find.
function testInputOutput({input, projection, expectedOutput, interestingIndexes = []}) {
    if (!input.hasOwnProperty("_id")) {
        input = Object.merge({_id: ++globalIdCounter}, input);
    }
    assert.commandWorked(coll.insert(input));
    assert.docEq(
        expectedOutput,
        coll.find({_id: input._id}, projection).limit(1).hint({$natural: 1}).toArray()[0],
        () =>
            tojson(coll.find({_id: input._id}, projection).limit(1).hint({$natural: 1}).explain()));

    for (let indexSpec of interestingIndexes) {
        assert.commandWorkedOrFailedWithCode(coll.createIndex(indexSpec),
                                             ErrorCodes.IndexAlreadyExists);
        assert.docEq(
            expectedOutput,
            coll.find({_id: input._id}, projection).hint(indexSpec).toArray()[0],
            () => tojson(
                coll.find({_id: input._id}, projection).hint(indexSpec).explain("executionStats")));
    }

    assert.docEq(
        expectedOutput,
        coll.aggregate([{$match: {_id: input._id}}, {$project: projection}], {hint: {$natural: 1}})
            .toArray()[0]);
}

// The basics: what happens when I include a top-level field?
(function testTopLevelInclusion() {
    // Test the basic "include a" projection. This should implicitly include the _id.
    const testIncludeA = (input, output) => testInputOutput({
        input: input,
        projection: {a: 1},
        expectedOutput: output,
        interestingIndexes: [{_id: 1, a: 1}, {a: 1, _id: 1}]
    });

    // Test some basic "normal" cases.
    testIncludeA({_id: ++globalIdCounter, a: "demo", b: "other", x: "extra"},
                 {_id: globalIdCounter, a: "demo"});
    testIncludeA({_id: ++globalIdCounter, a: "demo", aWSuffix: "other", 'a.b': "extra"},
                 {_id: globalIdCounter, a: "demo"});
    testIncludeA({_id: ++globalIdCounter, a: null, b: "other", x: "extra"},
                 {_id: globalIdCounter, a: null});

    // Test including "a" when "a" is missing/not present.
    // TODO SERVER-23229 this will return different results if there is a covering index, so here
    // but not elsewhere we don't use any "interestingIndexes."
    testInputOutput({
        input: {_id: ++globalIdCounter, b: "other", x: "extra"},
        projection: {a: 1},
        expectedOutput: {_id: globalIdCounter},
        interestingIndexes: []
    });
    testInputOutput({
        input: {_id: ++globalIdCounter},
        projection: {a: 1},
        expectedOutput: {_id: globalIdCounter},
        interestingIndexes: []
    });

    // Test a range of interesting values for "a". We expect everything to be preserved unmodified.
    const testIdentityInclusionA = (input) => testInputOutput({
        input: input,
        projection: {a: 1},
        expectedOutput: input,
        interestingIndexes: [{_id: 1, a: 1}, {a: 1, _id: 1}]
    });
    testIdentityInclusionA({_id: ++globalIdCounter, a: null});
    testIdentityInclusionA({_id: ++globalIdCounter, a: undefined});
    testIdentityInclusionA({_id: ++globalIdCounter, a: {}});
    testIdentityInclusionA({_id: ++globalIdCounter, a: []});
    testIdentityInclusionA({_id: ++globalIdCounter, a: {x: 1, b: "scalar"}});
    testIdentityInclusionA({_id: ++globalIdCounter, a: "scalar"});
    testIdentityInclusionA({_id: ++globalIdCounter, a: {b: {}}});
    testIdentityInclusionA({_id: ++globalIdCounter, a: [null]});
    testIdentityInclusionA({_id: ++globalIdCounter, a: ["scalar"]});
    testIdentityInclusionA({_id: ++globalIdCounter, a: [[]]});
    testIdentityInclusionA({_id: ++globalIdCounter, a: [{}]});
    testIdentityInclusionA({_id: ++globalIdCounter, a: [1, {}, 2]});
    testIdentityInclusionA({_id: ++globalIdCounter, a: [[1, 2], [{}], 2]});
    testIdentityInclusionA({_id: ++globalIdCounter, a: [{b: "scalar"}]});

    // Now test with the same documents but excluding the "_id" field.
    const testIncludeOnlyA = (input, output) => testInputOutput({
        input: input,
        projection: {a: 1, _id: 0},
        expectedOutput: output,
        // Can't use {a: "hashed"} here because it doesn't support array values.
        // Could use {a: "$**"}, but it can't be hinted without a query predicate so we'll leave
        // that coverage for elsewhere rather than add complexity to get a valid query predicate.
        interestingIndexes: [{a: 1}, {a: -1}]
    });

    // The "basics" again.
    testIncludeOnlyA({_id: ++globalIdCounter, a: "demo", b: "other", x: "extra"}, {a: "demo"});
    testIncludeOnlyA({_id: ++globalIdCounter, a: "demo", aWSuffix: "other", 'a.b': "extra"},
                     {a: "demo"});
    testIncludeOnlyA({_id: ++globalIdCounter, a: null, b: "other", x: "extra"}, {a: null});

    // Missing 'a' value again.
    // TODO SERVER-23229 this will return different results if there is a covering index, so here
    // but not elsewhere we don't use any "interestingIndexes".
    testInputOutput({
        input: {_id: ++globalIdCounter, b: "other", x: "extra"},
        projection: {a: 1, _id: 0},
        expectedOutput: {},
        interestingIndexes: []
    });
    testInputOutput({
        input: {_id: ++globalIdCounter},
        projection: {a: 1, _id: 0},
        expectedOutput: {},
        interestingIndexes: []
    });

    // Just a couple of the cases above to confirm the same behavior just without the _id.
    testIncludeOnlyA({_id: ++globalIdCounter, a: null}, {a: null});
    testIncludeOnlyA({_id: ++globalIdCounter, a: {}}, {a: {}});
    testIncludeOnlyA({_id: ++globalIdCounter, a: []}, {a: []});
    testIncludeOnlyA({_id: ++globalIdCounter, a: {x: 1, b: "scalar"}}, {a: {x: 1, b: "scalar"}});
    testIncludeOnlyA({_id: ++globalIdCounter, a: {b: {}}}, {a: {b: {}}});
    testIncludeOnlyA({_id: ++globalIdCounter, a: [{b: "scalar"}]}, {a: [{b: "scalar"}]});
}());

// Now test one level of nesting - a single "dot" in the path.
(function testInclusionOneLevelOfNesting() {
    // Test the basic "include a.b" projection. This should implicitly include the _id.
    const testIncludeADotB = (input, output) => testInputOutput({
        input: input,
        projection: {'a.b': 1},
        expectedOutput: output,
        interestingIndexes: [{_id: 1, 'a.b': 1}, {'a.b': 1, _id: 1}]
    });

    // Test some basic "normal" cases.
    // Test that it excludes extra fields at the root and at the sub-document.
    testIncludeADotB({_id: ++globalIdCounter, a: {b: "demo", y: "extra"}, x: "extra"},
                     {_id: globalIdCounter, a: {b: "demo"}});
    // Test that (at least for now) the dotted path doesn't work great here.
    testIncludeADotB({_id: ++globalIdCounter, a: {b: "demo"}, 'a.b': "extra"},
                     {_id: globalIdCounter, a: {b: "demo"}});
    // Test that '_id' within a sub-document is excluded.
    testIncludeADotB({_id: ++globalIdCounter, a: {b: "demo", _id: "extra"}},
                     {_id: globalIdCounter, a: {b: "demo"}});

    // Test array use case.
    testIncludeADotB({_id: ++globalIdCounter, a: [{b: 1, c: 1}, {b: 2, c: 2}], x: "extra"},
                     {_id: globalIdCounter, a: [{b: 1}, {b: 2}]});
    // Test that a missing field within an object in an array will show up as an empty object.
    testIncludeADotB({_id: ++globalIdCounter, a: [{b: 1, c: 1}, {c: 2}], x: "extra"},
                     {_id: globalIdCounter, a: [{b: 1}, {}]});

    // Test including "a.b" when "a.b" is missing/not present.
    //
    // TODO SERVER-23229 this will return different results if there is a covering index, so here
    // but not elsewhere we don't use any "interestingIndexes".
    // If there's a covered plan we will always construct {a: {b: null}} when we don't see an "a.b"
    // value, which is not always correct.
    //
    // Interestingly, this bug disappears if the index is multikey ('a' or 'a.b' are arrays), since
    // we will need to add a fetch to reconstruct the array anyway and will figure out the correct
    // answer in the process.
    //
    const testADotBNoIndexes = (input, output) => testInputOutput(
        {input: input, projection: {'a.b': 1}, expectedOutput: output, interestingIndexes: []});

    testADotBNoIndexes({_id: ++globalIdCounter, b: "other", x: "extra"}, {_id: globalIdCounter});
    testADotBNoIndexes({_id: ++globalIdCounter}, {_id: globalIdCounter});
    testADotBNoIndexes({_id: ++globalIdCounter, a: {}}, {_id: globalIdCounter, a: {}});
    testADotBNoIndexes({_id: ++globalIdCounter, a: "scalar"}, {_id: globalIdCounter});

    const testIncludeOnlyADotB = (input, output) => testInputOutput({
        input: input,
        projection: {'a.b': 1, _id: 0},
        expectedOutput: output,
        interestingIndexes: [{'a.b': 1}, {'a.b': -1}]
    });

    // The "basics" again - no _id this time.
    testIncludeOnlyADotB({_id: ++globalIdCounter, a: {b: "demo", y: "extra"}, x: "extra"},
                         {a: {b: "demo"}});
    testIncludeOnlyADotB({_id: ++globalIdCounter, a: {b: "demo"}, 'a.b': "extra"},
                         {a: {b: "demo"}});
    testIncludeOnlyADotB({_id: ++globalIdCounter, a: {b: "demo", _id: "extra"}}, {a: {b: "demo"}});

    testIncludeOnlyADotB({_id: ++globalIdCounter, a: [{b: 1, c: 1}, {b: 2, c: 2}], x: "extra"},
                         {a: [{b: 1}, {b: 2}]});
    testIncludeOnlyADotB({_id: ++globalIdCounter, a: [{b: 1, c: 1}, {c: 2}], x: "extra"},
                         {a: [{b: 1}, {}]});

    // More cases where 'a.b' doesn't exist - but with arrays this time.
    testIncludeOnlyADotB({_id: ++globalIdCounter, a: [], x: "extra"}, {a: []});
    testIncludeOnlyADotB({_id: ++globalIdCounter, a: [{}, {}], x: "extra"}, {a: [{}, {}]});
    testIncludeOnlyADotB({_id: ++globalIdCounter, a: ["scalar", "scalar"], x: "extra"}, {a: []});
    testIncludeOnlyADotB({_id: ++globalIdCounter, a: [null]}, {a: []});
    // This is an interesting case: the scalars are ignored but the shadow documents are preserved.
    testIncludeOnlyADotB({_id: ++globalIdCounter, a: ["scalar", {}, "scalar", {c: 1}], x: "extra"},
                         {a: [{}, {}]});
    // Further interest: the array within the array is preserved.
    testIncludeOnlyADotB({_id: ++globalIdCounter, a: [[]]}, {a: [[]]});
    // But not the scalar elements of it.
    testIncludeOnlyADotB({_id: ++globalIdCounter, a: [[1, 2, 3]]}, {a: [[]]});
    // But if there's a "b" again we see that.
    testIncludeOnlyADotB({_id: ++globalIdCounter, a: [[1, {b: 1}, {b: 2, c: 2}, "scalar"]]},
                         {a: [[{b: 1}, {b: 2}]]});
    testIncludeOnlyADotB({
        _id: ++globalIdCounter,
        a: [
            ["x", {b: 1}, {b: 2, c: 2}, "x"],
            [[{b: 1}]],
            [{b: 1}, [{b: 2}], [[{b: [2]}]]],
        ]
    },
                         {
                             a: [
                                 [{b: 1}, {b: 2}],
                                 [[{b: 1}]],
                                 [{b: 1}, [{b: 2}], [[{b: [2]}]]],
                             ]
                         });
    testIncludeOnlyADotB({_id: ++globalIdCounter, a: [[], [[], [], [1], [{c: 1}]], {b: 1}]}, {
        a: [
            [],
            [[], [], [], [{}]],
            {b: 1},
        ]
    });

    // Test the case where two paths in a projection go through the same parent object.
    const testIncludeOnlyADotBAndADotC = (input, output) => testInputOutput({
        input: input,
        projection: {'a.b': 1, 'a.c': 1, _id: 0},
        expectedOutput: output,
        interestingIndexes: [
            {a: 1},
            {'a.b': 1},
            {'a.c': 1},
            {'a.b': 1, 'a.c': 1},
            {'a.b': 1, 'a.c': -1},
        ]
    });
    testIncludeOnlyADotBAndADotC(
        {_id: ++globalIdCounter, a: {b: "scalar", c: "scalar", d: "extra"}},
        {a: {b: "scalar", c: "scalar"}});
    testIncludeOnlyADotBAndADotC(
        {_id: ++globalIdCounter, a: [{b: 1, c: 2, d: 3}, {b: 4, c: 5, d: 6}]},
        {a: [{b: 1, c: 2}, {b: 4, c: 5}]});

    // Array cases where one or both of the paths don't exist.
    testIncludeOnlyADotBAndADotC({_id: ++globalIdCounter, a: [{b: 1, c: 2}, {b: 3, d: 4}]},
                                 {a: [{b: 1, c: 2}, {b: 3}]});
    testIncludeOnlyADotBAndADotC({_id: ++globalIdCounter, a: [{c: 1, d: 2}, {b: 3, d: 4}]},
                                 {a: [{c: 1}, {b: 3}]});
    testIncludeOnlyADotBAndADotC({_id: ++globalIdCounter, a: []}, {a: []});
    testIncludeOnlyADotBAndADotC({_id: ++globalIdCounter, a: [{b: 1, c: 2}, "extra", {b: 3, c: 4}]},
                                 {a: [{b: 1, c: 2}, {b: 3, c: 4}]});

    // Non-array cases where one or both of the paths don't exist.
    //
    // TODO SERVER-23229: This will return different results if there is a covering index, so here
    // but not elsewhere we don't use any "interestingIndexes" in test cases.
    const testIncludeADotBAndCNoIndexes = (input, output) => testInputOutput({
        input: input,
        projection: {'a.b': 1, 'a.c': 1, _id: 0},
        expectedOutput: output,
        interestingIndexes: []
    });

    testIncludeADotBAndCNoIndexes({_id: ++globalIdCounter, a: {b: "scalar", d: "extra"}},
                                  {a: {b: "scalar"}});
    testIncludeADotBAndCNoIndexes({_id: ++globalIdCounter, a: {c: "scalar", d: "extra"}},
                                  {a: {c: "scalar"}});
    testIncludeADotBAndCNoIndexes({_id: ++globalIdCounter, a: {d: "extra"}}, {a: {}});
}());

(function testInclusionLevelsOfNesting() {
    // Test the basic "include a.b.c" projection. We've seen the implicit '_id' behavior above so
    // we'll always project _id out for these tests.
    const testIncludeADotBDotC = (input, output) => testInputOutput({
        input: input,
        projection: {'a.b.c': 1, _id: 0},
        expectedOutput: output,
        interestingIndexes: [{'a.b.c': 1}, {'a.b.c': -1}]
    });

    // Test some basic "normal" cases.
    // Test that it excludes extra fields at the root and at the sub-document
    testIncludeADotBDotC({a: {b: {c: "demo", z: "extra"}, y: "extra"}, x: "extra"},
                         {a: {b: {c: "demo"}}});

    // Test array use cases.
    // 'a' is an array.
    testIncludeADotBDotC({a: [{b: {c: 1, d: 1}}, {b: {c: 2, d: 2}}]},
                         {a: [{b: {c: 1}}, {b: {c: 2}}]});
    // 'a.b' is an array.
    testIncludeADotBDotC({a: {b: [{c: 1, d: 1}, {c: 2, d: 2}]}}, {a: {b: [{c: 1}, {c: 2}]}});
    // 'a' and 'a.b' are arrays.
    testIncludeADotBDotC(
        {a: [{b: [{c: 1, d: 1}, {c: 2, d: 2}]}, {b: [{c: 3, d: 3}, {c: 4, d: 4}]}]},
        {a: [{b: [{c: 1}, {c: 2}]}, {b: [{c: 3}, {c: 4}]}]});
    // 'a' and 'a.b' and 'a.b.c' are arrays.
    testIncludeADotBDotC(
        {
            a: [
                {b: [{c: [1, 2, 3], d: 1}, {c: [2, 3, 4], d: 2}]},
                {b: [{c: [3, 4, 5], d: 3}, {c: [4, 5, 6], d: 4}]}
            ]
        },
        {a: [{b: [{c: [1, 2, 3]}, {c: [2, 3, 4]}]}, {b: [{c: [3, 4, 5]}, {c: [4, 5, 6]}]}]});

    // Test that when the path is missing we preserve the structure.
    testIncludeADotBDotC({
        a: [
            // No 'b' here.
            {bPrime: [{c: "x"}, {c: "x"}]},
            // No 'c' anywhere
            {b: [{cPrime: "x", d: 1}, {cPrime: "x", d: 2}]},
            // Only some 'c's
            {b: [{c: 3, d: 3}, {cPrime: "x", d: 4}, {c: 5}]},
            // One more without a 'b' to prove we get trailing empty objects.
            {bPrime: {somethingDifferent: "just because"}},
        ]
    },
                         {
                             a: [
                                 {/* bPrime was here */},
                                 {b: [{}, {} /* all cPrimes */]},
                                 {b: [{c: 3}, {/*cPrime was here */}, {c: 5}]},
                                 {/* bPrime again */}
                             ]
                         });

    // Test including "a.b.c" when "a.b.c" is missing/not present.
    //
    // TODO SERVER-23229 this will return different results if there is a covering index, so here
    // but not elsewhere we don't use any "interestingIndexes".
    // If there's a covered plan we will always construct {a: {b: {c: null}}} when we don't see an
    // "a.b.c" value, which is not always correct.
    //
    // Interestingly, this bug disappears if the index is multikey ('a.b.c' or any parent is an
    // array), since we will need to add a fetch to reconstruct the array anyway and will figure out
    // the correct answer in the process.
    //
    const testADotBDotCNoIndexes = (input, output) => testInputOutput({
        input: input,
        projection: {'a.b.c': 1, _id: 0},
        expectedOutput: output,
        interestingIndexes: []
    });

    testADotBDotCNoIndexes({}, {});
    testADotBDotCNoIndexes({b: "other", x: "extra"}, {});
    testADotBDotCNoIndexes({a: "scalar"}, {});
    testADotBDotCNoIndexes({a: null}, {});
    testADotBDotCNoIndexes({a: {}}, {a: {}});
    testADotBDotCNoIndexes({a: {bPrime: {c: 1}}}, {a: {}});
    testADotBDotCNoIndexes({a: {b: "scalar"}}, {a: {}});
    testADotBDotCNoIndexes({a: {b: {x: 1, y: 1}}}, {a: {b: {}}});

    const testIncludeOnlyADotBDotC = (input, output) => testInputOutput({
        input: input,
        projection: {'a.b.c': 1, _id: 0},
        expectedOutput: output,
        interestingIndexes: [{'a.b.c': 1}, {'a.b.c': -1}]
    });

    // More cases where 'a.b.c' doesn't exist - but with arrays this time.

    // The cases from above with just 'a.b' are mostly the same for 'a.b.c':
    testIncludeOnlyADotBDotC({a: [], x: "extra"}, {a: []});
    testIncludeOnlyADotBDotC({a: [{}, {}], x: "extra"}, {a: [{}, {}]});
    testIncludeOnlyADotBDotC({a: ["scalar", "scalar"], x: "extra"}, {a: []});
    testIncludeOnlyADotBDotC({a: [null]}, {a: []});
    testIncludeOnlyADotBDotC({a: ["scalar", {}, "scalar", {c: 1}], x: "extra"}, {a: [{}, {}]});
    testIncludeOnlyADotBDotC({a: [[]]}, {a: [[]]});
    testIncludeOnlyADotBDotC({a: [[1, 2, 3]]}, {a: [[]]});
    // Now some with 'a.b' existing but no 'a.b.c'.
    testIncludeOnlyADotBDotC({a: {b: []}}, {a: {b: []}});
    testIncludeOnlyADotBDotC({a: {b: ["scalar"]}}, {a: {b: []}});
    testIncludeOnlyADotBDotC({a: {b: [[]]}}, {a: {b: [[]]}});
    testIncludeOnlyADotBDotC({a: [{b: "scalar"}]}, {a: [{}]});
    testIncludeOnlyADotBDotC({a: [{b: []}]}, {a: [{b: []}]});
    testIncludeOnlyADotBDotC({a: [{b: ["scalar"]}]}, {a: [{b: []}]});
    testIncludeOnlyADotBDotC({a: [{b: [[]]}]}, {a: [{b: [[]]}]});
    testIncludeOnlyADotBDotC({a: [{b: {x: 1}}]}, {a: [{b: {}}]});
    testIncludeOnlyADotBDotC({a: [{b: [{}]}]}, {a: [{b: [{}]}]});
    testIncludeOnlyADotBDotC({a: [[1, {b: 1}, {b: 2, c: 2}, "scalar"]]}, {a: [[{}, {}]]});
    testIncludeOnlyADotBDotC({a: [1, {b: [[1, 2], [{}], 2]}, 2]}, {a: [{b: [[], [{}]]}]});
    testIncludeOnlyADotBDotC({a: [[1, 2], [{b: [[1, 2], [{c: [[1, 2], [{}], 2]}], 2]}], 2]},
                             {a: [[], [{b: [[], [{c: [[1, 2], [{}], 2]}]]}]]});
    testIncludeOnlyADotBDotC({
        a: [
            ["x", {b: 1}, {b: 2, c: 2}, "x"],
            [[{b: 1}]],
            [{b: 1}, [{b: 2}], [[{b: [2]}]]],
        ]
    },
                             {
                                 a: [
                                     [{}, {}],
                                     [[{}]],
                                     [{}, [{}], [[{b: []}]]],
                                 ]
                             });
    testIncludeOnlyADotBDotC({a: [[], [[], [], [1], [{x: 1}]], {b: 1}]}, {
        a: [
            [],
            [[], [], [], [{}]],
            {},
        ]
    });

    // Now some new cases where 'a.b.c' sometimes exists.
    // One array path.
    // 'a' is array.
    testIncludeOnlyADotBDotC({a: [{b: {x: 1, c: "scalar"}}]}, {a: [{b: {c: "scalar"}}]});
    testIncludeOnlyADotBDotC({a: [1, {b: {c: {x: 1}}}, 2]}, {a: [{b: {c: {x: 1}}}]});
    testIncludeOnlyADotBDotC({a: [1, {b: {x: 1, c: "scalar"}}, 2]}, {a: [{b: {c: "scalar"}}]});
    testIncludeOnlyADotBDotC({a: [[1, 2], [{b: {x: 1, c: "scalar"}}], 2]},
                             {a: [[], [{b: {c: "scalar"}}]]});
    // 'b' is array.
    testIncludeOnlyADotBDotC({a: {b: [{c: "scalar"}, {c: "scalar2"}]}},
                             {a: {b: [{c: "scalar"}, {c: "scalar2"}]}});
    testIncludeOnlyADotBDotC({a: {b: [1, {c: "scalar"}, 2]}}, {a: {b: [{c: "scalar"}]}});
    testIncludeOnlyADotBDotC({a: {b: [[1, 2], [{c: "scalar"}], 2]}},
                             {a: {b: [[], [{c: "scalar"}]]}});
    // 'c' is array.
    testIncludeOnlyADotBDotC({a: {x: 1, b: {x: 1, c: ["scalar"]}}}, {a: {b: {c: ["scalar"]}}});
    testIncludeOnlyADotBDotC({a: {b: {c: [[1, 2], [{}], 2]}}}, {a: {b: {c: [[1, 2], [{}], 2]}}});

    // Two arrays.
    // 'b' and 'c' are arrays.
    testIncludeOnlyADotBDotC({a: {x: 1, b: [1, {c: [[1, 2], [{}], 2]}, 2]}},
                             {a: {b: [{c: [[1, 2], [{}], 2]}]}});
    testIncludeOnlyADotBDotC({a: {x: 1, b: [[1, 2], [{c: [[1, 2], [{}], 2]}], 2]}},
                             {a: {b: [[], [{c: [[1, 2], [{}], 2]}]]}});
    // 'a' and 'b' are arrays.
    testIncludeOnlyADotBDotC({a: [{b: [{c: "scalar"}]}]}, {a: [{b: [{c: "scalar"}]}]});
    testIncludeOnlyADotBDotC({a: [1, {b: [{c: {x: 1}}]}, 2]}, {a: [{b: [{c: {x: 1}}]}]});
    testIncludeOnlyADotBDotC({a: [1, {b: [1, {c: "scalar"}, 2]}, 2]}, {a: [{b: [{c: "scalar"}]}]});
    testIncludeOnlyADotBDotC({a: [1, {b: [[1, 2], [{c: "scalar"}], 2]}, 2]},
                             {a: [{b: [[], [{c: "scalar"}]]}]});
    testIncludeOnlyADotBDotC({a: [[1, 2], [{b: [1, {c: "scalar"}, 2]}], 2]},
                             {a: [[], [{b: [{c: "scalar"}]}]]});
    testIncludeOnlyADotBDotC({a: [[1, 2], [{b: [[1, 2], [{c: "scalar"}], 2]}], 2]},
                             {a: [[], [{b: [[], [{c: "scalar"}]]}]]});
    // 'a' and 'c' are arrays.
    testIncludeOnlyADotBDotC({a: [1, {b: {c: [1, {}, 2]}}, 2]}, {a: [{b: {c: [1, {}, 2]}}]});
    testIncludeOnlyADotBDotC({a: [1, {b: {x: 1, c: [[]]}}, 2]}, {a: [{b: {c: [[]]}}]});
    testIncludeOnlyADotBDotC({a: [1, {b: {x: 1, c: [1, {}, 2]}}, 2]}, {a: [{b: {c: [1, {}, 2]}}]});

    // Three arrays.
    testIncludeOnlyADotBDotC({a: [{b: [{c: ["scalar"]}]}]}, {a: [{b: [{c: ["scalar"]}]}]});
    testIncludeOnlyADotBDotC({a: [{b: [1, {c: ["scalar"]}, 2]}]}, {a: [{b: [{c: ["scalar"]}]}]});
    testIncludeOnlyADotBDotC({a: [{b: [[1, 2], [{c: ["scalar"]}], 2]}]},
                             {a: [{b: [[], [{c: ["scalar"]}]]}]});
    testIncludeOnlyADotBDotC({a: [1, {b: [[1, 2], [{c: [1, {}, 2]}], 2]}, 2]},
                             {a: [{b: [[], [{c: [1, {}, 2]}]]}]});
    testIncludeOnlyADotBDotC({a: [[1, 2], [{b: [[1, 2], [{c: [[1, 2], [{}], 2]}], 2]}], 2]},
                             {a: [[], [{b: [[], [{c: [[1, 2], [{}], 2]}]]}]]});
}());

// Now test the exclusion semantics. This part is a lot smaller since a lot of the behaviors mirror
// inclusion projection.
(function testExclusionSemantics() {
    // Test some basic top-level flat examples.
    testInputOutput({
        input: {_id: ++globalIdCounter, a: 1, b: 1, c: 1},
        projection: {a: 0},
        expectedOutput: {_id: globalIdCounter, b: 1, c: 1}
    });
    testInputOutput({
        input: {_id: ++globalIdCounter, a: 1, b: 1, c: 1},
        projection: {a: 0, b: 0},
        expectedOutput: {_id: globalIdCounter, c: 1}
    });

    // Test some dotted examples.
    testInputOutput({
        input: {_id: ++globalIdCounter, a: {b: 1, c: 1, d: 1}, x: {y: 1, z: 1}},
        projection: {"a.b": 0},
        expectedOutput: {_id: globalIdCounter, a: {c: 1, d: 1}, x: {y: 1, z: 1}}
    });
    // One notable difference between inclusion and exclusion projections is that parent's scalar
    // values remain untouched during an exclusion projection. The "scalar" here remains. In an
    // inclusion projection, these would disappear.
    testInputOutput({
        input: {_id: ++globalIdCounter, a: ["scalar", {b: 1, c: 1, d: 1}, {b: 2, c: 2}, {b: 3}]},
        projection: {"a.b": 0},
        expectedOutput: {_id: globalIdCounter, a: ["scalar", {c: 1, d: 1}, {c: 2}, {}]}
    });
    testInputOutput({
        input: {_id: ++globalIdCounter, a: [{b: 1, c: 1, d: 1}, {b: 2, c: 2}, {b: 3}]},
        projection: {"a.b": 0, "a.c": 0},
        expectedOutput: {_id: globalIdCounter, a: [{d: 1}, {}, {}]}
    });
    testInputOutput({
        input: {_id: ++globalIdCounter, a: [{b: 1, c: 1, d: 1}, {b: 2, c: 2}, {b: 3}]},
        projection: {"a.b": 0, "a.c": 0},
        expectedOutput: {_id: globalIdCounter, a: [{d: 1}, {}, {}]}
    });
    testInputOutput({
        input: {_id: ++globalIdCounter, a: []},
        projection: {"a.b": 0},
        expectedOutput: {_id: globalIdCounter, a: []}
    });
    testInputOutput({
        input: {_id: ++globalIdCounter, a: [[], [{b: [[1, 2], {c: 1, d: 1}]}]]},
        projection: {"a.b.c": 0},
        expectedOutput: {_id: globalIdCounter, a: [[], [{b: [[1, 2], {d: 1}]}]]}
    });
}());

// Now test the semantics of projecting a computed field. Again this is fairly similar to inclusion
// projections but with a few interesting twists.
(function testComputedProjections() {
    // Test some basic top-level flat examples.
    testInputOutput({
        input: {_id: ++globalIdCounter, a: 1, b: 1, c: 1},
        projection: {a: {$literal: 0}},
        expectedOutput: {_id: globalIdCounter, a: 0}
    });
    testInputOutput({
        input: {_id: ++globalIdCounter, a: 1, b: 1, c: 1},
        projection: {a: {$literal: 0}, b: "string"},
        expectedOutput: {_id: globalIdCounter, a: 0, b: "string"}
    });

    // Test some dotted examples.
    testInputOutput({
        input: {_id: ++globalIdCounter, a: {b: 1, c: 1, d: 1}, x: {y: 1, z: 1}},
        projection: {"a.b": "new value"},
        expectedOutput: {_id: globalIdCounter, a: {b: "new value"}}
    });
    // One notable difference for computed projections is that they overwrite scalars rather than
    // ignoring or removing them.
    testInputOutput({
        input: {_id: ++globalIdCounter, a: ["scalar", {b: 1, c: 1, d: 1}, {b: 2, c: 2}, {b: 3}]},
        projection: {"a.b": "new value"},
        expectedOutput: {
            _id: globalIdCounter,
            a: [{b: "new value"}, {b: "new value"}, {b: "new value"}, {b: "new value"}]
        }
    });
    testInputOutput({
        input: {_id: ++globalIdCounter, a: [{b: 1, c: 1, d: 1}, {b: 2, c: 2}, {b: 3}]},
        projection: {"a.b": "new value", "a.c": "new C"},
        expectedOutput: {
            _id: globalIdCounter,
            a: [
                {b: "new value", c: "new C"},
                {b: "new value", c: "new C"},
                {b: "new value", c: "new C"}
            ]
        }
    });
    testInputOutput({
        input: {_id: ++globalIdCounter, a: [{b: 1, c: 1, d: 1}, {b: 2, c: 2}, {b: 3}]},
        projection: {"a.b": "new value", "a.c": "new C"},
        expectedOutput: {
            _id: globalIdCounter,
            a: [
                {b: "new value", c: "new C"},
                {b: "new value", c: "new C"},
                {b: "new value", c: "new C"}
            ]
        }
    });
    testInputOutput({
        input: {_id: ++globalIdCounter, a: []},
        projection: {"a.b": "new value"},
        expectedOutput: {_id: globalIdCounter, a: []}
    });
    // Computed projections will traverse through double arrays and preserve structure. For example,
    // we preserve two brackets inside the existing 'b: [[1,2]]' and leave the empty array in 'a'
    // untouched rather than replace it with {b: {c: "new value"}}.
    testInputOutput({
        input: {_id: ++globalIdCounter, a: [[], [{b: [[1, 2], {c: 1, d: 1}]}]]},
        projection: {"a.b.c": "new value"},
        expectedOutput: {
            _id: globalIdCounter,
            a: [[], [{b: [[{c: "new value"}, {c: "new value"}], {c: "new value"}]}]]
        }
    });
}());

// Test that a numeric path component never describes an array index if used in a projection. We
// have the $slice operator for that functionality.
(function testNumericPathComponentsTreatedOnlyAsObjectKeyNames() {
    const kProjection = {_id: 0, "a.2": 1};
    const kIndexes = [{a: 1}];
    testInputOutput({
        input: {a: {"2": 99}, b: 1},
        projection: kProjection,
        expectedOutput: {a: {"2": 99}},
        interestingIndexes: kIndexes,
    });

    testInputOutput({
        input: {a: [0, 1, 2, 3, 4]},
        projection: kProjection,
        expectedOutput: {a: []},
        interestingIndexes: kIndexes,
    });

    testInputOutput({
        input: {a: [{x: 1}, {y: 1}, {z: 1}]},
        projection: kProjection,
        expectedOutput: {a: [{}, {}, {}]},
        interestingIndexes: kIndexes,
    });

    testInputOutput({
        input: {a: [0, 1, 2, 3, 4, {obj: 123}, {"2": 123}]},
        projection: kProjection,
        expectedOutput: {a: [{}, {"2": 123}]},
        interestingIndexes: kIndexes,
    });
})();

// Test some miscellaneous properties of projections.
(function testMiscellaneousProjections() {
    // Test including and excluding _id only.
    testInputOutput({
        input: {_id: ++globalIdCounter},
        projection: {_id: 1},
        expectedOutput: {_id: globalIdCounter},
        interestingIndexes: [{_id: 1}]
    });
    testInputOutput({
        input: {_id: ++globalIdCounter},
        projection: {_id: 0},
        expectedOutput: {},
    });
    testInputOutput({
        input: {_id: ++globalIdCounter, a: 1, b: 1},
        projection: {_id: 0},
        expectedOutput: {a: 1, b: 1},
    });

    // Test that you can specify nested paths with dots or as sub-objects and it'll mean the
    // same thing.
    testInputOutput({
        input: {measurements: {temperature: 20, pressure: 0.7, humidity: 0.4, time: new Date()}},
        projection: {'measurements.temperature': 1, 'measurements.pressure': 1, _id: 0},
        expectedOutput: {measurements: {temperature: 20, pressure: 0.7}},
        interestingIndexes: [{'measurements.temperature': 1, 'measurements.pressure': 1}]
    });
    testInputOutput({
        input: {measurements: {temperature: 20, pressure: 0.7, humidity: 0.4, time: new Date()}},
        projection: {measurements: {temperature: 1, pressure: 1}, _id: 0},
        expectedOutput: {measurements: {temperature: 20, pressure: 0.7}},
        interestingIndexes: [{'measurements.temperature': 1, 'measurements.pressure': 1}]
    });
    // Now with an exclusion projection.
    testInputOutput({
        input: {measurements: {temperature: 20, pressure: 0.7, humidity: 0.4, time: new Date()}},
        projection: {measurements: {humidity: 0, time: 0}, _id: 0},
        expectedOutput: {measurements: {temperature: 20, pressure: 0.7}},
    });
}());
