#include "helpers.h"

#include "../lib/gdal_helpers.h"
#include "../lib/message_reporter.h"
#include "../lib/dispatch.h"

#include <yandex/maps/wiki/tasks/task_logger.h>
#include <yandex/maps/wiki/common/geom.h>
#include <yandex/maps/wiki/common/string_utils.h>
#include <yandex/maps/wiki/revision/filters.h>
#include <yandex/maps/wiki/revision/commit_manager.h>
#include <yandex/maps/wiki/revision/branch_manager.h>

#include <contrib/restricted/boost/boost/filesystem.hpp>

#include <filesystem>
#include <sstream>

namespace fs = std::filesystem;
namespace rev = maps::wiki::revision;
namespace rf = maps::wiki::revision::filters;

namespace maps {
namespace wiki {
namespace importer {

namespace {

const size_t INDENT_OFFSET = 4;

size_t totalRevisionCount(const TestObject& object)
{
    size_t count = 1;
    for (const auto& relation : object.slaveRelations) {
        count += 1 + totalRevisionCount(relation.slaveObject);
    }
    return count;
}

} // namespace

fs::path dataPath(const std::string& relativeFilepath)
{
    return std::string(ArcadiaSourceRoot()
        + "/maps/wikimap/mapspro/services/tasks_misc/src/import_worker/tests/data/"
        + relativeFilepath);
}

void checkMessageReporter(const MessageReporter& messageReporter, size_t expectedWarningCount)
{
    if (messageReporter.hasErrors() ||
            messageReporter.size() != expectedWarningCount) {
        for (const auto& message : messageReporter.messages()) {
            UNIT_FAIL_NONFATAL(message.text());
        }
    }
}

RevisionTest::RevisionTest(pgpool3::Pool& pool)
    : txn_(pool.masterReadOnlyTransaction())
    , gateway_(*txn_)
    , snapshot_(gateway_.snapshot(gateway_.headCommitId()))
{
}

void RevisionTest::testObjects(const std::vector<TestObject>& objects, size_t expectedRevisionCount)
{
    if (objects.empty()) {
        auto revIds = snapshot_.revisionIdsByFilter(rf::ObjRevAttr::isNotDeleted());
        UNIT_ASSERT(revIds.empty());
        return;
    }

    size_t revisionCount = 0;
    std::set<std::string> topLevelCategories;
    for (const auto& object : objects) {
        revisionCount += totalRevisionCount(object);
        topLevelCategories.emplace("cat:" + extractCategory(object.attributes));
    }
    if (expectedRevisionCount == USE_COMPUTED_REVISION_COUNT) {
        expectedRevisionCount = revisionCount;
    }

    auto filter = rf::Attr::definedAny(topLevelCategories)
        && rf::ObjRevAttr::isNotDeleted();
    auto revs = snapshot_.objectRevisionsByFilter(filter);
    UNIT_ASSERT(!revs.empty());
    for (const auto& revision : revs) {
        if (!tryMatch(revision, objects)) {
            std::ostringstream stream;
            stream << "Failed to match revision:\n";
            printObject(stream, 0, revision);
            UNIT_FAIL_NONFATAL(stream.str());
        }
    }

    for (const auto& object : objects) {
        if (!tryMatch(revs, object)) {
            std::ostringstream stream;
            stream << "Failed to match object:\n";
            printObject(stream, 0, object);
            UNIT_FAIL_NONFATAL(stream.str());
        }
    }

    auto revIds = snapshot_.revisionIdsByFilter(rf::ObjRevAttr::isNotDeleted());
    UNIT_ASSERT_VALUES_EQUAL(revIds.size(), expectedRevisionCount);
}

bool RevisionTest::tryMatch(const rev::ObjectRevision& revision, const TestObject& object)
{
    UNIT_ASSERT(revision.data().attributes);
    if (object.attributes != *revision.data().attributes) {
        return false;
    }

    auto revs = snapshot_.loadSlaveRelations(revision.id().objectId());
    if (revs.size() != object.slaveRelations.size()) {
        return false;
    }

    for (const auto& revision : revs) {
        if (!tryMatch(revision, object.slaveRelations)) {
            return false;
        }
    }

    if (object.geometryWkt &&
        *object.geometryWkt != common::wkb2wkt(*revision.data().geometry)) {
            return false;
    }

    return true;
}

bool RevisionTest::tryMatch(const rev::ObjectRevision& revision, const std::vector<TestObject>& objects)
{
    for (const auto& object : objects) {
        if (tryMatch(revision, object)) {
            return true;
        }
    }
    return false;
}

bool RevisionTest::tryMatch(const rev::ObjectRevision& revision, const TestRelation& relation)
{
    UNIT_ASSERT(revision.data().attributes);
    if (relation.attributes != *revision.data().attributes) {
        return false;
    }

    UNIT_ASSERT(revision.data().relationData);
    auto slaveRevision = snapshot_.objectRevision(revision.data().relationData->slaveObjectId());
    if (!slaveRevision) {
        return false;
    }

    return tryMatch(*slaveRevision, relation.slaveObject);
}

bool RevisionTest::tryMatch(const rev::ObjectRevision& revision, const std::vector<TestRelation>& relations)
{
    return std::any_of(
        relations.begin(),
        relations.end(),
        [&](const TestRelation& relation) {
            return tryMatch(revision, relation);
        });
}

bool RevisionTest::tryMatch(const revision::Revisions& revisions, const TestObject& object)
{
    return std::any_of(
        revisions.begin(),
        revisions.end(),
        [&](const rev::ObjectRevision& revision) {
            return tryMatch(revision, object);
        });
}

void RevisionTest::printAttributes(std::ostream& stream, size_t indent, const StringMap& attributes)
{
    auto str = common::join(attributes,
        [](const StringMap::value_type& pair)
        {
            return "(" + pair.first + "=" + pair.second + ")";
        },
        ',');

    stream << std::string(indent, ' ') << str << std::endl;
}

void RevisionTest::printObject(std::ostream& stream, size_t indent, const rev::ObjectRevision& revision)
{
    stream << "id=" << revision.id() << " ";
    printAttributes(stream, indent, *revision.data().attributes);

    auto revs = snapshot_.loadSlaveRelations(revision.id().objectId());
    for (const auto& revision : revs) {
        printRelation(stream, indent + INDENT_OFFSET, revision);
    }

    if (revision.data().geometry) {
        printGeometry(stream, indent + INDENT_OFFSET, revision);
    }
}

void RevisionTest::printRelation(std::ostream& stream, size_t indent, const rev::ObjectRevision& revision)
{
    printAttributes(stream, indent, *revision.data().attributes);

    auto slaveRevision = snapshot_.objectRevision(revision.data().relationData->slaveObjectId());
    printObject(stream, indent + INDENT_OFFSET, *slaveRevision);
}

void RevisionTest::printGeometry(std::ostream& stream, size_t indent, const rev::ObjectRevision& revision)
{
    stream << std::string(indent, ' ') << "geom=" << common::wkb2wkt(*revision.data().geometry) << std::endl;
}

void RevisionTest::printObject(std::ostream& stream, size_t indent, const TestObject& object)
{
    printAttributes(stream, indent, object.attributes);

    for (const auto& relation : object.slaveRelations) {
        printRelation(stream, indent + INDENT_OFFSET, relation);
    }

    if (object.geometryWkt) {
        printGeometry(stream, indent + INDENT_OFFSET, object);
    }
}

void RevisionTest::printRelation(std::ostream& stream, size_t indent, const TestRelation& relation)
{
    printAttributes(stream, indent, relation.attributes);
    printObject(stream, indent + INDENT_OFFSET, relation.slaveObject);
}

void RevisionTest::printGeometry(std::ostream& stream, size_t indent, const TestObject& object)
{
    stream << std::string(indent, ' ') << "geom=" << *object.geometryWkt << std::endl;
}

void RandomDbFixture::performAction(
    Action action,
    const fs::path& dataDir,
    size_t expectedWarningCount,
    size_t skippedObjectCount,
    CheckCommits checkCommits,
    ApproveCommits approveCommits)
{
    try {
        performActionImpl(
            action,
            dataDir,
            expectedWarningCount,
            skippedObjectCount,
            checkCommits,
            approveCommits);
    } catch (const maps::Exception& e) {
        ERROR() << e;
        throw;
    }
}

void RandomDbFixture::performActionImpl(
    Action action,
    const fs::path& dataDir,
    size_t expectedWarningCount,
    size_t skippedObjectCount,
    CheckCommits checkCommits,
    ApproveCommits approveCommits)
{
    UNIT_ASSERT(expectedWarningCount >= skippedObjectCount); //TODO: BOOST_CHECK_GE

    common::ExtendedXmlDoc configXml(SERVICES_CONFIG_PATH);

    // TODO(HEREBEDRAGONS-232): Replace with analogue for std::filesystem.
    fs::path workDir = fs::temp_directory_path() / boost::filesystem::unique_path().string();
    fs::create_directories(workDir);

    TaskParams params(
        TEST_TASK_ID,
        TEST_UID,
        action,
        configXml,
        BinaryPath("maps/wikimap/mapspro/cfg/editor/editor.xml"),
        workDir,
        pool(),
        pool());

    tasks::TaskPgLogger logger(params.corePool, params.taskId);

    MessageReporter messageReporter;
    ObjectsCache cache;

    auto objects = gdal2objects(
        dataDir,
        params.action,
        cache,
        params.editorConfig,
        params.importConfig,
        messageReporter);
    checkMessageReporter(messageReporter);

    auto result = dispatchObjects(cache, params, logger, messageReporter);

    checkMessageReporter(messageReporter, expectedWarningCount);
    if (checkCommits == CheckCommits::Yes) {
        UNIT_ASSERT(!result.commitIds.empty());
    }
    UNIT_ASSERT_VALUES_EQUAL(result.skippedObjectIds.size(), skippedObjectCount);

    if (messageReporter.hasErrors()) {
        return;
    }

    if (approveCommits == ApproveCommits::Yes) {
        revision::BranchManager branchManager(*params.mainTxn);
        auto branches = branchManager.load({{revision::BranchType::Approved, 1}});
        if (branches.empty()) {
            branchManager.createApproved(TEST_UID, {});
        }

        revision::CommitManager commitManager(*params.mainTxn);
        commitManager.approve(result.commitIds);
    }

    params.mainTxn->commit();
}

} // namespace importer
} // namespace wiki
} // namespace maps
