#include "fixtures.h"

#include <maps/wikimap/mapspro/services/mrc/eye/lib/common/include/id.h>
#include <maps/wikimap/mapspro/services/mrc/eye/lib/object_manager/impl/db.h>
#include <maps/wikimap/mapspro/services/mrc/eye/lib/object_manager/impl/location.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 <maps/wikimap/mapspro/services/mrc/libs/db/include/eye/verified_detection_pair_match_gateway.h>

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

#include <maps/libs/geolib/include/test_tools/comparison.h>

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

namespace {

void checkRelationsCount(
    pqxx::transaction_base& txn,
    size_t count)
{
    db::eye::PrimaryDetectionRelations relations
        = db::eye::PrimaryDetectionRelationGateway(txn).load();
    ASSERT_EQ(relations.size(), count);
}

struct RelationState {
    db::TId primaryId;
    db::TId detectionId;
    bool deleted;
};

void checkRelation(
    pqxx::transaction_base& txn,
    const RelationState& state)
{
    db::eye::PrimaryDetectionRelations testRelations
        = db::eye::PrimaryDetectionRelationGateway(txn).load(
            db::eye::table::PrimaryDetectionRelation::primaryDetectionId == state.primaryId
            and db::eye::table::PrimaryDetectionRelation::detectionId == state.detectionId
        );
    EXPECT_EQ(testRelations.size(), 1u);
    EXPECT_EQ(testRelations[0].deleted(), state.deleted);
}

} // namespace

struct DbFixture : public BaseFixture
{
    db::eye::Devices devices;
    db::eye::Frames frames;
    db::eye::FrameLocations frameLocations;
    db::eye::DetectionGroups groups;
    db::eye::Detections detections;
    db::eye::Objects objects;
    db::eye::ObjectLocations objectLocations;
    db::eye::PrimaryDetectionRelations relations;
};


struct NoObjectsFixture: public DbFixture {
    NoObjectsFixture() {
        auto txn = newTxn();

        devices = {
            {db::eye::MrcDeviceAttrs{"M1"}},
        };
        db::eye::DeviceGateway(*txn).insertx(devices);

        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"), {1200, 800}, time()},
            {devices[0].id(), identical, makeUrlContext(4, "4"), {1200, 800}, time()},
            {devices[0].id(), identical, makeUrlContext(5, "5"), {1200, 800}, time()},
            {devices[0].id(), identical, makeUrlContext(6, "6"), {1200, 800}, time()},
        };
        db::eye::FrameGateway(*txn).insertx(frames);

        frameLocations = {
            {frames[0].id(), geolib3::Point2{0, 0}, toRotation(geolib3::Heading(90), identical)},
            {frames[1].id(), geolib3::Point2{1, 0}, toRotation(geolib3::Heading(90), identical)},
            {frames[2].id(), geolib3::Point2{0, 2}, toRotation(geolib3::Heading(90), identical)},
            {frames[3].id(), geolib3::Point2{0, 2}, toRotation(geolib3::Heading(90), identical)},
            {frames[4].id(), geolib3::Point2{0, 0}, toRotation(geolib3::Heading(90), identical)},
            {frames[5].id(), geolib3::Point2{0, 0}, toRotation(geolib3::Heading(90), identical)},
        };
        db::eye::FrameLocationGateway(*txn).insertx(frameLocations);

        groups = {
            {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},
        };
        db::eye::DetectionGroupGateway(*txn).insertx(groups);

        detections = {
            {groups[0].id(), db::eye::DetectedHouseNumber{{0, 0, 10, 10}, 1.0, "12"}},
            {groups[1].id(), db::eye::DetectedHouseNumber{{0, 0, 20, 20}, 1.0, "12"}},
            {groups[2].id(), db::eye::DetectedHouseNumber{{0, 0, 10, 10}, 1.0, "12"}},
            {groups[3].id(), db::eye::DetectedHouseNumber{{0, 0, 70, 70}, 1.0, "12"}},
            {groups[4].id(), db::eye::DetectedHouseNumber{{0, 0, 10, 10}, 1.0, "12"}},
            {groups[5].id(), db::eye::DetectedHouseNumber{{0, 0, 60, 60}, 1.0, "12"}},
        };
        db::eye::DetectionGateway(*txn).insertx(detections);

        txn->commit();
    }
};

TEST_F(NoObjectsFixture, no_new_objects_empty_db)
{
    db::IdTo<db::TIdSet> detectionIdsByPrimaryId{}; // no new objects

    {
        auto txn = newTxn();

        saveObjects(*txn, detectionIdsByPrimaryId);

        txn->commit();
    }

    db::eye::Objects allObjects = db::eye::ObjectGateway(*newTxn()).load();
    ASSERT_EQ(allObjects.size(), 0u);
}

TEST_F(NoObjectsFixture, one_new_object_empty_db)
{
    DetectionStore detectionStore;
    detectionStore.extendByDetections(*newTxn(), detections);

    db::IdTo<db::TIdSet> detectionIdsByPrimaryId{
        {
            detections[0].id(),
            {detections[0].id(), detections[1].id(), detections[2].id()}
        }
    };

    {
        auto txn = newTxn();

        saveObjects(*txn, detectionIdsByPrimaryId);

        txn->commit();
    }

    db::eye::Objects allObjects = db::eye::ObjectGateway(*newTxn()).load();
    ASSERT_EQ(allObjects.size(), 1u);
    const db::eye::Object& object = allObjects[0];
    EXPECT_EQ(object.primaryDetectionId(), detections[0].id());

    checkRelationsCount(*newTxn(), 2u);
    checkRelation(*newTxn(), {.primaryId = detections[0].id(), .detectionId = detections[1].id(), .deleted = false});
    checkRelation(*newTxn(), {.primaryId = detections[0].id(), .detectionId = detections[2].id(), .deleted = false});

    db::eye::ObjectLocations allLocations
        = db::eye::ObjectLocationGateway(*newTxn()).load();
    ASSERT_EQ(allLocations.size(), 1u);
    const db::eye::ObjectLocation& location = allLocations[0];
    EXPECT_EQ(location.objectId(), object.id());

    Location expectedLocation = makeObjectLocation(
        detectionStore,
        detectionIdsByPrimaryId.at(detections[0].id())
    );

    EXPECT_TRUE(
        geolib3::test_tools::approximateEqual(
            location.mercatorPos(),
            expectedLocation.mercatorPosition,
            geolib3::EPS
        )
    );
}

struct OneObjectFixture: public DbFixture {
    OneObjectFixture() {
        auto txn = newTxn();

        devices = {
            {db::eye::MrcDeviceAttrs{"M1"}},
        };
        db::eye::DeviceGateway(*txn).insertx(devices);

        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"), {1200, 800}, time()},
            {devices[0].id(), identical, makeUrlContext(4, "4"), {1200, 800}, time()},
            {devices[0].id(), identical, makeUrlContext(5, "5"), {1200, 800}, time()},
            {devices[0].id(), identical, makeUrlContext(6, "6"), {1200, 800}, time()},
        };
        db::eye::FrameGateway(*txn).insertx(frames);

        frameLocations = {
            {frames[0].id(), geolib3::Point2{0, 0}, toRotation(geolib3::Heading(90), identical)},
            {frames[1].id(), geolib3::Point2{1, 0}, toRotation(geolib3::Heading(90), identical)},
            {frames[2].id(), geolib3::Point2{0, 2}, toRotation(geolib3::Heading(90), identical)},
            {frames[3].id(), geolib3::Point2{0, 2}, toRotation(geolib3::Heading(90), identical)},
            {frames[4].id(), geolib3::Point2{0, 0}, toRotation(geolib3::Heading(90), identical)},
            {frames[5].id(), geolib3::Point2{0, 0}, toRotation(geolib3::Heading(90), identical)},
        };
        db::eye::FrameLocationGateway(*txn).insertx(frameLocations);

        groups = {
            {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},
        };
        db::eye::DetectionGroupGateway(*txn).insertx(groups);

