#include <maps/wikimap/mapspro/services/mrc/libs/db/tests/eye/common.h>

#include <maps/libs/chrono/include/time_point.h>
#include <maps/libs/common/include/exception.h>
#include <maps/libs/geolib/include/conversion.h>
#include <maps/libs/geolib/include/test_tools/test_tools.h>
#include <maps/wikimap/mapspro/services/mrc/libs/db/include/eye/frame_gateway.h>

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

namespace {

constexpr double EPS = 1e-6;

} // namespace

using Fixture = maps::mrc::db::tests::Fixture;

TEST(eye_frame, make_device_attrs)
{
    {
        const MrcDeviceAttrs attrs{"M1"};
        const auto expected = json(R"({"source": "mrc", "model": "M1"})");
        EXPECT_EQ(DeviceAttrs{attrs}.json(), expected);
    }

    {
        const MrcDeviceAttrs attrs{std::nullopt};
        const auto expected = json(R"({"source": "mrc", "model": null})");
        EXPECT_EQ(DeviceAttrs{attrs}.json(), expected);
    }
}

TEST(eye_frame, make_mrc_url_context)
{
    const MrcUrlContext urlContext{1, "1", "1"};

    const auto expected = json(
        R"({"source": "mrc", "feature_id": 1, "mds_group_id": "1", "mds_path": "1"})"
    );

    EXPECT_EQ(UrlContext{urlContext}.json(), expected);
}

TEST_F(Fixture, insert_device)
{
    Devices devices {
        Device(MrcDeviceAttrs{"M1"}),
        Device(MrcDeviceAttrs{"M2"}),
    };

    auto txn = txnHandle();

    const TId txnId = DeviceGateway(*txn).insertx(devices);
    txn->commit();

    {   // first
        auto txn = txnHandle();

        const Device device = DeviceGateway(*txn).loadById(devices[0].id());
        EXPECT_EQ(device.txnId(), txnId);

        const auto attrs = device.attrs().mrc();
        EXPECT_EQ(attrs.model, "M1");
    }

    {   // second
        auto txn = txnHandle();

        const Device device = DeviceGateway(*txn).loadById(devices[1].id());
        EXPECT_EQ(device.txnId(), txnId);

        const auto attrs = device.attrs().mrc();
        EXPECT_EQ(attrs.model, "M2");
    }
}

TEST_F(DeviceFixture, insert_frame)
{
    Frames frames {
        {
            devices[0].id(),
            common::ImageOrientation(common::Rotation::CW_0),
            MrcUrlContext{1, "1", "1"},
            {1280, 720},
            chrono::parseSqlDateTime("2016-04-02 05:57:11+03")
        },
        {
            devices[1].id(),
            common::ImageOrientation(common::Rotation::CW_90),
            MrcUrlContext{2, "2", "2"},
            {1280, 720},
            chrono::parseSqlDateTime("2017-05-06 07:01:15+03")
        },
    };

    auto txn = txnHandle();

    const TId txnId = FrameGateway(*txn).insertx(frames);

    txn->commit();

    {   // Check first
        auto txn = txnHandle();

        const auto frame = FrameGateway(*txn).loadById(frames[0].id());

        EXPECT_EQ(frame.txnId(), txnId);
        EXPECT_EQ(frame.deviceId(), devices[0].id());

        const common::ImageOrientation orientation(common::Rotation::CW_0);
        EXPECT_EQ(frame.orientation(), orientation);

        const auto urlContext = frame.urlContext().mrc();
        EXPECT_EQ(urlContext.featureId, 1);
        EXPECT_EQ(urlContext.mdsGroupId, "1");
        EXPECT_EQ(urlContext.mdsPath, "1");

        constexpr common::Size originalSize{1280, 720};
        EXPECT_EQ(frame.originalSize(), originalSize);

        const auto time = chrono::parseSqlDateTime("2016-04-02 05:57:11+03");
        EXPECT_EQ(frame.time(), time);
    }

    {   // Check second
        auto txn = txnHandle();

        const auto frame = FrameGateway(*txn).loadById(frames[1].id());

        EXPECT_EQ(frame.txnId(), txnId);
        EXPECT_EQ(frame.deviceId(), devices[1].id());

        const common::ImageOrientation orientation(common::Rotation::CW_90);
        EXPECT_EQ(frame.orientation(), orientation);

        const auto urlContext = frame.urlContext().mrc();
        EXPECT_EQ(urlContext.featureId, 2);
        EXPECT_EQ(urlContext.mdsGroupId, "2");
        EXPECT_EQ(urlContext.mdsPath, "2");

        constexpr common::Size originalSize{1280, 720};
        EXPECT_EQ(frame.originalSize(), originalSize);

        const auto time = chrono::parseSqlDateTime("2017-05-06 07:01:15+03");
        EXPECT_EQ(frame.time(), time);
    }
}

