#include <library/cpp/testing/gtest/gtest.h>
#include <maps/infra/yacare/include/test_utils.h>
#include <maps/libs/concurrent/include/scoped_guard.h>
#include <maps/libs/geolib/include/test_tools/comparison.h>
#include <maps/libs/geolib/include/vector.h>
#include <maps/libs/img/include/raster.h>
#include <maps/libs/mongo/include/init.h>
#include <maps/tools/grinder/common/include/internal_client.h>
#include <maps/wikimap/mapspro/services/mrc/libs/blackbox_client/include/blackbox_client.h>
#include <maps/wikimap/mapspro/services/mrc/libs/db/include/eye/hypothesis.h>
#include <maps/wikimap/mapspro/services/mrc/libs/db/include/eye/hypothesis_attrs.h>
#include <maps/wikimap/mapspro/services/mrc/libs/db/include/eye/hypothesis_gateway.h>
#include <maps/wikimap/mapspro/services/mrc/libs/db/include/feature_gateway.h>
#include <maps/wikimap/mapspro/services/mrc/libs/db/include/ride_gateway.h>
#include <maps/wikimap/mapspro/services/mrc/libs/db/include/ride_utility.h>
#include <maps/wikimap/mapspro/services/mrc/libs/db/include/walk_object_gateway.h>
#include <maps/wikimap/mapspro/services/mrc/long_tasks/async_ride_correction/lib/utility.h>
#include <maps/wikimap/mapspro/services/mrc/long_tasks/ride_inspector/lib/ride.h>
#include <maps/wikimap/mapspro/services/mrc/ugc_back/lib/configuration.h>
#include <maps/wikimap/mapspro/services/mrc/ugc_back/lib/utility.h>
#include <yandex/maps/geolib3/proto.h>
#include <yandex/maps/mrc/unittest/database_fixture.h>
#include <yandex/maps/mrc/unittest/grinder_fixture.h>
#include <yandex/maps/mrc/unittest/local_server.h>
#include <yandex/maps/mrc/unittest/unittest_config.h>
#include <yandex/maps/proto/mrcugc/ride.pb.h>

#include <filesystem>
#include <memory>

namespace maps::mrc::ugc_back::tests {

using namespace geolib3;
using namespace std::chrono_literals;
namespace fc = maps::feedback_client;

namespace {

const auto TIME = chrono::parseIsoDateTime("2021-02-04T15:25:00+03");
const auto SOURCE_ID = std::string{"src"};
const auto CLIENT_RIDE_ID = std::string{"b0c2d8c8-6fc6-45d0-9e8e-45e37bd29636"};
const auto NO_CLIENT_RIDE_ID = std::nullopt;
const auto FEEDBACK_TASK_ID = db::TId{12345};

const auto CLIENT_RIDE_IDS = std::vector<std::optional<std::string>>{
    CLIENT_RIDE_ID,
    NO_CLIENT_RIDE_ID,
};

struct Mongocxx {
    Mongocxx() { mongo::init(); }
};

struct Playground : Mongocxx {
    Playground()
        : grinderConfig(grinderFixture.grinderConfigPath())
        , grinderLogger(grinderConfig, std::chrono::seconds(10))
        , grinderClient(grinderConfig, grinderLogger)
    {
    }

    static Playground& instance()
    {
        static Playground playground;
        return playground;
    }

    unittest::WithUnittestConfig<unittest::DatabaseFixture,
                                 unittest::MdsStubFixture>
        databaseFixture;
    unittest::GrinderFixture grinderFixture;

    grinder::Config grinderConfig;
    grinder::MongoTaskLogger grinderLogger;
    grinder::InternalClient grinderClient;
};

chrono::TimePoint constGetCurrentTimestamp()
{
    return chrono::TimePoint(chrono::TimePoint::duration(0));
};

db::eye::Hypothesis makeHypothesis(db::eye::HypothesisType hypothesisType)
{
    using namespace db::eye;
    static const auto POS = geolib3::Point2{100, 500};
    switch (hypothesisType) {
        case HypothesisType::AbsentTrafficLight:
            return Hypothesis{POS, AbsentTrafficLightAttrs{}};
        case HypothesisType::AbsentHouseNumber:
            return Hypothesis{POS, AbsentHouseNumberAttrs{}};
        case HypothesisType::WrongSpeedLimit:
            return Hypothesis{POS, WrongSpeedLimitAttrs{}};
        case HypothesisType::AbsentParking:
            return Hypothesis{POS, AbsentParkingAttrs{}};
        case HypothesisType::WrongParkingFtType:
            return Hypothesis{POS, WrongParkingFtTypeAttrs{}};
        case HypothesisType::TrafficSign:
            return Hypothesis{POS, TrafficSignAttrs{}};
        case HypothesisType::WrongDirection:
            return Hypothesis{POS, WrongDirectionAttrs{}};
        case HypothesisType::ProhibitedPath:
            return Hypothesis{POS, ProhibitedPathAttrs{}};
        case HypothesisType::LaneHypothesis:
            return Hypothesis{POS, LaneHypothesisAttrs{}};
        case HypothesisType::SpeedBump:
            return Hypothesis{POS, SpeedBumpAttrs{}};
        case HypothesisType::RailwayCrossing:
            return Hypothesis{POS, RailwayCrossingAttrs{}};
    }
}

db::FeedbackIdToHypothesisTypeMap makeRideHypotheses()
{
    static auto feedbackId = db::TId{};
    return db::FeedbackIdToHypothesisTypeMap{
        {++feedbackId, db::eye::HypothesisType::AbsentTrafficLight},
        {++feedbackId, db::eye::HypothesisType::AbsentHouseNumber}};
}

class MockFeedbackClient : public feedback_client::IClient {
public:
    MockFeedbackClient(std::vector<fc::Task> tasks)
    {
        for (auto&& task : tasks) {
            auto id = task.id;
            tasks_.emplace(id, std::move(task));
        }
    }

    static fc::Task makeMockTask(
        int64_t id,
        fc::TaskState state,
        std::optional<fc::TaskResolution> resolution)
    {
        return fc::Task(
            id, "source", "type", fc::Workflow::Task, resolution,
            geolib3::Point2{1.0, 1.0}, std::string{"description"},
            fc::SourceContext::fromJson(json::Value::fromString("{}")), state);
    }

    fc::Task create(const fc::NewTask&) override
    {
        throw maps::RuntimeError("IClient::created() should not be called");
    }
    fc::Tasks get(const fc::Filter&) override
    {
        throw maps::RuntimeError("IClient::get() should not be called");
    }
    fc::Task getById(int64_t id) override
    {
        return tasks_.at(id);
    }
    fc::Task resolve(int64_t id, fc::TaskResolution resolution) override
    {
        auto& task = tasks_.at(id);
        task.state = (resolution == fc::TaskResolution::Accepted)
            ? fc::TaskState::Accepted
            : fc::TaskState::Rejected;
        task.resolution = resolution;
        return task;
    }
private:
    std::map<int64_t, fc::Task> tasks_;
};

struct Fixture : testing::Test {
    yacare::tests::UserIdHeaderFixture enableUserIdThroughHeader;

