// Test $minDistance option for $near and $nearSphere queries, and the $geoNear aggregation stage.
// @tags: [
//   requires_fastcount,
//   requires_getmore,
// ]

import {deg2rad, metersToRadians} from "jstests/libs/query/geo_math.js";

var t = db.geo_mindistance;
t.drop();

const km = 1000;

/**
 * Asserts that two numeric values are equal within some absolute error.
 */
function assertApproxEqual(rhs, lhs, error, msg) {
    assert.lt(Math.abs(rhs - rhs), error, msg);
}

/**
 * Count documents within some radius of (0, 0), in kilometers. With this function we can use
 * the existing $maxDistance option to test the newer $minDistance option's behavior.
 */
function n_docs_within(radius_km) {
    // $geoNear's distances are in meters for geoJSON points.
    return t
        .aggregate([
            {
                $geoNear: {
                    near: {type: 'Point', coordinates: [0, 0]},
                    distanceField: "dis",
                    spherical: true,
                    maxDistance: radius_km * km,
                }
            },
            {$limit: 1000}
        ])
        .itcount();
}

//
// Setup.
//

/**
 * Make 121 points from long, lat = (0, 0) (in Gulf of Guinea) to (10, 10) (inland Nigeria).
 */
for (var x = 0; x <= 10; x += 1) {
    for (var y = 0; y <= 10; y += 1) {
        t.insert({loc: [x, y]});
    }
}

t.createIndex({loc: "2dsphere"});

var n_docs = t.count(), geoJSONPoint = {type: 'Point', coordinates: [0, 0]}, legacyPoint = [0, 0];

//
// Test $near with GeoJSON point (required for $near with 2dsphere index). min/maxDistance are
// in meters.
//

var n_min1400_count =
    t.find({loc: {$near: {$geometry: geoJSONPoint, $minDistance: 1400 * km}}}).count();

assert.eq(n_docs - n_docs_within(1400),
          n_min1400_count,
          "Expected " + (n_docs - n_docs_within(1400)) +
              " points $near (0, 0) with $minDistance 1400 km, got " + n_min1400_count);

var n_bw500_and_1000_count =
    t.find({
         loc: {$near: {$geometry: geoJSONPoint, $minDistance: 500 * km, $maxDistance: 1000 * km}}
     }).count();

assert.eq(n_docs_within(1000) - n_docs_within(500),
          n_bw500_and_1000_count,
          "Expected " + (n_docs_within(1000) - n_docs_within(500)) +
              " points $near (0, 0) with $minDistance 500 km and $maxDistance 1000 km, got " +
              n_bw500_and_1000_count);

//
// $nearSphere with 2dsphere index can take a legacy or GeoJSON point. First test $nearSphere
// with legacy point. min/maxDistance are in radians.
//

n_min1400_count =
    t.find({loc: {$nearSphere: legacyPoint, $minDistance: metersToRadians(1400 * km)}}).count();

assert.eq(n_docs - n_docs_within(1400),
          n_min1400_count,
          "Expected " + (n_docs - n_docs_within(1400)) +
              " points $nearSphere (0, 0) with $minDistance 1400 km, got " + n_min1400_count);

n_bw500_and_1000_count = t.find({
                              loc: {
                                  $nearSphere: legacyPoint,
                                  $minDistance: metersToRadians(500 * km),
                                  $maxDistance: metersToRadians(1000 * km)
                              }
                          }).count();

assert.eq(n_docs_within(1000) - n_docs_within(500),
          n_bw500_and_1000_count,
          "Expected " + (n_docs_within(1000) - n_docs_within(500)) +
              " points $nearSphere (0, 0) with $minDistance 500 km and $maxDistance 1000 km, got " +
              n_bw500_and_1000_count);

//
// Test $nearSphere with GeoJSON point. min/maxDistance are in meters.
//

n_min1400_count = t.find({loc: {$nearSphere: geoJSONPoint, $minDistance: 1400 * km}}).count();

assert.eq(n_docs - n_docs_within(1400),
          n_min1400_count,
          "Expected " + (n_docs - n_docs_within(1400)) +
              " points $nearSphere (0, 0) with $minDistance 1400 km, got " + n_min1400_count);

n_bw500_and_1000_count =
    t.find({
         loc: {$nearSphere: geoJSONPoint, $minDistance: 500 * km, $maxDistance: 1000 * km}
     }).count();

assert.eq(n_docs_within(1000) - n_docs_within(500),
          n_bw500_and_1000_count,
          "Expected " + (n_docs_within(1000) - n_docs_within(500)) +
              " points $nearSphere (0, 0) with $minDistance 500 km and $maxDistance 1000 km, got " +
              n_bw500_and_1000_count);