        detections = {
            {groups[0].id(), db::eye::DetectedHouseNumber{{0, 0, 10, 10}, 1.0, "12"}},
            {groups[1].id(), db::eye::DetectedHouseNumber{{0, 0, 20, 20}, 1.0, "12"}},
            {groups[2].id(), db::eye::DetectedHouseNumber{{0, 0, 10, 10}, 1.0, "12"}},
            {groups[3].id(), db::eye::DetectedHouseNumber{{0, 0, 70, 70}, 1.0, "12"}},
            {groups[4].id(), db::eye::DetectedHouseNumber{{0, 0, 10, 10}, 1.0, "12"}},
            {groups[5].id(), db::eye::DetectedHouseNumber{{0, 0, 60, 60}, 1.0, "12"}},
        };
        db::eye::DetectionGateway(*txn).insertx(detections);

        objects = {
            {detections[0].id(), db::eye::HouseNumberAttrs{"12"}},
        };
        db::eye::ObjectGateway(*txn).insertx(objects);

        relations = {
            {detections[0].id(), detections[1].id()},
            {detections[0].id(), detections[2].id()},
            {detections[0].id(), detections[3].id()},
        };
        db::eye::PrimaryDetectionRelationGateway(*txn).insertx(relations);

        objectLocations = {
            {objects[0].id(), geolib3::Point2{20, 0}, toRotation(geolib3::Heading(90), identical)},
        };
        db::eye::ObjectLocationGateway(*txn).insertx(objectLocations);

        txn->commit();
    }
};

TEST_F(OneObjectFixture, one_new_object_one_object_in_db)
{
    DetectionStore detectionStore;
    detectionStore.extendByDetections(*newTxn(), detections);

    Location oldObjectLocation
        = makeObjectLocation(
            detectionStore,
            db::TIdSet{detections[0].id(), detections[1].id(), detections[2].id(), detections[3].id()}
        );
    objectLocations[0].setMercatorPos(oldObjectLocation.mercatorPosition);
    objectLocations[0].setRotation(oldObjectLocation.rotation);
    {
        auto txn = newTxn();
        db::eye::ObjectLocationGateway(*txn).upsertx(objectLocations);
        txn->commit();
    }
    const db::TId oldObjectLocationTxnId = objectLocations[0].txnId();

    const db::IdTo<db::TIdSet> detectionIdsByPrimaryId{
        {
            detections[0].id(),
            {detections[0].id(), detections[1].id(), detections[2].id(), detections[3].id()}
        },
        {
            detections[4].id(),
            {detections[4].id(), detections[5].id()}
        }
    };


    {
        auto txn = newTxn();

        saveObjects(*txn, detectionIdsByPrimaryId);

        txn->commit();
    }

    db::eye::Objects allObjects = db::eye::ObjectGateway(*newTxn()).load();
    ASSERT_EQ(allObjects.size(), 2u);

    db::eye::Objects oldObjects = db::eye::ObjectGateway(*newTxn()).load(
        db::eye::table::Object::primaryDetectionId == detections[0].id()
    );
    ASSERT_EQ(oldObjects.size(), 1u);
    EXPECT_EQ(oldObjects[0].deleted(), false);

    db::eye::Objects newObjects = db::eye::ObjectGateway(*newTxn()).load(
        db::eye::table::Object::primaryDetectionId == detections[4].id()
    );
    ASSERT_EQ(newObjects.size(), 1u);
    EXPECT_EQ(newObjects[0].primaryDetectionId(), detections[4].id());
    EXPECT_EQ(newObjects[0].deleted(), false);

    checkRelationsCount(*newTxn(), 4u);
    checkRelation(*newTxn(), {.primaryId = detections[0].id(), .detectionId = detections[1].id(), .deleted = false});
    checkRelation(*newTxn(), {.primaryId = detections[0].id(), .detectionId = detections[2].id(), .deleted = false});
    checkRelation(*newTxn(), {.primaryId = detections[0].id(), .detectionId = detections[3].id(), .deleted = false});
    checkRelation(*newTxn(), {.primaryId = detections[4].id(), .detectionId = detections[5].id(), .deleted = false});

    db::eye::ObjectLocations allLocations
        = db::eye::ObjectLocationGateway(*newTxn()).load();
    ASSERT_EQ(allLocations.size(), 2u);

    db::eye::ObjectLocations testLocations;
    testLocations = db::eye::ObjectLocationGateway(*newTxn()).load(
        db::eye::table::ObjectLocation::objectId == oldObjects[0].id()
    );
    ASSERT_EQ(testLocations.size(), 1u);
    EXPECT_EQ(testLocations[0].objectId(), oldObjects[0].id());
    EXPECT_EQ(testLocations[0].txnId(), oldObjectLocationTxnId);
    EXPECT_TRUE(
        geolib3::test_tools::approximateEqual(
            testLocations[0].mercatorPos(),
            oldObjectLocation.mercatorPosition,
            geolib3::EPS
        )
    );


    testLocations = db::eye::ObjectLocationGateway(*newTxn()).load(
        db::eye::table::ObjectLocation::objectId == newObjects[0].id()
    );
    ASSERT_EQ(testLocations.size(), 1u);
    EXPECT_EQ(testLocations[0].objectId(), newObjects[0].id());
    Location expectedLocation = makeObjectLocation(
        detectionStore,
        detectionIdsByPrimaryId.at(detections[4].id())
    );

    EXPECT_TRUE(
        geolib3::test_tools::approximateEqual(
            testLocations[0].mercatorPos(),
            expectedLocation.mercatorPosition,
            geolib3::EPS
        )
    );
}

TEST_F(OneObjectFixture, split_object_one_object_in_db)
{
    DetectionStore detectionStore;
    detectionStore.extendByDetections(*newTxn(), detections);

    Location oldObjectLocation
        = makeObjectLocation(
            detectionStore,
            db::TIdSet{detections[0].id(), detections[1].id(), detections[2].id(), detections[3].id()}
        );
    objectLocations[0].setMercatorPos(oldObjectLocation.mercatorPosition);
    objectLocations[0].setRotation(oldObjectLocation.rotation);
    {
        auto txn = newTxn();
        db::eye::ObjectLocationGateway(*txn).upsertx(objectLocations);
        txn->commit();
    }
    const db::TId oldObjectLocationTxnId = objectLocations[0].txnId();

    const db::IdTo<db::TIdSet> detectionIdsByPrimaryId{
        {
            detections[0].id(),
            {detections[0].id(), detections[1].id()}
        },
        {
            detections[2].id(),
            {detections[2].id(), detections[3].id()}
        },
    };

    {
        auto txn = newTxn();

        saveObjects(*txn, detectionIdsByPrimaryId);

        txn->commit();
    }

    db::eye::Objects allObjects = db::eye::ObjectGateway(*newTxn()).load();
    ASSERT_EQ(allObjects.size(), 2u);

    db::eye::Objects oldObjects = db::eye::ObjectGateway(*newTxn()).load(
        db::eye::table::Object::primaryDetectionId == detections[0].id()
    );
    ASSERT_EQ(oldObjects.size(), 1u);
    EXPECT_EQ(oldObjects[0].deleted(), false);

    db::eye::Objects newObjects = db::eye::ObjectGateway(*newTxn()).load(
        db::eye::table::Object::primaryDetectionId == detections[2].id()
    );
    ASSERT_EQ(newObjects.size(), 1u);
    EXPECT_EQ(newObjects[0].primaryDetectionId(), detections[2].id());
    EXPECT_EQ(newObjects[0].deleted(), false);

    checkRelationsCount(*newTxn(), 4u);
    checkRelation(*newTxn(), {.primaryId = detections[0].id(), .detectionId = detections[1].id(), .deleted = false});
    checkRelation(*newTxn(), {.primaryId = detections[0].id(), .detectionId = detections[2].id(), .deleted = true});
    checkRelation(*newTxn(), {.primaryId = detections[0].id(), .detectionId = detections[3].id(), .deleted = true});
    checkRelation(*newTxn(), {.primaryId = detections[2].id(), .detectionId = detections[3].id(), .deleted = false});

    db::eye::ObjectLocations allLocations
        = db::eye::ObjectLocationGateway(*newTxn()).load();
    ASSERT_EQ(allLocations.size(), 2u);

    db::eye::ObjectLocations testLocations;
    testLocations = db::eye::ObjectLocationGateway(*newTxn()).load(
        db::eye::table::ObjectLocation::objectId == oldObjects[0].id()
    );
    ASSERT_EQ(testLocations.size(), 1u);
    EXPECT_EQ(testLocations[0].objectId(), oldObjects[0].id());
    EXPECT_TRUE(testLocations[0].txnId() > oldObjectLocationTxnId);
    Location expectedLocation = makeObjectLocation(
        detectionStore,
        detectionIdsByPrimaryId.at(detections[0].id())
    );
    EXPECT_TRUE(
        geolib3::test_tools::approximateEqual(
            testLocations[0].mercatorPos(),
            expectedLocation.mercatorPosition,
            geolib3::EPS
        )
    );


    testLocations = db::eye::ObjectLocationGateway(*newTxn()).load(
        db::eye::table::ObjectLocation::objectId == newObjects[0].id()
    );
    ASSERT_EQ(testLocations.size(), 1u);
    EXPECT_EQ(testLocations[0].objectId(), newObjects[0].id());
    expectedLocation = makeObjectLocation(
        detectionStore,
        detectionIdsByPrimaryId.at(detections[2].id())
    );

    EXPECT_TRUE(
        geolib3::test_tools::approximateEqual(
            testLocations[0].mercatorPos(),
            expectedLocation.mercatorPosition,
            geolib3::EPS
        )
    );
}