    Fixture()
        : ugcEventsLogPath("ugc_events.log")
        , ugcEventLogger(new ugc_event_logger::Logger(ugcEventsLogPath,
                                                      std::chrono::seconds(600),
                                                      constGetCurrentTimestamp))
    {
        clearData();
        auto cfg = makeConfiguration(config(),
                                     Playground::instance().grinderConfig,
                                     *ugcEventLogger,
                                     std::chrono::seconds{10},
                                     std::nullopt);
        Configuration::swap(cfg);
        clearGrinder();
    }

    void clearGrinder()
    {
        for (const auto& taskId : grinderClient().listTaskIds()) {
            Playground::instance().grinderLogger.deleteRecords(taskId);
            grinderClient().deleteTask(taskId);
            grinderClient().deleteTaskChangeRequests(taskId);
        }
    }

    ~Fixture() { std::filesystem::remove(ugcEventsLogPath); }

    const common::Config& config() const
    {
        return Playground::instance().databaseFixture.config();
    }

    pgpool3::Pool& pool()
    {
        return Playground::instance().databaseFixture.pool();
    }

    grinder::InternalClient& grinderClient()
    {
        return Playground::instance().grinderClient;
    }

    void clearData()
    {
        auto features =
            db::FeatureGateway{*pool().masterReadOnlyTransaction()}.load();
        auto mds =
            Playground::instance().databaseFixture.config().makeMdsClient();
        for (const auto& feature : features) {
            mds.del(feature.mdsKey());
        }
        Playground::instance().databaseFixture.postgres().truncateTables();
    }

    db::Feature makePhoto(const std::string& userId,
                          const std::optional<std::string>& clientRideId,
                          db::Dataset dataset,
                          chrono::TimePoint time)
    {
        static auto jpg = maps::common::readFileToString("opencv_1.jpg");
        static auto pos = Point2{37.5636, 55.6676};
        static auto delta = Vector2{0.0001, 0.};
        static auto counter = 0;
        auto mdsKey =
            config().makeMdsClient().post(std::to_string(++counter), jpg).key();
        auto result = sql_chemistry::GatewayAccess<db::Feature>::construct()
                          .setDataset(dataset)
                          .setSourceId(SOURCE_ID)
                          .setTimestamp(time)
                          .setGeodeticPos(pos + delta * counter)
                          .setHeading(Heading{90})
                          .setMdsKey(mdsKey)
                          .setSize({240, 180})
                          .setUserId(userId)
                          .setAutomaticShouldBePublished(true)
                          .setIsPublished(true)
                          ;
        if (clientRideId) {
            result.setClientRideId(*clientRideId);
        }
        return result;
    }

    db::WalkObject makeWalkObject(const std::string& userId,
                                  chrono::TimePoint time)
    {
        return db::WalkObject(
            db::Dataset::Walks,
            SOURCE_ID,
            time,
            db::WalkFeedbackType::Other,
            Point2{37.5636, 55.6676})
                .setUserId(userId);
    }

    db::Feature savePhoto(const std::string& userId,
                          const std::optional<std::string>& clientRideId,
                          db::Dataset dataset = db::Dataset::Rides)
    {
        auto photo = makePhoto(userId, clientRideId, dataset, TIME);
        auto txn = pool().masterWriteableTransaction();
        db::FeatureGateway{*txn}.insert(photo);
        txn->commit();
        return photo;
    }

    db::Ride saveRide(const std::string& userId,
                      const std::optional<std::string>& clientRideId)
    {
        auto photos = db::Features{
            makePhoto(userId, clientRideId, db::Dataset::Rides, TIME)};
        auto ride = ride_inspector::makeRide(photos.begin(), photos.end());
        auto txn = pool().masterWriteableTransaction();
        db::FeatureGateway{*txn}.insert(photos);
        db::RideGateway{*txn}.insertx(ride);
        txn->commit();
        return ride;
    }

    void saveRideHypotheses(db::TId rideId,
                            const db::FeedbackIdToHypothesisTypeMap& hypotheses)
    {
        auto txn = pool().masterWriteableTransaction();
        for (const auto& [feedbackId, hypothesisType] : hypotheses) {
            auto hypothesis = makeHypothesis(hypothesisType);
            db::eye::HypothesisGateway(*txn).insertx(hypothesis);
            auto hypothesisFeedback =
                db::eye::HypothesisFeedback{hypothesis.id(), feedbackId};
            db::eye::HypothesisFeedbackGateway(*txn).insertx(
                hypothesisFeedback);
            auto rideHypothesis = db::RideHypothesis{rideId, hypothesis.id()};
            db::RideHypothesisGateway(*txn).insert(rideHypothesis);
        }
        txn->commit();
    }

    db::WalkObject saveWalkObjectWithPhoto(const std::string& userId)
    {
        auto photos = db::Features{
            makePhoto(userId, NO_CLIENT_RIDE_ID, db::Dataset::Walks, TIME)};
        auto walkObject = makeWalkObject(userId, TIME);
        walkObject.setFeedbackId(FEEDBACK_TASK_ID);
        walkObject.setObjectStatus(db::ObjectStatus::Published);

        auto txn = pool().masterWriteableTransaction();
        db::WalkObjectGateway{*txn}.insertx(walkObject);
        for (auto photo : photos) {
            photo.setWalkObjectId(walkObject.id());
        }
        db::FeatureGateway{*txn}.insert(photos);
        txn->commit();
        return walkObject;
    }

    std::string finishUgcLogAndGetContents()
    {
        ugcEventLogger.reset();
        return maps::common::readFileToString(ugcEventsLogPath);
    }

