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

#include <maps/infra/yacare/include/test_utils.h>
#include <maps/libs/chrono/include/time_point.h>
#include <maps/libs/common/include/file_utils.h>
#include <maps/libs/concurrent/include/scoped_guard.h>
#include <maps/libs/geolib/include/conversion.h>
#include <maps/libs/geolib/include/serialization.h>
#include <maps/libs/geolib/include/test_tools/comparison.h>
#include <maps/libs/http/include/http.h>
#include <maps/libs/http/include/test_utils.h>
#include <maps/libs/json/include/std/vector.h>
#include <maps/wikimap/mapspro/services/mrc/libs/db/include/ugc/gateway.h>
#include <maps/wikimap/mapspro/services/mrc/libs/db/include/ugc/tasks_group_gateway.h>
#include <maps/wikimap/mapspro/services/mrc/tasks-planner/lib/common.h>

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

using geolib3::Point2;
using geolib3::Polygon2;
using geolib3::MultiPolygon2;
using geolib3::PointsVector;

namespace {

constexpr double GEOM_EPS = 1e-9;
const std::string USER = "123";

struct TasksGroupParams {
    std::string name;
    std::variant<geolib3::MultiPolygon2, geolib3::Polygon2> geodeticGeom;
    bool useRouting = false;
    std::vector<int> fcs;
    bool useToll = false;
    uint32_t recommendedTaskLength{};
    std::optional<chrono::TimePoint> actualizedBefore;
    std::optional<std::vector<std::string>> cameraDirections;
    std::optional<float> minEdgeCoverageRatio;
    std::optional<bool> excludeDeadends;
    std::optional<bool> ignorePrivateArea;
    std::optional<std::vector<std::string>> allowedAssigneesLogins;
    std::optional<std::vector<std::string>> emails;
};

bool equal(const db::ugc::TasksGroupEmails& lhs,
           const std::vector<std::string>& rhs)
{
    if (lhs.size() != rhs.size()) {
        return false;
    }
    for (const auto& item : lhs) {
        if (std::find(rhs.begin(), rhs.end(), item.email()) == rhs.end()) {
            return false;
        }
    }
    return true;
}

void toJson(json::ObjectBuilder builder, const TasksGroupParams& tgp)
{
    builder["name"] = tgp.name;
    if (std::holds_alternative<geolib3::MultiPolygon2>(tgp.geodeticGeom)) {
        builder["geometry"] << geolib3::geojson(
                std::get<geolib3::MultiPolygon2>(tgp.geodeticGeom)
            );
    } else {
        builder["geometry"] << geolib3::geojson(
                std::get<geolib3::Polygon2>(tgp.geodeticGeom)
            );
    }
    builder["routing"] = tgp.useRouting;
    builder["fcs"] = tgp.fcs;
    builder["toll"] = false;
    builder["recommendedTaskLengthMeters"] = tgp.recommendedTaskLength;

    if (tgp.actualizedBefore.has_value()) {
        builder["actualizedBefore"] =
            chrono::formatIsoDateTime(tgp.actualizedBefore.value());
    }

    if (tgp.cameraDirections.has_value()) {
        builder["cameraDirections"] = tgp.cameraDirections.value();
    }

    if (tgp.minEdgeCoverageRatio.has_value()) {
        builder["minEdgeCoverageRatio"] = tgp.minEdgeCoverageRatio.value();
    }

    if (tgp.excludeDeadends.has_value()) {
        builder["excludeDeadends"] = tgp.excludeDeadends.value();
    }

    if (tgp.ignorePrivateArea.has_value()) {
        builder["ignorePrivateArea"] = tgp.ignorePrivateArea.value();
    }

    if (tgp.allowedAssigneesLogins.has_value()) {
        builder["allowedAssigneesLogins"] = tgp.allowedAssigneesLogins.value();
    }

    if (tgp.emails.has_value()) {
        builder["emails"] = tgp.emails.value();
    }
}

TasksGroupParams makeSampleTasksGroupParams() {
    return TasksGroupParams{
        .name = "Москва",
        .geodeticGeom = MultiPolygon2(
                {Polygon2{
                    PointsVector{{36.4, 55.6}, {36.5, 55.6}, {36.5, 55.7}, {36.4, 55.6}}
                    }
                }
            ),
        .fcs ={1, 2, 3, 4, 5, 6},
        .actualizedBefore = chrono::parseIsoDateTime("2018-01-01T00:00:00")
    };
}

void checkObjectSatisfiesParams(const TasksGroupKit& tasksGroupKit,
                                const TasksGroupParams& tgp)
{
    const auto& [tg, emails] = tasksGroupKit;
    EXPECT_EQ(tg.name(), tgp.name);
    EXPECT_EQ(tg.fcs(), tgp.fcs);
    if (std::holds_alternative<geolib3::MultiPolygon2>(tgp.geodeticGeom)) {
        EXPECT_TRUE(
            geolib3::test_tools::approximateEqual(
                geolib3::convertMercatorToGeodetic(tg.mercatorGeom()),
                std::get<geolib3::MultiPolygon2>(tgp.geodeticGeom),
                GEOM_EPS));
    } else {
        EXPECT_TRUE(
            geolib3::test_tools::approximateEqual(
                geolib3::convertMercatorToGeodetic(tg.mercatorGeom()),
                geolib3::MultiPolygon2({std::get<geolib3::Polygon2>(tgp.geodeticGeom)}),
                GEOM_EPS));
    }

    EXPECT_EQ(static_cast<bool>(tg.useRouting()), tgp.useRouting);
    EXPECT_EQ(static_cast<bool>(tg.useToll()), tgp.useToll);
    EXPECT_EQ(tg.recommendedTaskLengthMeters(), tgp.recommendedTaskLength);
    EXPECT_EQ(tg.actualizedBefore(), tgp.actualizedBefore);
    if (tgp.cameraDirections.has_value()) {
        EXPECT_EQ(tg.cameraDeviations().size(), tgp.cameraDirections->size());
    }

    EXPECT_EQ(tg.minEdgeCoverageRatio(), tgp.minEdgeCoverageRatio);

    if (tgp.excludeDeadends.has_value()) {
        EXPECT_EQ(tg.excludeDeadends(), tgp.excludeDeadends.value());
    }

    if (tgp.ignorePrivateArea.has_value()) {
        EXPECT_EQ(tg.ignorePrivateArea(), tgp.ignorePrivateArea.value());
    }

    EXPECT_EQ(tg.allowedAssigneesLogins(), tgp.allowedAssigneesLogins);

    if (tgp.emails.has_value()) {
        EXPECT_TRUE(equal(emails, tgp.emails.value()));
    }
}


db::ugc::TasksGroup createExampleTasksGroup1(maps::pgpool3::TransactionHandle&& txn)
{
    db::ugc::TasksGroup a{
        db::ugc::TasksGroupStatus::Draft,
        "Москва 2018.02",
        geolib3::convertGeodeticToMercator(
            MultiPolygon2{
                {Polygon2{PointsVector{{37.6, 55.6},
                                        {37.7, 55.6},
                                        {37.7, 55.7},
                                        {37.6, 55.6}
                            }}
                }
            }
        ),
        db::ugc::UseRouting::No,
        {1, 2, 3, 4, 5, 6, 7},
        db::ugc::UseToll::No,
        1000
    };
    a.setCreatedBy(USER);

    db::ugc::TasksGroupGateway gtw(*txn);
    gtw.insert(a);
    txn->commit();
    return a;
}

db::ugc::TasksGroup createExampleTasksGroup2(maps::pgpool3::TransactionHandle&& txn)
{
    db::ugc::TasksGroup a{
        db::ugc::TasksGroupStatus::Draft,
        "NiNo 2018.03",
        geolib3::convertGeodeticToMercator(
            MultiPolygon2{
                {Polygon2{PointsVector{{43.83, 56.3},
                                        {43.93, 56.3},
                                        {43.93, 56.2},
                                        {43.83, 56.2}
                            }}
                }}
        ),
        db::ugc::UseRouting::Yes,
        {1, 2, 3, 4, 5, 6, 7},
        db::ugc::UseToll::No,
        2000
    };

    db::ugc::TasksGroupGateway gtw(*txn);
    gtw.insert(a);
    txn->commit();
    return a;
}

void checkCreateTasksGroup(Fixture& fixture,
                           const TasksGroupParams& tasksGroupParams,
                           const std::optional<std::string>& uid = std::nullopt)
{
    json::Builder builder;
    builder << [&](json::ObjectBuilder builder) {
        toJson(builder, tasksGroupParams);
    };

    http::URL url("http://localhost/tasks_groups/");
    url.addParam("lang", "ru_RU");

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

    http::MockRequest request(http::POST, url);

    request.body = builder.str();

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

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

    {
        auto txn = fixture.txnHandle();
        auto tasksGroupIds = db::ugc::TasksGroupGateway{*txn}.loadIds();
        EXPECT_EQ(tasksGroupIds.size(), 1u);
        auto tasksGroupKit = loadTasksGroupKit(*txn, tasksGroupIds.front());
        checkObjectSatisfiesParams(tasksGroupKit, tasksGroupParams);
        if (uid.has_value()) {
            EXPECT_EQ(tasksGroupKit.obj.createdBy(), uid.value());
        }
    }
}

} // namespace

