#include "eye/frame_gateway.h"
#include "eye/recognition.h"
#include "eye/recognition_gateway.h"
#include "eye/recognition_value.h"
#include "gtest/gtest.h"
#include "image_box.h"
#include <mapreduce/yt/tests/yt_unittest_lib/yt_unittest_lib.h>
#include <maps/wikimap/mapspro/services/mrc/libs/common/include/algorithm/collection.h>
#include <maps/wikimap/mapspro/services/mrc/eye/lib/unit_test/include/frame.h>
#include <maps/wikimap/mapspro/services/mrc/eye/lib/sync_detection/include/sync_detection.h>
#include <maps/wikimap/mapspro/services/mrc/eye/lib/sync_detection/tests/fixture.h>

#include <maps/libs/log8/include/log8.h>
#include <maps/libs/introspection/include/stream_output.h>

#include <functional>
#include <unordered_map>

namespace maps::mrc::db::eye {

using maps::introspection::operator<<;

} //namespace maps::mrc::db::eye

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

TEST_F(Fixture, run)
{
    SyncDetection sync(makeSyncDetectionConfig());
    sync.processBatch(frameIdsAt({0, 1, 3, 5}));
}

TEST_F(Fixture, run_for_batch)
{
    SyncDetection sync(makeSyncDetectionConfig());

    EXPECT_EQ(sync.processBatchInLoopMode(4), true);
    EXPECT_EQ(sync.processBatchInLoopMode(4), true);
}

struct OneFrameRecognitionsFixture : public BaseFixture {
    static const int VERSION = 1;
    static const int BATCH_SIZE = 4;

    OneFrameRecognitionsFixture()
        : frame{device.id(), identical, makeUrlContext(1, "1"), imageSize, time()}
    {
        auto txn = newTxn();
        db::eye::FrameGateway(*txn).insertx(frame);

        recognitions = makeRecognitions();
        db::eye::RecognitionGateway(*txn).insertx(recognitions);
        txn->commit();
    }

    db::eye::Recognitions makeRecognitions()
    {
        ///    _______
        ///   |       |
        ///   | PNPOS |
        ///   |_______|
        ///     _________
        ///   | inf heavy|
        ///   |_________|
        ///
        ///    __________
        ///   | Mandatory|
        ///   |__________|

        return db::eye::Recognitions{
            {frame.id(),
             frame.orientation(),
             db::eye::RecognitionType::DetectSign,
             db::eye::RecognitionSource::Import,
             VERSION,
             db::eye::DetectedSigns{
                 db::eye::DetectedSign{
                     {315, 80, 345, 110},
                     traffic_signs::TrafficSign::ProhibitoryNoParkingOrStopping,
                     0.9,
                     false,
                     0.95},
                 db::eye::DetectedSign{
                     {310, 120, 355, 150},
                     traffic_signs::TrafficSign::InformationHeavyVehicle,
                     0.9,
                     false,
                     0.95},
                db::eye::DetectedSign{
                     {315, 160, 345, 190},
                     traffic_signs::TrafficSign::MandatoryTurnLeft,
                     0.9,
                     false,
                     0.95},
             }}};
    }

    const common::Size imageSize{640, 480};
    db::eye::Frame frame;
    db::eye::Recognitions recognitions;
};

struct OneFrameDetectionsFixture : public OneFrameRecognitionsFixture
{
    OneFrameDetectionsFixture()
    {
        groups =
            insertx<db::eye::DetectionGroupGateway>(db::eye::DetectionGroups{
                {frame.id(), db::eye::DetectionType::Sign}});
        detections = insertx<db::eye::DetectionGateway>(db::eye::Detections{
            {groups[0].id(),
             db::eye::DetectedSign{
                 {315, 80, 345, 110},
                 traffic_signs::TrafficSign::ProhibitoryNoParkingOrStopping,
                 1.0,
                 false,
                 0.0}},
            {groups[0].id(),
             db::eye::DetectedSign{
                 {310, 120, 355, 150},
                 traffic_signs::TrafficSign::InformationHeavyVehicle,
                 1.0,
                 false,
                 0.0}},
            {groups[0].id(),
             db::eye::DetectedSign{
                 {315, 160, 345, 190},
                 traffic_signs::TrafficSign::MandatoryTurnLeft,
                 1.0,
                 false,
                 0.0}},
        });
        relations = insertx<db::eye::DetectionRelationGateway>(
            db::eye::DetectionRelations{
                {detections[0].id(), detections[1].id()}});
    }