//
// Test $geoNear aggregation stage with GeoJSON point. Distances are in meters.
//

let geoNearCount = t.aggregate({
                        $geoNear: {
                            near: {type: 'Point', coordinates: [0, 0]},
                            minDistance: 1400 * km,
                            spherical: true,
                            distanceField: "d",
                        }
                    }).itcount();
assert.eq(n_docs - n_docs_within(1400),
          geoNearCount,
          "Expected " + (n_docs - n_docs_within(1400)) +
              " points geoNear (0, 0) with $minDistance 1400 km, got " + geoNearCount);

geoNearCount = t.aggregate({
                    $geoNear: {
                        near: {type: 'Point', coordinates: [0, 0]},
                        minDistance: 500 * km,
                        maxDistance: 1000 * km,
                        spherical: true,
                        distanceField: "d",
                    }
                }).itcount();
assert.eq(n_docs_within(1000) - n_docs_within(500),
          geoNearCount,
          "Expected " + (n_docs_within(1000) - n_docs_within(500)) +
              " points geoNear (0, 0) with $minDistance 500 km and $maxDistance 1000 km, got " +
              geoNearCount);

//
// Test $geoNear aggregation stage with legacy point. Distances are in radians.
//

geoNearCount = t.aggregate({
                    $geoNear: {
                        near: legacyPoint,
                        minDistance: metersToRadians(1400 * km),
                        spherical: true,
                        distanceField: "d",
                    }
                }).itcount();
assert.eq(n_docs - n_docs_within(1400),
          geoNearCount,
          "Expected " + (n_docs - n_docs_within(1400)) +
              " points geoNear (0, 0) with $minDistance 1400 km, got " + geoNearCount);

geoNearCount = t.aggregate({
                    $geoNear: {
                        near: legacyPoint,
                        minDistance: metersToRadians(500 * km),
                        maxDistance: metersToRadians(1000 * km),
                        spherical: true,
                        distanceField: "d",
                    }
                }).itcount();
assert.eq(n_docs_within(1000) - n_docs_within(500),
          geoNearCount,
          "Expected " + (n_docs_within(1000) - n_docs_within(500)) +
              " points geoNear (0, 0) with $minDistance 500 km and $maxDistance 1000 km, got " +
              geoNearCount);

t.drop();
assert.commandWorked(t.createIndex({loc: "2d"}));
assert.commandWorked(t.insert({loc: [0, 40]}));
assert.commandWorked(t.insert({loc: [0, 41]}));
assert.commandWorked(t.insert({loc: [0, 42]}));

// Test minDistance for 2d index with $near.
assert.eq(3, t.find({loc: {$near: [0, 0]}}).itcount());
assert.eq(1, t.find({loc: {$near: [0, 0], $minDistance: 41.5}}).itcount());

// Test minDistance for 2d index with $nearSphere. Distances are in radians.
assert.eq(3, t.find({loc: {$nearSphere: [0, 0]}}).itcount());
assert.eq(1, t.find({loc: {$nearSphere: [0, 0], $minDistance: deg2rad(41.5)}}).itcount());

// Test minDistance for 2d index with $geoNear stage and spherical=false.
let cmdResult =
    t.aggregate({$geoNear: {near: [0, 0], spherical: false, distanceField: "dis"}}).toArray();
assert.eq(3, cmdResult.length);
assert.eq(40, cmdResult[0].dis);
assert.eq(41, cmdResult[1].dis);
assert.eq(42, cmdResult[2].dis);

cmdResult = t.aggregate({
                 $geoNear: {
                     near: [0, 0],
                     minDistance: 41.5,
                     spherical: false,
                     distanceField: "dis",
                 }
             }).toArray();
assert.eq(1, cmdResult.length);
assert.eq(42, cmdResult[0].dis);

// Test minDistance for 2d index with $geoNear stage and spherical=true. Distances are in
// radians.
cmdResult =
    t.aggregate({$geoNear: {near: [0, 0], spherical: true, distanceField: "dis"}}).toArray();
assert.eq(3, cmdResult.length);
assertApproxEqual(deg2rad(40), cmdResult[0].dis, 1e-3);
assertApproxEqual(deg2rad(41), cmdResult[1].dis, 1e-3);
assertApproxEqual(deg2rad(42), cmdResult[2].dis, 1e-3);

cmdResult = t.aggregate({
                 $geoNear: {
                     near: [0, 0],
                     minDistance: deg2rad(41.5),
                     spherical: true,
                     distanceField: "dis",
                 }
             }).toArray();
assert.eq(1, cmdResult.length);
assertApproxEqual(deg2rad(42), cmdResult[0].dis, 1e-3);