TEST(tasks_group_api_should, test_create_tasks_group)
{
    Fixture fixture;
    auto tasksGroupParams = makeSampleTasksGroupParams();
    checkCreateTasksGroup(fixture, tasksGroupParams);
}

TEST(tasks_group_api_should, test_create_tasks_group_with_edge_coverage_ratio_and_exclude_deadends)
{
    Fixture fixture;
    auto tasksGroupParams = makeSampleTasksGroupParams();
    tasksGroupParams.minEdgeCoverageRatio = 0.8;
    tasksGroupParams.excludeDeadends = true;
    checkCreateTasksGroup(fixture, tasksGroupParams);
}


TEST(tasks_group_api_should, test_create_tasks_group_with_ignore_private_area)
{
    Fixture fixture;
    auto tasksGroupParams = makeSampleTasksGroupParams();
    tasksGroupParams.ignorePrivateArea = true;
    checkCreateTasksGroup(fixture, tasksGroupParams);
}


TEST(tasks_group_api_should, test_create_tasks_group_with_allowed_assignees_logins)
{
    Fixture fixture;
    auto tasksGroupParams = makeSampleTasksGroupParams();
    std::vector<std::string> testLogins = {"testLogin1", "testLogin2", "testLogin3"};
    tasksGroupParams.allowedAssigneesLogins = std::move(testLogins);
    checkCreateTasksGroup(fixture, tasksGroupParams);
}


TEST(tasks_group_api_should, test_create_tasks_group_authorized)
{
    Fixture fixture;
    auto tasksGroupParams = makeSampleTasksGroupParams();
    checkCreateTasksGroup(fixture, tasksGroupParams, "123");
}


TEST(tasks_group_api_should, test_create_tasks_group_with_multiple_camera_directions)
{
    Fixture fixture;
    auto tasksGroupParams = makeSampleTasksGroupParams();
    tasksGroupParams.cameraDirections = {"Front", "Right"};

    checkCreateTasksGroup(fixture, tasksGroupParams);
}

TEST(tasks_group_api_should, test_create_tasks_group_from_polygon)
{
    Fixture fixture;
    auto tasksGroupParams = makeSampleTasksGroupParams();
    tasksGroupParams.geodeticGeom = Polygon2(
            PointsVector{{36.4, 55.6}, {36.5, 55.6}, {36.5, 55.7}, {36.4, 55.6}}
        );

    checkCreateTasksGroup(fixture, tasksGroupParams);
}

