#include "../fixtures.h"
#include "../../lib/def.h"
#include "../../lib/fbapi/consts.h"
#include "../../lib/fbapi/entrance_heuristics.h"
#include "../../lib/fbapi/processing.h"
#include "../../lib/fbapi/traits.h"
#include "../../lib/revision/entrance.h"

#include <maps/libs/http/include/test_utils.h>
#include <yandex/maps/wiki/geom_tools/conversion.h>
#include <yandex/maps/wiki/revision/revisionsgateway.h>
#include <yandex/maps/wiki/social/feedback/agent.h>
#include <yandex/maps/wiki/social/feedback/attribute_names.h>
#include <yandex/maps/wiki/social/feedback/description.h>
#include <yandex/maps/wiki/social/feedback/gateway_rw.h>
#include <yandex/maps/wiki/unittest/arcadia.h>

#include <maps/libs/geolib/include/serialization.h>

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

namespace maps::wiki::schedule_feedback::tests{

namespace chr = std::chrono;
namespace sf = social::feedback;
namespace sfa = social::feedback::attrs;

using NewRevData = revision::RevisionsGateway::NewRevisionData;

namespace {

const TId TEST_USER = 111;
const TId FEEDBACK_ID = 123;
const ObjId ADDR_OBJ_ID = 1234;
const uint64_t ENTR_NUM = 3;
const uint64_t ENTR_NUM_OTHER = 4;

const std::string SUBSOURCE_ADD_OBJECT_ENTRANCE = "qid__add_object__aid__entrance";
const std::string SUBSOURCE_WRONG_ADDRESS_REPORT_LOCATION = "qid__wrong_address__aid__report_location";
const std::string SUBSOURCE_WRONG_ENTRANCE_NOT_FOUND = "qid__wrong_entrance__aid__not_found";

auto DESCR()
{
    return sf::Description("descr");
}

geolib3::Point2 COORDS_ORIGIN(0., 0.);
const social::TUid USER_ID = 1001;

sf::Task addNewTask(const sf::TaskNew& newTask, pgpool3::Pool& pool)
{
    auto txn = pool.masterWriteableTransaction();
    auto task = sf::GatewayRW(txn.get()).addTask(USER_ID, newTask);
    txn->commit();
    return task;
}

void createNewEntrance(
    const geolib3::Point2& positionMerc,
    uint64_t entrNum,
    pgpool3::Pool& pool)
{
    auto txn = pool.masterWriteableTransaction();
    revision::RevisionsGateway gtw(txn.get());

    NewRevData entrance;
    entrance.first = gtw.acquireObjectId();
    entrance.second.geometry = geolib3::WKB::toString(positionMerc);
    entrance.second.attributes = revision::Attributes{{"cat:poi_entrance", "1"}};

    NewRevData entranceName;
    entranceName.first = gtw.acquireObjectId();
    entranceName.second.attributes = revision::Attributes{
        {"cat:poi_nm", "1"},
        {"poi_nm:name", std::to_string(entrNum)},
        {"poi_nm:lang", "ru"},
        {"poi_nm:is_local", "1"}
    };

    NewRevData relation;
    relation.first = gtw.acquireObjectId();
    relation.second.relationData = revision::RelationData(
        entrance.first.objectId(), entranceName.first.objectId());
    relation.second.attributes = revision::Attributes{
        {"rel:master", "poi_entrance"},
        {"rel:slave", "poi_nm"},
        {"rel:role", "official"}
    };

    gtw.createCommit(std::list<NewRevData>{entrance, entranceName, relation},
                     TEST_USER, {{"description", "data"}});

    txn->commit();
}

void createNewBuilding(
    const geolib3::Polygon2& polygonMerc,
    pgpool3::Pool& pool)
{
    auto txn = pool.masterWriteableTransaction();
    revision::RevisionsGateway gtw(txn.get());

    NewRevData building;
    building.first = gtw.acquireObjectId();
    building.second.geometry = geolib3::WKB::toString(polygonMerc);
    building.second.attributes = revision::Attributes{{"cat:bld", "1"}};

    gtw.createCommit(std::list<NewRevData>{building}, TEST_USER, {{"description", "data"}});
    txn->commit();
}

} // namespace anonymous

Y_UNIT_TEST_SUITE(fbapi_entrance_tests) {

Y_UNIT_TEST_F(traits, DBFixture)
{
    auto txn = pool().masterWriteableTransaction();
    sf::Agent agent(txn.get(), USER_ID);
    {
        sf::TaskNew newTask(COORDS_ORIGIN, sf::Type::Other, FBAPI, DESCR());
        auto task = agent.addTask(newTask);
        UNIT_ASSERT(isFbapiTask(task));
    }
    {
        sf::TaskNew newTask(COORDS_ORIGIN, sf::Type::Entrance,
                            "no-matter", DESCR());
        newTask.attrs.addCustom(sfa::SUBSOURCE, SUBSOURCE_ADD_OBJECT_ENTRANCE);
        auto task = agent.addTask(newTask);
        UNIT_ASSERT(isAddEntranceTask(task));
    }
    {
        sf::TaskNew newTask(COORDS_ORIGIN, sf::Type::Address,
                            "no-matter", DESCR());
        newTask.attrs.addCustom(sfa::SUBSOURCE, SUBSOURCE_WRONG_ADDRESS_REPORT_LOCATION);
        auto task = agent.addTask(newTask);
        UNIT_ASSERT(isAddressReportLocationTask(task));
    }
}

Y_UNIT_TEST_F(fbapi_entrance_convertion, DBFixture)
{
    auto txn = pool().masterWriteableTransaction();
    sf::Agent agent(txn.get(), USER_ID);

    {
        // bad source
        sf::TaskNew newTask(COORDS_ORIGIN, sf::Type::Entrance,
                            "panamabanana", DESCR());
        newTask.attrs.addCustom(sfa::SUBSOURCE, SUBSOURCE_ADD_OBJECT_ENTRANCE);
        auto task = agent.addTask(newTask);
        UNIT_ASSERT_EXCEPTION(socialTaskToEntrance(task), LogicError);
    }
    {
        // bad type
        sf::TaskNew newTask(COORDS_ORIGIN, sf::Type::Other, FBAPI, DESCR());
        newTask.attrs.addCustom(sfa::SUBSOURCE, SUBSOURCE_ADD_OBJECT_ENTRANCE);
        auto task = agent.addTask(newTask);
        UNIT_ASSERT_EXCEPTION(socialTaskToEntrance(task), LogicError);
    }
    {
        // bad subsource
        sf::TaskNew newTask(COORDS_ORIGIN, sf::Type::Entrance, FBAPI, DESCR());
        newTask.attrs.addCustom(sfa::SUBSOURCE, SUBSOURCE_WRONG_ENTRANCE_NOT_FOUND);
        auto task = agent.addTask(newTask);
        UNIT_ASSERT_EXCEPTION(socialTaskToEntrance(task), LogicError);
    }

    sf::TaskNew commonNewTask(COORDS_ORIGIN, sf::Type::Entrance,
                              FBAPI, DESCR());
    commonNewTask.attrs.addCustom(sfa::SUBSOURCE, SUBSOURCE_ADD_OBJECT_ENTRANCE);

    {
        // no object id
        auto task = agent.addTask(commonNewTask);
        UNIT_ASSERT_EXCEPTION(socialTaskToEntrance(task), EntranceObjectIdNotExist);
    }
    {
        // no 'entrance name' attribute
        auto newTask = commonNewTask;
        newTask.objectId = ADDR_OBJ_ID;
        auto task = agent.addTask(newTask);
        UNIT_ASSERT_EXCEPTION(socialTaskToEntrance(task), ExtractEntranceNameError);
    }
    {
        // 'entrance name' attribute is not convertible to uint64_t
        auto newTask = commonNewTask;
        newTask.objectId = ADDR_OBJ_ID;
        newTask.attrs.addCustom(sfa::ENTRANCE_NAME, "asdf");
        auto task = agent.addTask(newTask);
        UNIT_ASSERT_EXCEPTION(socialTaskToEntrance(task), ExtractEntranceNameError);
    }
    {
        // everything is correct
        auto newTask = commonNewTask;
        newTask.objectId = ADDR_OBJ_ID;
        newTask.attrs.addCustom(sfa::ENTRANCE_NAME, std::to_string(ENTR_NUM));
        auto task = agent.addTask(newTask);

        auto entrance = socialTaskToEntrance(task);
        UNIT_ASSERT_VALUES_EQUAL(entrance.addressObjectId, ADDR_OBJ_ID);
        UNIT_ASSERT_VALUES_EQUAL(entrance.number, ENTR_NUM);
        UNIT_ASSERT_DOUBLES_EQUAL(entrance.positionMerc.x(), 0., 1.e-6);
        UNIT_ASSERT_DOUBLES_EQUAL(entrance.positionMerc.y(), 0., 1.e-6);
    }
}

Y_UNIT_TEST(entrance_number_is_too_big)
{
    {
        EntranceFb entr{FEEDBACK_ID, chr::system_clock::now(),
                        COORDS_ORIGIN, ADDR_OBJ_ID, 36};
        UNIT_ASSERT(entranceNumberIsTooBig(entr, 36) == false);
    }
    {
        EntranceFb entr{FEEDBACK_ID, chr::system_clock::now(),
                        COORDS_ORIGIN, ADDR_OBJ_ID, 37};
        UNIT_ASSERT(entranceNumberIsTooBig(entr, 36));
    }
}

Y_UNIT_TEST_F(entrance_exists_in_vicinity, DBFixture)
{
    geolib3::Point2 revEntrancePos(0, 0);
    createNewEntrance(revEntrancePos, ENTR_NUM, pool());

    auto coreReadTxn = pool().masterReadOnlyTransaction();

    {
        // Rev entrance exists
        EntranceFb entr{FEEDBACK_ID, chrono::TimePoint{},
                        revEntrancePos, ADDR_OBJ_ID, ENTR_NUM};
        UNIT_ASSERT(entranceExistsInVicinity(entr, 1, coreReadTxn.get()));
    }
    {
        // Entrance names differs
        EntranceFb entr{FEEDBACK_ID, chrono::TimePoint{}, revEntrancePos,
                        ADDR_OBJ_ID, ENTR_NUM_OTHER};
        UNIT_ASSERT(
            entranceExistsInVicinity(entr, 1, coreReadTxn.get()) == false);
    }
    {
        // Rev entrance is out of feedback vicinity
        double mercRadius =
            geom_tools::correctGeoDistanceToMerc(10, revEntrancePos);

        geolib3::Point2 entrancePos(mercRadius, mercRadius);
        EntranceFb entr{FEEDBACK_ID, chrono::TimePoint{}, entrancePos,
                        ADDR_OBJ_ID, ENTR_NUM};
        UNIT_ASSERT(
            entranceExistsInVicinity(entr, 9, coreReadTxn.get()) == false);
    }
}

Y_UNIT_TEST_F(faraway_from_any_building, DBFixture)
{
    geolib3::Polygon2 buildingPolygon(
        std::vector<geolib3::Point2>{
            {1, 1}, {-1, 1}, {-1, -1}, {1, -1}
        }
    );
    createNewBuilding(buildingPolygon, pool());

    auto coreReadTxn = pool().masterReadOnlyTransaction();

    {
        // Building exists in feedback entrance vicinity
        double mercRadius = 2;
        geolib3::Point2 entrancePos(mercRadius, mercRadius);
        double geoRadius =
            geom_tools::correctMercDistanceToGeo(mercRadius, entrancePos);

        EntranceFb entr{FEEDBACK_ID, chrono::TimePoint{}, entrancePos,
                        ADDR_OBJ_ID, ENTR_NUM};
        UNIT_ASSERT(farawayFromAnyBuilding(
            entr, geoRadius, coreReadTxn.get()) == false);
    }
    {
        // Feedback entrance is inside building
        geolib3::Point2 entrancePos(0, 0);
        double geoRadius =
            geom_tools::correctMercDistanceToGeo(0.5, entrancePos);

        EntranceFb entr{FEEDBACK_ID, chrono::TimePoint{}, entrancePos,
                        ADDR_OBJ_ID, ENTR_NUM};
        UNIT_ASSERT(farawayFromAnyBuilding(
            entr, geoRadius, coreReadTxn.get()) == false);
    }
    {
        // Feedback entrance is faraway from building
        geolib3::Point2 entrancePos(2, 2);
        double geoRadius =
            geom_tools::correctMercDistanceToGeo(0.5, entrancePos);

        EntranceFb entr{FEEDBACK_ID, chrono::TimePoint{}, entrancePos,
                        ADDR_OBJ_ID, ENTR_NUM};
        UNIT_ASSERT(farawayFromAnyBuilding(entr, geoRadius, coreReadTxn.get()));
    }
}

Y_UNIT_TEST_F(is_duplicate_with_previous_social_feedback, DBFixture)
{
    sf::TaskNew newTask(COORDS_ORIGIN, sf::Type::Entrance, FBAPI, DESCR());
    newTask.objectId = ADDR_OBJ_ID;
    newTask.attrs.addCustom(sfa::ENTRANCE_NAME, std::to_string(ENTR_NUM));
    newTask.attrs.addCustom(sfa::SUBSOURCE, SUBSOURCE_ADD_OBJECT_ENTRANCE);
    auto task = addNewTask(newTask, pool());

    double farawayDist =
        geom_tools::correctGeoDistanceToMerc(10 + 1, geolib3::Point2(0., 0.));

    auto readTxn = pool().masterReadOnlyTransaction();
    sf::GatewayRO gatewayRo(readTxn.get());

    {
        // duplicate exists
        EntranceFb entr{task.id() + 1, task.createdAt(), task.position(),
                        ADDR_OBJ_ID, ENTR_NUM};
        UNIT_ASSERT(isDuplicateWithPreviousSocialFeedback(
            entr, gatewayRo));
    }
    {
        // same social task ids
        EntranceFb entr{task.id(), task.createdAt(), task.position(),
                        ADDR_OBJ_ID, ENTR_NUM};
        UNIT_ASSERT(isDuplicateWithPreviousSocialFeedback(
            entr, gatewayRo) == false);
    }
    {
        // no earlier social tasks
        EntranceFb entr{task.id() + 1, task.createdAt() - chr::hours(1),
                      task.position(), ADDR_OBJ_ID, ENTR_NUM};
        UNIT_ASSERT(isDuplicateWithPreviousSocialFeedback(
            entr, gatewayRo) == false);
    }
    {
        // entrances numbers differs
        EntranceFb entr{task.id() + 1, task.createdAt(), task.position(),
                        ADDR_OBJ_ID, ENTR_NUM_OTHER};
        UNIT_ASSERT(isDuplicateWithPreviousSocialFeedback(
            entr, gatewayRo) == false);
    }
    {
        // no social task in bbox vicinity
        EntranceFb entr{task.id() + 1, task.createdAt(),
                        geolib3::Point2(farawayDist, farawayDist),
                        ADDR_OBJ_ID, ENTR_NUM};
        UNIT_ASSERT(isDuplicateWithPreviousSocialFeedback(
            entr, gatewayRo) == false);
    }
    {
        // no tasks with source = 'fbapi' in vicinity
        sf::TaskNew newTask(geolib3::Point2(farawayDist, farawayDist),
                            sf::Type::Entrance, "source", DESCR());
        newTask.objectId = ADDR_OBJ_ID;
        newTask.attrs.addCustom(sfa::ENTRANCE_NAME, std::to_string(ENTR_NUM));
        newTask.attrs.addCustom(sfa::SUBSOURCE, SUBSOURCE_ADD_OBJECT_ENTRANCE);

        auto task = addNewTask(newTask, pool());

        EntranceFb entr{task.id() + 1, task.createdAt(), task.position(),
                        ADDR_OBJ_ID, ENTR_NUM};
        UNIT_ASSERT(isDuplicateWithPreviousSocialFeedback(
            entr, gatewayRo) == false);
    }
    {
        // no tasks with type = 'Entrance' in vicinity
        sf::TaskNew newTask(geolib3::Point2(farawayDist, farawayDist),
                            sf::Type::Other, FBAPI, DESCR());
        newTask.objectId = ADDR_OBJ_ID;
        newTask.attrs.addCustom(sfa::ENTRANCE_NAME, std::to_string(ENTR_NUM));
        newTask.attrs.addCustom(sfa::SUBSOURCE, SUBSOURCE_ADD_OBJECT_ENTRANCE);

        auto task = addNewTask(newTask, pool());

        EntranceFb entr{task.id() + 1, task.createdAt(), task.position(),
                        ADDR_OBJ_ID, ENTR_NUM};
        UNIT_ASSERT(isDuplicateWithPreviousSocialFeedback(
            entr, gatewayRo) == false);
    }
}

Y_UNIT_TEST_F(should_be_rejected, DBFixture)
{
    auto socialTxn = pool().masterWriteableTransaction();
    sf::Agent agent(socialTxn.get(), USER_ID);
    auto coreTxn = pool().masterReadOnlyTransaction();
    // input asserts
    {
        // type is incorrect
        sf::TaskNew newTask(COORDS_ORIGIN, sf::Type::Other, FBAPI, DESCR());
        newTask.attrs.addCustom(sfa::SUBSOURCE, SUBSOURCE_ADD_OBJECT_ENTRANCE);
        auto task = agent.addTask(newTask);
        UNIT_ASSERT_EXCEPTION(entranceTaskHeuristicsAction(
            task, coreTxn.get(), agent.gatewayRo()), LogicError);
    }
    {
        // subsource is incorrect
        sf::TaskNew newTask(COORDS_ORIGIN, sf::Type::Entrance,
                            FBAPI, DESCR());
        newTask.attrs.addCustom(sfa::SUBSOURCE, SUBSOURCE_WRONG_ENTRANCE_NOT_FOUND);
        auto task = agent.addTask(newTask);
        UNIT_ASSERT_EXCEPTION(entranceTaskHeuristicsAction(
            task, coreTxn.get(), agent.gatewayRo()), LogicError);
    }
    {
        // source is incorrect
        sf::TaskNew newTask(COORDS_ORIGIN, sf::Type::Entrance,
                            "no-matter", DESCR());
        newTask.attrs.addCustom(sfa::SUBSOURCE, SUBSOURCE_ADD_OBJECT_ENTRANCE);
        auto task = agent.addTask(newTask);
        UNIT_ASSERT_EXCEPTION(entranceTaskHeuristicsAction(
            task, coreTxn.get(), agent.gatewayRo()), LogicError);
    }
}

namespace {

void addBuildingAtCoordsOrigin(pgpool3::Pool& pool)
{
    geolib3::Polygon2 buildingPolygon(
        std::vector<geolib3::Point2>{
            {1, 1}, {-1, 1}, {-1, -1}, {1, -1}
        }
    );
    createNewBuilding(buildingPolygon, pool);
}

sf::Task addAndProcessNewTask(const sf::TaskNew& newTask, pgpool3::Pool& pool)
{
    auto task = addNewTask(newTask, pool);
    auto taskId = task.id();

    {
        auto coreTxn = pool.masterReadOnlyTransaction();
        auto socialTxn = pool.masterWriteableTransaction();
        sf::GatewayRW gatewayRw(socialTxn.get());
        auto actualTask = gatewayRw.taskForUpdateById(taskId);
        ASSERT(actualTask);
        sf::Agent agent(socialTxn.get(), USER_ID);
        processIncomingFbapiTasks(coreTxn.get(), agent, {*actualTask});
        socialTxn->commit();
    }

    auto socialTxn = pool.masterReadOnlyTransaction();
    auto updatedTask = sf::GatewayRO(socialTxn.get()).taskById(taskId);
    ASSERT(updatedTask != std::nullopt);

    return *updatedTask;
}

sf::TaskNew newTypicalAddEntranceTask()
{
    sf::TaskNew newTask(COORDS_ORIGIN, sf::Type::Entrance, FBAPI, DESCR());
    newTask.objectId = ADDR_OBJ_ID;
    newTask.attrs.addCustom(sfa::ENTRANCE_NAME, std::to_string(ENTR_NUM));
    newTask.attrs.addCustom(sfa::SUBSOURCE, SUBSOURCE_ADD_OBJECT_ENTRANCE);
    return newTask;
}

} // namespace anonymous

Y_UNIT_TEST_F(process_incoming_entrance_tasks_not_filtered, DBFixture)
{
    auto mockAntifraud = maps::http::addMock(
        "http://router-prod.clean-web.yandex.net/v2",
        [](const maps::http::MockRequest&) {
            return maps::http::MockResponse::withStatus(200);
        }
    );


    // need to have building, otherwise feedback will be filtered
    addBuildingAtCoordsOrigin(pool());

    auto task = addAndProcessNewTask(newTypicalAddEntranceTask(), pool());

    UNIT_ASSERT_EQUAL(task.bucket(), sf::Bucket::Outgoing);
    UNIT_ASSERT_EQUAL(task.resolved(), std::nullopt);
}

//Y_UNIT_TEST_F(process_incoming_entrance_tasks_filtered, DBFixture)
//{
//    // no building => feedback is filtered
//    auto task = addAndProcessNewTask(newTypicalAddEntranceTask(), pool());
//
//    UNIT_ASSERT_EQUAL(task.bucket(), sf::Bucket::Outgoing);
//    UNIT_ASSERT(task.resolved() != std::nullopt);
//    UNIT_ASSERT_EQUAL(task.resolved()->resolution, sf::Resolution::createRejected());
//}

//Y_UNIT_TEST_F(process_incoming_report_location_fbapi_task, DBFixture)
//{
//    sf::TaskNew newTask(COORDS_ORIGIN, sf::Type::Address, FBAPI, DESCR());
//    newTask.attrs.addCustom(sfa::SUBSOURCE, SUBSOURCE_WRONG_ADDRESS_REPORT_LOCATION);
//
//    auto task = addAndProcessNewTask(newTask, pool());
//
//    UNIT_ASSERT_EQUAL(task.bucket(), sf::Bucket::Outgoing);
//    UNIT_ASSERT(task.resolved() != std::nullopt);
//    UNIT_ASSERT_EQUAL(task.resolved()->resolution, sf::Resolution::createRejected());
//}

} // Y_UNIT_TEST_SUITE(fbapi_entrance_tests)

} // namespace maps::wiki::schedule_feedback::tests
