#pragma once

#include "common.h"
#include "mocks.h"

#include <library/cpp/testing/gmock_in_unittest/gmock.h>
#include <library/cpp/testing/unittest/env.h>
#include <library/cpp/testing/unittest/registar.h>
#include <maps/infra/yacare/include/test_utils.h>
#include <maps/libs/auth/include/test_utils.h>
#include <maps/libs/common/include/file_utils.h>
#include <maps/libs/common/include/unique_ptr.h>
#include <maps/libs/http/include/http.h>
#include <maps/libs/log8/include/log8.h>
#include <maps/wikimap/mapspro/services/mrc/browser/lib/configuration.h>
#include <maps/wikimap/mapspro/services/mrc/libs/blackbox_client/include/blackbox_client.h>
#include <maps/wikimap/mapspro/services/mrc/libs/db/include/feature_gateway.h>
#include <maps/wikimap/mapspro/services/mrc/libs/fb/include/coverage_rtree_writer.h>
#include <maps/wikimap/mapspro/services/mrc/libs/fb/include/photo_to_edge_pairs_writer.h>
#include <maps/wikimap/mapspro/services/mrc/libs/fb/include/version.h>
#include <maps/wikimap/mapspro/services/mrc/libs/fb/include/write.h>
#include <maps/wikimap/mapspro/services/mrc/libs/ugc_event_logger/include/logger.h>
#include <maps/wikimap/mapspro/services/mrc/long_tasks/export_gen/lib/exporter.h>
#include <yandex/maps/mrc/unittest/database_fixture.h>
#include <yandex/maps/mrc/unittest/local_server.h>
#include <yandex/maps/wiki/unittest/config.h>

#include <boost/filesystem.hpp>

#include <filesystem>
#include <memory>
#include <set>
#include <string>
#include <thread>
#include <vector>

namespace maps::mrc::browser::tests {

const std::string TEST_GRAPH_PATH = BinaryPath("maps/data/test/graph4");
const std::string TEST_GEOID_PATH = BinaryPath("maps/data/test/geoid");
const std::string SERVICES_BASE_TEMPLATE =
    ArcadiaSourceRoot() + "/maps/wikimap/mapspro/cfg/services/services-base-template.xml";

const std::string UGC_EVENT_LOG_PATH = "egc_event.log";

constexpr auto RIDE_BREAK_INTERVAL = 2 * export_gen::MAX_TIME_INTERVAL;

// DEPENDS(maps/wikimap/mapspro/services/mrc/browser/tests/s3mds-stub)
const auto S3MDS_STUB_SERVER_PROVIDER = [](uint16_t port) {
    return process::Command(
        {BinaryPath("maps/wikimap/mapspro/services/mrc/browser/tests/"
                    "s3mds-stub/s3mds-stub"),
         unittest::LOCAL_HOST,
         std::to_string(port),
         SRC_("s3mds-stub/pano_tiles")});
};

class S3MDSStubFixture : private unittest::TestServer {
public:
    S3MDSStubFixture() : TestServer(S3MDS_STUB_SERVER_PROVIDER) {}

    S3MDSStubFixture(xml3::Doc& configDoc) : TestServer(S3MDS_STUB_SERVER_PROVIDER)
    {
        INFO() << "S3 MDS stub is running on " << getHost() << ":" << getPort();
        const char* const S3MDS_XPATH = "/config/external-services/s3mds";
        auto node = configDoc.node(S3MDS_XPATH);
        node.setAttr(
            "url",
            http::URL("http://localhost/").setPort(getPort()).toString());
    }

    std::uint16_t getS3MdsPort() const { return getPort(); }
    const std::string& getS3MdsHost() const { return getHost(); }
};

using PlaygroundBase = unittest::WithUnittestConfig<
    unittest::DatabaseFixture,
    unittest::MdsStubFixture,
    S3MDSStubFixture
>;

class Playground: public PlaygroundBase {

public:
    Playground()
        : tvmtoolRecipeHelper_("maps/libs/auth/tests/tvmtool.recipe.conf")
        , mapsproConfig_(
            postgres().host(),
            postgres().port(),
            postgres().dbname(),
            postgres().user(),
            postgres().password(),
            SERVICES_BASE_TEMPLATE
        )
    {
        postgres().executeSql(maps::common::readFileToString(SRC_("acl.sql")));
        postgres().createExtension("hstore");
        postgres().executeSql(maps::common::readFileToString(
            ArcadiaSourceRoot() + "/maps/wikimap/mapspro/libs/revision/sql/postgres_upgrade.sql"
        ));
    }

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

