#include <library/cpp/testing/unittest/registar.h>
#include <mapreduce/yt/tests/yt_unittest_lib/yt_unittest_lib.h>
#include <maps/libs/chrono/include/days.h>
#include <maps/libs/http/include/test_utils.h>
#include <maps/libs/tile/include/geometry.h>
#include <maps/wikimap/mapspro/services/mrc/libs/db/include/disqualified_source_gateway.h>
#include <maps/wikimap/mapspro/services/mrc/long_tasks/taxi_stat/lib/db.h>
#include <maps/wikimap/mapspro/services/mrc/long_tasks/taxi_stat/lib/yt.h>
#include <yandex/maps/mrc/unittest/database_fixture.h>

using namespace maps::chrono::literals;

namespace maps::mrc::taxi_stat::tests {

geolib3::Point2 geodeticCenter(const tile::Tile& tile)
{
    return convertMercatorToGeodetic(mercatorBBox(tile)).center();
}

const auto TILE_1 = tile::Tile(19805, 10276, 15);
const auto TILE_2 = tile::Tile(19808, 10274, 15);
const auto POS_1 = geodeticCenter(TILE_1);
const auto POS_2 = geodeticCenter(TILE_2);
const auto GEO_ID_1 = GeoId{1};
const auto GEO_ID_2 = GeoId{2};
const auto GEO_ID_3 = GeoId{3};
const auto DEVICE_ID_1 = DeviceId{1};
const auto DEVICE_ID_2 = DeviceId{2};
const auto DEVICE_ID_3 = DeviceId{3};

struct MockGeoEvaluator : GeoEvaluator {
    GeoIds evalGeoIds(const geolib3::Point2& geodeticPos) const override
    {
        if (POS_1 == geodeticPos) {
            return {GEO_ID_1};
        }
        if (POS_2 == geodeticPos) {
            return {GEO_ID_2, GEO_ID_3};
        }
        return {};
    }
};

struct Event {
    EventId id;
    double createdAt;
    geolib3::Point2 pos;
    TString eventType;
    std::optional<TString> presignedExternalVideoUrl;
    DeviceId deviceId;
};

using Events = std::vector<Event>;

const TString VIDEO_URL = "http://sda-videos.s3.mds.yandex.net/mp4";

std::pair<Events, Log> makeEventsAndExpectedLog()
{
    auto events = Events{
        Event{
            .id = 147274949,
            .createdAt = 1651866765.898768,
            .pos = POS_1,
            .eventType = "mrc_capture",
            .presignedExternalVideoUrl = VIDEO_URL,
            .deviceId = DEVICE_ID_1,
        },
        Event{
            .id = 152917518,
            .createdAt = 1652606665.074079,
            .pos = POS_1,
            .eventType = "mrc_capture",
            .presignedExternalVideoUrl = VIDEO_URL,
            .deviceId = DEVICE_ID_1,
        },
        Event{
            .id = 152923876,
            .createdAt = 1652607345.823757,
            .pos = POS_1,
            .eventType = "mrc_capture",
            .presignedExternalVideoUrl = VIDEO_URL,
            .deviceId = DEVICE_ID_2,
        },
        Event{
            .id = 152941399,
            .createdAt = 1652609233.374557,
            .pos = POS_1,
            .eventType = "mrc_capture",
            .deviceId = DEVICE_ID_1,
        },
        Event{
            .id = 153070306,
            .createdAt = 1652623220.448899,
            .pos = POS_1,
            .eventType = "trial_external",
            .presignedExternalVideoUrl = VIDEO_URL,
            .deviceId = DEVICE_ID_1,
        },
        Event{
            .id = 153082579,
            .createdAt = 1653091209.332209,
            .pos = POS_2,
            .eventType = "mrc_capture",
            .presignedExternalVideoUrl = VIDEO_URL,
            .deviceId = DEVICE_ID_2,
        },
    };
    auto log = Log{
        {DateRegion{.date = chrono::parseIsoDate("2022-05-06"),
                    .geoId = GEO_ID_1},
         Aggregate{.bytesNumber = 1,
                   .maxEventId = 147274949,
                   .deviceIdSet{DEVICE_ID_1}}},
        {DateRegion{.date = chrono::parseIsoDate("2022-05-15"),
                    .geoId = GEO_ID_1},
         Aggregate{.bytesNumber = 2,
                   .maxEventId = 152923876,
                   .deviceIdSet{DEVICE_ID_1, DEVICE_ID_2}}},
        {DateRegion{.date = chrono::parseIsoDate("2022-05-21"),
                    .geoId = GEO_ID_2},
         Aggregate{.bytesNumber = 1,
                   .maxEventId = 153082579,
                   .deviceIdSet{DEVICE_ID_2}}},
        {DateRegion{.date = chrono::parseIsoDate("2022-05-21"),
                    .geoId = GEO_ID_3},
         Aggregate{.bytesNumber = 1,
                   .maxEventId = 153082579,
                   .deviceIdSet{DEVICE_ID_2}}},
    };
    return {std::move(events), std::move(log)};
}

void saveEvents(NYT::ITransaction& txn,
                const Events& events,
                const TString& outputTable)
{
    txn.Remove(outputTable, NYT::TRemoveOptions().Force(true));
    auto writer = txn.CreateTableWriter<NYT::TNode>(
        NYT::TRichYPath(outputTable).SortedBy(events::ID));
    for (const auto& event : events) {
        auto row = NYT::TNode{};
        row[events::ID] = event.id;
        row[events::CREATED_AT] = event.createdAt;
        row[events::GNSS_LONGITUDE] = event.pos.x();
        row[events::GNSS_LATITUDE] = event.pos.y();
        row[events::EVENT_TYPE] = event.eventType;
        if (event.presignedExternalVideoUrl.has_value()) {
            row[events::PRESIGNED_EXTERNAL_VIDEO_URL] =
                event.presignedExternalVideoUrl.value();
        }
        row[events::DEVICE_ID] = event.deviceId;
        writer->AddRow(row);
    }
    writer->Finish();
}

class Playground
    : public unittest::WithUnittestConfig<unittest::DatabaseFixture> {
    Playground() = default;

public:
    static Playground& instance()
    {
        static Playground result;
        return result;
    }
};

std::pair<db::DisqualifiedSources, Bans> makeDisqualifiedSourcesAndExpectedBans(
    chrono::TimePoint now)
{
    auto disqSrcs = db::DisqualifiedSources{
        db::DisqualifiedSource{
            db::DisqType::DisableCapturing, "src1", now - 3_days, now - 1_days},
        db::DisqualifiedSource{
            db::DisqType::DisablePublishing, "src1", now - 1_days},
        db::DisqualifiedSource{
            db::DisqType::DisableCapturing, "src2", now - 2_days, now},
    };
    now = std::chrono::floor<chrono::Days>(now);
    auto bans = Bans{
        {
            BanKey{.date = now - 3_days,
                   .disqType = db::DisqType::DisableCapturing},
            1u,
        },
        {
            BanKey{.date = now - 2_days,
                   .disqType = db::DisqType::DisableCapturing},
            2u,
        },
        {
            BanKey{.date = now - 1_days,
                   .disqType = db::DisqType::DisableCapturing},
            1u,
        },
        {
            BanKey{.date = now - 1_days,
                   .disqType = db::DisqType::DisablePublishing},
            1u,
        },
    };
    return {std::move(disqSrcs), std::move(bans)};
}

Bans loadBans(NYT::ITransaction& txn, std::string_view bansTable)
{
    auto result = Bans{};
    if (!txn.Exists(TString(bansTable))) {
        return result;
    }
    for (auto reader = txn.CreateTableReader<NYT::TNode>(TString(bansTable));
         reader->IsValid();
         reader->Next()) {
        const auto& row = reader->GetRow();
        auto date = epochToDay(row[bans::DATE].AsInt64());
        auto disqType =
            static_cast<db::DisqType>(row[bans::DISQ_TYPE].AsInt64());
        auto count = row[bans::COUNT].AsUint64();
        result[BanKey{.date = date, .disqType = disqType}] = count;
    }
    return result;
}

struct TileRequest {
    TString request;
    TString timestamp;
    TString userAgent;
    TString vhost;
};

using TileRequests = std::vector<TileRequest>;

std::string makeRequest(DeviceId deviceId, const tile::Tile& tile)
{
    return concat(TAXI_REQUEST_PATH,
                  "?deviceid=",
                  deviceId,
                  "&x=",
                  tile.x(),
                  "&y=",
                  tile.y(),
                  "&z=",
                  tile.z());
}

std::pair<TileRequests, Activity> makeTileRequestsAndExpectedActivity()
{
    auto tileRequests = TileRequests{
        TileRequest{
            .request = makeRequest(DEVICE_ID_1, TILE_1),
            .timestamp = "2022-06-01T10:55:00",
            .userAgent = TAXI_USER_AGENT,
            .vhost = AGENT_PROXY_HOST,
        },
        TileRequest{
            .request = makeRequest(DEVICE_ID_1, TILE_1),
            .timestamp = "2022-06-01T10:56:00",
            .userAgent = TAXI_USER_AGENT,
            .vhost = AGENT_PROXY_HOST,
        },
        TileRequest{
            .request = makeRequest(DEVICE_ID_2, TILE_1),
            .timestamp = "2022-06-01T10:57:00",
            .userAgent = TAXI_USER_AGENT,
            .vhost = AGENT_PROXY_HOST,
        },
        TileRequest{
            .timestamp = "2022-06-02T10:10:00",
            .userAgent = TAXI_USER_AGENT,
            .vhost = AGENT_PROXY_HOST,
        },
        TileRequest{
            .request = makeRequest(DEVICE_ID_1, TILE_2),
            .timestamp = "2022-06-02T10:11:00",
            .userAgent = TAXI_USER_AGENT,
            .vhost = AGENT_PROXY_HOST,
        },
        TileRequest{
            .request = makeRequest(DEVICE_ID_2, TILE_2),
            .timestamp = "2022-06-02T10:12:00",
            .vhost = AGENT_PROXY_HOST,
        },
        TileRequest{
            .request = makeRequest(DEVICE_ID_3, TILE_2),
            .timestamp = "2022-06-02T10:13:00",
            .userAgent = TAXI_USER_AGENT,
            .vhost = concat(AGENT_PROXY_HOST, "-1"),
        },
    };
    auto activity = Activity{
        {DateRegion{.date = chrono::parseIsoDate("2022-06-01"),
                    .geoId = GEO_ID_1},
         2},
        {DateRegion{.date = chrono::parseIsoDate("2022-06-02"),
                    .geoId = GEO_ID_2},
         1},
        {DateRegion{.date = chrono::parseIsoDate("2022-06-02"),
                    .geoId = GEO_ID_3},
         1},
    };
    return {std::move(tileRequests), std::move(activity)};
}

void saveTileRequests(NYT::ITransaction& txn,
                      const TileRequests& tileRequests,
                      const TString& outputTable)
{
    txn.Remove(outputTable, NYT::TRemoveOptions().Force(true));
    auto writer = txn.CreateTableWriter<NYT::TNode>(
        NYT::TRichYPath(outputTable).SortedBy(logfeller::VHOST));
    for (const auto& tileRequest : tileRequests) {
        auto row = NYT::TNode{};
        row[logfeller::REQUEST] = tileRequest.request;
        row[logfeller::TIMESTAMP] = tileRequest.timestamp;
        row[logfeller::USER_AGENT] = tileRequest.userAgent;
        row[logfeller::VHOST] = tileRequest.vhost;
        writer->AddRow(row);
    }
    writer->Finish();
}

Y_UNIT_TEST_SUITE(suite)
{
    Y_UNIT_TEST(test_log)
    {
        auto mdsMock = http::addMock(VIDEO_URL, [](const http::MockRequest&) {
            auto result = http::MockResponse::withStatus(200);
            result.headers["Content-Length"] = "1";
            return result;
        });
        auto mockGeoEvaluator = MockGeoEvaluator{};

        auto [events, expectedLog] = makeEventsAndExpectedLog();
        auto client = NYT::NTesting::CreateTestClient();
        auto txn = client->StartTransaction();
        auto eventsTable = TString("//home/events");
        auto grepTable = TString("//home/grep");
        auto logTable = TString("//home/log");
        auto log = taxi_stat::Log{};

        saveEvents(*txn, events, eventsTable);
        grepEvents(*txn, eventsTable, grepTable, EventId{});
        loadEvents(*txn, mockGeoEvaluator, grepTable, log);
        UNIT_ASSERT_EQUAL(log, expectedLog);

        saveLog(*txn, log, logTable);
        log = loadLog(*txn, logTable);
        UNIT_ASSERT_EQUAL(log, expectedLog);
    }

    Y_UNIT_TEST(test_bans)
    {
        auto now = chrono::TimePoint::clock::now();
        auto [disqSrcs, expectedBans] =
            makeDisqualifiedSourcesAndExpectedBans(now);
        Playground::instance().postgres().truncateTables();
        auto& pool = Playground::instance().pool();
        {
            auto txn = pool.masterWriteableTransaction();
            db::DisqualifiedSourceGateway{*txn}.insert(disqSrcs);
            txn->commit();
        }

        auto bans = taxi_stat::loadBans(pool, now);
        UNIT_ASSERT_EQUAL(bans, expectedBans);

        auto client = NYT::NTesting::CreateTestClient();
        auto txn = client->StartTransaction();
        auto bansTable = TString("//home/bans");
        saveBans(*txn, bans, bansTable);
        bans = loadBans(*txn, bansTable);
        UNIT_ASSERT_EQUAL(bans, expectedBans);
    }

    Y_UNIT_TEST(test_activity)
    {
        auto mockGeoEvaluator = MockGeoEvaluator{};

        auto [tileRequests, expectedActivity] =
            makeTileRequestsAndExpectedActivity();
        auto client = NYT::NTesting::CreateTestClient();
        auto txn = client->StartTransaction();
        auto logfellerTable = TString("//home/2022-06-02");
        auto tileRequestsTable = TString("//home/tile_requests");
        auto activityTable = TString("//home/activity");
        auto activity = taxi_stat::Activity{};

        saveTileRequests(*txn, tileRequests, logfellerTable);
        grepTileRequestsFromDir(
            *txn, "//home", tileRequestsTable, std::nullopt);
        loadTileRequests(*txn, mockGeoEvaluator, tileRequestsTable, activity);
        UNIT_ASSERT_EQUAL(activity, expectedActivity);

        saveActivity(*txn, activity, activityTable);
        activity = loadActivity(*txn, activityTable);
        UNIT_ASSERT_EQUAL(activity, expectedActivity);
    }
}

}  // namespace maps::mrc::taxi_stat::tests
