/**
 *    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/jsobj.h"
#include "mongo/db/json.h"
#include "mongo/db/query/query_planner.h"
#include "mongo/db/query/query_planner_test_fixture.h"

namespace {

using namespace mongo;

TEST_F(QueryPlannerTest, Basic2DNonNear) {
    // 2d can answer: within poly, within center, within centersphere, within box.
    // And it can use an index (or not) for each of them.  As such, 2 solns expected.
    addIndex(BSON("a"
                  << "2d"));

    // Polygon
    runQuery(fromjson("{a : { $within: { $polygon : [[0,0], [2,0], [4,0]] } }}"));
    assertNumSolutions(2U);
    assertSolutionExists("{cscan: {dir: 1}}");
    assertSolutionExists("{fetch: {node: {ixscan: {pattern: {a: '2d'}}}}}");

    // Center
    runQuery(fromjson("{a : { $within : { $center : [[ 5, 5 ], 7 ] } }}"));
    assertNumSolutions(2U);
    assertSolutionExists("{cscan: {dir: 1}}");
    assertSolutionExists("{fetch: {node: {ixscan: {pattern: {a: '2d'}}}}}");

    // Centersphere
    runQuery(fromjson("{a : { $within : { $centerSphere : [[ 10, 20 ], 0.01 ] } }}"));
    assertNumSolutions(2U);
    assertSolutionExists("{cscan: {dir: 1}}");
    assertSolutionExists("{fetch: {node: {ixscan: {pattern: {a: '2d'}}}}}");

    // Within box.
    runQuery(fromjson("{a : {$within: {$box : [[0,0],[9,9]]}}}"));
    assertNumSolutions(2U);
    assertSolutionExists("{cscan: {dir: 1}}");
    assertSolutionExists("{fetch: {node: {ixscan: {pattern: {a: '2d'}}}}}");

    // TODO: test that we *don't* annotate for things we shouldn't.
}

TEST_F(QueryPlannerTest, Basic2DSphereCompound) {
    addIndex(BSON("a" << 1 << "b" << 1));
    addIndex(BSON("loc"
                  << "2dsphere"));

    runQuery(
        fromjson("{loc:{$near:{$geometry:{type:'Point',"
                 "coordinates : [-81.513743,28.369947] },"
                 " $maxDistance :100}},a: 'mouse'}"));
    assertNumSolutions(1U);
    assertSolutionExists(
        "{fetch: {node: {geoNear2dsphere: {pattern: {loc: '2dsphere'}, "
        "bounds: {loc: [['MinKey', 'MaxKey', true, true]]}}}}}");
}

TEST_F(QueryPlannerTest, Basic2DCompound) {
    addIndex(BSON("loc"
                  << "2d"
                  << "a" << 1));

    runQuery(
        fromjson("{ loc: { $geoWithin: { $box : [[0, 0],[10, 10]] } },"
                 "a: 'mouse' }"));
    assertNumSolutions(2U);

    // This query will generate complex bounds, so we relax the checks to make the test readable.
    relaxBoundsCheckingToSubsetOnly();

    assertSolutionExists("{cscan: {dir: 1}}");
    assertSolutionExists(
        "{fetch: {node: {ixscan: {pattern: {loc : '2d', a: 1},"
        "filter: {a: 'mouse'},"
        "bounds: {loc: [],"  // Ignored since complex
        "         a: [['MinKey','MaxKey',true,true]]}"
        "}}}}");
}

TEST_F(QueryPlannerTest, Multikey2DSphereCompound) {
    // true means multikey
    addIndex(BSON("a" << 1 << "b" << 1), true);
    addIndex(BSON("loc"
                  << "2dsphere"),
             true);

    runQuery(
        fromjson("{loc:{$near:{$geometry:{type:'Point',"
                 "coordinates : [-81.513743,28.369947] },"
                 " $maxDistance :100}},a: 'mouse'}"));
    assertNumSolutions(1U);
    assertSolutionExists(
        "{fetch: {node: {geoNear2dsphere: {pattern: {loc: '2dsphere'}, "
        "bounds: {loc: [['MinKey', 'MaxKey', true, true]]}}}}}");
}

TEST_F(QueryPlannerTest, Basic2DSphereNonNear) {
    // 2dsphere can do: within+geometry, intersects+geometry
    addIndex(BSON("a"
                  << "2dsphere"));

    runQuery(
        fromjson("{a: {$geoIntersects: {$geometry: {type: 'Point',"
                 "coordinates: [10.0, 10.0]}}}}"));
    assertNumSolutions(2U);
    assertSolutionExists("{cscan: {dir: 1}}");
    assertSolutionExists("{fetch: {node: {ixscan: {pattern: {a: '2dsphere'}}}}}");

    runQuery(fromjson("{a : { $geoWithin : { $centerSphere : [[ 10, 20 ], 0.01 ] } }}"));
    assertNumSolutions(2U);
    assertSolutionExists("{cscan: {dir: 1}}");
    assertSolutionExists("{fetch: {node: {ixscan: {pattern: {a: '2dsphere'}}}}}");

    // TODO: test that we *don't* annotate for things we shouldn't.
}

TEST_F(QueryPlannerTest, Multikey2DSphereNonNear) {
    // 2dsphere can do: within+geometry, intersects+geometry
    // true means multikey
    addIndex(BSON("a"
                  << "2dsphere"),
             true);

    runQuery(
        fromjson("{a: {$geoIntersects: {$geometry: {type: 'Point',"
                 "coordinates: [10.0, 10.0]}}}}"));
    assertNumSolutions(2U);
    assertSolutionExists("{cscan: {dir: 1}}");
    assertSolutionExists("{fetch: {node: {ixscan: {pattern: {a: '2dsphere'}}}}}");

    runQuery(fromjson("{a : { $geoWithin : { $centerSphere : [[ 10, 20 ], 0.01 ] } }}"));
    assertNumSolutions(2U);
    assertSolutionExists("{cscan: {dir: 1}}");
    assertSolutionExists("{fetch: {node: {ixscan: {pattern: {a: '2dsphere'}}}}}");

    // TODO: test that we *don't* annotate for things we shouldn't.
}

TEST_F(QueryPlannerTest, Basic2DGeoNear) {
    // Can only do near + old point.
    addIndex(BSON("a"
                  << "2d"));
    runQuery(fromjson("{a: {$near: [0,0], $maxDistance:0.3 }}"));
    assertNumSolutions(1U);
    assertSolutionExists("{geoNear2d: {a: '2d'}}");
}

TEST_F(QueryPlannerTest, Basic2DSphereGeoNear) {
    // Can do nearSphere + old point, near + new point.
    addIndex(BSON("a"
                  << "2dsphere"));

    runQuery(fromjson("{a: {$nearSphere: [0,0], $maxDistance: 0.31 }}"));
    ASSERT_EQUALS(getNumSolutions(), 1U);
    assertSolutionExists(
        "{geoNear2dsphere: {pattern: {a: '2dsphere'}, "
        "bounds: {a: [['MinKey', 'MaxKey', true, true]]}}}");

    runQuery(
        fromjson("{a: {$geoNear: {$geometry: {type: 'Point', coordinates: [0,0]},"
                 "$maxDistance:100}}}"));
    assertNumSolutions(1U);
    assertSolutionExists(
        "{geoNear2dsphere: {pattern: {a: '2dsphere'}, "
        "bounds: {a: [['MinKey', 'MaxKey', true, true]]}}}");
}

TEST_F(QueryPlannerTest, Multikey2DSphereGeoNear) {
    // Can do nearSphere + old point, near + new point.
    // true means multikey
    addIndex(BSON("a"
                  << "2dsphere"),
             true);

    runQuery(fromjson("{a: {$nearSphere: [0,0], $maxDistance: 0.31 }}"));
    ASSERT_EQUALS(getNumSolutions(), 1U);
    assertSolutionExists(
        "{geoNear2dsphere: {pattern: {a: '2dsphere'}, "
        "bounds: {a: [['MinKey', 'MaxKey', true, true]]}}}");

    runQuery(
        fromjson("{a: {$geoNear: {$geometry: {type: 'Point', coordinates: [0,0]},"
                 "$maxDistance:100}}}"));
    assertNumSolutions(1U);
    assertSolutionExists(
        "{geoNear2dsphere: {pattern: {a: '2dsphere'}, "
        "bounds: {a: [['MinKey', 'MaxKey', true, true]]}}}");
}

TEST_F(QueryPlannerTest, Basic2DSphereGeoNearReverseCompound) {
    addIndex(BSON("x" << 1));
    addIndex(BSON("x" << 1 << "a"
                      << "2dsphere"));
    runQuery(fromjson("{x:1, a: {$nearSphere: [0,0], $maxDistance: 0.31 }}"));

    assertNumSolutions(1U);
    assertSolutionExists(
        "{geoNear2dsphere: {pattern: {x: 1, a: '2dsphere'}, "
        "bounds: {x: [[1, 1, true, true]], a: [['MinKey', 'MaxKey', true, true]]}}}");
}

TEST_F(QueryPlannerTest, Multikey2DSphereGeoNearReverseCompound) {
    addIndex(BSON("x" << 1), true);
    addIndex(BSON("x" << 1 << "a"
                      << "2dsphere"),
             true);
    runQuery(fromjson("{x:1, a: {$nearSphere: [0,0], $maxDistance: 0.31 }}"));

    assertNumSolutions(1U);
    assertSolutionExists(
        "{geoNear2dsphere: {pattern: {x: 1, a: '2dsphere'}, "
        "bounds: {x: [[1, 1, true, true]], a: [['MinKey', 'MaxKey', true, true]]}}}");
}

TEST_F(QueryPlannerTest, 2DNonNearContainedOr) {
    addIndex(BSON("a"
                  << "2d"
                  << "x" << 1));
    addIndex(BSON("y" << 1));
    runQuery(
        fromjson("{$and: [{x: 1}, {$or: [{a: {$within: {$polygon: [[0, 0], [0, 1], [1, 0], [0, "
                 "0]]}}}, {y: 1}]}]}"));

    assertNumSolutions(2U);
    assertSolutionExists(
        "{fetch: {filter: {x: 1}, node: {or: {nodes: ["
        "{fetch: {filter: {a: {$within: {$polygon: [[0, 0], [0, 1], [1, 0], [0, 0]]}}},"
        "node: {ixscan: {pattern: {a: '2d', x: 1}, filter: {x: 1}}}}},"
        "{ixscan: {pattern: {y: 1}, bounds: {y: [[1, 1, true, true]]}}}"
        "]}}}}");
    assertSolutionExists("{cscan: {dir: 1}}}}");
}

TEST_F(QueryPlannerTest, 2DSphereV1NonNearContainedOr) {
    addIndex(BSON("x" << 1 << "a"
                      << "2dsphere"),
             BSON("2dsphereIndexVersion" << 1));
    addIndex(BSON("y" << 1));
    runQuery(
        fromjson("{$and: [{x: 1}, {$or: [{a: {$geoWithin: {$geometry: {type: 'Polygon', "
                 "coordinates: [[[0, 0], [0, 1], [1, 0], [0, 0]]]}}}}, {y: 1}]}]}"));

    assertNumSolutions(3U);
    assertSolutionExists(
        "{fetch: {filter: {x: 1}, node: {or: {nodes: ["
        "{fetch: {filter: {a: {$geoWithin: {$geometry: {type: 'Polygon', coordinates: [[[0, 0], "
        "[0, 1], [1, 0], [0, 0]]]}}}}, node: {ixscan: {pattern: {x: 1, a: '2dsphere'}, bounds: {x: "
        "[[1, 1, true, true]]}}}}},"
        "{ixscan: {pattern: {y: 1}, bounds: {y: [[1, 1, true, true]]}}}"
        "]}}}}");
    assertSolutionExists(
        "{fetch: {filter: {$or: [{a: {$geoWithin: {$geometry: {type: 'Polygon', coordinates: [[[0, "
        "0], [0, 1], [1, 0], [0, 0]]]}}}}, {y: 1}]}, node: "
        "{ixscan: {pattern: {x: 1, a: '2dsphere'}, bounds: {x: [[1, 1, true, true]], a: "
        "[['MinKey', 'MaxKey', true, true]]}}}}}");
    assertSolutionExists("{cscan: {dir: 1}}}}");
}

TEST_F(QueryPlannerTest, 2DSphereV2NonNearContainedOr) {
    addIndex(BSON("x" << 1 << "a"
                      << "2dsphere"),
             BSON("2dsphereIndexVersion" << 2));
    addIndex(BSON("y" << 1));
    runQuery(
        fromjson("{$and: [{x: 1}, {$or: [{a: {$geoWithin: {$geometry: {type: 'Polygon', "
                 "coordinates: [[[0, 0], [0, 1], [1, 0], [0, 0]]]}}}}, {y: 1}]}]}"));

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

TEST_F(QueryPlannerTest, NearNoIndex) {
    addIndex(BSON("x" << 1));
    runInvalidQuery(fromjson("{x:1, a: {$nearSphere: [0,0], $maxDistance: 0.31 }}"));
}

TEST_F(QueryPlannerTest, NearEmptyPath) {
    addIndex(BSON(""
                  << "2dsphere"));
    runInvalidQuery(fromjson("{'': {$near: {$geometry: {type: 'Point', coordinates: [0, 0]}}}}"));
}

TEST_F(QueryPlannerTest, TwoDSphereNoGeoPred) {
    addIndex(BSON("x" << 1 << "a"
                      << "2dsphere"));
    runQuery(fromjson("{x:1}"));

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

TEST_F(QueryPlannerTest, TwoDSphereNoGeoPredMultikey) {
    addIndex(BSON("x" << 1 << "a"
                      << "2dsphere"),
             true);
    runQuery(fromjson("{x:1}"));

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

// SERVER-14723
TEST_F(QueryPlannerTest, GeoNearMultipleRelevantIndicesButOnlyOneCompatible) {
    addIndex(BSON("a"
                  << "2dsphere"));
    addIndex(BSON("b" << 1 << "a"
                      << "2dsphere"));

    runQuery(
        fromjson("{a: {$nearSphere: {$geometry: {type: 'Point', coordinates: [0,0]}}},"
                 " b: {$exists: false}}"));

    assertNumSolutions(1U);
    assertSolutionExists(
        "{fetch: {filter: {b: {$exists: false}}, node: "
        "{geoNear2dsphere: {pattern: {a: '2dsphere'}, "
        "bounds: {a: [['MinKey', 'MaxKey', true, true]]}}}}}");
}

// SERVER-3984, $or 2d index
TEST_F(QueryPlannerTest, Or2DNonNear) {
    addIndex(BSON("a"
                  << "2d"));
    addIndex(BSON("b"
                  << "2d"));
    runQuery(
        fromjson("{$or: [ {a : { $within : { $polygon : [[0,0], [2,0], [4,0]] } }},"
                 " {b : { $within : { $center : [[ 5, 5 ], 7 ] } }} ]}"));

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

// SERVER-3984, $or 2d index
TEST_F(QueryPlannerTest, Or2DSameFieldNonNear) {
    addIndex(BSON("a"
                  << "2d"));
    runQuery(
        fromjson("{$or: [ {a : { $within : { $polygon : [[0,0], [2,0], [4,0]] } }},"
                 " {a : { $within : { $center : [[ 5, 5 ], 7 ] } }} ]}"));

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

// SERVER-3984, $or 2dsphere index
TEST_F(QueryPlannerTest, Or2DSphereNonNear) {
    addIndex(BSON("a"
                  << "2dsphere"));
    addIndex(BSON("b"
                  << "2dsphere"));
    runQuery(fromjson(
        "{$or: [ {a: {$geoIntersects: {$geometry: {type: 'Point', coordinates: [10.0, 10.0]}}}},"
        " {b: {$geoWithin: { $centerSphere: [[ 10, 20 ], 0.01 ] } }} ]}"));

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

// SERVER-3984, $or 2dsphere index
TEST_F(QueryPlannerTest, Or2DSphereNonNearMultikey) {
    // true means multikey
    addIndex(BSON("a"
                  << "2dsphere"),
             true);
    addIndex(BSON("b"
                  << "2dsphere"),
             true);
    runQuery(
        fromjson("{$or: [ {a: {$geoIntersects: {$geometry: "
                 "{type: 'Point', coordinates: [10.0, 10.0]}}}},"
                 " {b: {$geoWithin: { $centerSphere: [[ 10, 20 ], 0.01 ] } }} ]}"));

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

TEST_F(QueryPlannerTest, And2DSameFieldNonNear) {
    addIndex(BSON("a"
                  << "2d"));
    runQuery(
        fromjson("{$and: [ {a : { $within : { $polygon : [[0,0], [2,0], [4,0]] } }},"
                 " {a : { $within : { $center : [[ 5, 5 ], 7 ] } }} ]}"));

    assertNumSolutions(2U);
    assertSolutionExists("{cscan: {dir: 1}}");
    // Bounds of the two 2d geo predicates are combined into
    // a single index scan.
    assertSolutionExists("{fetch: {node: {ixscan: {pattern: {a: '2d'}}}}}");
}

TEST_F(QueryPlannerTest, And2DWith2DNearSameField) {
    addIndex(BSON("a"
                  << "2d"));
    runQuery(
        fromjson("{$and: [ {a : { $within : { $polygon : [[0,0], [2,0], [4,0]] } }},"
                 " {a : { $near : [ 5, 5 ] } } ]}"));

    // GEO_NEAR must use the index, and GEO predicate becomes a filter.
    assertNumSolutions(1U);
    assertSolutionExists("{fetch: { node : { geoNear2d: {a: '2d'} } } }");
}

TEST_F(QueryPlannerTest, And2DWith2DNearSameFieldMultikey) {
    const bool multikey = true;
    addIndex(BSON("geo"
                  << "2d"),
             multikey);
    runQuery(
        fromjson("{$and: [{geo: {$near: [0, 0]}}, "
                 "{geo: {$within: {$polygon: [[0, 0], [1, 0], [1, 1]]}}}]}"));

    // GEO_NEAR must use the index, and GEO predicate becomes a filter.
    assertNumSolutions(1U);
    assertSolutionExists(
        "{fetch: {filter: {geo: {$within: {$polygon: [[0, 0], [1, 0], [1, 1]]}}}, "
        "node: {geoNear2d: {geo: '2d'}}}}}");
}

TEST_F(QueryPlannerTest, And2DSphereSameFieldNonNear) {
    addIndex(BSON("a"
                  << "2dsphere"));
    runQuery(
        fromjson("{$and: [ {a: {$geoIntersects: {$geometry: "
                 "{type: 'Point', coordinates: [3.0, 1.0]}}}},"
                 "  {a: {$geoIntersects: {$geometry: "
                 "{type: 'Point', coordinates: [4.0, 1.0]}}}}]}"));

    assertNumSolutions(2U);
    assertSolutionExists("{cscan: {dir: 1}}");
    // Bounds of the two 2dsphere geo predicates are combined into
    // a single index scan.
    assertSolutionExists("{fetch: {node: {ixscan: {pattern: {a: '2dsphere'}}}}}");
}

TEST_F(QueryPlannerTest, And2DSphereSameFieldNonNearMultikey) {
    // true means multikey
    addIndex(BSON("a"
                  << "2dsphere"),
             true);
    runQuery(
        fromjson("{$and: [ {a: {$geoIntersects: {$geometry: "
                 "{type: 'Point', coordinates: [3.0, 1.0]}}}},"
                 "  {a: {$geoIntersects: {$geometry: "
                 "{type: 'Point', coordinates: [4.0, 1.0]}}}}]}"));

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

TEST_F(QueryPlannerTest, And2DSphereWithNearSameField) {
    addIndex(BSON("a"
                  << "2dsphere"));
    runQuery(
        fromjson("{$and: [{a: {$geoIntersects: {$geometry: "
                 "{type: 'Point', coordinates: [3.0, 1.0]}}}},"
                 "{a: {$near: {$geometry: "
                 "{type: 'Point', coordinates: [10.0, 10.0]}}}}]}"));

    // GEO_NEAR must use the index, and GEO predicate becomes a filter.
    assertNumSolutions(1U);
    assertSolutionExists("{fetch: {node: {geoNear2dsphere: {pattern: {a: '2dsphere'}}}}}}");
}

TEST_F(QueryPlannerTest, And2DSphereWithNearSameFieldMultikey) {
    // true means multikey
    addIndex(BSON("a"
                  << "2dsphere"),
             true);
    runQuery(
        fromjson("{$and: [{a: {$geoIntersects: {$geometry: "
                 "{type: 'Point', coordinates: [3.0, 1.0]}}}},"
                 "{a: {$near: {$geometry: "
                 "{type: 'Point', coordinates: [10.0, 10.0]}}}}]}"));

    // GEO_NEAR must use the index, and GEO predicate becomes a filter.
    assertNumSolutions(1U);
    assertSolutionExists(
        "{fetch: {node: {geoNear2dsphere: {pattern: {a: '2dsphere'}, "
        "bounds: {a: [['MinKey', 'MaxKey', true, true]]}}}}}");
}

TEST_F(QueryPlannerTest, Or2DSphereSameFieldNonNear) {
    addIndex(BSON("a"
                  << "2dsphere"));
    runQuery(
        fromjson("{$or: [ {a: {$geoIntersects: {$geometry: "
                 "{type: 'Point', coordinates: [3.0, 1.0]}}}},"
                 "  {a: {$geoIntersects: {$geometry: "
                 "{type: 'Point', coordinates: [4.0, 1.0]}}}}]}"));

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

TEST_F(QueryPlannerTest, Or2DSphereSameFieldNonNearMultikey) {
    // true means multikey
    addIndex(BSON("a"
                  << "2dsphere"),
             true);
    runQuery(
        fromjson("{$or: [ {a: {$geoIntersects: {$geometry: "
                 "{type: 'Point', coordinates: [3.0, 1.0]}}}},"
                 "  {a: {$geoIntersects: {$geometry: "
                 "{type: 'Point', coordinates: [4.0, 1.0]}}}}]}"));

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

TEST_F(QueryPlannerTest, CompoundMultikey2DSphereNear) {
    // true means multikey
    addIndex(BSON("a" << 1 << "b"
                      << "2dsphere"),
             true);
    runQuery(
        fromjson("{a: {$gte: 0}, b: {$near: {$geometry: "
                 "{type: 'Point', coordinates: [2, 2]}}}}"));

    assertNumSolutions(1U);
    assertSolutionExists(
        "{geoNear2dsphere: {pattern: {a: 1, b: '2dsphere'}, "
        "bounds: {a: [[0, Infinity, true, true]], b: [['MinKey', 'MaxKey', true, true]]}}}");
}

TEST_F(QueryPlannerTest, CompoundMultikey2DSphereNearFetchRequired) {
    // true means multikey
    addIndex(BSON("a" << 1 << "b"
                      << "2dsphere"),
             true);
    runQuery(
        fromjson("{a: {$gte: 0, $lt: 5}, b: {$near: {$geometry: "
                 "{type: 'Point', coordinates: [2, 2]}}}}"));

    assertNumSolutions(1U);
    assertSolutionExists(
        "{fetch: {filter: {a:{$gte:0}}, node: "
        "{geoNear2dsphere: {pattern: {a: 1, b: '2dsphere'}, "
        "bounds: {a: [[-Infinity, 5, true, false]], b: [['MinKey', 'MaxKey', true, true]]}}}}}");
}

TEST_F(QueryPlannerTest, CompoundMultikey2DSphereNearMultipleIndices) {
    // true means multikey
    addIndex(BSON("a" << 1 << "b"
                      << "2dsphere"),
             true);
    addIndex(BSON("c" << 1 << "b"
                      << "2dsphere"),
             true);
    runQuery(
        fromjson("{a: {$gte: 0}, c: 3, b: {$near: {$geometry: "
                 "{type: 'Point', coordinates: [2, 2]}}}}"));

    assertNumSolutions(2U);
    assertSolutionExists(
        "{fetch: {filter: {c:3}, node: "
        "{geoNear2dsphere: {pattern: {a: 1, b: '2dsphere'}, "
        "bounds: {a: [[0, Infinity, true, true]], b: [['MinKey', 'MaxKey', true, true]]}}}}}");
    assertSolutionExists(
        "{fetch: {filter: {a:{$gte:0}}, node: "
        "{geoNear2dsphere: {pattern: {c: 1, b: '2dsphere'}, "
        "bounds: {c: [[3, 3, true, true]], b: [['MinKey', 'MaxKey', true, true]]}}}}}");
}

TEST_F(QueryPlannerTest, CompoundMultikey2DSphereNearMultipleLeadingFields) {
    // true means multikey
    addIndex(BSON("a" << 1 << "b" << 1 << "c"
                      << "2dsphere"),
             true);
    runQuery(
        fromjson("{a: {$lt: 5, $gt: 1}, b: 6, c: {$near: {$geometry: "
                 "{type: 'Point', coordinates: [2, 2]}}}}"));

    assertNumSolutions(1U);
    assertSolutionExists(
        "{fetch: {filter: {a:{$gt:1}}, node: "
        "{geoNear2dsphere: {pattern: {a: 1, b: 1, c: '2dsphere'}, "
        "bounds: {a: [[-Infinity, 5, true, false]], b: [[6, 6, true, true]], "
        "c: [['MinKey', 'MaxKey', true, true]]}}}}}");
}

TEST_F(QueryPlannerTest, CompoundMultikey2DSphereNearMultipleGeoPreds) {
    // true means multikey
    addIndex(BSON("a" << 1 << "b" << 1 << "c"
                      << "2dsphere"),
             true);
    runQuery(
        fromjson("{a: 1, b: 6, $and: ["
                 "{c: {$near: {$geometry: {type: 'Point', coordinates: [2, 2]}}}},"
                 "{c: {$geoWithin: {$box: [ [1, 1], [3, 3] ] } } } ] }"));

    assertNumSolutions(1U);
    assertSolutionExists(
        "{fetch: {node: {geoNear2dsphere: {pattern: {a:1, b:1, c:'2dsphere'}, "
        "bounds: {a: [[1, 1, true, true]], b: [[6, 6, true, true]], "
        "c: [['MinKey', 'MaxKey', true, true]]}}}}}");
}

TEST_F(QueryPlannerTest, CompoundMultikey2DSphereNearCompoundTest) {
    // true means multikey
    addIndex(BSON("a" << 1 << "b"
                      << "2dsphere"
                      << "c" << 1 << "d" << 1),
             true);
    runQuery(
        fromjson("{a: {$gte: 0}, c: {$gte: 0, $lt: 4}, d: {$gt: 1, $lt: 5},"
                 "b: {$near: {$geometry: "
                 "{type: 'Point', coordinates: [2, 2]}}}}"));

    assertNumSolutions(1U);
    assertSolutionExists(
        "{fetch: {filter: {d:{$gt:1},c:{$gte:0}}, node: "
        "{geoNear2dsphere: {pattern: {a: 1, b: '2dsphere', c: 1, d: 1}, "
        "bounds: {a: [[0, Infinity, true, true]], b: [['MinKey', 'MaxKey', true, true]], "
        "c: [[-Infinity, 4, true, false]], d: [[-Infinity, 5, true, false]]}}}}}");
}

TEST_F(QueryPlannerTest, CompoundMultikey2DNear) {
    // true means multikey
    addIndex(BSON("a"
                  << "2d"
                  << "b" << 1),
             true);
    runQuery(fromjson("{a: {$near: [0, 0]}, b: {$gte: 0}}"));

    assertNumSolutions(1U);
    assertSolutionExists(
        "{fetch: { filter : {b:{$gte: 0}}, node: "
        "{geoNear2d: {a: '2d', b: 1} } } }");
}

// SERVER-9257
TEST_F(QueryPlannerTest, CompoundGeoNoGeoPredicate) {
    addIndex(BSON("creationDate" << 1 << "foo.bar"
                                 << "2dsphere"));
    runQuerySortProj(
        fromjson("{creationDate: { $gt: 7}}"), fromjson("{creationDate: 1}"), BSONObj());

    ASSERT_EQUALS(getNumSolutions(), 2U);
    assertSolutionExists(
        "{sort: {pattern: {creationDate: 1}, limit: 0, type: 'simple', node: "
        "{cscan: {dir: 1}}}}");
    assertSolutionExists(
        "{fetch: {node: {ixscan: {pattern: {creationDate: 1, 'foo.bar': '2dsphere'}}}}}");
}

// SERVER-9257
TEST_F(QueryPlannerTest, CompoundGeoNoGeoPredicateMultikey) {
    // true means multikey
    addIndex(BSON("creationDate" << 1 << "foo.bar"
                                 << "2dsphere"),
             true);
    runQuerySortProj(
        fromjson("{creationDate: { $gt: 7}}"), fromjson("{creationDate: 1}"), BSONObj());

    ASSERT_EQUALS(getNumSolutions(), 2U);
    assertSolutionExists(
        "{sort: {pattern: {creationDate: 1}, limit: 0, type: 'simple', node: "
        "{cscan: {dir: 1}}}}}}");
    assertSolutionExists(
        "{sort: {pattern: {creationDate: 1}, limit: 0, type: 'simple', node: {fetch: {node: "
        "{ixscan: {pattern: {creationDate: 1, 'foo.bar': '2dsphere'}}}}}}}");
}

// Test that a 2dsphere index can satisfy a whole index scan solution if the query has a GEO
// predicate on at least one of the indexed geo fields.
// Currently fails.  Tracked by SERVER-10801.
/*
TEST_F(QueryPlannerTest, SortOnGeoQuery) {
    addIndex(BSON("timestamp" << -1 << "position" << "2dsphere"));
    BSONObj query = fromjson("{position: {$geoWithin: {$geometry: {type: \"Polygon\", coordinates:
    [[[1, 1], [1, 90], [180, 90], [180, 1], [1, 1]]]}}}}"); BSONObj sort = fromjson("{timestamp:
    -1}");
    runQuerySortProj(query, sort, BSONObj());

    ASSERT_EQUALS(getNumSolutions(), 2U);
    assertSolutionExists("{sort: {pattern: {timestamp: -1}, limit: 0, "
                            "node: {cscan: {dir: 1}}}}");
    assertSolutionExists("{fetch: {node: {ixscan: {pattern: {timestamp: -1, position:
    '2dsphere'}}}}}"); }

TEST_F(QueryPlannerTest, SortOnGeoQueryMultikey) {
    // true means multikey
    addIndex(BSON("timestamp" << -1 << "position" << "2dsphere"), true);
    BSONObj query = fromjson("{position: {$geoWithin: {$geometry: {type: \"Polygon\", "
        "coordinates: [[[1, 1], [1, 90], [180, 90], [180, 1], [1, 1]]]}}}}");
    BSONObj sort = fromjson("{timestamp: -1}");
    runQuerySortProj(query, sort, BSONObj());

    ASSERT_EQUALS(getNumSolutions(), 2U);
    assertSolutionExists("{sort: {pattern: {timestamp: -1}, limit: 0, "
                            "node: {cscan: {dir: 1}}}}");
    assertSolutionExists("{fetch: {node: {ixscan: {pattern: "
                            "{timestamp: -1, position: '2dsphere'}}}}}");
}
*/