    auto makeConfiguration(std::optional<std::string> mrcPath = std::nullopt)
    {
        return std::make_shared<Configuration>(
            config(),
            wiki::common::ExtendedXmlDoc(mapsproConfig_.filepath()),
            TEST_GRAPH_PATH,
            TEST_GRAPH_PATH,
            TEST_GRAPH_PATH,
            TEST_GRAPH_PATH,
            TEST_GEOID_PATH,
            std::move(mrcPath),
            ArcadiaSourceRoot() + "/maps/wikimap/mapspro/services/mrc/browser/icons",
            ArcadiaSourceRoot() + "/maps/wikimap/mapspro/services/mrc/browser/tests/data/design",
            std::make_unique<blackbox_client::BlackboxClient>(
                tvmtoolRecipeHelper_.tvmtoolSettings()),
            std::make_unique<PermissionCheckerStub>()
        );
    }

    void makeCoverage()
    {
        auto ctx =
            export_gen::Context(TEST_GRAPH_PATH, db::GraphType::Road);
        auto features = db::FeatureGateway(*pool().slaveTransaction()).load();
        auto photos = export_gen::Photos{};
        for (const auto& feature : features) {
            photos.push_back(export_gen::toPhoto(feature));
        }
        auto [graph, photoToEdgePairs] = export_gen::makeGraphSummary(
            ctx, photos, export_gen::makeTrackPointProvider(pool()));
        graph.mrcVersion = fb::makeVersion(std::chrono::system_clock::now());
        fb::writeToFile(graph, TEST_GRAPH_PATH + "/graph_coverage.fb");
        fb::writeCoverageRtreeToDir(ctx.matcher.graph(), graph, TEST_GRAPH_PATH);
        fb::writePhotoToEdgePairsToFile(graph.version,
                                        graph.mrcVersion,
                                        1u /*PHOTO_TO_EDGE_SCHEMA_VERSION*/,
                                        photoToEdgePairs,
                                        TEST_GRAPH_PATH + "/photo_to_edge.fb");
    }

private:
    auth::TvmtoolRecipeHelper tvmtoolRecipeHelper_;
    wiki::unittest::ConfigFileHolder mapsproConfig_;
};

enum class FbExport { No, Yes };
constexpr auto PUBLIC = db::FeaturePrivacy::Public;
constexpr auto RESTRICTED = db::FeaturePrivacy::Restricted;
constexpr auto SECRET = db::FeaturePrivacy::Secret;

inline void setUserPrivacy(blackbox_client::Uid uid, db::FeaturePrivacy privacy)
{
    auto mockPermissionChecker = makeMockPermissionChecker();
    ON_CALL(*mockPermissionChecker, userMayViewPhotos(testing::_, testing::_))
        .WillByDefault(testing::Return(false));
    ON_CALL(*mockPermissionChecker, userMayViewPhotos(testing::_, PUBLIC))
        .WillByDefault(testing::Return(true));
    for (auto photoPrivacy : {PUBLIC, RESTRICTED, SECRET}) {
        ON_CALL(*mockPermissionChecker,
                userMayViewPhotos(std::make_optional(uid), photoPrivacy))
            .WillByDefault(testing::Return(privacy >= photoPrivacy));
    }
    Configuration::instance()->swapPermissionChecker(
        std::move(mockPermissionChecker));
}

inline void setRegionPrivacy(db::FeaturePrivacy privacy)
{
    auto mockRegionPrivacy = privacy::makeMockRegionPrivacy();
    ON_CALL(*mockRegionPrivacy,
            evalMinFeaturePrivacy(
                testing::Matcher<const geolib3::BoundingBox&>(testing::_)))
        .WillByDefault(testing::Return(privacy));
    Configuration::instance()->setRegionPrivacy(std::move(mockRegionPrivacy));
}

inline void waitTillAllDatasetsLoaded()
{
    INFO() << "Waiting till all datasets are loaded";
    while(!Configuration::instance()->areAllDatasetsInTargetState()) {
        std::this_thread::sleep_for(std::chrono::milliseconds(100));
    }
    INFO() << "All datasets loaded: " << Configuration::instance()->areAllDatasetsInTargetState();
}

template <FbExport FB_EXPORT,
          db::FeaturePrivacy USER_PRIVACY,
          db::FeaturePrivacy REGION_PRIVACY>
class BaseFixture : public NUnitTest::TBaseFixture {
public:
    static constexpr blackbox_client::Uid UID = 1;
    static constexpr auto RegionPrivacy = REGION_PRIVACY;