TEST_F(OneObjectFixture, add_new_object_remove_relation_one_object_in_db)
{
    DetectionStore detectionStore;
    detectionStore.extendByDetections(*newTxn(), detections);

    Location oldObjectLocation
        = makeObjectLocation(
            detectionStore,
            db::TIdSet{detections[0].id(), detections[1].id(), detections[2].id(), detections[3].id()}
        );
    objectLocations[0].setMercatorPos(oldObjectLocation.mercatorPosition);
    objectLocations[0].setRotation(oldObjectLocation.rotation);
    {
        auto txn = newTxn();
        db::eye::ObjectLocationGateway(*txn).upsertx(objectLocations);
        txn->commit();
    }
    const db::TId oldObjectLocationTxnId = objectLocations[0].txnId();

    const db::IdTo<db::TIdSet> detectionIdsByPrimaryId{
        {
            detections[0].id(),
            {detections[0].id(), detections[1].id(), detections[2].id()}
        },
        {
            detections[4].id(),
            {detections[4].id(), detections[3].id()}
        },
    };

    {
        auto txn = newTxn();

        saveObjects(*txn, detectionIdsByPrimaryId);

        txn->commit();
    }

    db::eye::Objects allObjects = db::eye::ObjectGateway(*newTxn()).load();
    ASSERT_EQ(allObjects.size(), 2u);

    db::eye::Objects oldObjects = db::eye::ObjectGateway(*newTxn()).load(
        db::eye::table::Object::primaryDetectionId == detections[0].id()
    );
    ASSERT_EQ(oldObjects.size(), 1u);
    EXPECT_EQ(oldObjects[0].deleted(), false);

    db::eye::Objects newObjects = db::eye::ObjectGateway(*newTxn()).load(
        db::eye::table::Object::primaryDetectionId == detections[4].id()
    );
    ASSERT_EQ(newObjects.size(), 1u);
    EXPECT_EQ(newObjects[0].primaryDetectionId(), detections[4].id());
    EXPECT_EQ(newObjects[0].deleted(), false);

    checkRelationsCount(*newTxn(), 4u);
    checkRelation(*newTxn(), {.primaryId = detections[0].id(), .detectionId = detections[1].id(), .deleted = false});
    checkRelation(*newTxn(), {.primaryId = detections[0].id(), .detectionId = detections[2].id(), .deleted = false});
    checkRelation(*newTxn(), {.primaryId = detections[0].id(), .detectionId = detections[3].id(), .deleted = true});
    checkRelation(*newTxn(), {.primaryId = detections[4].id(), .detectionId = detections[3].id(), .deleted = false});

    db::eye::ObjectLocations allLocations
        = db::eye::ObjectLocationGateway(*newTxn()).load();
    ASSERT_EQ(allLocations.size(), 2u);

    db::eye::ObjectLocations testLocations;
    testLocations = db::eye::ObjectLocationGateway(*newTxn()).load(
        db::eye::table::ObjectLocation::objectId == oldObjects[0].id()
    );
    ASSERT_EQ(testLocations.size(), 1u);
    EXPECT_EQ(testLocations[0].objectId(), oldObjects[0].id());
    EXPECT_TRUE(testLocations[0].txnId() > oldObjectLocationTxnId);
    Location expectedLocation = makeObjectLocation(
        detectionStore,
        detectionIdsByPrimaryId.at(detections[0].id())
    );
    EXPECT_TRUE(
        geolib3::test_tools::approximateEqual(
            testLocations[0].mercatorPos(),
            expectedLocation.mercatorPosition,
            geolib3::EPS
        )
    );


    testLocations = db::eye::ObjectLocationGateway(*newTxn()).load(
        db::eye::table::ObjectLocation::objectId == newObjects[0].id()
    );
    ASSERT_EQ(testLocations.size(), 1u);
    EXPECT_EQ(testLocations[0].objectId(), newObjects[0].id());
    expectedLocation = makeObjectLocation(
        detectionStore,
        detectionIdsByPrimaryId.at(detections[4].id())
    );

    EXPECT_TRUE(
        geolib3::test_tools::approximateEqual(
            testLocations[0].mercatorPos(),
            expectedLocation.mercatorPosition,
            geolib3::EPS
        )
    );
}

TEST_F(OneObjectFixture, absorb_old_object_one_object_in_db)
{
    DetectionStore detectionStore;
    detectionStore.extendByDetections(*newTxn(), detections);

    Location oldObjectLocation
        = makeObjectLocation(
            detectionStore,
            db::TIdSet{detections[0].id(), detections[1].id(), detections[2].id(), detections[3].id()}
        );
    objectLocations[0].setMercatorPos(oldObjectLocation.mercatorPosition);
    objectLocations[0].setRotation(oldObjectLocation.rotation);
    {
        auto txn = newTxn();
        db::eye::ObjectLocationGateway(*txn).upsertx(objectLocations);
        txn->commit();
    }
    const db::TId oldObjectLocationTxnId = objectLocations[0].txnId();

    const db::IdTo<db::TIdSet> detectionIdsByPrimaryId{
        {
            detections[4].id(),
            {
                detections[0].id(),
                detections[1].id(),
                detections[2].id(),
                detections[3].id(),
                detections[4].id()
            }
        },
    };

    {
        auto txn = newTxn();

        saveObjects(*txn, detectionIdsByPrimaryId);

        txn->commit();
    }

    db::eye::Objects allObjects = db::eye::ObjectGateway(*newTxn()).load();
    ASSERT_EQ(allObjects.size(), 2u);

    db::eye::Objects oldObjects = db::eye::ObjectGateway(*newTxn()).load(
        db::eye::table::Object::primaryDetectionId == detections[0].id()
    );
    ASSERT_EQ(oldObjects.size(), 1u);
    EXPECT_EQ(oldObjects[0].deleted(), true);

    db::eye::Objects newObjects = db::eye::ObjectGateway(*newTxn()).load(
        db::eye::table::Object::primaryDetectionId == detections[4].id()
    );
    ASSERT_EQ(newObjects.size(), 1u);
    EXPECT_EQ(newObjects[0].primaryDetectionId(), detections[4].id());
    EXPECT_EQ(newObjects[0].deleted(), false);

    checkRelationsCount(*newTxn(), 7u);
    checkRelation(*newTxn(), {.primaryId = detections[0].id(), .detectionId = detections[1].id(), .deleted = true});
    checkRelation(*newTxn(), {.primaryId = detections[0].id(), .detectionId = detections[2].id(), .deleted = true});
    checkRelation(*newTxn(), {.primaryId = detections[0].id(), .detectionId = detections[3].id(), .deleted = true});
    checkRelation(*newTxn(), {.primaryId = detections[4].id(), .detectionId = detections[0].id(), .deleted = false});
    checkRelation(*newTxn(), {.primaryId = detections[4].id(), .detectionId = detections[1].id(), .deleted = false});
    checkRelation(*newTxn(), {.primaryId = detections[4].id(), .detectionId = detections[2].id(), .deleted = false});
    checkRelation(*newTxn(), {.primaryId = detections[4].id(), .detectionId = detections[3].id(), .deleted = false});

    db::eye::ObjectLocations allLocations
        = db::eye::ObjectLocationGateway(*newTxn()).load();
    ASSERT_EQ(allLocations.size(), 2u);

    db::eye::ObjectLocations testLocations;
    testLocations = db::eye::ObjectLocationGateway(*newTxn()).load(
        db::eye::table::ObjectLocation::objectId == oldObjects[0].id()
    );
    ASSERT_EQ(testLocations.size(), 1u);
    EXPECT_EQ(testLocations[0].objectId(), oldObjects[0].id());
    EXPECT_EQ(testLocations[0].txnId(), oldObjectLocationTxnId);


    testLocations = db::eye::ObjectLocationGateway(*newTxn()).load(
        db::eye::table::ObjectLocation::objectId == newObjects[0].id()
    );
    ASSERT_EQ(testLocations.size(), 1u);
    EXPECT_EQ(testLocations[0].objectId(), newObjects[0].id());
    Location expectedLocation = makeObjectLocation(
        detectionStore,
        detectionIdsByPrimaryId.at(detections[4].id())
    );

    EXPECT_TRUE(
        geolib3::test_tools::approximateEqual(
            testLocations[0].mercatorPos(),
            expectedLocation.mercatorPosition,
            geolib3::EPS
        )
    );
}

