#include <maps/wikimap/mapspro/services/tasks_realtime/src/mrc_pedestrian_tasks_generator/lib/tasks_generator.h>

#include <yandex/maps/wiki/common/pgpool3_helpers.h>
#include <yandex/maps/wiki/revision/branch.h>
#include <yandex/maps/wiki/revision/branch_manager.h>
#include <yandex/maps/wiki/revision/objectrevision.h>
#include <yandex/maps/wiki/revision/revisionsgateway.h>

#include <yandex/maps/proto/ugc_account/backoffice.pb.h>

#include <maps/libs/geolib/include/polygon.h>
#include <maps/libs/geolib/include/serialization.h>
#include <yandex/maps/wiki/unittest/config.h>
#include <yandex/maps/wiki/unittest/localdb.h>

#include <maps/libs/http/include/test_utils.h>

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

#include <pqxx/pqxx>

namespace rev = maps::wiki::revision;
namespace pugc = yandex::maps::proto::ugc_account;

namespace maps::wiki::tasks::mrc_pedestrian::test {

namespace {

constexpr types::TUid TEST_UID = 1111111;
const std::string TEST_UID_STR = std::to_string(TEST_UID);
const rev::Attributes COMMIT_ATTRS = {{"description", "test"}};
const std::string UGC_BACKOFFICE_HOST = "http://ugc-backoffice-mock";
const std::string SERVICE_TICKET = "test-service-ticket";


std::string wkt2wkb(const std::string& wkt)
{
    auto polygon = geolib3::WKT::read<geolib3::Polygon2>(wkt);
    std::stringstream wkb;
    geolib3::WKB::write(polygon, wkb);
    return wkb.str();
}


class ObjectData {
public:
    ObjectData(rev::DBID objectId, const rev::Attributes& attrs, const rev::Wkb& geom)
        : data_(
            rev::RevisionID::createNewID(objectId),
            rev::ObjectData(attrs, std::nullopt, geom, std::nullopt))
    {
    }

    ObjectData(rev::RevisionID prevId, const rev::Attributes& attrs)
        : data_(
            prevId,
            rev::ObjectData(attrs, std::nullopt, std::nullopt, std::nullopt))
    {
        REQUIRE(prevId.valid(), "Revision id " << prevId << " is invalid");
    }

    ObjectData(rev::RevisionID prevId, bool deleted)
        : data_(
            prevId,
            rev::ObjectData(std::nullopt, std::nullopt, std::nullopt, std::nullopt, deleted))
    {
        REQUIRE(prevId.valid(), "Revision id " << prevId << " is invalid");
    }

    const rev::RevisionsGateway::NewRevisionData& data() const { return data_; }

private:
    rev::RevisionsGateway::NewRevisionData data_;
};


class UgcBackofficeMock {
public:
    struct Task {
        std::string id;
        std::set<types::TUid> uids;
        pugc::assignment::Status status{pugc::assignment::Status::ACTIVE};
        size_t timesUpdated{0};
        bool isIndoor;
        bool isPanoramic;
    };

    bool hasTask(const std::string& taskId)
    {
        return tasks_.find(taskId) != tasks_.end();
    }

    bool hasTask(const std::string& taskId, types::TUid uid)
    {
        auto taskIt = tasks_.find(taskId);
        return taskIt != tasks_.end() && taskIt->second.uids.count(uid) > 0;
    }

    const Task& getTask(const std::string& taskId)
    {
        ASSERT(hasTask(taskId));
        return tasks_.at(taskId);
    }

    void setStatus(const std::string& taskId, pugc::assignment::Status status)
    {
        ASSERT(hasTask(taskId));
        tasks_.at(taskId).status = status;
    }