TEST(tasks_group_api_should, test_create_tasks_group_with_emails)
{
    Fixture fixture;
    auto tasksGroupParams = makeSampleTasksGroupParams();
    tasksGroupParams.emails = {"a1@ya.ru", "a2@ya.ru"};
    checkCreateTasksGroup(fixture, tasksGroupParams);
}

TEST(tasks_group_api_should, test_create_tasks_group_from_featurecollection)
{
    Fixture fixture;

    Polygon2 area(
        PointsVector{{36.4, 55.6}, {36.5, 55.6}, {36.5, 55.7}, {36.4, 55.6}}

    );

    const std::string name = "Москва";
    chrono::TimePoint actualized = chrono::parseIsoDateTime("2018-01-01T00:00:00");
    std::vector<int> fcs{1, 2, 3, 4, 5, 6};

    json::Builder builder;
    builder << [&](json::ObjectBuilder builder) {
        builder["name"] = name;
        builder["geometry"] <<
            [&](json::ObjectBuilder bld) {
                bld["type"] = "FeatureCollection";
                bld["features"] =
                    [&](json::ArrayBuilder bld) {
                        bld << [&](json::ObjectBuilder bld) {
                            bld["type"] = "Feature";
                            bld["id"] = 0;
                            bld["geometry"] = geolib3::geojson(area);
                        };

                        // Test that non polygon geometry will be ignored
                        bld << [&](json::ObjectBuilder bld) {
                            bld["type"] = "Feature";
                            bld["id"] = 0;
                            bld["geometry"] = geolib3::geojson(Point2(36, 55));
                        };
                    };
            };
        builder["routing"] = true;
        builder["fcs"] = fcs;
        builder["actualizedBefore"] = chrono::formatIsoDateTime(actualized);
        builder["toll"] = false;
        builder["recommendedTaskLengthMeters"] = 8000;
    };

    http::MockRequest request(
        http::POST,
        http::URL("http://localhost/tasks_groups/")
            .addParam("lang", "ru_RU")
    );

    request.body = builder.str();

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

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

    {
        auto txn = fixture.txnHandle();
        db::ugc::TasksGroupGateway gtw(*txn);
        auto stored = gtw.load();
        EXPECT_EQ(stored.size(), 1u);
        const auto& obj = stored.at(0);
        EXPECT_EQ(obj.status(), db::ugc::TasksGroupStatus::Draft);
        EXPECT_EQ(obj.name(), name);
        EXPECT_EQ(obj.fcs(), fcs);
        EXPECT_TRUE(
            geolib3::test_tools::approximateEqual(
                geolib3::convertMercatorToGeodetic(stored.at(0).mercatorGeom()),
                geolib3::MultiPolygon2({area}),
                GEOM_EPS));
        EXPECT_EQ(obj.useRouting(), db::ugc::UseRouting::Yes);
        EXPECT_EQ(obj.useToll(), db::ugc::UseToll::No);
        EXPECT_EQ(obj.recommendedTaskLengthMeters(), 8000u);
    }
}

TEST(tasks_group_api_should, test_create_tasks_group_from_intersected_polygons)
{
    Fixture fixture;
    const std::string TEST_GEOMS_DIR = SRC_("test_geoms");
    const std::vector<std::string> TEST_FILES{
        "Podolsk.geojson",
        "Magnitogorsk.geojson",
    };

    const std::string name = "Москва";
    chrono::TimePoint actualized = chrono::parseIsoDateTime("2018-01-01T00:00:00");
    std::vector<int> fcs{1, 2, 3, 4, 5, 6};

    for(const auto& filename : TEST_FILES) {
        SCOPED_TRACE(filename);
        json::Builder builder;
        builder << [&](json::ObjectBuilder builder) {
            builder["name"] = name;
            builder["geometry"] <<
                json::Verbatim(maps::common::readFileToString(TEST_GEOMS_DIR + "/" + filename));
            builder["routing"] = false;
            builder["fcs"] = fcs;
            builder["actualizedBefore"] = chrono::formatIsoDateTime(actualized);
            builder["toll"] = false;
            builder["recommendedTaskLengthMeters"] = 8000;
        };


        http::MockRequest request(
            http::POST,
            http::URL("http://localhost/tasks_groups/")
        );

        request.body = builder.str();
        auto response = yacare::performTestRequest(request);
        EXPECT_EQ(response.status, 201);
    }

    {
        auto txn = fixture.txnHandle();
        db::ugc::TasksGroupGateway gtw(*txn);
        auto stored = gtw.load();
        EXPECT_EQ(stored.size(), TEST_FILES.size());
    }

}

TEST(tasks_group_api_should, test_get_all_tasks_groups)
{
    Fixture fixture;
    auto a = createExampleTasksGroup1(fixture.txnHandle());
    auto b = createExampleTasksGroup2(fixture.txnHandle());

    http::MockRequest request(
        http::GET,
        http::URL("http://localhost/tasks_groups/")
            .addParam("lang", "ru_RU")
    );

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

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

    {
        auto txn = fixture.txnHandle();
        a.setStatus(db::ugc::TasksGroupStatus::Open);
        db::ugc::TasksGroupGateway(*txn).update(a);
        txn->commit();
    }

    {
        http::MockRequest request(
            http::GET,
            http::URL("http://localhost/tasks_groups/")
                .addParam("status", "Draft,Open")
        );

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

        auto responseJson = json::Value::fromString(response.body);
        EXPECT_EQ(responseJson.size(), 2u);
    }

    auto c = createExampleTasksGroup2(fixture.txnHandle());

    {
        http::MockRequest request(
            http::GET,
            http::URL("http://localhost/tasks_groups/")
                .addParam("status", "Draft")
        );

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

        auto responseJson = json::Value::fromString(response.body);
        EXPECT_EQ(responseJson.size(), 2u);
    }
}