struct TwoObjectFixture: public DbFixture {
    TwoObjectFixture() {
        auto txn = newTxn();

        devices = {
            {db::eye::MrcDeviceAttrs{"M1"}},
        };
        db::eye::DeviceGateway(*txn).insertx(devices);

        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"), {1200, 800}, time()},
            {devices[0].id(), identical, makeUrlContext(4, "4"), {1200, 800}, time()},
            {devices[0].id(), identical, makeUrlContext(5, "5"), {1200, 800}, time()},
            {devices[0].id(), identical, makeUrlContext(6, "6"), {1200, 800}, time()},
        };
        db::eye::FrameGateway(*txn).insertx(frames);

        frameLocations = {
            {frames[0].id(), geolib3::Point2{0, 0}, toRotation(geolib3::Heading(90), identical)},
            {frames[1].id(), geolib3::Point2{1, 0}, toRotation(geolib3::Heading(90), identical)},
            {frames[2].id(), geolib3::Point2{0, 2}, toRotation(geolib3::Heading(90), identical)},
            {frames[3].id(), geolib3::Point2{0, 2}, toRotation(geolib3::Heading(90), identical)},
            {frames[4].id(), geolib3::Point2{0, 50}, toRotation(geolib3::Heading(90), identical)},
            {frames[5].id(), geolib3::Point2{50, 0}, toRotation(geolib3::Heading(90), identical)},
        };
        db::eye::FrameLocationGateway(*txn).insertx(frameLocations);

        groups = {
            {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},
        };
        db::eye::DetectionGroupGateway(*txn).insertx(groups);

        detections = {
            {groups[0].id(), db::eye::DetectedHouseNumber{{0, 0, 10, 10}, 1.0, "12"}},
            {groups[1].id(), db::eye::DetectedHouseNumber{{0, 0, 20, 20}, 1.0, "12"}},
            {groups[2].id(), db::eye::DetectedHouseNumber{{0, 0, 10, 10}, 1.0, "12"}},
            {groups[3].id(), db::eye::DetectedHouseNumber{{0, 0, 70, 70}, 1.0, "12"}},
            {groups[4].id(), db::eye::DetectedHouseNumber{{0, 0, 10, 10}, 1.0, "12"}},
            {groups[5].id(), db::eye::DetectedHouseNumber{{0, 0, 60, 60}, 1.0, "12"}},
        };
        db::eye::DetectionGateway(*txn).insertx(detections);

        objects = {
            {detections[0].id(), db::eye::HouseNumberAttrs{"12"}},
            {detections[4].id(), db::eye::HouseNumberAttrs{"12"}},
        };
        db::eye::ObjectGateway(*txn).insertx(objects);

        relations = {
            {detections[0].id(), detections[1].id()},
            {detections[0].id(), detections[2].id()},
            {detections[0].id(), detections[3].id()},
            {detections[4].id(), detections[5].id()},
        };
        db::eye::PrimaryDetectionRelationGateway(*txn).insertx(relations);

        objectLocations = {
            {objects[0].id(), geolib3::Point2{0, 0}, toRotation(geolib3::Heading(90), identical)},
            {objects[1].id(), geolib3::Point2{0, 0}, toRotation(geolib3::Heading(90), identical)},
        };
        db::eye::ObjectLocationGateway(*txn).insertx(objectLocations);

        txn->commit();
    }
};

TEST_F(TwoObjectFixture, merge_objects_two_object_in_db)
{
    DetectionStore detectionStore;
    detectionStore.extendByDetections(*newTxn(), detections);

    Location oldObjectLocation1
        = makeObjectLocation(
            detectionStore,
            db::TIdSet{detections[0].id(), detections[1].id(), detections[2].id(), detections[3].id()}
        );
    objectLocations[0].setMercatorPos(oldObjectLocation1.mercatorPosition);
    objectLocations[0].setRotation(oldObjectLocation1.rotation);
    Location oldObjectLocation2
        = makeObjectLocation(
            detectionStore,
            db::TIdSet{detections[4].id(), detections[5].id()}
        );
    objectLocations[1].setMercatorPos(oldObjectLocation2.mercatorPosition);
    objectLocations[1].setRotation(oldObjectLocation2.rotation);
    {
        auto txn = newTxn();
        db::eye::ObjectLocationGateway(*txn).upsertx(objectLocations);
        txn->commit();
    }
    const db::TId oldObjectLocationTxnId1 = objectLocations[0].txnId();
    const db::TId oldObjectLocationTxnId2 = objectLocations[1].txnId();

    const db::IdTo<db::TIdSet> detectionIdsByPrimaryId{
        {
            detections[0].id(),
            {
                detections[0].id(),
                detections[1].id(),
                detections[2].id(),
                detections[3].id(),
                detections[4].id(),
                detections[5].id(),
            }
        },
    };

    {
        auto txn = newTxn();

        saveObjects(*txn, detectionIdsByPrimaryId);

        txn->commit();
    }

    db::eye::Objects allObjects = db::eye::ObjectGateway(*newTxn()).load();
    ASSERT_EQ(allObjects.size(), 2u);

    db::eye::Objects oldObjects1 = db::eye::ObjectGateway(*newTxn()).load(
        db::eye::table::Object::primaryDetectionId == detections[0].id()
    );
    ASSERT_EQ(oldObjects1.size(), 1u);
    EXPECT_EQ(oldObjects1[0].primaryDetectionId(), detections[0].id());
    EXPECT_EQ(oldObjects1[0].deleted(), false);

    db::eye::Objects oldObjects2 = db::eye::ObjectGateway(*newTxn()).load(
        db::eye::table::Object::primaryDetectionId == detections[4].id()
    );
    ASSERT_EQ(oldObjects2.size(), 1u);
    EXPECT_EQ(oldObjects2[0].primaryDetectionId(), detections[4].id());
    EXPECT_EQ(oldObjects2[0].deleted(), true);

    checkRelationsCount(*newTxn(), 6u);
    checkRelation(*newTxn(), {.primaryId = detections[0].id(), .detectionId = detections[1].id(), .deleted = false});
    checkRelation(*newTxn(), {.primaryId = detections[0].id(), .detectionId = detections[2].id(), .deleted = false});
    checkRelation(*newTxn(), {.primaryId = detections[0].id(), .detectionId = detections[3].id(), .deleted = false});
    checkRelation(*newTxn(), {.primaryId = detections[4].id(), .detectionId = detections[5].id(), .deleted = true});
    checkRelation(*newTxn(), {.primaryId = detections[0].id(), .detectionId = detections[4].id(), .deleted = false});
    checkRelation(*newTxn(), {.primaryId = detections[0].id(), .detectionId = detections[5].id(), .deleted = false});

    db::eye::ObjectLocations allLocations
        = db::eye::ObjectLocationGateway(*newTxn()).load();
    ASSERT_EQ(allLocations.size(), 2u);

    db::eye::ObjectLocations testLocations;
    testLocations = db::eye::ObjectLocationGateway(*newTxn()).load(
        db::eye::table::ObjectLocation::objectId == oldObjects1[0].id()
    );
    ASSERT_EQ(testLocations.size(), 1u);
    EXPECT_EQ(testLocations[0].objectId(), oldObjects1[0].id());
    EXPECT_EQ(testLocations[0].txnId(), oldObjectLocationTxnId1);
    Location expectedLocation = makeObjectLocation(
        detectionStore,
        detectionIdsByPrimaryId.at(detections[0].id())
    );
    EXPECT_TRUE(
        geolib3::test_tools::approximateEqual(
            testLocations[0].mercatorPos(),
            expectedLocation.mercatorPosition,
            geolib3::EPS
        )
    );

    testLocations = db::eye::ObjectLocationGateway(*newTxn()).load(
        db::eye::table::ObjectLocation::objectId == oldObjects2[0].id()
    );
    ASSERT_EQ(testLocations.size(), 1u);
    EXPECT_EQ(testLocations[0].objectId(), oldObjects2[0].id());
    EXPECT_EQ(testLocations[0].txnId(), oldObjectLocationTxnId2);
}

