#include <library/cpp/testing/common/env.h>
#include <maps/libs/common/include/file_utils.h>
#include <maps/libs/geolib/include/contains.h>
#include <maps/libs/geolib/include/units_literals.h>
#include <maps/libs/log8/include/log8.h>
#include <maps/wikimap/mapspro/services/mrc/libs/db/include/feature_gateway.h>
#include <maps/wikimap/mapspro/services/mrc/libs/db/include/walk_object_gateway.h>
#include <maps/wikimap/mapspro/services/mrc/libs/fb/include/features_reader.h>
#include <maps/wikimap/mapspro/services/mrc/libs/fb/include/photo_to_edge_pairs_reader.h>
#include <maps/wikimap/mapspro/services/mrc/libs/fb/include/photo_to_edge_pairs_writer.h>
#include <maps/wikimap/mapspro/services/mrc/libs/fb/include/version.h>
#include <maps/wikimap/mapspro/services/mrc/libs/fb/include/walk_objects_reader.h>
#include <maps/wikimap/mapspro/services/mrc/libs/fb/include/write.h>
#include <maps/wikimap/mapspro/services/mrc/libs/privacy/tests/mocks/region_privacy.h>
#include <maps/wikimap/mapspro/services/mrc/long_tasks/export_gen/lib/convert.h>
#include <maps/wikimap/mapspro/services/mrc/long_tasks/export_gen/lib/exporter.h>
#include <maps/wikimap/mapspro/services/mrc/long_tasks/export_gen/lib/graph.h>
#include <maps/wikimap/mapspro/services/mrc/long_tasks/export_gen/lib/tools.h>
#include <yandex/maps/mrc/unittest/database_fixture.h>
#include <yandex/maps/mrc/unittest/local_server.h>

#include <filesystem>

namespace fs = std::filesystem;
using namespace testing;

namespace maps::mrc::export_gen::tests {
namespace {

class Fixture : public testing::Test,
                public unittest::WithUnittestConfig<unittest::DatabaseFixture>
{
public:
    Fixture()
    {
        auto txn = pool().masterWriteableTransaction();
        txn->exec(maps::common::readFileToString(SRC_("data.sql")));

        // touch feature_transaction
        auto feature = db::FeatureGateway{*txn}.loadOne();
        db::FeatureGateway{*txn}.update(feature, db::UpdateFeatureTxn::Yes);

        txn->commit();
    }
};

bool areEqual(const db::Feature& test, const db::Feature& expect)
{
    if (expect.showAuthorship().value_or(false) &&
        !expect.gdprDeleted().value_or(false)) {
        if (test.userId() != expect.userId()) {
            return false;
        }
    }
    else if (test.userId() != std::nullopt) {
        return false;
    }

    return test.sourceId() == expect.sourceId() &&
           geolib3::test_tools::approximateEqual(
               test.geodeticPos(), expect.geodeticPos(), geolib3::EPS) &&
           test.heading().value() ==
               static_cast<unsigned short>(expect.heading().value()) &&
           test.timestamp() == expect.timestamp() &&
           test.size() == expect.size() && test.mdsKey() == expect.mdsKey() &&
           test.dataset() == expect.dataset() &&
           test.orientation() == expect.orientation() &&
           test.cameraDeviation() ==
               (expect.graph() == db::GraphType::Pedestrian
                    ? db::CameraDeviation::Front
                    : expect.cameraDeviation()) &&
           test.privacy() == expect.privacy() &&
           test.graph() == expect.graph() &&
           test.walkObjectId() == expect.walkObjectId();
}

class IntersectionChecker {
public:
    IntersectionChecker(const db::Features& features) {
        for (const auto& feature : features) {
            featuresMap_.emplace(feature.id(), feature);
        }
    }

    bool operator()(db::TId id, const maps::geolib3::BoundingBox& bbox) {
        return geolib3::contains(bbox, featuresMap_.at(id).geodeticPos());
    }