    std::string ugcEventsLogPath;
    std::unique_ptr<ugc_event_logger::Logger> ugcEventLogger;
};

bool checkRide(const yandex::maps::proto::mrcphoto::ugc::ride::MyRide& lhs,
               const db::Ride& rhs)
{
    return lhs.id() == TString(std::to_string(rhs.rideId())) &&
           ugc_back::getClientRideId(lhs) == rhs.clientId() &&
           lhs.has_started_at() && lhs.has_finished_at() &&
           lhs.has_duration() && lhs.has_distance() &&
           lhs.photos_count() == rhs.photos() &&
           lhs.published_photos_count() == rhs.publishedPhotos() &&
           lhs.has_track() && lhs.has_bbox() &&
           lhs.show_authorship() == rhs.showAuthorship() &&
           lhs.has_track_preview() &&
           lhs.has_ride_status();
}

std::optional<std::string> getClientRideId(const json::Value& val)
{
    if (val.hasField(ride_correction::CLIENT_RIDE_ID_FIELD)) {
        return val[ride_correction::CLIENT_RIDE_ID_FIELD].as<std::string>();
    }
    return std::nullopt;
}

bool checkRide(const json::Value& lhs, const db::Ride& rhs)
{
    return lhs["type"].as<std::string>() ==
               ride_correction::GRINDER_WORKER_TYPE &&
           lhs[ride_correction::USER_ID_FIELD].as<std::string>() ==
               rhs.userId() &&
           lhs[ride_correction::SOURCE_ID_FIELD].as<std::string>() ==
               rhs.sourceId() &&
           getClientRideId(lhs) == rhs.clientId() &&
           maps::chrono::convertFromUnixTime(
               lhs[maps::mrc::ride_correction::START_TIME_FIELD]
                   .as<uint64_t>()) <= rhs.startTime() &&
           maps::chrono::convertFromUnixTime(
               lhs[maps::mrc::ride_correction::END_TIME_FIELD]
                   .as<uint64_t>()) >= rhs.endTime();
}

bool checkPhoto(
    const yandex::maps::proto::mrcphoto::ugc::ride::PhotoStream::Item& lhs,
    const db::Feature& rhs,
    const geolib3::Polyline2& polyline)
{
    return lhs.photo_id() == std::to_string(rhs.id()) &&
           geolib3::test_tools::approximateEqual(
               getGeodeticPos(rhs),
               decode(lhs.position()).pointOf(polyline),
               geolib3::EPS);
}

}  // anonymous namespace

TEST_F(Fixture, test_get)
{
    for (const auto& clientRideId : CLIENT_RIDE_IDS) {
        auto userId = std::string{"1"};
        auto ride = saveRide(userId, clientRideId);
        auto rideHypotheses = makeRideHypotheses();
        saveRideHypotheses(ride.rideId(), rideHypotheses);
        auto rq = http::MockRequest(
            http::GET,
            http::URL{"http://localhost/v1/rides/my/get"}
                .addParam("ride_id", std::to_string(ride.rideId()))
                .addParam("lang", "en_US"));
        rq.headers.emplace(yacare::tests::USER_ID_HEADER, userId);
        auto resp = yacare::performTestRequest(rq);
        EXPECT_EQ(resp.status, 200);
        auto msg = yandex::maps::proto::mrcphoto::ugc::ride::MyRide{};
        EXPECT_TRUE(msg.ParseFromString(TString{resp.body}));
        EXPECT_TRUE(checkRide(msg, ride));
        EXPECT_EQ(db::getRideHypotheses(msg), rideHypotheses);
        EXPECT_EQ(msg.started_at().text(), "12:25 PM");
        EXPECT_EQ(msg.finished_at().text(), "12:25 PM");
        EXPECT_EQ(msg.duration().text(), "0 sec");
        EXPECT_EQ(msg.distance().text(), "0 ft");
        EXPECT_TRUE(msg.ride_status().has_processed());
    }
}

TEST_F(Fixture, test_get_invalid_ride_id)
{
    auto userId = std::string{"1"};
    auto ride = saveRide(userId, NO_CLIENT_RIDE_ID);
    auto rq = http::MockRequest(
        http::GET,
        http::URL{"http://localhost/v1/rides/my/get"}.addParam("ride_id", "0"));
    rq.headers.emplace(yacare::tests::USER_ID_HEADER, userId);
    auto resp = yacare::performTestRequest(rq);
    EXPECT_EQ(resp.status, 404);
}

TEST_F(Fixture, test_get_invalid_user_id)
{
    auto validUserId = std::string{"1"};
    auto invalidUserId = std::string{"2"};
    auto ride = saveRide(validUserId, NO_CLIENT_RIDE_ID);
    auto rq = http::MockRequest(
        http::GET,
        http::URL{"http://localhost/v1/rides/my/get"}.addParam(
            "ride_id", std::to_string(ride.rideId())));
    rq.headers.emplace(yacare::tests::USER_ID_HEADER, invalidUserId);
    auto resp = yacare::performTestRequest(rq);
    EXPECT_EQ(resp.status, 404);
}

TEST_F(Fixture, test_get_invalid_lang)
{
    auto userId = std::string{"1"};
    auto ride = saveRide(userId, NO_CLIENT_RIDE_ID);
    auto rq = http::MockRequest(
        http::GET,
        http::URL{"http://localhost/v1/rides/my/get"}
            .addParam("ride_id", std::to_string(ride.rideId()))
            .addParam("lang", "fiasco"));
    rq.headers.emplace(yacare::tests::USER_ID_HEADER, userId);
    auto resp = yacare::performTestRequest(rq);
    EXPECT_EQ(resp.status, 400);
}

TEST_F(Fixture, test_photo)
{
    auto userId = std::string{"1"};
    auto photo = savePhoto(userId, NO_CLIENT_RIDE_ID);
    auto rq = http::MockRequest(
        http::GET,
        http::URL{"http://localhost/v1/rides/my/photo"}.addParam(
            "id", std::to_string(photo.id())));
    rq.headers.emplace(yacare::tests::USER_ID_HEADER, userId);
    auto resp = yacare::performTestRequest(rq);
    EXPECT_EQ(resp.status, 200);
    auto msg = yandex::maps::proto::mrcphoto::ugc::ride::MyPhoto{};
    EXPECT_TRUE(msg.ParseFromString(TString{resp.body}));
    EXPECT_EQ(msg.id(), TString{std::to_string(photo.id())});
    EXPECT_TRUE(msg.has_taken_at());
    EXPECT_TRUE(msg.has_geo_photo());
}

TEST_F(Fixture, test_photo_json_format)
{
    auto userId = std::string{"1"};
    auto photo = savePhoto(userId, NO_CLIENT_RIDE_ID);
    auto id = std::to_string(photo.id());
    auto rq = http::MockRequest(http::GET,
                                http::URL{"http://localhost/v1/rides/my/photo"}
                                    .addParam("id", id)
                                    .addParam("format", "json"));
    rq.headers.emplace(yacare::tests::USER_ID_HEADER, userId);
    auto resp = yacare::performTestRequest(rq);
    EXPECT_EQ(resp.status, 200);
    auto json = json::Value::fromString(resp.body);
    EXPECT_EQ(json["geo_photo"]["image"]["url_template"].as<std::string>(),
              "https://localhost/v1/rides/my/photo_image?photo_id=" + id +
                  "&size_name=%s");
}

TEST_F(Fixture, test_photo_invalid_id)
{
    auto userId = std::string{"1"};
    auto photo = savePhoto(userId, NO_CLIENT_RIDE_ID);
    auto rq = http::MockRequest(
        http::GET,
        http::URL{"http://localhost/v1/rides/my/photo"}.addParam("id", "0"));
    rq.headers.emplace(yacare::tests::USER_ID_HEADER, userId);
    auto resp = yacare::performTestRequest(rq);
    EXPECT_EQ(resp.status, 404);
}

TEST_F(Fixture, test_photo_invalid_user_id)
{
    auto validUserId = std::string{"1"};
    auto invalidUserId = std::string{"2"};
    auto photo = savePhoto(validUserId, NO_CLIENT_RIDE_ID);
    auto rq = http::MockRequest(
        http::GET,
        http::URL{"http://localhost/v1/rides/my/photo"}.addParam(
            "id", std::to_string(photo.id())));
    rq.headers.emplace(yacare::tests::USER_ID_HEADER, invalidUserId);
    auto resp = yacare::performTestRequest(rq);
    EXPECT_EQ(resp.status, 404);
}

TEST_F(Fixture, test_photo_substitute_host)
{
    auto userId = std::string{"1"};
    auto photo = savePhoto(userId, NO_CLIENT_RIDE_ID);
    auto id = std::to_string(photo.id());
    const std::string EXPECTED_HOST = "core-nmaps-mrc-ugc-back.maps.yandex.ua";
    auto rq = http::MockRequest(
        http::GET,
        http::URL{"http://" + EXPECTED_HOST + "/v1/rides/my/photo"}
            .addParam("id", id)
            .addParam("format", "json"));
    rq.headers.emplace(yacare::tests::USER_ID_HEADER, userId);
    auto resp = yacare::performTestRequest(rq);
    EXPECT_EQ(resp.status, 200);
    auto json = json::Value::fromString(resp.body);
    EXPECT_EQ(json["geo_photo"]["image"]["url_template"].as<std::string>(),
              "https://" + EXPECTED_HOST +
                  "/v1/rides/my/photo_image?photo_id=" + id + "&size_name=%s");
}

TEST_F(Fixture, test_photo_image)
{
    auto userId = std::string{"1"};
    auto ridePhoto = savePhoto(userId, NO_CLIENT_RIDE_ID, db::Dataset::Rides);
    auto walkPhoto = savePhoto(userId, NO_CLIENT_RIDE_ID, db::Dataset::Walks);

    std::map<std::string, db::Feature> urlPathToPhoto{
        {std::string{"/v1/rides/my/photo_image"}, std::move(ridePhoto)},
        {std::string{"/v1/walk_objects/my/photos"}, std::move(walkPhoto)}
    };

    for (const auto& [path, photo] : urlPathToPhoto) {
        auto rq =
            http::MockRequest(http::GET,
                              http::URL{"http://localhost" + path}.addParam(
                                  "photo_id", std::to_string(photo.id())));
        rq.headers.emplace(yacare::tests::USER_ID_HEADER, userId);
        auto resp = yacare::performTestRequest(rq);
        EXPECT_EQ(resp.status, 200);
        auto orig = img::RasterImage::fromJpegBlob(resp.body);

        rq = http::MockRequest(
            http::GET,
            http::URL{"http://localhost" + path}
                .addParam("photo_id", std::to_string(photo.id()))
                .addParam("size_name", "thumbnail"));
        rq.headers.emplace(yacare::tests::USER_ID_HEADER, userId);
        resp = yacare::performTestRequest(rq);
        EXPECT_EQ(resp.status, 200);
        auto thumb = img::RasterImage::fromJpegBlob(resp.body);

        EXPECT_GT(orig.size().width, thumb.size().width);
        EXPECT_GT(orig.size().height, thumb.size().height);
    }
}

TEST_F(Fixture, test_photo_log_ugc_event)
{
    auto userId = std::string{"1"};
    auto photo = savePhoto(userId, NO_CLIENT_RIDE_ID);
    auto rq = http::MockRequest(
        http::GET,
        http::URL{"http://localhost/v1/rides/my/photo_image"}.addParam(
            "photo_id", std::to_string(photo.id())));
    rq.headers.emplace(yacare::tests::USER_ID_HEADER, userId);
    auto resp = yacare::performTestRequest(rq);

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

    EXPECT_EQ(finishUgcLogAndGetContents(), EXPECTED_LOG);
}

TEST_F(Fixture, test_photo_image_options)
{
    const auto ACCESS_CONTROL_ALLOW_ORIGIN = "Access-Control-Allow-Origin";
    const auto ACCESS_CONTROL_ALLOW_METHODS = "Access-Control-Allow-Methods";
    struct {
        std::string origin;
        bool allowed;
    } tests[] = {
        {"http://maps.yandex.ru", true},
        {"bla-bla-bla", false},
    };

    for (const auto& path : {std::string{"/v1/rides/my/photo_image"},
                             std::string{"/v1/walk_objects/my/photos"}}) {
        for (const auto& [origin, allowed] : tests) {
            auto rq = http::MockRequest(http::Method{"OPTIONS"},
                                        http::URL{"http://localhost" + path});
            rq.headers["Origin"] = origin;
            auto resp = yacare::performTestRequest(rq);
            EXPECT_EQ(resp.status, 200);
            if (allowed) {
                EXPECT_EQ(resp.headers.at(ACCESS_CONTROL_ALLOW_ORIGIN), origin);
                EXPECT_EQ(resp.headers.at(ACCESS_CONTROL_ALLOW_METHODS),
                          "GET, OPTIONS");
            }
            else {
                EXPECT_FALSE(
                    resp.headers.contains(ACCESS_CONTROL_ALLOW_ORIGIN));
                EXPECT_FALSE(
                    resp.headers.contains(ACCESS_CONTROL_ALLOW_METHODS));
            }
        }
    }
}

TEST_F(Fixture, test_photo_image_invalid_photo_id)
{
    auto userId = std::string{"1"};
    auto photo = savePhoto(userId, NO_CLIENT_RIDE_ID);
    auto rq = http::MockRequest(
        http::GET,
        http::URL{"http://localhost/v1/rides/my/photo_image"}.addParam(
            "photo_id", "0"));
    rq.headers.emplace(yacare::tests::USER_ID_HEADER, userId);
    auto resp = yacare::performTestRequest(rq);
    EXPECT_EQ(resp.status, 404);
}

TEST_F(Fixture, test_photo_image_invalid_user_id)
{
    auto validUserId = std::string{"1"};
    auto invalidUserId = std::string{"2"};
    auto photo = savePhoto(validUserId, NO_CLIENT_RIDE_ID);
    auto rq = http::MockRequest(
        http::GET,
        http::URL{"http://localhost/v1/rides/my/photo_image"}.addParam(
            "photo_id", std::to_string(photo.id())));
    rq.headers.emplace(yacare::tests::USER_ID_HEADER, invalidUserId);
    auto resp = yacare::performTestRequest(rq);
    EXPECT_EQ(resp.status, 404);
}

TEST_F(Fixture, test_photo_image_invalid_size_name)
{
    auto userId = std::string{"1"};
    auto photo = savePhoto(userId, NO_CLIENT_RIDE_ID);
    auto rq =
        http::MockRequest(http::GET,
                          http::URL{"http://localhost/v1/rides/my/photo_image"}
                              .addParam("photo_id", std::to_string(photo.id()))
                              .addParam("size_name", "bla-bla-bla"));
    rq.headers.emplace(yacare::tests::USER_ID_HEADER, userId);
    auto resp = yacare::performTestRequest(rq);
    EXPECT_EQ(resp.status, 400);
}

TEST_F(Fixture, test_update)
{
    for (const auto& clientRideId : CLIENT_RIDE_IDS) {
        clearGrinder();

        auto userId = std::string{"1"};
        auto photos = db::Features{};
        for (const auto& time : {
                 "2021-02-05 20:10:14.526199+03",
                 "2021-02-05 20:10:14.528977+03",
                 "2021-02-05 20:10:14.531433+03",
                 "2021-02-05 20:10:15.536198+03",
                 "2021-02-05 20:10:15.539265+03",
             }) {
            photos.push_back(makePhoto(
                userId,
                clientRideId,
                db::Dataset::Rides,
                chrono::parseSqlDateTime(time)));
        }
        auto ride = ride_inspector::makeRide(photos.begin(), photos.end());
        {
            auto txn = pool().masterWriteableTransaction();
            db::FeatureGateway{*txn}.insert(photos);
            db::RideGateway{*txn}.insertx(ride);
            txn->commit();
        }
        auto rideHypotheses = makeRideHypotheses();
        saveRideHypotheses(ride.rideId(), rideHypotheses);
        auto& photoDeleteBefore = photos[1];
        auto& photoDeleteAfter = photos[3];
        auto update =
            yandex::maps::proto::mrcphoto::ugc::ride::UpdateRideRequest{};
        update.set_show_authorship(true);
        update.set_delete_photos_after_photo_id(
            TString(std::to_string(photoDeleteAfter.id())));
        update.set_delete_photos_before_photo_id(
            TString(std::to_string(photoDeleteBefore.id())));
        EXPECT_TRUE(update.IsInitialized());
        auto buf = TString{};
        EXPECT_TRUE(update.SerializeToString(&buf));
        auto rq = http::MockRequest(
            http::PUT,
            http::URL{"http://localhost/v1/rides/my/update"}.addParam(
                "ride_id", std::to_string(ride.rideId())));
        rq.headers.emplace(yacare::tests::USER_ID_HEADER, userId);
        rq.body.assign(buf.data(), buf.size());
        auto resp = yacare::performTestRequest(rq);
        EXPECT_EQ(resp.status, 200);
        auto updatedRide =
            db::RideGateway{*pool().masterReadOnlyTransaction()}.loadOne(
                db::table::Ride::rideId == ride.rideId());
        auto msg = yandex::maps::proto::mrcphoto::ugc::ride::MyRide{};
        EXPECT_TRUE(msg.ParseFromString(TString{resp.body}));
        EXPECT_TRUE(checkRide(msg, updatedRide));
        EXPECT_EQ(db::getRideHypotheses(msg), rideHypotheses);
        EXPECT_EQ(updatedRide.showAuthorship(), update.show_authorship());
        EXPECT_EQ(updatedRide.startTime(), getTimestamp(photoDeleteBefore));
        EXPECT_EQ(updatedRide.endTime(), getTimestamp(photoDeleteAfter));
        EXPECT_TRUE(
            db::DeletedIntervalGateway{*pool().masterReadOnlyTransaction()}
                .exists(makeFilter<db::table::DeletedInterval>(
                            ride.userId(), ride.sourceId(), ride.clientId()) &&
                        db::table::DeletedInterval::startedAt ==
                            ride.startTime() &&
                        db::table::DeletedInterval::endedAt <
                            getTimestamp(photoDeleteBefore)));
        EXPECT_TRUE(
            db::DeletedIntervalGateway{*pool().masterReadOnlyTransaction()}
                .exists(makeFilter<db::table::DeletedInterval>(
                            ride.userId(), ride.sourceId(), ride.clientId()) &&
                        db::table::DeletedInterval::startedAt >
                            getTimestamp(photoDeleteAfter) &&
                        db::table::DeletedInterval::endedAt == ride.endTime()));

        auto taskIds = grinderClient().listTaskIds();
        EXPECT_EQ(taskIds.size(), 1u);
        auto taskArgs = grinderClient().getTaskArgs(taskIds.front());
        EXPECT_TRUE(checkRide(taskArgs, ride));
        EXPECT_EQ(taskArgs[ride_correction::SHOW_AUTHORSHIP_FIELD].as<bool>(),
                  update.show_authorship());
    }
}

TEST_F(Fixture, test_update_delete_middle_photos)
{
    for (const auto& clientRideId : CLIENT_RIDE_IDS) {
        clearGrinder();

        auto userId = std::string{"1"};
        auto photos = db::Features{};
        for (const auto& time : {
                 "2021-02-05 20:10:14.526199+03",
                 "2021-02-05 20:10:14.528977+03",
                 "2021-02-05 20:10:14.531433+03",
                 "2021-02-05 20:10:15.536198+03",
                 "2021-02-05 20:10:15.539265+03",
             }) {
            photos.push_back(makePhoto(
                userId,
                clientRideId,
                db::Dataset::Rides,
                chrono::parseSqlDateTime(time)));
        }
        auto ride = ride_inspector::makeRide(photos.begin(), photos.end());
        {
            auto txn = pool().masterWriteableTransaction();
            db::FeatureGateway{*txn}.insert(photos);
            db::RideGateway{*txn}.insertx(ride);
            txn->commit();
        }
        auto& photoDeleteAfter = photos[0];
        auto& photoDeleteBefore = photos[4];
        auto update =
            yandex::maps::proto::mrcphoto::ugc::ride::UpdateRideRequest{};
        update.set_show_authorship(true);
        update.set_delete_photos_after_photo_id(
            TString(std::to_string(photoDeleteAfter.id())));
        update.set_delete_photos_before_photo_id(
            TString(std::to_string(photoDeleteBefore.id())));
        EXPECT_TRUE(update.IsInitialized());
        auto buf = TString{};
        EXPECT_TRUE(update.SerializeToString(&buf));
        auto rq = http::MockRequest(
            http::PUT,
            http::URL{"http://localhost/v1/rides/my/update"}.addParam(
                "ride_id", std::to_string(ride.rideId())));
        rq.headers.emplace(yacare::tests::USER_ID_HEADER, userId);
        rq.body.assign(buf.data(), buf.size());
        auto resp = yacare::performTestRequest(rq);
        EXPECT_EQ(resp.status, 200);
        auto updatedRide =
            db::RideGateway{*pool().masterReadOnlyTransaction()}.loadOne(
                db::table::Ride::rideId == ride.rideId());
        auto msg = yandex::maps::proto::mrcphoto::ugc::ride::MyRide{};
        EXPECT_TRUE(msg.ParseFromString(TString{resp.body}));
        EXPECT_TRUE(checkRide(msg, updatedRide));
        EXPECT_EQ(updatedRide.showAuthorship(), update.show_authorship());
        EXPECT_EQ(updatedRide.startTime(), getTimestamp(photos[0]));
        EXPECT_EQ(updatedRide.endTime(), getTimestamp(photos[4]));
        EXPECT_EQ(updatedRide.photos(), 2u);
        EXPECT_EQ(db::DeletedIntervalGateway{*pool().masterReadOnlyTransaction()}
            .count(makeFilter<db::table::DeletedInterval>(
                ride.userId(), ride.sourceId(), ride.clientId())), 1u);
        EXPECT_TRUE(
            db::DeletedIntervalGateway{*pool().masterReadOnlyTransaction()}
                .exists(makeFilter<db::table::DeletedInterval>(
                            ride.userId(), ride.sourceId(), ride.clientId()) &&
                        db::table::DeletedInterval::startedAt >
                            getTimestamp(photoDeleteAfter) &&
                        db::table::DeletedInterval::endedAt <
                            getTimestamp(photoDeleteBefore)));

        auto taskIds = grinderClient().listTaskIds();
        EXPECT_EQ(taskIds.size(), 1u);
        auto taskArgs = grinderClient().getTaskArgs(taskIds.front());
        EXPECT_TRUE(checkRide(taskArgs, ride));
        EXPECT_EQ(taskArgs[ride_correction::SHOW_AUTHORSHIP_FIELD].as<bool>(),
                  update.show_authorship());
    }
}

TEST_F(Fixture, test_update_invalid_ride_id)
{
    auto userId = std::string{"1"};
    auto ride = saveRide(userId, NO_CLIENT_RIDE_ID);
    auto update = yandex::maps::proto::mrcphoto::ugc::ride::UpdateRideRequest{};
    EXPECT_TRUE(update.IsInitialized());
    auto buf = TString{};
    EXPECT_TRUE(update.SerializeToString(&buf));
    auto rq = http::MockRequest(
        http::PUT,
        http::URL{"http://localhost/v1/rides/my/update"}.addParam("ride_id",
                                                                  "0"));
    rq.headers.emplace(yacare::tests::USER_ID_HEADER, userId);
    rq.body.assign(buf.data(), buf.size());
    auto resp = yacare::performTestRequest(rq);
    EXPECT_EQ(resp.status, 404);
}

TEST_F(Fixture, test_update_invalid_user_id)
{
    auto validUserId = std::string{"1"};
    auto invalidUserId = std::string{"2"};
    auto ride = saveRide(validUserId, NO_CLIENT_RIDE_ID);
    auto update = yandex::maps::proto::mrcphoto::ugc::ride::UpdateRideRequest{};
    EXPECT_TRUE(update.IsInitialized());
    auto buf = TString{};
    auto rq = http::MockRequest(
        http::PUT,
        http::URL{"http://localhost/v1/rides/my/update"}.addParam(
            "ride_id", std::to_string(ride.rideId())));
    rq.headers.emplace(yacare::tests::USER_ID_HEADER, invalidUserId);
    rq.body.assign(buf.data(), buf.size());
    auto resp = yacare::performTestRequest(rq);
    EXPECT_EQ(resp.status, 404);
}

TEST_F(Fixture, test_update_invalid_update_ride_request)
{
    auto userId = std::string{"1"};
    auto ride = saveRide(userId, NO_CLIENT_RIDE_ID);
    auto rq = http::MockRequest(
        http::PUT,
        http::URL{"http://localhost/v1/rides/my/update"}.addParam(
            "ride_id", std::to_string(ride.rideId())));
    rq.headers.emplace(yacare::tests::USER_ID_HEADER, userId);
    rq.body = "bla-bla-bla";
    auto resp = yacare::performTestRequest(rq);
    EXPECT_EQ(resp.status, 400);
}

TEST_F(Fixture, test_ride_delete)
{
    for (const auto& clientRideId : CLIENT_RIDE_IDS) {
        clearGrinder();

        auto userId = std::string{"1"};
        auto ride = saveRide(userId, clientRideId);
        auto rq = http::MockRequest(
            http::DELETE,
            http::URL{"http://localhost/v1/rides/my/delete"}.addParam(
                "ride_id", std::to_string(ride.rideId())));
        rq.headers.emplace(yacare::tests::USER_ID_HEADER, userId);

        bool isContributionModified = false;
        auto ugcMock =
            http::addMock(Configuration::instance()->contributionsModifyUrl(),
                          [&isContributionModified](const http::MockRequest&) {
                              isContributionModified = true;
                              return http::MockResponse::withStatus(200);
                          });

        auto resp = yacare::performTestRequest(rq);
        EXPECT_EQ(resp.status, 200);
        EXPECT_TRUE(isContributionModified);
        if (clientRideId) {
            EXPECT_TRUE(
                db::DeletedIntervalGateway{*pool().masterReadOnlyTransaction()}
                    .exists(
                        makeFilter<db::table::DeletedInterval>(
                            ride.userId(), ride.sourceId(), ride.clientId()) &&
                        db::table::DeletedInterval::startedAt <
                            ride.startTime() &&
                        db::table::DeletedInterval::endedAt > ride.endTime()));
        }
        else {
            EXPECT_TRUE(
                db::DeletedIntervalGateway{*pool().masterReadOnlyTransaction()}
                    .exists(
                        makeFilter<db::table::DeletedInterval>(
                            ride.userId(), ride.sourceId(), ride.clientId()) &&
                        db::table::DeletedInterval::startedAt ==
                            ride.startTime() &&
                        db::table::DeletedInterval::endedAt == ride.endTime()));
        }
        EXPECT_TRUE(db::RideGateway{*pool().masterReadOnlyTransaction()}
                        .loadOne(db::table::Ride::rideId == ride.rideId())
                        .isDeleted());

        auto taskIds = grinderClient().listTaskIds();
        EXPECT_EQ(taskIds.size(), 1u);
        auto taskArgs = grinderClient().getTaskArgs(taskIds.front());
        EXPECT_TRUE(checkRide(taskArgs, ride));
        EXPECT_FALSE(taskArgs.hasField(ride_correction::SHOW_AUTHORSHIP_FIELD));
    }
}

TEST_F(Fixture, test_chunk)
{
    for (const auto& clientRideId : CLIENT_RIDE_IDS) {
        auto userId = std::string{"1"};
        auto photos = db::Features{};
        for (const auto& time : {
                 "2021-02-05 20:10:14.526199+03",
                 "2021-02-05 20:10:14.528977+03",
                 "2021-02-05 20:10:14.531433+03",
                 "2021-02-05 20:10:15.536198+03",
                 "2021-02-05 20:10:15.539265+03",
                 "2021-02-05 20:10:15.541245+03",
                 "2021-02-05 20:10:15.543338+03",
                 "2021-02-05 20:10:17.548713+03",
                 "2021-02-05 20:10:17.552092+03",
                 "2021-02-05 20:10:17.554437+03",
             }) {
            photos.push_back(makePhoto(
                userId,
                clientRideId,
                db::Dataset::Rides,
                chrono::parseSqlDateTime(time)));
        }
        auto ride = ride_inspector::makeRide(photos.begin(), photos.end());
        {
            auto txn = pool().masterWriteableTransaction();
            db::FeatureGateway{*txn}.insert(photos);
            db::RideGateway{*txn}.insertx(ride);
            txn->commit();
        }

        auto onExit = concurrent::ScopedGuard{
            [prev =
                 Configuration::instance()->swapChunkSize(photos.size() / 3)] {
                Configuration::instance()->swapChunkSize(prev);
            }};

        auto rqGet = http::MockRequest(
            http::GET,
            http::URL{"http://localhost/v1/rides/my/get"}.addParam(
                "ride_id", std::to_string(ride.rideId())));
        rqGet.headers.emplace(yacare::tests::USER_ID_HEADER, userId);
        auto respGet = yacare::performTestRequest(rqGet);
        EXPECT_EQ(respGet.status, 200);
        auto msgGet = yandex::maps::proto::mrcphoto::ugc::ride::MyRide{};
        EXPECT_TRUE(msgGet.ParseFromString(TString{respGet.body}));
        EXPECT_TRUE(msgGet.has_track());
        auto polyline = geolib3::proto::decode(msgGet.track());
        EXPECT_TRUE(msgGet.has_track_preview());
        const auto& trackPreview = msgGet.track_preview();

        // preview
        EXPECT_TRUE(trackPreview.has_preview());
        EXPECT_FALSE(trackPreview.preview().items().empty());
        EXPECT_TRUE(checkPhoto(
            *trackPreview.preview().items().begin(), photos.front(), polyline));
        EXPECT_TRUE(checkPhoto(
            *trackPreview.preview().items().rbegin(), photos.back(), polyline));

        // chunk_list
        EXPECT_TRUE(trackPreview.has_chunk_list());
        EXPECT_FALSE(trackPreview.chunk_list().chunks().empty());
        auto photoIndex = size_t{0};
        for (const auto& chunk : trackPreview.chunk_list().chunks()) {
            auto buf = TString{};
            EXPECT_TRUE(chunk.chunk_descriptor().SerializeToString(&buf));
            auto rqChunk = http::MockRequest(
                http::POST, http::URL{"http://localhost/v1/rides/my/chunk"});
            rqChunk.headers.emplace(yacare::tests::USER_ID_HEADER, userId);
            rqChunk.body.assign(buf.data(), buf.size());
            auto respChunk = yacare::performTestRequest(rqChunk);
            EXPECT_EQ(respChunk.status, 200);

            auto msgChunk =
                yandex::maps::proto::mrcphoto::ugc::ride::TrackChunk{};
            EXPECT_TRUE(msgChunk.ParseFromString(TString{respChunk.body}));
            const auto& items = msgChunk.photos().items();
            EXPECT_FALSE(items.empty());
            ASSERT_TRUE(chunk.has_photos_count());
            EXPECT_EQ((std::uint32_t)items.size(), chunk.photos_count());
            for (const auto& item : items) {
                EXPECT_TRUE(checkPhoto(item, photos[photoIndex++], polyline));
            }
        }
        EXPECT_EQ(photoIndex, photos.size());
    }
}

TEST_F(Fixture, test_chunk_accuracy)
{
    struct {
        std::string time;
        Point2 point;
        PolylinePosition expect;
    } data[] = {
        {"2021-03-09 17:32:39.457+03",
         Point2{37.3734823957393, 55.8293095211408},
         PolylinePosition{0, 0.}},
        {"2021-03-09 17:32:40.216+03",
         Point2{37.3736273795424, 55.8293456851242},
         PolylinePosition{0, 1.}},
        {"2021-03-09 17:32:41.055+03",
         Point2{37.3738061868096, 55.8293901656413},
         PolylinePosition{1, 1.}},
        {"2021-03-09 17:32:41.699+03",
         Point2{37.3739439163083, 55.8294236572407},
         PolylinePosition{2, 1.}},
        {"2021-03-09 17:32:42.45+03 ",
         Point2{37.374118727772, 55.8294661660393},
         PolylinePosition{3, 1.}},
        {"2021-03-09 17:32:43.166+03",
         Point2{37.3742944473734, 55.8295088956691},
         PolylinePosition{4, 1.}},
    };

    auto userId = std::string{"1"};
    auto photos = db::Features{};
    for (auto [time, point, expect] : data) {
        photos.push_back(sql_chemistry::GatewayAccess<db::Feature>::construct()
                             .setDataset(db::Dataset::Rides)
                             .setSourceId(SOURCE_ID)
                             .setTimestamp(chrono::parseSqlDateTime(time))
                             .setGeodeticPos(point)
                             .setUserId(userId));
    }
    auto ride = ride_inspector::makeRide(photos.begin(), photos.end());
    {
        auto txn = pool().masterWriteableTransaction();
        db::FeatureGateway{*txn}.insert(photos);
        db::RideGateway{*txn}.insertx(ride);
        txn->commit();
    }

    auto onExit = concurrent::ScopedGuard{
        [prev = Configuration::instance()->swapChunkSize(photos.size())] {
            Configuration::instance()->swapChunkSize(prev);
        }};

    auto rqGet = http::MockRequest(
        http::GET,
        http::URL{"http://localhost/v1/rides/my/get"}.addParam(
            "ride_id", std::to_string(ride.rideId())));
    rqGet.headers.emplace(yacare::tests::USER_ID_HEADER, userId);
    auto respGet = yacare::performTestRequest(rqGet);
    EXPECT_EQ(respGet.status, 200);
    auto msgGet = yandex::maps::proto::mrcphoto::ugc::ride::MyRide{};
    EXPECT_TRUE(msgGet.ParseFromString(TString{respGet.body}));
    EXPECT_TRUE(msgGet.has_track_preview());
    const auto& trackPreview = msgGet.track_preview();
    EXPECT_TRUE(trackPreview.has_chunk_list());
    EXPECT_EQ(trackPreview.chunk_list().chunks().size(), 1);
    const auto& chunk = trackPreview.chunk_list().chunks().at(0);
    auto buf = TString{};
    EXPECT_TRUE(chunk.chunk_descriptor().SerializeToString(&buf));
    auto rqChunk = http::MockRequest(
        http::POST, http::URL{"http://localhost/v1/rides/my/chunk"});
    rqChunk.headers.emplace(yacare::tests::USER_ID_HEADER, userId);
    rqChunk.body.assign(buf.data(), buf.size());
    auto respChunk = yacare::performTestRequest(rqChunk);
    EXPECT_EQ(respChunk.status, 200);
    auto msgChunk = yandex::maps::proto::mrcphoto::ugc::ride::TrackChunk{};
    EXPECT_TRUE(msgChunk.ParseFromString(TString{respChunk.body}));
    const auto& items = msgChunk.photos().items();
    EXPECT_EQ((size_t)items.size(), std::size(data));
    for (size_t i = 0; i < std::size(data); ++i) {
        const auto& pos = items.at(i).position();
        const auto& expect = data[i].expect;
        EXPECT_EQ(pos.segment_index(), expect.segmentIndex);
        EXPECT_EQ(pos.segment_position(), expect.segmentPosition);
    }
}

TEST_F(Fixture, test_chunk_invalid_chunk_descriptor)
{
    auto userId = std::string{"1"};
    auto rq = http::MockRequest(
        http::POST, http::URL{"http://localhost/v1/rides/my/chunk"});
    rq.headers.emplace(yacare::tests::USER_ID_HEADER, userId);
    rq.body = "bla-bla-bla";
    auto resp = yacare::performTestRequest(rq);
    EXPECT_EQ(resp.status, 400);
}

TEST(polyline_position, test_one_segment)
{
    auto subpolyline = Subpolyline{{0u, 0.1}, {0u, 0.9}};
    struct {
        double from;
        double to;
    } tests[] = {
        {0.000, 0.1},
        {0.125, 0.2},
        {0.250, 0.3},
        {0.375, 0.4},
        {0.500, 0.5},
        {0.625, 0.6},
        {0.750, 0.7},
        {0.875, 0.8},
        {1.000, 0.9},
    };
    for (auto [from, to] : tests) {
        auto pos = PolylinePosition{0u, from};
        pos.translateTo(subpolyline);
        EXPECT_EQ(pos.segmentIndex, 0u);
        EXPECT_DOUBLE_EQ(pos.segmentPosition, to);
    }
}

TEST(polyline_position, test_start_segment_is_empty)
{
    auto subpolyline = Subpolyline{{0u, 1.}, {1u, 0.5}};
    for (double segPos = 0.1; segPos <= 1.; segPos += 0.1) {
        auto pos = PolylinePosition{0u, segPos};
        pos.translateTo(subpolyline);
        EXPECT_EQ(pos.segmentIndex, 1u);
        EXPECT_DOUBLE_EQ(pos.segmentPosition, segPos * 0.5);
    }
}

TEST(polyline_position, test_subpolyline_is_empty)
{
    struct {
        PolylinePosition bounds;
        PolylinePosition result;
    } tests[] = {
        {{0u, 0.}, {0u, 0.}},
        {{0u, 1.}, {0u, 1.}},
        {{1u, 0.}, {0u, 1.}},
        {{1u, 1.}, {1u, 1.}},
    };
    for (auto [bounds, result] : tests) {
        auto subpolyline = Subpolyline{bounds, bounds};
        auto pos = PolylinePosition{0u, 0.};
        pos.translateTo(subpolyline);
        EXPECT_EQ(pos.segmentIndex, result.segmentIndex);
        EXPECT_DOUBLE_EQ(pos.segmentPosition, result.segmentPosition);
    }
}

TEST_F(Fixture, test_stat)
{
    auto txn = pool().masterWriteableTransaction();

    auto userId = std::string{"1"};
    auto time = TIME;
    auto photos = db::Features{
        makePhoto(userId, NO_CLIENT_RIDE_ID, db::Dataset::Rides, time)};
    auto ride = ride_inspector::makeRide(photos.begin(), photos.end());

    db::FeatureGateway{*txn}.insert(photos);
    db::RideGateway{*txn}.insertx(ride);

    time = TIME + std::chrono::hours(1);
    photos = db::Features{
        makePhoto(userId, CLIENT_RIDE_ID, db::Dataset::Rides, time),
        makePhoto(userId, CLIENT_RIDE_ID, db::Dataset::Rides, time + 1s)};
    ride = ride_inspector::makeRide(photos.begin(), photos.end());

    db::FeatureGateway{*txn}.insert(photos);
    db::RideGateway{*txn}.insertx(ride);

    txn->commit();

    auto rq = http::MockRequest(
        http::GET,
        http::URL{"http://localhost/v1/rides/my/stat"}.addParam("lang",
                                                                "en_US"));
    rq.headers.emplace(yacare::tests::USER_ID_HEADER, userId);
    auto resp = yacare::performTestRequest(rq);
    EXPECT_EQ(resp.status, 200);
    auto msg = yandex::maps::proto::mrcphoto::ugc::ride::MyStat{};
    EXPECT_TRUE(msg.ParseFromString(TString{resp.body}));
    EXPECT_EQ(msg.rides_count(), 2u);
    EXPECT_EQ(msg.total_photos_count(), 3u);
    EXPECT_EQ(msg.total_published_photos_count(), 3u);
    EXPECT_EQ(msg.total_duration().text(), "1 min");
    EXPECT_EQ(msg.total_distance().text(), "19.7 ft");
}

TEST_F(Fixture, test_walk_object_delete)
{
    auto userId = std::string{"1"};
    auto walkObject = saveWalkObjectWithPhoto(userId);
    auto taskId = walkObject.feedbackTaskId();

    auto rq = http::MockRequest(
        http::DELETE,
        http::URL{"http://localhost/v1/walk_objects/my/delete"}.addParam(
            "id", std::to_string(walkObject.id())));
    rq.headers.emplace(yacare::tests::USER_ID_HEADER, userId);

    bool isContributionModified = false;

    auto cfg = Configuration::instance();
    cfg->setFeedbackClient(std::make_unique<MockFeedbackClient>(
        std::vector<fc::Task>{MockFeedbackClient::makeMockTask(
            taskId, fc::TaskState::Opened, std::nullopt)}
    ));

    auto ugcMock =
        http::addMock(
            Configuration::instance()->contributionsModifyUrl(),
            [&](const http::MockRequest&) {
                isContributionModified = true;
                return http::MockResponse::withStatus(200);
            });

    auto resp = yacare::performTestRequest(rq);
    EXPECT_EQ(resp.status, 200);
    EXPECT_TRUE(isContributionModified);
    auto task = cfg->feedbackClient().getById(taskId);
    EXPECT_EQ(task.state, fc::TaskState::Rejected);
    EXPECT_EQ(task.resolution, fc::TaskResolution::Rejected);

    auto photos = db::FeatureGateway{*pool().masterReadOnlyTransaction()}
        .load(db::table::Feature::walkObjectId == walkObject.id());
    for (const auto& photo: photos) {
        EXPECT_TRUE(photo.deletedByUser());
    }
}

TEST_F(Fixture, test_walk_object_delete_task_accepted)
{
    auto userId = std::string{"1"};
    auto walkObject = saveWalkObjectWithPhoto(userId);
    auto taskId = walkObject.feedbackTaskId();

    auto rq = http::MockRequest(
        http::DELETE,
        http::URL{"http://localhost/v1/walk_objects/my/delete"}.addParam(
            "id", std::to_string(walkObject.id())));
    rq.headers.emplace(yacare::tests::USER_ID_HEADER, userId);

    bool isContributionModified = false;

    auto cfg = Configuration::instance();
    cfg->setFeedbackClient(std::make_unique<MockFeedbackClient>(
        std::vector<fc::Task>{MockFeedbackClient::makeMockTask(
            taskId, fc::TaskState::Accepted, fc::TaskResolution::Accepted)}
    ));

    auto ugcMock =
        http::addMock(
            Configuration::instance()->contributionsModifyUrl(),
            [&](const http::MockRequest&) {
                isContributionModified = true;
                return http::MockResponse::withStatus(200);
            });

    auto resp = yacare::performTestRequest(rq);
    EXPECT_EQ(resp.status, 200);
    EXPECT_TRUE(isContributionModified);
    // Feedback task was already Accepted so its state has not changed
    auto task = cfg->feedbackClient().getById(taskId);
    EXPECT_EQ(task.state, fc::TaskState::Accepted);
    EXPECT_EQ(task.resolution, fc::TaskResolution::Accepted);

    auto photos = db::FeatureGateway{*pool().masterReadOnlyTransaction()}
        .load(db::table::Feature::walkObjectId == walkObject.id());
    for (const auto& photo: photos) {
        EXPECT_TRUE(photo.deletedByUser());
    }
}

}  // namespace maps::mrc::ugc_back::tests