TEST_F(TwoObjectFixture, mix_objects_two_object_in_db)
{
    DetectionStore detectionStore;
    detectionStore.extendByDetections(*newTxn(), detections);

    Location oldObjectLocation1
        = makeObjectLocation(
            detectionStore,
            db::TIdSet{detections[0].id(), detections[1].id(), detections[2].id(), detections[3].id()}
        );
    objectLocations[0].setMercatorPos(oldObjectLocation1.mercatorPosition);
    objectLocations[0].setRotation(oldObjectLocation1.rotation);
    Location oldObjectLocation2
        = makeObjectLocation(
            detectionStore,
            db::TIdSet{detections[4].id(), detections[5].id()}
        );
    objectLocations[1].setMercatorPos(oldObjectLocation2.mercatorPosition);
    objectLocations[1].setRotation(oldObjectLocation2.rotation);
    {
        auto txn = newTxn();
        db::eye::ObjectLocationGateway(*txn).upsertx(objectLocations);
        txn->commit();
    }
    const db::TId oldObjectLocationTxnId1 = objectLocations[0].txnId();
    const db::TId oldObjectLocationTxnId2 = objectLocations[1].txnId();

    const db::IdTo<db::TIdSet> detectionIdsByPrimaryId{
        {
            detections[0].id(),
            {
                detections[0].id(),
                detections[1].id(),
                detections[2].id(),
                detections[5].id(),
            }
        },
        {
            detections[4].id(),
            {
                detections[4].id(),
                detections[3].id()
            }
        },
    };

    {
        auto txn = newTxn();

        saveObjects(*txn, detectionIdsByPrimaryId);

        txn->commit();
    }

    db::eye::Objects allObjects = db::eye::ObjectGateway(*newTxn()).load();
    ASSERT_EQ(allObjects.size(), 2u);

    db::eye::Objects oldObjects1 = db::eye::ObjectGateway(*newTxn()).load(
        db::eye::table::Object::primaryDetectionId == detections[0].id()
    );
    ASSERT_EQ(oldObjects1.size(), 1u);
    EXPECT_EQ(oldObjects1[0].primaryDetectionId(), detections[0].id());
    EXPECT_EQ(oldObjects1[0].deleted(), false);

    db::eye::Objects oldObjects2 = db::eye::ObjectGateway(*newTxn()).load(
        db::eye::table::Object::primaryDetectionId == detections[4].id()
    );
    ASSERT_EQ(oldObjects2.size(), 1u);
    EXPECT_EQ(oldObjects2[0].primaryDetectionId(), detections[4].id());
    EXPECT_EQ(oldObjects2[0].deleted(), false);

    checkRelationsCount(*newTxn(), 6u);
    checkRelation(*newTxn(), {.primaryId = detections[0].id(), .detectionId = detections[1].id(), .deleted = false});
    checkRelation(*newTxn(), {.primaryId = detections[0].id(), .detectionId = detections[2].id(), .deleted = false});
    checkRelation(*newTxn(), {.primaryId = detections[0].id(), .detectionId = detections[3].id(), .deleted = true});
    checkRelation(*newTxn(), {.primaryId = detections[4].id(), .detectionId = detections[5].id(), .deleted = true});
    checkRelation(*newTxn(), {.primaryId = detections[4].id(), .detectionId = detections[3].id(), .deleted = false});
    checkRelation(*newTxn(), {.primaryId = detections[0].id(), .detectionId = detections[5].id(), .deleted = false});

    db::eye::ObjectLocations allLocations
        = db::eye::ObjectLocationGateway(*newTxn()).load();
    ASSERT_EQ(allLocations.size(), 2u);

    db::eye::ObjectLocations testLocations;
    testLocations = db::eye::ObjectLocationGateway(*newTxn()).load(
        db::eye::table::ObjectLocation::objectId == oldObjects1[0].id()
    );
    ASSERT_EQ(testLocations.size(), 1u);
    EXPECT_EQ(testLocations[0].objectId(), oldObjects1[0].id());
    EXPECT_TRUE(testLocations[0].txnId() > oldObjectLocationTxnId1);
    Location expectedLocation = makeObjectLocation(
        detectionStore,
        detectionIdsByPrimaryId.at(detections[0].id())
    );
    EXPECT_TRUE(
        geolib3::test_tools::approximateEqual(
            testLocations[0].mercatorPos(),
            expectedLocation.mercatorPosition,
            geolib3::EPS
        )
    );

    testLocations = db::eye::ObjectLocationGateway(*newTxn()).load(
        db::eye::table::ObjectLocation::objectId == oldObjects2[0].id()
    );
    ASSERT_EQ(testLocations.size(), 1u);
    EXPECT_EQ(testLocations[0].objectId(), oldObjects2[0].id());
    EXPECT_TRUE(testLocations[0].txnId() > oldObjectLocationTxnId2);
    expectedLocation = makeObjectLocation(
        detectionStore,
        detectionIdsByPrimaryId.at(detections[4].id())
    );
    EXPECT_TRUE(
        geolib3::test_tools::approximateEqual(
            testLocations[0].mercatorPos(),
            expectedLocation.mercatorPosition,
            geolib3::EPS
        )
    );
}

