/**
 *    Copyright (C) 2018-present MongoDB, Inc.
 *
 *    This program is free software: you can redistribute it and/or modify
 *    it under the terms of the Server Side Public License, version 1,
 *    as published by MongoDB, Inc.
 *
 *    This program is distributed in the hope that it will be useful,
 *    but WITHOUT ANY WARRANTY; without even the implied warranty of
 *    MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
 *    Server Side Public License for more details.
 *
 *    You should have received a copy of the Server Side Public License
 *    along with this program. If not, see
 *    <http://www.mongodb.com/licensing/server-side-public-license>.
 *
 *    As a special exception, the copyright holders give permission to link the
 *    code of portions of this program with the OpenSSL library under certain
 *    conditions as described in each individual source file and distribute
 *    linked combinations including the program with the OpenSSL library. You
 *    must comply with the Server Side Public License in all respects for
 *    all of the code used other than as permitted herein. If you modify file(s)
 *    with this exception, you may extend this exception to your version of the
 *    file(s), but you are not obligated to do so. If you do not wish to do so,
 *    delete this exception statement from your version. If you delete this
 *    exception statement from all source files in the program, then also delete
 *    it in the license file.
 */

#include "mongo/platform/basic.h"

#include "mongo/db/query/collation/collator_interface_mock.h"
#include "mongo/db/query/query_planner_test_fixture.h"