    bool operator()(const maps::geolib3::BoundingBox& bbox, db::TId id) {
        return geolib3::contains(bbox, featuresMap_.at(id).geodeticPos());
    }

private:
    std::unordered_map<db::TId, db::Feature> featuresMap_;
};

void checkFeaturesIndex(const std::string& exportDir, pgpool3::Pool& pool)
{
    auto reader = fb::FeaturesReader{exportDir};

    // Only published and not hidden features
    EXPECT_EQ(reader.featuresNumber(), 11u);

    // present feature ids
    for (int id : {1,2,3,4,5,6,8,10,12,16,17}) {
        auto expect = db::FeatureGateway(*pool.slaveTransaction()).loadById(id);
        auto test = reader.featureById(id);
        EXPECT_TRUE(test.has_value());
        EXPECT_TRUE(areEqual(test.value(), expect));
    }

    struct {
        unsigned featureId;
        unsigned userId;
    } userIdTests[] = {
        //{9u, 0u}, unpublished
        {10u, 0u},
        //{11u, 0u}, unpublished
        {12u, 84277110u},
        {16u, 0u},
        {17u, 0u},
    };
    for (auto& [featureId, userId] : userIdTests) {
        auto test = reader.featureById(featureId);
        if (userId) {
            EXPECT_EQ(test->userId(), std::to_string(userId));
        }
        else {
            EXPECT_EQ(test->userId(), std::nullopt);
        }
    }

    // missing feature ids: 7,9,11 - unpublished, 13,14,15 - hidden
    for (int id : {7,9,11,13,14,15}) {
        EXPECT_FALSE(reader.featureById(id).has_value());
    }

    EXPECT_EQ(reader.objectsInPhotoByFeatureId(1).size(), 2u);

    EXPECT_EQ(reader.objectsInPhotoByFeatureId(12).size(), 1u);
    EXPECT_EQ(reader.objectsInPhotoByFeatureId(12)[0].type(),
              db::ObjectInPhotoType::LicensePlate);

    EXPECT_TRUE(reader.objectsInPhotoByFeatureId(16).empty());
}

void checkFeaturesSecretIndex(const std::string& exportDir, pgpool3::Pool& pool)
{
    auto reader = fb::FeaturesReader{exportDir};

    // Only published and not hidden features
    EXPECT_EQ(reader.featuresNumber(), 3u);

    // present feature ids
    for (int id : {13, 14, 15}) {
        auto expect = db::FeatureGateway(*pool.slaveTransaction()).loadById(id);
        auto test = reader.featureById(id);
        EXPECT_TRUE(test.has_value());
        EXPECT_TRUE(areEqual(test.value(), expect));
    }
}

void checkWalkObjectsIndex(const std::string& exportDir, pgpool3::Pool& pool)
{
    auto reader = fb::WalkObjectsReader{exportDir};

    EXPECT_EQ(reader.walkObjectsNumber(), 2u);

    auto txn = pool.slaveTransaction();
    for (int id : {16, 17}) {
        auto dbWalkPhoto = db::FeatureGateway(*txn).loadById(id);
        EXPECT_TRUE(dbWalkPhoto.walkObjectId().has_value());
        auto dbWalkObject = db::WalkObjectGateway(*txn).loadById(dbWalkPhoto.walkObjectId().value());

        auto pos = reader.posByWalkObjectId(dbWalkObject.id());
        EXPECT_TRUE(pos.has_value());
        EXPECT_TRUE(geolib3::test_tools::approximateEqual(
            pos.value(),
            std::get<geolib3::Point2>(dbWalkObject.geodeticGeometry()),
            geolib3::EPS));
    }
}

void checkFeaturesPathIndex(const std::string& exportDir, pgpool3::Pool& pool)
{
    auto reader = fb::FeaturesReader{exportDir};

    // Only published and not hidden features
    EXPECT_EQ(reader.featuresNumber(), 11u);

    db::Feature feature = db::FeatureGateway(*pool.slaveTransaction()).loadById(4);

    auto range = reader.photoTimes(feature.sourceId());
    EXPECT_EQ(range.size(), 7u);
    auto it = std::lower_bound(range.begin(),
                               range.end(),
                               fb::TPhotoTime{.time = feature.timestamp()});
    EXPECT_NE(it, range.end());
    EXPECT_EQ(it->featureId, 4);
    EXPECT_NE(it, range.begin());
    EXPECT_EQ(std::prev(it)->featureId, 3);
    EXPECT_NE(std::next(it), range.end());
    EXPECT_EQ(std::next(it)->featureId, 5);
}

void checkFeaturesRtree(const std::string& exportDir, pgpool3::Pool& pool)
{
    auto reader = fb::FeaturesReader{exportDir};

    // Only published and not hidden features
    EXPECT_EQ(reader.featuresNumber(), 11u);

    auto features = db::FeatureGateway(*pool.slaveTransaction()).load();
    IntersectionChecker intersectionChecker(features);

    // Search in bbox of every feature
    for (const auto& feature : features) {
        auto range = reader.rtree().allIdsInWindow(
            feature.geodeticPos().boundingBox(), intersectionChecker);
        if ((feature.isPublished() || feature.shouldBePublished()) &&
            feature.moderatorsShouldBePublished().value_or(true) &&
            !feature.deletedByUser().value_or(false) &&
            feature.privacy() != db::FeaturePrivacy::Secret) {
            EXPECT_EQ(std::distance(range.begin(), range.end()), 1u);
            EXPECT_EQ(db::TId(*range.begin()), feature.id());
        } else {
            EXPECT_TRUE(range.empty());
        }
    }

    // Search in custom bbox
    geolib3::BoundingBox bbox(geolib3::Point2(44.011212, 56.314669),
                              geolib3::Point2(44.021072, 56.310560));
    auto range = reader.rtree().allIdsInWindow(bbox, intersectionChecker);
    EXPECT_EQ(std::distance(range.begin(), range.end()), 7);

    // Search in custom hidden bbox
    geolib3::BoundingBox hiddenBbox(geolib3::Point2(34.760988, 32.094252),
                                    geolib3::Point2(34.791372, 32.050695));
    range = reader.rtree().allIdsInWindow(hiddenBbox, intersectionChecker);
    EXPECT_EQ(range.begin(), range.end());
}

std::string pathJoin(const std::string& dirName, const std::string& fileName)
{
    if (dirName.back() == '/') {
        return dirName + fileName;
    }
    else {
        return dirName + "/" + fileName;
    }
}

void generatePhotos(const road_graph::Graph& graph,
                    db::GraphType graphType,
                    size_t edgeIndex,
                    Origin origin,
                    Photos& result)
{
    static auto featureId = db::TId{};
    static auto timestamp = chrono::parseSqlDateTime("2021-09-22 17:08:55+03");

    auto edgeId = graph.edgeIds()[edgeIndex];
    auto edgeData = graph.edgeData(edgeId);
    auto line = geolib3::Polyline2(edgeData.geometry());
    timestamp += std::chrono::hours{1};
    for (const auto& segment : line.segments()) {
        ++featureId;
        timestamp += std::chrono::seconds{1};
        auto pos = segment.start();
        result.push_back(Photo{
            .geodeticPos = pos,
            .sourceId = "source_id",
            .direction = toDirection(segment),
            .featureId = featureId,
            .timestamp = timestamp,
            .cameraDeviation = db::CameraDeviation::Front,
            .dataset = db::Dataset::Agents,
            .dayPart = common::getDayPart(timestamp, pos.y(), pos.x()),
            .graph = graphType,
            .origin = origin,
            .privacy = db::FeaturePrivacy::Public,
        });
    }
}

struct EqualTo {
    bool operator()(const fb::CoveredSubPolyline& lhs,
                    const fb::CoveredSubPolyline& rhs) const
    {
        return lhs.featureId() == rhs.featureId() &&
               lhs.begin() == rhs.begin() && lhs.end() == rhs.end();
    }

