#include "fixtures.h"
#include "mocks.h"

#include <maps/wikimap/mapspro/services/mrc/eye/lib/detection/include/match.h>
#include <maps/wikimap/mapspro/services/mrc/eye/lib/object_manager/impl/location.h>
#include <maps/wikimap/mapspro/services/mrc/eye/lib/object_manager/impl/batch.h>
#include <maps/wikimap/mapspro/services/mrc/eye/lib/object_manager/impl/metadata.h>
#include <maps/wikimap/mapspro/services/mrc/eye/lib/object_manager/include/object_manager.h>
#include <maps/wikimap/mapspro/services/mrc/eye/lib/unit_test/include/frame.h>

#include <maps/wikimap/mapspro/services/mrc/libs/db/include/eye/verified_detection_pair_match_gateway.h>
#include <maps/wikimap/mapspro/services/mrc/libs/db/include/eye/verified_detection_missing_on_frame_gateway.h>

#include <maps/libs/geolib/include/vector.h>
#include <maps/libs/sql_chemistry/include/system_information.h>

#include <library/cpp/testing/gtest/gtest.h>

#include <chrono>

using namespace std::literals::chrono_literals;

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


TEST_F(StraightForwardMatchFixture, should_create_two_objects)
{
    auto workerConfig = makeWorkerConfig({db::eye::DetectionType::Sign});
    ObjectManager objectManager(workerConfig);
    objectManager.processBatchInLoopMode(100);

    const auto clusters = loadDetectionClusters(*newTxn());
    EXPECT_EQ(clusters.size(), 2u);
    const auto detectionSets = clustersToSets(clusters);
    EXPECT_THAT(detectionSets, ::testing::UnorderedElementsAre(firstSignDetections, secondSignDetections));
}

TEST_F(StraightForwardMatchFixture, positive_verified_detection_pair_match)
{
    auto detectionIdsIt = firstSignDetections.begin();
    const auto primaryDetectionId = *detectionIdsIt;
    ++detectionIdsIt;
    const db::TId detectionToMatch = *detectionIdsIt;

    /// Create object for selected primaryDetection in order to
    /// narrow variants of VerifiedDetectionPairMatches
    {
        auto txn = newTxn();
        auto detectionIt = std::find_if(
            detections.begin(), detections.end(),
            [primaryDetectionId](const auto& detection) { return detection.id() == primaryDetectionId; }
        );
        ASSERT_TRUE(detectionIt != detections.end());

        makeSignObject(*txn, *detectionIt, {10, 0});

        db::eye::VerifiedDetectionPairMatch verifiedDetectionPairMatch{
            db::eye::VerificationSource::Toloka,
            primaryDetectionId,
            detectionToMatch,
            true // matches
        };
        db::eye::VerifiedDetectionPairMatchGateway{*txn}.insertx(verifiedDetectionPairMatch);

        txn->commit();
    }

    auto workerConfig = makeWorkerConfig({db::eye::DetectionType::Sign});
    ObjectManager objectManager(workerConfig);
    objectManager.processBatchInLoopMode(100);

    const auto clusters = loadDetectionClusters(*newTxn());
    EXPECT_EQ(clusters.size(), 2u);
    const auto detectionSets = clustersToSets(clusters);
    EXPECT_THAT(detectionSets, ::testing::UnorderedElementsAre(firstSignDetections, secondSignDetections));
}


TEST_F(StraightForwardMatchFixture, negative_verified_detection_pair_match)
{
    /// Isolate one of detections from its cluster by
    /// creating negative VerifiedDetectionPairMatches

    auto detectionIdsIt = firstSignDetections.begin();
    const auto primaryDetectionId = *detectionIdsIt;
    ++detectionIdsIt;
    const db::TId detectionToIsolate = *detectionIdsIt;

    /// Create object for selected primaryDetection in order to
    /// narrow variants of VerifiedDetectionPairMatches
    {
        auto txn = newTxn();
        auto detectionIt = std::find_if(
            detections.begin(), detections.end(),
            [primaryDetectionId](const auto& detection) { return detection.id() == primaryDetectionId; }
        );
        ASSERT_TRUE(detectionIt != detections.end());

        makeSignObject(*txn, *detectionIt, {10, 0});
        txn->commit();
    }

    auto workerConfig = makeWorkerConfig({db::eye::DetectionType::Sign});
    ObjectManager objectManager(workerConfig);
    objectManager.processBatchInLoopMode(100);

    {
        auto txn = newTxn();
        db::eye::VerifiedDetectionPairMatch verifiedDetectionPairMatch{
            db::eye::VerificationSource::Toloka,
            primaryDetectionId,
            detectionToIsolate,
            false // does not match
        };
        db::eye::VerifiedDetectionPairMatchGateway{*txn}.insertx(verifiedDetectionPairMatch);

        txn->commit();
    }

    objectManager.processBatchInLoopMode(100);

    const auto clusters = loadDetectionClusters(*newTxn());

    /// The isolated detection should be in a separate cluster
    EXPECT_EQ(clusters.size(), 3u);
    auto newFirstSignDetections = firstSignDetections;
    newFirstSignDetections.erase(detectionToIsolate);
    newFirstSignDetections.erase(primaryDetectionId);

    ASSERT_TRUE(clusters.count(primaryDetectionId));
    EXPECT_THAT(clusters.at(primaryDetectionId),
        ::testing::UnorderedElementsAreArray(newFirstSignDetections));

}


