#include "common.h"
#include "fixture.h"

#include <maps/libs/concurrent/include/scoped_guard.h>
#include <maps/libs/img/include/algorithm.h>
#include <maps/libs/img/include/raster.h>
#include <maps/libs/log8/include/log8.h>
#include <maps/libs/pgpool/include/pgpool3.h>
#include <maps/libs/tile/include/geometry.h>
#include <maps/libs/tile/include/tile.h>
#include <maps/wikimap/mapspro/services/mrc/browser/lib/tile_renderer.h>
#include <maps/wikimap/mapspro/services/mrc/browser/lib/tools.h>
#include <maps/wikimap/mapspro/services/mrc/browser/lib/undistort.h>
#include <maps/wikimap/mapspro/services/mrc/libs/db/include/feature_gateway.h>
#include <maps/libs/common/include/file_utils.h>
#include <yandex/maps/mrc/unittest/database_fixture.h>
#include <yandex/maps/mrc/unittest/local_server.h>
#include <yandex/maps/wiki/common/pgpool3_helpers.h>

#include <library/cpp/testing/gmock_in_unittest/gmock.h>
#include <library/cpp/testing/unittest/env.h>
#include <library/cpp/testing/unittest/registar.h>
#include <library/cpp/testing/unittest/tests_data.h>

#include <boost/filesystem.hpp>
#include <boost/lexical_cast.hpp>

#include <algorithm>
#include <cstdint>
#include <fstream>
#include <iterator>
#include <sstream>
#include <vector>