//
// Sort
//

TEST_F(QueryPlannerTest, CantUseNonCompoundGeoIndexToProvideSort) {
    addIndex(BSON("x"
                  << "2dsphere"));
    runQuerySortProj(BSONObj(), BSON("x" << 1), BSONObj());

    ASSERT_EQUALS(getNumSolutions(), 1U);
    assertSolutionExists(
        "{sort: {pattern: {x: 1}, limit: 0, type: 'simple', node: "
        "{cscan: {dir: 1, filter: {}}}}}");
}

TEST_F(QueryPlannerTest, CantUseNonCompoundGeoIndexToProvideSortWithIndexablePred) {
    addIndex(BSON("x"
                  << "2dsphere"));
    runQuerySortProj(fromjson("{x: {$geoIntersects: {$geometry: {type: 'Point',"
                              "                                  coordinates: [0, 0]}}}}"),
                     BSON("x" << 1),
                     BSONObj());

    ASSERT_EQUALS(getNumSolutions(), 2U);
    assertSolutionExists(
        "{sort: {pattern: {x: 1}, limit: 0, type: 'simple', node: "
        "{fetch: {node: {ixscan: {pattern: {x: '2dsphere'}}}}}}}");
    assertSolutionExists(
        "{sort: {pattern: {x: 1}, limit: 0, type: 'simple', node: "
        "{cscan: {dir: 1}}}}");
}

