#include <library/cpp/testing/common/env.h>
#include <library/cpp/testing/gtest/gtest.h>
#include <library/cpp/testing/unittest/env.h>
#include <maps/libs/road_graph/include/graph.h>
#include <maps/libs/succinct_rtree/include/rtree.h>
#include <maps/wikimap/mapspro/services/mrc/libs/db/include/eye/feature_gateway.h>
#include <maps/wikimap/mapspro/services/mrc/libs/db/include/eye/frame_gateway.h>
#include <maps/wikimap/mapspro/services/mrc/libs/db/include/eye/hypothesis_gateway.h>
#include <maps/wikimap/mapspro/services/mrc/libs/db/include/eye/object_gateway.h>
#include <maps/wikimap/mapspro/services/mrc/libs/db/include/eye/recognition_gateway.h>
#include <maps/wikimap/mapspro/services/mrc/libs/db/include/feature_gateway.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/write.h>
#include <maps/wikimap/mapspro/services/mrc/long_tasks/excess_features_eraser/lib/coverage.h>
#include <maps/wikimap/mapspro/services/mrc/long_tasks/excess_features_eraser/lib/db.h>
#include <maps/wikimap/mapspro/services/mrc/long_tasks/excess_features_eraser/lib/utility.h>
#include <maps/wikimap/mapspro/services/mrc/long_tasks/export_gen/lib/exporter.h>
#include <yandex/maps/mrc/unittest/database_fixture.h>

#include <span>