TEST(tasks_group_api_should, test_get_tasks_group_by_id)
{
    Fixture fixture;
    auto a = createExampleTasksGroup1(fixture.txnHandle());

    http::MockRequest request(
        http::GET,
        http::URL("http://localhost/tasks_groups/" + std::to_string(a.id()) + "/")
            .addParam("lang", "ru_RU")
    );

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

    auto responseSchema = readResponseSchemasFromSwagger(schemasPath());
    auto body = response.body;
    validateJson(body,
                 responseSchema.at({"GET", "/tasks_groups/{id}/", 200}),
                 schemasDir());
    auto bodyJson = json::Value::fromString(body);
    EXPECT_THAT(bodyJson["params"]["cameraDirections"].as<std::vector<std::string>>(),
                testing::ContainerEq(std::vector<std::string>{"Front"}));
    ASSERT_TRUE(bodyJson["params"].hasField("excludeDeadends"));
    EXPECT_FALSE(bodyJson["params"]["excludeDeadends"].as<bool>());
    ASSERT_TRUE(bodyJson["params"].hasField("ignorePrivateArea"));
    EXPECT_FALSE(bodyJson["params"]["ignorePrivateArea"].as<bool>());
}


TEST(tasks_group_api_should, test_get_tasks_group_by_created_by)
{
    Fixture fixture;
    auto a = createExampleTasksGroup1(fixture.txnHandle());

    {
        http::MockRequest request(
            http::GET,
            http::URL("http://localhost/tasks_groups/")
                .addParam("created_by", USER)
        );

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

        auto responseJson = json::Value::fromString(response.body);
        EXPECT_EQ(responseJson.size(), 1u);
    }


    {
        http::MockRequest request(
            http::GET,
            http::URL("http://localhost/tasks_groups/")
                .addParam("created_by", "1")
        );

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

        auto responseJson = json::Value::fromString(response.body);
        EXPECT_EQ(responseJson.size(), 0u);
    }
}


TEST(tasks_group_api_should, test_get_tasks_group_by_name)
{
    Fixture fixture;
    auto a = createExampleTasksGroup1(fixture.txnHandle());

    {
        http::MockRequest request(
            http::GET,
            http::URL("http://localhost/tasks_groups/")
                .addParam("name", "москва")
        );

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

        auto responseJson = json::Value::fromString(response.body);
        EXPECT_EQ(responseJson.size(), 1u);
    }


    {
        http::MockRequest request(
            http::GET,
            http::URL("http://localhost/tasks_groups/")
                .addParam("name", "NY")
        );

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

        auto responseJson = json::Value::fromString(response.body);
        EXPECT_EQ(responseJson.size(), 0u);
    }
}


TEST(tasks_group_api_should, test_get_tasks_groups_with_stats)
{
    Fixture fixture;
    auto a = createExampleTasksGroup1(fixture.txnHandle());
    auto taskA = fixture.createTask(a.id());
    auto b = createExampleTasksGroup2(fixture.txnHandle());
    auto taskB = fixture.createTask(b.id());

    http::MockRequest request(
        http::GET,
        http::URL("http://localhost/tasks_groups/")
            .addParam("lang", "ru_RU")
            .addParam("with_stats", "true")
    );


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

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

    auto responseJson = json::Value::fromString(body);
    auto statsJson = responseJson[0]["tasksStatusesStats"][0];
    EXPECT_EQ(statsJson["status"].as<std::string>(), "Open");
    EXPECT_EQ(statsJson["count"].as<int>(), 1);
}


