#include "common.h"
#include "fixture.h"

#include <maps/wikimap/mapspro/services/mrc/libs/db/include/feature_gateway.h>
#include <maps/wikimap/mapspro/services/mrc/libs/db/include/walk_object_gateway.h>

#include <yandex/maps/mrc/unittest/database_fixture.h>
#include <yandex/maps/mrc/unittest/local_server.h>
#include <yandex/maps/mrc/unittest/unittest_config.h>
#include <yandex/maps/proto/common2/geometry.sproto.h>
#include <yandex/maps/proto/offline-mrc/results.sproto.h>

#include <maps/infra/tvm2lua/ticket_parser2_lua/lib/lua_api.h>
#include <maps/infra/yacare/include/test_utils.h>

#include <maps/libs/auth/include/blackbox.h>
#include <maps/libs/common/include/file_utils.h>
#include <maps/libs/http/include/http.h>
#include <maps/libs/geolib/include/point.h>
#include <maps/libs/introspection/include/comparison.h>

#include <yandex/maps/geolib3/proto.h>
#include <yandex/maps/geolib3/sproto.h>

#include <library/cpp/testing/gmock_in_unittest/gmock.h>

#include <future>
#include <sstream>
#include <thread>

using namespace std::literals;
using namespace testing;

namespace maps::mrc::agent_proxy {

bool operator==(const PedestrianZone& lhs, const PedestrianZone& rhs)
{
    return std::make_tuple(lhs.objectId(), lhs.category(), lhs.status(), lhs.assigneeLogin())
        == std::make_tuple(rhs.objectId(), rhs.category(), rhs.status(), rhs.assigneeLogin());
}

} // namespace maps::mrc::agent_proxy

namespace presults = yandex::maps::sproto::offline::mrc::results;

