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

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

#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_;
};

fesa::DomainBuildResult parse_and_map(const TemporaryDeck& input) {
    auto parsed = fesa::parse_deck(input.path());
    if (!parsed.deck.has_value()) {
        return {std::nullopt, std::move(parsed.diagnostics)};
    }
    return fesa::map_deck_to_domain(*parsed.deck);
}

void expect_diagnostic(
    const fesa::DomainBuildResult& result,
    const std::string_view code,
    const std::size_t line) {
    EXPECT_FALSE(result.domain.has_value());
    const auto diagnostic = std::ranges::find_if(
        result.diagnostics,
        [code, line](const fesa::Diagnostic& candidate) {
            return candidate.stage == fesa::DiagnosticStage::semantic &&
                   candidate.code == code && candidate.source.has_value() &&
                   candidate.source->line == line;
        });
    EXPECT_NE(diagnostic, result.diagnostics.end());
}

std::string_view valid_flat_prefix() {
    return
        "*NODE\n"
        "1, 0.0, 0.0, 0.0\n"
        "2, 1.0, 0.0, 0.0\n"
        "*ELEMENT, TYPE=B31, ELSET=Beam\n"
        "1, 1, 2\n"
        "*ELSET, ELSET=Beam\n"
        "1\n"
        "*MATERIAL, NAME=Steel\n"
        "*ELASTIC\n"
        "200.0, 0.25\n";
}

std::string complete_flat_deck(std::string_view section_and_shear) {
    std::string contents{valid_flat_prefix()};
    contents.append(section_and_shear);
    contents.append(
        "*STEP\n"
        "*STATIC\n"
        "*END STEP\n");
    return contents;
}

TEST(MaterialMapping, ResolvesForwardMaterialAndExplicitElsetAssignment) {
    const TemporaryDeck input{
        "fesa-forward-material.inp",
        "*PART, NAME=BeamPart\n"
        "*NODE\n"
        "1, 0.0, 0.0, 0.0\n"
        "2, 1.0, 0.0, 0.0\n"
        "*ELEMENT, TYPE=B31\n"
        "1, 1, 2\n"
        "*ELSET, ELSET=Beam\n"
        "1\n"
        "*BEAM GENERAL SECTION, SECTION=GENERAL, ELSET=Beam, MATERIAL=Steel\n"
        "1.2, 2.0, 0.0, 3.0, 4.0\n"
        "0.0, 1.0, 0.0\n"
        "*END PART\n"
        "*ASSEMBLY, NAME=Assembly\n"
        "*INSTANCE, NAME=Beam-1, PART=BeamPart\n"
        "*END INSTANCE\n"
        "*END ASSEMBLY\n"
        "*MATERIAL, NAME=Steel\n"
        "*ELASTIC\n"
        "200.0, 0.25\n"
        "*STEP\n"
        "*STATIC\n"
        "*END STEP\n"};

    const auto result = parse_and_map(input);

    ASSERT_TRUE(result.domain.has_value());
    ASSERT_TRUE(result.diagnostics.empty());
    ASSERT_EQ(result.domain->materials().size(), 1U);
    EXPECT_DOUBLE_EQ(result.domain->materials()[0].young, 200.0);
    EXPECT_DOUBLE_EQ(result.domain->materials()[0].poisson, 0.25);
    ASSERT_EQ(result.domain->sections().size(), 1U);
    EXPECT_EQ(
        result.domain->beam_elements()[0].material,
        result.domain->materials()[0].id);
    EXPECT_EQ(
        result.domain->beam_elements()[0].section,
        result.domain->sections()[0].id);
}