TEST_F(QueryPlannerTest, CantUseCompoundGeoIndexToProvideSortIfNoGeoPred) {
    addIndex(BSON("x" << 1 << "y"
                      << "2dsphere"));
    runQuerySortProj(BSONObj(), BSON("x" << 1), BSONObj());

    ASSERT_EQUALS(getNumSolutions(), 1U);
    assertSolutionExists(
        "{sort: {pattern: {x: 1}, limit: 0, type: 'simple', node: "
        "{cscan: {dir: 1, filter: {}}}}}");
}

TEST_F(QueryPlannerTest, CanUseCompoundGeoIndexToProvideSortWithGeoPred) {
    addIndex(BSON("x" << 1 << "y"
                      << "2dsphere"));
    runQuerySortProj(fromjson("{x: 1, y: {$geoIntersects: {$geometry: {type: 'Point',"
                              "                                        coordinates: [0, 0]}}}}"),
                     BSON("x" << 1),
                     BSONObj());

    ASSERT_EQUALS(getNumSolutions(), 2U);
    assertSolutionExists(
        "{fetch: {node: "
        "{ixscan: {pattern: {x: 1, y: '2dsphere'}}}}}");
    assertSolutionExists(
        "{sort: {pattern: {x: 1}, limit: 0, type: 'simple', node: "
        "{cscan: {dir: 1}}}}");
}