namespace maps::mrc::agent_proxy::tests {
namespace {

const std::string REMOTE_ADDR = "127.0.0.1";
const std::string OAUTH_HEADER = "OAuth b1070fac00ce46388a2f2645dfff09c6";
const std::string DEVICE_ID = "MyFluffyPhone";
const std::string TASK_ID = "task-id";

const std::string WALK_UPLOAD_URL
    = "http://localhost/walks/upload?deviceid=" + DEVICE_ID;

const std::string PEDESTRIAN_UPLOAD_URL
    = "http://localhost/pedestrian_task/upload"
                           "?deviceid=" + DEVICE_ID
                           + "&task_id=" + TASK_ID;

const std::string PEDESTRIAN_TASK_COMPLETE_URL
    = "http://localhost/pedestrian_task/complete";

const std::string MDS_GROUP = "123";
const std::string MDS_PATH = "unittest/walk/images/1";
const std::string MDS_PATH_2 = "unittest/walk/images/2";

const auto TIMEOUT = 30s;

const auto COMMENT = std::string{"comment"};
const auto TIME = chrono::parseSqlDateTime("2021-05-09 11:38:00.000+03");

static const mrc::common::Blob& testImage() {
    static auto image = maps::common::readFileToString(SRC_("images/image.jpg"));
    return image;
}


std::string makeGoodRequestBody(size_t numberOfImages = 1)
{
    presults::Results results;

    presults::Object object;
    object.created() = 123;
    object.point() = geolib3::sproto::encode(geolib3::Point2(10.0, 20.0));
    object.type() = presults::ObjectType::OTHER;
    object.comment() = "test comment";

    for (size_t i = 0; i < numberOfImages; ++i) {
        presults::Location position;
        position.point()
                = geolib3::sproto::encode(geolib3::Point2(1.0 + i, 1.0 + i));
        position.speed() = 0.0;
        position.heading() = 10.0;
        position.accuracy() = 20.0;

        presults::Image image;
        image.created() = 100 + i;
        image.image() = testImage();
        image.estimatedPosition() = position;
        object.images().push_back(image);
    }

    results.objects().push_back(object);

    std::stringstream sstream;
    sstream << results;
    return sstream.str();
}

std::string makeGoodRequestBody(size_t createdMilliseconds,
                                const std::string& pedestrianTaskId)
{
    auto results = presults::Results{};
    auto object = presults::Object{};
    object.created() = createdMilliseconds;
    object.polyline() = geolib3::sproto::encode(
        geolib3::Polyline2(geolib3::PointsVector{{1.0, 1.0}, {1.1, 1.1}}));
    object.type() = presults::ObjectType::WALL;
    object.comment() = std::to_string(createdMilliseconds);
    object.pedestrian_task_id() = pedestrianTaskId;
    auto position = presults::Location{};
    position.point() = geolib3::sproto::encode(geolib3::Point2(1.0, 1.0));
    position.speed() = 0.0;
    position.heading() = 10.0;
    position.accuracy() = 20.0;
    auto image = presults::Image{};
    image.created() = createdMilliseconds;
    image.image() = testImage();
    image.estimatedPosition() = position;
    object.images().push_back(image);
    results.objects().push_back(object);
    return (std::stringstream{} << results).str();
}

http::MockRequest makeGoodRequest(const std::string& url)
{
    http::MockRequest request(http::POST, url);
    request.body = makeGoodRequestBody();
    request.headers = {
        { "AUTHORIZATION", OAUTH_HEADER },
        { "X_REAL_IP", REMOTE_ADDR }
    };
    return request;
}

} // namespace

TEST_F(Fixture, test_no_authorization)
{
    http::MockRequest request(http::POST, WALK_UPLOAD_URL);
    request.body = makeGoodRequestBody();
    request.headers = {
        { "X_REAL_IP", REMOTE_ADDR }
    };

    auto response = yacare::performTestRequest(request);

    EXPECT_EQ(response.status, HTTP_STATUS_UNAUTHORIZED);
}

TEST_F(Fixture, test_blackbox_error)
{
    BlackboxFixture mockAuth;
    auto blackboxMock = http::addMock(
        mockAuth.blackboxUrl(),
        [](const http::MockRequest&) {
            return http::MockResponse::fromArcadia("maps/libs/auth/tests/responses/oauth.expired");
        });

    auto response = yacare::performTestRequest(makeGoodRequest(WALK_UPLOAD_URL));

    EXPECT_EQ(response.status, HTTP_STATUS_UNAUTHORIZED);
}

TEST_F(Fixture, test_blackbox_error_500)
{
    BlackboxFixture mockAuth;
    auto blackboxMock = http::addMock(
        mockAuth.blackboxUrl(),
        [](const http::MockRequest&) {
            return http::MockResponse::withStatus(HTTP_STATUS_SERVER_ERROR);
        });

    auto response = yacare::performTestRequest(makeGoodRequest(WALK_UPLOAD_URL));

    EXPECT_EQ(response.status, HTTP_STATUS_SERVER_ERROR);
}

TEST_F(Fixture, test_mds_error)
{
    BlackboxFixture mockAuth;
    auto blackboxMock = addBlackboxMock(mockAuth.blackboxUrl());

    auto mdsMock = http::addMock(
        makeMdsUploadUrl(config(), MDS_PATH),
        [](const http::MockRequest&) {
            return http::MockResponse::withStatus(HTTP_STATUS_SERVER_ERROR);
        });

    auto response = yacare::performTestRequest(makeGoodRequest(WALK_UPLOAD_URL));

    EXPECT_EQ(response.status, HTTP_STATUS_SERVER_ERROR);
}

TEST_F(Fixture, test_no_image)
{
    BlackboxFixture mockAuth;
    auto blackboxMock = addBlackboxMock(mockAuth.blackboxUrl());

    presults::Results results;

    presults::Object object;
    object.created() = 123;
    object.point() = geolib3::sproto::encode(geolib3::Point2(10.0, 20.0));
    object.type() = presults::ObjectType::OTHER;
    object.comment() = "test comment";

    presults::Location position;
    position.point() = geolib3::sproto::encode(geolib3::Point2(10.1, 20.1));
    position.speed() = 0.0;
    position.heading() = 10.0;
    position.accuracy() = 20.0;

    results.objects().push_back(object);

    std::stringstream sstream;
    sstream << results;

    http::MockRequest request(http::POST, WALK_UPLOAD_URL);
    request.body = sstream.str();
    request.headers = {
        { "AUTHORIZATION", OAUTH_HEADER },
        { "X_REAL_IP", REMOTE_ADDR }
    };

    auto response = yacare::performTestRequest(request);

    EXPECT_EQ(response.status, HTTP_STATUS_OK);

    auto txn = pgPool().masterReadOnlyTransaction();

    auto photos = db::FeatureGateway{*txn}
        .load(db::table::Feature::sourceId.equals(DEVICE_ID));
    EXPECT_TRUE(photos.empty());

    auto objects = db::WalkObjectGateway{*txn}
        .load(db::table::WalkObject::deviceId.equals(DEVICE_ID));
    EXPECT_EQ(objects.size(), 1u);
}

TEST_F(Fixture, test_success)
{
    BlackboxFixture mockAuth;
    auto blackboxMock = addBlackboxMock(mockAuth.blackboxUrl());

    auto mdsMock = http::addMock(
        makeMdsUploadUrl(config(), MDS_PATH),
        [](const http::MockRequest&) {
            return http::MockResponse("<post key=\"" + MDS_GROUP + "/" + MDS_PATH + "\"></post>");
        });

    auto response = yacare::performTestRequest(makeGoodRequest(WALK_UPLOAD_URL));
    EXPECT_EQ(response.status, HTTP_STATUS_OK);

    auto txn = pgPool().masterReadOnlyTransaction();

    auto photos = db::FeatureGateway{*txn}
        .load(db::table::Feature::sourceId.equals(DEVICE_ID));
    ASSERT_EQ(photos.size(), 1u);

    auto objects = db::WalkObjectGateway{*txn}
        .load(db::table::WalkObject::deviceId.equals(DEVICE_ID));
    ASSERT_EQ(objects.size(), 1u);

    EXPECT_EQ(photos[0].walkObjectId(), objects[0].id());

    // Try to make the same request again
    response = yacare::performTestRequest(makeGoodRequest(WALK_UPLOAD_URL));
    EXPECT_EQ(response.status, HTTP_STATUS_OK);

    // No new objects should be created
    photos = db::FeatureGateway{*txn}
        .load(db::table::Feature::sourceId.equals(DEVICE_ID));
    ASSERT_EQ(photos.size(), 1u);
    EXPECT_TRUE(photos[0].hasSize());

    objects = db::WalkObjectGateway{*txn}
        .load(db::table::WalkObject::deviceId.equals(DEVICE_ID));
    ASSERT_EQ(objects.size(), 1u);
    EXPECT_EQ(objects[0].dataset(), db::Dataset::Walks);
    EXPECT_EQ(objects[0].userId(), "111111111");
    EXPECT_EQ(objects[0].deviceId(), "MyFluffyPhone");

    // wait till event log is flushed to disk
    std::this_thread::sleep_for(std::chrono::seconds(5));

    const std::string EXPECTED_LOG =
R"({"timestamp":"0","user":{"ip":"127.0.0.1","uid":"111111111"},"object":{"type":"photo","id":"1"},"action":"create"}
)";