    db::eye::DetectionGroups groups;
    db::eye::Detections detections;
    db::eye::DetectionRelations relations;

};

TEST_F(OneFrameRecognitionsFixture, detections_with_relations_are_created_from_recognitions)
{
    SyncDetection sync(makeSyncDetectionConfig());
    EXPECT_EQ(sync.processBatchInLoopMode(BATCH_SIZE), true);

    auto txn = newTxn();
    auto idToDetection = common::byId(db::eye::DetectionGateway(*txn).load());
    EXPECT_EQ(idToDetection.size(), 3u);

    const auto detectionRelations = db::eye::DetectionRelationGateway(*txn).load();

    EXPECT_EQ(detectionRelations.size(), 1u);
    const auto& relation = detectionRelations.at(0);
    EXPECT_EQ(
        idToDetection.at(relation.masterDetectionId())
            .attrs<db::eye::DetectedSign>()
            .type,
        traffic_signs::TrafficSign::ProhibitoryNoParkingOrStopping);
    EXPECT_EQ(
        idToDetection.at(relation.slaveDetectionId())
            .attrs<db::eye::DetectedSign>()
            .type,
        traffic_signs::TrafficSign::InformationHeavyVehicle);
}

TEST_F(OneFrameDetectionsFixture, delete_detections_with_relations_on_frame_deletion)
{
    {
        auto txn = newTxn();
        frame.setDeleted(true);
        db::eye::FrameGateway(*txn).updatex(frame);
        txn->commit();
    }

    SyncDetection sync(makeSyncDetectionConfig());
    EXPECT_EQ(sync.processBatchInLoopMode(BATCH_SIZE), true);

    auto txn = newTxn();

    const auto loadedDetections = db::eye::DetectionGateway(*txn).load();
    ASSERT_EQ(loadedDetections.size(), 3u);
    EXPECT_TRUE(loadedDetections.at(0).deleted());
    EXPECT_TRUE(loadedDetections.at(1).deleted());
    EXPECT_TRUE(loadedDetections.at(2).deleted());

    const auto loadedRelations = db::eye::DetectionRelationGateway(*txn).load();
    ASSERT_EQ(loadedRelations.size(), 1u);
    EXPECT_TRUE(loadedRelations.at(0).deleted());

}

TEST_F(OneFrameDetectionsFixture, restore_deleted_detections_with_relations_on_frame_deletion)
{
    {
        auto txn = newTxn();
        for (auto& detection : detections) {
            detection.setDeleted();
        }
        db::eye::DetectionGateway(*txn).updatex(detections);
        for (auto& relation : relations) {
            relation.setDeleted(true);
        }
        db::eye::DetectionRelationGateway(*txn).updatex(relations);
        txn->commit();
    }

    SyncDetection sync(makeSyncDetectionConfig());
    EXPECT_EQ(sync.processBatchInLoopMode(BATCH_SIZE), true);
    EXPECT_EQ(sync.processBatchInLoopMode(BATCH_SIZE), false);

    auto txn = newTxn();

    const auto loadedDetections = db::eye::DetectionGateway(*txn).load(
        not db::eye::table::Detection::deleted);
    ASSERT_EQ(loadedDetections.size(), 3u);

    const auto loadedRelations = db::eye::DetectionRelationGateway(*txn).load(
        not db::eye::table::DetectionRelation::deleted);
    ASSERT_EQ(loadedRelations.size(), 1u);
}

