#include <maps/wikimap/mapspro/tools/ymapsdf-conversion/ymapsdf2json/lib/ymapsdf2json.h>
#include <maps/wikimap/mapspro/tools/ymapsdf-conversion/ymapsdf2json/lib/id_manager.h>
#include <maps/wikimap/mapspro/tools/ymapsdf-conversion/ymapsdf2json/lib/categories.h>
#include <maps/wikimap/mapspro/tools/ymapsdf-conversion/ymapsdf2json/lib/common.h>
#include <maps/wikimap/mapspro/tools/ymapsdf-conversion/ymapsdf2json/lib/magic_strings.h>

#include <maps/libs/common/include/file_utils.h>
#include <maps/libs/json/include/value.h>
#include <maps/libs/local_postgres/include/instance.h>

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

#include <pqxx/pqxx>

#include <algorithm>
#include <sstream>
#include <string>
#include <iostream>
#include <fstream>
#include <cstdlib>
#include <cctype>

using namespace maps;

const std::string SCHEMA =
    "ymapsdf2json";

namespace maps {
extern const Category& AD_CATEGORY;
}

void
prettifyJson(std::istream& in, std::ostream& out)
{
    size_t nesting = 0;
    size_t spaces  = 0;

    auto newline = [&out, &nesting, &spaces]
    {
        if (!spaces) {
            out << '\n';
        }
        spaces = nesting * 4;
    };
    auto dump = [&out, &spaces](char c1, char c2 = '\0')
    {
        if (spaces) {
            out << std::string(spaces, ' ');
            spaces = 0;
        }
        out << c1;
        if (c2 != '\0') {
            out << c2;
        }
    };
    auto space = [&out] { out << ' '; };

    char c;
    while (in.get(c)) {
        if (c == '{' || c == '[') {
            ++nesting;
        } else if (c == '}' || c == ']') {
            --nesting;
        }
        if (c == '}' || c == ']')
            newline();
        dump(c);

        if (c == '"' || c == '\'') {
            char quote = c;
            while (in.get(c)) {
                if (c == '\\') {
                    in.get(c);
                    dump('\\', c);
                } else {
                    dump(c);
                    if (c == quote)
                        break;
                }
            }
            continue;
        }

        if (c == '{' || c == '[')
            newline();
        if (c == ',') {
            newline();
        } else if (c == ':') {
            space();
        }
    }
}

std::string
prettifyJson(const std::string& originalJson)
{
    std::istringstream in(originalJson);
    std::ostringstream out;
    prettifyJson(in, out);
    return out.str();
}

void writeJson(const Category& category,
    const std::vector<DBID>& ids,
    pqxx::work& work, json::ObjectBuilder builder)
{
    std::vector<DBID> sortedIds = ids;
    std::sort(sortedIds.begin(), sortedIds.end());
    registerIds(category.table(), sortedIds);
    size_t batchSize = 100;
    completeIdRegistration(work, batchSize);
    auto query = expandSqlTemplate(category.loadRowsSqlTemplate(), ids);
    query += " ORDER BY id ASC";
    auto result(workExec(work, query));
    for (const auto& row: result) {
        std::stringstream stream;
        json::Builder bodyBuilder{stream};
        bodyBuilder << [&](json::ObjectBuilder objectBuilder) {
            category.tupleToJson(objectBuilder, row);
        };
        auto id = convertId(category.table(), row.at(ID).as<DBID>());
        builder[id] = json::Verbatim(stream.str());
    }
}

void renewSchema(pqxx::work& tr,
                 const std::string& schemaName,
                 const std::string& schemaSql)
{
    std::ostringstream query;
    if (!schemaSql.empty()) {
        query << "DROP SCHEMA IF EXISTS " << schemaName << " CASCADE;";
        query << "CREATE SCHEMA " << schemaName << ";";
    }
    query << "SET search_path to " << schemaName << ",public;";
    query << schemaSql;
    tr.exec(query.str());
}

std::string readTestFileToString(const std::string& filePath)
{
    return maps::common::readFileToString(
        ArcadiaSourceRoot()
            + "/maps/wikimap/mapspro/tools/ymapsdf-conversion/ymapsdf2json/tests/"
            + filePath);
}

