/*
 * (C) Copyright 1996- ECMWF.
 *
 * This software is licensed under the terms of the Apache Licence Version 2.0
 * which can be obtained at http://www.apache.org/licenses/LICENSE-2.0.
 * In applying this licence, ECMWF does not waive the privileges and immunities
 * granted to it by virtue of its status as an intergovernmental organisation nor
 * does it submit to any jurisdiction.
 */

/*
 * This software was developed as part of the Horizon Europe programme funded project OpenCUBE
 * (Grant agreement: 101092984) horizon-opencube.eu
 */

/// @file   test_fam_common.h
/// @author Metin Cakircali
/// @date   Jun 2024

#pragma once

#include "fdb5/LibFdb5.h"
#include "fdb5/api/helpers/ListElement.h"
#include "fdb5/api/helpers/ListIterator.h"

#include "eckit/config/YAMLConfiguration.h"
#include "eckit/exception/Exceptions.h"
#include "eckit/filesystem/LocalPathName.h"
#include "eckit/filesystem/PathName.h"
#include "eckit/filesystem/TmpDir.h"
#include "eckit/filesystem/URI.h"
#include "eckit/io/DataHandle.h"
#include "eckit/log/Log.h"
#include "eckit/testing/ProcessFork.h"

#include <sys/mman.h>
#include <sys/wait.h>
#include <unistd.h>

#include <algorithm>
#include <cctype>
#include <cstdlib>
#include <cstring>
#include <fstream>
#include <iostream>
#include <ostream>
#include <regex>
#include <string>

using namespace std::string_literals;

//----------------------------------------------------------------------------------------------------------------------

#define TEST_LOG_INFO(msg) eckit::Log::info() << "INFO  [TEST_FAM] : " << msg << std::endl
#define TEST_LOG_DEBUG(msg) eckit::Log::debug<fdb5::LibFdb5>() << "DEBUG [TEST_FAM] : " << msg << std::endl

//----------------------------------------------------------------------------------------------------------------------

namespace fdb::test::fam {

/// Derives the POSIX shm name from an endpoint (host part only, matching the mock's cisServer key).
inline std::string shm_name_from_endpoint(const std::string& endpoint) {
    auto colon = endpoint.rfind(':');
    auto host = (colon != std::string::npos) ? endpoint.substr(0, colon) : endpoint;
    std::transform(host.begin(), host.end(), host.begin(),
                   [](unsigned char ch) { return std::isalnum(ch) ? static_cast<char>(ch) : '_'; });
    return "/eckit_fam_mock_" + (host.empty() ? "default" : host);
}

/// Per-process unique endpoint with atexit cleanup.
/// When ECKIT_FAM_TEST_ENDPOINT is set (real OpenFAM CIS), use it verbatim — the PID
/// mangling is only needed for the mock backend, where the endpoint string becomes a POSIX
/// shm key. With real OpenFAM, the endpoint must be a valid host:port for DNS resolution.
inline const std::string test_fdb_fam_endpoint = []() -> std::string {
    const char* ep = std::getenv("ECKIT_FAM_TEST_ENDPOINT");

    // Real OpenFAM: use the endpoint as-is.
    if (ep && *ep) {
        return ep;
    }

    // Mock OpenFAM: append "_<pid>" to produce a unique POSIX shm path per process.
    auto base = std::string("localhost:8880");
    auto colon = base.rfind(':');
    std::string endpoint;
    if (colon == std::string::npos) {
        endpoint = base + "_" + std::to_string(::getpid()) + ":0";
    }
    else {
        auto host = base.substr(0, colon);
        auto port = base.substr(colon);
        endpoint = host + "_" + std::to_string(::getpid()) + port;
    }
    static std::string shm_name = shm_name_from_endpoint(endpoint);
    std::atexit([] { ::shm_unlink(shm_name.c_str()); });
    // Export the computed endpoint so fork_and_exec'd children use the same shm segment.
    ::setenv("ECKIT_FAM_TEST_ENDPOINT", endpoint.c_str(), 1);
    return endpoint;
}();

inline void read_and_validate(eckit::DataHandle* dh, const char* data, const long length) {
    TEST_LOG_INFO("READ");

    dh->openForRead();

    std::string tmp;
    tmp.resize(length);
    char* buffer = tmp.data();

    const auto rlen = dh->read(buffer, length);

    TEST_LOG_INFO("VALIDATE");

    ASSERT(rlen == length);

    ASSERT(::memcmp(data, buffer, length) == 0);

    dh->close();
}

inline void write(const std::string& buffer, const eckit::PathName& path) {
    std::ofstream file(path);
    if (!file) {
        throw eckit::CantOpenFile(path);
    }
    file << buffer;
}

inline int count_list(fdb5::ListIterator& list, std::ostream& out = std::cout) {
    int count = 0;
    fdb5::ListElement elem;
    while (list.next(elem)) {
        elem.print(out, true, true, false, " ");
        out << '\n';
        ++count;
    }
    return count;
}

/// Default 3-level test schema shared across store and catalogue tests.
const std::string test_schema =
    "[ fam1a, fam1b, fam1c\n"
    "    [ fam2a, fam2b, fam2c\n"
    "       [ fam3a, fam3b, fam3c ]]]\n";

/// Build a YAML config string for a FAM-backed FDB test.
/// @param fam_uri    e.g. "fam://host:port/region"
/// @param catalogue  "toc" or "fam"
/// @param engine     empty for toc-based, "fam" for fam catalogue
inline std::string make_test_config(const std::string& fam_uri, const std::string& catalogue = "fam",
                                    const std::string& engine = "fam") {
    std::string config =
        "---\n"
        "type: local\n";
    if (!engine.empty()) {
        config += "engine: " + engine + "\n";
    }
    config +=
        "schema: ./schema\n"
        "store: fam\n"
        "catalogue: " +
        catalogue +
        "\n"
        "spaces:\n"
        "- handler: Default\n"
        "  roots:\n"
        "  - path: ./root\n"
        "fam_roots:\n"
        "- uri: " +
        fam_uri +
        "\n"
        "  writable: true\n"
        "  visit: true\n";
    return config;
}

struct FamSetup {
    FamSetup(const std::string& schema, std::string config) {
        eckit::LocalPathName root_dir(cwd_ + "/" + "root");
        root_dir.mkdir();
        write(schema, schemaPath);
        config = std::regex_replace(config, std::regex("./schema"), schemaPath.asString());
        config = std::regex_replace(config, std::regex("./root"), root_dir.c_str());
        write(config, configPath.asString());
    }

    eckit::TmpDir cwd_{eckit::LocalPathName::cwd().c_str()};

    eckit::PathName schemaPath{cwd_ + "/" + "schema"};
    eckit::PathName configPath{cwd_ + "/" + "config.yaml"};
};

//----------------------------------------------------------------------------------------------------------------------

}  // namespace fdb::test::fam

//----------------------------------------------------------------------------------------------------------------------

namespace fdb::test {

using eckit::testing::fork_and_exec;
using eckit::testing::get_worker_arg;
using eckit::testing::parse_worker_args;

}  // namespace fdb::test
