/**
 * This test ensures that queries using simple ranges handle bound inclusion properly.
 * @tags: [
 *   assumes_balancer_off
 * ]
 */
import {resultsEq} from "jstests/aggregation/extras/utils.js";
import {FixtureHelpers} from "jstests/libs/fixture_helpers.js";

const collNamePrefix = 'query_bound_inclusion_';
let collCount = 0;
let coll = db.getCollection(collNamePrefix + collCount++);
coll.drop();
assert.commandWorked(coll.insert({a: 1, b: 1}));
assert.commandWorked(coll.insert({a: 2, b: 2}));
assert.commandWorked(coll.insert({a: 3, b: 3}));

assert.commandWorked(coll.createIndex({a: 1}));

let res = coll.find().sort({a: 1}).toArray();
assert.eq(res.length, 3);
assert.eq(res[0].a, 1);
assert.eq(res[1].a, 2);
assert.eq(res[2].a, 3);

res = coll.find().sort({a: -1}).toArray();
assert.eq(res.length, 3);
assert.eq(res[0].a, 3);
assert.eq(res[1].a, 2);
assert.eq(res[2].a, 1);

res = coll.find().min({a: 1}).max({a: 3}).hint({a: 1}).toArray();
assert.eq(res.length, 2);
if (FixtureHelpers.numberOfShardsForCollection(coll) === 1) {
    assert.eq(res[0].a, 1);
    assert.eq(res[1].a, 2);
} else {
    // With more than one shard, we cannot assume the results will come back in order, since we
    // did not request a sort.
    assert(resultsEq(res.map((result) => result.a), [1, 2]));
}

res = coll.find().min({a: 1}).max({a: 3}).sort({a: -1}).hint({a: 1}).toArray();
assert.eq(res.length, 2);
assert.eq(res[0].a, 2);
assert.eq(res[1].a, 1);

assert.commandWorked(coll.createIndex({b: -1}));

res = coll.find().sort({b: -1}).toArray();
assert.eq(res.length, 3);
assert.eq(res[0].b, 3);
assert.eq(res[1].b, 2);
assert.eq(res[2].b, 1);

res = coll.find().sort({b: 1}).toArray();
assert.eq(res.length, 3);
assert.eq(res[0].b, 1);
assert.eq(res[1].b, 2);
assert.eq(res[2].b, 3);

res = coll.find().min({b: 3}).max({b: 1}).hint({b: -1}).toArray();
assert.eq(res.length, 2);
if (FixtureHelpers.numberOfShardsForCollection(coll) === 1) {
    assert.eq(res[0].b, 3);
    assert.eq(res[1].b, 2);
} else {
    // With more than one shard, we cannot assume the results will come back in order, since we
    // did not request a sort.
    assert(resultsEq(res.map((result) => result.b), [3, 2]));
}

res = coll.find().min({b: 3}).max({b: 1}).sort({b: 1}).hint({b: -1}).toArray();
assert.eq(res.length, 2);
assert.eq(res[0].b, 2);
assert.eq(res[1].b, 3);

// Bug SERVER-54552.
const testBoundsInclusivity = (indexDirection) => {
    coll = db.getCollection(collNamePrefix + collCount++);
    coll.drop();
    assert.commandWorked(coll.createIndex({i: indexDirection}));
    assert.commandWorked(coll.insert([{i: 'a'}, {i: 'b'}, {i: 'c'}, {i: 'd'}, {i: 'd'}, {i: 'e'}]));

    // Test for all the posible ranges.
    [['$gte', '$lte', 4], ['$gt', '$lte', 3], ['$gte', '$lt', 2], ['$gt', '$lt', 1]].forEach(
        ([gt, lt, count]) => {
            let query = {i: {}};
            query['i'][gt] = 'b';
            query['i'][lt] = 'd';

            assert.eq(count, coll.find(query).count(), {query, indexDirection});

            // Test with forward and backward sorts.
            assert.eq(count,
                      coll.find(query).sort({i: indexDirection}).count(),
                      {query, sort: 1, indexDirection});
            assert.eq(
                count, coll.find(query).sort({i: -1}).count(), {query, sort: -1, indexDirection});
        });
};
testBoundsInclusivity(1);
testBoundsInclusivity(-1);

// Tests inclusivity correctness with all sort directions on an index with a given direction
// for range queries that can use a 2 property compound index.
const testBoundsInclusivityCompound = (lIndexDirection, nIndexDirection) => {
    coll = db.getCollection(collNamePrefix + collCount++);
    coll.drop();
    assert.commandWorked(coll.createIndex({l: lIndexDirection, n: nIndexDirection}));
    let docs = [];
    ['a', 'b', 'c', 'd', 'e'].forEach((l) => [1, 2, 3, 4, 5].forEach((n) => docs.push({l, n})));
    assert.commandWorked(coll.insert(docs));

    // The eq variants of the operators add one more document to the result.
    const opExtra = {'$lt': 0, '$lte': 1, '$gt': 0, '$gte': 1};
    const lts = ['$lt', '$lte'];
    const gts = ['$gt', '$gte'];

    // Iterate through all posible combinations of operators.
    lts.forEach((llt) => gts.forEach((lgt) => {
        lts.forEach((nlt) => gts.forEach((ngt) => {
            let query = {l: {}, n: {}};
            query['l'][llt] = 'd';
            query['l'][lgt] = 'b';
            query['n'][nlt] = 4;
            query['n'][ngt] = 2;

            // Calculate the count of documents based on operators used.
            const count = (1 + opExtra[llt] + opExtra[lgt]) * (1 + opExtra[nlt] + opExtra[ngt]);
            assert.eq(count, coll.find(query).count(), {query, lIndexDirection, nIndexDirection});

            // Also check with all posible sort combinations.
            const withSort = (lSort, nSort) => {
                assert.eq(count,
                          coll.find(query).sort({l: lSort, n: nSort}).count(),
                          {query, lSort, nSort, lIndexDirection, nIndexDirection});
            };
            withSort(1, 1);
            withSort(-1, 1);
            withSort(1, -1);
            withSort(-1, -1);
        }));
    }));
};
testBoundsInclusivityCompound(1, 1);
testBoundsInclusivityCompound(-1, 1);
testBoundsInclusivityCompound(1, -1);
testBoundsInclusivityCompound(-1, -1);