//
// Negation
//

//
// 2D geo negation
// The filter b != 1 is embedded in the geoNear2d node.
// Can only do near + old point.
//
TEST_F(QueryPlannerTest, Negation2DGeoNear) {
    addIndex(BSON("a"
                  << "2d"));
    runQuery(fromjson("{$and: [{a: {$near: [0, 0], $maxDistance: 0.3}}, {b: {$ne: 1}}]}"));
    assertNumSolutions(1U);
    assertSolutionExists("{fetch: {node: { geoNear2d: {a: '2d'} } } }");
}

//
// 2DSphere geo negation
// Filter is embedded in a separate fetch node.
//
TEST_F(QueryPlannerTest, Negation2DSphereGeoNear) {
    // Can do nearSphere + old point, near + new point.
    addIndex(BSON("a"
                  << "2dsphere"));

    runQuery(
        fromjson("{$and: [{a: {$nearSphere: [0,0], $maxDistance: 0.31}}, "
                 "{b: {$ne: 1}}]}"));
    assertNumSolutions(1U);
    assertSolutionExists(
        "{fetch: {node: {geoNear2dsphere: {pattern: {a: '2dsphere'}, "
        "bounds: {a: [['MinKey', 'MaxKey', true, true]]}}}}}");

    runQuery(
        fromjson("{$and: [{a: {$geoNear: {$geometry: {type: 'Point', "
                 "coordinates: [0, 0]},"
                 "$maxDistance: 100}}},"
                 "{b: {$ne: 1}}]}"));
    assertNumSolutions(1U);
    assertSolutionExists(
        "{fetch: {node: {geoNear2dsphere: {pattern: {a: '2dsphere'}, "
        "bounds: {a: [['MinKey', 'MaxKey', true, true]]}}}}}");
}

//
// 2DSphere geo negation
// Filter is embedded in a separate fetch node.
//
TEST_F(QueryPlannerTest, Negation2DSphereGeoNearMultikey) {
    // Can do nearSphere + old point, near + new point.
    // true means multikey
    addIndex(BSON("a"
                  << "2dsphere"),
             true);

    runQuery(
        fromjson("{$and: [{a: {$nearSphere: [0,0], $maxDistance: 0.31}}, "
                 "{b: {$ne: 1}}]}"));
    assertNumSolutions(1U);
    assertSolutionExists(
        "{fetch: {node: {geoNear2dsphere: {pattern: {a: '2dsphere'}, "
        "bounds: {a: [['MinKey', 'MaxKey', true, true]]}}}}}");

    runQuery(
        fromjson("{$and: [{a: {$geoNear: {$geometry: {type: 'Point', "
                 "coordinates: [0, 0]},"
                 "$maxDistance: 100}}},"
                 "{b: {$ne: 1}}]}"));
    assertNumSolutions(1U);
    assertSolutionExists(
        "{fetch: {node: {geoNear2dsphere: {pattern: {a: '2dsphere'}, "
        "bounds: {a: [['MinKey', 'MaxKey', true, true]]}}}}}");
}

//
// Tests for intersecting and compounding bounds on multikey 2dsphere indexes when path-level
// multikey information is available.
//
using QueryPlannerGeo2dsphereTest = QueryPlannerTest;

TEST_F(QueryPlannerGeo2dsphereTest, CanIntersectBoundsWhenFirstFieldIsNotMultikey) {
    MultikeyPaths multikeyPaths{MultikeyComponents{}, {0U}, MultikeyComponents{}};
    addIndex(BSON("a" << 1 << "b" << 1 << "geo"
                      << "2dsphere"),
             multikeyPaths);
    runQuery(fromjson("{a: {$gte: 0, $lt: 10}, b: 2, geo: {$nearSphere: [0, 0]}}"));

    assertNumSolutions(1U);
    assertSolutionExists(
        "{geoNear2dsphere: {pattern: {a: 1, b: 1, geo: '2dsphere'}, "
        "bounds: {a: [[0, 10, true, false]], b: [[2, 2, true, true]], "
        "geo: [['MinKey', 'MaxKey', true, true]]}}}");
}

TEST_F(QueryPlannerGeo2dsphereTest,
       CanIntersectBoundsOnFirstFieldWhenItAndSharedPrefixAreNotMultikey) {
    MultikeyPaths multikeyPaths{MultikeyComponents{}, {1U}, MultikeyComponents{}};
    addIndex(BSON("a.b" << 1 << "a.c" << 1 << "a.geo"
                        << "2dsphere"),
             multikeyPaths);
    runQuery(fromjson("{'a.b': {$gte: 0, $lt: 10}, 'a.c': 2, 'a.geo': {$nearSphere: [0, 0]}}"));

    assertNumSolutions(1U);
    assertSolutionExists(
        "{geoNear2dsphere: {pattern: {'a.b': 1, 'a.c': 1, 'a.geo': '2dsphere'}, "
        "bounds: {'a.b': [[0, 10, true, false]], 'a.c': [[2, 2, true, true]], "
        "'a.geo': [['MinKey', 'MaxKey', true, true]]}}}");
}

TEST_F(QueryPlannerGeo2dsphereTest, CannotIntersectBoundsWhenFirstFieldIsMultikey) {
    MultikeyPaths multikeyPaths{{0U}, MultikeyComponents{}, MultikeyComponents{}};
    addIndex(BSON("a" << 1 << "b" << 1 << "geo"
                      << "2dsphere"),
             multikeyPaths);
    runQuery(fromjson("{a: {$gte: 0, $lt: 10}, b: 2, geo: {$nearSphere: [0, 0]}}"));

    assertNumSolutions(1U);
    assertSolutionExists(
        "{fetch: {node: {geoNear2dsphere: {pattern: {a: 1, b: 1, geo: '2dsphere'}, "
        "bounds: {a: [[-Infinity, 10, true, false]], b: [[2, 2, true, true]], "
        "geo: [['MinKey', 'MaxKey', true, true]]}}}}}");
}

TEST_F(QueryPlannerGeo2dsphereTest, CanIntersectBoundsWhenFirstFieldIsMultikeyButHasElemMatch) {
    MultikeyPaths multikeyPaths{{0U}, MultikeyComponents{}, MultikeyComponents{}};
    addIndex(BSON("a" << 1 << "b" << 1 << "geo"
                      << "2dsphere"),
             multikeyPaths);
    runQuery(fromjson("{a: {$elemMatch: {$gte: 0, $lt: 10}}, b: 2, geo: {$nearSphere: [0, 0]}}"));

    assertNumSolutions(1U);
    assertSolutionExists(
        "{fetch: {node: {geoNear2dsphere: {pattern: {a: 1, b: 1, geo: '2dsphere'}, "
        "bounds: {a: [[0, 10, true, false]], b: [[2, 2, true, true]], "
        "geo: [['MinKey', 'MaxKey', true, true]]}}}}}");
}

TEST_F(QueryPlannerGeo2dsphereTest,
       CannotComplementBoundsOnFirstFieldWhenItIsMultikeyAndHasNotEqualExpr) {
    MultikeyPaths multikeyPaths{{0U}, MultikeyComponents{}, MultikeyComponents{}};
    addIndex(BSON("a" << 1 << "b" << 1 << "geo"
                      << "2dsphere"),
             multikeyPaths);
    runInvalidQuery(fromjson("{a: {$ne: 3}, b: 2, geo: {$nearSphere: [0, 0]}}"));

    assertNoSolutions();
}

TEST_F(QueryPlannerGeo2dsphereTest,
       CanIntersectBoundsWhenFirstFieldIsMultikeyAndHasNotInsideElemMatch) {
    MultikeyPaths multikeyPaths{{0U}, MultikeyComponents{}, MultikeyComponents{}};
    addIndex(BSON("a" << 1 << "b" << 1 << "geo"
                      << "2dsphere"),
             multikeyPaths);
    runQuery(fromjson(
        "{a: {$elemMatch: {$not: {$gte: 10}, $gte: 0}}, b: 2, geo: {$nearSphere: [0, 0]}}"));

    assertNumSolutions(1U);
    assertSolutionExists(
        "{fetch: {node: {geoNear2dsphere: {pattern: {a: 1, b: 1, geo: '2dsphere'}, "
        "bounds: {a: [[0, 10, true, false]], b: [[2, 2, true, true]], "
        "geo: [['MinKey', 'MaxKey', true, true]]}}}}}");
}

TEST_F(QueryPlannerGeo2dsphereTest,
       CanIntersectBoundsOnFirstFieldWhenSharedPrefixIsMultikeyButHasElemMatch) {
    MultikeyPaths multikeyPaths{{0U}, {0U}, {0U}};
    addIndex(BSON("a.b" << 1 << "a.c" << 1 << "a.geo"
                        << "2dsphere"),
             multikeyPaths);
    runQuery(fromjson(
        "{a: {$elemMatch: {b: {$gte: 0, $lt: 10}, c: 2}}, 'a.geo': {$nearSphere: [0, 0]}}"));

    assertNumSolutions(1U);
    assertSolutionExists(
        "{fetch: {node: {geoNear2dsphere: {pattern: {'a.b': 1, 'a.c': 1, 'a.geo': '2dsphere'}, "
        "bounds: {'a.b': [[0, 10, true, false]], 'a.c': [[2, 2, true, true]], "
        "'a.geo': [['MinKey', 'MaxKey', true, true]]}}}}}");
}

TEST_F(QueryPlannerGeo2dsphereTest,
       CannotIntersectBoundsOnFirstFieldWhenItAndSharedPrefixAreMultikey) {
    MultikeyPaths multikeyPaths{{0U, 1U}, {0U}, {0U}};
    addIndex(BSON("a.b" << 1 << "a.c" << 1 << "a.geo"
                        << "2dsphere"),
             multikeyPaths);
    runQuery(fromjson(
        "{a: {$elemMatch: {b: {$gte: 0, $lt: 10}, c: 2}}, 'a.geo': {$nearSphere: [0, 0]}}"));

    assertNumSolutions(1U);
    assertSolutionExists(
        "{fetch: {node: {geoNear2dsphere: {pattern: {'a.b': 1, 'a.c': 1, 'a.geo': '2dsphere'}, "
        "bounds: {'a.b': [[-Infinity, 10, true, false]], 'a.c': [[2, 2, true, true]], "
        "'a.geo': [['MinKey', 'MaxKey', true, true]]}}}}}");
}