    std::string logContents = maps::common::readFileToString(UGC_EVENTS_LOG);
    EXPECT_EQ(logContents, EXPECTED_LOG);
}

TEST_F(Fixture, test_simultaneous_requests)
{
    BlackboxFixture mockAuth;
    auto blackboxMock = addBlackboxMock(mockAuth.blackboxUrl());

    std::promise<void> firstRequestStartedBarrier;

    auto mdsMock = http::addMock(
        makeMdsUploadUrl(config(), MDS_PATH),
        [&](const http::MockRequest&) {
            // Unblock the second request
            firstRequestStartedBarrier.set_value();
            return http::MockResponse("<post key=\"" + MDS_GROUP + "/" + MDS_PATH + "\"></post>");
        });

    auto mdsMock2 = http::addMock(
        makeMdsUploadUrl(config(), MDS_PATH_2),
        [&](const http::MockRequest&) {
            REQUIRE(false, "Second MDS request should not happen");
            return http::MockResponse::withStatus(HTTP_STATUS_OK);
        });

    // The first request will wait in the mds mock
    auto firstRequestFinishedFuture = std::async(
        std::launch::async,
        []{
            auto response = yacare::performTestRequest(makeGoodRequest(WALK_UPLOAD_URL));
            EXPECT_EQ(response.status, HTTP_STATUS_OK);
        });

    // Wait until the first request is actually started
    std::future<void> firstRequestStartedFuture = firstRequestStartedBarrier.get_future();
    auto status = firstRequestStartedFuture.wait_for(TIMEOUT);
    EXPECT_EQ(status, std::future_status::ready);

    // Try to make the same request simultaneously
    auto response = yacare::performTestRequest(makeGoodRequest(WALK_UPLOAD_URL));
    EXPECT_EQ(response.status, HTTP_STATUS_OK);

    // Wait until the first request is complete
    status = firstRequestFinishedFuture.wait_for(TIMEOUT);
    EXPECT_EQ(status, std::future_status::ready);

    auto txn = pgPool().masterReadOnlyTransaction();

    auto photos = db::FeatureGateway{*txn}
        .load(db::table::Feature::sourceId.equals(DEVICE_ID));
    EXPECT_EQ(photos.size(), 1u);

    auto objects = db::WalkObjectGateway{*txn}
        .load(db::table::WalkObject::deviceId.equals(DEVICE_ID));
    EXPECT_EQ(objects.size(), 1u);
}


TEST_F(Fixture, upload_from_belarus_user_without_phone_test)
{
    auth::UserInfo userInfo;
    userInfo.setUid(USER_ID);
    yacare::tests::UserInfoFixture userInfoFixture(std::move(userInfo));

    http::MockRequest request(
            http::POST,
            http::URL("http://localhost/walks/upload")
                .addParam("deviceid", DEVICE_ID)
                .addParam("lang", "ru_RU")
    );

    request.headers.emplace("X_REAL_IP", BELARUS_IP);
    request.body = makeGoodRequestBody();

    auto response = yacare::performTestRequest(request);

    ASSERT_EQ(response.status, 451);

    auto txn = pgPool().masterReadOnlyTransaction();

    auto photos = db::FeatureGateway{*txn}
        .load(db::table::Feature::sourceId.equals(DEVICE_ID));
    EXPECT_EQ(photos.size(), 0u);

    auto objects = db::WalkObjectGateway{*txn}
        .load(db::table::WalkObject::deviceId.equals(DEVICE_ID));
    EXPECT_EQ(objects.size(), 0u);
}

TEST_F(Fixture, upload_from_belarus_user_with_phone_test)
{
    auth::UserInfo userInfo;
    userInfo.setUid(USER_ID);
    userInfo.setPhones({auth::Phone("1" /* phone_id */)});
    yacare::tests::UserInfoFixture userInfoFixture(std::move(userInfo));

    http::MockRequest request(
            http::POST,
            http::URL("http://localhost/walks/upload")
                .addParam("deviceid", DEVICE_ID)
                .addParam("lang", "ru_RU")
    );

    request.headers.emplace("X_REAL_IP", BELARUS_IP);
    request.body = makeGoodRequestBody();

    auto response = yacare::performTestRequest(request);

    ASSERT_EQ(response.status, 200);

    auto txn = pgPool().masterReadOnlyTransaction();

    auto photos = db::FeatureGateway{*txn}
        .load(db::table::Feature::sourceId.equals(DEVICE_ID));
    EXPECT_EQ(photos.size(), 1u);

    auto objects = db::WalkObjectGateway{*txn}
        .load(db::table::WalkObject::deviceId.equals(DEVICE_ID));
    EXPECT_EQ(objects.size(), 1u);
}

TEST_F(Fixture, test_two_images)
{
    BlackboxFixture mockAuth;
    auto blackboxMock = addBlackboxMock(mockAuth.blackboxUrl());

    auto mdsMock = http::addMock(
        makeMdsUploadUrl(config(), MDS_PATH),
        [](const http::MockRequest&) {
            return http::MockResponse("<post key=\"" + MDS_GROUP + "/" + MDS_PATH + "\"></post>");
        });
    auto mdsMock2 = http::addMock(
        makeMdsUploadUrl(config(), MDS_PATH_2),
        [&](const http::MockRequest&) {
            return http::MockResponse("<post key=\"" + MDS_GROUP + "/" + MDS_PATH_2 + "\"></post>");
        });

    http::MockRequest request(http::POST, WALK_UPLOAD_URL);
    request.body = makeGoodRequestBody(2);
    request.headers = {
        { "AUTHORIZATION", OAUTH_HEADER },
        { "X_REAL_IP", REMOTE_ADDR }
    };

    auto response = yacare::performTestRequest(request);
    EXPECT_EQ(response.status, HTTP_STATUS_OK);

    auto txn = pgPool().masterReadOnlyTransaction();

    auto objects = db::WalkObjectGateway{*txn}
        .load(db::table::WalkObject::deviceId.equals(DEVICE_ID));
    EXPECT_EQ(objects.size(), 1u);

    auto photos = db::FeatureGateway{*txn}
        .load(db::table::Feature::sourceId.equals(DEVICE_ID));
    EXPECT_EQ(photos.size(), 2u);
    for (const auto& photo : photos) {
        EXPECT_EQ(photo.walkObjectId(), objects[0].id());
    }
}

