#include <maps/wikimap/mapspro/services/tasks_feedback/src/reject_feedback_worker/lib/worker.h>

#include <yandex/maps/wiki/common/robot.h>
#include <maps/libs/log8/include/log8.h>
#include <maps/libs/geolib/include/conversion.h>
#include <yandex/maps/wiki/social/feedback/agent.h>
#include <yandex/maps/wiki/social/feedback/task_new.h>
#include <maps/wikimap/mapspro/libs/social/magic_strings.h>

#include <yandex/maps/wiki/common/string_utils.h>
#include <maps/libs/enum_io/include/enum_io.h>

#include <yandex/maps/wiki/unittest/arcadia.h>
#include <yandex/maps/wiki/unittest/localdb.h>

namespace maps::wiki::tasks_feedback::reject_fb::tests {

namespace sf = social::feedback;
using sf::RejectReason;
using sf::Workflow;
using sf::TaskNew;
using sf::Type;

using geolib3::Point2;
using geolib3::Polygon2;
using geolib3::geoPoint2Mercator;
using geolib3::convertGeodeticToMercator;
using geolib3::convertMercatorToGeodetic;

using common::ROBOT_UID;

namespace {

const Polygon2 GEO_POLYGON{
    {
        Point2(37., 55.), Point2(37., 56.),
        Point2(38., 56.), Point2(38., 55.),
        Point2(37., 55.)
    }
};

const Point2 GEO_POINT_INSIDE_POLYGON{37.5, 55.5};
const Point2 GEO_POINT_OUTSIDE_POLYGON{38.5, 55.5};

const std::string SRC_FEEDBACK = "fbapi";
const std::string SRC_1 = "src1";
const std::string SRC_2 = "src2,'escaped_chars\t_check";

const std::string REJECTED_FEEDBACK_ID = "rejected_feedback_id";


struct DBFixture : unittest::ArcadiaDbFixture
{
    pqxx::connection conn;

