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

#include <maps/libs/auth/include/blackbox.h>
#include <maps/libs/auth/include/user_info.h>
#include <maps/libs/chrono/include/time_point.h>
#include <maps/wikimap/mapspro/services/mrc/libs/db/include/track_point_gateway.h>
#include <maps/wikimap/mapspro/services/mrc/libs/db/include/ride_recording_report_gateway.h>

#include <yandex/maps/mrc/signal_queue/result_queue.h>
#include <yandex/maps/mrc/signal_queue/results.h>
#include <yandex/maps/pb_stream2/writer.h>
#include <yandex/maps/proto/common2/geometry.sproto.h>
#include <yandex/maps/proto/mrc/common/error.sproto.h>
#include <yandex/maps/proto/offline-mrc/results.sproto.h>
#include <yandex/maps/proto/offline_recording/record.pb.h>
#include <maps/infra/yacare/include/yacare.h>
#include <maps/infra/yacare/include/test_utils.h>

#include <boost/lexical_cast.hpp>

#include <sstream>

namespace presults = yandex::maps::sproto::offline::mrc::results;
namespace precord = yandex::maps::proto::offline::recording::record;

using namespace yacare::tests;

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

namespace {

constexpr chrono::TimePoint FIRST_TIMESTAMP{std::chrono::seconds(100500)};
constexpr chrono::TimePoint LAST_TIMESTAMP{std::chrono::seconds(100600)};

uint64_t timePointToSeconds(chrono::TimePoint time)
{
    return std::chrono::duration_cast<std::chrono::seconds>(
                time.time_since_epoch()).count();
}

std::string makeRecordingRecords(chrono::TimePoint firstTimestamp,
                                 chrono::TimePoint lastTimestamp)
{
    std::ostringstream stream;
    maps::pb_stream2::Writer writer(&stream);

    auto firstTimestampMs = timePointToSeconds(firstTimestamp);

    // Add some records to the report
    precord::Record record;
    for (int i = 0; i < 3; ++i) {
        record.set_timestamp(firstTimestampMs + i);
        writer << record;
    }
    record.set_timestamp(timePointToSeconds(lastTimestamp));
    writer << record;
    writer.close();
    stream.flush();
    return stream.str();
}

presults::Results makeRideResults(
    const std::optional<std::string>& clientRideId = std::nullopt)
{
    presults::Results results;

    presults::Image image;
    image.created() = 2;
    image.image() = "raw data";

    results.images().push_back(image);

    presults::TrackPoint trackPoint;
    trackPoint.time() = 1;
    trackPoint.location().point().lon() = 46.;
    trackPoint.location().point().lat() = 54.;
    trackPoint.location().heading() = 15.;

    results.track().push_back(trackPoint);
    trackPoint.time() = 3;
    results.track().push_back(trackPoint);

    results.reports().push_back(
        makeRecordingRecords(FIRST_TIMESTAMP, LAST_TIMESTAMP));

    if (clientRideId) {
        results.client_ride_id() = *clientRideId;
    }

    return results;
}


} // namespace