    explicit BaseFixture(std::function<void(pgpool3::Pool& pool)> makeData = nullptr)
    {
        clearData();
        if (makeData) {
            makeData(pgPool());
        }
        else {
            insertData();
            setFeaturesPrivacy(pgPool(), REGION_PRIVACY);
            Playground::instance().makeCoverage();
        }
        auto configuration = Playground::instance().makeConfiguration(
            FB_EXPORT == FbExport::Yes ? std::optional{exportToFb()}
                                       : std::nullopt);
        Configuration::swap(configuration);
        setUserPrivacy(UID, USER_PRIVACY);
        setRegionPrivacy(REGION_PRIVACY);
        waitTillAllDatasetsLoaded();
    }

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

    common::SharedPool sharedPool()
    {
        return Playground::instance().sharedPool();
    }

    maps::pgpool3::TransactionHandle txnHandle()
    {
        return pgPool().masterWriteableTransaction();
    }

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

    void insertData()
    {
        {
            auto txn = txnHandle();
            txn->exec(maps::common::readFileToString(SRC_("data.sql")));
            txn->commit();
        }

        auto mds = Playground::instance().config().makeMdsClient();
        auto features = db::FeatureGateway{*txnHandle()}.load();
        for (auto& feature : features) {
            auto blob = feature.dataset() == db::Dataset::TaxiSignalQ2
                ? testSignalqImageBlob()
                : testImageBlob();
            feature.setMdsKey(
                mds.post(std::to_string(feature.id()), blob).key());

            // some datasets have limited accuracy
            auto pos = feature.geodeticPos();
            feature.setGeodeticPos(geolib3::Point2(
                static_cast<float>(pos.x()), static_cast<float>(pos.y())));
            feature.setHeading(geolib3::Heading(
                static_cast<short>(feature.heading().value())));
        }

        {
            auto txn = txnHandle();
            db::FeatureGateway{*txn}.update(features, db::UpdateFeatureTxn::No);
            txn->commit();
        }
    }

    static mrc::common::Blob testImageBlob()
    {
        static mrc::common::Blob imageBlob
            = maps::common::readFileToString(SRC_("fisheye.jpg"));
        return imageBlob;
    }

    static mrc::common::Blob testSignalqImageBlob()
    {
        static mrc::common::Blob imageBlob
            = maps::common::readFileToString(SRC_("signalq.jpg"));
        return imageBlob;
    }

    yacare::tests::UserIdHeaderFixture yacareUserIdFeaderFixture;

    std::string exportToFb()
    {
        std::string outputMrcPath = GetWorkPath() + "/mrc_export" + std::to_string(++mrcExportCnt_);
        std::filesystem::remove_all(outputMrcPath);
        std::filesystem::create_directories(outputMrcPath);

        std::string outputFeaturesSecretPath = GetWorkPath() + "/mrc_features_secret" + std::to_string(++mrcExportCnt_);
        std::filesystem::remove_all(outputFeaturesSecretPath);
        std::filesystem::create_directories(outputFeaturesSecretPath);

        export_gen::generateExport(
                pgPool(),
                outputMrcPath,
                outputFeaturesSecretPath,
                fb::makeVersion(std::chrono::system_clock::now()));
        return outputMrcPath;
    }

private:

    size_t mrcExportCnt_{};
};

template <class TFixture>
struct MockedGraphFixtureImpl : public TFixture {