TEST_F(Fixture, test_empty_and_nonempty_images)
{
    BlackboxFixture mockAuth;
    auto blackboxMock = addBlackboxMock(mockAuth.blackboxUrl());

    auto mdsMock = http::addMock(
        makeMdsUploadUrl(config(), MDS_PATH),
        [](const http::MockRequest&) {
            return http::MockResponse("<post key=\"" + MDS_GROUP + "/" + MDS_PATH + "\"></post>");
        });

    presults::Results results;

    presults::Object object;
    object.created() = 123;
    object.point() = geolib3::sproto::encode(geolib3::Point2(10.0, 20.0));
    object.type() = presults::ObjectType::OTHER;
    object.comment() = "test comment";

    for (int i = 0; i < 2; ++i) {
        presults::Location position;
        position.point() = geolib3::sproto::encode(geolib3::Point2(0.1 * i, 0.1 * i));
        position.speed() = 0.0;
        position.heading() = 10.0;
        position.accuracy() = 20.0;

        presults::Image image;
        image.created() = 222 + i;
        if (i == 0) {
            image.image() = "raw data";
        } else {
            image.image() = "";
        }

        image.estimatedPosition() = position;
        object.images().push_back(image);
    }

    results.objects().push_back(object);

    std::stringstream sstream;
    sstream << results;

    http::MockRequest request(http::POST, WALK_UPLOAD_URL);
    request.body = sstream.str();

    request.headers = {
        { "AUTHORIZATION", OAUTH_HEADER },
        { "X_REAL_IP", REMOTE_ADDR }
    };

    auto response = yacare::performTestRequest(request);
    EXPECT_EQ(response.status, HTTP_STATUS_OK);

    auto txn = pgPool().masterReadOnlyTransaction();

    auto objects = db::WalkObjectGateway{*txn}
        .load(db::table::WalkObject::deviceId.equals(DEVICE_ID));
    EXPECT_EQ(objects.size(), 1u);

    auto photos = db::FeatureGateway{*txn}
        .load(db::table::Feature::sourceId.equals(DEVICE_ID));
    EXPECT_EQ(photos.size(), 1u);
    for (const auto& photo : photos) {
        EXPECT_EQ(photo.walkObjectId(), objects[0].id());
    }
}

TEST_F(Fixture, test_empty_images)
{
    BlackboxFixture mockAuth;
    auto blackboxMock = addBlackboxMock(mockAuth.blackboxUrl());

    presults::Results results;

    presults::Object object;
    object.created() = 123;
    object.point() = geolib3::sproto::encode(geolib3::Point2(10.0, 20.0));
    object.type() = presults::ObjectType::OTHER;
    object.comment() = "test comment";

    for (int i = 0; i < 2; ++i) {
        presults::Location position;
        position.point() = geolib3::sproto::encode(geolib3::Point2(0.1 * i, 0.1 * i));
        position.speed() = 0.0;
        position.heading() = 10.0;
        position.accuracy() = 20.0;

        presults::Image image;
        image.created() = 222 + i;
        image.estimatedPosition() = position;
        image.image() = "";
        object.images().push_back(image);
    }

    results.objects().push_back(object);

    std::stringstream sstream;
    sstream << results;

    http::MockRequest request(http::POST, WALK_UPLOAD_URL);
    request.body = sstream.str();
    request.headers = {
        { "AUTHORIZATION", OAUTH_HEADER },
        { "X_REAL_IP", REMOTE_ADDR }
    };

    auto response = yacare::performTestRequest(request);
    EXPECT_EQ(response.status, HTTP_STATUS_BAD_REQUEST);

    auto txn = pgPool().masterReadOnlyTransaction();

    auto objects = db::WalkObjectGateway{*txn}
        .load(db::table::WalkObject::deviceId.equals(DEVICE_ID));
    EXPECT_EQ(objects.size(), 0u);

    auto photos = db::FeatureGateway{*txn}
        .load(db::table::Feature::sourceId.equals(DEVICE_ID));
    EXPECT_EQ(photos.size(), 0u);
}