namespace {

using namespace mongo;

TEST_F(QueryPlannerTest, StringComparisonWithNullCollatorOnIndexResultsInCollscan) {
    addIndex(fromjson("{a: 1}"));

    runQueryAsCommand(
        fromjson("{find: 'testns', filter: {a: {$lt: 'foo'}}, collation: {locale: 'reverse'}}"));

    assertNumSolutions(1U);
    assertSolutionExists("{cscan: {dir: 1}}");
}

TEST_F(QueryPlannerTest, StringComparisonWithNullCollatorOnQueryResultsInCollscan) {
    CollatorInterfaceMock collator(CollatorInterfaceMock::MockType::kAlwaysEqual);
    addIndex(fromjson("{a: 1}"), &collator);

    runQuery(fromjson("{a: {$lt: 'foo'}}"));

    assertNumSolutions(1U);
    assertSolutionExists("{cscan: {dir: 1}}");
}

TEST_F(QueryPlannerTest, StringComparisonWithUnequalCollatorsResultsInCollscan) {
    CollatorInterfaceMock alwaysEqualCollator(CollatorInterfaceMock::MockType::kAlwaysEqual);
    addIndex(fromjson("{a: 1}"), &alwaysEqualCollator);

    runQueryAsCommand(
        fromjson("{find: 'testns', filter: {a: {$lt: 'foo'}}, collation: {locale: 'reverse'}}"));

    assertNumSolutions(1U);
    assertSolutionExists("{cscan: {dir: 1}}");
}

TEST_F(QueryPlannerTest, StringComparisonWithMatchingCollationUsesIndexWithTransformedBounds) {
    CollatorInterfaceMock collator(CollatorInterfaceMock::MockType::kReverseString);
    addIndex(fromjson("{a: 1}"), &collator);

    runQueryAsCommand(
        fromjson("{find: 'testns', filter: {a: {$lt: 'foo'}}, collation: {locale: 'reverse'}}"));

    assertNumSolutions(2U);
    assertSolutionExists("{cscan: {dir: 1}}");
    assertSolutionExists(
        "{fetch: {filter: null, collation: {locale: 'reverse'}, node: {ixscan: "
        "{pattern: {a: 1}, filter: null, "
        "bounds: {a: [['', 'oof', true, false]]}}}}}");
}

TEST_F(QueryPlannerTest, StringComparisonAndNonStringComparisonCanUseSeparateIndices) {
    CollatorInterfaceMock reverseStringCollator(CollatorInterfaceMock::MockType::kReverseString);
    CollatorInterfaceMock alwaysEqualCollator(CollatorInterfaceMock::MockType::kAlwaysEqual);

    addIndex(fromjson("{a: 1}"), &reverseStringCollator);
    addIndex(fromjson("{b: 1}"), &alwaysEqualCollator);

    // The string predicate can use index {a: 1}, since the collators match. The non-string
    // comparison can use index {b: 1}, even though the collators don't match.
    runQueryAsCommand(
        fromjson("{find: 'testns', filter: {a: {$lt: 'foo'}, b: {$lte: 4}}, collation: {locale: "
                 "'reverse'}}"));

    assertNumSolutions(3U);
    assertSolutionExists("{cscan: {dir: 1}}");
    assertSolutionExists(
        "{fetch: {filter: {b: {$lte: 4}}, collation: {locale: 'reverse'}, node: "
        "{ixscan: {pattern: {a: 1}, "
        "filter: null, bounds: {a: [['', 'oof', true, false]]}}}}}");
    assertSolutionExists(
        "{fetch: {filter: {a: {$lt: 'foo'}}, collation: {locale: 'reverse'}, node: {ixscan: "
        "{pattern: {b: 1}, filter: null, "
        "bounds: {b: [[-Infinity, 4, true, true]]}}}}}");
}

TEST_F(QueryPlannerTest, StringEqWrtCollatorCannotBeCovered) {
    CollatorInterfaceMock collator(CollatorInterfaceMock::MockType::kReverseString);
    addIndex(fromjson("{a: 1}"), &collator);

    runQueryAsCommand(
        fromjson("{find: 'testns', filter: {a: 'string'}, projection: {_id: 0, a: 1}, collation: "
                 "{locale: 'reverse'}}"));

    assertNumSolutions(2U);
    assertSolutionExists("{proj: {spec: {_id: 0, a: 1}, node: {cscan: {dir: 1}}}}");
    assertSolutionExists(
        "{proj: {spec: {_id: 0, a: 1}, node: {fetch: {filter: null, collation: "
        "{locale: 'reverse'}, node: "
        "{ixscan: {pattern: {a: 1}, filter: null, bounds: {a: [['gnirts', 'gnirts', true, "
        "true]]}}}}}}}");
}

TEST_F(QueryPlannerTest, StringGteWrtCollatorCannotBeCovered) {
    CollatorInterfaceMock collator(CollatorInterfaceMock::MockType::kReverseString);
    addIndex(fromjson("{a: 1}"), &collator);

    runQueryAsCommand(fromjson(
        "{find: 'testns', filter: {a: {$gte: 'string'}}, projection: {_id: 0, a: 1}, collation: "
        "{locale: 'reverse'}}"));

    assertNumSolutions(2U);
    assertSolutionExists("{proj: {spec: {_id: 0, a: 1}, node: {cscan: {dir: 1}}}}");
    assertSolutionExists(
        "{proj: {spec: {_id: 0, a: 1}, node: {fetch: {filter: null, collation: "
        "{locale: 'reverse'}, node: "
        "{ixscan: {pattern: {a: 1}, filter: null, bounds: {a: [['gnirts', {}, true, "
        "false]]}}}}}}}");
}

TEST_F(QueryPlannerTest, InContainingStringCannotBeCoveredWithCollator) {
    CollatorInterfaceMock collator(CollatorInterfaceMock::MockType::kReverseString);
    addIndex(fromjson("{a: 1}"), &collator);

    runQueryAsCommand(fromjson(
        "{find: 'testns', filter: {a: {$in: [2, 'foo']}}, projection: {_id: 0, a: 1}, collation: "
        "{locale: 'reverse'}}"));

    assertNumSolutions(2U);
    assertSolutionExists("{proj: {spec: {_id: 0, a: 1}, node: {cscan: {dir: 1}}}}");
    assertSolutionExists(
        "{proj: {spec: {_id: 0, a: 1}, node: {fetch: {filter: null, collation: "
        "{locale: 'reverse'}, node: "
        "{ixscan: {pattern: {a: 1}, filter: null, bounds: {a: [[2,2,true,true],"
        "['oof','oof',true,true]]}}}}}}}");
}

TEST_F(QueryPlannerTest, TypeStringCannotBeCoveredWithCollator) {
    CollatorInterfaceMock collator(CollatorInterfaceMock::MockType::kReverseString);
    addIndex(fromjson("{a: 1}"), &collator);

    runQueryAsCommand(fromjson(
        "{find: 'testns', filter: {a: {$type: 'string'}}, projection: {_id: 0, a: 1}, collation: "
        "{locale: 'reverse'}}"));

    assertNumSolutions(2U);
    assertSolutionExists("{proj: {spec: {_id: 0, a: 1}, node: {cscan: {dir: 1}}}}");
    assertSolutionExists(
        "{proj: {spec: {_id: 0, a: 1}, node: {fetch: {filter: {a:{$type:'string'}}, collation: "
        "{locale: 'reverse'}, node: {ixscan: {pattern: {a: 1}, filter: null, "
        "bounds: {a: [['',{},true,false]]}}}}}}}");
}

TEST_F(QueryPlannerTest, NotWithStringBoundsCannotBeCoveredWithCollator) {
    CollatorInterfaceMock collator(CollatorInterfaceMock::MockType::kReverseString);
    addIndex(fromjson("{a: 1}"), &collator);

    runQueryAsCommand(
        fromjson("{find: 'testns', filter: {a: {$ne: 2}}, projection: {_id: 0, a: 1}, collation: "
                 "{locale: 'reverse'}}"));

    assertNumSolutions(2U);
    assertSolutionExists("{proj: {spec: {_id: 0, a: 1}, node: {cscan: {dir: 1}}}}");
    assertSolutionExists(
        "{proj: {spec: {_id: 0, a: 1}, node: {fetch: {filter: null, collation: "
        "{locale: 'reverse'}, node: {ixscan: {pattern: {a: 1}, filter: null, "
        "bounds: {a: [['MinKey',2,true,false], [2,'MaxKey',false,true]]}}}}}}}");
}

TEST_F(QueryPlannerTest, ExistsTrueCannotBeCoveredWithSparseIndexAndCollator) {
    CollatorInterfaceMock collator(CollatorInterfaceMock::MockType::kReverseString);
    addIndex(fromjson("{a: 1}"), &collator);
    params.indices.back().sparse = true;

    runQueryAsCommand(fromjson(
        "{find: 'testns', filter: {a: {$exists: true}}, projection: {_id: 0, a: 1}, collation: "
        "{locale: 'reverse'}}"));

    assertNumSolutions(2U);
    assertSolutionExists("{proj: {spec: {_id: 0, a: 1}, node: {cscan: {dir: 1}}}}");
    assertSolutionExists(
        "{proj: {spec: {_id: 0, a: 1}, node: {fetch: {filter: null, collation: "
        "{locale: 'reverse'}, node: {ixscan: {pattern: {a: 1}, filter: null, "
        "bounds: {a: [['MinKey','MaxKey',true,true]]}}}}}}}");
}

TEST_F(QueryPlannerTest, MinMaxWithStringBoundsCannotBeCoveredWithCollator) {
    CollatorInterfaceMock collator(CollatorInterfaceMock::MockType::kReverseString);
    addIndex(fromjson("{a: 1, b: 1}"), &collator);

    runQueryAsCommand(fromjson(
        "{find: 'testns', min: {a: 1, b: 2}, max: {a: 2, b: 1}, "
        "projection: {_id: 0, a: 1, b: 1}, hint: {a: 1, b: 1}, collation: {locale: 'reverse'}}"));

    assertNumSolutions(1U);
    assertSolutionExists(
        "{proj: {spec: {_id: 0, a: 1, b: 1}, node: {fetch: {filter: null, collation: "
        "{locale: 'reverse'}, node: {ixscan: {pattern: {a: 1, b: 1}}}}}}}");
}

TEST_F(QueryPlannerTest, SimpleRegexCanUseAnIndexWithACollatorWithLooseBounds) {
    CollatorInterfaceMock collator(CollatorInterfaceMock::MockType::kReverseString);
    addIndex(fromjson("{a: 1}"), &collator);

    // Since the index has a collation, the regex must be applied after fetching the documents
    // (INEXACT_FETCH tightness).
    runQueryAsCommand(
        fromjson("{find: 'testns', filter: {a: /^simple/}, collation: {locale: 'reverse'}}"));

    assertNumSolutions(2U);
    assertSolutionExists("{cscan: {dir: 1}}");
    assertSolutionExists(
        "{fetch: {filter: {a: /^simple/}, node: {ixscan: {pattern: {a: 1}, filter: null, bounds: "
        "{a: [['', {}, true, false], [/^simple/, /^simple/, true, true]]}}}}}");
}

TEST_F(QueryPlannerTest, SimpleRegexCanUseAnIndexWithoutACollatorWithTightBounds) {
    addIndex(fromjson("{a: 1}"));

    runQueryAsCommand(
        fromjson("{find: 'testns', filter: {a: /^simple/}, collation: {locale: 'reverse'}}"));

    assertNumSolutions(2U);
    assertSolutionExists("{cscan: {dir: 1}}");
    assertSolutionExists(
        "{fetch: {filter: null, node: {ixscan: {pattern: {a: 1}, filter: null, bounds: "
        "{a: [['simple', 'simplf', true, false], [/^simple/, /^simple/, true, true]]}}}}}");
}

TEST_F(QueryPlannerTest, NonSimpleRegexCanUseAnIndexWithoutACollatorAsInexactCovered) {
    addIndex(fromjson("{a: 1}"));

    runQueryAsCommand(
        fromjson("{find: 'testns', filter: {a: /nonsimple/}, collation: {locale: 'reverse'}}"));

    assertNumSolutions(2U);
    assertSolutionExists("{cscan: {dir: 1}}");
    assertSolutionExists(
        "{fetch: {filter: null, node: {ixscan: {pattern: {a: 1}, filter: {a: /nonsimple/}, bounds: "
        "{a: [['', {}, true, false], [/nonsimple/, /nonsimple/, true, true]]}}}}}");
}

TEST_F(QueryPlannerTest, AccessPlannerCorrectlyCombinesComparisonKeyBounds) {
    CollatorInterfaceMock collator(CollatorInterfaceMock::MockType::kReverseString);
    addIndex(fromjson("{a: 1, b: 1}"), &collator);

    runQueryAsCommand(fromjson(
        "{find: 'testns', filter: {a: {$gte: 'foo', $lte: 'zfoo'}, b: 'bar'}, collation: {locale: "
        "'reverse'}}"));

    assertNumSolutions(2U);
    assertSolutionExists("{cscan: {dir: 1}}");
    assertSolutionExists(
        "{fetch: {filter: null, collation: {locale: 'reverse'}, "
        "node: {ixscan: {pattern: {a: 1, b: "
        "1}, filter: null, bounds: {a: [['oof','oofz',true,true]], b: "
        "[['rab','rab',true,true]]}}}}}");
}

TEST_F(QueryPlannerTest, OrQueryResultsInCollscanWhenOnlyOneBranchHasIndexWithMatchingCollation) {
    CollatorInterfaceMock reverseStringCollator(CollatorInterfaceMock::MockType::kReverseString);
    CollatorInterfaceMock alwaysEqualCollator(CollatorInterfaceMock::MockType::kAlwaysEqual);

    addIndex(fromjson("{a: 1}"), &reverseStringCollator);
    addIndex(fromjson("{b: 1}"), &alwaysEqualCollator);

    runQueryAsCommand(
        fromjson("{find: 'testns', filter: {$or: [{a: 'foo'}, {b: 'bar'}]}, collation: {locale: "
                 "'reverse'}}"));

    assertNumSolutions(1U);
    assertSolutionExists("{cscan: {dir: 1}}");
}

TEST_F(QueryPlannerTest, OrQueryCanBeIndexedWhenBothBranchesHaveIndexWithMatchingCollation) {
    CollatorInterfaceMock collator(CollatorInterfaceMock::MockType::kReverseString);
    addIndex(fromjson("{a: 1}"), &collator);
    addIndex(fromjson("{b: 1}"), &collator);

    runQueryAsCommand(
        fromjson("{find: 'testns', filter: {$or: [{a: 'foo'}, {b: 'bar'}]}, collation: {locale: "
                 "'reverse'}}"));

    assertNumSolutions(2U);
    assertSolutionExists("{cscan: {dir: 1}}");
    assertSolutionExists(
        "{fetch: {node: {or: {nodes: ["
        "{ixscan: {pattern: {a: 1}, bounds: {a: [['oof','oof',true,true]]}}},"
        "{ixscan: {pattern: {b: 1}, bounds: {b: [['rab','rab',true,true]]}}}]}}}}");
}

TEST_F(QueryPlannerTest, ElemMatchObjectResultsInCorrectComparisonKeyBounds) {
    CollatorInterfaceMock collator(CollatorInterfaceMock::MockType::kReverseString);
    addIndex(fromjson("{'a.b': 1}"), &collator);

    runQueryAsCommand(fromjson(
        "{find: 'testns', filter: {a: {$elemMatch: {b: {$gte: 'foo', $lte: 'zfoo'}}}}, collation: "
        "{locale: "
        "'reverse'}}"));

    assertNumSolutions(2U);
    assertSolutionExists("{cscan: {dir: 1}}");
    assertSolutionExists(
        "{fetch: {filter: {a:{$elemMatch:{b:{$gte:'foo',$lte:'zfoo'}}}}, collation: {locale: "
        "'reverse'}, node: {ixscan: {pattern: "
        "{'a.b': 1}, filter: null, bounds: {'a.b': [['oof','oofz',true,true]]}}}}}");
}

TEST_F(QueryPlannerTest, QueryForNestedObjectWithMatchingCollatorCanUseIndex) {
    CollatorInterfaceMock collator(CollatorInterfaceMock::MockType::kReverseString);
    addIndex(fromjson("{a: 1}"), &collator);

    runQueryAsCommand(
        fromjson("{find: 'testns', filter: {a: {b: 1}}, collation: {locale: 'reverse'}}"));

    assertNumSolutions(2U);
    assertSolutionExists("{cscan: {dir: 1}}");
    assertSolutionExists("{fetch: {node: {ixscan: {pattern: {a: 1}}}}}");
}

TEST_F(QueryPlannerTest, QueryForNestedObjectWithNonMatchingCollatorCantUseIndexWithCollator) {
    CollatorInterfaceMock indexCollator(CollatorInterfaceMock::MockType::kReverseString);
    addIndex(fromjson("{a: 1}"), &indexCollator);

    runQuery(fromjson("{a: {b: 1}}"));

    assertNumSolutions(1U);
    assertSolutionExists("{cscan: {dir: 1}}");
}

TEST_F(QueryPlannerTest, CannotUseIndexWithNonMatchingCollatorForSort) {
    CollatorInterfaceMock indexCollator(CollatorInterfaceMock::MockType::kReverseString);
    addIndex(fromjson("{a: 1}"), &indexCollator);

    runQueryAsCommand(fromjson("{find: 'testns', filter: {b: 1}, sort: {a: 1}}"));

    assertNumSolutions(1U);
    assertSolutionExists(
        "{sort: {pattern: {a: 1}, type: 'simple', limit: 0,"
        "node: {cscan: {dir: 1, filter: {b: 1}}}}}");
}

TEST_F(QueryPlannerTest, CanUseIndexWithMatchingCollatorForSort) {
    CollatorInterfaceMock indexCollator(CollatorInterfaceMock::MockType::kReverseString);
    addIndex(fromjson("{a: 1}"), &indexCollator);

    runQueryAsCommand(
        fromjson("{find: 'testns', filter: {b: 1}, sort: {a: 1}, collation: {locale: 'reverse'}}"));

    assertNumSolutions(2U);
    assertSolutionExists(
        "{sort: {pattern: {a: 1}, type: 'simple', limit: 0,"
        "node: {cscan: {dir: 1, filter: {b: 1}, collation: {locale: 'reverse'}}}}}");
    assertSolutionExists(
        "{fetch: {filter: {b: 1}, collation: {locale: 'reverse'}, node: {ixscan: {pattern: {a: "
        "1}}}}}");
}

TEST_F(QueryPlannerTest, IndexWithNonMatchingCollatorCausesInMemorySort) {
    CollatorInterfaceMock indexCollator(CollatorInterfaceMock::MockType::kReverseString);
    addIndex(fromjson("{a: 1}"), &indexCollator);

    runQueryAsCommand(fromjson("{find: 'testns', filter: {a: {'$exists': true}}, sort: {a: 1}}"));

    assertNumSolutions(2U);
    assertSolutionExists(
        "{sort: {pattern: {a: 1}, limit: 0, type: 'simple', node:"
        "{fetch: {node : {ixscan: {pattern: {a: 1}}}}}}}");
    assertSolutionExists(
        "{sort: {pattern: {a: 1}, limit: 0, type: 'simple', node:"
        "{cscan: {dir: 1, filter: {a: {'$exists': true}}}}}}");
}

TEST_F(QueryPlannerTest, IndexWithMatchingCollatorDoesNotCauseInMemorySort) {
    CollatorInterfaceMock indexCollator(CollatorInterfaceMock::MockType::kReverseString);
    addIndex(fromjson("{a: 1}"), &indexCollator);

    runQueryAsCommand(
        fromjson("{find: 'testns', filter: {a: {'$exists': true}}, sort: {a: 1},"
                 "collation: {locale: 'reverse'}}"));

    assertNumSolutions(2U);
    assertSolutionExists("{fetch: {node : {ixscan: {pattern: {a: 1}}}}}");
    assertSolutionExists(
        "{sort: {pattern: {a: 1}, limit: 0, type: 'simple', node:"
        "{cscan: {dir: 1, filter: {a: {'$exists': true}}}}}}}}");
}

TEST_F(QueryPlannerTest, CompoundIndexWithNonMatchingCollatorCausesInMemorySort) {
    CollatorInterfaceMock indexCollator(CollatorInterfaceMock::MockType::kReverseString);
    addIndex(fromjson("{a: 1, b: 1, c: 1, d: 1}"), &indexCollator);

    runQueryAsCommand(
        fromjson("{find: 'testns', filter: {a: 1, b: 2, c: {a: 1}},"
                 "sort: {a: 1, b: 1, c: 1}}"));

    assertNumSolutions(2U);
    assertSolutionExists(
        "{sort: {pattern: {a: 1, b: 1, c: 1}, limit: 0, type: 'simple', node:"
        "{fetch: {node: {ixscan: {pattern: {a: 1, b: 1, c: 1, d: 1}}}}}}}");
    assertSolutionExists(
        "{sort: {pattern: {a: 1, b: 1, c: 1}, limit: 0, type: 'simple', node:"
        "{cscan: {dir: 1, filter: {a: 1, b: 2, c: {a: 1}}}}}}");
}

TEST_F(QueryPlannerTest, CompoundIndexWithNonMatchingPrefixedCollatorDoesNotCauseInMemorySort) {
    CollatorInterfaceMock indexCollator(CollatorInterfaceMock::MockType::kReverseString);
    addIndex(fromjson("{a: 1, b: 1, c: 1, d: 1}"), &indexCollator);

    runQueryAsCommand(
        fromjson("{find: 'testns', filter: {a: 1, b: 2, c: {a: 1 } },"
                 "sort: {a: 1, b: 1 }}"));

    assertNumSolutions(2U);
    assertSolutionExists("{fetch: {node : {ixscan: {pattern: {a: 1, b: 1, c: 1, d: 1}}}}}");
    assertSolutionExists(
        "{sort: {pattern: {a: 1, b: 1}, limit: 0, type: 'simple', node: "
        "{cscan: {dir: 1, filter : {a: 1, b: 2, c: {a: 1}}}}}}");
}

TEST_F(QueryPlannerTest, SuccessfullyPlanWhenMinMaxHaveNumberBoundariesAndCollationsDontMatch) {
    CollatorInterfaceMock indexCollator(CollatorInterfaceMock::MockType::kToLowerString);
    addIndex(fromjson("{a: 1, b: 1, c: 1}"), &indexCollator);

    runQueryAsCommand(
        fromjson("{find: 'testns', min: {a: 1, b: 1, c: 1}, max: {a: 3, b: 3, c: 3},"
                 "hint: {a:1, b: 1, c: 1}}"));

    assertNumSolutions(1U);
    assertSolutionExists("{fetch: {node: {ixscan: {pattern: {a: 1, b: 1, c: 1}}}}}");
}

TEST_F(QueryPlannerTest, FailToPlanWhenMinHasStringBoundaryAndCollationsDontMatch) {
    CollatorInterfaceMock indexCollator(CollatorInterfaceMock::MockType::kToLowerString);
    addIndex(fromjson("{a: 1, b: 1, c: 1}"), &indexCollator);
    runInvalidQueryAsCommand(
        fromjson("{find: 'testns', min: {a: 1, b: 'foo', c: 1}, "
                 "hint: {a: 1, b: 1, c: 1}}"));
}

TEST_F(QueryPlannerTest, FailToPlanWhenMaxHasStringBoundaryAndCollationsDontMatch) {
    CollatorInterfaceMock indexCollator(CollatorInterfaceMock::MockType::kToLowerString);
    addIndex(fromjson("{a: 1, b: 1, c: 1}"), &indexCollator);
    runInvalidQueryAsCommand(
        fromjson("{find: 'testns', max: {a: 1, b: 'foo', c: 1}, hint: {a: 1, b: 1, c: 1}}"));
}

TEST_F(QueryPlannerTest, FailToPlanWhenMinHasObjectBoundaryAndCollationsDontMatch) {
    CollatorInterfaceMock indexCollator(CollatorInterfaceMock::MockType::kToLowerString);
    addIndex(fromjson("{a: 1, b: 1, c: 1}"), &indexCollator);
    runInvalidQueryAsCommand(
        fromjson("{find: 'testns', min: {a: 1, b: {d: 'foo'}, c: 1}, "
                 "hint: {a: 1, b: 1, c: 1}}"));
}

TEST_F(QueryPlannerTest, FailToPlanWhenMaxHasObjectBoundaryAndCollationsDontMatch) {
    CollatorInterfaceMock indexCollator(CollatorInterfaceMock::MockType::kToLowerString);
    addIndex(fromjson("{a: 1, b: 1, c: 1}"), &indexCollator);
    runInvalidQueryAsCommand(
        fromjson("{find: 'testns', max: {a: 1, b: {d: 'foo'}, c: 1}, "
                 "hint: {a: 1, b: 1, c: 1}}"));
}

TEST_F(QueryPlannerTest, FailToPlanWhenMinHasArrayBoundaryAndCollationsDontMatch) {
    CollatorInterfaceMock indexCollator(CollatorInterfaceMock::MockType::kToLowerString);
    addIndex(fromjson("{a: 1, b: 1, c: 1}"), &indexCollator);
    runInvalidQueryAsCommand(
        fromjson("{find: 'testns', min: {a: 1, b: 1, c: [1, 'foo']}, "
                 "hint: {a: 1, b: 1, c: 1}}"));
}

TEST_F(QueryPlannerTest, FailToPlanWhenMaxHasArrayBoundaryAndCollationsDontMatch) {
    CollatorInterfaceMock indexCollator(CollatorInterfaceMock::MockType::kToLowerString);
    addIndex(fromjson("{a: 1, b: 1, c: 1}"), &indexCollator);
    runInvalidQueryAsCommand(
        fromjson("{find: 'testns', max: {a: 1, b: 1, c: [1, 'foo']}, "
                 "hint: {a: 1, b: 1, c: 1}}"));
}

TEST_F(QueryPlannerTest, FailToPlanWhenHintingIndexIncompatibleWithMinDueToCollation) {
    CollatorInterfaceMock indexCollator(CollatorInterfaceMock::MockType::kToLowerString);
    addIndex(fromjson("{a: 1}"), &indexCollator, "indexToHint"_sd);
    addIndex(fromjson("{a: 1}"));
    runInvalidQueryAsCommand(fromjson("{find: 'testns', min: {a: 'foo'}, hint: 'indexToHint'}"));
}

TEST_F(QueryPlannerTest, FailToPlanWhenHintingIndexIncompatibleWithMaxDueToCollation) {
    CollatorInterfaceMock indexCollator(CollatorInterfaceMock::MockType::kToLowerString);
    addIndex(fromjson("{a: 1}"), &indexCollator, "indexToHint"_sd);
    addIndex(fromjson("{a: 1}"));
    runInvalidQueryAsCommand(fromjson("{find: 'testns', max: {a: 'foo'}, hint: 'indexToHint'}"));
}

TEST_F(QueryPlannerTest, SuccessWithIndexWithMatchingSimpleCollationWhenMinHasStringBoundary) {
    addIndex(fromjson("{a: 1}"), nullptr, "noCollation"_sd);

    runQueryAsCommand(fromjson("{find: 'testns', min: {a: 'foo'}, hint: {a: 1}}"));

    assertNumSolutions(1U);
    assertSolutionExists("{fetch: {node: {ixscan: {pattern: {a: 1}, name: 'noCollation'}}}}");
}

TEST_F(QueryPlannerTest, SuccessWithIndexWithMatchingNonSimpleCollationWhenMinHasStringBoundary) {
    CollatorInterfaceMock indexCollator(CollatorInterfaceMock::MockType::kReverseString);
    addIndex(fromjson("{a: 1}"), &indexCollator, "withCollation"_sd);

    runQueryAsCommand(fromjson(
        "{find: 'testns', min: {a: 'foo'}, hint: {a: 1}, collation: {locale: 'reverse'}}"));

    assertNumSolutions(1U);
    assertSolutionExists("{fetch: {node: {ixscan: {pattern: {a: 1}, name: 'withCollation'}}}}");
}

TEST_F(QueryPlannerTest, SuccessWithIndexWithMatchingSimpleCollationWhenMaxHasStringBoundary) {
    addIndex(fromjson("{a: 1}"), nullptr, "noCollation"_sd);

    runQueryAsCommand(fromjson("{find: 'testns', max: {a: 'foo'}, hint: {a: 1}}"));

    assertNumSolutions(1U);
    assertSolutionExists("{fetch: {node: {ixscan: {pattern: {a: 1}, name: 'noCollation'}}}}");
}

TEST_F(QueryPlannerTest, MustSortInMemoryWhenMinMaxQueryHasCollationAndIndexDoesNot) {
    addIndex(fromjson("{a: 1, b: 1}"));

    runQueryAsCommand(
        fromjson("{find: 'testns', min: {a: 1, b: 1}, max: {a: 2, b: 1}, hint: {a:1, b:1}, "
                 "collation: {locale: 'reverse'}, sort: {a: 1, b: 1}}"));

    assertNumSolutions(1U);

    assertSolutionExists(
        "{fetch: {node: {sort: {pattern: {a: 1, b: 1}, limit: 0, type: 'default', node: {ixscan: "
        "{pattern: {a: 1, b: 1}}}}}}}}}");
}

TEST_F(QueryPlannerTest, MustSortInMemoryWhenMinMaxIndexHasCollationAndQueryDoesNot) {
    CollatorInterfaceMock collator(CollatorInterfaceMock::MockType::kReverseString);
    addIndex(fromjson("{a: 1, b:1}"), &collator);

    runQueryAsCommand(
        fromjson("{find: 'testns', min: {a: 1, b: 1}, max: {a: 2, b: 1}, sort: {a: 1, b: 1}, "
                 "hint: {a: 1, b: 1}}"));

    assertNumSolutions(1U);

    assertSolutionExists(
        "{sort: {pattern: {a: 1, b: 1}, limit: 0, type: 'simple', node:"
        "{fetch: {node: {ixscan: {pattern: {a: 1, b: 1}}}}}}}");
}

TEST_F(QueryPlannerTest, CanProduceCoveredSortPlanWhenQueryHasCollationButIndexDoesNot) {
    addIndex(fromjson("{a: 1, b: 1}"));

    runQueryAsCommand(fromjson(
        "{find: 'testns', projection: {a: 1, b: 1, _id: 0}, min: {a: 1, b: 1}, max: {a: 2, "
        "b: 1}, hint: {a: 1, b: 1}, collation: {locale: 'reverse'}, sort: {a: 1, b: 1}}"));

    assertNumSolutions(1U);
    assertSolutionExists(
        "{sort: {pattern: {a: 1, b: 1}, limit: 0, type: 'default', node: "
        "{proj: {spec: {a: 1, b: 1, _id: 0}, node: {ixscan: {pattern: {a: 1, b: 1}}}}}}}");
}

TEST_F(QueryPlannerTest, CannotUseIndexWhenQueryHasNoCollationButIndexHasNonSimpleCollation) {
    CollatorInterfaceMock collator(CollatorInterfaceMock::MockType::kToLowerString);
    addIndex(fromjson("{a: 1, b: 1}"), &collator);

    runQueryAsCommand(
        fromjson("{find: 'testns', projection: {a: 1, b:1, _id: 0}, sort: {a: 1, b: 1}}"));

    assertNumSolutions(1U);
    assertSolutionExists(
        "{sort: {pattern: {a: 1, b: 1}, limit: 0, type: 'simple', node: "
        "{proj: {spec: {a: 1, b: 1, _id: 0}, node: {cscan: {dir: 1}}}}}}");
}

TEST_F(QueryPlannerTest, CannotUseIndexWhenQueryHasDifferentNonSimpleCollationThanIndex) {
    CollatorInterfaceMock collator(CollatorInterfaceMock::MockType::kToLowerString);
    addIndex(fromjson("{a: 1, b: 1}"), &collator);

    runQueryAsCommand(
        fromjson("{find: 'testns', projection: {a: 1, b:1, _id: 0}, collation: {locale: "
                 "'reverse'}, sort: {a: 1, b: 1}}"));

    assertNumSolutions(1U);
    assertSolutionExists(
        "{sort: {pattern: {a: 1, b: 1}, limit: 0, type: 'simple', node: "
        "{proj: {spec: {a: 1, b: 1, _id: 0}, node: {cscan: {dir: 1}}}}}}");
}

// This test verifies that an in-memory sort stage is added and sort provided by an index is not
// used when the collection has a compound index with a non-simple collation and we issue a
// non-collatable point-query on the prefix of the index key together with a sort on a suffix of the
// index key. This is a test for SERVER-48993.
TEST_F(QueryPlannerTest,
       MustSortInMemoryWhenPointPrefixQueryHasSimpleCollationButIndexHasNonSimpleCollation) {
    CollatorInterfaceMock collator(CollatorInterfaceMock::MockType::kToLowerString);
    addIndex(fromjson("{a: 1, b: 1}"), &collator);

    // No explicit collation on the query. This will implicitly use the simple collation since the
    // collection does not have a default collation.
    runQueryAsCommand(fromjson("{find: 'testns', filter: {a: 2}, sort: {b: 1}}"));

    assertNumSolutions(2U);
    assertSolutionExists(
        "{sort: {pattern: {b: 1}, limit: 0, type: 'simple', node: "
        "{cscan: {dir: 1}}}}}");
    assertSolutionExists(
        "{sort: {pattern: {b: 1}, limit: 0, type: 'simple', node: "
        "{fetch: {node: "
        "{ixscan: {pattern: {a: 1, b: 1}}}}}}}");

    // A query with an explicit simple collation.
    runQueryAsCommand(
        fromjson("{find: 'testns', filter: {a: 2}, sort: {b: 1}, collation: {locale: 'simple'}}"));

    assertNumSolutions(2U);
    assertSolutionExists(
        "{sort: {pattern: {b: 1}, limit: 0, type: 'simple', node: "
        "{cscan: {dir: 1}}}}}");
    assertSolutionExists(
        "{sort: {pattern: {b: 1}, limit: 0, type: 'simple', node: "
        "{fetch: {node: "
        "{ixscan: {pattern: {a: 1, b: 1}}}}}}}");
}

// This test verifies that an in-memory sort stage is added and sort provided by an index is not
// used when the collection has a compound index with a non-specified collation and we issue a
// non-collatable point-query on the prefix of the index key together with a sort on the suffix and
// an explicit non-simple collation. This is a test for SERVER-48993.
TEST_F(QueryPlannerTest,
       MustSortInMemoryWhenPointPrefixQueryHasNonSimpleCollationButIndexHasSimpleCollation) {
    addIndex(fromjson("{a: 1, b: 1}"));

    runQueryAsCommand(
        fromjson("{find: 'testns', filter: {a: 2}, sort: {b: 1}, collation: {locale: "
                 "'reverse'}}"));

    assertSolutionExists(
        "{fetch: {node: "
        "{sort: {pattern: {b: 1}, limit: 0, type: 'default', node: "
        "{ixscan: {pattern: {a: 1, b: 1}}}}}}}");
}

// This test verifies that an in-memory sort stage is added and sort provided by indexes is not used
// when the collection has a compound index with a non-simple collation and we issue a
// non-collatable point-query on the prefix of the index key together with a sort on the suffix and
// an explicit non-simple collation that differs from the index collation. This is a test for
// SERVER-48993.
TEST_F(QueryPlannerTest, MustSortInMemoryWhenPointPrefixQueryCollationDoesNotMatchIndexCollation) {
    CollatorInterfaceMock collator(CollatorInterfaceMock::MockType::kToLowerString);
    addIndex(fromjson("{a: 1, b: 1}"), &collator);

    runQueryAsCommand(
        fromjson("{find: 'testns', filter: {a: 2}, sort: {b: 1}, collation: {locale: 'reverse'}}"));

    assertNumSolutions(2U);
    assertSolutionExists(
        "{sort: {pattern: {b: 1}, limit: 0, type: 'simple', node: "
        "{cscan: {dir: 1}}}}}");
    assertSolutionExists(
        "{sort: {pattern: {b: 1}, limit: 0, type: 'simple', node: "
        "{fetch: {node: "
        "{ixscan: {pattern: {a: 1, b: 1}}}}}}}");
}

// This test verifies that a sort is provided by an index when the collection has a compound index
// with a non-simple collation and we issue a query with a different non-simple collation is a
// non-collatable point-query on the prefix, a non-collatable range-query on the suffix, and a sort
// on the suffix key. This is a test for SERVER-48993.
TEST_F(QueryPlannerTest,
       IndexCanSortWhenPointPrefixQueryCollationDoesNotMatchIndexButSortRangeIsNonCollatable) {
    CollatorInterfaceMock collator(CollatorInterfaceMock::MockType::kToLowerString);
    addIndex(fromjson("{a: 1, b: 1}"), &collator);

    runQueryAsCommand(
        fromjson("{find: 'testns', filter: {a: 2, b: {$gte: 0, $lt: 10}}, sort: {b: 1}}"));

    assertNumSolutions(2U);
    assertSolutionExists(
        "{sort: {pattern: {b: 1}, limit: 0, type: 'simple', node: "
        "{cscan: {dir: 1}}}}}");
    assertSolutionExists(
        "{fetch: {node: "
        "{ixscan: {pattern: {a: 1, b: 1}, bounds: {a: [[2, 2, true, true]], b: [[0, 10, true, "
        "false]]}}}}}");
}

// This test verifies that an in-memory sort stage is added when the collection has a compound index
// with a non-simple collation and we issue a query with a different non-simple collation is a
// non-collatable point-query on the prefix, a collatable range-query on the suffix, and a sort on
// the suffix key. This is a test for SERVER-48993.
TEST_F(QueryPlannerTest,
       MustSortInMemoryWhenPointPrefixQueryCollationDoesNotMatchIndexAndSortRangeIsCollatable) {
    CollatorInterfaceMock collator(CollatorInterfaceMock::MockType::kToLowerString);
    addIndex(fromjson("{a: 1, b: 1}"), &collator);

    runQueryAsCommand(
        fromjson("{find: 'testns', filter: {a: 2, b: {$gte: 'B', $lt: 'T'}}, sort: {b: 1}}"));

    assertNumSolutions(2U);
    assertSolutionExists(
        "{sort: {pattern: {b: 1}, limit: 0, type: 'simple', node: "
        "{cscan: {dir: 1}}}}}");
    assertSolutionExists(
        "{sort: {pattern: {b: 1}, limit: 0, type: 'simple', node: "
        "{fetch: {node: "
        "{ixscan: {pattern: {a: 1, b: 1}, bounds: {a: [[2, 2, true, true]]}}}}}}}");
}

// This test verifies that a SORT_MERGE stage is added when the collection has a compound index with
// a non-simple collation and we issue a query with a different non-simple collation is a
// non-collatable multi-point query on the prefix, a non-collatable range-query on the suffix, and a
// sort on the suffix key.This is a test for SERVER-48993.
TEST_F(QueryPlannerTest,
       CanExplodeForSortWhenPointPrefixQueryCollationDoesNotMatchIndexButSortRangeIsNonCollatable) {
    CollatorInterfaceMock collator(CollatorInterfaceMock::MockType::kToLowerString);
    addIndex(fromjson("{a: 1, b: 1, c: 1}"), &collator);

    runQueryAsCommand(fromjson(
        "{find: 'testns', filter: {a: {$in: [2, 5]}, b: {$gte: 0, $lt: 10}}, sort: {b: 1}}"));

    assertNumSolutions(2U);
    assertSolutionExists(
        "{fetch: {node: "
        "{mergeSort: {nodes: {"
        "n0: {ixscan: {pattern: {a: 1, b: 1, c: 1}, bounds: {a: [[2, 2, true, true]], b: [[0, 10, "
        "true, false]]}}}, "
        "n1: {ixscan: {pattern: {a: 1, b: 1, c: 1}, bounds: {a: [[5, 5, true, true]], b: [[0, 10, "
        "true, false]]}}} "
        "}}}}}");
}

/**
 * This test confirms that we place a fetch stage before sort in the case where both query
 * and index have the same non-simple collation. To handle this scenario without this fetch would
 * require a mechanism to ensure we don't attempt to encode for collation an already encoded index
 * key entry when generating the sort key.
 */
TEST_F(QueryPlannerTest, MustFetchBeforeSortWhenQueryHasSameNonSimpleCollationAsIndex) {
    params.options &= ~QueryPlannerParams::INCLUDE_COLLSCAN;
    CollatorInterfaceMock collator(CollatorInterfaceMock::MockType::kReverseString);
    addIndex(fromjson("{a: 1, b: 1}"), &collator);

    runQueryAsCommand(
        fromjson("{find: 'testns', filter: {a: {$gt: 0}}, projection: {a: 1, b:1, _id: 0}, "
                 "collation: {locale: 'reverse'}, sort: {b: 1, a: 1}}"));

    assertNumSolutions(1U);
    assertSolutionExists(
        "{sort: {pattern: {b: 1, a: 1}, limit: 0, type: 'simple', node: "
        "{proj: {spec: {a: 1, b: 1, _id: 0}, node:"
        "{fetch: {filter: null, collation: {locale: 'reverse'}, node:"
        "{ixscan: {pattern: {a: 1, b: 1}}}}}}}}}}}");
}

TEST_F(QueryPlannerTest, NoSortStageWhenMinMaxIndexCollationDoesNotMatchButBoundsContainNoStrings) {
    addIndex(fromjson("{a: 1, b: 1, c: 1}"));

    runQueryAsCommand(
        fromjson("{find: 'testns', min: {a: 1, b: 8, c: 1}, max: {a: 1, b: 8, c: 100}, "
                 "hint: {a: 1, b: 1, c: 1}, collation: {locale: 'reverse'}, "
                 "sort: {a: 1, b: 1, c: 1}}"));

    assertNumSolutions(1U);
    assertSolutionExists("{fetch: {node: {ixscan: {pattern: {a: 1, b: 1, c: 1}}}}}");
}

}  // namespace