TEST_F(TwoObjectFixture, split_objects_change_primary_two_object_in_db)
{
    DetectionStore detectionStore;
    detectionStore.extendByDetections(*newTxn(), detections);

    Location oldObjectLocation1
        = makeObjectLocation(
            detectionStore,
            db::TIdSet{detections[0].id(), detections[1].id(), detections[2].id(), detections[3].id()}
        );
    objectLocations[0].setMercatorPos(oldObjectLocation1.mercatorPosition);
    objectLocations[0].setRotation(oldObjectLocation1.rotation);
    Location oldObjectLocation2
        = makeObjectLocation(
            detectionStore,
            db::TIdSet{detections[4].id(), detections[5].id()}
        );
    objectLocations[1].setMercatorPos(oldObjectLocation2.mercatorPosition);
    objectLocations[1].setRotation(oldObjectLocation2.rotation);
    {
        auto txn = newTxn();
        db::eye::ObjectLocationGateway(*txn).upsertx(objectLocations);
        txn->commit();
    }
    const db::TId oldObjectLocationTxnId1 = objectLocations[0].txnId();
    const db::TId oldObjectLocationTxnId2 = objectLocations[1].txnId();

    const db::IdTo<db::TIdSet> detectionIdsByPrimaryId{
        {
            detections[1].id(),
            {
                detections[1].id(),
                detections[2].id(),
                detections[4].id(),
            }
        },
        {
            detections[3].id(),
            {
                detections[0].id(),
                detections[3].id(),
            }
        },
        {
            detections[5].id(),
            {
                detections[5].id(),
            }
        },
    };

    {
        auto txn = newTxn();

        saveObjects(*txn, detectionIdsByPrimaryId);

        txn->commit();
    }

    db::eye::Objects allObjects = db::eye::ObjectGateway(*newTxn()).load();
    ASSERT_EQ(allObjects.size(), 5u);

    db::eye::Objects oldObjects1 = db::eye::ObjectGateway(*newTxn()).load(
        db::eye::table::Object::primaryDetectionId == detections[0].id()
    );
    ASSERT_EQ(oldObjects1.size(), 1u);
    EXPECT_EQ(oldObjects1[0].primaryDetectionId(), detections[0].id());
    EXPECT_EQ(oldObjects1[0].deleted(), true);

    db::eye::Objects oldObjects2 = db::eye::ObjectGateway(*newTxn()).load(
        db::eye::table::Object::primaryDetectionId == detections[4].id()
    );
    ASSERT_EQ(oldObjects2.size(), 1u);
    EXPECT_EQ(oldObjects2[0].primaryDetectionId(), detections[4].id());
    EXPECT_EQ(oldObjects2[0].deleted(), true);

    db::eye::Objects newObjects1 = db::eye::ObjectGateway(*newTxn()).load(
        db::eye::table::Object::primaryDetectionId == detections[1].id()
    );
    ASSERT_EQ(newObjects1.size(), 1u);
    EXPECT_EQ(newObjects1[0].primaryDetectionId(), detections[1].id());
    EXPECT_EQ(newObjects1[0].deleted(), false);

    db::eye::Objects newObjects2 = db::eye::ObjectGateway(*newTxn()).load(
        db::eye::table::Object::primaryDetectionId == detections[3].id()
    );
    ASSERT_EQ(newObjects2.size(), 1u);
    EXPECT_EQ(newObjects2[0].primaryDetectionId(), detections[3].id());
    EXPECT_EQ(newObjects2[0].deleted(), false);

    db::eye::Objects newObjects3 = db::eye::ObjectGateway(*newTxn()).load(
        db::eye::table::Object::primaryDetectionId == detections[5].id()
    );
    ASSERT_EQ(newObjects3.size(), 1u);
    EXPECT_EQ(newObjects3[0].primaryDetectionId(), detections[5].id());
    EXPECT_EQ(newObjects3[0].deleted(), false);

    checkRelationsCount(*newTxn(), 7u);

    checkRelation(*newTxn(), {.primaryId = detections[0].id(), .detectionId = detections[1].id(), .deleted = true});
    checkRelation(*newTxn(), {.primaryId = detections[0].id(), .detectionId = detections[2].id(), .deleted = true});
    checkRelation(*newTxn(), {.primaryId = detections[0].id(), .detectionId = detections[3].id(), .deleted = true});
    checkRelation(*newTxn(), {.primaryId = detections[4].id(), .detectionId = detections[5].id(), .deleted = true});
    checkRelation(*newTxn(), {.primaryId = detections[1].id(), .detectionId = detections[2].id(), .deleted = false});
    checkRelation(*newTxn(), {.primaryId = detections[1].id(), .detectionId = detections[4].id(), .deleted = false});
    checkRelation(*newTxn(), {.primaryId = detections[3].id(), .detectionId = detections[0].id(), .deleted = false});

    db::eye::ObjectLocations allLocations
        = db::eye::ObjectLocationGateway(*newTxn()).load();
    ASSERT_EQ(allLocations.size(), 5u);

    db::eye::ObjectLocations testLocations;
    testLocations = db::eye::ObjectLocationGateway(*newTxn()).load(
        db::eye::table::ObjectLocation::objectId == oldObjects1[0].id()
    );
    ASSERT_EQ(testLocations.size(), 1u);
    EXPECT_EQ(testLocations[0].objectId(), oldObjects1[0].id());
    EXPECT_EQ(testLocations[0].txnId(), oldObjectLocationTxnId1);

    testLocations = db::eye::ObjectLocationGateway(*newTxn()).load(
        db::eye::table::ObjectLocation::objectId == oldObjects2[0].id()
    );
    ASSERT_EQ(testLocations.size(), 1u);
    EXPECT_EQ(testLocations[0].objectId(), oldObjects2[0].id());
    EXPECT_EQ(testLocations[0].txnId(), oldObjectLocationTxnId2);


    testLocations = db::eye::ObjectLocationGateway(*newTxn()).load(
        db::eye::table::ObjectLocation::objectId == newObjects1[0].id()
    );
    ASSERT_EQ(testLocations.size(), 1u);
    EXPECT_EQ(testLocations[0].objectId(), newObjects1[0].id());
    Location expectedLocation = makeObjectLocation(
        detectionStore,
        detectionIdsByPrimaryId.at(detections[1].id())
    );
    EXPECT_TRUE(
        geolib3::test_tools::approximateEqual(
            testLocations[0].mercatorPos(),
            expectedLocation.mercatorPosition,
            geolib3::EPS
        )
    );

    testLocations = db::eye::ObjectLocationGateway(*newTxn()).load(
        db::eye::table::ObjectLocation::objectId == newObjects2[0].id()
    );
    ASSERT_EQ(testLocations.size(), 1u);
    EXPECT_EQ(testLocations[0].objectId(), newObjects2[0].id());
    expectedLocation = makeObjectLocation(
        detectionStore,
        detectionIdsByPrimaryId.at(detections[3].id())
    );
    EXPECT_TRUE(
        geolib3::test_tools::approximateEqual(
            testLocations[0].mercatorPos(),
            expectedLocation.mercatorPosition,
            geolib3::EPS
        )
    );

    testLocations = db::eye::ObjectLocationGateway(*newTxn()).load(
        db::eye::table::ObjectLocation::objectId == newObjects3[0].id()
    );
    ASSERT_EQ(testLocations.size(), 1u);
    EXPECT_EQ(testLocations[0].objectId(), newObjects3[0].id());
    expectedLocation = makeObjectLocation(
        detectionStore,
        detectionIdsByPrimaryId.at(detections[5].id())
    );
    EXPECT_TRUE(
        geolib3::test_tools::approximateEqual(
            testLocations[0].mercatorPos(),
            expectedLocation.mercatorPosition,
            geolib3::EPS
        )
    );
}

struct DeletedObjectsAndRelationsFixture: public DbFixture {
    DeletedObjectsAndRelationsFixture() {
        auto txn = newTxn();

        devices = {
            {db::eye::MrcDeviceAttrs{"M1"}},
        };
        db::eye::DeviceGateway(*txn).insertx(devices);

        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"), {1200, 800}, time()},
            {devices[0].id(), identical, makeUrlContext(4, "4"), {1200, 800}, time()},
            {devices[0].id(), identical, makeUrlContext(5, "5"), {1200, 800}, time()},
            {devices[0].id(), identical, makeUrlContext(6, "6"), {1200, 800}, time()},
        };
        db::eye::FrameGateway(*txn).insertx(frames);

        frameLocations = {
            {frames[0].id(), geolib3::Point2{0, 0}, toRotation(geolib3::Heading(90), identical)},
            {frames[1].id(), geolib3::Point2{1, 0}, toRotation(geolib3::Heading(90), identical)},
            {frames[2].id(), geolib3::Point2{0, 2}, toRotation(geolib3::Heading(90), identical)},
            {frames[3].id(), geolib3::Point2{0, 2}, toRotation(geolib3::Heading(90), identical)},
            {frames[4].id(), geolib3::Point2{0, 50}, toRotation(geolib3::Heading(90), identical)},
            {frames[5].id(), geolib3::Point2{50, 0}, toRotation(geolib3::Heading(90), identical)},
        };
        db::eye::FrameLocationGateway(*txn).insertx(frameLocations);

        groups = {
            {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},
        };
        db::eye::DetectionGroupGateway(*txn).insertx(groups);

        detections = {
            {groups[0].id(), db::eye::DetectedHouseNumber{{0, 0, 10, 10}, 1.0, "12"}},
            {groups[1].id(), db::eye::DetectedHouseNumber{{0, 0, 20, 20}, 1.0, "12"}},
            {groups[2].id(), db::eye::DetectedHouseNumber{{0, 0, 10, 10}, 1.0, "12"}},
            {groups[3].id(), db::eye::DetectedHouseNumber{{0, 0, 70, 70}, 1.0, "12"}},
            {groups[4].id(), db::eye::DetectedHouseNumber{{0, 0, 15, 15}, 1.0, "12"}},
            {groups[5].id(), db::eye::DetectedHouseNumber{{0, 0, 60, 60}, 1.0, "12"}},
        };
        db::eye::DetectionGateway(*txn).insertx(detections);

        objects = {
            {detections[0].id(), db::eye::HouseNumberAttrs{"12"}},
            {detections[4].id(), db::eye::HouseNumberAttrs{"12"}},
            {detections[5].id(), db::eye::HouseNumberAttrs{"12"}},
        };
        objects[1].setDeleted(true);
        objects[2].setDeleted(true);
        db::eye::ObjectGateway(*txn).insertx(objects);

        relations = {
            {detections[0].id(), detections[1].id()},
            {detections[0].id(), detections[2].id()},
            {detections[0].id(), detections[3].id()},
            {detections[0].id(), detections[4].id()},
            {detections[4].id(), detections[0].id()},
            {detections[4].id(), detections[1].id()}
        };
        relations[4].setDeleted(true);
        relations[5].setDeleted(true);
        db::eye::PrimaryDetectionRelationGateway(*txn).insertx(relations);