TEST_F(Fixture, test_duplicate_features)
{
    BlackboxFixture mockAuth;
    auto blackboxMock = addBlackboxMock(mockAuth.blackboxUrl());

    auto mdsMock = http::addMock(
        makeMdsUploadUrl(config(), MDS_PATH),
        [](const http::MockRequest&) {
            return http::MockResponse("<post key=\"" + MDS_GROUP + "/" + MDS_PATH + "\"></post>");
        });

    auto makeRequest = [](size_t objectCreatedAt) {
        presults::Results results;

        presults::Object object;
        object.created() = objectCreatedAt;
        object.point() = geolib3::sproto::encode(geolib3::Point2(10.0, 20.0));
        object.type() = presults::ObjectType::OTHER;

        presults::Location position;
        position.point() = geolib3::sproto::encode(geolib3::Point2(1.0, 1.0));
        position.speed() = 0.0;
        position.heading() = 10.0;

        presults::Image image;
        image.created() = 100;
        image.image() = "raw data";
        image.estimatedPosition() = position;
        object.images().push_back(image);

        results.objects().push_back(object);
        std::stringstream sstream;
        sstream << results;
        http::MockRequest request(http::POST, WALK_UPLOAD_URL);
        request.body = sstream.str();
        request.headers = {
            { "AUTHORIZATION", OAUTH_HEADER },
            { "X_REAL_IP", REMOTE_ADDR }
        };
        return request;
    };

    {
        auto response = yacare::performTestRequest(makeRequest(100));
        EXPECT_EQ(response.status, HTTP_STATUS_OK);

        auto txn = pgPool().masterReadOnlyTransaction();
        auto photos = db::FeatureGateway{*txn}
            .load(db::table::Feature::sourceId.equals(DEVICE_ID));
        ASSERT_EQ(photos.size(), 1u);

        auto objects = db::WalkObjectGateway{*txn}
            .load(db::table::WalkObject::deviceId.equals(DEVICE_ID));
        ASSERT_EQ(objects.size(), 1u);
    }

    {
        // Send second request with the same feature
        auto response = yacare::performTestRequest(makeRequest(200));

        // Check that no new feature/object have been created
        auto txn = pgPool().masterReadOnlyTransaction();
        auto photos = db::FeatureGateway{*txn}
            .load(db::table::Feature::sourceId.equals(DEVICE_ID));
        ASSERT_EQ(photos.size(), 1u);

        auto objects = db::WalkObjectGateway{*txn}
            .load(db::table::WalkObject::deviceId.equals(DEVICE_ID));
        ASSERT_EQ(objects.size(), 1u);
    }
}

TEST_F(Fixture, test_pedestrian_upload_no_images)
{
    BlackboxFixture mockAuth;
    auto blackboxMock = addBlackboxMock(mockAuth.blackboxUrl());

    http::MockRequest request(http::POST, PEDESTRIAN_UPLOAD_URL);
    request.body = makeGoodRequestBody(0);
    request.headers = {
        { "AUTHORIZATION", OAUTH_HEADER },
        { "X_REAL_IP", REMOTE_ADDR }
    };

    auto response = yacare::performTestRequest(request);
    EXPECT_EQ(response.status, HTTP_STATUS_OK);

    auto txn = pgPool().masterReadOnlyTransaction();

    auto photos = db::FeatureGateway{*txn}
        .load(db::table::Feature::sourceId.equals(DEVICE_ID));
    EXPECT_TRUE(photos.empty());

    auto objects = db::WalkObjectGateway{*txn}
        .load(db::table::WalkObject::deviceId.equals(DEVICE_ID));
    ASSERT_EQ(objects.size(), 1u);
    ASSERT_TRUE(objects[0].taskId().has_value());
    EXPECT_EQ(objects[0].taskId().value(), TASK_ID);
    EXPECT_FALSE(objects[0].hasActionType());
}

TEST_F(Fixture, test_pedestrian_two_images)
{
    BlackboxFixture mockAuth;
    auto blackboxMock = addBlackboxMock(mockAuth.blackboxUrl());

    auto mdsMock = http::addMock(
        makeMdsUploadUrl(config(), MDS_PATH),
        [](const http::MockRequest&) {
            return http::MockResponse("<post key=\"" + MDS_GROUP + "/" + MDS_PATH + "\"></post>");
        });
    auto mdsMock2 = http::addMock(
        makeMdsUploadUrl(config(), MDS_PATH_2),
        [&](const http::MockRequest&) {
            return http::MockResponse("<post key=\"" + MDS_GROUP + "/" + MDS_PATH_2 + "\"></post>");
        });


    http::MockRequest request(http::POST, PEDESTRIAN_UPLOAD_URL);
    request.body = makeGoodRequestBody(2);
    request.headers = {
        { "AUTHORIZATION", OAUTH_HEADER },
        { "X_REAL_IP", REMOTE_ADDR }
    };

    auto response = yacare::performTestRequest(request);
    EXPECT_EQ(response.status, HTTP_STATUS_OK);

    auto txn = pgPool().masterReadOnlyTransaction();

    auto objects = db::WalkObjectGateway{*txn}
        .load(db::table::WalkObject::deviceId.equals(DEVICE_ID));
    ASSERT_EQ(objects.size(), 1u);
    EXPECT_EQ(objects[0].dataset(), db::Dataset::PedestrianTask);
    EXPECT_EQ(objects[0].userId(), "111111111");
    EXPECT_EQ(objects[0].deviceId(), "MyFluffyPhone");

    auto photos = db::FeatureGateway{*txn}
        .load(db::table::Feature::sourceId.equals(DEVICE_ID));
    EXPECT_EQ(photos.size(), 2u);
    for (const auto& photo : photos) {
        EXPECT_EQ(photo.walkObjectId(), objects[0].id());
    }
}

