#include <fesa/analysis/run_solver.hpp>
#include <fesa/io/hdf5/writer.hpp>

#include <cmath>
#include <cstdlib>
#include <filesystem>
#include <fstream>
#include <iterator>
#include <stdexcept>
#include <string>
#include <string_view>
#include <system_error>
#include <utility>

#include <gtest/gtest.h>

namespace {

class TemporaryPath final {
public:
    explicit TemporaryPath(std::filesystem::path path)
        : path_{std::move(path)} {
        std::error_code error;
        std::filesystem::create_directories(path_.parent_path(), error);
        if (error) {
            throw std::runtime_error{"Failed to create pipeline test directory."};
        }
        std::filesystem::remove(path_, error);
    }

    ~TemporaryPath() {
        std::error_code error;
        std::filesystem::remove(path_, error);
    }

    TemporaryPath(const TemporaryPath&) = delete;
    TemporaryPath& operator=(const TemporaryPath&) = delete;

    [[nodiscard]] const std::filesystem::path& path() const noexcept {
        return path_;
    }

private:
    std::filesystem::path path_;
};

std::filesystem::path fixture_path(const std::string_view name) {
    return std::filesystem::path{FESA_TEST_SOURCE_DIR} / "fixtures" /
           "abaqus" / name;
}

std::filesystem::path test_output_path(const std::string_view name) {
    return std::filesystem::path{FESA_TEST_BINARY_DIR} / "testing" / name;
}

void write_text(
    const std::filesystem::path& path,
    const std::string_view contents) {
    std::filesystem::create_directories(path.parent_path());
    std::ofstream output{path, std::ios::binary};
    output.write(
        contents.data(), static_cast<std::streamsize>(contents.size()));
    if (!output) {
        throw std::runtime_error{"Failed to write pipeline test input."};
    }
}

std::string quote(const std::filesystem::path& path) {
    return '"' + path.string() + '"';
}

std::string path_utf8(const std::filesystem::path& path) {
    const std::u8string value = path.u8string();
    return {reinterpret_cast<const char*>(value.data()), value.size()};
}

std::string read_text(const std::filesystem::path& path) {
    std::ifstream input{path, std::ios::binary};
    return {
        std::istreambuf_iterator<char>{input},
        std::istreambuf_iterator<char>{},
    };
}

TEST(MinimalCantileverPipeline, WritesReadableFiniteEquilibratedResults) {
    const TemporaryPath output{
        test_output_path("minimal-cantilever-pipeline.h5")};

    const fesa::AnalysisRunResult run = fesa::run_solver({
        fixture_path("minimal_cantilever.inp"),
        output.path(),
    });

    ASSERT_TRUE(run.succeeded);
    EXPECT_TRUE(run.diagnostics.empty());

    const fesa::Hdf5ReadResult read =
        fesa::read_hdf5_results(output.path());
    ASSERT_TRUE(read.database.has_value());
    ASSERT_TRUE(read.model.has_value());
    ASSERT_TRUE(read.metadata.has_value());
    EXPECT_TRUE(read.diagnostics.empty());
    EXPECT_EQ(read.database->schema_version, "2.0.0");
    EXPECT_EQ(
        read.metadata->input_source,
        path_utf8(fixture_path("minimal_cantilever.inp")));
    EXPECT_EQ(
        read.metadata->input_fingerprint,
        "fnv1a64:73f31da4615f09b3");

    ASSERT_EQ(read.model->nodes.size(), 2U);
    EXPECT_EQ(read.model->nodes[0].id, fesa::NodeId{0});
    EXPECT_EQ(read.model->nodes[1].id, fesa::NodeId{1});

    ASSERT_EQ(read.database->steps.size(), 1U);
    ASSERT_EQ(read.database->steps[0].frames.size(), 1U);
    const fesa::NodalFrame& nodal =
        read.database->steps[0].frames[0].nodal;
    ASSERT_EQ(nodal.node_ids.size(), 2U);
    ASSERT_EQ(nodal.displacement.size(), 2U);
    ASSERT_EQ(nodal.reaction.size(), 2U);
    EXPECT_EQ(nodal.node_ids[0], fesa::NodeId{0});
    EXPECT_EQ(nodal.node_ids[1], fesa::NodeId{1});

    for (const auto& values : nodal.displacement) {
        for (const double value : values) {
            EXPECT_TRUE(std::isfinite(value));
        }
    }
    for (const auto& values : nodal.reaction) {
        for (const double value : values) {
            EXPECT_TRUE(std::isfinite(value));
        }
    }

    const double total_y_reaction =
        nodal.reaction[0][1] + nodal.reaction[1][1];
    EXPECT_NEAR(total_y_reaction - 1.0, 0.0, 1.0e-12);
    EXPECT_LT(nodal.displacement[1][1], 0.0);
}

TEST(MinimalCantileverPipeline, CliPreservesFailureSourceDiagnostic) {
    const TemporaryPath input{
        test_output_path("minimal-cantilever-invalid.inp")};
    const TemporaryPath output{
        test_output_path("minimal-cantilever-invalid.h5")};
    const TemporaryPath error_output{
        test_output_path("minimal-cantilever-invalid.stderr.txt")};
    write_text(
        input.path(),
        "*NODE\n"
        "*UNSUPPORTED\n");

    const std::string command =
        '"' + quote(std::filesystem::path{FESA_CLI_PATH}) + " solve " +
        quote(input.path()) + " --output " + quote(output.path()) +
        " 1>NUL 2>" + quote(error_output.path()) + '"';
    const int exit_code = std::system(command.c_str());

    EXPECT_NE(exit_code, 0);
    EXPECT_FALSE(std::filesystem::exists(output.path()));
    const std::string diagnostic = read_text(error_output.path());
    EXPECT_NE(diagnostic.find("syntax"), std::string::npos);
    EXPECT_NE(
        diagnostic.find("abaqus.unsupported_keyword"),
        std::string::npos);
    EXPECT_NE(diagnostic.find(input.path().string()), std::string::npos);
    EXPECT_NE(diagnostic.find(":2:1"), std::string::npos);
}

}  // namespace