TEST_F(QueryPlannerGeo2dsphereTest, CanIntersectBoundsWhenSecondFieldIsNotMultikey) {
    MultikeyPaths multikeyPaths{{0U}, MultikeyComponents{}, MultikeyComponents{}};
    addIndex(BSON("a" << 1 << "b" << 1 << "geo"
                      << "2dsphere"),
             multikeyPaths);
    runQuery(fromjson("{a: 2, b: {$gte: 0, $lt: 10}, geo: {$nearSphere: [0, 0]}}"));

    assertNumSolutions(1U);
    assertSolutionExists(
        "{geoNear2dsphere: {pattern: {a: 1, b: 1, geo: '2dsphere'}, "
        "bounds: {a: [[2, 2, true, true]], b: [[0, 10, true, false]], "
        "geo: [['MinKey', 'MaxKey', true, true]]}}}");
}

TEST_F(QueryPlannerGeo2dsphereTest,
       CanIntersectBoundsOnSecondFieldWhenItAndSharedPrefixAreNotMultikey) {
    MultikeyPaths multikeyPaths{{1U}, MultikeyComponents{}, MultikeyComponents{}};
    addIndex(BSON("a.b" << 1 << "a.c" << 1 << "a.geo"
                        << "2dsphere"),
             multikeyPaths);
    runQuery(fromjson("{'a.b': 2, 'a.c': {$gte: 0, $lt: 10}, 'a.geo': {$nearSphere: [0, 0]}}"));

    assertNumSolutions(1U);
    assertSolutionExists(
        "{geoNear2dsphere: {pattern: {'a.b': 1, 'a.c': 1, 'a.geo': '2dsphere'}, "
        "bounds: {'a.b': [[2, 2, true, true]], 'a.c': [[0, 10, true, false]], "
        "'a.geo': [['MinKey', 'MaxKey', true, true]]}}}");
}

TEST_F(QueryPlannerGeo2dsphereTest, CannotIntersectBoundsWhenSecondFieldIsMultikey) {
    MultikeyPaths multikeyPaths{MultikeyComponents{}, {0U}, MultikeyComponents{}};
    addIndex(BSON("a" << 1 << "b" << 1 << "geo"
                      << "2dsphere"),
             multikeyPaths);
    runQuery(fromjson("{a: 2, b: {$gte: 0, $lt: 10}, geo: {$nearSphere: [0, 0]}}"));

    assertNumSolutions(1U);
    assertSolutionExists(
        "{fetch: {node: {geoNear2dsphere: {pattern: {a: 1, b: 1, geo: '2dsphere'}, "
        "bounds: {a: [[2, 2, true, true]], b: [[-Infinity, 10, true, false]], "
        "geo: [['MinKey', 'MaxKey', true, true]]}}}}}");
}

TEST_F(QueryPlannerGeo2dsphereTest, CanIntersectBoundsWhenSecondFieldIsMultikeyButHasElemMatch) {
    MultikeyPaths multikeyPaths{MultikeyComponents{}, {0U}, MultikeyComponents{}};
    addIndex(BSON("a" << 1 << "b" << 1 << "geo"
                      << "2dsphere"),
             multikeyPaths);
    runQuery(fromjson("{a: 2, b: {$elemMatch: {$gte: 0, $lt: 10}}, geo: {$nearSphere: [0, 0]}}"));

    assertNumSolutions(1U);
    assertSolutionExists(
        "{fetch: {node: {geoNear2dsphere: {pattern: {a: 1, b: 1, geo: '2dsphere'}, "
        "bounds: {a: [[2, 2, true, true]], b: [[0, 10, true, false]], "
        "geo: [['MinKey', 'MaxKey', true, true]]}}}}}");
}

TEST_F(QueryPlannerGeo2dsphereTest,
       CannotComplementBoundsOnSecondFieldWhenItIsMultikeyAndHasNotEqualExpr) {
    MultikeyPaths multikeyPaths{MultikeyComponents{}, {0U}, MultikeyComponents{}};
    addIndex(BSON("a" << 1 << "b" << 1 << "geo"
                      << "2dsphere"),
             multikeyPaths);
    runQuery(fromjson("{a: 2, b: {$ne: 3}, geo: {$nearSphere: [0, 0]}}"));

    assertNumSolutions(1U);
    assertSolutionExists(
        "{fetch: {node: {geoNear2dsphere: {pattern: {a: 1, b: 1, geo: '2dsphere'}, "
        "bounds: {a: [[2, 2, true, true]], b: [['MinKey', 'MaxKey', true, true]], "
        "geo: [['MinKey', 'MaxKey', true, true]]}}}}}");
}

TEST_F(QueryPlannerGeo2dsphereTest,
       CanIntersectBoundsWhenSecondFieldIsMultikeyAndHasNotInsideElemMatch) {
    MultikeyPaths multikeyPaths{MultikeyComponents{}, {0U}, MultikeyComponents{}};
    addIndex(BSON("a" << 1 << "b" << 1 << "geo"
                      << "2dsphere"),
             multikeyPaths);
    runQuery(fromjson(
        "{a: 2, b: {$elemMatch: {$not: {$gte: 10}, $gte: 0}}, geo: {$nearSphere: [0, 0]}}"));

    assertNumSolutions(1U);
    assertSolutionExists(
        "{fetch: {node: {geoNear2dsphere: {pattern: {a: 1, b: 1, geo: '2dsphere'}, "
        "bounds: {a: [[2, 2, true, true]], b: [[0, 10, true, false]], "
        "geo: [['MinKey', 'MaxKey', true, true]]}}}}}");
}

TEST_F(QueryPlannerGeo2dsphereTest,
       CanIntersectBoundsOnSecondFieldWhenSharedPrefixIsMultikeyButHasElemMatch) {
    MultikeyPaths multikeyPaths{{0U}, {0U}, {0U}};
    addIndex(BSON("a.b" << 1 << "a.c" << 1 << "a.geo"
                        << "2dsphere"),
             multikeyPaths);
    runQuery(fromjson(
        "{a: {$elemMatch: {b: 2, c: {$gte: 0, $lt: 10}}}, 'a.geo': {$nearSphere: [0, 0]}}"));

    assertNumSolutions(1U);
    assertSolutionExists(
        "{fetch: {node: {geoNear2dsphere: {pattern: {'a.b': 1, 'a.c': 1, 'a.geo': '2dsphere'}, "
        "bounds: {'a.b': [[2, 2, true, true]], 'a.c': [[0, 10, true, false]], "
        "'a.geo': [['MinKey', 'MaxKey', true, true]]}}}}}");
}

TEST_F(QueryPlannerGeo2dsphereTest,
       CannotIntersectBoundsOnSecondFieldWhenItAndSharedPrefixAreMultikey) {
    MultikeyPaths multikeyPaths{{0U}, {0U, 1U}, {0U}};
    addIndex(BSON("a.b" << 1 << "a.c" << 1 << "a.geo"
                        << "2dsphere"),
             multikeyPaths);
    runQuery(fromjson(
        "{a: {$elemMatch: {b: 2, c: {$gte: 0, $lt: 10}}}, 'a.geo': {$nearSphere: [0, 0]}}"));

    assertNumSolutions(1U);
    assertSolutionExists(
        "{fetch: {node: {geoNear2dsphere: {pattern: {'a.b': 1, 'a.c': 1, 'a.geo': '2dsphere'}, "
        "bounds: {'a.b': [[2, 2, true, true]], 'a.c': [[-Infinity, 10, true, false]], "
        "'a.geo': [['MinKey', 'MaxKey', true, true]]}}}}}");
}

TEST_F(QueryPlannerGeo2dsphereTest, CannotIntersectBoundsOfTwoSeparateElemMatches) {
    MultikeyPaths multikeyPaths{{0U}, {0U}, {0U}};
    addIndex(BSON("a.b" << 1 << "a.c" << 1 << "a.geo"
                        << "2dsphere"),
             multikeyPaths);

    runQuery(
        fromjson("{$and: [{a: {$elemMatch: {b: {$gte: 0}, c: {$lt: 20}}}}, "
                 "{a: {$elemMatch: {b: {$lt: 10}, c: {$gte: 5}}}}, "
                 "{'a.geo': {$nearSphere: [0, 0]}}]}"));

    assertNumSolutions(1U);
    assertSolutionExists(
        "{fetch: {node: {geoNear2dsphere: {pattern: {'a.b': 1, 'a.c': 1, 'a.geo': '2dsphere'}, "
        "bounds: {'a.b': [[-Infinity, 10, true, false]], 'a.c': [[5, Infinity, true, true]], "
        "'a.geo': [['MinKey', 'MaxKey', true, true]]}}}}}");
}

TEST_F(QueryPlannerGeo2dsphereTest, CanCompoundBoundsWhenSharedPrefixIsNotMultikey) {
    MultikeyPaths multikeyPaths{{1U}, {1U}, {1U}};
    addIndex(BSON("a.b" << 1 << "a.c" << 1 << "a.geo"
                        << "2dsphere"),
             multikeyPaths);
    runQuery(fromjson("{'a.b': 2, 'a.c': 3, 'a.geo': {$nearSphere: [0, 0]}}"));

    assertNumSolutions(1U);
    assertSolutionExists(
        "{geoNear2dsphere: {pattern: {'a.b': 1, 'a.c': 1, 'a.geo': '2dsphere'}, "
        "bounds: {'a.b': [[2, 2, true, true]], 'a.c': [[3, 3, true, true]], "
        "'a.geo': [['MinKey', 'MaxKey', true, true]]}}}");
}

TEST_F(QueryPlannerGeo2dsphereTest,
       CanCompoundBoundsWhenSharedPrefixIsNotMultikeyAndFirstFieldIsGeo) {
    MultikeyPaths multikeyPaths{{1U}, {1U}, {1U}};
    addIndex(BSON("a.geo"
                  << "2dsphere"
                  << "a.b" << 1 << "a.c" << 1),
             multikeyPaths);
    runQuery(fromjson("{'a.geo': {$nearSphere: [0, 0]}, 'a.b': 2, 'a.c': 3}"));

    assertNumSolutions(1U);
    assertSolutionExists(
        "{geoNear2dsphere: {pattern: {'a.geo': '2dsphere', 'a.b': 1, 'a.c': 1}, "
        "bounds: {'a.geo': [['MinKey', 'MaxKey', true, true]], 'a.b': [[2, 2, true, true]], "
        "'a.c': [[3, 3, true, true]]}}}");
}