TEST_F(Fixture, test_different_geometries)
{
    BlackboxFixture mockAuth;
    auto blackboxMock = addBlackboxMock(mockAuth.blackboxUrl());

    auto polyline = geolib3::Polyline2(geolib3::PointsVector{{1.0, 1.0}, {2.0, 2.0}});
    auto polygon = geolib3::Polygon2(geolib3::PointsVector{{1.0, 1.0}, {1.0, 2.0}, {2.0, 1.0}});

    presults::Results results;
    for (size_t i = 0; i < 2; ++i) {
        presults::Object object;
        object.created() = 123 + i;
        if (i == 0) {
            object.polyline() = geolib3::sproto::encode(polyline);
        } else {
            object.polygon() = geolib3::sproto::encode(polygon);
        }
        object.type() = presults::ObjectType::OTHER;
        results.objects().push_back(object);
    }

    std::stringstream sstream;
    sstream << results;

    http::MockRequest request(http::POST, PEDESTRIAN_UPLOAD_URL);
    request.body = sstream.str();
    request.headers = {
        { "AUTHORIZATION", OAUTH_HEADER },
        { "X_REAL_IP", REMOTE_ADDR }
    };

    auto response = yacare::performTestRequest(request);
    EXPECT_EQ(response.status, HTTP_STATUS_OK);

    auto txn = pgPool().masterReadOnlyTransaction();

    auto objects = db::WalkObjectGateway{*txn}.load();
    ASSERT_EQ(objects.size(), 2u);
    EXPECT_EQ(std::get<geolib3::Polyline2>(objects[0].geodeticGeometry()).points(),
              polyline.points());
    EXPECT_EQ(std::get<geolib3::Polygon2>(objects[1].geodeticGeometry()).exteriorRing().toPolyline().points(),
              polygon.exteriorRing().toPolyline().points());
}

TEST_F(Fixture, test_missing_object_geometry)
{
    BlackboxFixture mockAuth;
    auto blackboxMock = addBlackboxMock(mockAuth.blackboxUrl());

    presults::Results results;

    presults::Object object;
    object.created() = 123;
    object.type() = presults::ObjectType::OTHER;

    presults::Location position;
    position.point() = geolib3::sproto::encode(geolib3::Point2(1.0, 1.0));
    position.speed() = 0.0;
    position.heading() = 10.0;
    position.accuracy() = 20.0;

    presults::Image image;
    image.created() = 222;
    image.estimatedPosition() = position;
    image.image() = "raw data";
    object.images().push_back(image);

    results.objects().push_back(object);

    std::stringstream sstream;
    sstream << results;

    http::MockRequest request(http::POST, PEDESTRIAN_UPLOAD_URL);
    request.body = sstream.str();
    request.headers = {
        { "AUTHORIZATION", OAUTH_HEADER },
        { "X_REAL_IP", REMOTE_ADDR }
    };

    auto response = yacare::performTestRequest(request);
    EXPECT_EQ(response.status, HTTP_STATUS_BAD_REQUEST);

    auto txn = pgPool().masterReadOnlyTransaction();

    auto objects = db::WalkObjectGateway{*txn}.load();
    EXPECT_TRUE(objects.empty());
}

TEST_F(Fixture, test_pedestrian_object_optional_fields)
{
    BlackboxFixture mockAuth;
    auto blackboxMock = addBlackboxMock(mockAuth.blackboxUrl());

    presults::Results results;

    presults::Object object;
    object.created() = 123;
    object.point() = geolib3::sproto::encode(geolib3::Point2(10.0, 20.0));
    object.type() = presults::ObjectType::OTHER;
    object.indoorLevelId() = "1";
    object.actionType() = presults::ActionType::ADD;

    presults::Location position;
    position.point() = geolib3::sproto::encode(geolib3::Point2(10.1, 20.1));
    position.speed() = 0.0;
    position.heading() = 10.0;
    position.accuracy() = 20.0;

    results.objects().push_back(object);

    std::stringstream sstream;
    sstream << results;

    http::MockRequest request(http::POST, PEDESTRIAN_UPLOAD_URL);
    request.body = sstream.str();
    request.headers = {
        { "AUTHORIZATION", OAUTH_HEADER },
        { "X_REAL_IP", REMOTE_ADDR }
    };

    auto response = yacare::performTestRequest(request);

    EXPECT_EQ(response.status, HTTP_STATUS_OK);

    auto txn = pgPool().masterReadOnlyTransaction();

    auto objects = db::WalkObjectGateway{*txn}
        .load(db::table::WalkObject::deviceId.equals(DEVICE_ID));
    EXPECT_EQ(objects.size(), 1u);
    EXPECT_EQ(objects[0].indoorLevelId(), "1");
    EXPECT_TRUE(objects[0].hasActionType());
    EXPECT_EQ(objects[0].actionType(), db::ObjectActionType::Add);
}

TEST_F(Fixture, test_invalid_polygon_geometry_recoverable)
{
    BlackboxFixture mockAuth;
    auto blackboxMock = addBlackboxMock(mockAuth.blackboxUrl());

    presults::Results results;

    presults::Object object;
    object.created() = 123;
    object.type() = presults::ObjectType::OTHER;

    // Self-intersects
    auto polygon = geolib3::Polygon2(
        geolib3::PointsVector{{1.0, 1.0}, {1.0, -1.0}, {0.0, 0.0}, {2.0, 0.0}},
        geolib3::Validate::No);
    object.polygon() = geolib3::sproto::encode(polygon);
    results.objects().push_back(object);

    std::stringstream sstream;
    sstream << results;

    http::MockRequest request(http::POST, PEDESTRIAN_UPLOAD_URL);
    request.body = sstream.str();
    request.headers = {
        { "AUTHORIZATION", OAUTH_HEADER },
        { "X_REAL_IP", REMOTE_ADDR }
    };

    auto response = yacare::performTestRequest(request);
    EXPECT_EQ(response.status, HTTP_STATUS_OK);

    auto txn = pgPool().masterReadOnlyTransaction();

    auto objects = db::WalkObjectGateway{*txn}.load();
    EXPECT_EQ(objects.size(), 1u);
}

