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

#include <maps/infra/yacare/include/test_utils.h>
#include <maps/libs/concurrent/include/scoped_guard.h>
#include <maps/libs/http/include/test_utils.h>
#include <maps/libs/http/include/url.h>
#include <maps/libs/introspection/include/comparison.h>
#include <maps/libs/introspection/include/stream_output.h>
#include <maps/libs/json/include/builder.h>
#include <maps/libs/json/include/value.h>
#include <maps/wikimap/mapspro/services/mrc/libs/db/include/common.h>
#include <maps/wikimap/mapspro/services/mrc/libs/db/include/feature_gateway.h>
#include <maps/wikimap/mapspro/services/mrc/libs/db/include/object_in_photo_gateway.h>
#include <maps/wikimap/mapspro/services/mrc/libs/db/include/track_point_gateway.h>
#include <maps/wikimap/mapspro/services/mrc/libs/graph_matcher_adapter/include/feature_positioner.h>
#include <maps/libs/geolib/include/direction.h>
#include <maps/libs/geolib/include/distance.h>
#include <maps/libs/geolib/include/test_tools/io_operations.h>

#include <opencv2/opencv.hpp>

using namespace std::chrono_literals;

namespace maps::geolib3 {
using maps::geolib3::io::operator <<;
}

namespace maps::mrc::tasks_planner::tests {

namespace {
db::ObjectsInPhoto
addObjectsInPhoto(maps::pgpool3::TransactionHandle&& txn,
                  db::TId featureId)
{
    db::ObjectsInPhoto result{
        db::ObjectInPhoto{featureId, db::ObjectInPhotoType::Face, 0, 0, 32, 32, 1.},
        db::ObjectInPhoto{featureId, db::ObjectInPhotoType::LicensePlate, 10, 10, 32, 32, 1.}
    };

    db::ObjectInPhotoGateway(*txn).insert(result);
    txn->commit();
    return result;
}

db::Features makeRide(db::GraphType graphType,
                      const adapters::CompactGraphMatcherAdapter& matcher,
                      pgpool3::TransactionHandle&& txn)
{
    auto SOURCE_ID = "iPhone100500";
    auto timestamp = chrono::parseSqlDateTime("2017-05-17 11:03:16+03");
    auto heading = geolib3::Heading{20};
    auto points = geolib3::PointsVector{{37.606339, 55.690217},
                                        {37.606469, 55.690464},
                                        {37.606623, 55.690731},
                                        {37.606824, 55.691011}};

    db::TrackPoints trackPoints;
    db::Features features;
    for (auto& point : points) {
        trackPoints.emplace_back()
            .setSourceId(SOURCE_ID)
            .setTimestamp(timestamp += 500ms)
            .setGeodeticPos(point)
            .setHeading(heading)
            .setAccuracyMeters(40);
        features.push_back(
            sql_chemistry::GatewayAccess<db::Feature>::construct()
                .setSourceId(SOURCE_ID)
                .setSize(1080, 920)
                .setTimestamp(timestamp += 500ms));
    }
    features.pop_back();  // inner only

    auto trackPointProvider = [&](const std::string& sourceId,
                                  chrono::TimePoint startTime,
                                  chrono::TimePoint endTime) {
        db::TrackPoints result;
        for (const auto& trackPoint : trackPoints) {
            if (trackPoint.sourceId() == sourceId &&
                trackPoint.timestamp() >= startTime &&
                trackPoint.timestamp() <= endTime) {
                result.push_back(trackPoint);
            }
        }
        return result;
    };

    auto trackType = db::GraphType::Road == graphType
                         ? track_classifier::TrackType::Vehicle
                         : track_classifier::TrackType::Pedestrian;
    adapters::FeaturePositioner{{{graphType, &matcher}},
                                trackPointProvider,
                                adapters::classifyAs(trackType)}(features);
    db::TrackPointGateway(*txn).insert(trackPoints);
    db::FeatureGateway(*txn).insert(features);
    txn->commit();
    return features;
}

}

TEST(photos_api_should, test_get_photo)
{
    Fixture fixture;
    auto photo = fixture.createFeature();

    http::MockRequest request(
        http::GET,
        http::URL("http://localhost/photos/" + std::to_string(photo.id()))
    );

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

    auto responseSchema = readResponseSchemasFromSwagger(schemasPath());
    auto body = response.body;
    validateJson(body,
                 responseSchema.at({"GET", "/photos/{photo_id}", 200}),
                 schemasDir());

    auto bodyJson = json::Value::fromString(body);
    EXPECT_EQ(http::URL(bodyJson["image"]["url"].as<std::string>()).path(),
              "/photos/" + std::to_string(photo.id()) + "/image");
}

TEST(photos_api_should, test_get_photo_withoutsize)
{
    Fixture fixture;
    auto photo = fixture.createFeatureWithoutSize();

    http::MockRequest request(
        http::GET,
        http::URL("http://localhost/photos/" + std::to_string(photo.id()))
    );

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

TEST(photos_api_should, test_patch_photo)
{
    Fixture fixture;
    auto photo = fixture.createFeature();
    EXPECT_FALSE(db::FeatureGateway(*fixture.txnHandle())
                     .loadById(photo.id())
                     .moderatorsShouldBePublished()
                     .has_value());

    http::MockRequest request(
        http::PATCH,
        http::URL("http://localhost/photos/" + std::to_string(photo.id()))
            .addParam("rotate_by", "90")
            .addParam("is_published", "true")
    );

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

    auto responseSchema = readResponseSchemasFromSwagger(schemasPath());
    auto body = response.body;
    validateJson(body,
                 responseSchema.at({"PATCH", "/photos/{photo_id}", 200}),
                 schemasDir());

    auto bodyJson = json::Value::fromString(body);
    EXPECT_TRUE(bodyJson["isPublished"].as<bool>());
    EXPECT_EQ(bodyJson["image"]["width"].as<int>(), 1080);
    EXPECT_EQ(bodyJson["image"]["height"].as<int>(), 1920);
    EXPECT_TRUE(db::FeatureGateway(*fixture.txnHandle())
                    .loadById(photo.id())
                    .moderatorsShouldBePublished()
                    .value());
}

TEST(photos_api_should, test_get_photo_image)
{
    Fixture fixture;
    auto photo = fixture.createFeature();


    http::MockRequest request(
        http::GET,
        http::URL("http://localhost/photos/" + std::to_string(photo.id()) + "/image")
    );

    auto response = yacare::performTestRequest(request);
    EXPECT_EQ(response.status, 200);
    EXPECT_EQ(response.headers["Content-Type"], "image/jpeg");
    const cv::Mat image = mrc::common::decodeImage(response.body);
    EXPECT_EQ(image.rows, 1080);
    EXPECT_EQ(image.cols, 1920);
}

TEST(photos_api_should, test_get_photo_image_orientation)
{
    Fixture fixture;
    auto photo = fixture.createFeature();

    {
        photo.setOrientation(common::ImageOrientation(common::Rotation::CW_90));
        auto txn = fixture.txnHandle();
        db::FeatureGateway(*txn).update(photo, db::UpdateFeatureTxn::Yes);
        txn->commit();
    }


    http::MockRequest request(
        http::GET,
        http::URL("http://localhost/photos/" + std::to_string(photo.id()) + "/image")
    );

    auto response = yacare::performTestRequest(request);
    EXPECT_EQ(response.status, 200);
    EXPECT_EQ(response.headers["Content-Type"], "image/jpeg");
    const cv::Mat image = mrc::common::decodeImage(response.body);
    EXPECT_EQ(image.rows, 1920);
    EXPECT_EQ(image.cols, 1080);
}


TEST(photos_api_should, test_get_photo_thumbnail)
{
    Fixture fixture;
    auto photo = fixture.createFeature();


    http::MockRequest request(
        http::GET,
        http::URL("http://localhost/photos/" + std::to_string(photo.id()) + "/thumbnail")
    );

    auto response = yacare::performTestRequest(request);
    EXPECT_EQ(response.status, 200);
    EXPECT_EQ(response.headers["Content-Type"], "image/jpeg");
    const cv::Mat image = mrc::common::decodeImage(response.body);
    EXPECT_EQ(image.rows, 160);
    EXPECT_EQ(image.cols, 284);
}


TEST(photos_api_should, test_get_photo_objects)
{
    Fixture fixture;
    auto photo = fixture.createFeature();
    auto featureId = photo.id();
    addObjectsInPhoto(fixture.txnHandle(), featureId);


    http::MockRequest request(
        http::GET,
        http::URL("http://localhost/photos/" + std::to_string(featureId) + "/objects")
    );

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

    auto responseSchema = readResponseSchemasFromSwagger(schemasPath());
    validateJson(response.body,
                 responseSchema.at({"GET", "/photos/{photo_id}/objects", 200}),
                 schemasDir());
}


TEST(photos_api_should, test_create_photo_object)
{
    Fixture fixture;
    auto photo = fixture.createFeature();
    auto featureId = photo.id();

    json::Builder builder;
    builder << [&](json::ObjectBuilder builder) {
        builder["type"] = "Face";
        builder["bbox"] << [](json::ArrayBuilder builder) {
            builder << 0 << 0 << 32 << 32;
        };
    };


    http::MockRequest request(
        http::POST,
        http::URL("http://localhost/photos/" + std::to_string(featureId) + "/objects")
    );
    request.body = builder.str();

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

    auto responseSchema = readResponseSchemasFromSwagger(schemasPath());
    validateJson(response.body,
                 responseSchema.at({"POST", "/photos/{photo_id}/objects", 200}),
                 schemasDir());

    {
        auto objects = db::ObjectInPhotoGateway(*fixture.txnHandle()).load();
        EXPECT_EQ(objects.size(), 1u);
        EXPECT_EQ(objects.at(0).featureId(), featureId);
        EXPECT_EQ(objects.at(0).type(), db::ObjectInPhotoType::Face);
        EXPECT_EQ(objects.at(0).imageBox(), common::ImageBox(0, 0, 32, 32));
    }
}

TEST(photos_api_should, test_create_photo_object_on_rotated_image)
{
    Fixture fixture;
    auto photo = fixture.createFeature();
    auto featureId = photo.id();

    {
        photo.setOrientation(common::ImageOrientation(common::Rotation::CW_90));
        auto txn = fixture.txnHandle();
        db::FeatureGateway(*txn).update(photo, db::UpdateFeatureTxn::Yes);
        txn->commit();
    }

    json::Builder builder;
    builder << [&](json::ObjectBuilder builder) {
        builder["type"] = "Face";
        builder["bbox"] << [](json::ArrayBuilder builder) {
            builder << 0 << 0 << 32 << 32;
        };
    };


    http::MockRequest request(
        http::POST,
        http::URL("http://localhost/photos/" + std::to_string(featureId) + "/objects")
    );
    request.body = builder.str();

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


    {
        auto objects = db::ObjectInPhotoGateway(*fixture.txnHandle()).load();
        EXPECT_EQ(objects.size(), 1u);
        EXPECT_EQ(objects.at(0).featureId(), featureId);
        EXPECT_EQ(objects.at(0).type(), db::ObjectInPhotoType::Face);
        EXPECT_EQ(objects.at(0).imageBox(), common::ImageBox(0, 1048, 32, 1080));
    }
}


TEST(photos_api_should, test_delete_photo_object)
{
    Fixture fixture;
    auto photo = fixture.createFeature();
    auto featureId = photo.id();
    auto objects = addObjectsInPhoto(fixture.txnHandle(), featureId);

    auto objectToDelete = objects.at(0).id();


    http::MockRequest request(
        http::DELETE,
        http::URL("http://localhost/photos/" + std::to_string(featureId) + "/objects/"
            + std::to_string(objectToDelete))
    );

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

    {
        auto object = db::ObjectInPhotoGateway(*fixture.txnHandle()).tryLoadById(objectToDelete);
        EXPECT_FALSE(object.has_value());
    }
}

TEST(photos_api_should, test_get_photos_by_id)
{
    Fixture fixture;
    auto photo1 = fixture.createFeature();
    auto photo2 = fixture.createFeature();

    http::MockRequest request(
        http::GET, http::URL("http://localhost/photos")
                       .addParam("photo_id", photo1.id()));

    auto response = yacare::performTestRequest(request);
    EXPECT_EQ(response.status, 200);
    auto responseSchema = readResponseSchemasFromSwagger(schemasPath());
    auto body = response.body;
    validateJson(body, responseSchema.at({"GET", "/photos", 200}),
                 schemasDir());
    auto bodyJson = json::Value::fromString(body);
    ASSERT_EQ(bodyJson.size(), 1u);
    EXPECT_EQ(bodyJson[0]["id"].as<std::string>(), std::to_string(photo1.id()));
}

TEST(photos_api_should, test_get_photos_by_source)
{
    Fixture fixture;
    auto time = std::chrono::system_clock::now();
    auto photo1 = fixture.createFeature(time);
    time += 50ms;
    auto takenAtAfter = time;
    time += 50ms;
    auto photo2 = fixture.createFeature(time);
    time += 50ms;
    auto takenAtBefore = time;
    time += 50ms;
    auto photo3 = fixture.createFeature(time);

    http::MockRequest request(
        http::GET,
        http::URL("http://localhost/photos")
            .addParam("source_id", "1234")
            .addParam("taken_at_after", chrono::formatIsoDateTime(takenAtAfter))
            .addParam("taken_at_before",
                      chrono::formatIsoDateTime(takenAtBefore))
            .addParam("results", "2"));

    auto response = yacare::performTestRequest(request);
    EXPECT_EQ(response.status, 200);
    auto responseSchema = readResponseSchemasFromSwagger(schemasPath());
    auto body = response.body;
    validateJson(body, responseSchema.at({"GET", "/photos", 200}),
                 schemasDir());
    auto bodyJson = json::Value::fromString(body);
    EXPECT_EQ(bodyJson.size(), 1u);
    EXPECT_EQ(bodyJson[0]["id"].as<std::string>(), std::to_string(photo2.id()));
}

TEST(photos_api_should, test_get_photos_by_uid)
{
    static const std::string USER_1 = "1";
    static const std::string USER_2 = "2";

    Fixture fixture;

    auto time = std::chrono::system_clock::now();
    auto photo1 = fixture.createRidePhoto(USER_1, time);
    time += 50ms;
    auto takenAtAfter = time;
    time += 50ms;
    auto photo2 = fixture.createRidePhoto(USER_1, time);
    auto photo3 = fixture.createRidePhoto(USER_2, time);
    time += 50ms;
    auto photo4 = fixture.createRidePhoto(USER_1, time);
    time += 50ms;
    auto photo5 = fixture.createRidePhoto(USER_1, time);
    time += 50ms;
    auto photo6 = fixture.createRidePhoto(USER_1, time);

    http::MockRequest request(
    http::GET,
    http::URL("http://localhost/photos")
        .addParam("uid", USER_1)
        .addParam("taken_at_after", chrono::formatIsoDateTime(takenAtAfter))
        .addParam("results", "2")
        .addParam("skip", "1"));

    auto response = yacare::performTestRequest(request);
    EXPECT_EQ(response.status, 200);
    auto responseSchema = readResponseSchemasFromSwagger(schemasPath());
    auto body = response.body;
    validateJson(body, responseSchema.at({"GET", "/photos", 200}),
                schemasDir());
    auto bodyJson = json::Value::fromString(body);
    EXPECT_EQ(bodyJson.size(), 2u);
    EXPECT_EQ(bodyJson[0]["id"].as<std::string>(), std::to_string(photo4.id()));
    EXPECT_EQ(bodyJson[1]["id"].as<std::string>(), std::to_string(photo5.id()));
}


struct GetPhotosParams {
    std::optional<db::TId> photoId;
    std::optional<std::string> sourceId;
    std::optional<std::string> uid;
    std::optional<std::string> login;
    std::optional<chrono::TimePoint> takenAtAfter;
    std::optional<chrono::TimePoint> takenAtBefore;
    std::optional<int> results;
    std::optional<int> skip;
    std::optional<std::string> sort;
};

struct GetPhotosResult {

    GetPhotosResult(int httpStatus_, db::TIds photoIds_ = {}, int totalCount_ = 0)
        : httpStatus(httpStatus_)
        , photoIds(std::move(photoIds_))
        , totalCount(totalCount_)
    {}

    template<typename T>
    static auto introspect(T& o) {
        return std::tie(o.httpStatus, o.photoIds, o.totalCount);
    }

    int httpStatus{};
    db::TIds photoIds;
    int totalCount{};
};

using maps::introspection::operator==;
using maps::introspection::operator<<;


TEST(photos_api_should, test_get_photos)
{
    static const std::string USER_1 = "1";
    static const std::string USER_2 = "2";
    static const std::string USER_1_LOGIN = "login_1";
    static const std::string UNKNOWN_USER_LOGIN = "unknown";

    Fixture fixture;

    auto blackbox = std::make_unique<testing::NiceMock<MockBlackboxClient>>();
    ON_CALL(*blackbox, uidByLogin(testing::Eq(USER_1_LOGIN)))
        .WillByDefault(testing::Return(std::stol(USER_1)));
    ON_CALL(*blackbox, uidByLogin(testing::Eq(UNKNOWN_USER_LOGIN)))
        .WillByDefault(testing::Return(std::nullopt));
    auto prevBlackbox = Globals::swap(std::move(blackbox));
    concurrent::ScopedGuard onExit(
        [&] { Globals::swap(std::move(prevBlackbox)); });

    auto time = std::chrono::system_clock::now();
    auto photo1 = fixture.createFeature(time);
    time += 50ms;
    auto photo2 = fixture.createRidePhoto(USER_1, time);
    auto photo3 = fixture.createRidePhoto(USER_2, time);
    time += 50ms;
    auto photo4 = fixture.createRidePhoto(USER_1, time);
    time += 50ms;
    auto photo5 = fixture.createRidePhoto(USER_1, time);
    time += 50ms;
    auto photo6 = fixture.createRidePhoto(USER_1, time);

    auto makeRequest = [&](const GetPhotosParams& params)
        {

            http::URL request("http://localhost/photos");

            if (params.photoId.has_value()) {
                request.addParam("photo_id", params.photoId.value());
            }

            if (params.sourceId.has_value()) {
                request.addParam("source_id", params.sourceId.value());
            }

            if (params.uid.has_value()) {
                request.addParam("uid", params.uid.value());
            }

            if (params.login.has_value()) {
                request.addParam("login", params.login.value());
            }

            if (params.takenAtAfter.has_value()) {
                request.addParam("taken_at_after", chrono::formatIsoDateTime(params.takenAtAfter.value()));
            }

            if (params.takenAtBefore.has_value()) {
                request.addParam("taken_at_before", chrono::formatIsoDateTime(params.takenAtBefore.value()));
            }

            if (params.results.has_value()) {
                request.addParam("results", params.results.value());
            }

            if (params.skip.has_value()) {
                request.addParam("skip", params.skip.value());
            }

            if (params.sort.has_value()) {
                request.addParam("sort", params.sort.value());
            }

            auto response = yacare::performTestRequest(http::MockRequest(http::GET, request));
            GetPhotosResult result(response.status);

            if (response.status == 200) {
                auto responseSchema = readResponseSchemasFromSwagger(schemasPath());
                auto body = response.body;
                validateJson(body, responseSchema.at({"GET", "/photos", 200}),
                            schemasDir());
                auto bodyJson = json::Value::fromString(body);

                for (const auto& itemJson : bodyJson) {
                    result.photoIds.push_back(
                            std::stol(itemJson["id"].as<std::string>()));
                }
                result.totalCount = std::stol(response.headers["X-Total-Count"]);
            }
            return result;
        };

    ;
    EXPECT_EQ(makeRequest(GetPhotosParams{}), GetPhotosResult(200, {photo1.id(), photo2.id(), photo3.id(), photo4.id(), photo5.id(), photo6.id()}, 6));
    // test results, skip, sort
    EXPECT_EQ(makeRequest(GetPhotosParams{.skip=1}), GetPhotosResult(200, {photo2.id(), photo3.id(), photo4.id(), photo5.id(), photo6.id()}, 6));
    EXPECT_EQ(makeRequest(GetPhotosParams{.skip=10}), GetPhotosResult(200, {}, 6));

    EXPECT_EQ(makeRequest(GetPhotosParams{.results=1, .skip=1}), GetPhotosResult(200, {photo2.id()}, 6));
    EXPECT_EQ(makeRequest(GetPhotosParams{.results=1, .skip=5}), GetPhotosResult(200, {photo6.id()}, 6));
    EXPECT_EQ(makeRequest(GetPhotosParams{.results=1, .skip=10}), GetPhotosResult(200, {}, 6));
    EXPECT_EQ(makeRequest(GetPhotosParams{.results=0, .skip=1}), GetPhotosResult(200, {}, 6));

    EXPECT_EQ(makeRequest(GetPhotosParams{.sort="asc"}), GetPhotosResult(200, {photo1.id(), photo2.id(), photo3.id(), photo4.id(), photo5.id(), photo6.id()}, 6));
    EXPECT_EQ(makeRequest(GetPhotosParams{.sort="desc"}), GetPhotosResult(200, {photo6.id(), photo5.id(), photo4.id(), photo3.id(), photo2.id(), photo1.id()}, 6));
    EXPECT_EQ(makeRequest(GetPhotosParams{.results=2, .skip=1, .sort="desc"}), GetPhotosResult(200, {photo5.id(), photo4.id()}, 6));

    EXPECT_EQ(makeRequest(GetPhotosParams{.sort="wrong"}), GetPhotosResult(400));

    // test filters
    EXPECT_EQ(makeRequest(GetPhotosParams{.photoId=photo1.id()}), GetPhotosResult(200, {photo1.id()}, 1));
    EXPECT_EQ(makeRequest(GetPhotosParams{.photoId=photo1.id(), .skip=1}), GetPhotosResult(200, {}, 1));
    EXPECT_EQ(makeRequest(GetPhotosParams{.sourceId=photo1.sourceId()}), GetPhotosResult(200, {photo1.id(), photo2.id(), photo3.id(), photo4.id(), photo5.id(), photo6.id()}, 6));
    EXPECT_EQ(makeRequest(GetPhotosParams{.sourceId=photo1.sourceId()+"_"}), GetPhotosResult(200, {}, 0));
    EXPECT_EQ(makeRequest(GetPhotosParams{.uid=USER_1}), GetPhotosResult(200, {photo2.id(), photo4.id(), photo5.id(), photo6.id()}, 4));
    EXPECT_EQ(makeRequest(GetPhotosParams{.login=USER_1_LOGIN}), GetPhotosResult(200, {photo2.id(), photo4.id(), photo5.id(), photo6.id()}, 4));
    EXPECT_EQ(makeRequest(GetPhotosParams{.login=UNKNOWN_USER_LOGIN}), GetPhotosResult(400));
    EXPECT_EQ(makeRequest(GetPhotosParams{.takenAtAfter=photo2.timestamp()}), GetPhotosResult(200, {photo2.id(), photo3.id(), photo4.id(), photo5.id(), photo6.id()}, 5));
    EXPECT_EQ(makeRequest(GetPhotosParams{.takenAtBefore=photo5.timestamp()}), GetPhotosResult(200, {photo1.id(), photo2.id(), photo3.id(), photo4.id(), photo5.id()}, 5));
    EXPECT_EQ(makeRequest(GetPhotosParams{.takenAtAfter=photo2.timestamp(), .takenAtBefore=photo5.timestamp()}), GetPhotosResult(200, {photo2.id(), photo3.id(), photo4.id(), photo5.id()}, 4));
}

TEST(photos_api_should, test_patch_photos_is_published_batch_update)
{
    Fixture fixture;
    auto time = std::chrono::system_clock::now();
    auto photo1 = fixture.createFeature(time);
    time += 50ms;
    auto photo2 = fixture.createFeature(time);
    time += 50ms;
    auto photo3 = fixture.createFeature();

    auto publish = {photo1.id(), photo3.id()};
    auto unpublish = {photo2.id()};
    http::MockRequest request(
        http::PATCH,
        http::URL("http://localhost/photos_is_published_batch_update")
            .addParam("unpublish", wiki::common::join(unpublish, ',')));
    request.body =
        "{\"publish\": [" +
        wiki::common::join(
            publish,
            [](const auto& val) { return '"' + std::to_string(val) + '"'; },
            ',') +
        "]}";
    auto response = yacare::performTestRequest(request);
    EXPECT_EQ(response.status, 200);

    auto txn = fixture.txnHandle();
    auto gtw = db::FeatureGateway{*txn};
    for (auto& photo : gtw.loadByIds(publish)) {
        EXPECT_TRUE(photo.shouldBePublished());
    }
    for (auto& photo : gtw.loadByIds(unpublish)) {
        EXPECT_FALSE(photo.isPublished());
        EXPECT_FALSE(photo.shouldBePublished());
    }
}

TEST(photos_api_should, test_patch_photos_batch_edit)
{
    Fixture fixture;
    auto time = std::chrono::system_clock::now();
    auto photo1 = fixture.createFeature(time);
    time += 50ms;
    auto photo2 = fixture.createFeature(time);
    time += 50ms;
    auto photo3 = fixture.createFeature(time);

    http::MockRequest request(http::PATCH,
                              http::URL("http://localhost/photos_batch_edit"));
    request.body = "[{\"filter\":{\"sourceId\":\"" + photo1.sourceId() +
                   "\",\"takenAtAfter\":\"" +
                   chrono::formatIsoDateTime(photo1.timestamp()) +
                   "\",\"takenAtBefore\":\"" +
                   chrono::formatIsoDateTime(photo1.timestamp()) +
                   "\"},\"patch\":{\"cameraDirection\":\"Right\"}},"
                   "{\"filter\":{\"ids\":[\"" +
                   std::to_string(photo2.id()) +
                   "\"]},\"patch\":{\"isPublished\":true}},"
                   "{\"filter\":{\"ids\":[\"" +
                   std::to_string(photo3.id()) +
                   "\"]},\"patch\":{\"privacy\":\"Restricted\"}}]";
    auto response = yacare::performTestRequest(request);
    EXPECT_EQ(response.status, 200);

    auto txn = fixture.txnHandle();
    auto gtw = db::FeatureGateway{*txn};
    for (auto& photo : gtw.load()) {
        if (photo.id() == photo1.id()) {
            EXPECT_EQ(photo.cameraDeviation(), db::CameraDeviation::Right);
        }
        else {
            EXPECT_EQ(photo.cameraDeviation(), db::CameraDeviation::Front);
        }

        if (photo.id() == photo2.id()) {
            EXPECT_TRUE(photo.shouldBePublished());
        }
        else {
            EXPECT_FALSE(photo.shouldBePublished());
        }

        if (photo.id() == photo3.id()) {
            EXPECT_EQ(photo.privacy(), db::FeaturePrivacy::Restricted);
        }
        else {
            EXPECT_EQ(photo.privacy(), db::FeaturePrivacy::Public);
        }
    }
}

TEST(photos_api_should, test_patch_photos_batch_edit_by_user_id)
{
    Fixture fixture;
    std::string UID1 = "1";
    std::string UID2 = "2";
    std::string LOGIN1 = "user1";
    std::string LOGIN2 = "user2";

    auto photo1 = fixture.createFeature();
    auto photo2 = fixture.createRidePhoto(UID1);
    auto photo3 = fixture.createRidePhoto(UID2);

    auto blackbox = std::make_unique<testing::NiceMock<MockBlackboxClient>>();
    EXPECT_CALL(*blackbox, uidByLogin(testing::Eq(LOGIN2)))
        .WillOnce(testing::Return(std::stol(UID2)));
    auto prevBlackbox = Globals::swap(std::move(blackbox));
    concurrent::ScopedGuard onExit(
        [&] { Globals::swap(std::move(prevBlackbox)); });

    http::MockRequest request(http::PATCH,
                              http::URL("http://localhost/photos_batch_edit"));
    request.body = "[{\"filter\":{\"uid\":\"" + UID1 +
                    "\",\"takenAtAfter\":\"" +
                   chrono::formatIsoDateTime(photo1.timestamp()) +
                   "\",\"takenAtBefore\":\"" +
                   chrono::formatIsoDateTime(photo3.timestamp()) +
                   "\"},\"patch\":{\"cameraDirection\":\"Right\"}}," +
                   "{\"filter\":{\"login\":\"" + LOGIN2 +
                   "\",\"takenAtAfter\":\"" +
                   chrono::formatIsoDateTime(photo1.timestamp()) +
                   "\",\"takenAtBefore\":\"" +
                   chrono::formatIsoDateTime(photo3.timestamp()) +
                   "\"},\"patch\":{\"cameraDirection\":\"Left\"}}]";

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

    auto txn = fixture.txnHandle();
    auto gtw = db::FeatureGateway{*txn};
    for (auto& photo : gtw.load()) {
        if (photo.id() == photo2.id()) {
            EXPECT_EQ(photo.cameraDeviation(), db::CameraDeviation::Right);
        } else if (photo.id() == photo3.id()) {
            EXPECT_EQ(photo.cameraDeviation(), db::CameraDeviation::Left);
        } else {
            EXPECT_EQ(photo.cameraDeviation(), db::CameraDeviation::Front);
        }
    }
}

TEST(photos_api_should, test_patch_photos_batch_edit_unknown_user)
{
    Fixture fixture;
    std::string LOGIN = "user";
    auto time = std::chrono::system_clock::now();

    auto blackbox = std::make_unique<testing::NiceMock<MockBlackboxClient>>();
    EXPECT_CALL(*blackbox, uidByLogin(testing::Eq(LOGIN)))
        .WillOnce(testing::Return(std::nullopt));
    auto prevBlackbox = Globals::swap(std::move(blackbox));
    concurrent::ScopedGuard onExit(
        [&] { Globals::swap(std::move(prevBlackbox)); });

    http::MockRequest request(http::PATCH,
                              http::URL("http://localhost/photos_batch_edit"));
    request.body = "[{\"filter\":{\"login\":\"" + LOGIN +
                    "\",\"takenAtAfter\":\"" +
                   chrono::formatIsoDateTime(time) +
                   "\",\"takenAtBefore\":\"" +
                   chrono::formatIsoDateTime(time) +
                   "\"},\"patch\":{\"cameraDirection\":\"Right\"}}]";

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


TEST(photos_api_should, test_patch_photos_batch_edit_malformat_params)
{
    Fixture fixture;
    std::string LOGIN = "user";
    auto time = std::chrono::system_clock::now();

    http::MockRequest request(http::PATCH,
                              http::URL("http://localhost/photos_batch_edit"));
    request.body = "[{\"filter\":{\"sourceId\":\"source_id\",\"takenAtAfter\":\"" +
                   chrono::formatIsoDateTime(time) +
                   "\"},\"patch\":{\"cameraDirection\":\"Right\"}}]";

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


/**
 * @see https://jing.yandex-team.ru/files/naplavkov/mapsmrc_2561.png
 */
TEST(photos_api_should, test_patch_photos_adjust_positions)
{
    Fixture fixture;
    auto photos = makeRide(
        db::GraphType::Road, Globals::roadMatcher(), fixture.txnHandle());
    // index -> point
    std::vector<std::pair<size_t, geolib3::Point2>> snapPoints{
        {0, {37.605914, 55.690336}},
        {2, {37.606268, 55.690744}}
    };

    auto makeRequestBody = [&](bool dryRun) {
        json::Builder bld;
        bld << [&](json::ObjectBuilder bld) {
            bld["filter"] << [&](json::ObjectBuilder bld) {
                bld["sourceId"] = photos.front().sourceId();
                bld["takenAtAfter"] =
                    chrono::formatIsoDateTime(photos.front().timestamp());
                bld["takenAtBefore"] =
                    chrono::formatIsoDateTime(photos.back().timestamp());
            };
            bld["snapTo"] << [&](json::ArrayBuilder bld) {
                for (auto& idxPoint : snapPoints) {
                        bld << [&](json::ObjectBuilder bld) {
                        bld["photoId"] = std::to_string(photos[idxPoint.first].id());
                        bld["position"] << [&](json::ArrayBuilder bld) {
                            bld << idxPoint.second.x();
                            bld << idxPoint.second.y();
                        };
                    };
                }

            };
            bld["dryRun"] = dryRun;
        };
        return bld.str();
    };

    {
        http::MockRequest request(
            http::POST, http::URL("http://localhost/photos_adjust_positions"));
        request.body = makeRequestBody(true);
        auto response = yacare::performTestRequest(request);
        ASSERT_EQ(response.status, 200);

        auto responseSchema = readResponseSchemasFromSwagger(schemasPath());
        validateJson(response.body,
                    responseSchema.at({"POST", "/photos_adjust_positions", 200}),
                    schemasDir());

        auto adjustedPhotos = db::FeatureGateway{*fixture.txnHandle()}.load(
            db::table::Feature::sourceId.equals(photos.front().sourceId()) &&
            db::table::Feature::date.between(photos.front().timestamp(),
                                            photos.back().timestamp()),
            sql_chemistry::orderBy(db::table::Feature::id).asc());

        EXPECT_EQ(adjustedPhotos.size(), photos.size());
        for (size_t i = 0; i < adjustedPhotos.size(); ++i) {
            EXPECT_EQ(adjustedPhotos[i].geodeticPos(), photos[i].geodeticPos());
        }
    }

    {
        http::MockRequest request(
            http::POST, http::URL("http://localhost/photos_adjust_positions"));
        request.body = makeRequestBody(false);
        auto response = yacare::performTestRequest(request);
        ASSERT_EQ(response.status, 200);

        auto responseSchema = readResponseSchemasFromSwagger(schemasPath());
        validateJson(response.body,
                    responseSchema.at({"POST", "/photos_adjust_positions", 200}),
                    schemasDir());


        auto adjustedPhotos = db::FeatureGateway{*fixture.txnHandle()}.load(
            db::table::Feature::sourceId.equals(photos.front().sourceId()) &&
            db::table::Feature::date.between(photos.front().timestamp(),
                                            photos.back().timestamp()),
            sql_chemistry::orderBy(db::table::Feature::id).asc());

        EXPECT_EQ(adjustedPhotos.size(), photos.size());
        for (size_t i = 0; i < adjustedPhotos.size(); ++i) {
            EXPECT_NE(adjustedPhotos[i].geodeticPos(), photos[i].geodeticPos())
                << "photo " << i << " was not modified";
        }

        for (const auto& [idx, point] : snapPoints) {
            EXPECT_EQ(adjustedPhotos[idx].geodeticPos(), point)
                << "photo " << idx << " has wrong position";
        }

    }
}

TEST(photos_api_should, test_patch_photos_adjust_positions_augmented_track_points)
{
    Fixture fixture;

    auto photos = makeRide(
        db::GraphType::Road, Globals::roadMatcher(), fixture.txnHandle());

    auto makeRequestBody = [&](db::TId photoId, const geolib3::Point2& pos) {
        json::Builder bld;
        bld << [&](json::ObjectBuilder bld) {
            bld["filter"] << [&](json::ObjectBuilder bld) {
                bld["sourceId"] = photos.front().sourceId();
                bld["takenAtAfter"] =
                    chrono::formatIsoDateTime(photos.front().timestamp());
                bld["takenAtBefore"] =
                    chrono::formatIsoDateTime(photos.back().timestamp());
            };
            bld["snapTo"] << [&](json::ArrayBuilder bld) {
                bld << [&](json::ObjectBuilder bld) {
                    bld["photoId"] = std::to_string(photoId);
                    bld["position"] << [&](json::ArrayBuilder bld) {
                        bld << pos.x();
                        bld << pos.y();
                    };
                };
            };
            bld["dryRun"] = false;
        };
        return bld.str();
    };

    http::MockRequest request(
        http::POST, http::URL("http://localhost/photos_adjust_positions"));
    for (size_t idx = 0; idx < photos.size(); ++idx) {
        for (size_t attempt = 1; attempt < 3; ++attempt) {
            auto& photo = photos[idx];
            auto offset = geolib3::Vector2(attempt * 0.0001, attempt * 0.0001);
            auto pos = photo.geodeticPos() + offset;
            request.body = makeRequestBody(photo.id(), pos);
            auto response = yacare::performTestRequest(request);
            EXPECT_EQ(response.status, 200);

            // previous track points are not erased
            EXPECT_EQ(idx + 1,
                      db::TrackPointGateway{*fixture.txnHandle()}.count(
                          db::table::TrackPoint::isAugmented.is(true)));

            auto trackPoints = db::TrackPointGateway{*fixture.txnHandle()}.load(
                db::table::TrackPoint::sourceId.equals(photo.sourceId()) &&
                db::table::TrackPoint::timestamp.equals(photo.timestamp()) &&
                db::table::TrackPoint::isAugmented.is(true));

            // only one track point for photo
            EXPECT_EQ(trackPoints.size(), 1u);
            auto& trackPoint = trackPoints.front();
            EXPECT_EQ(trackPoint.geodeticPos(), pos);

            auto adjustedPhotos = db::FeatureGateway{*fixture.txnHandle()}.load(
                db::table::Feature::sourceId.equals(photo.sourceId()) &&
                db::table::Feature::date.equals(photo.timestamp()));

            // photo is snapped
            EXPECT_EQ(adjustedPhotos.size(), 1u);
            auto& adjustedPhoto = adjustedPhotos.front();
            EXPECT_EQ(adjustedPhoto.geodeticPos(), pos);
        }
    }
}

} // maps::mrc::tasks_planner::tests