TEST_F(QueryPlannerGeo2dsphereTest, CannotCompoundBoundsWhenSharedPrefixIsMultikey) {
    MultikeyPaths multikeyPaths{{0U}, {0U}, {0U}};
    addIndex(BSON("a.b" << 1 << "a.c" << 1 << "a.geo"
                        << "2dsphere"),
             multikeyPaths);
    runQuery(fromjson("{'a.b': 2, 'a.c': 3, 'a.geo': {$nearSphere: [0, 0]}}"));

    assertNumSolutions(1U);
    assertSolutionExists(
        "{fetch: {node: {geoNear2dsphere: {pattern: {'a.b': 1, 'a.c': 1, 'a.geo': '2dsphere'}, "
        "bounds: {'a.b': [[2, 2, true, true]], 'a.c': [['MinKey', 'MaxKey', true, true]], "
        "'a.geo': [['MinKey', 'MaxKey', true, true]]}}}}}");
}

TEST_F(QueryPlannerGeo2dsphereTest,
       CannotCompoundBoundsWhenSharedPrefixIsMultikeyAndFirstFieldIsGeo) {
    MultikeyPaths multikeyPaths{{0U}, {0U}, {0U}};
    addIndex(BSON("a.geo"
                  << "2dsphere"
                  << "a.b" << 1 << "a.c" << 1),
             multikeyPaths);
    runQuery(fromjson("{'a.geo': {$nearSphere: [0, 0]}, 'a.b': 2, 'a.c': 3}"));

    assertNumSolutions(1U);
    assertSolutionExists(
        "{fetch: {node: {geoNear2dsphere: {pattern: {'a.geo': '2dsphere', 'a.b': 1, 'a.c': 1}, "
        "bounds: {'a.geo': [['MinKey', 'MaxKey', true, true]], "
        "'a.b': [['MinKey', 'MaxKey', true, true]], "
        "'a.c': [['MinKey', 'MaxKey', true, true]]}}}}}");
}

TEST_F(QueryPlannerGeo2dsphereTest, CanCompoundBoundsWhenSharedPrefixIsMultikeyButHasElemMatch) {
    MultikeyPaths multikeyPaths{{0U}, {0U}, {0U}};
    addIndex(BSON("a.b" << 1 << "a.c" << 1 << "a.geo"
                        << "2dsphere"),
             multikeyPaths);
    runQuery(fromjson("{a: {$elemMatch: {b: 2, c: 3}}, 'a.geo': {$nearSphere: [0, 0]}}"));

    assertNumSolutions(1U);
    assertSolutionExists(
        "{fetch: {node: {geoNear2dsphere: {pattern: {'a.b': 1, 'a.c': 1, 'a.geo': '2dsphere'}, "
        "bounds: {'a.b': [[2, 2, true, true]], 'a.c': [[3, 3, true, true]], "
        "'a.geo': [['MinKey', 'MaxKey', true, true]]}}}}}");
}

TEST_F(QueryPlannerGeo2dsphereTest,
       CannotCompoundBoundsWhenSharedPrefixIsMultikeyButHasElemMatchAndFirstFieldIsGeo) {
    MultikeyPaths multikeyPaths{{0U}, {0U}, {0U}};
    addIndex(BSON("a.geo"
                  << "2dsphere"
                  << "a.b" << 1 << "a.c" << 1),
             multikeyPaths);
    runQuery(fromjson("{'a.geo': {$nearSphere: [0, 0]}, a: {$elemMatch: {b: 2, c: 3}}}"));

    assertNumSolutions(1U);
    assertSolutionExists(
        "{fetch: {node: {geoNear2dsphere: {pattern: {'a.geo': '2dsphere', 'a.b': 1, 'a.c': 1}, "
        "bounds: {'a.geo': [['MinKey', 'MaxKey', true, true]], "
        "'a.b': [['MinKey', 'MaxKey', true, true]], "
        "'a.c': [['MinKey', 'MaxKey', true, true]]}}}}}");
}

TEST_F(QueryPlannerGeo2dsphereTest, CannotCompoundBoundsWhenSharedPrefixInsideElemMatchIsMultikey) {
    MultikeyPaths multikeyPaths{{0U, 1U}, {0U, 1U}, {0U, 1U}};
    addIndex(BSON("a.b.c" << 1 << "a.b.d" << 1 << "a.b.geo"
                          << "2dsphere"),
             multikeyPaths);
    runQuery(fromjson("{a: {$elemMatch: {'b.c': 2, 'b.d': 3}}, 'a.b.geo': {$nearSphere: [0, 0]}}"));

    assertNumSolutions(1U);
    assertSolutionExists(
        "{fetch: {node: {geoNear2dsphere: {"
        "pattern: {'a.b.c': 1, 'a.b.d': 1, 'a.b.geo': '2dsphere'}, "
        "bounds: {'a.b.c': [[2, 2, true, true]], 'a.b.d': [['MinKey', 'MaxKey', true, true]], "
        "'a.b.geo': [['MinKey', 'MaxKey', true, true]]}}}}}");
}

TEST_F(QueryPlannerGeo2dsphereTest,
       CannotCompoundBoundsWhenSharedPrefixInsideElemMatchIsMultikeyAndFirstFieldIsGeo) {
    MultikeyPaths multikeyPaths{{0U, 1U}, {0U, 1U}, {0U, 1U}};
    addIndex(BSON("a.b.geo"
                  << "2dsphere"
                  << "a.b.c" << 1 << "a.b.d" << 1),
             multikeyPaths);
    runQuery(fromjson("{'a.b.geo': {$nearSphere: [0, 0]}, a: {$elemMatch: {'b.c': 2, 'b.d': 3}}}"));

    assertNumSolutions(1U);
    assertSolutionExists(
        "{fetch: {node: {geoNear2dsphere: {"
        "pattern: {'a.b.geo': '2dsphere', 'a.b.c': 1, 'a.b.d': 1}, "
        "bounds: {'a.b.geo': [['MinKey', 'MaxKey', true, true]], "
        "'a.b.c': [['MinKey', 'MaxKey', true, true]], "
        "'a.b.d': [['MinKey', 'MaxKey', true, true]]}}}}}");
}

TEST_F(QueryPlannerGeo2dsphereTest, CanIntersectBoundsOn2dsphereFieldWhenItIsNotMultikey) {
    MultikeyPaths multikeyPaths{MultikeyComponents{}};
    addIndex(BSON("geo"
                  << "2dsphere"),
             multikeyPaths);
    runQuery(
        fromjson("{$and: [{geo: {$nearSphere: [0, 0]}}, "
                 "{geo: {$geoIntersects: {$geometry: {type: 'Point', coordinates: [0, 0]}}}}]}"));

    assertNumSolutions(1U);
    assertSolutionExists(
        "{fetch: {filter: {geo: {$geoIntersects: "
        "{$geometry: {type: 'Point', coordinates: [0, 0]}}}}, "
        "node: {geoNear2dsphere: {pattern: {geo: '2dsphere'}}}}}}");
}

TEST_F(QueryPlannerGeo2dsphereTest, CannotIntersectBoundsOn2dsphereFieldWhenItIsMultikey) {
    MultikeyPaths multikeyPaths{{0U}};
    addIndex(BSON("geo"
                  << "2dsphere"),
             multikeyPaths);
    runQuery(
        fromjson("{$and: [{geo: {$nearSphere: [0, 0]}}, "
                 "{geo: {$geoIntersects: {$geometry: {type: 'Point', coordinates: [0, 0]}}}}]}"));

    assertNumSolutions(1U);
    assertSolutionExists(
        "{fetch: {filter: {geo: {$geoIntersects: "
        "{$geometry: {type: 'Point', coordinates: [0, 0]}}}}, "
        "node: {geoNear2dsphere: {pattern: {geo: '2dsphere'}, "
        "bounds: {geo: [['MinKey', 'MaxKey', true, true]]}}}}}}");
}

//
// 2dsphere V2 sparse indices, SERVER-9639
//

// A fixture to help run tests for multiple 2dsphere index versions.
class QueryPlanner2dsphereVersionTest : public QueryPlannerTest {
public:
    // For each 2dsphere index version in 'versions', verifies the planner returns
    // 'errorCode' for 'predicate' given 'keyPatterns'.
    void assertExpectedFailureFor2dsphereIndexVersions(std::vector<int> versions,
                                                       std::vector<BSONObj> keyPatterns,
                                                       BSONObj predicate,
                                                       ErrorCodes::Error errorCode) {
        for (auto version : versions) {
            params.indices.clear();
            for (auto keyPattern : keyPatterns) {
                addIndex(keyPattern, BSON("2dsphereIndexVersion" << version));
            }

            runInvalidQuery(predicate);
            ASSERT_EQUALS(plannerStatus.code(), errorCode);
        }
    }

    // For each 2dsphere index version in 'versions', verifies the planner generates
    // 'expectedSolutions' for 'predicate' given 'keyPatterns'.
    void testMultiple2dsphereIndexVersions(std::vector<int> versions,
                                           std::vector<BSONObj> keyPatterns,
                                           BSONObj predicate,
                                           std::vector<std::string> expectedSolutions) {
        for (auto version : versions) {
            params.indices.clear();
            for (auto keyPattern : keyPatterns) {
                addIndex(keyPattern, BSON("2dsphereIndexVersion" << version));
            }

            runQuery(predicate);

            assertNumSolutions(expectedSolutions.size());
            for (auto solution : expectedSolutions) {
                assertSolutionExists(solution);
            }
        }
    }

    // For each 2dsphere index version in 'versions', verifies the planner generates
    // 'numExpectedSolutions' for 'predicate' given 'keyPattern'.
    void testMultiple2dsphereIndexVersions(std::vector<int> versions,
                                           std::vector<BSONObj> keyPatterns,
                                           BSONObj predicate,
                                           size_t numExpectedSolutions) {
        for (auto version : versions) {
            params.indices.clear();
            for (auto keyPattern : keyPatterns) {
                addIndex(keyPattern, BSON("2dsphereIndexVersion" << version));
            }

            runQuery(predicate);

            assertNumSolutions(numExpectedSolutions);
        }
    }
};

// Basic usage of a sparse 2dsphere index.  V1 ignores the sparse field.  We can use any prefix
// of the index as every document is indexed.
TEST_F(QueryPlanner2dsphereVersionTest, TwoDSphereSparseV1) {
    std::vector<int> versions{1};
    std::vector<BSONObj> keyPatterns = {BSON("nonGeo" << 1 << "geo"
                                                      << "2dsphere")};
    BSONObj predicate = fromjson("{nonGeo: 7}");
    std::vector<std::string> solutions = {
        "{cscan: {dir: 1}}", "{fetch: {node: {ixscan: {pattern: {nonGeo: 1, geo: '2dsphere'}}}}}"};

    testMultiple2dsphereIndexVersions(versions, keyPatterns, predicate, solutions);
}

// V2 and V3 are "geo sparse" and remove the nonGeo assignment.  Can't use the index prefix here as
// it's a V2 index and we have no geo pred.
TEST_F(QueryPlanner2dsphereVersionTest, TwoDSphereSparseCantUse) {
    std::vector<int> versions{2, 3};
    std::vector<BSONObj> keyPatterns = {BSON("nonGeo" << 1 << "geo"
                                                      << "2dsphere")};
    BSONObj predicate = fromjson("{nonGeo: 7}");
    std::vector<std::string> solutions = {"{cscan: {dir: 1}}"};

    testMultiple2dsphereIndexVersions(versions, keyPatterns, predicate, solutions);
}

