#include <gtest/gtest.h>

#include <algorithm>
#include <filesystem>
#include <fstream>
#include <stdexcept>
#include <string>
#include <string_view>
#include <system_error>
#include <vector>

#include <fesa/validation/reference_csv.hpp>

namespace {

class TemporaryCsv final {
public:
    TemporaryCsv(std::string_view name, std::string_view contents)
        : path_{std::filesystem::path{testing::TempDir()} / name} {
        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 temporary reference CSV."};
        }
    }

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

    TemporaryCsv(const TemporaryCsv&) = delete;
    TemporaryCsv& operator=(const TemporaryCsv&) = 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" /
           "reference" / name;
}

std::filesystem::path supplied_reference_path(const std::string_view name) {
    return std::filesystem::path{FESA_TEST_SOURCE_DIR}.parent_path() /
           "reference" / "cantilever beam" / name;
}

bool has_diagnostic(
    const std::vector<fesa::Diagnostic>& diagnostics,
    const std::string_view code) {
    return std::ranges::any_of(
        diagnostics,
        [code](const fesa::Diagnostic& diagnostic) {
            return diagnostic.code == code;
        });
}

TEST(ReferenceCsv, ReadsSuppliedDisplacementsWithWhitespaceHeader) {
    const auto result = fesa::read_reference_csv(
        fesa::ReferenceQuantity::displacement,
        supplied_reference_path("cantilever beam displacements.csv"),
        "Part-1-1");

    ASSERT_TRUE(result.diagnostics.empty());
    ASSERT_EQ(result.rows.size(), 11U);
    EXPECT_EQ(
        result.rows.front().quantity,
        fesa::ReferenceQuantity::displacement);
    EXPECT_EQ(result.rows.front().position.instance_name, "PART-1_1-1");
    EXPECT_EQ(result.rows.front().position.entity_label, 1);
    EXPECT_FALSE(result.rows.front().position.end_node_label.has_value());
    EXPECT_EQ(
        result.rows.front().values,
        (std::vector<double>{0.0, 0.0, -1.0e-30, 0.0, 1.0e-29, 0.0}));
    EXPECT_EQ(result.rows.back().position.entity_label, 11);
    EXPECT_EQ(result.rows.back().values[2], -1.91857e-2);
}

TEST(ReferenceCsv, ReadsSuppliedReactionsWithWhitespaceHeader) {
    const auto result = fesa::read_reference_csv(
        fesa::ReferenceQuantity::reaction,
        supplied_reference_path("cantilever beam reactions.csv"),
        "Part-1-1");

    ASSERT_TRUE(result.diagnostics.empty());
    ASSERT_EQ(result.rows.size(), 11U);
    EXPECT_EQ(result.rows.front().position.instance_name, "PART-1_1-1");
    EXPECT_EQ(result.rows.front().position.entity_label, 1);
    EXPECT_FALSE(result.rows.front().position.end_node_label.has_value());
    EXPECT_EQ(
        result.rows.front().values,
        (std::vector<double>{0.0, 0.0, 1.0e6, 0.0, -1.0e7, 0.0}));
}

TEST(InternalForceCsv, MapsAllSixComponentsAndElementEndPosition) {
    const auto result = fesa::read_reference_csv(
        fesa::ReferenceQuantity::internal_force,
        fixture_path("internalforces.csv"),
        "unused-request-name");

    ASSERT_TRUE(result.diagnostics.empty());
    ASSERT_EQ(result.rows.size(), 2U);
    EXPECT_EQ(
        result.rows.front().quantity,
        fesa::ReferenceQuantity::internal_force);
    EXPECT_EQ(result.rows.front().position.instance_name, "Part-1-1");
    EXPECT_EQ(result.rows.front().position.entity_label, 501);
    ASSERT_TRUE(result.rows.front().position.end_node_label.has_value());
    EXPECT_EQ(*result.rows.front().position.end_node_label, 101);
    EXPECT_EQ(
        result.rows.front().values,
        (std::vector<double>{1.25, 3.75, -2.5, -6.25, -4.0, 5.5}));
}

TEST(InternalForceCsv, AcceptsTrailingEmptyAbaqusExportColumns) {
    const TemporaryCsv input{
        "fesa-reference-trailing-empty-columns.csv",
        "Part Instance Name,Element Label,Node Label,SF-SF1,SF-SF2,"
        "SF-SF3,SM-SM1,SM-SM2,SM-SM3,,,\n"
        "PART-1_1-1,1,1,1,2,3,4,5,6,,,\n"};

    const auto result = fesa::read_reference_csv(
        fesa::ReferenceQuantity::internal_force,
        input.path(),
        "unused-request-name");

    ASSERT_TRUE(result.diagnostics.empty());
    ASSERT_EQ(result.rows.size(), 1U);
    EXPECT_EQ(
        result.rows.front().values,
        (std::vector<double>{1.0, 3.0, 2.0, 6.0, 4.0, 5.0}));
}