TEST_F(Fixture, test_invalid_polygon_geometry_nonrecoverable)
{
    BlackboxFixture mockAuth;
    auto blackboxMock = addBlackboxMock(mockAuth.blackboxUrl());

    presults::Results results;

    presults::Object object;
    object.created() = 123;
    object.type() = presults::ObjectType::OTHER;

    // Too few points
    auto polygon = geolib3::Polygon2(
        geolib3::PointsVector{{1.0, 1.0}, {1.0, -1.0}, {1.0, -1.0}},
        geolib3::Validate::No);
    object.polygon() = geolib3::sproto::encode(polygon);
    results.objects().push_back(object);

    std::stringstream sstream;
    sstream << results;

    http::MockRequest request(http::POST, PEDESTRIAN_UPLOAD_URL);
    request.body = sstream.str();
    request.headers = {
        { "AUTHORIZATION", OAUTH_HEADER },
        { "X_REAL_IP", REMOTE_ADDR }
    };

    auto response = yacare::performTestRequest(request);
    EXPECT_EQ(response.status, HTTP_STATUS_BAD_REQUEST);
}

namespace {

const wiki::revision::DBID WIKI_OBJECT_ID = 12345;
const std::string CATEGORY = "mrc_pedestrian_region";
const std::string ASSIGNEE_LOGIN = "assignee-login";

const http::HeaderMap HEADERS = {
    { "AUTHORIZATION", OAUTH_HEADER },
    { "X_REAL_IP", REMOTE_ADDR }
};

PedestrianZone makeZone(
    const std::string& category,
    const std::string& status,
    const std::string& assigneeLogin)
{
    WikiEditorObject object;
    object.id = WIKI_OBJECT_ID;
    object.categoryId = category;
    object.plainAttributes = {
        {"mrc_pedestrian_region:status", status},
        {"mrc_pedestrian_region:pedestrian_login", assigneeLogin},
    };

    return PedestrianZone{object};
}

} // namespace

TEST_F(Fixture, complete_pedestrian_task_base_case)
{
    auth::UserInfo userInfo;
    userInfo.setUid(USER_ID);
    userInfo.setLogin(ASSIGNEE_LOGIN);
    yacare::tests::UserInfoFixture userInfoFixture(std::move(userInfo));

    const auto URL = PEDESTRIAN_TASK_COMPLETE_URL
        + "?task_id=mrc_pedestrian:" + std::to_string(WIKI_OBJECT_ID);

    http::MockRequest request(http::PUT, URL);
    request.headers = HEADERS;

    ON_CALL(wikiEditorClientMock(), getPedestrianZone(testing::Eq(WIKI_OBJECT_ID)))
        .WillByDefault(testing::Return(makeZone(CATEGORY, "can_start", ASSIGNEE_LOGIN)
    ));

    auto zone = makeZone("mrc_pedestrian_region", "awaiting_check", ASSIGNEE_LOGIN);
    ON_CALL(wikiEditorClientMock(), savePedestrianZone(testing::Eq(zone)))
        .WillByDefault(testing::Return(zone));

    auto response = yacare::performTestRequest(request);
    EXPECT_EQ(response.status, HTTP_STATUS_OK);
}

TEST_F(Fixture, complete_pedestrian_task_already_completed)
{
    auth::UserInfo userInfo;
    userInfo.setUid(USER_ID);
    userInfo.setLogin(ASSIGNEE_LOGIN);
    yacare::tests::UserInfoFixture userInfoFixture(std::move(userInfo));

    const auto URL = PEDESTRIAN_TASK_COMPLETE_URL
        + "?task_id=mrc_pedestrian:" + std::to_string(WIKI_OBJECT_ID);

    http::MockRequest request(http::PUT, URL);
    request.headers = HEADERS;

    // Region is already in `awaiting_check` state, no update needed
    ON_CALL(wikiEditorClientMock(), getPedestrianZone(testing::Eq(WIKI_OBJECT_ID)))
        .WillByDefault(testing::Return(makeZone(CATEGORY, "awaiting_check", ASSIGNEE_LOGIN)
    ));

    auto response = yacare::performTestRequest(request);
    EXPECT_EQ(response.status, HTTP_STATUS_OK);
}

TEST_F(Fixture, complete_pedestrian_task_wrong_category)
{
    auto mockAuth = BlackboxFixture{};
    auto blackboxMock = addBlackboxMock(mockAuth.blackboxUrl());

    const auto URL = PEDESTRIAN_TASK_COMPLETE_URL
        + "?task_id=mrc_pedestrian:" + std::to_string(WIKI_OBJECT_ID);

    http::MockRequest request(http::PUT, URL);
    request.headers = HEADERS;

    ON_CALL(wikiEditorClientMock(), getPedestrianZone(testing::Eq(WIKI_OBJECT_ID)))
        .WillByDefault(testing::Return(makeZone("wrong_category", "awaiting_check", ASSIGNEE_LOGIN)
    ));

    auto response = yacare::performTestRequest(request);
    EXPECT_EQ(response.status, HTTP_STATUS_BAD_REQUEST);
}

TEST_F(Fixture, complete_pedestrian_task_non_wiki_editor_object)
{
    auto mockAuth = BlackboxFixture{};
    auto blackboxMock = addBlackboxMock(mockAuth.blackboxUrl());

    const auto URL = PEDESTRIAN_TASK_COMPLETE_URL
        + "?task_id=wrong_prefix:" + std::to_string(WIKI_OBJECT_ID);

    http::MockRequest request(http::PUT, URL);
    request.headers = HEADERS;

    EXPECT_CALL(wikiEditorClientMock(), getPedestrianZone(testing::Eq(WIKI_OBJECT_ID)))
        .WillRepeatedly(Throw(maps::Exception()));

    auto response = yacare::performTestRequest(request);
    EXPECT_EQ(response.status, HTTP_STATUS_NOT_FOUND);
}