// We can use the index here as we have a geo pred.
TEST_F(QueryPlanner2dsphereVersionTest, TwoDSphereSparseOnePred) {
    std::vector<int> versions{2, 3};
    std::vector<BSONObj> keyPatterns = {BSON("geo"
                                             << "2dsphere")};
    BSONObj predicate =
        fromjson("{geo : { $geoWithin : { $centerSphere : [[ 10, 20 ], 0.01 ] } }}}");

    testMultiple2dsphereIndexVersions(versions, keyPatterns, predicate, 2U);
}

// V2 and V3 are geo-sparse and the planner removes the nonGeo assignment when there's no geo pred.
TEST_F(QueryPlanner2dsphereVersionTest, TwoDSphereSparseTwoPreds) {
    std::vector<int> versions{2, 3};
    std::vector<BSONObj> keyPatterns = {BSON("nonGeo" << 1 << "geo"
                                                      << "2dsphere"
                                                      << "geo2"
                                                      << "2dsphere")};

    // Non-geo preds can only use a collscan.
    {
        BSONObj predicate = fromjson("{nonGeo: 7}");
        std::vector<std::string> solutions = {"{cscan: {dir: 1}}"};
        testMultiple2dsphereIndexVersions(versions, keyPatterns, predicate, solutions);
    }

    // One geo pred so we can use the index.
    {
        BSONObj predicate =
            fromjson("{nonGeo: 7, geo : { $geoWithin : { $centerSphere : [[ 10, 20 ], 0.01] } }}}");
        testMultiple2dsphereIndexVersions(versions, keyPatterns, predicate, 2U);
    }

    // Two geo preds, so we can use the index still.
    {
        BSONObj predicate = fromjson(
            "{nonGeo: 7, geo : { $geoWithin : { $centerSphere : [[ 10, 20 ], 0.01 ] }},"
            " geo2 : { $geoWithin : { $centerSphere : [[ 10, 20 ], 0.01 ] }}}");
        testMultiple2dsphereIndexVersions(versions, keyPatterns, predicate, 2U);
    }
}

TEST_F(QueryPlanner2dsphereVersionTest, TwoDNearCompound) {
    std::vector<int> versions{2, 3};
    std::vector<BSONObj> keyPatterns = {BSON("geo"
                                             << "2dsphere"
                                             << "nongeo" << 1)};
    BSONObj predicate = fromjson("{geo: {$nearSphere: [-71.34895, 42.46037]}}");
    testMultiple2dsphereIndexVersions(versions, keyPatterns, predicate, 1U);
}

TEST_F(QueryPlanner2dsphereVersionTest, TwoDSphereSparseBelowOr) {
    params.options = QueryPlannerParams::NO_TABLE_SCAN;

    std::vector<int> versions{2, 3};
    std::vector<BSONObj> keyPatterns = {BSON("geo1"
                                             << "2dsphere"
                                             << "a" << 1 << "b" << 1),
                                        BSON("geo2"
                                             << "2dsphere"
                                             << "a" << 1 << "b" << 1)};

    BSONObj predicate = fromjson(
        "{a: 4, b: 5, $or: ["
        "{geo1: {$geoWithin: {$centerSphere: [[10, 20], 0.01]}}},"
        "{geo2: {$geoWithin: {$centerSphere: [[10, 20], 0.01]}}}]}");

    std::vector<std::string> solutions = {
        "{fetch: {filter: {a: 4, b: 5}, node: {or: {nodes: ["
        "{fetch: {node: {ixscan: {pattern: {geo1:'2dsphere',a:1,b:1}}}}},"
        "{fetch: {node: {ixscan: {pattern: {geo2:'2dsphere',a:1,b:1}}}}}"
        "]}}}}"};

    testMultiple2dsphereIndexVersions(versions, keyPatterns, predicate, solutions);
}

TEST_F(QueryPlanner2dsphereVersionTest, TwoDSphereSparseBelowElemMatch) {
    params.options = QueryPlannerParams::NO_TABLE_SCAN;

    std::vector<int> versions{2, 3};
    std::vector<BSONObj> keyPatterns = {BSON("a.b"
                                             << "2dsphere"
                                             << "a.c" << 1)};

    BSONObj predicate = fromjson(
        "{a: {$elemMatch: {b: {$geoWithin: {$centerSphere: [[10,20], 0.01]}},"
        "c: {$gt: 3}}}}");
    std::vector<std::string> solutions = {
        "{fetch: {node: {ixscan: {pattern: {'a.b': '2dsphere', 'a.c': 1}}}}}"};

    testMultiple2dsphereIndexVersions(versions, keyPatterns, predicate, solutions);
}

TEST_F(QueryPlanner2dsphereVersionTest,
       TwoDSphereSparseGeoPredicateInsideElemMatchWithOneElementOnTrailingField) {
    params.options = QueryPlannerParams::NO_TABLE_SCAN;

    std::vector<int> versions{2, 3};
    std::vector<BSONObj> keyPatterns = {BSON("a" << 1 << "b.c"
                                                 << "2dsphere")};

    BSONObj predicate =
        fromjson("{a: 1, b: {$elemMatch: {c: {$geoWithin: {$centerSphere: [[0, 0], 1]}}}}}");
    std::vector<std::string> solutions = {
        "{fetch: {filter: {b: {$elemMatch: {c: {$geoWithin: {$centerSphere: [[0, 0], 1]}}}}}, "
        "node: {ixscan: {pattern: {a: 1, 'b.c': '2dsphere'}}}}}"};

    testMultiple2dsphereIndexVersions(versions, keyPatterns, predicate, solutions);
}

TEST_F(QueryPlanner2dsphereVersionTest,
       TwoDSphereSparseGeoPredicateInsideElemMatchWithTwoElementsOnTrailingField) {
    params.options = QueryPlannerParams::NO_TABLE_SCAN;

    std::vector<int> versions{2, 3};
    std::vector<BSONObj> keyPatterns = {BSON("a" << 1 << "b.c" << 1 << "b.d"
                                                 << "2dsphere")};

    BSONObj predicate = fromjson(
        "{a: 1, f: 2, b: {$elemMatch: {c: 3, d: {$geoWithin: {$centerSphere: [[0, 0], 1]}}}}}");
    std::vector<std::string> solutions = {
        "{fetch: {filter: {f: 2, b: {$elemMatch: {c: 3, d: "
        "{$geoWithin: {$centerSphere: [[0, 0], 1]}}}}}, node: "
        "{ixscan: {pattern: {a: 1, 'b.c': 1, 'b.d': '2dsphere'}}}}}"};

    testMultiple2dsphereIndexVersions(versions, keyPatterns, predicate, solutions);
}

TEST_F(QueryPlanner2dsphereVersionTest,
       TwoDSphereSparseGeoPredicateInsideElemMatchWithUnindexedPredicate) {
    params.options = QueryPlannerParams::NO_TABLE_SCAN;

    std::vector<int> versions{2, 3};
    std::vector<BSONObj> keyPatterns = {BSON("a" << 1 << "b.c" << 1 << "b.d"
                                                 << "2dsphere")};

    BSONObj predicate = fromjson(
        "{a: 1, f: 2, b: {$elemMatch: {zz: 3, d: {$geoWithin: {$centerSphere: [[0, 0], 1]}}}}}");
    std::vector<std::string> solutions = {
        "{fetch: {filter: {f: 2, b: {$elemMatch: {zz: 3, d: "
        "{$geoWithin: {$centerSphere: [[0, 0], 1]}}}}}, node: "
        "{ixscan: {pattern: {a: 1, 'b.c': 1, 'b.d': '2dsphere'}}}}}"};

    testMultiple2dsphereIndexVersions(versions, keyPatterns, predicate, solutions);
}

TEST_F(QueryPlanner2dsphereVersionTest, TwoDSphereSparseNestedElemMatch) {
    params.options = QueryPlannerParams::NO_TABLE_SCAN;

    std::vector<int> versions{2, 3};
    std::vector<BSONObj> keyPatterns = {BSON("a" << 1 << "b.c" << 1 << "b.d.e"
                                                 << "2dsphere")};

    BSONObj predicate = fromjson(
        "{a: 1, f: 2, b: {$elemMatch: {c: 3, d: {$elemMatch: {e: "
        "{$geoWithin: {$centerSphere: [[0, 0], 1]}}}}}}}");
    std::vector<std::string> solutions = {
        "{fetch: {filter: {f: 2, b: {$elemMatch: {c: 3, d: {$elemMatch:"
        "{e: {$geoWithin: {$centerSphere: [[0, 0], 1]}}}}}}}, node: "
        "{ixscan: {pattern: {a: 1, 'b.c': 1, 'b.d.e': '2dsphere'}}}}}"};

    testMultiple2dsphereIndexVersions(versions, keyPatterns, predicate, solutions);
}

TEST_F(QueryPlanner2dsphereVersionTest, TwoDSphereSparseNestedElemMatchInsideOr) {
    params.options = QueryPlannerParams::NO_TABLE_SCAN;

    std::vector<int> versions{2, 3};
    std::vector<BSONObj> keyPatterns = {BSON("a" << 1 << "b.c" << 1 << "b.d.e"
                                                 << "2dsphere")};

    BSONObj predicate = fromjson(
        "{$or: [{a: 5, 'b.d.e': {$geoWithin: {$centerSphere: [[1, 1], 2]}}},"
        "{a: 1, f: 2, b: {$elemMatch: {c: 3, d: {$elemMatch: {e: "
        "{$geoWithin: {$centerSphere: [[0, 0], 1]}}}}}}}]}");
    std::vector<std::string> solutions = {
        "{or: {nodes: ["
        "{fetch: {filter: {f: 2, b: {$elemMatch: {c: 3, d: {$elemMatch: "
        "{e: {$geoWithin: {$centerSphere: [[0, 0], 1]}}}}}}}, node: "
        "{ixscan: {pattern: {a: 1, 'b.c': 1, 'b.d.e': '2dsphere'}}}}}, "
        "{fetch: {filter: {'b.d.e': {$geoWithin: {$centerSphere: [[1, 1], 2]}}}, node: "
        "{ixscan: {pattern: {a: 1, 'b.c': 1, 'b.d.e': '2dsphere'}}}}}]}}"};

    testMultiple2dsphereIndexVersions(versions, keyPatterns, predicate, solutions);
}

TEST_F(QueryPlanner2dsphereVersionTest, NegationWithoutGeoPredCannotUseGeoIndex) {
    params.options = QueryPlannerParams::NO_TABLE_SCAN;

    std::vector<int> versions{2, 3};
    std::vector<BSONObj> keyPatterns = {BSON("a" << 1 << "b"
                                                 << "2dsphere")};

    BSONObj predicate = fromjson("{a: {$ne: 3}}");

    // Only a COLLSCAN is possible, but COLLSCANs are prohibited above.
    ErrorCodes::Error err = ErrorCodes::NoQueryExecutionPlans;
    assertExpectedFailureFor2dsphereIndexVersions(versions, keyPatterns, predicate, err);
}