TEST(StressCsv, FillsOmittedInstanceAndReadsCentroidStress) {
    const auto result = fesa::read_reference_csv(
        fesa::ReferenceQuantity::centroid_stress,
        fixture_path("stresses.csv"),
        "Part-1-1");

    ASSERT_TRUE(result.diagnostics.empty());
    ASSERT_EQ(result.rows.size(), 2U);
    EXPECT_EQ(result.rows.front().position.instance_name, "Part-1-1");
    EXPECT_EQ(result.rows.front().position.entity_label, 501);
    ASSERT_TRUE(result.rows.front().position.end_node_label.has_value());
    EXPECT_EQ(*result.rows.front().position.end_node_label, 101);
    EXPECT_EQ(result.rows.front().values, (std::vector<double>{42.5}));
    EXPECT_EQ(result.rows.back().values, (std::vector<double>{-17.25}));
}

TEST(ReferenceCsv, AcceptsUtf8BomOnlyAtTheStartAndTrimsValues) {
    const TemporaryCsv input{
        "fesa-reference-bom.csv",
        "\xEF\xBB\xBF Part Instance Name , Node Label , U-U1 , U-U2 , "
        "U-U3 , UR-UR1 , UR-UR2 , UR-UR3\n"
        " Beam-1 , 7 , 1 , 2 , 3 , 4 , 5 , 6 \n"};

    const auto result = fesa::read_reference_csv(
        fesa::ReferenceQuantity::displacement, input.path(), "unused");

    ASSERT_TRUE(result.diagnostics.empty());
    ASSERT_EQ(result.rows.size(), 1U);
    EXPECT_EQ(result.rows[0].position.instance_name, "Beam-1");
    EXPECT_EQ(result.rows[0].position.entity_label, 7);
    EXPECT_EQ(
        result.rows[0].values,
        (std::vector<double>{1.0, 2.0, 3.0, 4.0, 5.0, 6.0}));
}

TEST(ReferenceCsv, DiagnosesMissingRequiredColumn) {
    const TemporaryCsv input{
        "fesa-reference-missing-column.csv",
        "Node Label,U-U1,U-U2,U-U3,UR-UR1,UR-UR2,Unexpected\n"
        "1,0,0,0,0,0,0\n"};

    const auto result = fesa::read_reference_csv(
        fesa::ReferenceQuantity::displacement, input.path(), "Part-1-1");

    EXPECT_TRUE(result.rows.empty());
    ASSERT_TRUE(has_diagnostic(
        result.diagnostics, "validation.reference_csv_missing_column"));
    ASSERT_TRUE(result.diagnostics.front().source.has_value());
    EXPECT_EQ(result.diagnostics.front().source->line, 1U);
}

TEST(ReferenceCsv, DiagnosesDuplicateResultPosition) {
    const TemporaryCsv input{
        "fesa-reference-duplicate.csv",
        "Element Label,Node Label,Sxx\n"
        "8,2,10\n"
        "8,2,11\n"};

    const auto result = fesa::read_reference_csv(
        fesa::ReferenceQuantity::centroid_stress,
        input.path(),
        "Part-1-1");

    EXPECT_TRUE(result.rows.empty());
    ASSERT_TRUE(has_diagnostic(
        result.diagnostics, "validation.reference_csv_duplicate_row"));
    ASSERT_TRUE(result.diagnostics.front().source.has_value());
    EXPECT_EQ(result.diagnostics.front().source->line, 3U);
}

TEST(ReferenceCsv, DiagnosesInvalidAndNonfiniteNumbers) {
    const TemporaryCsv invalid{
        "fesa-reference-invalid-number.csv",
        "Node Label,RF-RF1,RF-RF2,RF-RF3,RM-RM1,RM-RM2,RM-RM3\n"
        "1,0,0,not-a-number,0,0,0\n"};
    const TemporaryCsv nonfinite{
        "fesa-reference-nonfinite-number.csv",
        "Element Label,Node Label,Sxx\n"
        "8,2,nan\n"};

    const auto invalid_result = fesa::read_reference_csv(
        fesa::ReferenceQuantity::reaction,
        invalid.path(),
        "Part-1-1");
    const auto nonfinite_result = fesa::read_reference_csv(
        fesa::ReferenceQuantity::centroid_stress,
        nonfinite.path(),
        "Part-1-1");

    EXPECT_TRUE(invalid_result.rows.empty());
    EXPECT_TRUE(nonfinite_result.rows.empty());
    EXPECT_TRUE(has_diagnostic(
        invalid_result.diagnostics,
        "validation.reference_csv_invalid_number"));
    EXPECT_TRUE(has_diagnostic(
        nonfinite_result.diagnostics,
        "validation.reference_csv_invalid_number"));
}

TEST(ReferenceCsv, RejectsUtf8BomOutsideTheFileStart) {
    const TemporaryCsv input{
        "fesa-reference-misplaced-bom.csv",
        "Node Label,\xEF\xBB\xBF U-U1,U-U2,U-U3,UR-UR1,UR-UR2,UR-UR3\n"
        "1,0,0,0,0,0,0\n"};

    const auto result = fesa::read_reference_csv(
        fesa::ReferenceQuantity::displacement, input.path(), "Part-1-1");

    EXPECT_TRUE(result.rows.empty());
    EXPECT_TRUE(has_diagnostic(
        result.diagnostics,
        "validation.reference_csv_invalid_encoding"));
}

}  // namespace