        objectLocations = {
            {objects[0].id(), geolib3::Point2{0, 0}, toRotation(geolib3::Heading(90), identical)},
            {objects[1].id(), geolib3::Point2{0, 0}, toRotation(geolib3::Heading(90), identical)},
            {objects[2].id(), geolib3::Point2{0, 0}, toRotation(geolib3::Heading(90), identical)},
        };
        db::eye::ObjectLocationGateway(*txn).insertx(objectLocations);

        txn->commit();
    }
};

TEST_F(DeletedObjectsAndRelationsFixture, restore_objects_and_relations_deleted_objects_and_relations_in_db)
{
    DetectionStore detectionStore;
    detectionStore.extendByDetections(*newTxn(), detections);

    Location oldObjectLocation1
        = makeObjectLocation(
            detectionStore,
            db::TIdSet{
                detections[0].id(),
                detections[1].id(),
                detections[2].id(),
                detections[3].id(),
                detections[4].id()
            }
        );
    objectLocations[0].setMercatorPos(oldObjectLocation1.mercatorPosition);
    objectLocations[0].setRotation(oldObjectLocation1.rotation);
    Location oldObjectLocation2
        = makeObjectLocation(
            detectionStore,
            db::TIdSet{detections[4].id(), detections[0].id(), detections[1].id()}
        );
    objectLocations[1].setMercatorPos(oldObjectLocation2.mercatorPosition);
    objectLocations[1].setRotation(oldObjectLocation2.rotation);
    Location oldObjectLocation3
        = makeObjectLocation(
            detectionStore,
            db::TIdSet{detections[5].id()}
        );
    objectLocations[2].setMercatorPos(oldObjectLocation3.mercatorPosition);
    objectLocations[2].setRotation(oldObjectLocation3.rotation);

    {
        auto txn = newTxn();
        db::eye::ObjectLocationGateway(*txn).upsertx(objectLocations);
        txn->commit();
    }
    const db::TId oldObjectLocationTxnId1 = objectLocations[0].txnId();
    const db::TId oldObjectLocationTxnId2 = objectLocations[1].txnId();
    const db::TId oldObjectLocationTxnId3 = objectLocations[2].txnId();

    const db::IdTo<db::TIdSet> detectionIdsByPrimaryId{
        {
            detections[1].id(),
            {
                detections[1].id(),
                detections[2].id(),
            }
        },
        {
            detections[4].id(),
            {
                detections[0].id(),
                detections[4].id(),
            }
        },
        {
            detections[5].id(),
            {
                detections[5].id(),
                detections[3].id()
            }
        },
    };

    {
        auto txn = newTxn();

        saveObjects(*txn, detectionIdsByPrimaryId);

        txn->commit();
    }

    db::eye::Objects allObjects = db::eye::ObjectGateway(*newTxn()).load();
    ASSERT_EQ(allObjects.size(), 4u);

    db::eye::Objects oldObjects = db::eye::ObjectGateway(*newTxn()).load(
        db::eye::table::Object::primaryDetectionId == detections[0].id()
    );
    ASSERT_EQ(oldObjects.size(), 1u);
    EXPECT_EQ(oldObjects[0].primaryDetectionId(), detections[0].id());
    EXPECT_EQ(oldObjects[0].deleted(), true);

    db::eye::Objects newObjects1 = db::eye::ObjectGateway(*newTxn()).load(
        db::eye::table::Object::primaryDetectionId == detections[1].id()
    );
    ASSERT_EQ(newObjects1.size(), 1u);
    EXPECT_EQ(newObjects1[0].primaryDetectionId(), detections[1].id());
    EXPECT_EQ(newObjects1[0].deleted(), false);

    db::eye::Objects newObjects2 = db::eye::ObjectGateway(*newTxn()).load(
        db::eye::table::Object::primaryDetectionId == detections[4].id()
    );
    ASSERT_EQ(newObjects2.size(), 1u);
    EXPECT_EQ(newObjects2[0].primaryDetectionId(), detections[4].id());
    EXPECT_EQ(newObjects2[0].deleted(), false);

    db::eye::Objects newObjects3 = db::eye::ObjectGateway(*newTxn()).load(
        db::eye::table::Object::primaryDetectionId == detections[5].id()
    );
    ASSERT_EQ(newObjects3.size(), 1u);
    EXPECT_EQ(newObjects3[0].primaryDetectionId(), detections[5].id());
    EXPECT_EQ(newObjects3[0].deleted(), false);


    checkRelationsCount(*newTxn(), 8u);

    checkRelation(*newTxn(), {.primaryId = detections[0].id(), .detectionId = detections[1].id(), .deleted = true});
    checkRelation(*newTxn(), {.primaryId = detections[0].id(), .detectionId = detections[2].id(), .deleted = true});
    checkRelation(*newTxn(), {.primaryId = detections[0].id(), .detectionId = detections[3].id(), .deleted = true});
    checkRelation(*newTxn(), {.primaryId = detections[0].id(), .detectionId = detections[4].id(), .deleted = true});
    checkRelation(*newTxn(), {.primaryId = detections[1].id(), .detectionId = detections[2].id(), .deleted = false});
    checkRelation(*newTxn(), {.primaryId = detections[4].id(), .detectionId = detections[0].id(), .deleted = false});
    checkRelation(*newTxn(), {.primaryId = detections[5].id(), .detectionId = detections[3].id(), .deleted = false});
    checkRelation(*newTxn(), {.primaryId = detections[4].id(), .detectionId = detections[1].id(), .deleted = true});

    db::eye::ObjectLocations allLocations
        = db::eye::ObjectLocationGateway(*newTxn()).load();
    ASSERT_EQ(allLocations.size(), 4u);

    db::eye::ObjectLocations testLocations;
    testLocations = db::eye::ObjectLocationGateway(*newTxn()).load(
        db::eye::table::ObjectLocation::objectId == oldObjects[0].id()
    );
    ASSERT_EQ(testLocations.size(), 1u);
    EXPECT_EQ(testLocations[0].objectId(), oldObjects[0].id());
    EXPECT_EQ(testLocations[0].txnId(), oldObjectLocationTxnId1);

    testLocations = db::eye::ObjectLocationGateway(*newTxn()).load(
        db::eye::table::ObjectLocation::objectId == newObjects1[0].id()
    );
    ASSERT_EQ(testLocations.size(), 1u);
    EXPECT_EQ(testLocations[0].objectId(), newObjects1[0].id());
    Location expectedLocation = makeObjectLocation(
        detectionStore,
        detectionIdsByPrimaryId.at(detections[1].id())
    );
    EXPECT_TRUE(
        geolib3::test_tools::approximateEqual(
            testLocations[0].mercatorPos(),
            expectedLocation.mercatorPosition,
            geolib3::EPS
        )
    );

    testLocations = db::eye::ObjectLocationGateway(*newTxn()).load(
        db::eye::table::ObjectLocation::objectId == newObjects2[0].id()
    );
    ASSERT_EQ(testLocations.size(), 1u);
    EXPECT_EQ(testLocations[0].objectId(), newObjects2[0].id());
    EXPECT_TRUE(testLocations[0].txnId() > oldObjectLocationTxnId2);
    expectedLocation = makeObjectLocation(
        detectionStore,
        detectionIdsByPrimaryId.at(detections[4].id())
    );
    EXPECT_TRUE(
        geolib3::test_tools::approximateEqual(
            testLocations[0].mercatorPos(),
            expectedLocation.mercatorPosition,
            geolib3::EPS
        )
    );

    testLocations = db::eye::ObjectLocationGateway(*newTxn()).load(
        db::eye::table::ObjectLocation::objectId == newObjects3[0].id()
    );
    ASSERT_EQ(testLocations.size(), 1u);
    EXPECT_EQ(testLocations[0].objectId(), newObjects3[0].id());
    EXPECT_TRUE(testLocations[0].txnId() > oldObjectLocationTxnId3);
    expectedLocation = makeObjectLocation(
        detectionStore,
        detectionIdsByPrimaryId.at(detections[5].id())
    );
    EXPECT_TRUE(
        geolib3::test_tools::approximateEqual(
            testLocations[0].mercatorPos(),
            expectedLocation.mercatorPosition,
            geolib3::EPS
        )
    );
}