TEST_F(OneFrameDetectionsFixture, detection_relations_are_updated_on_frame_orientation_change)
{
    {
        auto txn = newTxn();
        frame.setOrientation(upsideDown);
        recognitions = makeRecognitions();
        db::eye::FrameGateway(*txn).updatex(frame);
        db::eye::RecognitionGateway(*txn).insertx(recognitions);
        txn->commit();
    }

    SyncDetection sync(makeSyncDetectionConfig());
    EXPECT_EQ(sync.processBatchInLoopMode(BATCH_SIZE), true);

    auto txn = newTxn();
    auto idToDetection = common::byId(db::eye::DetectionGateway(*txn).load());

    EXPECT_EQ(idToDetection.size(), 3u);
    const auto detectionRelations = db::eye::DetectionRelationGateway(*txn).load();
    EXPECT_EQ(detectionRelations.size(), 2u);
    EXPECT_TRUE(detectionRelations.at(0).deleted());
    const auto& relation = detectionRelations.at(1);
    EXPECT_FALSE(relation.deleted());
    EXPECT_EQ(
        idToDetection.at(relation.masterDetectionId())
            .attrs<db::eye::DetectedSign>()
            .type,
        traffic_signs::TrafficSign::MandatoryTurnLeft);
    EXPECT_EQ(
        idToDetection.at(relation.slaveDetectionId())
            .attrs<db::eye::DetectedSign>()
            .type,
        traffic_signs::TrafficSign::InformationHeavyVehicle);
}

TEST_F(OneFrameDetectionsFixture, rerecognition_with_the_same_result)
{
    {
        auto txn = newTxn();
        recognitions = makeRecognitions();
        recognitions[0].setVersion(VERSION + 1);
        db::eye::RecognitionGateway(*txn).insertx(recognitions);
        txn->commit();
    }

    SyncDetection sync(makeSyncDetectionConfig());
    EXPECT_EQ(sync.processBatchInLoopMode(BATCH_SIZE), true);

    auto txn = newTxn();
    auto idToDetection = common::byId(db::eye::DetectionGateway(*txn).load());
    EXPECT_EQ(idToDetection.size(), 3u);

    const auto detectionRelations = db::eye::DetectionRelationGateway(*txn).load();

    EXPECT_EQ(detectionRelations.size(), 1u);
    const auto& relation = detectionRelations.at(0);
    EXPECT_EQ(
        idToDetection.at(relation.masterDetectionId())
            .attrs<db::eye::DetectedSign>()
            .type,
        traffic_signs::TrafficSign::ProhibitoryNoParkingOrStopping);
    EXPECT_EQ(
        idToDetection.at(relation.slaveDetectionId())
            .attrs<db::eye::DetectedSign>()
            .type,
        traffic_signs::TrafficSign::InformationHeavyVehicle);
}

TEST_F(OneFrameDetectionsFixture, rerecognition_with_slite_shift)
{
    {
        const int shiftX = 1;
        auto txn = newTxn();
        recognitions = makeRecognitions();
        auto detectedSigns = recognitions[0].value<db::eye::DetectedSigns>();
        for (auto& sign : detectedSigns) {
            sign.box = common::ImageBox(
                sign.box.minX() + shiftX,
                sign.box.minY(),
                sign.box.maxX() + shiftX,
                sign.box.maxY());
        }
        recognitions[0].setVersion(VERSION + 1)
            .setValue(detectedSigns);
        db::eye::RecognitionGateway(*txn).insertx(recognitions);
        txn->commit();
    }

    SyncDetection sync(makeSyncDetectionConfig());
    EXPECT_EQ(sync.processBatchInLoopMode(BATCH_SIZE), true);

    auto txn = newTxn();
    auto idToDetection = common::byId(db::eye::DetectionGateway(*txn).load());
    EXPECT_EQ(idToDetection.size(), 3u);

    const auto detectionRelations = db::eye::DetectionRelationGateway(*txn).load();

    EXPECT_EQ(detectionRelations.size(), 1u);
    const auto& relation = detectionRelations.at(0);
    EXPECT_EQ(
        idToDetection.at(relation.masterDetectionId())
            .attrs<db::eye::DetectedSign>()
            .type,
        traffic_signs::TrafficSign::ProhibitoryNoParkingOrStopping);
    EXPECT_EQ(
        idToDetection.at(relation.slaveDetectionId())
            .attrs<db::eye::DetectedSign>()
            .type,
        traffic_signs::TrafficSign::InformationHeavyVehicle);
}

