#include <fesa/io/abaqus/parser.hpp>

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

#include <gtest/gtest.h>

namespace {

class TemporaryDeck final {
public:
    TemporaryDeck(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 Abaqus deck."};
        }
    }

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

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

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

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

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

const fesa::DeckRecord& record(
    const std::vector<fesa::DeckRecord>& records,
    const std::string_view keyword) {
    const auto found = std::ranges::find(
        records, keyword, &fesa::DeckRecord::keyword);
    if (found == records.end()) {
        throw std::runtime_error{"Expected deck record was not parsed."};
    }
    return *found;
}

TEST(AbaqusParser, ParsesCaseInsensitiveKeywordsCommentsAndCommaFields) {
    const auto path = fixture_path("minimal_cantilever.inp");

    const auto result = fesa::parse_deck(path);

    ASSERT_TRUE(result.deck.has_value());
    EXPECT_TRUE(result.diagnostics.empty());
    EXPECT_EQ(
        result.input_fingerprint,
        "fnv1a64:73f31da4615f09b3");
    EXPECT_TRUE(result.deck->parts.empty());
    EXPECT_FALSE(result.deck->assembly.has_value());

    const auto& nodes = record(result.deck->global_records, "NODE");
    ASSERT_EQ(nodes.data.size(), 2U);
    EXPECT_EQ(nodes.data[0], (std::vector<std::string>{
                                 "1", "0.0", "0.0", "0.0"}));
    EXPECT_EQ(nodes.data[1], (std::vector<std::string>{
                                 "2", "1.0", "0.0", "0.0"}));
    EXPECT_EQ(nodes.source.file, path);
    EXPECT_EQ(nodes.source.line, 3U);
    EXPECT_EQ(nodes.source.column, 1U);

    const auto& element = record(result.deck->global_records, "ELEMENT");
    EXPECT_EQ(element.parameters.at("TYPE"), "B31");
    EXPECT_EQ(element.parameters.at("ELSET"), "Beam");
    EXPECT_EQ(element.source.line, 8U);
}

TEST(AbaqusParser, PreservesUtf8ParameterValues) {
    const std::string utf8_name{
        "\xEB\xB9\x94\xEB\xB6\x80\xED\x92\x88"};
    const TemporaryDeck input{
        "fesa-parser-utf8.inp",
        "*MATERIAL, NAME=" + utf8_name + "\n"
        "*ELASTIC\n"
        "210000.0, 0.3\n"};

    const auto result = fesa::parse_deck(input.path());

    ASSERT_TRUE(result.deck.has_value());
    ASSERT_TRUE(result.diagnostics.empty());
    const auto& material = record(result.deck->global_records, "MATERIAL");
    EXPECT_EQ(material.parameters.at("NAME"), utf8_name);
}

TEST(AbaqusParser, RejectsUnsupportedKeywordsInsteadOfIgnoringThem) {
    const TemporaryDeck input{
        "fesa-parser-unsupported.inp",
        "** The error source must identify the keyword line.\n"
        "*INCLUDE, INPUT=other.inp\n"};

    const auto result = fesa::parse_deck(input.path());

    EXPECT_FALSE(result.deck.has_value());
    ASSERT_EQ(result.diagnostics.size(), 1U);
    EXPECT_EQ(
        result.diagnostics[0].stage, fesa::DiagnosticStage::syntax);
    EXPECT_EQ(
        result.diagnostics[0].code, "abaqus.unsupported_keyword");
    ASSERT_TRUE(result.diagnostics[0].source.has_value());
    EXPECT_EQ(result.diagnostics[0].source->file, input.path());
    EXPECT_EQ(result.diagnostics[0].source->line, 2U);
}

TEST(AbaqusParser, RejectsUnsupportedParametersOnSupportedKeywords) {
    struct Case final {
        std::string_view name;
        std::string_view contents;
        std::size_t line;
    };
    const Case cases[]{
        {"node", "*NODE, EXTRA=1\n1, 0.0, 0.0, 0.0\n", 1U},
        {"element", "*ELEMENT, TYPE=B31, EXTRA=1\n1, 1, 2\n", 1U},
        {"part", "*PART, NAME=P, EXTRA=1\n*END PART\n", 1U},
        {"end-part", "*PART, NAME=P\n*END PART, EXTRA=1\n", 2U},
        {
            "assembly",
            "*ASSEMBLY, NAME=A, EXTRA=1\n*END ASSEMBLY\n",
            1U,
        },
        {
            "end-assembly",
            "*ASSEMBLY, NAME=A\n*END ASSEMBLY, EXTRA=1\n",
            2U,
        },
        {
            "instance",
            "*ASSEMBLY, NAME=A\n"
            "*INSTANCE, NAME=I, PART=P, EXTRA=1\n"
            "*END INSTANCE\n"
            "*END ASSEMBLY\n",
            2U,
        },
        {
            "end-instance",
            "*ASSEMBLY, NAME=A\n"
            "*INSTANCE, NAME=I, PART=P\n"
            "*END INSTANCE, EXTRA=1\n"
            "*END ASSEMBLY\n",
            3U,
        },
        {"nset", "*NSET, NSET=S, EXTRA=1\n1\n", 1U},
        {"elset", "*ELSET, ELSET=S, EXTRA=1\n1\n", 1U},
        {"material", "*MATERIAL, NAME=M, EXTRA=1\n", 1U},
        {
            "elastic",
            "*MATERIAL, NAME=M\n*ELASTIC, EXTRA=1\n1.0, 0.3\n",
            2U,
        },
        {
            "beam-general-section",
            "*BEAM GENERAL SECTION, SECTION=GENERAL, ELSET=S, "
            "MATERIAL=M, EXTRA=1\n"
            "1.0, 1.0, 0.0, 1.0, 1.0\n"
            "0.0, 1.0, 0.0\n",
            1U,
        },
        {
            "transverse-shear-stiffness",
            "*TRANSVERSE SHEAR STIFFNESS, EXTRA=1\n1.0, 1.0, 0.0\n",
            1U,
        },
    };

    for (const auto& test_case : cases) {
        const TemporaryDeck input{
            "fesa-parser-unsupported-parameter-" +
                std::string{test_case.name} + ".inp",
            test_case.contents,
        };

        const auto result = fesa::parse_deck(input.path());

        SCOPED_TRACE(test_case.name);
        EXPECT_FALSE(result.deck.has_value());
        ASSERT_EQ(result.diagnostics.size(), 1U);
        EXPECT_EQ(
            result.diagnostics[0].code,
            "abaqus.syntax.unsupported_parameter");
        ASSERT_TRUE(result.diagnostics[0].source.has_value());
        EXPECT_EQ(result.diagnostics[0].source->line, test_case.line);
    }
}

TEST(AbaqusParser, RejectsInvalidParameterFormsAndSetScopeOptions) {
    struct Case final {
        std::string_view name;
        std::string_view contents;
        std::string_view code;
    };
    const Case cases[]{
        {
            "generate-valued",
            "*NSET, NSET=S, GENERATE=YES\n1, 2, 1\n",
            "abaqus.syntax.unsupported_parameter",
        },
        {
            "generate-empty-valued",
            "*NSET, NSET=S, GENERATE=\n1, 2, 1\n",
            "abaqus.syntax.unsupported_parameter",
        },
        {
            "empty-element-set",
            "*ELEMENT, TYPE=B31, ELSET=\n1, 1, 2\n",
            "abaqus.syntax.invalid_parameter",
        },
        {
            "instance-on-flat-set",
            "*NSET, NSET=S, INSTANCE=I\n1\n",
            "abaqus.syntax.unsupported_parameter",
        },
        {
            "instance-on-part-set",
            "*PART, NAME=P\n"
            "*NSET, NSET=S, INSTANCE=I\n"
            "1\n"
            "*END PART\n",
            "abaqus.syntax.unsupported_parameter",
        },
    };

    for (const auto& test_case : cases) {
        const TemporaryDeck input{
            "fesa-parser-parameter-form-" + std::string{test_case.name} +
                ".inp",
            test_case.contents,
        };

        const auto result = fesa::parse_deck(input.path());

        SCOPED_TRACE(test_case.name);
        EXPECT_FALSE(result.deck.has_value());
        ASSERT_EQ(result.diagnostics.size(), 1U);
        EXPECT_EQ(result.diagnostics[0].code, test_case.code);
        ASSERT_TRUE(result.diagnostics[0].source.has_value());
        EXPECT_EQ(result.diagnostics[0].source->line, 1U +
            (test_case.name == "instance-on-part-set" ? 1U : 0U));
    }
}

TEST(AbaqusParser, RejectsModelKeywordsOutsideTheirDocumentedScopes) {
    struct Case final {
        std::string_view name;
        std::string_view contents;
        std::string_view code;
        std::size_t line;
    };
    const Case cases[]{
        {
            "node-in-assembly",
            "*ASSEMBLY, NAME=A\n"
            "*NODE\n"
            "1, 0.0, 0.0, 0.0\n"
            "*END ASSEMBLY\n",
            "abaqus.syntax.invalid_node_scope",
            2U,
        },
        {
            "material-in-part",
            "*PART, NAME=P\n"
            "*MATERIAL, NAME=M\n"
            "*END PART\n",
            "abaqus.syntax.invalid_material_scope",
            2U,
        },
        {
            "section-in-assembly",
            "*ASSEMBLY, NAME=A\n"
            "*BEAM GENERAL SECTION, SECTION=GENERAL, ELSET=S, MATERIAL=M\n"
            "1.0, 1.0, 0.0, 1.0, 1.0\n"
            "0.0, 1.0, 0.0\n"
            "*END ASSEMBLY\n",
            "abaqus.syntax.invalid_section_scope",
            2U,
        },
    };

    for (const auto& test_case : cases) {
        const TemporaryDeck input{
            "fesa-parser-invalid-scope-" + std::string{test_case.name} +
                ".inp",
            test_case.contents,
        };

        const auto result = fesa::parse_deck(input.path());

        SCOPED_TRACE(test_case.name);
        EXPECT_FALSE(result.deck.has_value());
        ASSERT_EQ(result.diagnostics.size(), 1U);
        EXPECT_EQ(result.diagnostics[0].code, test_case.code);
        ASSERT_TRUE(result.diagnostics[0].source.has_value());
        EXPECT_EQ(result.diagnostics[0].source->line, test_case.line);
    }
}

TEST(AbaqusParser, RejectsDataRowsOnMaterial) {
    const TemporaryDeck input{
        "fesa-parser-material-data.inp",
        "*MATERIAL, NAME=Steel\n"
        "unexpected data\n"};

    const auto result = fesa::parse_deck(input.path());

    EXPECT_FALSE(result.deck.has_value());
    ASSERT_EQ(result.diagnostics.size(), 1U);
    EXPECT_EQ(
        result.diagnostics[0].code,
        "abaqus.syntax.data_without_keyword");
    ASSERT_TRUE(result.diagnostics[0].source.has_value());
    EXPECT_EQ(result.diagnostics[0].source->line, 2U);
}

TEST(ScopedDeck, PreservesPartAssemblyAndInstanceScopes) {
    const auto path =
        fixture_path("minimal_part_instance_cantilever.inp");

    const auto result = fesa::parse_deck(path);

    ASSERT_TRUE(result.deck.has_value());
    EXPECT_TRUE(result.diagnostics.empty());
    ASSERT_EQ(result.deck->parts.size(), 1U);
    EXPECT_EQ(result.deck->parts[0].name, "BeamPart");
    EXPECT_EQ(result.deck->parts[0].source.file, path);
    EXPECT_EQ(result.deck->parts[0].source.line, 2U);
    EXPECT_EQ(record(result.deck->parts[0].records, "NODE").source.line, 3U);

    ASSERT_TRUE(result.deck->assembly.has_value());
    EXPECT_EQ(result.deck->assembly->name, "RootAssembly");
    EXPECT_EQ(result.deck->assembly->source.line, 19U);
    ASSERT_EQ(result.deck->assembly->instances.size(), 1U);
    EXPECT_EQ(result.deck->assembly->instances[0].name, "Beam-1");
    EXPECT_EQ(result.deck->assembly->instances[0].part_name, "BeamPart");
    EXPECT_EQ(result.deck->assembly->instances[0].source.line, 20U);
    EXPECT_TRUE(
        result.deck->assembly->instances[0].transform_data.empty());
    EXPECT_EQ(
        record(result.deck->assembly->records, "NSET")
            .parameters.at("INSTANCE"),
        "Beam-1");

    EXPECT_NE(
        std::ranges::find(
            result.deck->global_records,
            "MATERIAL",
            &fesa::DeckRecord::keyword),
        result.deck->global_records.end());
}

TEST(ScopedDeck, PreservesInstanceTransformDataWithoutApplyingIt) {
    const TemporaryDeck input{
        "fesa-parser-transform.inp",
        "*PART, NAME=BeamPart\n"
        "*END PART\n"
        "*ASSEMBLY, NAME=RootAssembly\n"
        "*INSTANCE, NAME=Beam-1, PART=BeamPart\n"
        "1.0, 2.0, 3.0\n"
        "0.0, 0.0, 1.0, 90.0\n"
        "*END INSTANCE\n"
        "*END ASSEMBLY\n"};

    const auto result = fesa::parse_deck(input.path());

    ASSERT_TRUE(result.deck.has_value());
    ASSERT_TRUE(result.deck->assembly.has_value());
    ASSERT_EQ(result.deck->assembly->instances.size(), 1U);
    EXPECT_EQ(
        result.deck->assembly->instances[0].transform_data,
        (std::vector<std::vector<std::string>>{
            {"1.0", "2.0", "3.0"},
            {"0.0", "0.0", "1.0", "90.0"}}));
}

TEST(ScopedDeck, ReportsUnexpectedAndUnclosedScopeTerminators) {
    struct Case final {
        std::string_view name;
        std::string_view contents;
        std::string_view code;
        std::size_t line;
    };
    const Case cases[]{
        {
            "fesa-parser-unexpected-end-part.inp",
            "*END PART\n",
            "abaqus.syntax.unexpected_end_part",
            1U,
        },
        {
            "fesa-parser-unclosed-part.inp",
            "*PART, NAME=BeamPart\n"
            "*NODE\n"
            "1, 0.0, 0.0, 0.0\n",
            "abaqus.syntax.unclosed_part",
            1U,
        },
        {
            "fesa-parser-unexpected-end-instance.inp",
            "*ASSEMBLY, NAME=RootAssembly\n"
            "*END INSTANCE\n",
            "abaqus.syntax.unexpected_end_instance",
            2U,
        },
        {
            "fesa-parser-unclosed-assembly.inp",
            "*ASSEMBLY, NAME=RootAssembly\n",
            "abaqus.syntax.unclosed_assembly",
            1U,
        },
    };

    for (const auto& test_case : cases) {
        const TemporaryDeck input{test_case.name, test_case.contents};

        const auto result = fesa::parse_deck(input.path());

        SCOPED_TRACE(test_case.name);
        EXPECT_FALSE(result.deck.has_value());
        ASSERT_EQ(result.diagnostics.size(), 1U);
        EXPECT_EQ(result.diagnostics[0].code, test_case.code);
        ASSERT_TRUE(result.diagnostics[0].source.has_value());
        EXPECT_EQ(result.diagnostics[0].source->line, test_case.line);
    }
}

}  // namespace