    bool operator()(const fb::TEdgeCoverage& lhs,
                    const fb::TEdgeCoverage& rhs) const
    {
        return lhs.coverageFraction == rhs.coverageFraction &&
               lhs.actualizationDate == rhs.actualizationDate &&
               lhs.cameraDeviation == rhs.cameraDeviation &&
               lhs.privacy == rhs.privacy &&
               std::is_permutation(lhs.coveredSubpolylines.begin(),
                                   lhs.coveredSubpolylines.end(),
                                   rhs.coveredSubpolylines.begin(),
                                   rhs.coveredSubpolylines.end(),
                                   *this);
    }

    bool operator()(const fb::TEdge& lhs, const fb::TEdge& rhs) const
    {
        return lhs.id == rhs.id && std::is_permutation(lhs.coverages.begin(),
                                                       lhs.coverages.end(),
                                                       rhs.coverages.begin(),
                                                       rhs.coverages.end(),
                                                       *this);
    }

    bool operator()(const fb::TGraph& lhs, const fb::TGraph& rhs) const
    {
        return std::is_permutation(lhs.edges.begin(),
                                   lhs.edges.end(),
                                   rhs.edges.begin(),
                                   rhs.edges.end(),
                                   *this);
    }
};

db::TId featureId(const Photo& photo)
{
    return photo.featureId;
}

db::TId featureId(const fb::PhotoToEdgePair& photoToEdgePair)
{
    return photoToEdgePair.featureId();
}

db::TId featureId(const fb::CoveredSubPolyline& coveredSubPolyline)
{
    return coveredSubPolyline.featureId();
}

auto equalFeatureId = [](const auto& lhs, const auto& rhs) {
    return featureId(lhs) == featureId(rhs);
};

road_graph::EdgeId edgeId(road_graph::EdgeId id)
{
    return id;
}

road_graph::EdgeId edgeId(const fb::PhotoToEdgePair& photoToEdgePair)
{
    return road_graph::EdgeId{photoToEdgePair.edgeId()};
}

road_graph::EdgeId edgeId(const fb::TEdge& edge)
{
    return road_graph::EdgeId{edge.id};
}

auto equalEdgeId = [](const auto& lhs, const auto& rhs) {
    return edgeId(lhs) == edgeId(rhs);
};

} // anonymous namespace


TEST_F(Fixture, test_export_generator)
{
    const std::string mrcExportDir = pathJoin(GetWorkPath(), "output_fb_files");
    fs::create_directories(mrcExportDir);

    const std::string featuresSecretExportDir = pathJoin(GetWorkPath(), "features_secret_fb_files");
    fs::create_directories(featuresSecretExportDir);

    const std::string VERSION = "test-version";
    generateExport(pool(), mrcExportDir, featuresSecretExportDir, VERSION);

    checkFeaturesIndex(mrcExportDir, pool());
    checkWalkObjectsIndex(mrcExportDir, pool());
    checkFeaturesPathIndex(mrcExportDir, pool());
    checkFeaturesRtree(mrcExportDir, pool());
    checkFeaturesSecretIndex(featuresSecretExportDir, pool());
}

TEST_F(Fixture, generate_graph)
{
    auto graphPath = BinaryPath("maps/data/test/graph3");
    auto graphType = db::GraphType::Road;
    auto graph = road_graph::Graph(graphPath + "/road_graph.fb");
    auto photos = Photos{};
    generatePhotos(graph, graphType, 1u, Origin::Fb, photos);
    EXPECT_GT(photos.size(), 1u);
    photos.front().privacy = db::FeaturePrivacy::Secret;

    auto destGraph = pathJoin(GetWorkPath(), "datasetGraph");
    auto destGraphPro = pathJoin(GetWorkPath(), "datasetGraphPro");
    auto destPhotoToEdge = pathJoin(GetWorkPath(), "datasetPhotoToEdge");
    auto destPhotoToEdgePro = pathJoin(GetWorkPath(), "datasetPhotoToEdgePro");
    for (const auto& path :
         {destGraph, destGraphPro, destPhotoToEdge, destPhotoToEdgePro}) {
        fs::create_directories(path);
    }
    generateGraph(GraphDescriptor{.graphDir = graphPath,
                                  .graphType = db::GraphType::Road},
                  pool(),
                  photos,
                  destGraph,
                  destGraphPro,
                  destPhotoToEdge,
                  destPhotoToEdgePro,
                  "version",
                  "previous version");

    auto reader =
        fb::PhotoToEdgePairsReader{destPhotoToEdge + "/" + PHOTO_TO_EDGE_FILE};
    EXPECT_GT(reader.photoToEdgePairNumber(), 0u);
    for (const auto& photo : photos) {
        auto result = reader.lookupByFeatureId(photo.featureId);
        EXPECT_EQ(result.empty(), photo.privacy == db::FeaturePrivacy::Secret);
    }
}

TEST_F(Fixture, test_fb_loader)
{
    // initial export
    auto dbLoaderDefaultDir = GetWorkPath() + "/db_loader_default";
    auto dbLoaderSecretDir = GetWorkPath() + "/db_loader_secret";
    auto txnId = getLastFeatureTxnId(pool());
    generateExport(DbLoader{pool()},
                   dbLoaderDefaultDir,
                   dbLoaderSecretDir,
                   fb::makeVersion(maps::chrono::TimePoint::clock::now()),
                   txnId);
    checkFeaturesIndex(dbLoaderDefaultDir, pool());

    // first incremental export: without diff
    auto fbLoaderDefaultDir1 = GetWorkPath() + "/fb_loader_default_1";
    auto fbLoaderSecretDir1 = GetWorkPath() + "/fb_loader_secret_1";
    auto txnId1 = getLastFeatureTxnId(pool());
    generateExport(FbLoader{pool(),
                            dbLoaderDefaultDir,
                            dbLoaderSecretDir,
                            txnId,
                            EMappingMode::Standard},
                   fbLoaderDefaultDir1,
                   fbLoaderSecretDir1,
                   fb::makeVersion(maps::chrono::TimePoint::clock::now()),
                   txnId1);
    checkFeaturesIndex(fbLoaderDefaultDir1, pool());

    // new features
    auto newFeatures = db::Features{
        db::Feature{"src1",
                    geolib3::Point2{43.9990, 56.3220},
                    geolib3::Heading{360.0},
                    "2016-04-01 05:57:09+03",
                    mds::Key{"4510", "1460732825/1010624788.jpg"},
                    db::Dataset::Agents}
            .setPrivacy(db::FeaturePrivacy::Public),
        db::Feature{"src2",
                    geolib3::Point2{43.9992, 56.3218},
                    geolib3::Heading{111.71},
                    "2016-04-02 05:57:11+03",
                    mds::Key{"4510", "1460732825/1545540516.jpg"},
                    db::Dataset::Agents}
            .setPrivacy(db::FeaturePrivacy::Secret),
    };
    for (auto& feature : newFeatures) {
        feature.setSize({6, 9})
            .setAutomaticShouldBePublished(true)
            .setIsPublished(true);
    }
    {
        auto txn = pool().masterWriteableTransaction();
        db::FeatureGateway{*txn}.insert(newFeatures);
        db::FeatureGateway{*txn}.update(newFeatures, db::UpdateFeatureTxn::Yes);
        txn->commit();
    }

    // unpublish old feature
    auto oldFeatures = db::Features{};
    {
        auto txn = pool().masterWriteableTransaction();
        auto oldFeatures = db::FeatureGateway{*txn}.loadByIds({1});
        for (auto& feature : oldFeatures) {
            feature.setAutomaticShouldBePublished(false);
        }
        db::FeatureGateway{*txn}.update(oldFeatures, db::UpdateFeatureTxn::Yes);
        txn->commit();
    }

    // second incremental export: with diff
    auto fbLoaderDefaultDir2 = GetWorkPath() + "/fb_loader_default_2";
    auto fbLoaderSecretDir2 = GetWorkPath() + "/fb_loader_secret_2";
    generateExport(FbLoader{pool(),
                            dbLoaderDefaultDir,
                            dbLoaderSecretDir,
                            txnId1,
                            EMappingMode::Standard},
                   fbLoaderDefaultDir2,
                   fbLoaderSecretDir2,
                   fb::makeVersion(maps::chrono::TimePoint::clock::now()),
                   getLastFeatureTxnId(pool()));

    // check diff
    auto defaultReader = fb::FeaturesReader{fbLoaderDefaultDir2};
    auto secretReader = fb::FeaturesReader{fbLoaderSecretDir2};
    for (const auto& expect : newFeatures) {
        auto test = expect.privacy() == db::FeaturePrivacy::Secret
                        ? secretReader.featureById(expect.id())
                        : defaultReader.featureById(expect.id());
        EXPECT_TRUE(test.has_value());
        EXPECT_TRUE(areEqual(test.value(), expect));
    }
    for (const auto& feature : oldFeatures) {
        EXPECT_FALSE(defaultReader.featureById(feature.id()).has_value());
        EXPECT_FALSE(secretReader.featureById(feature.id()).has_value());
    }
}

TEST_F(Fixture, test_feature_schema_version)
{
    // old schema export
    auto oldSchemaDefaultDir = GetWorkPath() + "/old_schema_default";
    auto oldSchemaSecretDir = GetWorkPath() + "/old_schema_secret";
    auto txnId = getLastFeatureTxnId(pool());
    generateExport(DbLoader{pool()},
                   oldSchemaDefaultDir,
                   oldSchemaSecretDir,
                   fb::makeVersion(maps::chrono::TimePoint::clock::now()),
                   txnId,
                   CURRENT_FEATURE_SCHEMA_VERSION - 1);
    checkFeaturesIndex(oldSchemaDefaultDir, pool());

    // cannot be read from dataset
    EXPECT_THROW(FbLoader(pool(),
                          oldSchemaDefaultDir,
                          oldSchemaSecretDir,
                          txnId,
                          EMappingMode::Standard),
                 maps::Exception);

    // new export will take place anyway
    auto newSchemaDefaultDir = GetWorkPath() + "/new_schema_default";
    auto newSchemaSecretDir = GetWorkPath() + "/new_schema_secret";
    generateExport(*makeLoader(pool(), oldSchemaDefaultDir, oldSchemaSecretDir, txnId),
                   newSchemaDefaultDir,
                   newSchemaSecretDir,
                   fb::makeVersion(maps::chrono::TimePoint::clock::now()),
                   getLastFeatureTxnId(pool()));
    checkFeaturesIndex(newSchemaDefaultDir, pool());
}

TEST(tests, test_using_dataset_in_graph_coverage)
{
    auto testGraphDir = BinaryPath("maps/data/test/graph3");
    auto prevGraphDir = std::string{GetWorkPath() + "/prev_graph"};
    auto prevCoverageFile = prevGraphDir + "/" + GRAPH_COVERAGE_FILE;
    auto prevPhotoToEdgeFile = prevGraphDir + "/" + PHOTO_TO_EDGE_FILE;
    auto prevPersistentIndexFile = testGraphDir + "/" + EDGES_PERSISTENT_INDEX_FILE;
    auto graphType = db::GraphType::Road;
    auto ctx = Context{testGraphDir, graphType};
    auto photos = Photos{};
    generatePhotos(ctx.matcher.graph(), graphType, 0 /*edge*/, Origin::Fb, photos);
    fs::create_directories(prevGraphDir);
    {
        auto [graph, photoToEdgePairs] =
            makeGraphSummary(ctx, photos, makeTrackPointProviderStub());
        fb::writeToFile(graph, prevCoverageFile);
        fb::writePhotoToEdgePairsToFile(
            graph.version,
            {} /*mrcVersion*/,
            CURRENT_PHOTO_TO_EDGE_SCHEMA_VERSION,
            photoToEdgePairs,
            prevPhotoToEdgeFile);
    }

    auto areEqual = [&] {
        ctx.prevGraph.reset();
        ctx.prevPersistentIndex.reset();
        ctx.prevPhotoToEdge.reset();
        auto withoutDatasets =
            makeGraphSummary(ctx, photos, makeTrackPointProviderStub());
        EXPECT_FALSE(withoutDatasets.graph.edges.empty());

        ctx.prevGraph.emplace(prevCoverageFile);
        ctx.prevPersistentIndex.emplace(prevPersistentIndexFile);
        ctx.prevPhotoToEdge.emplace(prevPhotoToEdgeFile);
        auto withDatasets =
            makeGraphSummary(ctx, photos, makeTrackPointProviderStub());
        EXPECT_FALSE(withDatasets.graph.edges.empty());

        ctx.prevGraph.reset();
        ctx.prevPersistentIndex.emplace(prevPersistentIndexFile);
        ctx.prevPhotoToEdge.emplace(prevPhotoToEdgeFile);
        auto withoutCoverage =
            makeGraphSummary(ctx, photos, makeTrackPointProviderStub());
        EXPECT_FALSE(withoutCoverage.graph.edges.empty());

        auto equalTo = EqualTo{};
        return equalTo(withoutDatasets.graph, withDatasets.graph) &&
               equalTo(withDatasets.graph, withoutCoverage.graph);
    };

    EXPECT_TRUE(areEqual());
    generatePhotos(ctx.matcher.graph(), graphType, 1 /*edge*/, Origin::Db, photos);
    EXPECT_TRUE(areEqual());
    photos.erase(photos.begin());
    EXPECT_TRUE(areEqual());
}

TEST(tests, test_standalone_photos_dataset)
{
    auto testGraphDir = BinaryPath("maps/data/test/graph3");
    auto graphType = db::GraphType::Road;
    auto ctx = Context{testGraphDir, graphType};
    auto photos = Photos{};
    generatePhotos(ctx.matcher.graph(), graphType, 0 /*edge*/, Origin::Fb, photos);
    for (auto& photo : photos) {
        photo.dataset = db::Dataset::Rides;
        EXPECT_FALSE(isStandalonePhotosDataset(photo.dataset));
    }
    EXPECT_FALSE(makeGraphSummary(ctx, photos, makeTrackPointProviderStub())
                     .graph.edges.empty());
    for (auto& photo : photos) {
        photo.dataset = db::Dataset::Walks;
        EXPECT_TRUE(isStandalonePhotosDataset(photo.dataset));
    }
    EXPECT_TRUE(makeGraphSummary(ctx, photos, makeTrackPointProviderStub())
                    .graph.edges.empty());
}

TEST(make_graph_summary_tests, basic)
{
    auto testGraphDir = BinaryPath("maps/data/test/graph3");
    auto graphType = db::GraphType::Road;
    auto ctx = Context{testGraphDir, graphType};
    auto edgeId = ctx.matcher.graph().edgeIds()[1];
    auto polyline =
        geolib3::Polyline2(ctx.matcher.graph().edgeData(edgeId).geometry());
    EXPECT_GT(polyline.segmentsNumber(), 1u);

    auto makePhoto = [&](const geolib3::Point2& pos,
                         geolib3::Direction2 direction,
                         chrono::TimePoint timestamp) mutable {
        static auto featureId = db::TId{};
        return Photo{
            .geodeticPos = pos,
            .sourceId = "source_id",
            .direction = direction,
            .featureId = ++featureId,
            .timestamp = timestamp,
            .dataset = db::Dataset::Agents,
            .dayPart = common::getDayPart(timestamp, pos.y(), pos.x()),
            .graph = graphType,
        };
    };

    auto photos = Photos{};
    auto timestamp = chrono::parseSqlDateTime("2021-11-16 12:05:30+03");
    for (const auto& segment : polyline.segments()) {
        auto direction = toDirection(segment);
        for (int i = 0; i < 2; ++i) {
            photos.push_back(
                makePhoto(fastGeoShift(segment.start(), i * direction.vector()),
                          direction,
                          timestamp + photos.size() * std::chrono::seconds{1}));
        }
    }

    auto [graph, photoToEdgePairs] =
        makeGraphSummary(ctx, photos, makeTrackPointProviderStub());
    EXPECT_TRUE(std::is_permutation(photos.begin(),
                                    photos.end(),
                                    photoToEdgePairs.begin(),
                                    photoToEdgePairs.end(),
                                    equalFeatureId));
    EXPECT_TRUE(std::all_of(photoToEdgePairs.begin(),
                            photoToEdgePairs.end(),
                            [&](const fb::PhotoToEdgePair& photoToEdgePair) {
                                return photoToEdgePair.edgeId() ==
                                       edgeId.value();
                            }));

    EXPECT_EQ(graph.edges.size(), 1u);
    auto& edge = graph.edges.front();
    EXPECT_EQ(edge.id, edgeId.value());
    auto& coverages = edge.coverages;
    EXPECT_EQ(coverages.size(), 1u);
    auto& coverage = coverages.front();
    EXPECT_EQ(coverage.actualizationDate, timestamp);
    EXPECT_GT(coverage.coverageFraction, 0.f);
    auto& coveredSubpolylines = coverage.coveredSubpolylines;
    EXPECT_TRUE(std::is_permutation(photos.begin(),
                                    photos.end(),
                                    coveredSubpolylines.begin(),
                                    coveredSubpolylines.end(),
                                    equalFeatureId));
    EXPECT_EQ(std::adjacent_find(coveredSubpolylines.begin(),
                                 coveredSubpolylines.end(),
                                 [](const fb::CoveredSubPolyline& lhs,
                                    const fb::CoveredSubPolyline& rhs) {
                                     return lhs.end() > rhs.begin();
                                 }),
              coveredSubpolylines.end());
}

TEST(make_graph_summary_tests, should_be_minimal)
{
    auto testGraphDir = BinaryPath("maps/data/test/graph3");
    auto graphType = db::GraphType::Road;
    auto ctx = Context{testGraphDir, graphType};
    auto edgeId = ctx.matcher.graph().edgeIds()[1];
    auto polyline =
        geolib3::Polyline2(ctx.matcher.graph().edgeData(edgeId).geometry());

    auto makePhoto = [&](const geolib3::Point2& pos,
                         geolib3::Direction2 direction,
                         chrono::TimePoint timestamp) mutable {
        static auto featureId = db::TId{};
        return Photo{
            .geodeticPos = pos,
            .sourceId = "source_id",
            .direction = direction,
            .featureId = ++featureId,
            .timestamp = timestamp,
            .dataset = db::Dataset::Agents,
            .dayPart = common::getDayPart(timestamp, pos.y(), pos.x()),
            .graph = graphType,
        };
    };

    auto timestamp1 = chrono::parseSqlDateTime("2021-11-16 12:05:30+03");
    auto timestamp2 = timestamp1 + std::chrono::hours{1};
    const auto& segment = polyline.segmentAt(0);
    auto photos =
        Photos{makePhoto(segment.start(), toDirection(segment), timestamp1),
               makePhoto(segment.start(), toDirection(segment), timestamp2)};
    auto [graph, _] =
        makeGraphSummary(ctx, photos, makeTrackPointProviderStub());

    EXPECT_EQ(graph.edges.size(), 1u);
    auto& edge = graph.edges.front();
    auto& coverages = edge.coverages;
    EXPECT_EQ(coverages.size(), 1u);
    auto& coverage = coverages.front();
    EXPECT_EQ(coverage.actualizationDate, photos.back().timestamp);
    auto& coveredSubpolylines = coverage.coveredSubpolylines;
    EXPECT_EQ(coveredSubpolylines.size(), 1u);
    auto& coveredSubpolyline = coveredSubpolylines.front();
    EXPECT_TRUE(equalFeatureId(coveredSubpolyline, photos.back()));
}

TEST(make_graph_summary_tests, old_photo_fills_gap_between_new_ones)
{
    auto testGraphDir = BinaryPath("maps/data/test/graph3");
    auto graphType = db::GraphType::Road;
    auto ctx = Context{testGraphDir, graphType};
    auto edgeId = ctx.matcher.graph().edgeIds()[1];
    auto polyline =
        geolib3::Polyline2(ctx.matcher.graph().edgeData(edgeId).geometry());

    auto makePhoto = [&](const geolib3::Point2& pos,
                         geolib3::Direction2 direction,
                         chrono::TimePoint timestamp) mutable {
        static auto featureId = db::TId{};
        return Photo{
            .geodeticPos = pos,
            .sourceId = "source_id",
            .direction = direction,
            .featureId = ++featureId,
            .timestamp = timestamp,
            .dataset = db::Dataset::Agents,
            .dayPart = common::getDayPart(timestamp, pos.y(), pos.x()),
            .graph = graphType,
        };
    };

    auto timestampOld = chrono::parseSqlDateTime("2021-11-19 15:21:30+03");
    auto timestampMid = timestampOld + std::chrono::hours{1};
    auto timestampNew = timestampMid + std::chrono::hours{1};
    EXPECT_GT(polyline.segmentsNumber(), 1u);
    const auto& segment1 = polyline.segmentAt(0);
    const auto& segment2 = polyline.segmentAt(1);
    auto photos = Photos{
        makePhoto(segment1.midpoint(), toDirection(segment1), timestampOld),
        makePhoto(segment1.start(), toDirection(segment1), timestampMid),
        makePhoto(segment2.start(), toDirection(segment2), timestampNew),
    };
    auto [graph, _] =
        makeGraphSummary(ctx, photos, makeTrackPointProviderStub());

    EXPECT_EQ(graph.edges.size(), 1u);
    auto& edge = graph.edges.front();
    auto& coverages = edge.coverages;
    EXPECT_EQ(coverages.size(), 1u);
    auto& coverage = coverages.front();
    auto& coveredSubpolylines = coverage.coveredSubpolylines;
    EXPECT_EQ(coveredSubpolylines.size(), 3u);

    // there is a gap between new photos
    EXPECT_LT(coveredSubpolylines.at(0).end(),
              coveredSubpolylines.at(2).begin());

    // old photo completely fills the gap
    EXPECT_EQ(coveredSubpolylines.at(0).end(),
              coveredSubpolylines.at(1).begin());
    EXPECT_EQ(coveredSubpolylines.at(1).end(),
              coveredSubpolylines.at(2).begin());
}

TEST(make_graph_summary_tests, trackpoints_at_crossroad)
{
    auto testGraphDir = BinaryPath("maps/data/test/graph3");
    auto graphType = db::GraphType::Road;
    auto ctx = Context{testGraphDir, graphType};
    auto pivotEdgeId = [&] {
        auto range = ctx.matcher.rtree().nearestBaseEdges(
            geolib3::Point2{37.605539, 55.731835});
        EXPECT_FALSE(range.empty());
        return *range.begin();
    }();
    auto outEdgeIds = [&] {
        auto result = std::set<road_graph::EdgeId>{};
        auto range = ctx.matcher.graph().outEdgeIds(
            ctx.matcher.graph().edge(pivotEdgeId).target);
        for (auto outEdgeId : range) {
            result.insert(ctx.matcher.graph().base(outEdgeId));
        }
        EXPECT_GE(result.size(), 2u);
        return result;
    }();
    auto photo = Photo{};
    auto trackPoints = db::TrackPoints{};
    auto expectedEdgeIds = std::vector<road_graph::EdgeId>{};

    // edge with photo before crossroad
    expectedEdgeIds.push_back(pivotEdgeId);
    auto polyline = geolib3::Polyline2(
        ctx.matcher.graph().edgeData(pivotEdgeId).geometry());
    auto segment = polyline.segmentAt(polyline.segmentsNumber() - 1);
    auto direction = toDirection(segment);
    auto pos = fastGeoShift(segment.end(),
                            4 * MIN_COVERAGE_METERS * (-direction).vector());
    auto timestamp = chrono::parseSqlDateTime("2021-11-23 15:51:30+03");
    photo = Photo{
        .geodeticPos = pos,
        .sourceId = "source_id",
        .direction = direction,
        .featureId = 1,
        .timestamp = timestamp,
        .dataset = db::Dataset::Agents,
        .dayPart = common::getDayPart(timestamp, pos.y(), pos.x()),
        .graph = graphType,
    };
    trackPoints.emplace_back()
        .setSourceId(std::string{photo.sourceId})
        .setGeodeticPos(photo.geodeticPos)
        .setHeading(photo.direction.heading())
        .setTimestamp(photo.timestamp);

    // one of out edges after crossroad
    expectedEdgeIds.push_back(*outEdgeIds.begin());
    polyline = geolib3::Polyline2(
        ctx.matcher.graph().edgeData(*outEdgeIds.begin()).geometry());
    segment = polyline.segmentAt(0);
    direction = toDirection(segment);
    trackPoints.emplace_back()
        .setSourceId(std::string{photo.sourceId})
        .setGeodeticPos(fastGeoShift(
            segment.start(), 4 * MIN_COVERAGE_METERS * direction.vector()))
        .setHeading(direction.heading())
        .setTimestamp(photo.timestamp + std::chrono::seconds(1));

    auto [graph, photoToEdgePairs] =
        makeGraphSummary(ctx, {photo}, [&](auto&&...) { return trackPoints; });

    EXPECT_TRUE(std::is_permutation(expectedEdgeIds.begin(),
                                    expectedEdgeIds.end(),
                                    graph.edges.begin(),
                                    graph.edges.end(),
                                    equalEdgeId));
    EXPECT_TRUE(std::is_permutation(expectedEdgeIds.begin(),
                                    expectedEdgeIds.end(),
                                    photoToEdgePairs.begin(),
                                    photoToEdgePairs.end(),
                                    equalEdgeId));
}

TEST(make_graph_summary_tests, thresholds)
{
    using namespace std::chrono_literals;
    using namespace maps::geolib3::literals;

    auto testGraphDir = BinaryPath("maps/data/test/graph3");
    auto graphType = db::GraphType::Road;
    auto ctx = Context{testGraphDir, graphType};
    auto pivotEdgeId = ctx.matcher.graph().edgeIds()[1];
    auto polyline = geolib3::Polyline2(
        ctx.matcher.graph().edgeData(pivotEdgeId).geometry());
    const auto& segment = polyline.segmentAt(0);
    auto direction = toDirection(segment);
    auto sourceId = std::string{"source_id"};
    auto timestamp = chrono::parseSqlDateTime("2021-11-24 16:18:30+03");

    auto makeTrackPoint = [&](const geolib3::Point2& pos,
                              geolib3::Direction2 dir,
                              chrono::TimePoint time) {
        return db::TrackPoint{}
            .setSourceId(sourceId)
            .setGeodeticPos(pos)
            .setHeading(dir.heading())
            .setTimestamp(time);
    };

    auto makePhoto = [&](const geolib3::Point2& pos,
                         geolib3::Direction2 dir,
                         chrono::TimePoint time) mutable {
        static auto featureId = db::TId{};
        return Photo{
            .geodeticPos = pos,
            .sourceId = sourceId,
            .direction = dir,
            .featureId = ++featureId,
            .timestamp = time,
            .dataset = db::Dataset::Agents,
            .dayPart = common::getDayPart(timestamp, pos.y(), pos.x()),
            .graph = graphType,
        };
    };

    auto trackPoints = db::TrackPoints{
        makeTrackPoint(segment.start(), direction, timestamp + 1s),
        makeTrackPoint(segment.midpoint(), direction, timestamp + 2s),
        makeTrackPoint(segment.end(), direction, timestamp + 3s),
    };

    auto photos = Photos{
        // normal
        makePhoto(segment.start(), direction, timestamp + 1s),

        // too rotated
        makePhoto(segment.midpoint(),
                  direction +
                      geolib3::Direction2(COVERAGE_ANGLE_DIFF_THRESHOLD) +
                      geolib3::Direction2(1_deg),
                  timestamp + 2s),

        // too far
        makePhoto(fastGeoShift(segment.end(),
                               (COVERAGE_METERS_SNAP_THRESHOLD + 1) *
                                   direction.vector()),
                  direction,
                  timestamp + 3s),

    };

    auto graphSummary =
        makeGraphSummary(ctx, photos, [&](auto&&...) { return trackPoints; });
    EXPECT_EQ(graphSummary.photoToEdgePairs.size(), photos.size());

    auto findPhotoToEdge = [&](const Photo& photo) {
        return std::find_if(graphSummary.photoToEdgePairs.begin(),
                            graphSummary.photoToEdgePairs.end(),
                            [&](const auto& photoToEdgePair) {
                                return equalFeatureId(photo, photoToEdgePair);
                            });
    };

    // normal
    auto it = findPhotoToEdge(photos.at(0));
    EXPECT_NE(it, graphSummary.photoToEdgePairs.end());
    EXPECT_TRUE(equalEdgeId(*it, pivotEdgeId));
    EXPECT_NE(it->begin(), it->end());  // not empty

    // too rotated
    it = findPhotoToEdge(photos.at(1));
    EXPECT_NE(it, graphSummary.photoToEdgePairs.end());
    EXPECT_TRUE(equalEdgeId(*it, pivotEdgeId));
    EXPECT_EQ(it->begin(), it->end());  // empty

    // too far
    it = findPhotoToEdge(photos.at(2));
    EXPECT_TRUE(equalEdgeId(*it, pivotEdgeId));
    EXPECT_EQ(it->begin(), it->end());  // empty
}

TEST(make_graph_summary_tests, avoid_nexar)
{
    auto testGraphDir = BinaryPath("maps/data/test/graph3");
    auto graphType = db::GraphType::Road;
    auto ctx = Context{testGraphDir, graphType};
    auto edgeId = ctx.matcher.graph().edgeIds()[1];
    auto polyline =
        geolib3::Polyline2(ctx.matcher.graph().edgeData(edgeId).geometry());

    auto makePhoto = [&](const geolib3::Point2& pos,
                         geolib3::Direction2 direction,
                         chrono::TimePoint timestamp,
                         db::Dataset dataset,
                         common::DayPart dayPart) mutable {
        static auto featureId = db::TId{};
        return Photo{
            .geodeticPos = pos,
            .sourceId = "source_id",
            .direction = direction,
            .featureId = ++featureId,
            .timestamp = timestamp,
            .dataset = dataset,
            .dayPart = dayPart,
            .graph = graphType,
        };
    };

    auto timestampOld = chrono::parseSqlDateTime("2021-12-23 09:51:30+03");
    auto timestampNew = timestampOld + std::chrono::hours{1};
    const auto& segment = polyline.segmentAt(0);
    auto photos = Photos{
        makePhoto(segment.start(),
                  toDirection(segment),
                  timestampOld,
                  db::Dataset::Agents,
                  common::DayPart::Day),
        makePhoto(segment.start(),
                  toDirection(segment),
                  timestampNew,
                  db::Dataset::NexarDashcams,
                  common::DayPart::Day),
        makePhoto(segment.start(),
                  toDirection(segment),
                  timestampNew,
                  db::Dataset::Agents,
                  common::DayPart::Night),
    };
    auto [graph, _] =
        makeGraphSummary(ctx, photos, makeTrackPointProviderStub());

    EXPECT_EQ(graph.edges.size(), 1u);
    auto& edge = graph.edges.front();
    auto& coverages = edge.coverages;
    EXPECT_EQ(coverages.size(), 1u);
    auto& coverage = coverages.front();
    EXPECT_EQ(coverage.actualizationDate, timestampOld);
    auto& coveredSubpolylines = coverage.coveredSubpolylines;
    EXPECT_EQ(coveredSubpolylines.size(), 1u);
    auto& coveredSubpolyline = coveredSubpolylines.front();
    auto& photo = photos.front();
    EXPECT_NE(photo.dataset, db::Dataset::NexarDashcams);
    EXPECT_NE(photo.dayPart, common::DayPart::Night);
    EXPECT_TRUE(equalFeatureId(coveredSubpolyline, photo));
}

TEST_F(Fixture, test_make_loader)
{
    auto defaultDir = GetWorkPath() + "/default";
    auto secretDir = GetWorkPath() + "/secret";
    auto txnId = getLastFeatureTxnId(pool());
    generateExport(DbLoader{pool()},
                   defaultDir,
                   secretDir,
                   fb::makeVersion(maps::chrono::TimePoint::clock::now()),
                   txnId,
                   CURRENT_FEATURE_SCHEMA_VERSION);
    checkFeaturesIndex(defaultDir, pool());

    auto loader = makeLoader(pool(), defaultDir, secretDir, db::TId{});
    EXPECT_EQ(loader->fbVersion(), std::string_view{});  // DbLoader

    loader = makeLoader(pool(), defaultDir, secretDir, txnId);
    EXPECT_NE(loader->fbVersion(), std::string_view{});  // FbLoader
}

} // maps::mrc::export_gen::tests