TEST_F(StraightForwardMatchFixture, positive_verified_detection_pair_match_to_different_cluster)
{
    const auto primaryDetectionId = *firstSignDetections.begin();
    // select detections from the same passage
    db::TIds detectionsToMatch{};
    const auto& passageDetections = detectionsInPassages[1];
    for (const auto& detections : passageDetections.detectionsInFrames) {
        // MockDetectionMatcher matches these detections to second cluster
        detectionsToMatch.push_back(detections.at(1).id());
    }

    /// Create object for selected primaryDetection in order to
    /// narrow variants of VerifiedDetectionPairMatches
    {
        auto txn = newTxn();
        auto detectionIt = std::find_if(
            detections.begin(), detections.end(),
            [primaryDetectionId](const auto& detection) { return detection.id() == primaryDetectionId; }
        );
        ASSERT_TRUE(detectionIt != detections.end());

        makeSignObject(*txn, *detectionIt, {10, 0});

        for (auto id: detectionsToMatch) {
            db::eye::VerifiedDetectionPairMatch verifiedDetectionPairMatch{
                db::eye::VerificationSource::Toloka,
                primaryDetectionId,
                id,
                true // matches
            };
            db::eye::VerifiedDetectionPairMatchGateway{*txn}.insertx(
                verifiedDetectionPairMatch);
        }

        txn->commit();
    }

    auto workerConfig = makeWorkerConfig({db::eye::DetectionType::Sign});
    ObjectManager objectManager(workerConfig);
    objectManager.processBatchInLoopMode(100);

    const auto clusters = loadDetectionClusters(*newTxn());
}

/// Between the first two passages added a new one in which one of objects
/// is missing.
class ObjectsMissingInPassageFixture : public StraightForwardMatchFixture
{
public:
    ObjectsMissingInPassageFixture()
    {
        auto txn = newTxn();
        const auto passageOffset = 600s;
        addFrameWithDetections(*txn, devices.at(0).id(), passageOffset + 0s, {0, 0}, geolib3::Heading{90},
            {traffic_signs::TrafficSign::ProhibitoryMaxSpeed50});

        secondSignDetections.insert(detections.back().id());

        missingObjectsDetectionGroupId = groups.back().id();
        missingObjectsFrame = frames.back();

        std::vector<db::eye::VerifiedDetectionMissingOnFrame> missings;
        for (const auto& detections: detectionsInPassages[0].detectionsInFrames) {
            for (const auto& detection : detections) {
                if (firstSignDetections.contains(detection.id())) {
                    missings.emplace_back(
                    db::eye::VerificationSource::Toloka,
                    detection.id(),
                    missingObjectsFrame->id(),
                    db::eye::VerifiedDetectionMissingOnFrameIsVisible::No,
                    db::eye::VerifiedDetectionMissingOnFrameMissingReason::Missing);
                    missingDetectionIds.insert(detection.id());
                }
            }
        }

        db::eye::VerifiedDetectionMissingOnFrameGateway{*txn}.insertx(missings);

        txn->commit();
    }

    db::TId missingObjectsDetectionGroupId;
    db::TIdSet missingDetectionIds;
    std::optional<db::eye::Frame> missingObjectsFrame;
};

TEST_F(ObjectsMissingInPassageFixture, test_processing_verified_missingness)
{
    auto workerConfig = makeWorkerConfig({db::eye::DetectionType::Sign});
    ObjectManager objectManager(workerConfig);

    // process 1st passage
    objectManager.processBatch(collectDetectionGroupIds(detectionsInPassages.at(0)));

    auto objects = db::eye::ObjectGateway{*newTxn()}.load();
    EXPECT_THAT(objects, ::testing::SizeIs(2u));

    // process batch with missing object
    objectManager.processBatch({missingObjectsDetectionGroupId});

    objects = db::eye::ObjectGateway{*newTxn()}.load();

    std::vector<std::optional<chrono::TimePoint>> objectDisappearenceDates;
    for (const auto& object : objects) {
        objectDisappearenceDates.push_back(object.disappearedAt());
    }

    EXPECT_THAT(objectDisappearenceDates,
        ::testing::UnorderedElementsAre(std::nullopt, missingObjectsFrame->time()));

    // process all of the rest detection groups
    objectManager.processBatchInLoopMode(100);

    // should create one new object instead of the disappeared one
    const auto clusters = loadDetectionClusters(*newTxn());
    EXPECT_EQ(clusters.size(), 3u);
    const auto detectionSets = clustersToSets(clusters);

    db::TIdSet newFirstSignDetections;
    std::set_difference(
        firstSignDetections.begin(),
        firstSignDetections.end(),
        missingDetectionIds.begin(),
        missingDetectionIds.end(),
        std::inserter(newFirstSignDetections, newFirstSignDetections.end()));

    EXPECT_THAT(
        detectionSets,
        ::testing::UnorderedElementsAre(
            missingDetectionIds, newFirstSignDetections, secondSignDetections));
}

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