TEST(ShearDefault, PreservesPhase1DefaultEffectiveAreasAndSource) {
    const TemporaryDeck input{
        "fesa-default-shear.inp",
        complete_flat_deck(
            "*BEAM GENERAL SECTION, SECTION=GENERAL, ELSET=Beam, MATERIAL=Steel\n"
            "1.2, 2.0, 0.0, 3.0, 4.0\n"
            "0.0, 1.0, 0.0\n")};

    const auto result = parse_and_map(input);

    ASSERT_TRUE(result.domain.has_value());
    ASSERT_EQ(result.domain->sections().size(), 1U);
    const fesa::BeamSection& section = result.domain->sections()[0];
    EXPECT_DOUBLE_EQ(section.shear_area_y, 1.0);
    EXPECT_DOUBLE_EQ(section.shear_area_z, 1.0);
    EXPECT_EQ(
        section.shear_source,
        fesa::ShearPropertySource::phase1_default);
}

TEST(ShearDefault, ConvertsExplicitK23AndK13UsingIsotropicShearModulus) {
    const TemporaryDeck input{
        "fesa-explicit-shear.inp",
        complete_flat_deck(
            "*BEAM GENERAL SECTION, SECTION=GENERAL, ELSET=Beam, MATERIAL=Steel\n"
            "1.2, 2.0, 0.0, 3.0, 4.0\n"
            "0.0, 1.0, 0.0\n"
            "*TRANSVERSE SHEAR STIFFNESS\n"
            "64.0, 40.0, 0.0\n")};

    const auto result = parse_and_map(input);

    ASSERT_TRUE(result.domain.has_value());
    ASSERT_EQ(result.domain->sections().size(), 1U);
    const fesa::BeamSection& section = result.domain->sections()[0];
    EXPECT_DOUBLE_EQ(section.shear_area_y, 0.8);
    EXPECT_DOUBLE_EQ(section.shear_area_z, 0.5);
    EXPECT_EQ(section.shear_source, fesa::ShearPropertySource::input);
}

TEST(BeamSection, RejectsInvalidElasticPropertiesAtTheDataRow) {
    const TemporaryDeck input{
        "fesa-invalid-elastic.inp",
        "*MATERIAL, NAME=Steel\n"
        "*ELASTIC\n"
        "-1.0, 0.25\n"
        "*STEP\n"
        "*STATIC\n"
        "*END STEP\n"};

    const auto result = parse_and_map(input);

    expect_diagnostic(
        result, "abaqus.semantic.invalid_elastic_data", 3U);
}

TEST(BeamSection, RejectsMaterialWithoutElasticData) {
    const TemporaryDeck input{
        "fesa-missing-elastic.inp",
        "*MATERIAL, NAME=Steel\n"
        "*STEP\n"
        "*STATIC\n"
        "*END STEP\n"};

    const auto result = parse_and_map(input);

    expect_diagnostic(result, "abaqus.semantic.missing_elastic", 1U);
}

TEST(MaterialMapping, RejectsDuplicateMaterialDefinition) {
    const TemporaryDeck input{
        "fesa-duplicate-material.inp",
        "*MATERIAL, NAME=Steel\n"
        "*ELASTIC\n"
        "200.0, 0.25\n"
        "*MATERIAL, NAME=Steel\n"
        "*ELASTIC\n"
        "210.0, 0.3\n"
        "*STEP\n"
        "*STATIC\n"
        "*END STEP\n"};

    const auto result = parse_and_map(input);

    expect_diagnostic(
        result, "abaqus.semantic.duplicate_material", 4U);
}

TEST(MaterialMapping, RejectsSectionWithMissingMaterialReference) {
    const TemporaryDeck input{
        "fesa-missing-material.inp",
        complete_flat_deck(
            "*BEAM GENERAL SECTION, SECTION=GENERAL, ELSET=Beam, MATERIAL=Missing\n"
            "1.2, 2.0, 0.0, 3.0, 4.0\n"
            "0.0, 1.0, 0.0\n")};

    const auto result = parse_and_map(input);

    expect_diagnostic(result, "abaqus.semantic.missing_material", 11U);
}

TEST(BeamSection, RejectsNonzeroProductMomentAtTheDataRow) {
    const TemporaryDeck input{
        "fesa-nonzero-iyz.inp",
        complete_flat_deck(
            "*BEAM GENERAL SECTION, SECTION=GENERAL, ELSET=Beam, MATERIAL=Steel\n"
            "1.2, 2.0, 0.1, 3.0, 4.0\n"
            "0.0, 1.0, 0.0\n")};

    const auto result = parse_and_map(input);

    expect_diagnostic(
        result, "abaqus.semantic.invalid_section_data", 12U);
}

