#include <library/cpp/testing/common/env.h>
#include <library/cpp/testing/gtest/gtest.h>
#include <maps/libs/http/include/test_utils.h>
#include <maps/wikimap/mapspro/services/mrc/libs/db/include/ride_gateway.h>
#include <maps/wikimap/mapspro/services/mrc/libs/db/include/walk_object_gateway.h>
#include <maps/wikimap/mapspro/services/mrc/long_tasks/ugc_uploader/lib/db.h>
#include <maps/wikimap/mapspro/services/mrc/long_tasks/ugc_uploader/lib/ugc.h>
#include <maps/wikimap/mapspro/services/mrc/long_tasks/ugc_uploader/lib/utility.h>
#include <yandex/maps/geolib3/proto.h>
#include <yandex/maps/mrc/unittest/database_fixture.h>
#include <yandex/maps/proto/ugc_account/backoffice.pb.h>

#include <boost/lexical_cast.hpp>

namespace maps::mrc::ugc_uploader::tests {

using namespace std::chrono_literals;

struct Fixture : testing::Test,
                 unittest::WithUnittestConfig<unittest::DatabaseFixture> {
};

using ModifyContributions =
    yandex::maps::proto::ugc_account::backoffice::ModifyContributions;

const auto USER_ID = std::string{"usr"};
const auto SOURCE_ID = std::string{"src"};
const auto TIME = chrono::parseIsoDateTime("2021-02-13T12:37:00+03");
const auto PHOTOS_NUMBER = 1234;
const auto COMMENT = std::string{"comment"};

const auto CLIENT_RIDE_IDS = std::vector<std::optional<std::string>>{
    std::nullopt,
    std::string{"b0c2d8c8-6fc6-45d0-9e8e-45e37bd29636"},
};

const auto HYPOTHESES = db::FeedbackIdToHypothesisTypeMap{
    {42,  //< feedbackId
     db::eye::HypothesisType::AbsentTrafficLight},
    {100500,  //< feedbackId
     db::eye::HypothesisType::AbsentHouseNumber}};

const auto POINT = geolib3::Point2{37.5636, 55.6676};

const auto RIDE = db::Ride{USER_ID,
                           TIME - 24h,
                           TIME - 22h,
                           SOURCE_ID,
                           {},      //< track
                           100500,  //< meters
                           PHOTOS_NUMBER,
                           TIME - 2h,
                           TIME - 1h,
                           false}  //< deleted
                      .setPublishedPhotos(PHOTOS_NUMBER / 2)
                      .setShowAuthorship(true);

const auto PHOTO = sql_chemistry::GatewayAccess<db::Feature>::construct()
                       .setSourceId(SOURCE_ID)
                       .setTimestamp(TIME - 23h)
                       .setGeodeticPos(POINT)
                       .setHeading(geolib3::Heading{90})
                       .setMdsKey(mds::Key{"a", "b"})
                       .setSize({240, 180})
                       .setUserId(USER_ID);

const auto WALK_OBJECT = db::WalkObject{db::Dataset::Walks,
                                        SOURCE_ID,
                                        TIME - 24h,
                                        db::WalkFeedbackType::Other,
                                        POINT,
                                        COMMENT}
                             .setUserId(USER_ID);

std::optional<std::string> getClientRideId(const auto& proto)
{
    if (proto.has_client_ride_id()) {
        return std::string(proto.client_ride_id());
    }
    return std::nullopt;
}

TEST_F(Fixture, test_ride_db)
{
    auto rides = db::Rides{3, RIDE};
    {
        auto txn = pool().masterWriteableTransaction();
        auto time = TIME - 24h;
        for (size_t i = 0; i < rides.size(); ++i, time += 1h) {
            db::RideGateway{*txn}.insertx(rides[i].setTimes(time, time));
        }
        txn->commit();
    }
    {
        auto txn = pool().masterWriteableTransaction();
        db::RideGateway{*txn}.updatex(rides[1]);
        txn->commit();
    }
    EXPECT_EQ(queueSize<db::Ride>(*pool().masterReadOnlyTransaction()),
              rides.size());
    for (size_t i : {0, 2, 1}) {
        EXPECT_TRUE(queuePop<db::Ride>(pool(), [&](const db::Ride& ride) {
            EXPECT_EQ(ride.rideId(), rides[i].rideId());
        }));
    }
    EXPECT_EQ(queueSize<db::Ride>(*pool().masterReadOnlyTransaction()), 0u);
    EXPECT_FALSE(queuePop<db::Ride>(pool(), [](auto&&) { ASSERT(false); }));
}

TEST_F(Fixture, test_walk_object_db)
{
    auto walkObjects = db::WalkObjects{3, WALK_OBJECT};
    {
        auto txn = pool().masterWriteableTransaction();
        auto time = TIME - 24h;
        for (size_t i = 0; i < walkObjects.size(); ++i, time += 1h) {
            db::WalkObjectGateway{*txn}.insertx(
                walkObjects[i].setCreatedAt(time));
        }
        txn->commit();
    }
    {
        auto txn = pool().masterWriteableTransaction();
        db::WalkObjectGateway{*txn}.updatex(walkObjects[1]);
        txn->commit();
    }
    EXPECT_EQ(queueSize<db::WalkObject>(*pool().masterReadOnlyTransaction()),
              walkObjects.size());
    for (size_t i : {0, 2, 1}) {
        EXPECT_TRUE(
            queuePop<db::WalkObject>(pool(), [&](const db::WalkObject& obj) {
                EXPECT_EQ(obj.id(), walkObjects[i].id());
            }));
    }
    EXPECT_EQ(queueSize<db::WalkObject>(*pool().masterReadOnlyTransaction()),
              0u);
    EXPECT_FALSE(
        queuePop<db::WalkObject>(pool(), [](auto&&) { ASSERT(false); }));
}

TEST_F(Fixture, test_ride_ugc_upsert)
{
    for (const auto& clientRideId : CLIENT_RIDE_IDS) {
        auto ride = RIDE;
        auto photo = PHOTO;
        if (clientRideId) {
            ride.setClientId(*clientRideId);
            photo.setClientRideId(*clientRideId);
        }

        auto ugcMock = http::addMock(
            makeContributionsModifyUrl(config()),
            [&](const http::MockRequest& response) {
                auto msg = ModifyContributions{};
                EXPECT_TRUE(msg.ParseFromString(TString{response.body}));
                EXPECT_EQ(msg.modify_contribution_size(), 1);
                const auto& modifyContribution = msg.modify_contribution(0);
                EXPECT_TRUE(modifyContribution.has_upsert());
                const auto& upsert = modifyContribution.upsert();
                EXPECT_EQ(upsert.id(),
                          MRC_RIDE_CONTRIBUTION_NAMESPACE + ":" +
                              std::to_string(ride.rideId()));
                const auto& contribution = upsert.contribution();
                EXPECT_EQ(static_cast<time_t>(contribution.timestamp()),
                          chrono::convertToUnixTime(ride.startTime()));
                EXPECT_GT(contribution.lang_to_metadata_size(), 1);
                for (const auto& [lang, locale] : supportedLocales()) {
                    const auto& metadata =
                        contribution.lang_to_metadata().at(TString{lang});
                    EXPECT_TRUE(metadata.has_mrc_ride());
                    const auto& mrcRide = metadata.mrc_ride();
                    EXPECT_EQ(mrcRide.id(),
                              TString{std::to_string(ride.rideId())});
                    EXPECT_TRUE(mrcRide.has_started_at());
                    EXPECT_TRUE(mrcRide.has_finished_at());
                    EXPECT_TRUE(mrcRide.has_duration());
                    EXPECT_TRUE(mrcRide.has_distance());
                    EXPECT_EQ(mrcRide.photos_count(), ride.photos());
                    EXPECT_EQ(mrcRide.published_photos_count(),
                              ride.publishedPhotos());
                    EXPECT_TRUE(mrcRide.has_album_image());
                    EXPECT_TRUE(mrcRide.has_ride_status());
                    EXPECT_EQ(mrcRide.show_authorship(), ride.showAuthorship());
                    EXPECT_EQ(getClientRideId(mrcRide), ride.clientId());
                    EXPECT_EQ(db::getRideHypotheses(mrcRide), HYPOTHESES);
                }
                return http::MockResponse::withStatus(201);
            });

        EXPECT_NO_THROW(
            pushContribution(std::nullopt,
                             makeContributionsModifyUrl(config()),
                             config().externals().mapsCoreNmapsMrcUgcBackUrl(),
                             ride,
                             photo,
                             HYPOTHESES));
    }
}

TEST_F(Fixture, test_walk_object_ugc_upsert)
{
    auto walkObject = WALK_OBJECT;
    auto photo = PHOTO;
    photo.setWalkObjectId(walkObject.id());

    auto geosearchMock = http::addMock(
        config().externals().geosearchUrl(), [&](const http::MockRequest&) {
            return http::MockResponse::withStatus(200);
        });

    auto ugcMock = http::addMock(
        makeContributionsModifyUrl(config()),
        [&](const http::MockRequest& response) {
            auto msg = ModifyContributions{};
            EXPECT_TRUE(msg.ParseFromString(TString{response.body}));
            EXPECT_EQ(msg.modify_contribution_size(), 1);
            const auto& modifyContribution = msg.modify_contribution(0);
            EXPECT_TRUE(modifyContribution.has_upsert());
            const auto& upsert = modifyContribution.upsert();
            EXPECT_EQ(upsert.id(),
                      MRC_WALK_OBJECT_CONTRIBUTION_NAMESPACE + ":" +
                          std::to_string(walkObject.id()));
            const auto& contribution = upsert.contribution();
            EXPECT_EQ(static_cast<time_t>(contribution.timestamp()),
                      chrono::convertToUnixTime(walkObject.createdAt()));
            EXPECT_GT(contribution.lang_to_metadata_size(), 1);
            for (const auto& [lang, locale] : supportedLocales()) {
                const auto& metadata =
                    contribution.lang_to_metadata().at(TString{lang});
                EXPECT_TRUE(metadata.has_mrc_walk_object());
                const auto& mrcWalkObject = metadata.mrc_walk_object();
                EXPECT_EQ(mrcWalkObject.id(),
                          TString{std::to_string(walkObject.id())});
                EXPECT_TRUE(mrcWalkObject.has_feedback_type());
                EXPECT_TRUE(mrcWalkObject.has_status());
                EXPECT_TRUE(mrcWalkObject.has_geometry());
                EXPECT_TRUE(mrcWalkObject.has_comment());
                EXPECT_EQ(mrcWalkObject.comment(), COMMENT);
                EXPECT_TRUE(mrcWalkObject.has_client_object_id());
                EXPECT_EQ(mrcWalkObject.client_object_id(), "1613122620000");
                EXPECT_EQ(mrcWalkObject.photos().size(), 1);
                auto& geoPhoto = mrcWalkObject.photos().at(0);
                auto& shootingPoint = geoPhoto.shooting_point();
                EXPECT_EQ(geolib3::proto::decode(shootingPoint.point().point()),
                          photo.geodeticPos());
                EXPECT_EQ(geolib3::Heading(shootingPoint.direction().azimuth()),
                          photo.heading());
                EXPECT_EQ(chrono::convertFromUnixTime(geoPhoto.taken_at()),
                          photo.timestamp());
            }
            return http::MockResponse::withStatus(201);
        });
    EXPECT_NO_THROW(
        pushContribution(std::nullopt,
                         makeContributionsModifyUrl(config()),
                         config().externals().mapsCoreNmapsMrcUgcBackUrl(),
                         config().externals().geosearchUrl(),
                         walkObject,
                         std::async(std::launch::deferred,
                                    [&] { return db::Features{photo}; })));
}

TEST_F(Fixture, test_ugc_ride_delete)
{
    auto ride = RIDE;
    ride.setIsDeleted(true);
    auto ugcMock = http::addMock(
        makeContributionsModifyUrl(config()),
        [&](const http::MockRequest& response) {
            auto msg = ModifyContributions{};
            EXPECT_TRUE(msg.ParseFromString(TString{response.body}));
            EXPECT_EQ(msg.modify_contribution_size(), 1);
            const auto& modifyContribution = msg.modify_contribution(0);
            EXPECT_TRUE(modifyContribution.has_delete_());
            const auto& del = modifyContribution.delete_();
            EXPECT_EQ(del.id(),
                      MRC_RIDE_CONTRIBUTION_NAMESPACE + ":" +
                          std::to_string(ride.rideId()));
            return http::MockResponse::withStatus(200);
        });
    pushContribution(std::nullopt,
                     makeContributionsModifyUrl(config()),
                     config().externals().mapsCoreNmapsMrcUgcBackUrl(),
                     ride,
                     std::nullopt,  //< sizedPhoto
                     {});           //< hypotheses
}

TEST_F(Fixture, test_ugc_walk_object_delete)
{
    auto walkObject = WALK_OBJECT;
    auto ugcMock = http::addMock(
        makeContributionsModifyUrl(config()),
        [&](const http::MockRequest& response) {
            auto msg = ModifyContributions{};
            EXPECT_TRUE(msg.ParseFromString(TString{response.body}));
            EXPECT_EQ(msg.modify_contribution_size(), 1);
            const auto& modifyContribution = msg.modify_contribution(0);
            EXPECT_TRUE(modifyContribution.has_delete_());
            const auto& del = modifyContribution.delete_();
            EXPECT_EQ(del.id(),
                      MRC_WALK_OBJECT_CONTRIBUTION_NAMESPACE + ":" +
                          std::to_string(walkObject.id()));
            return http::MockResponse::withStatus(200);
        });
    delWalkObjectContribution(
        std::nullopt,
        makeContributionsModifyUrl(config()),
        walkObject);                     
}

TEST_F(Fixture, test_ugc_too_many_requests)
{
    auto ugcMock = http::addMock(makeContributionsModifyUrl(config()),
                                 [](const http::MockRequest&) {
                                     return http::MockResponse::withStatus(429);
                                 });
    EXPECT_THROW(
        pushContribution(std::nullopt,
                         makeContributionsModifyUrl(config()),
                         config().externals().mapsCoreNmapsMrcUgcBackUrl(),
                         RIDE,
                         PHOTO,
                         HYPOTHESES),
        maps::RuntimeError);
}

}  // namespace maps::mrc::ugc_uploader::tests