    DBFixture() : conn(connectionString())
    {
        log8::setLevel(log8::Level::INFO);

    }
};

const std::string SEPARATOR = ",";

template<class T>
std::string joinedStr(
    pqxx::transaction_base& txn,
    const std::vector<T>& v)
{
    return common::join(
        v,
        [&txn](const T& el) { return txn.quote(std::string(toString(el))); },
        SEPARATOR
    );
}
template<>
std::string joinedStr(
    pqxx::transaction_base& txn,
    const std::vector<std::string>& v)
{
    return common::join(
        v,
        [&txn](const std::string& el) { return txn.quote(el); },
        SEPARATOR
    );
}

template<class T>
std::string arrayString(
    pqxx::transaction_base& txn,
    const std::vector<T>& v)
{
    return "ARRAY[" + joinedStr(txn, v) + ']';
}

// NOTE: Copied from here:
//  https://a.yandex-team.ru/arc/trunk/arcadia/maps/wikimap/mapspro/libs/social/helpers.h?rev=r5899845#L42
//  May be need to move it into some kind of common header?
template<class Geometry>
std::string makePqxxGeomExpr(
    pqxx::transaction_base& txn,
    const Geometry& geometry)
{
    auto wkb = geolib3::WKB::toString(geometry);
    std::stringstream result;
    result << social::sql::func::ST_GEOMFROMWKB << "("
           << "'" << txn.esc_raw(wkb) << "', " << social::sql::value::MERCATOR_SRID
           << ")";
    return result.str();
}

revision::DBID addRejectFeedbackTask(
    pqxx::transaction_base& txn,
    const Polygon2& geoPolygon,
    const std::vector<Workflow>& workflows,
    const std::vector<std::string>& sources,
    const std::vector<Type>& types,
    RejectReason rejectReason)
{
    // create task in base table
    const auto dbTaskId = [&txn]() {
        const std::string query =
            "INSERT INTO service.task (type, created_by, modified_by) "
            " VALUES ('reject_feedback', 1, 1) RETURNING id";
        const auto r = txn.exec1(query);
        return r["id"].as<revision::DBID>();
    }();

    { // create long task.
        const auto mercPolygon = convertGeodeticToMercator(geoPolygon);

        std::stringstream query;
        query << "INSERT INTO service.reject_feedback_task"
              << " (id, aoi, workflows, sources, types, reject_reason) "
              << " VALUES (" << dbTaskId << ", "
              << makePqxxGeomExpr(txn, mercPolygon) << ","
              << arrayString(txn, workflows) << "::social.feedback_workflow[],"
              << arrayString(txn, sources) << "::text[],"
              << arrayString(txn, types) << "::social.feedback_type[],"
              << " '" << rejectReason << '\''
              << " )";
        INFO() << "Create task query: " << query.str();
        txn.exec(query.str());
    }
    return dbTaskId;
}

bool operator==(const Polygon2& lhs, const Polygon2& rhs)
{
    if (lhs.totalPointsNumber() != rhs.totalPointsNumber()) {
        return false;
    }
    for (size_t i = 0; i < lhs.totalPointsNumber(); ++i) {
        if (lhs.pointAt(i) != rhs.pointAt(i)) {
            return false;
        }
    }
    return true;
}

social::TId createTask(
    pqxx::transaction_base& txn,
    const Point2& geoPosition,
    const std::string& source,
    Type type)
{
    TaskNew task{
        convertGeodeticToMercator(geoPosition),
        type,
        source,
        sf::Description{}};

    sf::Agent agent{txn, ROBOT_UID};
    const auto createdTask = agent.addTask(task);
    UNIT_ASSERT(agent.revealTaskByIdCascade(createdTask.id()).has_value());
    return createdTask.id();
}

} // anonymous namespace

Y_UNIT_TEST_SUITE(task_params_tests) {

Y_UNIT_TEST_F(parse_polygon_and_empty_arrays, DBFixture)
{
    pqxx::work txn(conn);
    const auto dbTaskId = addRejectFeedbackTask(
        txn,
        GEO_POLYGON,
        {}, // Workflows
        {}, // sources
        {}, // types
        RejectReason::Spam
    );
    const TaskParams expected{
        dbTaskId,
        convertGeodeticToMercator(GEO_POLYGON),
        {}, // Workflows
        {}, // sources
        {}, // types
        sf::RejectReason::Spam
    };

    const auto actual = TaskParams::fromTxn(txn, dbTaskId);

    UNIT_ASSERT_EQUAL(expected.dbTaskId(), actual.dbTaskId());
    UNIT_ASSERT_EQUAL(expected.polygon(), actual.polygon());
    UNIT_ASSERT_EQUAL(expected.workflows(), actual.workflows());
    UNIT_ASSERT_EQUAL(expected.sources(), actual.sources());
    UNIT_ASSERT_EQUAL(expected.types(), actual.types());
    UNIT_ASSERT_EQUAL(expected.rejectReason(), actual.rejectReason());
} // Y_UNIT_TEST_F

Y_UNIT_TEST_F(parse_params_arrays, DBFixture)
{
    pqxx::work txn(conn);
    const std::vector workflows{Workflow::Task, Workflow::Feedback};
    const std::vector<std::string> sources{SRC_1, SRC_2};
    const std::vector types{Type::Poi, Type::Road, Type::NoRoad};

    const auto dbTaskId = addRejectFeedbackTask(
        txn,
        GEO_POLYGON,
        workflows,
        sources,
        types,
        RejectReason::Spam
    );
    const TaskParams expected{
        dbTaskId,
        convertGeodeticToMercator(GEO_POLYGON),
        workflows,
        sources,
        types,
        sf::RejectReason::Spam
    };

    const auto actual = TaskParams::fromTxn(txn, dbTaskId);

    UNIT_ASSERT_EQUAL(expected.dbTaskId(), actual.dbTaskId());
    UNIT_ASSERT_EQUAL(expected.polygon(), actual.polygon());
    UNIT_ASSERT_EQUAL(expected.workflows(), actual.workflows());
    UNIT_ASSERT_EQUAL(expected.sources(), actual.sources());
    UNIT_ASSERT_EQUAL(expected.types(), actual.types());
    UNIT_ASSERT_EQUAL(expected.rejectReason(), actual.rejectReason());
} // Y_UNIT_TEST_F

} // Y_UNIT_TEST_SUITE

Y_UNIT_TEST_SUITE(reject_tasks_tests) {

Y_UNIT_TEST_F(zero_tasks, DBFixture)
{
    const auto longTaskId = [&]() { // add reject task.
        pqxx::work txn{conn};
        const auto dbTaskId = addRejectFeedbackTask(
            txn,
            GEO_POLYGON,
            {Workflow::Feedback},
            {SRC_1},
            {Type::Road},
            RejectReason::Spam
        );
        txn.commit();
        return dbTaskId;
    }();

    const auto taskParams = [&]() {
        pqxx::work txn{conn};
        return TaskParams::fromTxn(txn, longTaskId);
    }();

    { // check reject.
        pqxx::work txn(conn);
        const auto rejectedIds = impl::rejectTasks(txn, taskParams);
        UNIT_ASSERT(rejectedIds.empty());
    }
} // Y_UNIT_TEST_F

Y_UNIT_TEST_F(check_filters, DBFixture)
{
    const auto chosenWorkflow = Workflow::Feedback;
    const auto chosenSource = SRC_FEEDBACK;
    const auto chosenType = Type::Road;

    const auto differentSource = SRC_2;
    const auto differentType = Type::NoRoad;

    const auto longTaskId = [&]() { // add reject task.
        pqxx::work txn{conn};
        const auto dbTaskId = addRejectFeedbackTask(
            txn,
            GEO_POLYGON,
            {chosenWorkflow},
            {chosenSource},
            {chosenType},
            RejectReason::Spam
        );
        txn.commit();
        return dbTaskId;
    }();

    const auto taskParams = [&]() {
        pqxx::work txn{conn};
        return TaskParams::fromTxn(txn, longTaskId);
    }();

    const auto expectedIds = [&]() {
        pqxx::work txn(conn);
        social::TIds expectedIds;

        const auto taskId = createTask( // the only task meeting all filters.
            txn, GEO_POINT_INSIDE_POLYGON, chosenSource, chosenType);
        expectedIds.insert(taskId);

        // create tasks not meeting all the filters.
        createTask(txn, GEO_POINT_OUTSIDE_POLYGON, chosenSource, chosenType);
        createTask(txn, GEO_POINT_INSIDE_POLYGON, differentSource, chosenType);
        createTask(txn, GEO_POINT_INSIDE_POLYGON, chosenSource, differentType);

        txn.commit();
        return expectedIds;
    }();

    { // check reject.
        pqxx::work txn(conn);
        const auto rejectedIds = impl::rejectTasks(txn, taskParams);
        UNIT_ASSERT_EQUAL(rejectedIds.size(), 1);
        UNIT_ASSERT_EQUAL(rejectedIds, expectedIds);
    }
} // Y_UNIT_TEST_F

Y_UNIT_TEST_F(more_tasks_than_batch_size, DBFixture)
{
    const auto workflow = Workflow::Feedback;
    const auto source = SRC_FEEDBACK;
    const auto type = Type::Road;
    const auto rejectReason = RejectReason::NoInfo;

    const auto longTaskId = [&]() { // add reject task.
        pqxx::work txn{conn};
        const auto dbTaskId = addRejectFeedbackTask(
            txn, GEO_POLYGON, {workflow}, {source}, {type}, rejectReason);
        txn.commit();
        return dbTaskId;
    }();

    const auto taskParams = [&]() {
        pqxx::work txn{conn};
        return TaskParams::fromTxn(txn, longTaskId);
    }();

    const auto expectedIds = [&]() {
        pqxx::work txn(conn);
        social::TIds expectedIds;
        const size_t tasksNumber = (taskParams.batchSize() / 2) * 5;
        for (size_t i = 0; i < tasksNumber; ++i) {
            const auto taskId = createTask(
                txn, GEO_POINT_INSIDE_POLYGON, source, type);
            expectedIds.insert(taskId);
        }
        txn.commit();
        return expectedIds;
    }();

    { // check reject.
        pqxx::work txn(conn);
        const auto rejectedIds = impl::rejectTasks(txn, taskParams);
        UNIT_ASSERT_EQUAL(rejectedIds, expectedIds);
    }
} // Y_UNIT_TEST_F

Y_UNIT_TEST_F(rejected_tasks_count_equals_batch_size, DBFixture)
{
    const auto workflow = Workflow::Feedback;
    const auto source = SRC_FEEDBACK;
    const auto type = Type::Road;
    const auto rejectReason = RejectReason::NoInfo;

    const auto longTaskId = [&]() { // add reject task.
        pqxx::work txn{conn};
        const auto dbTaskId = addRejectFeedbackTask(
            txn, GEO_POLYGON, {workflow}, {source}, {type}, rejectReason);
        txn.commit();
        return dbTaskId;
    }();

    const auto taskParams = [&]() {
        pqxx::work txn{conn};
        return TaskParams::fromTxn(txn, longTaskId);
    }();

    const auto expectedIds = [&]() {
        pqxx::work txn(conn);
        social::TIds expectedIds;
        const auto tasksNumber = taskParams.batchSize();
        for (size_t i = 0; i < tasksNumber; ++i) {
            const auto taskId = createTask(
                txn, GEO_POINT_INSIDE_POLYGON, source, type);
            expectedIds.insert(taskId);
        }
        txn.commit();
        return expectedIds;
    }();

    { // check reject.
        pqxx::work txn(conn);
        const auto rejectedIds = impl::rejectTasks(txn, taskParams);
        UNIT_ASSERT_EQUAL(rejectedIds, expectedIds);
    }
} // Y_UNIT_TEST_F

} // Y_UNIT_TEST_SUITE

Y_UNIT_TEST_SUITE(save_result_tests) {

Y_UNIT_TEST_F(ids_number_equals_batch_size, DBFixture)
{
    const auto longTaskId = [&]() { // add reject task.
        pqxx::work txn{conn};
        const auto dbTaskId = addRejectFeedbackTask(
            txn,
            GEO_POLYGON,
            {},
            {},
            {},
            RejectReason::Spam
        );
        txn.commit();
        return dbTaskId;
    }();

    const auto idsNumber = TaskParams::batchSize();
    social::TIds expectedIds;
    for (size_t i = 1; i <= idsNumber; ++i) {
        expectedIds.insert(i);
    }

    { // save expectedIds to db
        pqxx::work txn(conn);
        impl::saveRejectedTaskIds(longTaskId, expectedIds, txn, TaskParams::batchSize());
        txn.commit();
    }

    { // check expectedIds are saved
        pqxx::work txn{conn};

        const auto rows = txn.exec(
            "SELECT " + REJECTED_FEEDBACK_ID + " from service.reject_feedback_result"
            " WHERE task_id = " + std::to_string(longTaskId));

        social::TIds actualIds;
        for (const auto& r: rows) {
            actualIds.insert(r[REJECTED_FEEDBACK_ID].as<social::TId>());
        }
        UNIT_ASSERT_EQUAL(expectedIds, actualIds);
    }
} // Y_UNIT_TEST_F

Y_UNIT_TEST_F(more_ids_than_batch_size, DBFixture)
{
    const auto longTaskId = [&]() { // add reject task.
        pqxx::work txn{conn};
        const auto dbTaskId = addRejectFeedbackTask(
            txn,
            GEO_POLYGON,
            {},
            {},
            {},
            RejectReason::Spam
        );
        txn.commit();
        return dbTaskId;
    }();

    const size_t idsNumber = (TaskParams::batchSize() / 2) * 5;
    social::TIds expectedIds;
    for (size_t i = 1; i <= idsNumber; ++i) {
        expectedIds.insert(i);
    }

    { // save expectedIds to db
        pqxx::work txn(conn);
        impl::saveRejectedTaskIds(longTaskId, expectedIds, txn, TaskParams::batchSize());
        txn.commit();
    }

    { // check expectedIds are saved
        pqxx::work txn{conn};

        const auto rows = txn.exec(
            "SELECT " + REJECTED_FEEDBACK_ID + " from service.reject_feedback_result"
            " WHERE task_id = " + std::to_string(longTaskId));

        social::TIds actualIds;
        for (const auto& r: rows) {
            actualIds.insert(r[REJECTED_FEEDBACK_ID].as<social::TId>());
        }
        UNIT_ASSERT_EQUAL(expectedIds, actualIds);
    }
} // Y_UNIT_TEST_F

Y_UNIT_TEST_F(zero_ids, DBFixture)
{
    const auto longTaskId = [&]() { // add reject task.
        pqxx::work txn{conn};
        const auto dbTaskId = addRejectFeedbackTask(
            txn,
            GEO_POLYGON,
            {},
            {},
            {},
            RejectReason::Spam
        );
        txn.commit();
        return dbTaskId;
    }();

    {
        pqxx::work txn(conn);
        impl::saveRejectedTaskIds(longTaskId, social::TIds{}, txn, TaskParams::batchSize());
        txn.commit();
    }

    {
        pqxx::work txn{conn};

        const auto rows = txn.exec(
            "SELECT " + REJECTED_FEEDBACK_ID + " from service.reject_feedback_result"
            " WHERE task_id = " + std::to_string(longTaskId));

        UNIT_ASSERT(rows.empty());
    }
} // Y_UNIT_TEST_F

} // Y_UNIT_TEST_SUITE

} // namespace maps::wiki::tasks_feedback::reject_fb::tests