TEST_F(OneFrameDetectionsFixture, rerecognition_with_big_shift)
{
    {
        const int shiftX = 50;
        auto txn = newTxn();
        recognitions = makeRecognitions();
        auto detectedSigns = recognitions[0].value<db::eye::DetectedSigns>();
        for (auto& sign : detectedSigns) {
            sign.box = common::ImageBox(
                sign.box.minX() + shiftX,
                sign.box.minY(),
                sign.box.maxX() + shiftX,
                sign.box.maxY());
        }
        recognitions[0].setVersion(VERSION + 1)
            .setValue(detectedSigns);
        db::eye::RecognitionGateway(*txn).insertx(recognitions);
        txn->commit();
    }

    SyncDetection sync(makeSyncDetectionConfig());
    EXPECT_EQ(sync.processBatchInLoopMode(BATCH_SIZE), true);

    auto txn = newTxn();
    auto idToDetection = common::byId(db::eye::DetectionGateway(*txn).load());
    EXPECT_EQ(idToDetection.size(), 6u);
    for (const auto& [id, detection] : idToDetection) {
        SCOPED_TRACE("Detection id = " + std::to_string(id));
        EXPECT_EQ(detection.deleted(), id < 4);
    }

    const auto detectionRelations = db::eye::DetectionRelationGateway(*txn).load();

    ASSERT_EQ(detectionRelations.size(), 2u);
    EXPECT_TRUE(detectionRelations.at(0).deleted());
    EXPECT_FALSE(detectionRelations.at(1).deleted());
    const auto& relation = detectionRelations.at(1);
    EXPECT_EQ(
        idToDetection.at(relation.masterDetectionId())
            .attrs<db::eye::DetectedSign>()
            .type,
        traffic_signs::TrafficSign::ProhibitoryNoParkingOrStopping);
    EXPECT_EQ(
        idToDetection.at(relation.slaveDetectionId())
            .attrs<db::eye::DetectedSign>()
            .type,
        traffic_signs::TrafficSign::InformationHeavyVehicle);
}

TEST_F(OneFrameDetectionsFixture, rerecognition_with_main_sign_changed_type)
{
    const auto newMainType = traffic_signs::TrafficSign::ProhibitoryMaxSpeed;
    {
        auto txn = newTxn();
        recognitions = makeRecognitions();
        auto detectedSigns = recognitions[0].value<db::eye::DetectedSigns>();
        detectedSigns[0].type = newMainType;
        recognitions[0].setVersion(VERSION + 1)
            .setValue(detectedSigns);
        db::eye::RecognitionGateway(*txn).insertx(recognitions);
        txn->commit();
    }

    SyncDetection sync(makeSyncDetectionConfig());
    EXPECT_EQ(sync.processBatchInLoopMode(BATCH_SIZE), true);

    auto txn = newTxn();
    auto idToDetection = common::byId(db::eye::DetectionGateway(*txn).load());
    EXPECT_EQ(idToDetection.size(), 4u);
    EXPECT_TRUE(idToDetection.at(1).deleted());

    const auto detectionRelations = db::eye::DetectionRelationGateway(*txn).load();

    ASSERT_EQ(detectionRelations.size(), 2u);
    EXPECT_TRUE(detectionRelations.at(0).deleted());
    EXPECT_FALSE(detectionRelations.at(1).deleted());
    const auto& relation = detectionRelations.at(1);
    EXPECT_FALSE(relation.deleted());
    EXPECT_EQ(
        idToDetection.at(relation.masterDetectionId())
            .attrs<db::eye::DetectedSign>()
            .type,
        newMainType);
    EXPECT_EQ(
        idToDetection.at(relation.slaveDetectionId())
            .attrs<db::eye::DetectedSign>()
            .type,
        traffic_signs::TrafficSign::InformationHeavyVehicle);
}