    http::MockHandle addTaskStatusMock()
    {
        http::URL url{UGC_BACKOFFICE_HOST};
        url.setPath("/v1/assignments/status");

        return http::addMock(url, [=](const http::MockRequest& request) {
            auto taskId = *request.url.optParam<std::string>("task_id");

            auto taskIt = tasks_.find(taskId);
            if (taskIt == tasks_.end()) {
                return http::MockResponse::withStatus(404);
            } else {
                pugc::backoffice::AssignmentStatus response;
                response.set_status(taskIt->second.status);
                return http::MockResponse(response.SerializeAsString());
            }
        });
    }

    http::MockHandle addTaskCreateMock()
    {
        http::URL url{UGC_BACKOFFICE_HOST};
        url.setPath("/v1/assignments/create");

        return http::addMock(url, [=](const http::MockRequest& request) {
            auto taskId = *request.url.optParam<std::string>("task_id");
            pugc::backoffice::Task task;
            if (!task.ParseFromString(TString{request.body})) {
                return http::MockResponse::withStatus(400);
            }
            auto uid = std::stoll(task.uid(0));
            auto&& pedestrianAssignment = task.lang_to_metadata().at("ru_RU").pedestrian_assignment();

            auto taskIt = tasks_.find(taskId);
            if (taskIt == tasks_.end()) {
                tasks_.emplace(taskId, Task{
                    .id = taskId,
                    .uids = {uid},
                    .isIndoor = pedestrianAssignment.is_indoor(),
                    .isPanoramic = pedestrianAssignment.is_panoramic()
                });
            } else {
                taskIt->second.uids.insert(uid);
                taskIt->second.timesUpdated++;
                taskIt->second.isIndoor = pedestrianAssignment.is_indoor();
                taskIt->second.isPanoramic= pedestrianAssignment.is_panoramic();
            }
            return http::MockResponse();
        });
    }

    http::MockHandle addTaskDeleteMock()
    {
        http::URL url{UGC_BACKOFFICE_HOST};
        url.setPath("/v1/assignments/delete");

        return http::addMock(url, [=](const http::MockRequest& request) {
            auto taskId = *request.url.optParam<std::string>("task_id");
            auto uid = *request.url.optParam<types::TUid>("uid");

            auto taskIt = tasks_.find(taskId);
            if (taskIt != tasks_.end()) {
                taskIt->second.uids.erase(uid);
                if (taskIt->second.uids.empty()) {
                    tasks_.erase(taskId);
                }
                return http::MockResponse();
            } else {
                return http::MockResponse::withStatus(404);
            }
        });
    }

private:
    std::unordered_map<std::string, Task> tasks_;
};


class TestFixture : public ::testing::Test, public unittest::MapsproDbFixture
{
public:
    TestFixture()
        : unittest::MapsproDbFixture()
        , tasksGenerator_(
            pool(),
            UgcBackofficeClient(
                UGC_BACKOFFICE_HOST,
                std::make_unique<FixedServiceTicketProvider>(SERVICE_TICKET)))
    {
    }

    rev::Commit createCommit(const std::vector<ObjectData>& objectsData)
    {
        auto txn = pool().masterWriteableTransaction();
        auto branch = revision::BranchManager(*txn).load(revision::TRUNK_BRANCH_ID);
        rev::RevisionsGateway gateway(*txn, branch);

        std::vector<revision::RevisionsGateway::NewRevisionData> newData;
        for (const auto& objectData : objectsData) {
            newData.push_back(objectData.data());
        }
        auto commit = gateway.createCommit(newData, TEST_UID, COMMIT_ATTRS);
        txn->commit();
        return commit;
    }

    TasksGenerator& tasksGenerator() { return tasksGenerator_; }