TEST(BeamSection, RejectsNonpositiveGeneralPropertyAtTheDataRow) {
    const TemporaryDeck input{
        "fesa-invalid-area.inp",
        complete_flat_deck(
            "*BEAM GENERAL SECTION, SECTION=GENERAL, ELSET=Beam, MATERIAL=Steel\n"
            "0.0, 2.0, 0.0, 3.0, 4.0\n"
            "0.0, 1.0, 0.0\n")};

    const auto result = parse_and_map(input);

    expect_diagnostic(
        result, "abaqus.semantic.invalid_section_data", 12U);
}

TEST(BeamSection, RejectsNonfiniteOrientationAtTheOrientationRow) {
    const TemporaryDeck input{
        "fesa-nonfinite-orientation.inp",
        complete_flat_deck(
            "*BEAM GENERAL SECTION, SECTION=GENERAL, ELSET=Beam, MATERIAL=Steel\n"
            "1.2, 2.0, 0.0, 3.0, 4.0\n"
            "0.0, nan, 0.0\n")};

    const auto result = parse_and_map(input);

    expect_diagnostic(
        result, "abaqus.semantic.invalid_section_data", 13U);
}

TEST(BeamSection, RejectsSectionWhoseElementSetDoesNotExist) {
    const TemporaryDeck input{
        "fesa-missing-section-elset.inp",
        complete_flat_deck(
            "*BEAM GENERAL SECTION, SECTION=GENERAL, ELSET=Missing, MATERIAL=Steel\n"
            "1.2, 2.0, 0.0, 3.0, 4.0\n"
            "0.0, 1.0, 0.0\n")};

    const auto result = parse_and_map(input);

    expect_diagnostic(
        result, "abaqus.semantic.missing_element_set", 11U);
}

TEST(BeamSection, RejectsNonzeroScfAtTheDataRow) {
    const TemporaryDeck input{
        "fesa-nonzero-scf.inp",
        complete_flat_deck(
            "*BEAM GENERAL SECTION, SECTION=GENERAL, ELSET=Beam, MATERIAL=Steel\n"
            "1.2, 2.0, 0.0, 3.0, 4.0\n"
            "0.0, 1.0, 0.0\n"
            "*TRANSVERSE SHEAR STIFFNESS\n"
            "64.0, 40.0, 0.25\n")};

    const auto result = parse_and_map(input);

    expect_diagnostic(result, "abaqus.semantic.nonzero_scf", 15U);
}

TEST(BeamSection, RejectsElementWithoutSectionAssignment) {
    const TemporaryDeck input{
        "fesa-missing-section.inp",
        complete_flat_deck(
            "*BEAM GENERAL SECTION, SECTION=GENERAL, ELSET=Other, MATERIAL=Steel\n"
            "1.2, 2.0, 0.0, 3.0, 4.0\n"
            "0.0, 1.0, 0.0\n"
            "*ELSET, ELSET=Other\n")};

    const auto result = parse_and_map(input);

    expect_diagnostic(result, "abaqus.semantic.missing_section", 5U);
}

TEST(BeamSection, RejectsDuplicateSectionAssignment) {
    const TemporaryDeck input{
        "fesa-duplicate-section.inp",
        complete_flat_deck(
            "*BEAM GENERAL SECTION, SECTION=GENERAL, ELSET=Beam, MATERIAL=Steel\n"
            "1.2, 2.0, 0.0, 3.0, 4.0\n"
            "0.0, 1.0, 0.0\n"
            "*BEAM GENERAL SECTION, SECTION=GENERAL, ELSET=Beam, MATERIAL=Steel\n"
            "1.2, 2.0, 0.0, 3.0, 4.0\n"
            "0.0, 1.0, 0.0\n")};

    const auto result = parse_and_map(input);

    expect_diagnostic(
        result, "abaqus.semantic.duplicate_section_assignment", 14U);
}

}  // namespace