TEST_F(Fixture, ride_upload_test)
{
    UserIdHeaderFixture userIdHeaderFixture;

    for (const auto& clientRideId : CLIENT_RIDE_IDS) {
        Playground::instance().postgres().truncateTables();
        Playground::instance().clearMds();

        auto results = makeRideResults(clientRideId);
        std::stringstream sstream;
        sstream << results;

        http::MockRequest request(http::POST,
                                  http::URL("http://localhost/ugc/rides/upload")
                                      .addParam("deviceid", SOURCE_ID)
                                      .addParam("uid", USER_ID));

        request.headers.emplace(USER_ID_HEADER, USER_ID);
        const std::string clientIp = "192.168.1.1";
        const uint16_t clientPort = 9999;
        setClientHostPort(request, clientIp, clientPort);
        request.body = sstream.str();

        auto response = yacare::performTestRequest(request);

        ASSERT_EQ(response.status, 200);

        auto txn = pgPool().masterReadOnlyTransaction();
        auto trackPoints = db::TrackPointGateway{*txn}.load(
            db::table::TrackPoint::sourceId.equals(SOURCE_ID));
        ASSERT_EQ(trackPoints.size(), 2ul);

        auto reports = db::rides::RideRecordingReportGateway{*txn}.load(
            db::rides::table::RideRecordingReport::sourceId.equals(SOURCE_ID));
        ASSERT_EQ(reports.size(), 1ul);

        EXPECT_EQ(reports.front().startedAt(), FIRST_TIMESTAMP);
        EXPECT_EQ(reports.front().finishedAt(), LAST_TIMESTAMP);

        signal_queue::ResultsQueue queue(
            config().signalsUploader().queuePath());
        EXPECT_EQ(queue.count<signal_queue::RideImage>(), 1ul);

        auto rideImage = queue.pop<signal_queue::RideImage>();
        ASSERT_TRUE(rideImage);

        EXPECT_EQ(rideImage->userId(), USER_ID);

        ASSERT_TRUE(rideImage->sourceIp());
        EXPECT_EQ(*rideImage->sourceIp(), clientIp);

        ASSERT_TRUE(rideImage->sourcePort());
        EXPECT_EQ(*rideImage->sourcePort(), clientPort);

        if (clientRideId) {
            ASSERT_TRUE(rideImage->clientRideId());
            EXPECT_EQ(rideImage->clientRideId().get(), *clientRideId);
        }
        else {
            EXPECT_FALSE(rideImage->clientRideId());
        }
    }
}


TEST_F(Fixture, ride_upload_from_belarus_user_without_phone_test)
{
    auth::UserInfo userInfo;
    userInfo.setUid(USER_ID);
    UserInfoFixture userInfoFixture(std::move(userInfo));
    auto results = makeRideResults();
    std::stringstream sstream;
    sstream << results;

    http::MockRequest request(
            http::POST,
            http::URL("http://localhost/ugc/rides/upload")
                .addParam("deviceid", SOURCE_ID)
                .addParam("lang", "ru_RU")
    );

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

    auto response = yacare::performTestRequest(request);

    ASSERT_EQ(response.status, 451);
    ASSERT_EQ(response.headers.at("Content-Type"), "application/x-protobuf");

    auto txn = pgPool().masterReadOnlyTransaction();
    auto trackPoints = db::TrackPointGateway{*txn}.load(
        db::table::TrackPoint::sourceId.equals(SOURCE_ID));
    ASSERT_EQ(trackPoints.size(), 0ul);

    auto reports = db::rides::RideRecordingReportGateway{*txn}.load(
        db::rides::table::RideRecordingReport::sourceId.equals(SOURCE_ID));
    ASSERT_EQ(reports.size(), 0ul);

    signal_queue::ResultsQueue queue(
        config().signalsUploader().queuePath());
    EXPECT_EQ(queue.count<signal_queue::RideImage>(), 0ul);

    auto error = boost::lexical_cast<yandex::maps::sproto::mrc::common::Error>(response.body);
    EXPECT_TRUE(error.code());
    EXPECT_EQ(*error.code(), yandex::maps::sproto::mrc::common::Error::Code::LEGAL_REASON_PHONE_REQUIRED);
    EXPECT_EQ(error.description(), DEFAULT_BELARUS_UPLOAD_DECLINE_DESCRIPTION);
}