struct RemoveProcessedDetectionGroupIdsFixture: public DbFixture {
    RemoveProcessedDetectionGroupIdsFixture() {
        auto txn = newTxn();

        devices = {
            {db::eye::MrcDeviceAttrs{"M1"}},
        };
        db::eye::DeviceGateway(*txn).insertx(devices);

        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"), {1200, 800}, time()},
            {devices[0].id(), identical, makeUrlContext(4, "4"), {1200, 800}, time()},
            {devices[0].id(), identical, makeUrlContext(5, "5"), {1200, 800}, time()},
            {devices[0].id(), identical, makeUrlContext(6, "6"), {1200, 800}, time()},
        };
        db::eye::FrameGateway(*txn).insertx(frames);

        frameLocations = {
            {frames[0].id(), geolib3::Point2{0, 0}, toRotation(geolib3::Heading(90), identical)},
            {frames[1].id(), geolib3::Point2{1, 0}, toRotation(geolib3::Heading(90), identical)},
            {frames[2].id(), geolib3::Point2{0, 2}, toRotation(geolib3::Heading(90), identical)},
            {frames[3].id(), geolib3::Point2{0, 2}, toRotation(geolib3::Heading(90), identical)},
            {frames[4].id(), geolib3::Point2{0, 50}, toRotation(geolib3::Heading(90), identical)},
            {frames[5].id(), geolib3::Point2{50, 0}, toRotation(geolib3::Heading(90), identical)},
        };
        db::eye::FrameLocationGateway(*txn).insertx(frameLocations);

        groups = {
            {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},
        };
        db::eye::DetectionGroupGateway(*txn).insertx(groups);

        detections = {
            // #0 останется - нет связей и нет объекта
            {groups[0].id(), db::eye::DetectedHouseNumber{{0, 0, 10, 10}, 1.0, "12"}},
            // #1 удалится - главная детекция objects[0]
            {groups[1].id(), db::eye::DetectedHouseNumber{{0, 0, 20, 20}, 1.0, "12"}},
            // #2 останется - главная детекция удаленного objects[1]
            {groups[2].id(), db::eye::DetectedHouseNumber{{0, 0, 10, 10}, 1.0, "12"}},
            // #3 останется - главная детекция objects[2], но сама детекция удалена
            {groups[3].id(), db::eye::DetectedHouseNumber{{0, 0, 70, 70}, 1.0, "12"}},
            // #4 удалится - связана с detections[1]
            {groups[4].id(), db::eye::DetectedHouseNumber{{0, 0, 15, 15}, 1.0, "12"}},
            // #5 останется - связана с detections[1], но связь удалена
            {groups[5].id(), db::eye::DetectedHouseNumber{{0, 0, 60, 60}, 1.0, "12"}},
            // #6 останется - связана с удаленной detections[3]
            {groups[0].id(), db::eye::DetectedHouseNumber{{0, 0, 60, 60}, 1.0, "12"}},
            // #7 останется - связана удаленной связью с удаленным objects[1]
            {groups[0].id(), db::eye::DetectedHouseNumber{{0, 0, 60, 60}, 1.0, "12"}},
        };
        detections[3].setDeleted();
        db::eye::DetectionGateway(*txn).insertx(detections);

        objects = {
            {detections[1].id(), db::eye::HouseNumberAttrs{"12"}},
            {detections[2].id(), db::eye::HouseNumberAttrs{"12"}},
            {detections[3].id(), db::eye::HouseNumberAttrs{"12"}},
        };
        objects[1].setDeleted(true);
        db::eye::ObjectGateway(*txn).insertx(objects);

        relations = {
            {detections[1].id(), detections[4].id()},
            {detections[1].id(), detections[5].id()},
            {detections[3].id(), detections[6].id()},
            {detections[2].id(), detections[7].id()},
        };
        relations[1].setDeleted(true);
        relations[3].setDeleted(true);
        db::eye::PrimaryDetectionRelationGateway(*txn).insertx(relations);

        txn->commit();
    }

    std::vector<TxnIdDetectionGroupId> loadTxnDetectionGroupIds() const
    {
        std::vector<TxnIdDetectionGroupId> result;

        auto txn = newTxn();
        auto groupIds = db::eye::DetectionGroupGateway(*txn).loadIds();
        const auto groupIdToDetectionIds = loadGroupIdToDetectionIdsMap(*txn, groupIds);
        for (const auto& [groupId, detectionIds] : groupIdToDetectionIds) {
            std::vector<TxnIdDetectionId> txnDetectionIds;
            txnDetectionIds.reserve(detectionIds.size());
            for (auto detectionId : detectionIds) {
                txnDetectionIds.push_back({.txnId = 0, .detectionId = detectionId});
            }
            result.push_back({.detectionGroupId = groupId, .txnDetectionIds = std::move(txnDetectionIds)});
        }

        for (const auto groupId : groupIds) {
            if (!groupIdToDetectionIds.count(groupId)) {
                result.push_back({.detectionGroupId = groupId});
            }
        }
        return result;
    }
};

TEST_F(RemoveProcessedDetectionGroupIdsFixture, remove_processed_detection_ids)
{
    auto txnDetectionGroupIds = loadTxnDetectionGroupIds();
    auto txn = newTxn();
    removeProcessedDetectionGroupIds(*txn, &txnDetectionGroupIds);
    auto detectionIds = extractDetectionGroupIds(txnDetectionGroupIds);

    EXPECT_THAT(
        detectionIds,
        ::testing::UnorderedElementsAre(
            groups[0].id(),
            groups[2].id(),
            groups[3].id(),
            groups[5].id()
        ));
}

TEST_F(RemoveProcessedDetectionGroupIdsFixture, take_verification_requests_into_account)
{
    auto txnDetectionGroupIds = loadTxnDetectionGroupIds();

    {
        auto txn = newTxn();
        objects[1].setDeleted(false);
        objects.emplace_back(detections[0].id(), db::eye::HouseNumberAttrs{"12"});
        relations[1].setDeleted(false);
        relations[3].setDeleted(false);
        detections[3].setDeleted(false);


        db::eye::DetectionGateway(*txn).updatex(detections);
        db::eye::ObjectGateway(*txn).updatex(objects[1]);
        db::eye::ObjectGateway(*txn).insertx(objects.back());
        db::eye::PrimaryDetectionRelationGateway(*txn).updatex(relations);
        db::eye::DetectionGateway(*txn).updatex(detections);
        txn->commit();
    }

    {
        auto txn = newTxn();
        auto curTxnDetectionGroupIds = txnDetectionGroupIds;
        removeProcessedDetectionGroupIds(*txn, &curTxnDetectionGroupIds);
        EXPECT_THAT(
            extractDetectionGroupIds(curTxnDetectionGroupIds),
            ::testing::UnorderedElementsAre()
        );
    }

    {
        auto txn = newTxn();
        db::eye::VerifiedDetectionPairMatches verifiedMatches = {
            {db::eye::VerificationSource::Toloka, detections[0].id(), detections[1].id(), std::nullopt},  // unprocessed verification match request
            {db::eye::VerificationSource::Toloka, detections[1].id(), detections[2].id(), true},  // processed, but incoherent with current clusters
            {db::eye::VerificationSource::Toloka, detections[1].id(), detections[3].id(), false},  // procesed and coherent
            {db::eye::VerificationSource::Toloka, detections[1].id(), detections[4].id(), false}, // processed, but incoherent with current clusters
            {db::eye::VerificationSource::Toloka, detections[1].id(), detections[5].id(), true},  // procesed and coherent
        };

        db::eye::VerifiedDetectionPairMatchGateway(*txn).insertx(verifiedMatches);
        txn->commit();
        for (auto& txnDetectionGroupId : txnDetectionGroupIds) {
            for (auto& txnDetectionId : txnDetectionGroupId.txnDetectionIds) {
                txnDetectionId.txnId = verifiedMatches.front().txnId();
            }
        }
    }

    {
        auto txn = newTxn();
        auto curTxnDetectionGroupIds = txnDetectionGroupIds;
        removeProcessedDetectionGroupIds(*txn, &curTxnDetectionGroupIds);
        EXPECT_THAT(
            extractDetectionGroupIds(curTxnDetectionGroupIds),
            ::testing::UnorderedElementsAre(
                groups[1].id(),
                groups[2].id(),
                groups[4].id()
            )
        );
    }
}

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