TEST_F(Fixture, complete_pedestrian_task_wrong_login)
{
    auth::UserInfo userInfo;
    userInfo.setUid(USER_ID);
    userInfo.setLogin("wrong-login");
    yacare::tests::UserInfoFixture userInfoFixture(std::move(userInfo));

    const auto URL = PEDESTRIAN_TASK_COMPLETE_URL
        + "?task_id=mrc_pedestrian:" + std::to_string(WIKI_OBJECT_ID);

    http::MockRequest request(http::PUT, URL);
    request.headers = HEADERS;

    ON_CALL(wikiEditorClientMock(), getPedestrianZone(testing::Eq(WIKI_OBJECT_ID)))
        .WillByDefault(testing::Return(makeZone(CATEGORY, "can_start", ASSIGNEE_LOGIN)
    ));

    auto response = yacare::performTestRequest(request);
    EXPECT_EQ(response.status, HTTP_STATUS_OK);
}

TEST_F(Fixture, complete_pedestrian_task_status_conflict)
{
    auth::UserInfo userInfo;
    userInfo.setUid(USER_ID);
    userInfo.setLogin(ASSIGNEE_LOGIN);
    yacare::tests::UserInfoFixture userInfoFixture(std::move(userInfo));

    const auto URL = PEDESTRIAN_TASK_COMPLETE_URL
        + "?task_id=mrc_pedestrian:" + std::to_string(WIKI_OBJECT_ID);

    http::MockRequest request(http::PUT, URL);
    request.headers = HEADERS;

    ON_CALL(wikiEditorClientMock(), getPedestrianZone(testing::Eq(WIKI_OBJECT_ID)))
        .WillByDefault(testing::Return(makeZone(CATEGORY, "draft", ASSIGNEE_LOGIN)
    ));

    auto response = yacare::performTestRequest(request);
    // Zone status is draft, so will not update it
    EXPECT_EQ(response.status, HTTP_STATUS_OK);
}

TEST_F(Fixture, test_delayed)
{
    // initialization
    auto userInfo = auth::UserInfo{};
    userInfo.setUid(USER_ID);
    userInfo.setLogin(ASSIGNEE_LOGIN);
    auto userInfoFixture = yacare::tests::UserInfoFixture{userInfo};
    auto mdsMocks = std::vector<http::MockHandle>{};
    for (const auto& mdsPath : {MDS_PATH, MDS_PATH_2}) {
        mdsMocks.push_back(http::addMock(
            makeMdsUploadUrl(config(), mdsPath), [=](const http::MockRequest&) {
                return http::MockResponse("<post key=\"" + MDS_GROUP +
                                          "/unittest/walk/images/" + mdsPath +
                                          "\"></post>");
            }));
    }
    ON_CALL(wikiEditorClientMock(),
            getPedestrianZone(testing::Eq(WIKI_OBJECT_ID)))
        .WillByDefault(
            testing::Return(makeZone(CATEGORY, "can_start", ASSIGNEE_LOGIN)));
    auto zone =
        makeZone("mrc_pedestrian_region", "awaiting_check", ASSIGNEE_LOGIN);
    ON_CALL(wikiEditorClientMock(), savePedestrianZone(testing::Eq(zone)))
        .WillByDefault(testing::Return(zone));
    auto taskId = "mrc_pedestrian:" + std::to_string(WIKI_OBJECT_ID);
    auto loadObjects = [&] {
        return db::WalkObjectGateway{*pgPool().masterReadOnlyTransaction()}
            .load(db::table::WalkObject::userId.equals(USER_ID) &&
                  db::table::WalkObject::deviceId.equals(DEVICE_ID) &&
                  db::table::WalkObject::taskId.equals(taskId));
    };

    // first upload
    auto request = http::MockRequest{http::POST, PEDESTRIAN_UPLOAD_URL};
    request.headers = HEADERS;
    request.body = makeGoodRequestBody(1000,  //< createdMilliseconds
                                       taskId);
    auto response = yacare::performTestRequest(request);
    EXPECT_EQ(response.status, HTTP_STATUS_OK);

    auto objects = loadObjects();
    EXPECT_EQ(objects.size(), 1u);
    EXPECT_EQ(objects.front().status(), db::ObjectStatus::Delayed);

    // second upload
    request.headers = HEADERS;
    request.body = makeGoodRequestBody(2000,  //< createdMilliseconds
                                       taskId);
    response = yacare::performTestRequest(request);
    EXPECT_EQ(response.status, HTTP_STATUS_OK);

    objects = loadObjects();
    EXPECT_EQ(objects.size(), 2u);
    EXPECT_EQ(objects.front().status(), db::ObjectStatus::Delayed);
    EXPECT_EQ(objects.back().status(), db::ObjectStatus::Delayed);

    // complete
    request = http::MockRequest{http::PUT,
                                PEDESTRIAN_TASK_COMPLETE_URL + "?deviceid=" +
                                    DEVICE_ID + "&task_id=" + taskId};
    request.headers = HEADERS;
    response = yacare::performTestRequest(request);
    EXPECT_EQ(response.status, HTTP_STATUS_OK);

    objects = loadObjects();
    EXPECT_EQ(objects.size(), 2u);
    EXPECT_EQ(objects.front().status(), db::ObjectStatus::Pending);
    EXPECT_EQ(objects.back().status(), db::ObjectStatus::Pending);
}

} // maps::mrc::agent_proxy::tests