TEST(tasks_group_api_should, test_delete_tasks_group_by_id)
{
    Fixture fixture;
    auto a = createExampleTasksGroup1(fixture.txnHandle());
    auto b = createExampleTasksGroup2(fixture.txnHandle());

    {
        http::MockRequest request(
            http::DELETE,
            http::URL("http://localhost/tasks_groups/" + std::to_string(a.id()) + "/")
        );

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

    // check can't delete objects in not draft status
    {
        b.setStatus(db::ugc::TasksGroupStatus::Open);
        auto txn = fixture.txnHandle();
        db::ugc::TasksGroupGateway gtw(*txn);
        gtw.update(b);
        txn->commit();

        http::MockRequest request(
            http::DELETE,
            http::URL("http://localhost/tasks_groups/" + std::to_string(b.id()) + "/")
        );

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

TEST(tasks_group_api_should, test_patch_tasks_group_by_id)
{
    Fixture fixture;
    auto a = createExampleTasksGroup1(fixture.txnHandle());
    auto txn = fixture.txnHandle();
    db::ugc::TasksGroupGateway gtw(*txn);

    const auto responseSchema = readResponseSchemasFromSwagger(schemasPath());

    http::MockRequest request(
        http::PATCH,
        http::URL("http://localhost/tasks_groups/" + std::to_string(a.id()) + "/")
            .addParam("lang", "ru_RU")
    );

    MultiPolygon2 area(
        {
            Polygon2{
                PointsVector{{43.83, 56.3},{43.93, 56.3},{43.93, 56.2},{43.83, 56.2}}
            }
        }
    );

    const std::string name = "Podolsk";
    const chrono::TimePoint actualized = chrono::parseIsoDateTime("2018-04-01T00:00:00");
    const std::vector<int> fcs{1, 2, 3};
    const uint32_t newRecommendedTaskLength = 18000;
    const std::vector<std::string> allowedAssigneesLogins{"testLogin1", "testLogin2", "testLogin3"};
    const std::vector<std::string> emails{"a1@ya.ru", "a2@ya.ru"};

    json::Builder builder;
    builder << [&](json::ObjectBuilder builder) {
        builder["params"] << [&](json::ObjectBuilder builder) {
            builder["name"] = name;
            builder["geometry"] << geolib3::geojson(area);
            builder["routing"] = false;
            builder["fcs"] = fcs;
            builder["actualizedBefore"] = chrono::formatIsoDateTime(actualized);
            builder["toll"] = true;
            builder["recommendedTaskLengthMeters"] = newRecommendedTaskLength;
            builder["allowedAssigneesLogins"] = allowedAssigneesLogins;
            builder["emails"] = emails;
        };
    };

    request.body = builder.str();

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

    validateJson(response.body,
                responseSchema.at({"PATCH", "/tasks_groups/{id}/", 200}),
                schemasDir());

    gtw.reload(a);

    EXPECT_EQ(a.status(), db::ugc::TasksGroupStatus::Draft);
    EXPECT_EQ(a.name(), name);
    EXPECT_EQ(a.fcs(), fcs);
    EXPECT_TRUE(
        geolib3::test_tools::approximateEqual(
            geolib3::convertMercatorToGeodetic(a.mercatorGeom()),
            area,
            GEOM_EPS));
    EXPECT_EQ(a.useRouting(), db::ugc::UseRouting::No);
    EXPECT_EQ(a.useToll(), db::ugc::UseToll::Yes);
    EXPECT_EQ(a.recommendedTaskLengthMeters(), newRecommendedTaskLength);
    EXPECT_EQ(a.allowedAssigneesLogins(), allowedAssigneesLogins);

    auto tasksGroupEmails = db::ugc::TasksGroupEmailGateway{*txn}.load(
        db::ugc::table::TasksGroupEmail::tasksGroupId == a.id());
    EXPECT_TRUE(equal(tasksGroupEmails, emails));

    // Make tasks available to all users
    json::Builder builder2;
    builder2 << [&](json::ObjectBuilder builder) {
        builder["params"] << [&](json::ObjectBuilder builder) {
            builder["name"] = name + "_available_to_all";
        };
    };

    request.body = builder2.str();

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

    validateJson(response.body,
                responseSchema.at({"PATCH", "/tasks_groups/{id}/", 200}),
                schemasDir());

    gtw.reload(a);
    EXPECT_EQ(a.allowedAssigneesLogins(), std::nullopt);

    tasksGroupEmails = db::ugc::TasksGroupEmailGateway{*txn}.load(
        db::ugc::table::TasksGroupEmail::tasksGroupId == a.id());
    EXPECT_TRUE(equal(tasksGroupEmails, emails));
}


TEST(tasks_group_api_should, test_patch_tasks_group_is_idempotent)
{
    Fixture fixture;
    auto tasksGroup = createExampleTasksGroup1(fixture.txnHandle());

    const MultiPolygon2 area(
        {
            Polygon2{
                PointsVector{{43.83, 56.3},{43.93, 56.3},{43.93, 56.2},{43.83, 56.2}}
            }
        }
    );

    const std::string name = "Podolsk";
    const std::vector<int> fcs{1, 2, 3};
    const uint32_t recommendedTaskLength = 18000;
    const std::vector<std::string> allowedAssigneesLogins{"testLogin1", "testLogin2", "testLogin3"};

    {
        auto txn = fixture.txnHandle();
        db::ugc::TasksGroupGateway gtw(*txn);
        tasksGroup.setStatus(db::ugc::TasksGroupStatus::InProgress);
        tasksGroup.setName(name);
        tasksGroup.setFcs(fcs);
        tasksGroup.setMercatorGeom(geolib3::convertGeodeticToMercator(area));
        tasksGroup.setUseRouting(db::ugc::UseRouting::No);
        tasksGroup.setUseToll(db::ugc::UseToll::Yes);
        tasksGroup.setRecommendedTaskLengthMeters(recommendedTaskLength);
        tasksGroup.setAllowedAssigneesLogins(allowedAssigneesLogins);
        gtw.update(tasksGroup);
        txn->commit();
    }

    json::Builder builder;
    builder << [&](json::ObjectBuilder builder) {
        builder["params"] << [&](json::ObjectBuilder builder) {
            builder["status"] = "InProgress";
            builder["name"] = name;
            builder["geometry"] << geolib3::geojson(area);
            builder["routing"] = false;
            builder["fcs"] = fcs;
            builder["toll"] = true;
            builder["recommendedTaskLengthMeters"] = recommendedTaskLength;
            builder["allowedAssigneesLogins"] = allowedAssigneesLogins;
        };
    };

    http::MockRequest request(
        http::PATCH,
        http::URL("http://localhost/tasks_groups/" + std::to_string(tasksGroup.id()) + "/")
            .addParam("lang", "ru_RU")
    );

    request.body = builder.str();

    auto checkTaskGroupAttributes =
        [&]() {
            db::ugc::TasksGroupGateway(*fixture.txnHandle()).reload(tasksGroup);
            EXPECT_EQ(tasksGroup.status(), db::ugc::TasksGroupStatus::InProgress);
            EXPECT_EQ(tasksGroup.name(), name);
            EXPECT_EQ(tasksGroup.fcs(), fcs);
            EXPECT_TRUE(
                geolib3::test_tools::approximateEqual(
                    geolib3::convertMercatorToGeodetic(tasksGroup.mercatorGeom()),
                    area,
                    GEOM_EPS));
            EXPECT_EQ(tasksGroup.useRouting(), db::ugc::UseRouting::No);
            EXPECT_EQ(tasksGroup.useToll(), db::ugc::UseToll::Yes);
            EXPECT_EQ(tasksGroup.recommendedTaskLengthMeters(), recommendedTaskLength);
            EXPECT_EQ(tasksGroup.allowedAssigneesLogins(), allowedAssigneesLogins);
        };

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

    checkTaskGroupAttributes();

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


TEST(tasks_group_api_should, test_patch_tasks_groups_refuses_in_progress_edits)
{
    Fixture fixture;
    auto tasksGroup = createExampleTasksGroup1(fixture.txnHandle());

    const MultiPolygon2 area(
        {
            Polygon2{
                PointsVector{{43.83, 56.3},{43.93, 56.3},{43.93, 56.2},{43.83, 56.2}}
            }
        }
    );

    const std::string name = "Podolsk";
    const std::vector<int> fcs{1, 2, 3};
    const uint32_t recommendedTaskLength = 18000;
    const std::vector<std::string> allowedAssigneesLogins{"testLogin1", "testLogin2", "testLogin3"};

    {
        auto txn = fixture.txnHandle();
        db::ugc::TasksGroupGateway gtw(*txn);
        tasksGroup.setStatus(db::ugc::TasksGroupStatus::InProgress);
        tasksGroup.setName(name);
        tasksGroup.setFcs(fcs);
        tasksGroup.setMercatorGeom(geolib3::convertGeodeticToMercator(area));
        tasksGroup.setUseRouting(db::ugc::UseRouting::No);
        tasksGroup.setUseToll(db::ugc::UseToll::Yes);
        tasksGroup.setRecommendedTaskLengthMeters(recommendedTaskLength);
        tasksGroup.setAllowedAssigneesLogins(allowedAssigneesLogins);
        gtw.update(tasksGroup);
        txn->commit();
    }

    json::Builder builder;
    builder << [&](json::ObjectBuilder builder) {
        builder["params"] << [&](json::ObjectBuilder builder) {
            builder["status"] = "InProgress";
            builder["name"] = name + "1";
        };
    };

    http::MockRequest request(
        http::PATCH,
        http::URL("http://localhost/tasks_groups/" + std::to_string(tasksGroup.id()) + "/")
            .addParam("lang", "ru_RU")
    );

    request.body = builder.str();

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

    db::ugc::TasksGroupGateway(*fixture.txnHandle()).reload(tasksGroup);

    EXPECT_EQ(tasksGroup.status(), db::ugc::TasksGroupStatus::InProgress);
    EXPECT_EQ(tasksGroup.name(), name);
    EXPECT_EQ(tasksGroup.fcs(), fcs);
    EXPECT_TRUE(
        geolib3::test_tools::approximateEqual(
            geolib3::convertMercatorToGeodetic(tasksGroup.mercatorGeom()),
            area,
            GEOM_EPS));
    EXPECT_EQ(tasksGroup.useRouting(), db::ugc::UseRouting::No);
    EXPECT_EQ(tasksGroup.useToll(), db::ugc::UseToll::Yes);
    EXPECT_EQ(tasksGroup.recommendedTaskLengthMeters(), recommendedTaskLength);
    EXPECT_EQ(tasksGroup.allowedAssigneesLogins(), allowedAssigneesLogins);
}


TEST(tasks_group_api_should, test_patch_tasks_group_by_id_status_transitions)
{
    Fixture fixture;
    auto tasksGroup = createExampleTasksGroup1(fixture.txnHandle());
    auto txn = fixture.txnHandle();
    db::ugc::TasksGroupGateway gtw(*txn);

    {
        http::MockRequest request(
            http::PATCH,
            http::URL("http://localhost/tasks_groups/" + std::to_string(tasksGroup.id()) + "/")
        );
        request.body = "{\"status\":\"Generating\"}";
        auto response = yacare::performTestRequest(request);
        EXPECT_EQ(response.status, 200);
    }

    gtw.reload(tasksGroup);
    EXPECT_EQ(tasksGroup.status(), db::ugc::TasksGroupStatus::Generating);
    auto task = fixture.createTask(tasksGroup.id());

    {
        http::MockRequest request(
            http::PATCH,
            http::URL("http://localhost/tasks_groups/" + std::to_string(tasksGroup.id()) + "/")
        );
        request.body = "{\"status\":\"Draft\"}";
        auto response = yacare::performTestRequest(request);
        EXPECT_EQ(response.status, 200);
    }
    gtw.reload(tasksGroup);
    EXPECT_EQ(tasksGroup.status(), db::ugc::TasksGroupStatus::Draft);
    // tasks should be deleted

    EXPECT_EQ(
        db::ugc::TaskGateway(*txn).count(db::ugc::table::Task::tasksGroupId == tasksGroup.id()),
        0u);


    tasksGroup.setStatus(db::ugc::TasksGroupStatus::Open);
    {
        auto masterTxn = fixture.txnHandle();
        db::ugc::TasksGroupGateway(*masterTxn).update(tasksGroup);
        masterTxn->commit();
    }

    task = fixture.createTask(tasksGroup.id());
    {
        http::MockRequest request(
            http::PATCH,
            http::URL("http://localhost/tasks_groups/" + std::to_string(tasksGroup.id()) + "/")
        );
        request.body = "{\"status\":\"InProgress\"}";
        auto response = yacare::performTestRequest(request);
        EXPECT_EQ(response.status, 200);
    }
    gtw.reload(tasksGroup);
    EXPECT_EQ(tasksGroup.status(), db::ugc::TasksGroupStatus::InProgress);
    db::ugc::TaskGateway(*txn).reload(task);
    EXPECT_EQ(task.status(), db::ugc::TaskStatus::New);

    {
        http::MockRequest request(
            http::PATCH,
            http::URL("http://localhost/tasks_groups/" + std::to_string(tasksGroup.id()) + "/")
        );
        request.body = "{\"status\":\"Open\"}";
        auto response = yacare::performTestRequest(request);
        EXPECT_EQ(response.status, 400);
    }
    gtw.reload(tasksGroup);
    EXPECT_EQ(tasksGroup.status(), db::ugc::TasksGroupStatus::InProgress);

    {
        http::MockRequest request(
            http::PATCH,
            http::URL("http://localhost/tasks_groups/" + std::to_string(tasksGroup.id()) + "/")
        );
        request.body = "{\"status\":\"Closed\"}";
        auto response = yacare::performTestRequest(request);
        EXPECT_EQ(response.status, 200);
    }
    gtw.reload(tasksGroup);
    EXPECT_EQ(tasksGroup.status(), db::ugc::TasksGroupStatus::Closed);

    db::ugc::TaskGateway(*txn).reload(task);
    EXPECT_EQ(task.status(), db::ugc::TaskStatus::Done);
}

TEST(tasks_group_api_should, test_patch_tasks_group_allows_to_edit_assigned_logins)
{
    Fixture fixture;
    auto tasksGroup = createExampleTasksGroup1(fixture.txnHandle());

    const MultiPolygon2 area(
        {
            Polygon2{
                PointsVector{{43.83, 56.3},{43.93, 56.3},{43.93, 56.2},{43.83, 56.2}}
            }
        }
    );

    const std::string name = "Podolsk";
    const std::vector<int> fcs{1, 2, 3};
    const uint32_t recommendedTaskLength = 18000;
    const std::vector<std::string> allowedAssigneesLogins{"testLogin1", "testLogin2", "testLogin3"};

    {
        auto txn = fixture.txnHandle();
        db::ugc::TasksGroupGateway gtw(*txn);
        tasksGroup.setStatus(db::ugc::TasksGroupStatus::InProgress);
        tasksGroup.setName(name);
        tasksGroup.setFcs(fcs);
        tasksGroup.setMercatorGeom(geolib3::convertGeodeticToMercator(area));
        tasksGroup.setUseRouting(db::ugc::UseRouting::No);
        tasksGroup.setUseToll(db::ugc::UseToll::Yes);
        tasksGroup.setRecommendedTaskLengthMeters(recommendedTaskLength);
        tasksGroup.setAllowedAssigneesLogins(allowedAssigneesLogins);
        gtw.update(tasksGroup);
        txn->commit();
    }


    const std::vector<std::string> newAllowedAssigneesLogins{"testLogin1", "testLogin2"};

    json::Builder builder;
    builder << [&](json::ObjectBuilder builder) {
        builder["params"] << [&](json::ObjectBuilder builder) {
            builder["allowedAssigneesLogins"] = newAllowedAssigneesLogins;
        };
    };

    http::MockRequest request(
        http::PATCH,
        http::URL("http://localhost/tasks_groups/" + std::to_string(tasksGroup.id()) + "/")
            .addParam("lang", "ru_RU")
    );

    request.body = builder.str();

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

    db::ugc::TasksGroupGateway(*fixture.txnHandle()).reload(tasksGroup);

    EXPECT_EQ(tasksGroup.status(), db::ugc::TasksGroupStatus::InProgress);
    EXPECT_EQ(tasksGroup.name(), name);
    EXPECT_EQ(tasksGroup.fcs(), fcs);
    EXPECT_TRUE(
        geolib3::test_tools::approximateEqual(
            geolib3::convertMercatorToGeodetic(tasksGroup.mercatorGeom()),
            area,
            GEOM_EPS));
    EXPECT_EQ(tasksGroup.useRouting(), db::ugc::UseRouting::No);
    EXPECT_EQ(tasksGroup.useToll(), db::ugc::UseToll::Yes);
    EXPECT_EQ(tasksGroup.recommendedTaskLengthMeters(), recommendedTaskLength);
    EXPECT_EQ(tasksGroup.allowedAssigneesLogins(), newAllowedAssigneesLogins);

    /// If allowedAssigneesLogins is not defined than task is awailable to everyone
    request.body = "{\"params\": {}}";

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

    db::ugc::TasksGroupGateway(*fixture.txnHandle()).reload(tasksGroup);

    EXPECT_EQ(tasksGroup.status(), db::ugc::TasksGroupStatus::InProgress);
    EXPECT_EQ(tasksGroup.name(), name);
    EXPECT_EQ(tasksGroup.fcs(), fcs);
    EXPECT_TRUE(
        geolib3::test_tools::approximateEqual(
            geolib3::convertMercatorToGeodetic(tasksGroup.mercatorGeom()),
            area,
            GEOM_EPS));
    EXPECT_EQ(tasksGroup.useRouting(), db::ugc::UseRouting::No);
    EXPECT_EQ(tasksGroup.useToll(), db::ugc::UseToll::Yes);
    EXPECT_EQ(tasksGroup.recommendedTaskLengthMeters(), recommendedTaskLength);
    EXPECT_FALSE(tasksGroup.allowedAssigneesLogins().has_value());
}

TEST(tasks_group_api_should, test_grinder_task_created)
{
    Fixture fixture;
    const auto responseSchema = readResponseSchemasFromSwagger(schemasPath());
    auto a = createExampleTasksGroup1(fixture.txnHandle());
    auto txn = fixture.txnHandle();
    db::ugc::TasksGroupGateway gtw(*txn);

    EXPECT_EQ(fixture.grinderClient().listTaskIds().size(), 0u);

    http::MockRequest request(
        http::PATCH,
        http::URL("http://localhost/tasks_groups/" + std::to_string(a.id()) + "/")
            .addParam("lang", "ru_RU")
    );
    request.body = "{\"status\": \"Generating\" }";

    auto response = yacare::performTestRequest(request);
    EXPECT_EQ(response.status, 200);
    validateJson(response.body,
                responseSchema.at({"PATCH", "/tasks_groups/{id}/", 200}),
                schemasDir());

    auto taskIds = fixture.grinderClient().listTaskIds();
    EXPECT_EQ(taskIds.size(), 1u);

    auto taskMeta = fixture.grinderClient().getTaskArgs(taskIds.at(0));
    EXPECT_EQ(taskMeta["type"].as<std::string>(), "generate_tasks");
    EXPECT_EQ(taskMeta["tasks_group_id"].as<db::TId>(), a.id());

    gtw.reload(a);
    EXPECT_EQ(a.status(), db::ugc::TasksGroupStatus::Generating);
}


TEST(tasks_group_api_should, test_get_tasks_groups_report_csv)
{
    Fixture fixture;

    auto blackbox
        = std::make_unique<testing::NiceMock<MockBlackboxClient>>();
    EXPECT_CALL(
        *blackbox, uidToLoginMap(testing::Eq(blackbox_client::Uids{123})))
        .WillOnce(testing::Return(
            blackbox_client::UidToLoginMap{{123, "qwerty"}}));
    auto prevBlackbox = Globals::swap(std::move(blackbox));
    concurrent::ScopedGuard onExit(
        [&] { Globals::swap(std::move(prevBlackbox)); });

    auto tasksGroup = createExampleTasksGroup1(fixture.txnHandle());
    auto task = fixture.createTask(tasksGroup.id());
    auto assignment = fixture.createAssignment(task, USER);
    auto review = fixture.fillAssignmentReview(assignment.id());

    http::MockRequest request(
        http::GET, http::URL("http://localhost/tasks_groups/"
                       + std::to_string(tasksGroup.id()) + "/report")
                       .addParam("lang", "ru"));

    auto response = yacare::performTestRequest(request);
    EXPECT_EQ(response.status, 200);
    EXPECT_EQ(response.body,
        "task_id\ttask_name\ttask_distance\tassignment_status\tassigned_to\t"
        "camera_direction\ttotal_coverage\tgood_coverage\tprocessed_photos\t"
        "good_photos\tactualization_date\ttask_distance_in_meters\r\n"
        "1\tMoscow\t101 км\tInProgress\tqwerty\tFront\t50%\t40%\t10000\t8000\t"
        "2017-10-10T16:00:00Z\t100500\r\n");
}


TEST(tasks_group_api_should, test_get_tasks_groups_report_json)
{
    Fixture fixture;

    auto blackbox = std::make_unique<testing::NiceMock<MockBlackboxClient>>();
    EXPECT_CALL(*blackbox,
                uidToLoginMap(testing::Eq(blackbox_client::Uids{123})))
        .WillOnce(
            testing::Return(blackbox_client::UidToLoginMap{{123, "qwerty"}}));
    auto prevBlackbox = Globals::swap(std::move(blackbox));
    concurrent::ScopedGuard onExit(
        [&] { Globals::swap(std::move(prevBlackbox)); });

    auto tasksGroup = createExampleTasksGroup1(fixture.txnHandle());
    auto task = fixture.createTask(tasksGroup.id());
    auto assignment = fixture.createAssignment(task, USER);
    auto review = fixture.fillAssignmentReview(assignment.id());

    http::MockRequest request(
        http::GET, http::URL("http://localhost/tasks_groups/" +
                             std::to_string(tasksGroup.id()) + "/report")
                       .addParam("lang", "ru")
                       .addParam("format", "json"));

    auto response = yacare::performTestRequest(request);
    EXPECT_EQ(response.status, 200);
    EXPECT_EQ(json::Value::fromString(response.body),
              json::Value::fromString(R"([
    {
        "task_id": 1,
        "task_name": "Moscow",
        "task_distance": 100.5,
        "assignment_status": "InProgress",
        "assigned_to": "qwerty",
        "camera_direction": "Front",
        "total_coverage": 0.5,
        "good_coverage": 0.4,
        "processed_photos": 10000,
        "good_photos": 8000,
        "actualization_date": "2017-10-10T16:00:00Z"
    }
])"));
}

TEST(tasks_group_api_should, test_patch_tasks_group_emails)
{
    using TEmails = std::vector<std::string>;
    using TMaybeEmails = std::optional<TEmails>;

    auto fixture = Fixture{};
    auto emails = TEmails{"a1@ya.ru", "a2@ya.ru"};
    auto txn = fixture.txnHandle();

    // create tasks group with emails
    auto tasksGroupId = [&] {
        auto tasksGroupParams = makeSampleTasksGroupParams();
        tasksGroupParams.emails = emails;
        checkCreateTasksGroup(fixture, tasksGroupParams);
        auto tasksGroupIds = db::ugc::TasksGroupGateway{*txn}.loadIds();
        EXPECT_EQ(tasksGroupIds.size(), 1u);
        return tasksGroupIds.front();
    }();
    EXPECT_TRUE(equal(loadTasksGroupKit(*txn, tasksGroupId).emails, emails));

    auto patchEmails = [tasksGroupId](const TMaybeEmails& maybeEmails) {
        auto builder = json::Builder{};
        builder << [&](json::ObjectBuilder bld) {
            bld["params"] << [&](json::ObjectBuilder bld) {
                if (maybeEmails.has_value()) {
                    bld["emails"] = maybeEmails.value();
                }
            };
        };

        auto request =
            http::MockRequest{http::PATCH,
                              http::URL("http://localhost/tasks_groups/" +
                                        std::to_string(tasksGroupId) + "/")};
        request.body = builder.str();

        auto response = yacare::performTestRequest(request);
        EXPECT_EQ(response.status, 200);
        static auto schema = readResponseSchemasFromSwagger(schemasPath());
        validateJson(response.body,
                     schema.at({"PATCH", "/tasks_groups/{id}/", 200}),
                     schemasDir());
    };

    // add email
    emails.push_back("a3@ya.ru");
    patchEmails(emails);
    EXPECT_TRUE(equal(loadTasksGroupKit(*txn, tasksGroupId).emails, emails));

    // do nothig
    patchEmails(std::nullopt);
    EXPECT_TRUE(equal(loadTasksGroupKit(*txn, tasksGroupId).emails, emails));

    // update email
    emails.front() = "a4@ya.ru";
    patchEmails(emails);
    EXPECT_TRUE(equal(loadTasksGroupKit(*txn, tasksGroupId).emails, emails));

    // remove email
    emails.erase(emails.begin());
    patchEmails(emails);
    EXPECT_TRUE(equal(loadTasksGroupKit(*txn, tasksGroupId).emails, emails));

    // remove all emails
    emails.clear();
    patchEmails(emails);
    EXPECT_TRUE(equal(loadTasksGroupKit(*txn, tasksGroupId).emails, emails));
}

} // namespace tests
} // namespace tasks_planner
} // namespace mrc
} // namespace maps
