#include "helpers.h"

#include <maps/wikimap/mapspro/services/editor/src/utils.h>
#include <maps/wikimap/mapspro/services/editor/src/configs/config.h>
#include <maps/wikimap/mapspro/services/editor/src/revisions_facade.h>
#include <maps/wikimap/mapspro/services/editor/src/objects_cache.h>
#include <maps/wikimap/mapspro/services/editor/src/revisions_facade.h>

#include "tests_common.h"

#include <yandex/maps/wiki/common/string_utils.h>
#include <maps/libs/common/include/file_utils.h>
#include <maps/libs/common/include/exception.h>
#include <maps/libs/json/include/value.h>

#include <library/cpp/testing/unittest/env.h>
#include <library/cpp/testing/unittest/registar.h>
#include <library/cpp/testing/gmock_in_unittest/gmock.h>

#include <cstdio>
#include <fstream>
#include <iostream>
#include <sstream>
#include <stdexcept>
#include <random>
#include <mutex>

namespace maps {
namespace wiki {
namespace tests {

namespace {

const size_t TMP_FILE_POSTFIX_LENGH = 10;

const std::string HEX_CHARS = "abcdefABCDEF0123456789";
const std::string JSON_VALIDATOR = BinaryPath("maps/tools/json-validator/json-validator");
const std::string XMLLINT_PATH= BinaryPath("contrib/tools/xmllint/xmllint");

std::string
randomHexString(size_t length)
{
    std::string result;

    std::random_device rd;
    std::default_random_engine randomEngine(rd());
    std::uniform_int_distribution<size_t> uniformDist(0, HEX_CHARS.size() - 1);

    for (size_t i = 0; i < length; ++i) {
        result += HEX_CHARS[uniformDist(randomEngine)];
    }

    return result;
}

class TemporaryFile
{
public:
    TemporaryFile();
    ~TemporaryFile();

    const std::string& name() const { return name_; }