    MockedGraphFixtureImpl(
        std::function<void(pgpool3::Pool& pool)> makeData = nullptr)
    : TFixture(makeData)
    , graph(TEST_GRAPH_PATH + "/road_graph.fb")
    , rtree(TEST_GRAPH_PATH + "/rtree.fb", graph)
    , persistentIndex(TEST_GRAPH_PATH + "/edges_persistent_index.fb")
    {
        roadGraphMock = makeMockGraph();
        pedestrianGraphMock = makeMockGraph();
        roadPhotoToEdgeMock = std::make_shared<MockPhotoToEdge>();
        pedestrianPhotoToEdgeMock = std::make_shared<MockPhotoToEdge>();

        Configuration::instance()->setRoadGraph(roadGraphMock);
        Configuration::instance()->setPedestrianGraph(pedestrianGraphMock);
        Configuration::instance()->setRoadPhotoToEdge(roadPhotoToEdgeMock);
        Configuration::instance()->setPedestrianPhotoToEdge(pedestrianPhotoToEdgeMock);
    }

    std::shared_ptr<MockGraph> makeMockGraph()
    {
        auto graphPtr = std::make_shared<MockGraph>();

        ON_CALL(*graphPtr, graph())
                .WillByDefault(testing::ReturnRef(graph));
        ON_CALL(*graphPtr, rtree())
                .WillByDefault(testing::ReturnRef(rtree));
        ON_CALL(*graphPtr, persistentIndex())
                .WillByDefault(testing::ReturnRef(persistentIndex));
        return graphPtr;
    }

    road_graph::Graph graph;
    succinct_rtree::Rtree rtree;
    road_graph::PersistentIndex persistentIndex;

    std::shared_ptr<MockGraph> roadGraphMock;
    std::shared_ptr<MockGraph> pedestrianGraphMock;
    std::shared_ptr<MockPhotoToEdge> roadPhotoToEdgeMock;
    std::shared_ptr<MockPhotoToEdge> pedestrianPhotoToEdgeMock;
};

template <bool UserMayViewPhotos>
using UserMayViewPhotosFixture =
    BaseFixture<FbExport::No, UserMayViewPhotos ? SECRET : PUBLIC, PUBLIC>;

using Fixture = UserMayViewPhotosFixture<true>;
using FixtureFb = BaseFixture<FbExport::Yes, SECRET, PUBLIC>;
using MockedGraphFixture = MockedGraphFixtureImpl<Fixture>;
using MockedGraphFixtureFb = MockedGraphFixtureImpl<FixtureFb>;
using RegularUserAccessSecretRegionFixture = BaseFixture<FbExport::No, PUBLIC, SECRET>;
using OutsourcerUserAccessSecretRegionFixture = BaseFixture<FbExport::No, RESTRICTED, SECRET>;
using TrustedUserAccessSecretRegionFixture = BaseFixture<FbExport::No, SECRET, SECRET>;
using RegularUserAccessRestrictedRegionFixture = BaseFixture<FbExport::No, PUBLIC, RESTRICTED>;
using OutsourcerUserAccessRestrictedRegionFixture = BaseFixture<FbExport::No, RESTRICTED, RESTRICTED>;
using TrustedUserAccessRestrictedRegionFixture = BaseFixture<FbExport::No, SECRET, RESTRICTED>;
using RegularUserAccessPublicRegionFixture = BaseFixture<FbExport::No, PUBLIC, PUBLIC>;
using OutsourcerUserAccessPublicRegionFixture = BaseFixture<FbExport::No, RESTRICTED, PUBLIC>;
using TrustedUserAccessPublicRegionFixture = BaseFixture<FbExport::No, SECRET, PUBLIC>;
using RegularUserAccessSecretRegionFixtureFb = BaseFixture<FbExport::Yes, PUBLIC, SECRET>;
using TrustedUserAccessSecretRegionFixtureFb = BaseFixture<FbExport::Yes, SECRET, SECRET>;
using RegularUserAccessPublicRegionFixtureFb = BaseFixture<FbExport::Yes, PUBLIC, PUBLIC>;

} // namespace maps::mrc::browser::tests