namespace maps::mrc::browser::tests {

using namespace geolib3;
using namespace browser;
using namespace wiki::common;

namespace {

const std::string DATA_PATH = "./tests/data.sql";
const tile::Tile FEATURES_TILE(79222, 41104, 17);
const double TOLERANCE = 0.0001;

using TIdsSet = std::set<db::TId>;

TIdsSet collectFeatureIds(const db::Features& features)
{
    TIdsSet result;
    for (const auto& feature: features) {
        result.insert(feature.id());
    }
    return result;
}

} // anonymous namespace

Y_UNIT_TEST_SUITE(tests)
{

Y_UNIT_TEST(testToTrack)
{
    auto track
        = toTrack("{\"geometry\": [[37.2545, 55.1030],[37.2521, "
                  "55.1023],[37.2508, 55.1024],[37.2501, 55.1020]]}");
    PointsVector result{{37.2545, 55.1030},
                        {37.2521, 55.1023},
                        {37.2508, 55.1024},
                        {37.2501, 55.1020}};
    EXPECT_EQ(track.points(), result);
}

Y_UNIT_TEST(testLoadVisibleFeatures)
{
    Fixture fixture;
    auto dataAccess = std::make_shared<DbDataAccess>(fixture.sharedPool());

    EXPECT_EQ(8ul, loadFeaturesWithMargins(dataAccess, mercatorBBox(FEATURES_TILE), {.uid=fixture.UID}).size());

    EXPECT_EQ(4ul, loadFeaturesWithMargins(
                       dataAccess, mercatorBBox(FEATURES_TILE),
                       mrc::browser::FeatureFilter{.cameraDeviation=db::CameraDeviation::Right, .uid=fixture.UID})
                       .size());
}

Y_UNIT_TEST(testToPathJson)
{
    Fixture fixture;
    static const std::string RESPONSE_PATH
        = "get_path.response.json";
    static const std::string SCHEMA_PATH
        = apiResponseSchemaPath("browser/get_path.response.schema.json");
    static const db::TId BASE_FEATURE_ID = 2;

    json::Builder builder;
    builder << [&](json::ObjectBuilder b) {
        toPathJson(BASE_FEATURE_ID,
                   db::FeatureGateway{*fixture.txnHandle()}.loadSequence(
                       BASE_FEATURE_ID, 1, 1),
                   "http://localhost/",
                   ResponseOptions{},
                   b);
    };

    maps::common::writeFile(RESPONSE_PATH, builder.str());
    validateJson(RESPONSE_PATH, SCHEMA_PATH);

    auto json = json::Value::fromFile(RESPONSE_PATH);
    auto features = json["data"]["features"];
    EXPECT_EQ(features.size(), 2ul);

    auto feature = *features.begin();
    auto id = feature["properties"]["id"];
    EXPECT_EQ(boost::lexical_cast<db::TId>(id.as<std::string>()),
              BASE_FEATURE_ID);
    auto coordinates = feature["geometry"]["coordinates"];
    EXPECT_EQ(coordinates.size(), 2ul);
    EXPECT_NEAR(coordinates[0].as<double>(), 37.5915711, TOLERANCE);
    EXPECT_NEAR(coordinates[1].as<double>(), 55.7317344, TOLERANCE);
    auto heading = feature["properties"]["heading"];
    EXPECT_NEAR(heading.as<double>(), 227, TOLERANCE);

    feature = *std::next(features.begin());
    id = feature["properties"]["id"];
    EXPECT_EQ(boost::lexical_cast<db::TId>(id.as<std::string>()), 4l);
}

Y_UNIT_TEST(testToSimilarJson)
{
    Fixture fixture;
    static const std::string RESPONSE_PATH
        = "get_similar.response.json";
    static const std::string SCHEMA_PATH
        = apiResponseSchemaPath("browser/get_similar.response.schema.json");
    static const db::TId BASE_FEATURE_ID = 2;

    IdToPointMap targetPoints = {
        { BASE_FEATURE_ID, geolib3::Point2(37.591535, 55.732321) }
    };

    json::Builder builder;
    builder << [&](json::ObjectBuilder b) {
        auto txn = fixture.txnHandle();
        db::FeatureGateway gtw{*txn};
        auto ids = gtw.loadIds();
        auto features = gtw.loadByIds(ids);
        toSimilarJson(BASE_FEATURE_ID, std::move(features), targetPoints,
                      "http://localhost/", ResponseOptions{}, b);
    };

    maps::common::writeFile(RESPONSE_PATH, builder.str());
    validateJson(RESPONSE_PATH, SCHEMA_PATH);

    auto json = json::Value::fromFile(RESPONSE_PATH);
    auto features = json["data"]["features"];
    EXPECT_EQ(features.size(), 2ul);

    auto feature = features[0];
    auto id = feature["properties"]["id"];
    EXPECT_EQ(boost::lexical_cast<db::TId>(id.as<std::string>()),
              BASE_FEATURE_ID);
    auto coordinates = feature["geometry"]["coordinates"];
    EXPECT_EQ(coordinates.size(), 2ul);
    EXPECT_NEAR(coordinates[0].as<double>(), 37.591571, TOLERANCE);
    EXPECT_NEAR(coordinates[1].as<double>(), 55.731734, TOLERANCE);
    auto heading = feature["properties"]["heading"];
    EXPECT_NEAR(heading.as<double>(), 357.969333, TOLERANCE);
    EXPECT_TRUE(feature["properties"].hasField("imagePreview"));
    EXPECT_TRUE(feature["properties"].hasField("imageFull"));
    EXPECT_TRUE(
        feature["properties"].hasField("targetGeometry"));
}

Y_UNIT_TEST(testToNearJson)
{
    Fixture fixture;
    static const std::string RESPONSE_PATH = "get_near.response.json";
    static const std::string SCHEMA_PATH =
        apiResponseSchemaPath("browser/get_near.response.schema.json");

    IdToPointMap targetPoints = {{2, geolib3::Point2(0.0, 0.0)}};

    json::Builder builder;
    builder << [&](json::ObjectBuilder b) {
        auto txn = fixture.txnHandle();
        db::FeatureGateway gtw{*txn};
        auto ids = gtw.loadIds();
        auto features = gtw.loadByIds(ids);
        toNearJson(geolib3::Point2(37.59167, 55.73181),
                   geolib3::Direction2{geolib3::Heading{227}},
                   SIMILARITY_RADIUS_METERS,
                   std::move(features),
                   targetPoints,
                   "http://localhost/",
                   ResponseOptions{},
                   b);
    };

    maps::common::writeFile(RESPONSE_PATH, builder.str());
    validateJson(RESPONSE_PATH, SCHEMA_PATH);

    auto json = json::Value::fromFile(RESPONSE_PATH);
    auto features = json["data"]["features"];
    EXPECT_EQ(features.size(), 2ul);
}

Y_UNIT_TEST(testToDirectionsResponseJson)
{
    Fixture fixture;
    const std::string RESPONSE = "get_directions.response.json";
    const std::string SCHEMA =
        apiResponseSchemaPath("browser/get_directions.response.schema.json");

    json::Builder builder;
    builder << [&](json::ObjectBuilder bld) {
        auto baseRay = db::Ray{geolib3::Point2(37.569462, 55.791449),
                               Direction2(Heading(120))};
        auto feature = db::Feature{"source",
                                   Point2(37.569588, 55.791293),
                                   Heading(190),
                                   "2019-09-16 11:22:00",
                                   mds::Key("groupId", "path"),
                                   {}}
                           .setSize(1920, 1080)
                           .setAutomaticShouldBePublished(true)
                           .setIsPublished(true)
                           .setCameraDeviation(db::CameraDeviation::Front)
                           .setPrivacy(db::FeaturePrivacy::Public);
        toDirectionsResponseJson(baseRay,
                                 {feature},
                                 "http://localhost/",
                                 false /*with_authors*/,
                                 bld);
    };

    maps::common::writeFile(RESPONSE, builder.str());
    validateJson(RESPONSE, SCHEMA);

    auto json = json::Value::fromFile(RESPONSE);
    auto features = json["directions"]["features"];
    EXPECT_EQ(features.size(), 1u);
    auto feature = features[0];
    EXPECT_TRUE(feature.hasField(FIELD_ANGLE));
    EXPECT_EQ(feature[FIELD_ANGLE].as<double>(), 21.);
    EXPECT_TRUE(feature.hasField(FIELD_DISTANCE));
    EXPECT_EQ(feature[FIELD_DISTANCE].as<double>(), 19.);
}

Y_UNIT_TEST(testUndistort)
{
    std::ifstream input(dataPath("./tests/fisheye.jpg"), std::ios::binary);
    common::Bytes distorted((std::istreambuf_iterator<char>(input)),
                                 (std::istreambuf_iterator<char>()));
    auto undistorted = maps::mrc::common::encodeImage(
        undistort(maps::mrc::common::decodeImage(distorted)));
    EXPECT_FALSE(undistorted.empty());
}

Y_UNIT_TEST(testSnapFeatureToRoadGraph)
{
    Fixture fixture;
    for (const auto& test : {db::Feature{1}
                                 .setGeodeticPos(Point2{37.5344, 55.7253})
                                 .setHeading(Heading{161}),
                             db::Feature{2}
                                 .setGeodeticPos(Point2{37.5345, 55.7252})
                                 .setHeading(Heading{151})
                                 .setCameraDeviation(db::CameraDeviation::Right)}) {
        auto feature = test;
        EXPECT_TRUE(
            snapToGraph(*Configuration::instance()->roadGraph(), feature));
        auto meters = fastGeoDistance(feature.geodeticPos(),
                                      test.geodeticPos());
        auto angle = angleBetween(Direction2{feature.heading()},
                                  Direction2{test.heading()});
        EXPECT_GT(meters, 1.);
        EXPECT_LT(meters, export_gen::COVERAGE_METERS_SNAP_THRESHOLD);
        EXPECT_GT(angle, Radians{PI / 180.});
        EXPECT_LT(angle, export_gen::COVERAGE_ANGLE_DIFF_THRESHOLD);
    }
}

Y_UNIT_TEST(testLoadFeaturesTourAlongLine)
{
    /*
      mid ▼       ▲
          │ 0   6 ║
          │ 1   5 ║
          │ 2 3 4 ║
    old ►═╪═══════╝
          └─────────►
    new ►═══════════►
    */

    const std::array POINTS = {
        geolib3::Point2{37.563593, 55.667642},
        geolib3::Point2{37.563218, 55.667432},
        geolib3::Point2{37.562877, 55.667100},
        geolib3::Point2{37.563185, 55.666995},
        geolib3::Point2{37.563510, 55.666883},
        geolib3::Point2{37.563835, 55.667183},
        geolib3::Point2{37.564095, 55.667454},
        geolib3::Point2{37.564304, 55.667627}
    };

    const auto NORTH = Heading{31};
    const auto EAST = Heading{121};
    const auto SOUTH = Heading{211};
    const std::string SRC_OLD = "old";
    const std::string SRC_MID = "mid";
    const std::string SRC_NEW = "new";
    const mds::Key MDS("groupId", "path");

    auto features = db::Features{
        db::Feature{SRC_OLD, POINTS[2], EAST, "2019-09-16 11:22:00", MDS, {}},
        db::Feature{SRC_OLD, POINTS[3], EAST, "2019-09-16 11:22:01", MDS, {}},
        db::Feature{SRC_OLD, POINTS[4], EAST, "2019-09-16 11:22:02", MDS, {}},
        db::Feature{SRC_OLD, POINTS[5], NORTH, "2019-09-16 11:22:03", MDS, {}},
        db::Feature{SRC_OLD, POINTS[6], NORTH, "2019-09-16 11:22:04", MDS, {}},
        db::Feature{SRC_MID, POINTS[0], SOUTH, "2019-09-16 12:03:00", MDS, {}},
        db::Feature{SRC_MID, POINTS[1], SOUTH, "2019-09-16 12:03:01", MDS, {}},
        db::Feature{SRC_MID, POINTS[2], EAST, "2019-09-16 12:03:02", MDS, {}},
        db::Feature{SRC_MID, POINTS[3], EAST, "2019-09-16 12:03:03", MDS, {}},
        db::Feature{SRC_MID, POINTS[4], EAST, "2019-09-16 12:03:04", MDS, {}},
        db::Feature{SRC_NEW, POINTS[2], EAST, "2019-09-16 14:44:00", MDS, {}},
        db::Feature{SRC_NEW, POINTS[3], EAST, "2019-09-16 14:44:01", MDS, {}},
        db::Feature{SRC_NEW, POINTS[4], EAST, "2019-09-16 14:44:02", MDS, {}},
    };

    for (auto& feature : features) {
        feature.setSize(1920, 1080)
            .setAutomaticShouldBePublished(true)
            .setIsPublished(true)
            .setCameraDeviation(db::CameraDeviation::Front)
            .setPrivacy(db::FeaturePrivacy::Public);
    }

    UserMayViewPhotosFixture<true> fixture([&](pgpool3::Pool& pool){
        auto txn = pool.masterWriteableTransaction();
        db::FeatureGateway{*txn}.insert(features);
        db::updateFeaturesTransaction(features, *txn);
        txn->commit();
        Playground::instance().makeCoverage();
    });

    Polyline2 line;
    for (size_t i : {0, 2, 4, 7}) {
        line.add(POINTS[i]);
    }

    auto result = loadFeaturesTourAlongLine(Configuration::instance()->dataAccess(),
                                            Configuration::instance()->roadGraph(),
                                            line,
                                            db::CameraDeviation::Front);
    const std::array EXPECTED_INDICES = {5, 6, 10, 11, 12, 3, 4};
    std::vector<db::TId> expectedIds;
    for (int64_t idx : EXPECTED_INDICES) {
        expectedIds.push_back(features.at(idx).id());
    }
    std::vector<db::TId> resultIds;
    for (const auto& feature : result) {
        resultIds.push_back(feature.id());
    }
    EXPECT_THAT(resultIds, testing::ElementsAreArray(expectedIds));
}

Y_UNIT_TEST(testEvalCoveredRoadGraphDirections)
{
    /* Road graph configuration
       Notice features are marked by indices in the features array
       Their ID's are greater by 1

                two-way
                   ║
                   ║▲
                   ║15
              ‖‖‖‖‖‖‖‖‖‖‖‖ crosswalks
                 16║       ═
                  ▼║       ═
                   ║       ═  ◀14
    ◀-◀-◀-◀-◀-◀-◀-◀╫◀-◀-◀-◀═◀-◀-◀-◀ one-way
                  0║▲      ═
                  ▼║9      ═
                   ║       ═
                  1║▲      ═
                  ▼║8      ═
                   ║       ═
               ▲  2║▲      ═
      forbidden┘  ▼║7      ═
    ▶─▶─▶─▶─▶─▶─▶─▶╫▶─▶─▶─▶═▶─▶─▶─▶ one-way
     10▶  11▶  12▶ ║       ═ 13▶[too distant from all other features]
                  3║▲  ┌▶  ═
                  ▼║6  forbidden
                   ║
                  4║▲
                  ▼║5
                   ║
    */

    // Prepare test data
    const auto EAST = Heading{120};
    const auto WEST = Heading{280};
    const auto NORTH = Heading{10};
    const auto SOUTH = Heading{190};
    const auto SRC_A = std::string("A");
    const auto SRC_B = std::string("B");
    const auto SRC_C = std::string("C");
    const auto SRC_D = std::string("D");
    const auto TIME_0 = std::string("2019-09-16 11:22:00");
    const auto TIME_1 = std::string("2019-09-16 12:22:00");
    const auto TIME_2 = std::string("2019-09-16 13:22:00");
    const auto TIME_3 = std::string("2019-09-16 14:22:00");
    const auto TIME_4 = std::string("2019-09-16 15:22:00");
    const auto MDS = mds::Key("groupId", "path");

    const std::array POINTS = {
        Point2{37.569592, 55.791617}, // 0
        Point2{37.569605, 55.791512}, // 1
        Point2{37.569563, 55.791444}, // 2
        Point2{37.569588, 55.791293}, // 3
        Point2{37.569552, 55.791215}, // 4

        Point2{37.569239, 55.791547}, // 5
        Point2{37.569406, 55.791476}, // 6
        Point2{37.569443, 55.791461}, // 7
        Point2{37.570098, 55.791257}, // 8

        Point2{37.569972, 55.791634}, // 9
        Point2{37.569569, 55.791831}, // 10
        Point2{37.569582, 55.791775}, // 11
    };

    auto features = db::Features{
        db::Feature{SRC_A, POINTS[0], SOUTH, TIME_0, MDS, {}},  // 0
        db::Feature{SRC_A, POINTS[1], SOUTH, TIME_1, MDS, {}},  // 1
        db::Feature{SRC_A, POINTS[2], SOUTH, TIME_2, MDS, {}},  // 2
        db::Feature{SRC_A, POINTS[3], SOUTH, TIME_3, MDS, {}},  // 3
        db::Feature{SRC_A, POINTS[4], SOUTH, TIME_4, MDS, {}},  // 4

        db::Feature{SRC_B, POINTS[4], NORTH, TIME_0, MDS, {}},  // 5
        db::Feature{SRC_B, POINTS[3], NORTH, TIME_1, MDS, {}},  // 6
        db::Feature{SRC_B, POINTS[2], NORTH, TIME_2, MDS, {}},  // 7
        db::Feature{SRC_B, POINTS[1], NORTH, TIME_3, MDS, {}},  // 8
        db::Feature{SRC_B, POINTS[0], NORTH, TIME_4, MDS, {}},  // 9

        db::Feature{SRC_C, POINTS[5], EAST, TIME_0, MDS, {}},  // 10
        db::Feature{SRC_C, POINTS[6], EAST, TIME_1, MDS, {}},  // 11
        db::Feature{SRC_C, POINTS[7], EAST, TIME_2, MDS, {}},  // 12
        db::Feature{SRC_C, POINTS[8], EAST, TIME_4, MDS, {}},  // 13

        db::Feature{SRC_D, POINTS[9], WEST, TIME_0, MDS, {}},  // 14
        db::Feature{SRC_D, POINTS[10], NORTH, TIME_1, MDS, {}},  // 15
        db::Feature{SRC_D, POINTS[11], SOUTH, TIME_2, MDS, {}},  // 16
    };

    for (auto& feature : features) {
        feature.setSize(1920, 1080)
            .setAutomaticShouldBePublished(true)
            .setIsPublished(true)
            .setCameraDeviation(db::CameraDeviation::Front)
            .setPrivacy(db::FeaturePrivacy::Public);
    }

    UserMayViewPhotosFixture<true> fixture([&](pgpool3::Pool& pool) {
        auto txn = pool.masterWriteableTransaction();
        db::FeatureGateway{*txn}.insert(features);
        db::updateFeaturesTransaction(features, *txn);
        txn->commit();
        Playground::instance().makeCoverage();
    });

    FeatureFilter filter{.uid = fixture.UID};

    auto data = Configuration::instance()->dataAccess();
    auto graph = Configuration::instance()->roadGraph();

    // No turnaround feature around
    {
        const auto ray = db::getRay(features[11]);
        const TIdsSet expected = {
            features[10].id(), // backward feature on the same edge
            features[12].id()  // forward feature on the same edge
        };

        const TIdsSet actual = collectFeatureIds(
            evalCoveredRoadGraphDirections(ray, *data, *graph, filter));

        EXPECT_EQ(actual, expected);
    }

    // No crossroads before/after backward/forward features
    {
        const auto ray = db::getRay(features[1]);
        const TIdsSet expected = {
            features[0].id(), // backward feature on the same edge
            features[2].id(), // forward feature on the same edge
            features[8].id()  // turnaround feature on a reverse edge
        };

        const TIdsSet actual = collectFeatureIds(
            evalCoveredRoadGraphDirections(ray, *data, *graph, filter));

        EXPECT_EQ(actual, expected);
    }

    // Backward feature multiple edges behind
    {
        const auto ray = db::getRay(features[0]);
        const TIdsSet expected = {
            features[16].id(), // backward feature behind a crossroad
            features[14].id(), // backward feature muiltiple edges behind
            features[1].id(),  // forward feature on the same edge
            features[9].id()   // turnaround feature on a reverse edge
        };

        const TIdsSet actual = collectFeatureIds(
            evalCoveredRoadGraphDirections(ray, *data, *graph, filter));

        EXPECT_EQ(actual, expected);
    }

    // Follow crossroads in front in all physically accessible directions
    {
        const auto ray = db::getRay(features[2]);
        const TIdsSet expected = {
            features[1].id(),  // backward feature on the same edge
            features[3].id(),  // forward feature behind a crossroad
            features[7].id()   // turnaround feature on a reverse edge
            // Notice that the feature 13 is too distant from the feature 2
            // so it is not included into the result
        };

        const TIdsSet actual = collectFeatureIds(
            evalCoveredRoadGraphDirections(ray, *data, *graph, filter));

        EXPECT_EQ(actual, expected);
    }

    // Backward features from multiple incoming edges
    {
        const auto ray = db::getRay(features[3]);
        const TIdsSet expected = {
            features[2].id(), // backward feature from an incoming edge
            features[12].id(),// backward feature from another incoming edge
            features[4].id(), // forward feature on the same edge
            features[6].id()  // turnaround feature on a reverse edge
        };

        const TIdsSet actual = collectFeatureIds(
            evalCoveredRoadGraphDirections(ray, *data, *graph, filter));

        EXPECT_EQ(actual, expected);
    }

    // Forward features on multiple outgoing edges
    {
        const auto ray = db::getRay(features[12]);
        const TIdsSet expected = {
            features[3].id(), // forward feature on and outgoing edge
            features[7].id(), // forward feature on and outgoing edge
            features[11].id() // backward feature on the same edge
        };

        const TIdsSet actual = collectFeatureIds(
            evalCoveredRoadGraphDirections(ray, *data, *graph, filter));

        EXPECT_EQ(actual, expected);
    }

    // No forward features within distance limits
    {
        const auto ray = db::getRay(features[4]);
        const TIdsSet expected = {
            features[3].id(), // backward feature on the same edge
            features[5].id()  // turnaround feature on a reverse edge
        };

        const TIdsSet actual = collectFeatureIds(
            evalCoveredRoadGraphDirections(ray, *data, *graph, filter));

        EXPECT_EQ(actual, expected);
    }

    // Turnaround feature behind roads joint when there is signle incoming edge
    {
        const auto ray = db::getRay(features[16]);
        const TIdsSet expected = {
            features[15].id(), // turnaround feature behind a single joint
            features[0].id()   // forward feature after a crossroad
        };

        const TIdsSet actual = collectFeatureIds(
            evalCoveredRoadGraphDirections(ray, *data, *graph, filter));

        EXPECT_EQ(actual, expected);
    }

    // There is a forward feature after multiple joints/crossroads which is
    // within dinstance limits
    {
        const auto ray = db::getRay(features[14]);
        const TIdsSet expected = {
            features[0].id(), // forward feature after multiple joints
        };

        const TIdsSet actual = collectFeatureIds(
            evalCoveredRoadGraphDirections(ray, *data, *graph, filter));

        EXPECT_EQ(actual, expected);
    }

    // There is a turnaround feature behind a roads joint on a single incoming edge
    {
        const auto ray = db::getRay(features[15]);
        const TIdsSet expected = {
            features[9].id(), // backward feature after multiple joints
            features[16].id() // turnaround feature from a single incoming edge
        };

        const TIdsSet actual = collectFeatureIds(
            evalCoveredRoadGraphDirections(ray, *data, *graph, filter));

        EXPECT_EQ(actual, expected);
    }

    // No features around within distance limits
    {
        const auto ray = db::getRay(features[13]);
        const TIdsSet expected = {};

        const TIdsSet actual = collectFeatureIds(
            evalCoveredRoadGraphDirections(ray, *data, *graph, filter));

        EXPECT_EQ(actual, expected);
    }
}

Y_UNIT_TEST(testRemoveOldFeatures)
{
   /*
    Map   : { 0(old) -- 1(old) -- 2(old) -- 3(new) -- 4(old) -- 5(old) -- 6(new) -- 7(new) -- 8(new) }
    Result: { 0(old) -- 1(old) -- 2(old) -- ------ -- 4(old) -- 5(old) -- 6(new) -- ------ -- 8(new) }
    distances:
    0(old) -> 1(old) : 18.5699
    1(old) -> 2(old) : 20.734
    2(old) -> 3(new) : 9.0801
    3(new) -> 4(old) : 7.40393
    4(old) -> 5(old) : 18.3372
    5(old) -> 6(new) : 34.5298
    6(new) -> 7(new) : 10.4283
    7(old) -> 8(new) : 9.97684
   */
    const std::string SRC_OLD = "old";
    const std::string SRC_NEW = "new";

    const std::array FEATURE_PROPS = {
        std::make_tuple(geolib3::Point2{37.57596804852838,  55.67849743685277},  "2020-11-11 11:22:00", SRC_OLD),
        std::make_tuple(geolib3::Point2{37.57612629886029,  55.67863838426207},  "2020-11-11 11:22:01", SRC_OLD),
        std::make_tuple(geolib3::Point2{37.57629796023716,  55.678797517821565}, "2020-11-11 11:22:02", SRC_OLD),
        std::make_tuple(geolib3::Point2{37.57637306208963,  55.678867233261784}, "2020-11-11 14:22:03", SRC_NEW),
        std::make_tuple(geolib3::Point2{37.57643207068792,  55.678924824199825}, "2020-11-11 11:22:04", SRC_OLD),
        std::make_tuple(geolib3::Point2{37.576587638810786, 55.679064254518316}, "2020-11-11 11:22:05", SRC_OLD),
        std::make_tuple(geolib3::Point2{37.576877317384394, 55.67932795835126},  "2020-11-21 14:22:06", SRC_NEW),
        std::make_tuple(geolib3::Point2{37.57697119469998,  55.67940525051025},  "2020-11-11 14:22:07", SRC_NEW),
        std::make_tuple(geolib3::Point2{37.577051660970426, 55.679482542515856}, "2020-11-21 14:22:08", SRC_NEW)
    };

    db::Features features;
    int featuresCount = 9;
    for (db::TId i = 0; i < featuresCount; ++i) {
        features.push_back(db::Feature{i}
                            .setGeodeticPos(std::get<0>(FEATURE_PROPS[i]))
                            .setTimestamp(std::get<1>(FEATURE_PROPS[i]))
                            .setSourceId(std::get<2>(FEATURE_PROPS[i]))
                            );
    }

    const std::array EXPECTED_INDICES = {0, 1, 2, 4, 5, 6, 8};
    std::vector<db::TId> expectedIds;
    for (db::TId idx : EXPECTED_INDICES) {
        expectedIds.push_back(features.at(idx).id());
    }

    auto resultFeatures = leaveSameFeaturesByThreshold(features, 22);

    std::vector<db::TId> resultIds;
    for (const auto& feature : resultFeatures) {
        resultIds.push_back(feature.id());
    }

    EXPECT_THAT(resultIds, testing::ElementsAreArray(expectedIds));
}

}

} // namespace maps::mrc::browser::tests