TEST_F(FrameFixture, update_frame)
{
    auto txn = txnHandle();

    const common::ImageOrientation orientation(common::Rotation::CCW_90);

    frames[0].setDeleted(true);
    frames[1].setOrientation(orientation);

    const TId txnId = FrameGateway(*txn).upsertx(TArrayRef(frames.begin(), frames.begin() + 2));

    txn->commit();

    {   // check orientation
        auto txn = txnHandle();
        const auto frame = FrameGateway(*txn).loadById(frames[0].id());

        EXPECT_EQ(frame.txnId(), txnId);
        EXPECT_TRUE(frame.deleted());
    }

    {   // check deleted
        auto txn = txnHandle();
        const auto frame = FrameGateway(*txn).loadById(frames[1].id());

        EXPECT_EQ(frame.txnId(), txnId);
        EXPECT_EQ(frame.orientation(), orientation);
    }
}

TEST_F(FrameFixture, insert_frame_location)
{
    auto txn = txnHandle();

    FrameLocations locations {
        {
            frames[0].id(),
            {5.0, 3.0},
            Eigen::Quaterniond::Identity()
        },
        {
            frames[1].id(),
            {100.0, 53.0},
            Eigen::Quaterniond(
                Eigen::AngleAxisd(M_PI, Eigen::Vector3d::UnitZ())
            ),
            Eigen::Vector3d::UnitX()
        },
    };

    const TId txnId = FrameLocationGateway(*txn).insertx(locations);

    txn->commit();

    {   // first
        auto txn = txnHandle();

        const auto location = FrameLocationGateway(*txn).loadById(locations[0].frameId());
        EXPECT_EQ(location.txnId(), txnId);

        const geolib3::Point2 mercator {5.0, 3.0};
        const auto geodetic = geolib3::convertMercatorToGeodetic(mercator);
        EXPECT_TRUE(geolib3::test_tools::approximateEqual(location.mercatorPos(), mercator, EPS));
        EXPECT_TRUE(geolib3::test_tools::approximateEqual(location.geodeticPos(), geodetic, EPS));

        const auto rotation = Eigen::Quaterniond::Identity();
        EXPECT_TRUE(location.rotation().isApprox(rotation, EPS));

        EXPECT_TRUE(!location.hasMove());
    }

    {   // second
        auto txn = txnHandle();

        const auto location = FrameLocationGateway(*txn).loadById(locations[1].frameId());
        EXPECT_EQ(location.txnId(), txnId);

        const geolib3::Point2 mercator{100.0, 53.0};
        const auto geodetic = geolib3::convertMercatorToGeodetic(mercator);
        EXPECT_TRUE(geolib3::test_tools::approximateEqual(location.mercatorPos(), mercator, EPS));
        EXPECT_TRUE(geolib3::test_tools::approximateEqual(location.geodeticPos(), geodetic, EPS));

        const Eigen::Quaterniond rotation(Eigen::AngleAxisd(M_PI, Eigen::Vector3d::UnitZ()));
        EXPECT_TRUE(location.rotation().isApprox(rotation, EPS));

        const auto move = Eigen::Vector3d::UnitX();
        EXPECT_TRUE(location.hasMove());
        EXPECT_TRUE(location.move().isApprox(move, EPS));
    }
}

TEST_F(FrameFixture, unique_frame_location)
{
    auto txn = txnHandle();

    FrameLocations locations {
        {frames[0].id(), {1.0, 0.0}, Eigen::Quaterniond::Identity()},
        {frames[0].id(), {0.0, 1.0}, Eigen::Quaterniond::Identity()},
    };

    EXPECT_THROW(
        FrameLocationGateway(*txn).insertx(locations),
        maps::sql_chemistry::UniqueViolationError
    );
}