TEST_F(Fixture, ride_upload_from_belarus_user_with_phone_test)
{
    auth::UserInfo userInfo;
    userInfo.setUid(USER_ID);
    userInfo.setPhones({auth::Phone("1" /* phone_id */)});
    UserInfoFixture userInfoFixture(std::move(userInfo));
    auto results = makeRideResults();
    std::stringstream sstream;
    sstream << results;

    http::MockRequest request(
            http::POST,
            http::URL("http://localhost/ugc/rides/upload")
                .addParam("deviceid", SOURCE_ID)
                .addParam("lang", "ru_RU")
    );

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

    auto response = yacare::performTestRequest(request);

    ASSERT_EQ(response.status, 200);

    auto txn = pgPool().masterReadOnlyTransaction();
    auto trackPoints = db::TrackPointGateway{*txn}.load(
        db::table::TrackPoint::sourceId.equals(SOURCE_ID));
    ASSERT_EQ(trackPoints.size(), 2ul);

    auto reports = db::rides::RideRecordingReportGateway{*txn}.load(
        db::rides::table::RideRecordingReport::sourceId.equals(SOURCE_ID));
    ASSERT_EQ(reports.size(), 1ul);

    EXPECT_EQ(reports.front().startedAt(), FIRST_TIMESTAMP);
    EXPECT_EQ(reports.front().finishedAt(), LAST_TIMESTAMP);

    signal_queue::ResultsQueue queue(
        config().signalsUploader().queuePath());
    EXPECT_EQ(queue.count<signal_queue::RideImage>(), 1ul);
}

TEST_F(Fixture, drive_upload_test)
{
    auto results = makeRideResults();
    std::stringstream sstream;
    sstream << results;

    http::MockRequest request(
            http::POST,
            http::URL("http://localhost/ugc/drive/upload")
                .addParam("deviceid", SOURCE_ID));

    request.body = sstream.str();

    auto response = yacare::performTestRequest(request);

    ASSERT_EQ(response.status, 200);

    auto txn = pgPool().masterReadOnlyTransaction();
    auto trackPoints = db::TrackPointGateway{*txn}.load(
        db::table::TrackPoint::sourceId.equals(SOURCE_ID));
    ASSERT_EQ(trackPoints.size(), 2ul);

    signal_queue::ResultsQueue queue(
        config().signalsUploader().queuePath());
    EXPECT_EQ(queue.count<signal_queue::RideImage>(), 1ul);
}

TEST_F(Fixture, corrupted_recording_test)
{
    // MAPSMRC-2957: ignore corrupted recording protobufs
    UserIdHeaderFixture userIdHeaderFixture;
    auto results = makeRideResults();

    // Make some corrupted recordings
    {
        std::ostringstream stream;
        maps::pb_stream2::Writer writer(&stream);

        const auto firstTimestampMs = timePointToSeconds(FIRST_TIMESTAMP);
        const auto lastTimestampMs = timePointToSeconds(LAST_TIMESTAMP);

        precord::Record record;
        record.set_timestamp(firstTimestampMs);
        writer << record;
        record.set_timestamp(lastTimestampMs);
        writer << record;
        writer.close();
        stream.flush();

        auto recording = stream.str();
        ASSERT_GT(recording.size(), 4ul);
        recording[0] = 'D';
        recording[1] = 'E';
        recording[2] = 'A';
        recording[3] = 'D';
        results.reports().clear();
        results.reports().push_back(recording);
    }

    std::ostringstream stream;
    maps::pb_stream2::Writer writer(&stream);

    std::stringstream sstream;
    sstream << results;

    http::MockRequest request(
            http::POST,
            http::URL("http://localhost/ugc/rides/upload")
                .addParam("deviceid", SOURCE_ID)
                .addParam("uid", USER_ID));

    request.headers.emplace(USER_ID_HEADER, USER_ID);
    request.body = sstream.str();

    auto response = yacare::performTestRequest(request);

    ASSERT_EQ(response.status, 422);

    // Corrupted recording file is dropped
    auto txn = pgPool().masterReadOnlyTransaction();
    auto reports = db::rides::RideRecordingReportGateway{*txn}.load(
        db::rides::table::RideRecordingReport::sourceId.equals(SOURCE_ID));
    ASSERT_EQ(reports.size(), 0ul);

    // But image is OK so it got into the results queue
    signal_queue::ResultsQueue queue(
        config().signalsUploader().queuePath());
    EXPECT_EQ(queue.count<signal_queue::RideImage>(), 1ul);
}

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