#include <maps/wikimap/mapspro/services/mrc/eye/lib/detection/tests/fixture.h>

#include <maps/wikimap/mapspro/services/mrc/eye/lib/common/include/id.h>
#include <maps/wikimap/mapspro/services/mrc/eye/lib/detection/include/position_clusterizer.h>
#include <maps/wikimap/mapspro/services/mrc/eye/lib/location/include/rotation.h>
#include <maps/wikimap/mapspro/services/mrc/eye/lib/unit_test/include/frame.h>

#include <maps/wikimap/mapspro/services/mrc/libs/db/include/eye/frame_gateway.h>
#include <maps/wikimap/mapspro/services/mrc/libs/db/include/eye/recognition_gateway.h>

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

namespace maps::mrc::eye::tests {

struct PositionClusterizerFixture: public Fixture {
    PositionClusterizerFixture() {
        devices = insertx<db::eye::DeviceGateway>(
            db::eye::Devices {
                {db::eye::MrcDeviceAttrs{""}}
            }
        );

        frames = insertx<db::eye::FrameGateway>(
            db::eye::Frames {
                {devices[0].id(), identical, makeUrlContext(1, "1"), {1200, 800}, time()},
                {devices[0].id(), identical, makeUrlContext(2, "2"), {1200, 800}, time()},
                {devices[0].id(), identical, makeUrlContext(3, "3"), {3600, 2400}, time()},
                {devices[0].id(), identical, makeUrlContext(4, "4"), {3600, 2400}, time()},
                {devices[0].id(), identical, makeUrlContext(5, "5"), {3600, 2400}, time()},
                {devices[0].id(), identical, makeUrlContext(6, "6"), {3600, 2400}, time()},
                {devices[0].id(), identical, makeUrlContext(7, "7"), {3600, 2400}, time()},
            }
        );

        locations = insertx<db::eye::FrameLocationGateway>(
            db::eye::FrameLocations {
                {frames[0].id(), geolib3::Point2{0, 0}, toRotation(geolib3::Heading(90), identical)},
                {frames[1].id(), geolib3::Point2{10, 0}, toRotation(geolib3::Heading(90), identical)},
                {frames[2].id(), geolib3::Point2{0, 10}, toRotation(geolib3::Heading(90), identical)},
                {frames[3].id(), geolib3::Point2{0, 10}, toRotation(geolib3::Heading(90), identical)},
                {frames[4].id(), geolib3::Point2{0, 10}, toRotation(geolib3::Heading(90), identical)},
                {frames[5].id(), geolib3::Point2{100, 100}, toRotation(geolib3::Heading(90), identical)},
                {frames[6].id(), geolib3::Point2{100, 105}, toRotation(geolib3::Heading(90), identical)},
            }
        );
        for (const auto& frame : frames) {
            privacies.emplace_back(frame.id(), db::FeaturePrivacy::Public);
        }
        privacies = insertx<db::eye::FramePrivacyGateway>(std::move(privacies));

        groups = insertx<db::eye::DetectionGroupGateway>(
            db::eye::DetectionGroups {
                {frames[0].id(), db::eye::DetectionType::HouseNumber},
                {frames[1].id(), db::eye::DetectionType::HouseNumber},
                {frames[2].id(), db::eye::DetectionType::HouseNumber},
                {frames[3].id(), db::eye::DetectionType::HouseNumber},
                {frames[4].id(), db::eye::DetectionType::HouseNumber},
                {frames[5].id(), db::eye::DetectionType::HouseNumber},
                {frames[6].id(), db::eye::DetectionType::HouseNumber},
            }
        );
        detections = insertx<db::eye::DetectionGateway>(
            db::eye::Detections {
                {groups[0].id(), db::eye::DetectedHouseNumber{{100, 100, 200, 200}, 1.0, "12"}},
                {groups[1].id(), db::eye::DetectedHouseNumber{{400, 100, 500, 200}, 1.0, "12"}},
                {groups[2].id(), db::eye::DetectedHouseNumber{{100, 100, 200, 200}, 1.0, "12"}},
                {groups[3].id(), db::eye::DetectedHouseNumber{{100, 100, 200, 200}, 1.0, "12"}},
                {groups[4].id(), db::eye::DetectedHouseNumber{{100, 100, 200, 200}, 1.0, "11"}},
                {groups[5].id(), db::eye::DetectedHouseNumber{{100, 100, 200, 200}, 1.0, "11"}},
                {groups[6].id(), db::eye::DetectedHouseNumber{{100, 100, 200, 200}, 1.0, "11"}},
            }
        );
        store  = DetectionStore {
            byId(groups),
            byId(detections),
            byId(frames),
            byId(locations),
            byId(privacies),
            byId(devices)
        };
    }

