/**
 * Tests that the $lookup stage respects the collation when the local and/or foreign collections
 * are sharded.
 *
 * The comparison of string values between the 'localField' and 'foreignField' should use the
 * collation either explicitly set on the aggregation operation, or the collation inherited from the
 * collection the "aggregate" command was performed on.
 *
 * @tags: [
 *   requires_fcv_50,
 * ]
 */
(function() {
"use strict";

load("jstests/aggregation/extras/utils.js");                     // for arrayEq
load("jstests/noPassthrough/libs/server_parameter_helpers.js");  // For setParameterOnAllHosts.
load("jstests/libs/discover_topology.js");                       // For findDataBearingNodes.

function runTests(withDefaultCollationColl, withoutDefaultCollationColl, collation) {
    // Test that the $lookup stage respects the inherited collation.
    let res = withDefaultCollationColl
                      .aggregate([{
                          $lookup: {
                              from: withoutDefaultCollationColl.getName(),
                              localField: "str",
                              foreignField: "str",
                              as: "matched",
                          },
                      }])
                      .toArray();
    assert.eq(1, res.length, tojson(res));

    let expected = [{_id: "lowercase", str: "abc"}, {_id: "uppercase", str: "ABC"}];
    assert(
        arrayEq(expected, res[0].matched),
        "Expected " + tojson(expected) + " to equal " + tojson(res[0].matched) + " up to ordering");

    res = withDefaultCollationColl
                  .aggregate([{
                      $lookup: {
                          from: withoutDefaultCollationColl.getName(),
                          let : {str1: "$str"},
                          pipeline: [
                              {$match: {$expr: {$eq: ["$str", "$$str1"]}}},
                              {
                                $lookup: {
                                    from: withoutDefaultCollationColl.getName(),
                                    let : {str2: "$str"},
                                    pipeline: [{$match: {$expr: {$eq: ["$str", "$$str1"]}}}],
                                    as: "matched2"
                                }
                              }
                          ],
                          as: "matched1",
                      },
                  }])
                  .toArray();
    assert.eq(1, res.length, tojson(res));

    expected = [
        {
            "_id": "lowercase",
            "str": "abc",
            "matched2": [{"_id": "lowercase", "str": "abc"}, {"_id": "uppercase", "str": "ABC"}]
        },
        {
            "_id": "uppercase",
            "str": "ABC",
            "matched2": [{"_id": "lowercase", "str": "abc"}, {"_id": "uppercase", "str": "ABC"}]
        }
    ];
    assert(arrayEq(expected, res[0].matched1),
           "Expected " + tojson(expected) + " to equal " + tojson(res[0].matched1) +
               " up to ordering. " + tojson(res));

    // Test that the $lookup stage respects the inherited collation when it optimizes with an
    // $unwind stage.
    res = withDefaultCollationColl
                  .aggregate([
                      {
                        $lookup: {
                            from: withoutDefaultCollationColl.getName(),
                            localField: "str",
                            foreignField: "str",
                            as: "matched",
                        },
                      },
                      {$unwind: "$matched"},
                  ])
                  .toArray();
    assert.eq(2, res.length, tojson(res));

    expected = [
        {_id: "lowercase", str: "abc", matched: {_id: "lowercase", str: "abc"}},
        {_id: "lowercase", str: "abc", matched: {_id: "uppercase", str: "ABC"}}
    ];
    assert(arrayEq(expected, res),
           "Expected " + tojson(expected) + " to equal " + tojson(res) + " up to ordering");

    res = withDefaultCollationColl
                  .aggregate([
                      {
                        $lookup: {
                            from: withoutDefaultCollationColl.getName(),
                            let : {str1: "$str"},
                            pipeline: [
                                {$match: {$expr: {$eq: ["$str", "$$str1"]}}},
                                {
                                  $lookup: {
                                      from: withoutDefaultCollationColl.getName(),
                                      let : {str2: "$str"},
                                      pipeline: [{$match: {$expr: {$eq: ["$str", "$$str1"]}}}],
                                      as: "matched2"
                                  }
                                },
                                {$unwind: "$matched2"},
                            ],
                            as: "matched1",
                        },
                      },
                      {$unwind: "$matched1"},
                  ])
                  .toArray();
    assert.eq(4, res.length, tojson(res));

    expected = [
        {
            "_id": "lowercase",
            "str": "abc",
            "matched1":
                {"_id": "lowercase", "str": "abc", "matched2": {"_id": "lowercase", "str": "abc"}}
        },
        {
            "_id": "lowercase",
            "str": "abc",
            "matched1":
                {"_id": "lowercase", "str": "abc", "matched2": {"_id": "uppercase", "str": "ABC"}}
        },
        {
            "_id": "lowercase",
            "str": "abc",
            "matched1":
                {"_id": "uppercase", "str": "ABC", "matched2": {"_id": "lowercase", "str": "abc"}}
        },
        {
            "_id": "lowercase",
            "str": "abc",
            "matched1":
                {"_id": "uppercase", "str": "ABC", "matched2": {"_id": "uppercase", "str": "ABC"}}
        }
    ];
    assert(arrayEq(expected, res),
           "Expected " + tojson(expected) + " to equal " + tojson(res) + " up to ordering");

    // Test that the $lookup stage respects an explicit collation on the aggregation operation.
    res = withoutDefaultCollationColl
                  .aggregate(
                      [
                        {$match: {_id: "lowercase"}},
                        {
                          $lookup: {
                              from: withoutDefaultCollationColl.getName(),
                              localField: "str",
                              foreignField: "str",
                              as: "matched",
                          },
                        },
                      ],
                      collation)
                  .toArray();
    assert.eq(1, res.length, tojson(res));

    expected = [{_id: "lowercase", str: "abc"}, {_id: "uppercase", str: "ABC"}];
    assert(
        arrayEq(expected, res[0].matched),
        "Expected " + tojson(expected) + " to equal " + tojson(res[0].matched) + " up to ordering");

    res = withoutDefaultCollationColl
                  .aggregate(
                      [
                        {$match: {_id: "lowercase"}},
                        {
                          $lookup: {
                              from: withoutDefaultCollationColl.getName(),
                              let : {str1: "$str"},
                              pipeline: [
                                  {$match: {$expr: {$eq: ["$str", "$$str1"]}}},
                                  {
                                    $lookup: {
                                        from: withoutDefaultCollationColl.getName(),
                                        let : {str2: "$str"},
                                        pipeline: [{$match: {$expr: {$eq: ["$str", "$$str1"]}}}],
                                        as: "matched2"
                                    }
                                  }
                              ],
                              as: "matched1",
                          },
                        }
                      ],
                      collation)
                  .toArray();
    assert.eq(1, res.length, tojson(res));

    expected = [
        {
            "_id": "lowercase",
            "str": "abc",
            "matched2": [{"_id": "lowercase", "str": "abc"}, {"_id": "uppercase", "str": "ABC"}]
        },
        {
            "_id": "uppercase",
            "str": "ABC",
            "matched2": [{"_id": "lowercase", "str": "abc"}, {"_id": "uppercase", "str": "ABC"}]
        }
    ];
    assert(arrayEq(expected, res[0].matched1),
           "Expected " + tojson(expected) + " to equal " + tojson(res[0].matched1) +
               " up to ordering");

    // Test that the $lookup stage respects an explicit collation on the aggregation operation
    // when it optimizes with an $unwind stage.
    res = withoutDefaultCollationColl
                  .aggregate(
                      [
                        {$match: {_id: "lowercase"}},
                        {
                          $lookup: {
                              from: withoutDefaultCollationColl.getName(),
                              localField: "str",
                              foreignField: "str",
                              as: "matched",
                          },
                        },
                        {$unwind: "$matched"},
                      ],
                      collation)
                  .toArray();
    assert.eq(2, res.length, tojson(res));

    expected = [
        {_id: "lowercase", str: "abc", matched: {_id: "lowercase", str: "abc"}},
        {_id: "lowercase", str: "abc", matched: {_id: "uppercase", str: "ABC"}}
    ];
    assert(arrayEq(expected, res),
           "Expected " + tojson(expected) + " to equal " + tojson(res) + " up to ordering");

    res = withoutDefaultCollationColl
                  .aggregate(
                      [
                        {$match: {_id: "lowercase"}},
                        {
                          $lookup: {
                              from: withoutDefaultCollationColl.getName(),
                              let : {str1: "$str"},
                              pipeline: [
                                  {$match: {$expr: {$eq: ["$str", "$$str1"]}}},
                                  {
                                    $lookup: {
                                        from: withoutDefaultCollationColl.getName(),
                                        let : {str2: "$str"},
                                        pipeline: [{$match: {$expr: {$eq: ["$str", "$$str1"]}}}],
                                        as: "matched2"
                                    }
                                  },
                                  {$unwind: "$matched2"},
                              ],
                              as: "matched1",
                          },
                        },
                        {$unwind: "$matched1"},
                      ],
                      collation)
                  .toArray();
    assert.eq(4, res.length, tojson(res));

    expected = [
        {
            "_id": "lowercase",
            "str": "abc",
            "matched1":
                {"_id": "lowercase", "str": "abc", "matched2": {"_id": "lowercase", "str": "abc"}}
        },
        {
            "_id": "lowercase",
            "str": "abc",
            "matched1":
                {"_id": "lowercase", "str": "abc", "matched2": {"_id": "uppercase", "str": "ABC"}}
        },
        {
            "_id": "lowercase",
            "str": "abc",
            "matched1":
                {"_id": "uppercase", "str": "ABC", "matched2": {"_id": "lowercase", "str": "abc"}}
        },
        {
            "_id": "lowercase",
            "str": "abc",
            "matched1":
                {"_id": "uppercase", "str": "ABC", "matched2": {"_id": "uppercase", "str": "ABC"}}
        }
    ];
    assert(arrayEq(expected, res),
           "Expected " + tojson(expected) + " to equal " + tojson(res) + " up to ordering");

    // Test that an explicit collation on the $lookup stage is respected.
    res = withoutDefaultCollationColl
                  .aggregate(
                      [
                        {$match: {_id: "lowercase"}},
                        {
                          $lookup: {
                              from: withoutDefaultCollationColl.getName(),
                              localField: "str",
                              foreignField: "str",
                              as: "matched",
                              _internalCollation: collation["collation"],
                          },
                        },
                      ])
                  .toArray();
    assert.eq(1, res.length, tojson(res));

    expected = [{_id: "lowercase", str: "abc"}, {_id: "uppercase", str: "ABC"}];
    assert(
        arrayEq(expected, res[0].matched),
        "Expected " + tojson(expected) + " to equal " + tojson(res[0].matched) + " up to ordering");

    res = withoutDefaultCollationColl
                  .aggregate(
                      [
                        {$match: {_id: "lowercase"}},
                        {
                          $lookup: {
                              from: withoutDefaultCollationColl.getName(),
                              let : {str1: "$str"},
                              pipeline: [
                                  {$match: {$expr: {$eq: ["$str", "$$str1"]}}},
                                  {
                                    $lookup: {
                                        from: withoutDefaultCollationColl.getName(),
                                        let : {str2: "$str"},
                                        pipeline: [{$match: {$expr: {$eq: ["$str", "$$str1"]}}}],
                                        as: "matched2"
                                    }
                                  }
                              ],
                              as: "matched1",
                              _internalCollation: collation["collation"],
                          },
                        }
                      ])
                  .toArray();
    assert.eq(1, res.length, tojson(res));

    expected = [
        {
            "_id": "lowercase",
            "str": "abc",
            "matched2": [{"_id": "lowercase", "str": "abc"}, {"_id": "uppercase", "str": "ABC"}]
        },
        {
            "_id": "uppercase",
            "str": "ABC",
            "matched2": [{"_id": "lowercase", "str": "abc"}, {"_id": "uppercase", "str": "ABC"}]
        }
    ];
    assert(arrayEq(expected, res[0].matched1),
           "Expected " + tojson(expected) + " to equal " + tojson(res[0].matched1) +
               " up to ordering");

    // Test that an explicit collation on the $lookup stage takes precedence over a command
    // collation.
    res = withoutDefaultCollationColl
                  .aggregate(
                      [
                        {$match: {_id: "lowercase"}},
                        {
                          $lookup: {
                              from: withoutDefaultCollationColl.getName(),
                              localField: "str",
                              foreignField: "str",
                              as: "matched",
                              _internalCollation: {locale: "simple"},
                          },
                        },
                      ],
                      collation)
                  .toArray();
    assert.eq(1, res.length, tojson(res));

    expected = [{_id: "lowercase", str: "abc"}];
    assert(
        arrayEq(expected, res[0].matched),
        "Expected " + tojson(expected) + " to equal " + tojson(res[0].matched) + " up to ordering");

    res = withoutDefaultCollationColl
                  .aggregate(
                      [
                        {$match: {_id: "lowercase"}},
                        {
                          $lookup: {
                              from: withoutDefaultCollationColl.getName(),
                              let : {str1: "$str"},
                              pipeline: [
                                  {$match: {$expr: {$eq: ["$str", "$$str1"]}}},
                                  {
                                    $lookup: {
                                        from: withoutDefaultCollationColl.getName(),
                                        let : {str2: "$str"},
                                        pipeline: [{$match: {$expr: {$eq: ["$str", "$$str1"]}}}],
                                        as: "matched2"
                                    }
                                  }
                              ],
                              as: "matched1",
                              _internalCollation: {locale: "simple"},
                          },
                        }
                      ],collation)
                  .toArray();
    assert.eq(1, res.length, tojson(res));

    expected =
        [{"_id": "lowercase", "str": "abc", "matched2": [{"_id": "lowercase", "str": "abc"}]}];
    assert(arrayEq(expected, res[0].matched1),
           "Expected " + tojson(expected) + " to equal " + tojson(res[0].matched1) +
               " up to ordering");

    // Test that the $lookup stage uses the "simple" collation if a collation isn't set on the
    // collection or the aggregation operation, even if the foreign collection has a collation.
    // TODO SERVER-38830: when a pipeline $lookup is capable of serializing its 'let' variables to
    // remote shards, add a test-case to exercise SERVER-43350.
    // TODO SERVER-32536: Enable the following test.
    // res = withoutDefaultCollationColl
    // .aggregate([
    // {$match: {_id: "lowercase"}},
    // {
    // $lookup: {
    // from: withDefaultCollationColl.getName(),
    // localField: "str",
    // foreignField: "str",
    // as: "matched",
    // },
    // },
    // ])
    // .toArray();
    // assert.eq([{_id: "lowercase", str: "abc", matched: [{_id: "lowercase", str: "abc"}]}], res);

    res = withoutDefaultCollationColl
                  .aggregate([
                      {$match: {_id: "lowercase"}},
                      {
                        $lookup: {
                            from: withoutDefaultCollationColl.getName(),
                            let : {str1: "$str"},
                            pipeline: [
                                {$match: {$expr: {$eq: ["$str", "$$str1"]}}},
                                {
                                  $lookup: {
                                      from: withoutDefaultCollationColl.getName(),
                                      let : {str2: "$str"},
                                      pipeline: [{$match: {$expr: {$eq: ["$str", "$$str1"]}}}],
                                      as: "matched2"
                                  }
                                },
                                {$unwind: "$matched2"},
                            ],
                            as: "matched1",
                        },
                      },
                  ])
                  .toArray();
    assert.eq(
        [{
            "_id": "lowercase",
            "str": "abc",
            "matched1":
                [{"_id": "lowercase", "str": "abc", "matched2": {"_id": "lowercase", "str": "abc"}}]
        }],
        res);
}

const st = new ShardingTest({shards: 2});
// TODO SERVER-38830: when a pipeline $lookup is capable of serializing its 'let' variables to
// remote shards this can be re-enabled.
// setParameterOnAllHosts(
// DiscoverTopology.findNonConfigNodes(st.s), "internalQueryAllowShardedLookup", true);

const testName = "collation_lookup";
const caseInsensitive = {
    collation: {locale: "en_US", strength: 2}
};

const mongosDB = st.s0.getDB(testName);
const withDefaultCollationColl = mongosDB[testName + "_with_default"];
const withoutDefaultCollationColl = mongosDB[testName + "_without_default"];

assert.commandWorked(
    mongosDB.createCollection(withDefaultCollationColl.getName(), caseInsensitive));
assert.commandWorked(withDefaultCollationColl.insert({_id: "lowercase", str: "abc"}));

assert.commandWorked(withoutDefaultCollationColl.insert({_id: "lowercase", str: "abc"}));
assert.commandWorked(withoutDefaultCollationColl.insert({_id: "uppercase", str: "ABC"}));
assert.commandWorked(withoutDefaultCollationColl.insert({_id: "unmatched", str: "def"}));

//
// Sharded collection with default collation and unsharded collection without a default
// collation.
//
assert.commandWorked(
    withDefaultCollationColl.createIndex({str: 1}, {collation: {locale: "simple"}}));

// Enable sharding on the test DB and ensure its primary is shard0000.
assert.commandWorked(mongosDB.adminCommand({enableSharding: mongosDB.getName()}));
st.ensurePrimaryShard(mongosDB.getName(), st.shard0.shardName);

// Shard the collection with a default collation.
assert.commandWorked(mongosDB.adminCommand({
    shardCollection: withDefaultCollationColl.getFullName(),
    key: {str: 1},
    collation: {locale: "simple"}
}));

// Split the collection into 2 chunks.
assert.commandWorked(
    mongosDB.adminCommand({split: withDefaultCollationColl.getFullName(), middle: {str: "abc"}}));

// Move the chunk containing {str: "abc"} to shard0001.
assert.commandWorked(mongosDB.adminCommand({
    moveChunk: withDefaultCollationColl.getFullName(),
    find: {str: "abc"},
    to: st.shard1.shardName
}));

runTests(withDefaultCollationColl, withoutDefaultCollationColl, caseInsensitive);

// TODO: Enable the following tests once SERVER-32536 is fixed.
//
// Sharded collection with default collation and sharded collection without a default
// collation.
//

// Shard the collection without a default collation.
// assert.commandWorked(mongosDB.adminCommand({
//     shardCollection: withoutDefaultCollationColl.getFullName(),
//     key: {_id: 1},
// }));

// // Split the collection into 2 chunks.
// assert.commandWorked(mongosDB.adminCommand(
//     {split: withoutDefaultCollationColl.getFullName(), middle: {_id: "unmatched"}}));

// // Move the chunk containing {_id: "lowercase"} to shard0001.
// assert.commandWorked(mongosDB.adminCommand({
//     moveChunk: withoutDefaultCollationColl.getFullName(),
//     find: {_id: "lowercase"},
//     to: st.shard1.shardName
// }));

// runTests(withDefaultCollationColl, withoutDefaultCollationColl, caseInsensitive);

st.stop();
})();