TEST_F(OneFrameDetectionsFixture, rerecognition_with_slave_sign_changed_type)
{
    const auto newSlaveType = traffic_signs::TrafficSign::InformationInZone;
    {
        auto txn = newTxn();
        recognitions = makeRecognitions();
        auto detectedSigns = recognitions[0].value<db::eye::DetectedSigns>();
        detectedSigns[1].type = newSlaveType;
        recognitions[0].setVersion(VERSION + 1)
            .setValue(detectedSigns);
        db::eye::RecognitionGateway(*txn).insertx(recognitions);
        txn->commit();
    }

    SyncDetection sync(makeSyncDetectionConfig());
    EXPECT_EQ(sync.processBatchInLoopMode(BATCH_SIZE), true);

    auto txn = newTxn();
    auto idToDetection = common::byId(db::eye::DetectionGateway(*txn).load());
    EXPECT_EQ(idToDetection.size(), 4u);
    EXPECT_TRUE(idToDetection.at(2).deleted());

    const auto detectionRelations = db::eye::DetectionRelationGateway(*txn).load();

    ASSERT_EQ(detectionRelations.size(), 2u);
    EXPECT_TRUE(detectionRelations.at(0).deleted());
    EXPECT_FALSE(detectionRelations.at(1).deleted());
    const auto& relation = detectionRelations.at(1);
    EXPECT_FALSE(relation.deleted());
    EXPECT_EQ(
        idToDetection.at(relation.masterDetectionId())
            .attrs<db::eye::DetectedSign>()
            .type,
        traffic_signs::TrafficSign::ProhibitoryNoParkingOrStopping);
    EXPECT_EQ(
        idToDetection.at(relation.slaveDetectionId())
            .attrs<db::eye::DetectedSign>()
            .type,
        newSlaveType);
}

TEST_F(OneFrameDetectionsFixture, rerecognition_with_slave_sign_changed_type_to_unslave)
{
    const auto newType = traffic_signs::TrafficSign::MandatoryCycleRoute;
    {
        auto txn = newTxn();
        recognitions = makeRecognitions();
        auto detectedSigns = recognitions[0].value<db::eye::DetectedSigns>();
        detectedSigns[1].type = newType;
        recognitions[0].setVersion(VERSION + 1)
            .setValue(detectedSigns);
        db::eye::RecognitionGateway(*txn).insertx(recognitions);
        txn->commit();
    }

    SyncDetection sync(makeSyncDetectionConfig());
    EXPECT_EQ(sync.processBatchInLoopMode(BATCH_SIZE), true);

    auto txn = newTxn();
    auto idToDetection = common::byId(db::eye::DetectionGateway(*txn).load());
    EXPECT_EQ(idToDetection.size(), 4u);
    EXPECT_TRUE(idToDetection.at(2).deleted());

    const auto detectionRelations = db::eye::DetectionRelationGateway(*txn).load();

    ASSERT_EQ(detectionRelations.size(), 1u);
    EXPECT_TRUE(detectionRelations.at(0).deleted());
}

TEST_F(OneFrameDetectionsFixture, rerecognition_on_frame_with_deleted_detections)
{
    {
        auto txn = newTxn();
        detections[0].setDeleted();
        db::eye::DetectionGateway(*txn).updatex(detections);
        for (auto& relation : relations) {
            relation.setDeleted(true);
        }
        db::eye::DetectionRelationGateway(*txn).updatex(relations);

        const int shiftX = 1;
        recognitions = makeRecognitions();
        auto detectedSigns = recognitions[0].value<db::eye::DetectedSigns>();
        for (auto& sign : detectedSigns) {
            sign.box = common::ImageBox(
                sign.box.minX() + shiftX,
                sign.box.minY(),
                sign.box.maxX() + shiftX,
                sign.box.maxY());
        }
        recognitions[0].setVersion(VERSION + 1)
            .setValue(detectedSigns);
        db::eye::RecognitionGateway(*txn).insertx(recognitions);

        txn->commit();
    }

    SyncDetection sync(makeSyncDetectionConfig());
    EXPECT_EQ(sync.processBatchInLoopMode(BATCH_SIZE), true);

    auto txn = newTxn();

    auto idToDetection = common::byId(db::eye::DetectionGateway(*txn).load());
    EXPECT_EQ(idToDetection.size(), 4u);

    const auto detectionRelations = db::eye::DetectionRelationGateway(*txn).load();

    ASSERT_EQ(detectionRelations.size(), 2u);
    EXPECT_TRUE(detectionRelations.at(0).deleted());
    EXPECT_FALSE(detectionRelations.at(1).deleted());
    const auto& relation = detectionRelations.at(1);
    EXPECT_EQ(
        idToDetection.at(relation.masterDetectionId())
            .attrs<db::eye::DetectedSign>()
            .type,
        traffic_signs::TrafficSign::ProhibitoryNoParkingOrStopping);
    EXPECT_EQ(
        idToDetection.at(relation.slaveDetectionId())
            .attrs<db::eye::DetectedSign>()
            .type,
        traffic_signs::TrafficSign::InformationHeavyVehicle);
}

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