void loadData(pqxx::work& tr, const std::string& dataFilePath)
{
    renewSchema(tr, SCHEMA,
        maps::common::readFileToString(
            BinaryPath("maps/doc/schemas/ymapsdf/package/ymapsdf_import_partial.sql")));
    tr.exec(readTestFileToString(dataFilePath));
}

bool compareIgnoreSpaces(std::string a, std::string b)
{
    a.erase(std::remove_if(a.begin(), a.end(), isspace), a.end());
    a.erase(std::remove_if(a.begin(), a.end(), iscntrl), a.end());

    b.erase(std::remove_if(b.begin(), b.end(), isspace), b.end());
    b.erase(std::remove_if(b.begin(), b.end(), iscntrl), b.end());
    return a == b;
}

void writeIdMap(pqxx::work& work, std::ostream& out)
{
    const std::string query =
        "SELECT table_name, ymapsdf_id, tds_id "
        "FROM ymapsdf_tds ORDER BY table_name, ymapsdf_id";
    auto result(workExec(work, query));
    for (const auto& row: result) {
        out << row[0].as<std::string>() << ", "
            << row[1].as<DBID>() << ", "
            << row[2].as<DBID>() << std::endl;
    }
}

Y_UNIT_TEST_SUITE(test_ymapsdf2json)
{

Y_UNIT_TEST(test_separate_categories)
{
    local_postgres::Database instance;
    instance.createExtension("postgis");

    Params params;
    params.db.connString = instance.connectionString();
    params.db.schema = SCHEMA;
    params.idMode = IdMode::Renumber;
    params.idStartFrom = 1000001;

    for (const Category& category : CATEGORIES) {
        std::cout << "test category writer: " << category.name() << std::endl;

        const std::string jsonFile = category.name() + ".json";
        const std::string jsonOutFile = category.name() + ".out.json";
        const std::string sqlFile = category.name() + ".sql";
        const std::string idMapFile = category.name() + ".idmap";

        pqxx::connection conn(instance.connectionString());
        pqxx::work writeTr(conn);
        loadData(writeTr, sqlFile);
        writeTr.commit();

        pqxx::work readTr(conn);
        setSearchPath(readTr, SCHEMA);
        std::vector<DBID> ids(loadIds(readTr, category.loadIdsSql()));
        ASSERT(!ids.empty());
        initIdManager(params);
        std::stringstream ss;
        json::Builder builder(ss);
        builder << [&](json::ObjectBuilder objectBuilder) {
            writeJson(category, ids, readTr, objectBuilder);
        };
        bool cmpRes = compareIgnoreSpaces(ss.str(), readTestFileToString(jsonFile));
        if (!cmpRes) {
            std::ofstream out(jsonOutFile);
            out << prettifyJson(ss.str());
            out.close();
        }
        std::ofstream idOut(idMapFile);
        writeIdMap(readTr, idOut);

        UNIT_ASSERT_C(cmpRes, category.name());
    }
}

Y_UNIT_TEST(test_keep_original)
{
    local_postgres::Database instance;
    instance.createExtension("postgis");

    Params params;
    params.db.connString = instance.connectionString();
    params.db.schema = SCHEMA;
    params.idMode = IdMode::KeepOriginal;

    const Category& category = maps::AD_CATEGORY;

    const std::string jsonFile = category.name() + ".original.json";
    const std::string jsonOutFile = category.name() + ".original.out.json";
    const std::string sqlFile = category.name() + ".sql";

    pqxx::connection conn(instance.connectionString());
    pqxx::work writeTr(conn);
    loadData(writeTr, sqlFile);
    writeTr.commit();

    pqxx::work readTr(conn);
    setSearchPath(readTr, SCHEMA);
    std::vector<DBID> ids(loadIds(readTr, category.loadIdsSql()));


    ASSERT(!ids.empty());
    initIdManager(params);
    std::stringstream ss;
    json::Builder builder(ss);
    builder << [&](json::ObjectBuilder objectBuilder) {
        writeJson(category, ids, readTr, objectBuilder);
    };

    UNIT_ASSERT_EQUAL(maxId(), 12);

    bool cmpRes = compareIgnoreSpaces(ss.str(), readTestFileToString(jsonFile));
    if (!cmpRes) {
        std::ofstream out(jsonOutFile);
        out<<ss.str();
        out.close();
    }
    UNIT_ASSERT(cmpRes);
}

} // Y_UNIT_TEST_SUITE
