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

#include <gtest/gtest.h>

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

namespace {

constexpr double kEquilibriumRelativeTolerance = 2.0e-13;
constexpr double kEquilibriumAbsoluteTolerance = 1.0e-12;
constexpr std::string_view kInstanceName = "PART-1_1-1";

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 reference 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 reference_path(const std::string_view name) {
    return std::filesystem::path{FESA_REPOSITORY_ROOT} / "reference" /
           "cantilever beam" / name;
}

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

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

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>{},
    };
}

const fesa::Vec3& coordinates_for(
    const fesa::Hdf5ModelSnapshot& model,
    const fesa::NodeId node_id) {
    const auto found = std::ranges::find_if(
        model.nodes,
        [node_id](const fesa::Hdf5NodeSnapshot& node) {
            return node.id == node_id;
        });
    if (found == model.nodes.end()) {
        throw std::runtime_error{
            "Result or load references an unknown node ID."};
    }
    return found->coordinates;
}

void add_nodal_resultant(
    std::array<double, 3>& force,
    std::array<double, 3>& moment,
    const fesa::Vec3& position,
    const std::array<double, 6>& values) {
    force[0] += values[0];
    force[1] += values[1];
    force[2] += values[2];
    moment[0] += values[3] + position.y * values[2] -
                 position.z * values[1];
    moment[1] += values[4] + position.z * values[0] -
                 position.x * values[2];
    moment[2] += values[5] + position.x * values[1] -
                 position.y * values[0];
}

void expect_finite(const fesa::ResultFrame& frame) {
    for (const auto& displacement : frame.nodal.displacement) {
        for (const double value : displacement) {
            EXPECT_TRUE(std::isfinite(value));
        }
    }
    for (const auto& reaction : frame.nodal.reaction) {
        for (const double value : reaction) {
            EXPECT_TRUE(std::isfinite(value));
        }
    }
    for (const fesa::BeamElementFrame& beam : frame.element.beams) {
        EXPECT_TRUE(fesa::is_finite(beam.local_frame.ex));
        EXPECT_TRUE(fesa::is_finite(beam.local_frame.ey));
        EXPECT_TRUE(fesa::is_finite(beam.local_frame.ez));
        for (const fesa::BeamSectionResult& end : beam.end_results) {
            EXPECT_TRUE(std::isfinite(end.xi));
            EXPECT_TRUE(std::isfinite(end.centroid_sigma_xx));
            for (const double value : end.section_strain) {
                EXPECT_TRUE(std::isfinite(value));
            }
            for (const double value : end.section_force) {
                EXPECT_TRUE(std::isfinite(value));
            }
            for (const double value : end.sigma_xx) {
                EXPECT_TRUE(std::isfinite(value));
            }
        }
    }
}

TEST(CantileverReference, CorrelatesAllAvailableAbaqusResults) {
    const TemporaryPath results{
        test_output_path("cantilever-reference.h5")};
    const TemporaryPath standard_output{
        test_output_path("cantilever-reference-compare.stdout.txt")};
    const TemporaryPath error_output{
        test_output_path("cantilever-reference-compare.stderr.txt")};

    const fesa::AnalysisRunResult run = fesa::run_solver({
        reference_path("cantilever beam fesa.inp"),
        results.path(),
    });
    ASSERT_TRUE(run.succeeded);
    ASSERT_TRUE(run.diagnostics.empty());

    const fesa::Hdf5ReadResult read =
        fesa::read_hdf5_results(results.path());
    ASSERT_TRUE(read.diagnostics.empty());
    ASSERT_TRUE(read.database.has_value());
    ASSERT_TRUE(read.model.has_value());
    ASSERT_TRUE(read.analysis.has_value());
    ASSERT_EQ(read.database->steps.size(), 1U);
    ASSERT_EQ(read.database->steps[0].frames.size(), 1U);

    const fesa::ResultFrame& frame =
        read.database->steps[0].frames[0];
    expect_finite(frame);

    std::array<double, 3> total_force{};
    std::array<double, 3> total_moment{};
    std::array<double, 3> applied_force{};
    std::array<double, 3> applied_moment{};
    ASSERT_EQ(frame.nodal.node_ids.size(), frame.nodal.reaction.size());
    for (std::size_t index = 0; index < frame.nodal.node_ids.size(); ++index) {
        add_nodal_resultant(
            total_force,
            total_moment,
            coordinates_for(*read.model, frame.nodal.node_ids[index]),
            frame.nodal.reaction[index]);
    }
    for (const fesa::NodalLoad& load : read.analysis->step.nodal_loads) {
        const fesa::Vec3& position =
            coordinates_for(*read.model, load.node);
        add_nodal_resultant(
            total_force,
            total_moment,
            position,
            load.values);
        add_nodal_resultant(
            applied_force,
            applied_moment,
            position,
            load.values);
    }
    for (std::size_t component = 0; component < total_force.size(); ++component) {
        const double tolerance = std::max(
            kEquilibriumAbsoluteTolerance,
            kEquilibriumRelativeTolerance *
                std::abs(applied_force[component]));
        EXPECT_NEAR(total_force[component], 0.0, tolerance);
    }
    for (std::size_t component = 0; component < total_moment.size(); ++component) {
        const double tolerance = std::max(
            kEquilibriumAbsoluteTolerance,
            kEquilibriumRelativeTolerance *
                std::abs(applied_moment[component]));
        EXPECT_NEAR(total_moment[component], 0.0, tolerance);
    }

    const std::string command =
        '"' + quote(std::filesystem::path{FESA_REFERENCE_COMPARE_PATH}) +
        " --results " + quote(results.path()) +
        " --instance " + std::string{kInstanceName} +
        " --displacements " +
        quote(reference_path("cantilever beam displacements.csv")) +
        " --reactions " +
        quote(reference_path("cantilever beam reactions.csv")) +
        " --internal-forces " +
        quote(reference_path("cantilever beam elemental forces.csv")) +
        std::string{" --displacement-absolute-scale 1e-10"} +
        " --reaction-absolute-scale 1e-8" +
        " --internal-force-absolute-scale 1e-8" +
        " 1>" + quote(standard_output.path()) +
        " 2>" + quote(error_output.path()) + '"';
    const int exit_code = std::system(command.c_str());

    const std::string standard_text = read_text(standard_output.path());
    const std::string error_text = read_text(error_output.path());

    EXPECT_EQ(exit_code, 0)
        << standard_text << error_text;
    EXPECT_NE(
        standard_text.find("quantity=displacement"),
        std::string::npos);
    EXPECT_NE(
        standard_text.find("quantity=reaction"),
        std::string::npos);
    EXPECT_NE(
        standard_text.find("quantity=internal_force"),
        std::string::npos);
    EXPECT_NE(standard_text.find("rmse="), std::string::npos);
    EXPECT_NE(standard_text.find("relative_l2="), std::string::npos);
}

}  // namespace