TEST_F(FrameFixture, update_frame_location)
{
    FrameLocations locations {
        {
            frames[0].id(),
            {5.0, 3.0},
            Eigen::Quaterniond::Identity()
        },
        {
            frames[2].id(),
            {2.0, 4.0},
            Eigen::Quaterniond(
                Eigen::AngleAxisd(M_PI, Eigen::Vector3d::UnitZ())
            )
        },
        {
            frames[3].id(),
            {-2.0, -4.0},
            Eigen::Quaterniond(
                Eigen::AngleAxisd(M_PI, Eigen::Vector3d::UnitZ())
            )
        },
    };

    auto txn = txnHandle();

    FrameLocationGateway(*txn).insertx(locations);

    txn->commit();

    TId txnId;
    {   // update
        auto txn = txnHandle();

        locations[0].setMercatorPos(geolib3::Point2(50, 10));
        locations[1].setGeodeticPos(geolib3::Point2(30, 40));

        locations[2].setRotation(
            Eigen::Quaterniond(
                Eigen::AngleAxisd(-M_PI, Eigen::Vector3d::UnitY())
            )
        ).setMove(
            Eigen::Vector3d::UnitX()
        );

        txnId = FrameLocationGateway(*txn).updatex(locations);

        txn->commit();
    }

    {   // first
        auto txn = txnHandle();

        const auto location = FrameLocationGateway(*txn).loadById(locations[0].frameId());
        EXPECT_EQ(location.txnId(), txnId);

        const geolib3::Point2 position{50, 10};
        EXPECT_EQ(location.mercatorPos(), position);
    }

    {   // second
        auto txn = txnHandle();

        const auto location = FrameLocationGateway(*txn).loadById(locations[1].frameId());
        EXPECT_EQ(location.txnId(), txnId);

        const geolib3::Point2 position{30, 40};
        EXPECT_EQ(location.geodeticPos(), position);
    }

    {   // third
        auto txn = txnHandle();

        const auto location = FrameLocationGateway(*txn).loadById(locations[2].frameId());
        EXPECT_EQ(location.txnId(), txnId);

        const Eigen::Quaterniond rotation(Eigen::AngleAxisd(-M_PI, Eigen::Vector3d::UnitY()));
        EXPECT_TRUE(location.rotation().isApprox(rotation, EPS));

        const auto move = Eigen::Vector3d::UnitX();
        EXPECT_TRUE(location.hasMove());
        EXPECT_TRUE(location.move().isApprox(move, EPS));
    }
}

TEST_F(FrameFixture, insert_frame_privacy)
{
    auto txn = txnHandle();

    FramePrivacies privacies {
        FramePrivacy(frames[0].id(), FeaturePrivacy::Public),
        FramePrivacy(frames[1].id(), FeaturePrivacy::Restricted),
        FramePrivacy(frames[2].id(), FeaturePrivacy::Secret),
    };

    const auto txnId = FramePrivacyGateway(*txn).insertx(privacies);

    txn->commit();

    {   // first
        auto txn = txnHandle();
        const auto privacy = FramePrivacyGateway(*txn).loadById(privacies[0].frameId());
        EXPECT_EQ(privacy.txnId(), txnId);
        EXPECT_EQ(privacy.type(), FeaturePrivacy::Public);
    }

    {   // second
        auto txn = txnHandle();
        const auto privacy = FramePrivacyGateway(*txn).loadById(privacies[1].frameId());
        EXPECT_EQ(privacy.txnId(), txnId);
        EXPECT_EQ(privacy.type(), FeaturePrivacy::Restricted);
    }

    {   // third
        auto txn = txnHandle();
        const auto privacy = FramePrivacyGateway(*txn).loadById(privacies[2].frameId());
        EXPECT_EQ(privacy.txnId(), txnId);
        EXPECT_EQ(privacy.type(), FeaturePrivacy::Secret);
    }
}

TEST_F(FrameFixture, unique_frame_privacy)
{
    FramePrivacies privacies {
        FramePrivacy(frames[0].id(), FeaturePrivacy::Public),
        FramePrivacy(frames[0].id(), FeaturePrivacy::Restricted),
    };

    auto txn = txnHandle();

    EXPECT_THROW(
        FramePrivacyGateway(*txn).insertx(privacies),
        maps::sql_chemistry::UniqueViolationError
    );
}

TEST_F(FrameFixture, update_frame_privacy)
{
    FramePrivacies privacies {
        {frames[0].id(), FeaturePrivacy::Public},
        {frames[1].id(), FeaturePrivacy::Restricted},
        {frames[2].id(), FeaturePrivacy::Secret},
    };

    auto txn = txnHandle();

    FramePrivacyGateway(*txn).insertx(privacies);

    txn->commit();

    TId txnId;
    {   // update
        auto txn = txnHandle();

        auto& privacy = privacies[0];

        privacy.setType(FeaturePrivacy::Secret);
        txnId =  FramePrivacyGateway(*txn).updatex(privacy);

        txn->commit();
    }

    {   // check
        auto txn = txnHandle();
        const auto privacy = FramePrivacyGateway(*txn).loadById(privacies[0].frameId());

        EXPECT_EQ(privacy.txnId(), txnId);
        EXPECT_EQ(privacy.type(), FeaturePrivacy::Secret);
    }
}

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