    void write(const std::string& data);
    std::string read() const;

private:
    std::string name_;
};

TemporaryFile::TemporaryFile()
{
    while (true) {
        name_ = cfg()->tmpDir() + "/editor_tmp_" + randomHexString(TMP_FILE_POSTFIX_LENGH);

        if (!std::ifstream(name_)) {
            break;
        }
    }
}

TemporaryFile::~TemporaryFile()
{
    std::remove(name_.c_str());
}

void
TemporaryFile::write(const std::string& data)
{
    std::ofstream output(name_, std::ios::binary);
    output << data;
}

std::string
TemporaryFile::read() const
{
    std::ifstream input(name_, std::ios::binary);
    std::stringstream ss;
    ss << input.rdbuf();
    return ss.str();
}

} // namespace

std::string
loadFile(const std::string& path)
{
    return maps::common::readFileToString(ArcadiaSourceRoot() + "/maps/wikimap/mapspro/services/editor/src/" + path);
}

std::string
prettifyXml(const std::string& originalXml)
{
    TemporaryFile tmp;
    tmp.write(originalXml);

    ReadingPipe pipe(XMLLINT_PATH + " --format " + tmp.name());
    return pipe.read();
}

namespace {

const std::string JSON_SCHEMAS_DIR = ArcadiaSourceRoot() + "/maps/wikimap/mapspro/schemas/editor/json/";
const std::string XML_SCHEMA = ArcadiaSourceRoot() + "/maps/wikimap/mapspro/schemas/editor/editor.xsd";

const std::string JSON_EDITOR_API = "editor.api.json";
const std::string JSON_SOCIAL_API = "social.api.json";

typedef std::map<std::string, std::string> TaskNameToSchemaName;

std::unique_ptr<TaskNameToSchemaName> g_taskNameToJsonRequestSchema;
std::unique_ptr<TaskNameToSchemaName> g_taskNameToJsonResponseSchema;

const TaskNameToSchemaName& jsonRequestSchemas() {
    ASSERT(g_taskNameToJsonRequestSchema);
    return *g_taskNameToJsonRequestSchema;
}

const TaskNameToSchemaName& jsonResponseSchemas() {
    ASSERT(g_taskNameToJsonResponseSchema);
    return *g_taskNameToJsonResponseSchema;
}

void
validateJson(
    const std::string& json, const std::string& taskName,
    const TaskNameToSchemaName& schemaFiles)
{
    const auto it = schemaFiles.find(taskName);
    REQUIRE(
        it != schemaFiles.end(),
        "There is no schema file for task: '" << taskName << "'"
    );

    auto schemaPath = JSON_SCHEMAS_DIR + schemaFiles.at(taskName);
    auto command = JSON_VALIDATOR + " -s " + schemaPath + " 1>/dev/null";

    FILE *in;

    REQUIRE((in = popen(command.c_str(), "w")), "Could not popen for validation");
    fprintf(in, "%s", json.c_str());

    int exitStatus = pclose(in);
    REQUIRE(!exitStatus, "Validation with " << schemaPath << " failed with exit code " << exitStatus << " json:\n" << json);
}

void initJsonSchema(const std::string& name)
{
    ASSERT(g_taskNameToJsonRequestSchema && g_taskNameToJsonResponseSchema);

    json::Value json = json::Value::fromFile(name);
    for (const auto& api: json["apis"]) {
        for (const auto& operation: api["operations"]) {
            const std::string name = operation["nickname"].as<std::string>();

            const auto& responseType = operation["type"];
            if (responseType.isObject()) {
                g_taskNameToJsonResponseSchema->insert(
                    {name, responseType["$ref"].toString()}
                );
            }

            if (operation["method"].toString() != "POST") {
                continue;
            }
            for (const auto& parametr: operation["parameters"]) {
                if (parametr["paramType"].toString() != "body") {
                    continue;
                }

                const auto& requestType = parametr["type"];
                if (requestType.isObject()) {
                    g_taskNameToJsonRequestSchema->insert(
                        {name, requestType["$ref"].toString()}
                    );
                }
                break;
            }
        }
    }
}

void initJsonSchemasStorage()
{
    g_taskNameToJsonRequestSchema.reset(new TaskNameToSchemaName());
    g_taskNameToJsonResponseSchema.reset(new TaskNameToSchemaName());

    initJsonSchema(JSON_SCHEMAS_DIR + JSON_EDITOR_API);
    initJsonSchema(JSON_SCHEMAS_DIR + JSON_SOCIAL_API);
}
} // namespace

std::once_flag schemasLoaded;
void initJsonSchemas()
{
    std::call_once(schemasLoaded, initJsonSchemasStorage);
}

void
validateJsonRequest(const std::string& json, const std::string& taskName)
{
    validateJson(json, taskName, jsonRequestSchemas());
}

void
validateJsonResponse(const std::string& json, const std::string& taskName)
{
    validateJson(json, taskName, jsonResponseSchemas());
}

void
validateXmlResponse(const std::string& xml)
{
    auto command = XMLLINT_PATH + " --schema " + XML_SCHEMA + " --noout -";

    FILE *in;
    REQUIRE((in = popen(command.c_str(), "w")), "Could not popen for validation");
    fprintf(in, "%s", xml.c_str());

    int exitStatus = pclose(in);
    REQUIRE(!exitStatus, "Validation failed with exit code " << exitStatus);
}

std::string toString(const std::vector<TOid>& ids)
{
    return "[" + common::join(ids, ", ") + "]";
}

std::string toString(const revision::RevisionID& revisionId) {
    std::stringstream out;
    out << revisionId;
    return out.str();
}

void toIdArray(json::ArrayBuilder array, std::initializer_list<TOid> ids)
{
    for (const auto& id: ids) {
        array << std::to_string(id);
    }
}

ObjectPtr
getObject(const std::string& categoryId)
{
    auto branchCtx = BranchContextFacade::acquireRead(
        0, "");
    ObjectsCache cache(branchCtx, boost::none);
    auto revs = cache.revisionsFacade().snapshot().revisionIdsByFilter(
    revision::filters::Attr("cat:" + categoryId).defined());
    EXPECT_TRUE(revs.size() < 2);
    if (revs.empty()) {
        return ObjectPtr();
    }
    auto id  = revs.begin()->objectId();
    return cache.getExisting(id);
}

TOIds objectIdsByCategory(const ObjectsCache& cache, const std::string& categoryName)
{
    auto revisions = cache.revisionsFacade().snapshot().revisionIdsByFilter(
        revision::filters::Attr("cat:" + categoryName).defined());
    TOIds result;
    for (const auto& revision: revisions) {
        result.insert(revision.objectId());
    }
    return result;
}


} // namespace tests
} // namespace wiki
} // namespace maps