TEST_F(QueryPlannerTest, 2dInexactFetchPredicateOverTrailingFieldHandledCorrectly) {
    params.options = QueryPlannerParams::NO_TABLE_SCAN;

    addIndex(BSON("a"
                  << "2d"
                  << "b" << 1));

    runQuery(fromjson("{a: {$geoWithin: {$center: [[0, 0], 1]}}, b: {$exists: true}}"));
    assertNumSolutions(1U);
    assertSolutionExists(
        "{fetch: {filter: {a: {$geoWithin: {$center: [[0, 0], 1]}}, b: {$exists: true}}, node: "
        "{ixscan: {filter: null, pattern: {a: '2d', b: 1}}}}}");
}

TEST_F(QueryPlannerTest, 2dInexactFetchPredicateOverTrailingFieldHandledCorrectlyMultikey) {
    params.options = QueryPlannerParams::NO_TABLE_SCAN;

    const bool multikey = true;
    addIndex(BSON("a"
                  << "2d"
                  << "b" << 1),
             multikey);

    runQuery(fromjson("{a: {$geoWithin: {$center: [[0, 0], 1]}}, b: {$exists: true}}"));
    assertNumSolutions(1U);
    assertSolutionExists(
        "{fetch: {filter: {a: {$geoWithin: {$center: [[0, 0], 1]}}, b: {$exists: true}}, node: "
        "{ixscan: {filter: null, pattern: {a: '2d', b: 1}}}}}");
}

TEST_F(QueryPlannerTest, 2dNearInexactFetchPredicateOverTrailingFieldHandledCorrectly) {
    params.options = QueryPlannerParams::NO_TABLE_SCAN;

    addIndex(BSON("a"
                  << "2d"
                  << "b" << 1));

    runQuery(fromjson("{a: {$near: [0, 0]}, b: {$exists: true}}"));
    assertNumSolutions(1U);
    assertSolutionExists(
        "{fetch: {filter: {b: {$exists: true}}, node: {geoNear2d: {a: '2d', b: 1}}}}");
}

TEST_F(QueryPlannerTest, 2dNearInexactFetchPredicateOverTrailingFieldMultikey) {
    params.options = QueryPlannerParams::NO_TABLE_SCAN;

    const bool multikey = true;
    addIndex(BSON("a"
                  << "2d"
                  << "b" << 1),
             multikey);

    runQuery(fromjson("{a: {$near: [0, 0]}, b: {$exists: true}}"));
    assertNumSolutions(1U);
    assertSolutionExists(
        "{fetch: {filter: {b: {$exists: true}}, node: {geoNear2d: {a: '2d', b: 1}}}}");
}

TEST_F(QueryPlannerTest, 2dNearWithInternalExprEqOverTrailingField) {
    params.options = QueryPlannerParams::NO_TABLE_SCAN;
    addIndex(BSON("a"
                  << "2d"
                  << "b" << 1));

    runQuery(fromjson("{a: {$near: [0, 0]}, b: {$_internalExprEq: 1}}"));
    assertNumSolutions(1U);
    assertSolutionExists("{geoNear2d: {a: '2d', b: 1}}}}");
}

TEST_F(QueryPlannerTest, 2dNearWithInternalExprEqOverTrailingFieldMultikey) {
    const bool multikey = true;
    addIndex(BSON("a"
                  << "2d"
                  << "b" << 1),
             multikey);

    runQuery(fromjson("{a: {$near: [0, 0]}, b: {$_internalExprEq: 1}}"));
    assertNumSolutions(1U);
    assertSolutionExists(
        "{fetch: {filter: {b: {$_internalExprEq: 1}}, node: {geoNear2d: {a: '2d', b: 1}}}}");
}

TEST_F(QueryPlannerTest, 2dGeoWithinWithInternalExprEqOverTrailingField) {
    params.options = QueryPlannerParams::NO_TABLE_SCAN;
    addIndex(BSON("a"
                  << "2d"
                  << "b" << 1));

    runQuery(
        fromjson("{a: {$within: {$polygon: [[0,0], [2,0], [4,0]]}}, b: {$_internalExprEq: 2}}"));
    assertNumSolutions(1U);
    assertSolutionExists(
        "{fetch: {filter: {a: {$within: {$polygon: [[0,0], [2,0], [4,0]]}}}, node:"
        "{ixscan: {filter: {b: {$_internalExprEq: 2}}, pattern: {a: '2d', b: 1}}}}}");
}

TEST_F(QueryPlannerTest, 2dsphereNearWithInternalExprEq) {
    addIndex(BSON("a" << 1 << "b"
                      << "2dsphere"));
    runQuery(
        fromjson("{a: {$_internalExprEq: 0}, b: {$near: {$geometry: "
                 "{type: 'Point', coordinates: [2, 2]}}}}"));

    assertNumSolutions(1U);
    assertSolutionExists(
        "{geoNear2dsphere: {pattern: {a: 1, b: '2dsphere'}, "
        "bounds: {a: [[0,0,true,true]], b: [['MinKey','MaxKey',true,true]]}}}");
}

TEST_F(QueryPlannerTest, 2dsphereNonNearWithInternalExprEqOverLeadingField) {
    params.options = QueryPlannerParams::NO_TABLE_SCAN;
    addIndex(BSON("a" << 1 << "b"
                      << "2dsphere"));

    runQuery(
        fromjson("{a: {$_internalExprEq: 0}, b: {$geoWithin: {$centerSphere: [[0, 0], 10]}}}"));

    // This query will generate complex bounds, so we relax the checks to make the test readable.
    relaxBoundsCheckingToSubsetOnly();

    assertNumSolutions(1U);
    assertSolutionExists(
        "{fetch: {filter: {b: {$geoWithin: {$centerSphere: [[0, 0], 10]}}}, node: "
        "{ixscan: {pattern: {a: 1, b: '2dsphere'}, filter: null, bounds:"
        "{a: [[0,0,true,true]], b: []}}}}}");
}

TEST_F(QueryPlannerTest, 2dsphereNonNearWithInternalExprEqOverLeadingFieldMultikey) {
    params.options = QueryPlannerParams::NO_TABLE_SCAN;
    const bool multikey = true;
    addIndex(BSON("a" << 1 << "b"
                      << "2dsphere"),
             multikey);

    runInvalidQuery(
        fromjson("{a: {$_internalExprEq: 0}, b: {$geoWithin: {$centerSphere: [[0, 0], 10]}}}"));
    assertNoSolutions();
}

TEST_F(QueryPlannerTest, 2dsphereNonNearWithInternalExprEqOverTrailingField) {
    params.options = QueryPlannerParams::NO_TABLE_SCAN;
    addIndex(BSON("a"
                  << "2dsphere"
                  << "b" << 1));

    runQuery(
        fromjson("{b: {$_internalExprEq: 0}, a: {$geoWithin: {$centerSphere: [[0, 0], 10]}}}"));

    // This query will generate complex bounds, so we relax the checks to make the test readable.
    relaxBoundsCheckingToSubsetOnly();

    assertNumSolutions(1U);
    assertSolutionExists(
        "{fetch: {filter: {a: {$geoWithin: {$centerSphere: [[0, 0], 10]}}}, node: "
        "{ixscan: {pattern: {a : '2dsphere', b: 1}, filter: null, bounds:"
        "{a: [],"  // Complex, so leaving empty.
        "b: [[0,0,true,true]]}}}}}");
}

TEST_F(QueryPlannerTest, 2dsphereNonNearWithInternalExprEqOverTrailingFieldMultikey) {
    params.options = QueryPlannerParams::NO_TABLE_SCAN;
    const bool multikey = true;
    addIndex(BSON("a"
                  << "2dsphere"
                  << "b" << 1),
             multikey);

    runQuery(
        fromjson("{a: {$geoWithin: {$centerSphere: [[0, 0], 10]}}, b: {$_internalExprEq: 0}}"));

    // This query will generate complex bounds, so we relax the checks to make the test readable.
    relaxBoundsCheckingToSubsetOnly();

    assertNumSolutions(1U);
    assertSolutionExists(
        "{fetch: {filter: {a: {$geoWithin: {$centerSphere: [[0,0],10]}}, b: {$_internalExprEq: 0}},"
        "node: {ixscan: {pattern: {a : '2dsphere', b: 1}, filter: null, bounds:"
        "{a: [],"  // Complex, so leaving empty.
        "b: [['MinKey','MaxKey',true,true]]}}}}}");
}

TEST_F(QueryPlannerTest, 2dsphereBucketWithInternalBucketGeoWithin) {
    params.options = QueryPlannerParams::NO_TABLE_SCAN;
    addIndex(BSON("data.a"
                  << "2dsphere_bucket"),
             false);

    runQuery(
        fromjson("{$_internalBucketGeoWithin: {withinRegion: {$centerSphere: [[0, 0], 10]}, field: "
                 "\"a\"}}"));

    // This query will generate complex bounds, so we relax the checks to make the test readable.
    relaxBoundsCheckingToSubsetOnly();

    assertNumSolutions(1U);
    assertSolutionExists(
        "{fetch: {filter: {$_internalBucketGeoWithin: {withinRegion: {$centerSphere: [[0,0],10]}, "
        "field: 'a'}},"
        "node: {ixscan: {pattern: {'data.a' : '2dsphere_bucket'}, filter: null, bounds:"
        "{'data.a': []"  // Complex, so leaving empty.
        "}}}}}");
}

TEST_F(QueryPlannerTest, 2dsphereBucketWithInternalBucketGeoWithin2dGeneratesCollScan) {
    params.options = QueryPlannerParams::NO_TABLE_SCAN;
    addIndex(BSON("data.a"
                  << "2dsphere_bucket"),
             false);

    // This query should not produce bounds.
    runInvalidQuery(
        fromjson("{$_internalBucketGeoWithin: {withinRegion: {$center: [[0, 0], 10]}, field: "
                 "\"a\"}}"));
    ASSERT_EQUALS(plannerStatus.code(), ErrorCodes::NoQueryExecutionPlans);
}

TEST_F(QueryPlannerTest, 2dWithinPredicateOverTrailingFieldElemMatchMultikey) {
    params.options = QueryPlannerParams::NO_TABLE_SCAN;

    const bool multikey = true;
    addIndex(BSON("a"
                  << "2d"
                  << "b" << 1),
             multikey);

    runQuery(fromjson("{a: {$geoWithin: {$center: [[0, 0], 1]}}, b: {$elemMatch: {c: 1}}}"));
    assertNumSolutions(1U);
    assertSolutionExists(
        "{fetch: {filter: {$and: [{b: {$elemMatch: {c: 1}}}, {a: {$geoWithin: {$center: [ [ 0, 0 "
        "], 1 ]}}}]}, node: {ixscan: {filter: null, pattern: {a: '2d', b: 1}}}}}");
}


}  // namespace