namespace maps::mrc::excess_features_eraser::tests {

using namespace testing;

const std::string TEST_GRAPH_PATH = BinaryPath("maps/data/test/graph4");

struct Fixture : testing::Test,
                 unittest::WithUnittestConfig<unittest::DatabaseFixture> {
};

void insertHypothesis(pqxx::transaction_base& txn,
                      std::span<const db::Feature> features)
{
    ASSERT(!features.empty());
    auto device = db::eye::Device{db::eye::MrcDeviceAttrs{"M1"}};
    db::eye::DeviceGateway(txn).insertx(device);
    auto detections = db::eye::Detections{};
    for (const auto& feature : features) {
        auto frame = db::eye::Frame{
            device.id(),
            feature.orientation(),
            db::eye::MrcUrlContext{.featureId = feature.id(),
                                   .mdsGroupId = feature.mdsGroupId(),
                                   .mdsPath = feature.mdsPath()},
            feature.size(),
            feature.timestamp()};
        db::eye::FrameGateway(txn).insertx(frame);
        db::eye::FeatureToFrameGateway(txn).insert(
            db::eye::FeatureToFrame{feature.id(), frame.id()});
        auto group = db::eye::DetectionGroup{
            frame.id(), db::eye::DetectionType::HouseNumber};
        db::eye::DetectionGroupGateway(txn).insertx(group);
        detections.emplace_back(
            group.id(),
            db::eye::DetectedHouseNumbers({db::eye::DetectedHouseNumber{
                common::ImageBox(100, 100, 200, 200), 0.98, "22"}}));
    }
    db::eye::DetectionGateway(txn).insertx(detections);
    auto primaryDetectionId = detections.front().id();
    for (const auto& detection : detections) {
        if (primaryDetectionId != detection.id()) {
            auto primaryDetectionRelation = db::eye::PrimaryDetectionRelation(
                primaryDetectionId, detection.id());
            db::eye::PrimaryDetectionRelationGateway{txn}.insertx(
                primaryDetectionRelation);
        }
    }
    auto object =
        db::eye::Object{primaryDetectionId, db::eye::HouseNumberAttrs{"22"}};
    db::eye::ObjectGateway(txn).insertx(object);
    auto hypothesis = db::eye::Hypothesis{
        geolib3::Point2{200, 300},
        db::eye::AbsentHouseNumberAttrs{"22"},
    };
    db::eye::HypothesisGateway(txn).insertx(hypothesis);
    db::eye::HypothesisObjectGateway(txn).insertx(
        db::eye::HypothesisObject{hypothesis.id(), object.id()});
    static auto feedbackTaskId = db::TId{};
    auto hypothesisFeedback =
        db::eye::HypothesisFeedback{hypothesis.id(), ++feedbackTaskId};
    db::eye::HypothesisFeedbackGateway(txn).insertx(hypothesisFeedback);
}

geolib3::Polyline2 edgeInWindow(const geolib3::BoundingBox& geodeticBox)
{
    static const auto graph = road_graph::Graph(
        concat(TEST_GRAPH_PATH, "/", export_gen::ROAD_GRAPH_FILE));
    static const auto rtree = succinct_rtree::Rtree(
        concat(TEST_GRAPH_PATH, "/", export_gen::RTREE_FILE), graph);
    auto result = geolib3::Polyline2{};
    auto minFc = std::numeric_limits<db::TFc>::max();
    auto edgeIds = rtree.baseEdgesInWindow(geodeticBox);
    for (const auto& edgeId : edgeIds) {
        auto edgeData = graph.edgeData(edgeId);
        auto fc = db::TFc(edgeData.category());
        if (fc < minFc) {
            result = geolib3::Polyline2(edgeData.geometry());
            minFc = fc;
        }
    }
    ASSERT(result.segmentsNumber());
    return result;
}

geolib3::Heading heading(const geolib3::Segment2& segment)
{
    return geolib3::Direction2(segment).heading();
}

void makeCoverage(pgpool3::Pool& pool)
{
    auto ctx = export_gen::Context(TEST_GRAPH_PATH, db::GraphType::Road);
    auto features = db::FeatureGateway(*pool.slaveTransaction())
                        .load(db::table::Feature::isPublished.is(true) ||
                              db::table::Feature::shouldBePublished.is(true));
    auto photos = export_gen::Photos{};
    for (const auto& feature : features) {
        photos.push_back(export_gen::toPhoto(feature));
    }
    auto [graph, photoToEdgePairs] = export_gen::makeGraphSummary(
        ctx, photos, export_gen::makeTrackPointProviderStub());
    graph.mrcVersion = fb::makeVersion(std::chrono::system_clock::now());
    fb::writeToFile(
        graph, concat(TEST_GRAPH_PATH, "/", export_gen::GRAPH_COVERAGE_FILE));
    fb::writePhotoToEdgePairsToFile(
        graph.version,
        graph.mrcVersion,
        export_gen::CURRENT_PHOTO_TO_EDGE_SCHEMA_VERSION,
        photoToEdgePairs,
        concat(TEST_GRAPH_PATH, "/", export_gen::PHOTO_TO_EDGE_FILE));
}

TEST_F(Fixture, testLoadFeatureIdsFromHypotheses)
{
    static const auto TIME = chrono::parseSqlDateTime("2022-06-29 14:34:00+03");
    auto features = db::Features{};
    for (int i = 0; i < 10; ++i) {
        features.push_back(
            sql_chemistry::GatewayAccess<db::Feature>::construct()
                .setUserId(std::to_string(i))
                .setTimestamp(TIME)
                .setDataset(db::Dataset::Agents)
                .setSourceId(std::to_string(i))
                .setMdsKey({std::to_string(i), std::to_string(i)})
                .setSize(6, 9));
    }
    auto hypothetic = std::span{features.begin() + 2, features.begin() + 4};
    {
        auto txn = pool().masterWriteableTransaction();
        db::FeatureGateway{*txn}.insert(features);
        insertHypothesis(*txn, hypothetic);
        txn->commit();
    }

    auto result = loadFeatureIdsFromHypotheses(
        pool(), invokeForEach(features, &db::Feature::id));
    auto expected = invokeForEach(hypothetic, &db::Feature::id);
    EXPECT_THAT(result, ::testing::UnorderedElementsAreArray(expected));
}

TEST_F(Fixture, testLoadNotForCoverFeatureIds)
{
    using namespace std::literals::chrono_literals;
    static const auto NOW = chrono::parseSqlDateTime("2022-07-01 12:00:00+03");
    static const auto SEGMENT =
        edgeInWindow(
            geolib3::BoundingBox(geolib3::Point2(37.560474, 55.705200),
                                 geolib3::Point2(37.569025, 55.707763)))
            .segmentAt(0);
    auto features = db::Features{
        // recent: day, low quality
        db::Feature(std::string{"src0"},
                    SEGMENT.midpoint(),
                    heading(SEGMENT),
                    chrono::formatSqlDateTime(NOW),
                    mds::Key("unittest/0"),
                    db::Dataset::Agents)
            .setSize(6, 9)
            .setQuality(0.1)
            .setAutomaticShouldBePublished(true),
        // Age_30_90: night, high quality
        db::Feature(std::string{"src1"},
                    SEGMENT.midpoint(),
                    heading(SEGMENT),
                    chrono::formatSqlDateTime(NOW - 948h),
                    mds::Key("unittest/1"),
                    db::Dataset::Agents)
            .setSize(6, 9)
            .setQuality(0.9)
            .setAutomaticShouldBePublished(true),
        // Age_30_90: day, low quality
        db::Feature(std::string{"src2"},
                    SEGMENT.midpoint(),
                    heading(SEGMENT),
                    chrono::formatSqlDateTime(NOW - 960h),
                    mds::Key("unittest/2"),
                    db::Dataset::Agents)
            .setSize(6, 9)
            .setQuality(0.1)
            .setAutomaticShouldBePublished(true),
        // Age_30_90: day, mid quality
        db::Feature(std::string{"src3"},
                    SEGMENT.midpoint(),
                    heading(SEGMENT),
                    chrono::formatSqlDateTime(NOW - 960h),
                    mds::Key("unittest/3"),
                    db::Dataset::Agents)
            .setSize(6, 9)
            .setQuality(0.5)
            .setAutomaticShouldBePublished(true),
        // Age_7_30: night, low quality
        db::Feature(std::string{"src4"},
                    SEGMENT.midpoint(),
                    heading(SEGMENT),
                    chrono::formatSqlDateTime(NOW - 228h),
                    mds::Key("unittest/4"),
                    db::Dataset::Agents)
            .setSize(6, 9)
            .setQuality(0.1)
            .setAutomaticShouldBePublished(true),
        // Age_7_30: day, high quality
        db::Feature(std::string{"src5"},
                    SEGMENT.midpoint(),
                    heading(SEGMENT),
                    chrono::formatSqlDateTime(NOW - 240h),
                    mds::Key("unittest/5"),
                    db::Dataset::Agents)
            .setSize(6, 9)
            .setQuality(0.9)
            .setAutomaticShouldBePublished(true),
    };
    {
        auto txn = pool().masterWriteableTransaction();
        db::FeatureGateway{*txn}.insert(features);
        txn->commit();
    }
    makeCoverage(pool());
    auto datasets = Datasets(TEST_GRAPH_PATH, TEST_GRAPH_PATH);
    auto result = loadNotForCoverFeatureIds(pool(), datasets, NOW);
    auto expected = db::TIds{};
    for (auto i : {1, 2}) {
        expected.push_back(features.at(i).id());
    }
    EXPECT_THAT(result, ::testing::UnorderedElementsAreArray(expected));
}

}  // namespace maps::mrc::excess_features_eraser::tests