    db::eye::Devices devices;
    db::eye::Frames frames;
    db::eye::FrameLocations locations;
    db::eye::FramePrivacies privacies;
    db::eye::DetectionGroups groups;
    db::eye::Detections detections;
    DetectionStore store;
};

Y_UNIT_TEST_SUITE_F(position_clusterizer, PositionClusterizerFixture)
{

Y_UNIT_TEST(empty)
{
    PositionDetectionClusterizer clusterizer;

    auto result = clusterizer.clusterize(store, {}, {});

    UNIT_ASSERT(result.empty());
}

Y_UNIT_TEST(do_not_merge_detection_without_edges)
{
    PositionDetectionClusterizer clusterizer;
    const db::TIdSet detectionIds{
        detections[0].id(),
        detections[1].id(),
        detections[2].id(),
        detections[4].id(),
    };
    MatchedFrameDetections matches{
        {
            {frames[0].id(), detections[0].id()},
            {frames[1].id(), detections[1].id()},
            1.
        },
        {
            {frames[0].id(), detections[0].id()},
            {frames[2].id(), detections[2].id()},
            1.
        },
        {
            {frames[1].id(), detections[1].id()},
            {frames[2].id(), detections[2].id()},
            1.
        },
    };
    std::vector<db::TIdSet> result = clusterizer.clusterize(store, detectionIds, matches);

    std::vector<db::TIdSet> expected{
        {detections[0].id(), detections[1].id(), detections[2].id()},
        {detections[4].id()}
    };

    UNIT_ASSERT(
        std::is_permutation(result.begin(), result.end(), expected.begin())
    );
}

Y_UNIT_TEST(do_not_merge_distance_detection)
{
    PositionDetectionClusterizer clusterizer;
    const db::TIdSet detectionIds{
        detections[0].id(),
        detections[5].id(),
    };
    MatchedFrameDetections matches{
        {
            {frames[0].id(), detections[0].id()},
            {frames[5].id(), detections[5].id()},
            1.
        },
    };
    std::vector<db::TIdSet> result = clusterizer.clusterize(store, detectionIds, matches);

    std::vector<db::TIdSet> expected{
        {detections[0].id()},
        {detections[5].id()}
    };

    UNIT_ASSERT(
        std::is_permutation(result.begin(), result.end(), expected.begin())
    );
}

Y_UNIT_TEST(merge_two_detection_clusters)
{
    PositionDetectionClusterizer clusterizer;
    const db::TIdSet detectionIds{
        detections[0].id(),
        detections[1].id(),
        detections[2].id(),
        detections[5].id(),
        detections[6].id(),
    };
    MatchedFrameDetections matches{
        {
            {frames[0].id(), detections[0].id()},
            {frames[1].id(), detections[1].id()},
            1.
        },
        {
            {frames[0].id(), detections[0].id()},
            {frames[2].id(), detections[2].id()},
            1.
        },
        {
            {frames[1].id(), detections[1].id()},
            {frames[2].id(), detections[2].id()},
            1.
        },
        {
            {frames[0].id(), detections[0].id()},
            {frames[5].id(), detections[5].id()},
            1.
        },
        {
            {frames[5].id(), detections[5].id()},
            {frames[6].id(), detections[6].id()},
            1.
        },
    };


    std::vector<db::TIdSet> result = clusterizer.clusterize(store, detectionIds, matches);

    std::vector<db::TIdSet> expected{
        {detections[0].id(), detections[1].id(), detections[2].id()},
        {detections[5].id(), detections[6].id()}
    };

    UNIT_ASSERT(
        std::is_permutation(result.begin(), result.end(), expected.begin())
    );
}

Y_UNIT_TEST(take_negative_verdict_into_account)
{
    PositionDetectionClusterizer clusterizer;
    const db::TIdSet detectionIds{
        detections[0].id(),
        detections[1].id(),
        detections[2].id(),
    };
    MatchedFrameDetections matches{
        {
            {frames[0].id(), detections[0].id()},
            {frames[1].id(), detections[1].id()},
            1.
        },
        {
            {frames[0].id(), detections[0].id()},
            {frames[2].id(), detections[2].id()},
            1.
        },
        {
            {frames[1].id(), detections[1].id()},
            {frames[2].id(), detections[2].id()},
            1.,
            MatchedFrameDetection::Verdict::No
        },
    };
    std::vector<db::TIdSet> result = clusterizer.clusterize(store, detectionIds, matches);

    std::vector<db::TIdSet> expected{
        {detections[0].id(), detections[1].id()},
        {detections[2].id()}
    };

    EXPECT_EQ(result.size(), 2u);
    for (const auto& detectionIds : result) {
        EXPECT_FALSE(detectionIds.count(detections[1].id()) &&
            detectionIds.count(detections[2].id()));
    }
}

Y_UNIT_TEST(takes_positive_verdict_into_account)
{
    PositionDetectionClusterizer clusterizer;
    const db::TIdSet detectionIds{
        detections[4].id(),
        detections[5].id(),
    };
    MatchedFrameDetections matches{
        {
            {frames[4].id(), detections[4].id()},
            {frames[5].id(), detections[5].id()},
            1.,
            MatchedFrameDetection::Verdict::Yes
        },
    };
    std::vector<db::TIdSet> result = clusterizer.clusterize(store, detectionIds, matches);

    std::vector<db::TIdSet> expected{
        {detections[4].id(), detections[5].id()}
    };

    EXPECT_THAT(result, ::testing::UnorderedElementsAreArray(expected));
}

} // Y_UNIT_TEST_SUITE

} // namespace maps::mrc::eye::tests