    UgcBackofficeMock& ugcBackofficeMock() { return ugcBackofficeMock_; }

private:
    TasksGenerator tasksGenerator_;
    UgcBackofficeMock ugcBackofficeMock_;
};

} // namespace

TEST_F(TestFixture, test_create_draft_zone_create_task)
{
    const std::vector<ObjectData> COMMIT = {
        {101,
        {{"cat:mrc_pedestrian_region", "1"},
         {"mrc_pedestrian_region:name", "zone_1"},
         {"mrc_pedestrian_region:status", "draft"},
         {"mrc_pedestrian_region:pedestrian_login_uid", TEST_UID_STR}},
        wkt2wkb("POLYGON ((10 10, 10 20, 20 20, 20 10, 10 10))")}
    };

    createCommit(COMMIT);

    const std::string TASK_ID = "mrc_pedestrian:101";

    auto taskStatusMock = ugcBackofficeMock().addTaskStatusMock();
    auto taskCreateMock = ugcBackofficeMock().addTaskCreateMock();

    tasksGenerator().consumeNewCommits();
    EXPECT_FALSE(ugcBackofficeMock().hasTask(TASK_ID));
}

TEST_F(TestFixture, test_create_can_start_zone_create_task)
{
    const std::vector<ObjectData> COMMIT = {
        {101,
        {{"cat:mrc_pedestrian_region", "1"},
         {"mrc_pedestrian_region:name", "zone_1"},
         {"mrc_pedestrian_region:status", "can_start"},
         {"mrc_pedestrian_region:pedestrian_login_uid", TEST_UID_STR}},
        wkt2wkb("POLYGON ((10 10, 10 20, 20 20, 20 10, 10 10))")}
    };

    createCommit(COMMIT);

    const std::string TASK_ID = "mrc_pedestrian:101";

    auto taskStatusMock = ugcBackofficeMock().addTaskStatusMock();
    auto taskCreateMock = ugcBackofficeMock().addTaskCreateMock();

    tasksGenerator().consumeNewCommits();

    EXPECT_TRUE(ugcBackofficeMock().hasTask(TASK_ID, TEST_UID));
}

TEST_F(TestFixture, test_update_zone_create_task)
{
    const std::vector<ObjectData> COMMIT_1 = {
        {101,
        {{"cat:mrc_pedestrian_region", "1"},
         {"mrc_pedestrian_region:name", "zone_1"},
         {"mrc_pedestrian_region:status", "draft"},
         {"mrc_pedestrian_region:pedestrian_login_uid", TEST_UID_STR}},
        wkt2wkb("POLYGON ((10 10, 10 20, 20 20, 20 10, 10 10))")}
    };

    auto commit = createCommit(COMMIT_1);

    const std::string TASK_ID = "mrc_pedestrian:101";

    auto taskStatusMock = ugcBackofficeMock().addTaskStatusMock();
    auto taskCreateMock = ugcBackofficeMock().addTaskCreateMock();

    tasksGenerator().consumeNewCommits();

    EXPECT_FALSE(ugcBackofficeMock().hasTask(TASK_ID));

    // Update zone state from draft to can_start
    const std::vector<ObjectData> COMMIT_2 = {
        {{101, commit.id()},
         {{"cat:mrc_pedestrian_region", "1"},
          {"mrc_pedestrian_region:name", "zone_1"},
          {"mrc_pedestrian_region:status", "can_start"},
          {"mrc_pedestrian_region:pedestrian_login_uid", TEST_UID_STR}}}
    };

    createCommit(COMMIT_2);
    tasksGenerator().consumeNewCommits();

    EXPECT_TRUE(ugcBackofficeMock().hasTask(TASK_ID));
    EXPECT_EQ(ugcBackofficeMock().getTask(TASK_ID).timesUpdated, 0u);
}

TEST_F(TestFixture, test_update_zone_update_task)
{
    const std::vector<ObjectData> COMMIT_1 = {
        {101,
        {{"cat:mrc_pedestrian_region", "1"},
         {"mrc_pedestrian_region:name", "zone_1"},
         {"mrc_pedestrian_region:status", "can_start"},
         {"mrc_pedestrian_region:pedestrian_login_uid", TEST_UID_STR}},
        wkt2wkb("POLYGON ((10 10, 10 20, 20 20, 20 10, 10 10))")}
    };

    auto commit = createCommit(COMMIT_1);

    const std::string TASK_ID = "mrc_pedestrian:101";

    auto taskStatusMock = ugcBackofficeMock().addTaskStatusMock();
    auto taskCreateMock = ugcBackofficeMock().addTaskCreateMock();

    tasksGenerator().consumeNewCommits();

    EXPECT_TRUE(ugcBackofficeMock().hasTask(TASK_ID, TEST_UID));
    auto task = ugcBackofficeMock().getTask(TASK_ID);
    EXPECT_EQ(task.timesUpdated, 0u);
    EXPECT_EQ(task.isIndoor, false);
    EXPECT_EQ(task.isPanoramic, false);

    // Update name attribute
    const std::vector<ObjectData> COMMIT_2 = {
        {{101, commit.id()},
         {{"cat:mrc_pedestrian_region", "1"},
          {"mrc_pedestrian_region:name", "new_zone_1"},
          {"mrc_pedestrian_region:status", "can_start"},
          {"mrc_pedestrian_region:pedestrian_login_uid", TEST_UID_STR}}}
    };

    commit = createCommit(COMMIT_2);
    tasksGenerator().consumeNewCommits();

    EXPECT_TRUE(ugcBackofficeMock().hasTask(TASK_ID, TEST_UID));
    task = ugcBackofficeMock().getTask(TASK_ID);
    EXPECT_EQ(task.timesUpdated, 1u);

    // Update is_indoor and is_panoramic attributes
    const std::vector<ObjectData> COMMIT_3 = {
        {{101, commit.id()},
         {{"cat:mrc_pedestrian_region", "1"},
          {"mrc_pedestrian_region:name", "new_zone_1"},
          {"mrc_pedestrian_region:status", "can_start"},
          {"mrc_pedestrian_region:is_indoor", "1"},
          {"mrc_pedestrian_region:is_panoramic", "1"},
          {"mrc_pedestrian_region:pedestrian_login_uid", TEST_UID_STR}}}
    };

    commit = createCommit(COMMIT_3);
    tasksGenerator().consumeNewCommits();

    EXPECT_TRUE(ugcBackofficeMock().hasTask(TASK_ID, TEST_UID));
    task = ugcBackofficeMock().getTask(TASK_ID);
    EXPECT_EQ(task.timesUpdated, 2u);
    EXPECT_EQ(task.isIndoor, true);
    EXPECT_EQ(task.isPanoramic, true);

    // Update pedestrian_login_uid attribute
    constexpr types::TUid TEST_UID_2 = 2222222;

    auto taskDeleteMock = ugcBackofficeMock().addTaskDeleteMock();

    const std::vector<ObjectData> COMMIT_4 = {
        {{101, commit.id()},
         {{"cat:mrc_pedestrian_region", "1"},
          {"mrc_pedestrian_region:name", "new_zone_1"},
          {"mrc_pedestrian_region:status", "can_start"},
          {"mrc_pedestrian_region:pedestrian_login_uid", std::to_string(TEST_UID_2)}}}
    };

    createCommit(COMMIT_4);
    tasksGenerator().consumeNewCommits();

    EXPECT_TRUE(ugcBackofficeMock().hasTask(TASK_ID, TEST_UID_2));
    EXPECT_FALSE(ugcBackofficeMock().hasTask(TASK_ID, TEST_UID));
}

TEST_F(TestFixture, test_update_zone_delete_task)
{
    const std::vector<ObjectData> COMMIT_1 = {
        {101,
        {{"cat:mrc_pedestrian_region", "1"},
         {"mrc_pedestrian_region:name", "zone_1"},
         {"mrc_pedestrian_region:status", "can_start"},
         {"mrc_pedestrian_region:pedestrian_login_uid", TEST_UID_STR}},
        wkt2wkb("POLYGON ((10 10, 10 20, 20 20, 20 10, 10 10))")}
    };

    auto commit = createCommit(COMMIT_1);

    const std::string TASK_ID = "mrc_pedestrian:101";

    auto taskStatusMock = ugcBackofficeMock().addTaskStatusMock();
    auto taskCreateMock = ugcBackofficeMock().addTaskCreateMock();
    auto taskDeleteMock = ugcBackofficeMock().addTaskDeleteMock();

    tasksGenerator().consumeNewCommits();

    EXPECT_TRUE(ugcBackofficeMock().hasTask(TASK_ID));
    EXPECT_EQ(ugcBackofficeMock().getTask(TASK_ID).timesUpdated, 0u);

    // Update zone state from can_start to draft
    const std::vector<ObjectData> COMMIT_2 = {
        {{101, commit.id()},
         {{"cat:mrc_pedestrian_region", "1"},
          {"mrc_pedestrian_region:name", "zone_1"},
          {"mrc_pedestrian_region:status", "draft"},
          {"mrc_pedestrian_region:pedestrian_login_uid", TEST_UID_STR}}}
    };

    createCommit(COMMIT_2);
    tasksGenerator().consumeNewCommits();

    EXPECT_FALSE(ugcBackofficeMock().hasTask(TASK_ID));
}

TEST_F(TestFixture, test_delete_zone_delete_task)
{
    const std::vector<ObjectData> COMMIT_1 = {
        {101,
        {{"cat:mrc_pedestrian_region", "1"},
         {"mrc_pedestrian_region:name", "zone_1"},
         {"mrc_pedestrian_region:status", "can_start"},
         {"mrc_pedestrian_region:pedestrian_login_uid", TEST_UID_STR}},
        wkt2wkb("POLYGON ((10 10, 10 20, 20 20, 20 10, 10 10))")}
    };

    auto commit = createCommit(COMMIT_1);

    const std::string TASK_ID = "mrc_pedestrian:101";

    auto taskStatusMock = ugcBackofficeMock().addTaskStatusMock();
    auto taskCreateMock = ugcBackofficeMock().addTaskCreateMock();
    auto taskDeleteMock = ugcBackofficeMock().addTaskDeleteMock();

    tasksGenerator().consumeNewCommits();

    EXPECT_TRUE(ugcBackofficeMock().hasTask(TASK_ID));
    EXPECT_EQ(ugcBackofficeMock().getTask(TASK_ID).timesUpdated, 0u);

    // Delete zone
    const std::vector<ObjectData> COMMIT_2 = {
        {{101, commit.id()}, true}
    };

    createCommit(COMMIT_2);
    tasksGenerator().consumeNewCommits();

    EXPECT_FALSE(ugcBackofficeMock().hasTask(TASK_ID));
}

TEST_F(TestFixture, test_empty_uid)
{
    const std::vector<ObjectData> COMMIT_1 = {
        {101,
        {{"cat:mrc_pedestrian_region", "1"},
         {"mrc_pedestrian_region:name", "zone_1"},
         {"mrc_pedestrian_region:status", "can_start"},
         {"mrc_pedestrian_region:pedestrian_login_uid", ""}},
        wkt2wkb("POLYGON ((10 10, 10 20, 20 20, 20 10, 10 10))")}
    };

    auto commit = createCommit(COMMIT_1);

    const std::string TASK_ID = "mrc_pedestrian:101";

    auto taskStatusMock = ugcBackofficeMock().addTaskStatusMock();
    auto taskCreateMock = ugcBackofficeMock().addTaskCreateMock();

    tasksGenerator().consumeNewCommits();

    EXPECT_FALSE(ugcBackofficeMock().hasTask(TASK_ID));
}

TEST_F(TestFixture, test_zone_without_status)
{
    // Object has no status attribute
    const std::vector<ObjectData> COMMIT_1 = {
        {101,
        {{"cat:mrc_pedestrian_region", "1"},
         {"mrc_pedestrian_region:name", "zone_1"},
         {"mrc_pedestrian_region:pedestrian_login_uid", TEST_UID_STR}},
        wkt2wkb("POLYGON ((10 10, 10 20, 20 20, 20 10, 10 10))")}
    };

    auto commit = createCommit(COMMIT_1);

    const std::string TASK_ID = "mrc_pedestrian:101";

    auto taskStatusMock = ugcBackofficeMock().addTaskStatusMock();
    auto taskCreateMock = ugcBackofficeMock().addTaskCreateMock();

    tasksGenerator().consumeNewCommits();

    EXPECT_FALSE(ugcBackofficeMock().hasTask(TASK_ID));

    // Add status attribute
    const std::vector<ObjectData> COMMIT_2 = {
        {{101, commit.id()},
         {{"cat:mrc_pedestrian_region", "1"},
          {"mrc_pedestrian_region:name", "zone_1"},
          {"mrc_pedestrian_region:status", "can_start"},
          {"mrc_pedestrian_region:pedestrian_login_uid", TEST_UID_STR}}}
    };

    commit = createCommit(COMMIT_2);
    tasksGenerator().consumeNewCommits();

    EXPECT_TRUE(ugcBackofficeMock().hasTask(TASK_ID));
}

TEST_F(TestFixture, test_update_login_in_not_visible_status)
{
    const std::vector<ObjectData> COMMIT_1 = {
        {101,
        {{"cat:mrc_pedestrian_region", "1"},
         {"mrc_pedestrian_region:name", "zone_1"},
         {"mrc_pedestrian_region:status", "can_start"},
         {"mrc_pedestrian_region:pedestrian_login_uid", TEST_UID_STR}},
        wkt2wkb("POLYGON ((10 10, 10 20, 20 20, 20 10, 10 10))")}
    };

    auto commit = createCommit(COMMIT_1);

    const std::string TASK_ID = "mrc_pedestrian:101";

    auto taskStatusMock = ugcBackofficeMock().addTaskStatusMock();
    auto taskCreateMock = ugcBackofficeMock().addTaskCreateMock();

    tasksGenerator().consumeNewCommits();

    EXPECT_TRUE(ugcBackofficeMock().hasTask(TASK_ID, TEST_UID));
    auto task = ugcBackofficeMock().getTask(TASK_ID);
    EXPECT_EQ(task.timesUpdated, 0u);
    EXPECT_EQ(task.isIndoor, false);
    EXPECT_EQ(task.isPanoramic, false);

    // Update zone state from can_start to awaiting_check
    const std::vector<ObjectData> COMMIT_2 = {
        {{101, commit.id()},
         {{"cat:mrc_pedestrian_region", "1"},
          {"mrc_pedestrian_region:status", "awaiting_check"}}}
    };

    commit = createCommit(COMMIT_2);

    auto taskDeleteMock = ugcBackofficeMock().addTaskDeleteMock();
    tasksGenerator().consumeNewCommits();

    EXPECT_FALSE(ugcBackofficeMock().hasTask(TASK_ID));

    // Update pedestrian_login_uid attribute (zone is still awaiting_check)
    constexpr types::TUid TEST_UID_2 = 2222222;

    const std::vector<ObjectData> COMMIT_3 = {
        {{101, commit.id()},
         {{"cat:mrc_pedestrian_region", "1"},
          {"mrc_pedestrian_region:pedestrian_login_uid", std::to_string(TEST_UID_2)}}}
    };

    createCommit(COMMIT_3);
    tasksGenerator().consumeNewCommits();

    EXPECT_FALSE(ugcBackofficeMock().hasTask(TASK_ID, TEST_UID_2));
    EXPECT_FALSE(ugcBackofficeMock().hasTask(TASK_ID, TEST_UID));
}

} // maps::wiki::tasks::mrc_pedestrian::test

