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

#include <algorithm>
#include <filesystem>
#include <fstream>
#include <stdexcept>
#include <string>
#include <string_view>
#include <system_error>
#include <tuple>
#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_;
};

fesa::SetResolutionResult parse_and_resolve(
    const TemporaryDeck& input) {
    const fesa::ParseDeckResult parsed = fesa::parse_deck(input.path());
    if (!parsed.deck.has_value()) {
        throw std::runtime_error{"Test deck did not parse."};
    }
    return fesa::resolve_sets(*parsed.deck);
}

const fesa::ResolvedSet& find_set(
    const fesa::SetResolutionResult& result,
    const fesa::ResolvedSetScope scope,
    const std::string_view scope_name,
    const fesa::ResolvedSetKind kind,
    const std::string_view set_name) {
    const auto found = std::ranges::find_if(
        result.sets,
        [=](const fesa::ResolvedSet& set) {
            return set.scope == scope && set.scope_name == scope_name &&
                   set.kind == kind && set.set_name == set_name;
        });
    if (found == result.sets.end()) {
        throw std::runtime_error{"Expected resolved set was not found."};
    }
    return *found;
}

const fesa::Diagnostic& find_diagnostic(
    const fesa::SetResolutionResult& result,
    const std::string_view code,
    const std::size_t line) {
    const auto found = std::ranges::find_if(
        result.diagnostics,
        [=](const fesa::Diagnostic& diagnostic) {
            return diagnostic.code == code && diagnostic.source.has_value() &&
                   diagnostic.source->line == line;
        });
    if (found == result.diagnostics.end()) {
        throw std::runtime_error{"Expected set diagnostic was not found."};
    }
    return *found;
}

TEST(
    SetResolution,
    CanonicalizesExplicitGenerateNestedForwardDuplicateAndEmptySets) {
    const TemporaryDeck input{
        "fesa-set-resolution-valid.inp",
        "*PART, NAME=BeamPart\n"
        "*NODE\n"
        "1, 0, 0, 0\n"
        "2, 1, 0, 0\n"
        "3, 2, 0, 0\n"
        "*ELEMENT, TYPE=B31, ELSET=ImplicitElements\n"
        "10, 1, 2\n"
        "20, 2, 3\n"
        "*NSET, NSET=AllNodes\n"
        "GeneratedNodes, 3, 1, 3\n"
        "*NSET, NSET=GeneratedNodes, GENERATE\n"
        "1, 3, 1\n"
        "*NSET, NSET=EmptyNodes\n"
        "*ELSET, ELSET=AllElements\n"
        "GeneratedElements, ImplicitElements, 20\n"
        "*ELSET, ELSET=GeneratedElements, GENERATE\n"
        "10, 20, 10\n"
        "*END PART\n"};

    const fesa::SetResolutionResult result = parse_and_resolve(input);

    ASSERT_TRUE(result.diagnostics.empty());
    EXPECT_EQ(
        find_set(
            result,
            fesa::ResolvedSetScope::part,
            "BeamPart",
            fesa::ResolvedSetKind::node,
            "AllNodes")
            .sorted_unique_labels,
        (std::vector<std::int64_t>{1, 2, 3}));
    EXPECT_TRUE(
        find_set(
            result,
            fesa::ResolvedSetScope::part,
            "BeamPart",
            fesa::ResolvedSetKind::node,
            "EmptyNodes")
            .sorted_unique_labels.empty());
    EXPECT_EQ(
        find_set(
            result,
            fesa::ResolvedSetScope::part,
            "BeamPart",
            fesa::ResolvedSetKind::element,
            "AllElements")
            .sorted_unique_labels,
        (std::vector<std::int64_t>{10, 20}));
}

TEST(PartSet, KeepsNodeAndElementSetNamespacesSeparate) {
    const TemporaryDeck input{
        "fesa-part-set-kind-collision.inp",
        "*PART, NAME=BeamPart\n"
        "*NODE\n"
        "1, 0, 0, 0\n"
        "2, 1, 0, 0\n"
        "*ELEMENT, TYPE=B31\n"
        "1, 1, 2\n"
        "*NSET, NSET=Shared\n"
        "1\n"
        "*ELSET, ELSET=Shared\n"
        "1\n"
        "*END PART\n"};

    const fesa::SetResolutionResult result = parse_and_resolve(input);

    ASSERT_TRUE(result.diagnostics.empty());
    EXPECT_EQ(
        find_set(
            result,
            fesa::ResolvedSetScope::part,
            "BeamPart",
            fesa::ResolvedSetKind::node,
            "Shared")
            .sorted_unique_labels,
        (std::vector<std::int64_t>{1}));
    EXPECT_EQ(
        find_set(
            result,
            fesa::ResolvedSetScope::part,
            "BeamPart",
            fesa::ResolvedSetKind::element,
            "Shared")
            .sorted_unique_labels,
        (std::vector<std::int64_t>{1}));
}

TEST(AssemblySet, KeepsPartScopeSeparateAndLiftsActivePartLabels) {
    const TemporaryDeck input{
        "fesa-assembly-set-scope-collision.inp",
        "*PART, NAME=BeamPart\n"
        "*NODE\n"
        "1, 0, 0, 0\n"
        "2, 1, 0, 0\n"
        "*NSET, NSET=Shared\n"
        "1\n"
        "*END PART\n"
        "*ASSEMBLY, NAME=RootAssembly\n"
        "*INSTANCE, NAME=Beam-1, PART=BeamPart\n"
        "*END INSTANCE\n"
        "*NSET, NSET=Shared, INSTANCE=Beam-1\n"
        "2, 2\n"
        "*END ASSEMBLY\n"};

    const fesa::SetResolutionResult result = parse_and_resolve(input);

    ASSERT_TRUE(result.diagnostics.empty());
    ASSERT_EQ(result.sets.size(), 2U);
    EXPECT_EQ(
        find_set(
            result,
            fesa::ResolvedSetScope::part,
            "BeamPart",
            fesa::ResolvedSetKind::node,
            "Shared")
            .sorted_unique_labels,
        (std::vector<std::int64_t>{1}));
    EXPECT_EQ(
        find_set(
            result,
            fesa::ResolvedSetScope::assembly,
            "RootAssembly",
            fesa::ResolvedSetKind::node,
            "Shared")
            .sorted_unique_labels,
        (std::vector<std::int64_t>{2}));
    EXPECT_TRUE(std::ranges::is_sorted(
        result.sets,
        [](const fesa::ResolvedSet& left, const fesa::ResolvedSet& right) {
            return std::tuple{
                       left.scope,
                       left.scope_name,
                       left.kind,
                       left.set_name} <
                   std::tuple{
                       right.scope,
                       right.scope_name,
                       right.kind,
                       right.set_name};
        }));
}

TEST(SetResolution, ReportsCycleAtTheClosingReferenceSource) {
    const TemporaryDeck input{
        "fesa-set-resolution-cycle.inp",
        "*NSET, NSET=First\n"
        "Second\n"
        "*NSET, NSET=Second\n"
        "First\n"};

    const fesa::SetResolutionResult result = parse_and_resolve(input);

    EXPECT_TRUE(result.sets.empty());
    const fesa::Diagnostic& diagnostic =
        find_diagnostic(result, "abaqus.semantic.set_cycle", 4U);
    EXPECT_EQ(diagnostic.stage, fesa::DiagnosticStage::semantic);
    EXPECT_EQ(diagnostic.source->file, input.path());
}

TEST(SetResolution, ReportsUnknownSetAndEntityAtTheirMemberRows) {
    const TemporaryDeck input{
        "fesa-set-resolution-missing.inp",
        "*NODE\n"
        "1, 0, 0, 0\n"
        "*NSET, NSET=MissingEntity\n"
        "2\n"
        "*NSET, NSET=MissingSet\n"
        "Unknown\n"};

    const fesa::SetResolutionResult result = parse_and_resolve(input);

    EXPECT_TRUE(result.sets.empty());
    EXPECT_EQ(result.diagnostics.size(), 2U);
    EXPECT_EQ(
        find_diagnostic(
            result, "abaqus.semantic.missing_set_member", 4U)
            .stage,
        fesa::DiagnosticStage::semantic);
    EXPECT_EQ(
        find_diagnostic(
            result, "abaqus.semantic.missing_set_member", 6U)
            .stage,
        fesa::DiagnosticStage::semantic);
}

TEST(SetResolution, ReportsInvalidGenerateRangesInInputOrder) {
    const TemporaryDeck input{
        "fesa-set-resolution-generate-invalid.inp",
        "*NSET, NSET=Reversed, GENERATE\n"
        "3, 1, 1\n"
        "*NSET, NSET=NotDivisible, GENERATE\n"
        "1, 4, 2\n"
        "*ELSET, ELSET=WrongFieldCount, GENERATE\n"
        "1, 2\n"};

    const fesa::SetResolutionResult result = parse_and_resolve(input);

    EXPECT_TRUE(result.sets.empty());
    ASSERT_EQ(result.diagnostics.size(), 3U);
    for (std::size_t index = 0; index < result.diagnostics.size(); ++index) {
        EXPECT_EQ(
            result.diagnostics[index].code,
            "abaqus.semantic.invalid_generate");
        ASSERT_TRUE(result.diagnostics[index].source.has_value());
    }
    EXPECT_EQ(result.diagnostics[0].source->line, 2U);
    EXPECT_EQ(result.diagnostics[1].source->line, 4U);
    EXPECT_EQ(result.diagnostics[2].source->line, 6U);
}

TEST(AssemblySet, RejectsAnInstanceOtherThanTheSingleActiveInstance) {
    const TemporaryDeck input{
        "fesa-assembly-set-wrong-instance.inp",
        "*PART, NAME=BeamPart\n"
        "*NODE\n"
        "1, 0, 0, 0\n"
        "*END PART\n"
        "*ASSEMBLY, NAME=RootAssembly\n"
        "*INSTANCE, NAME=Beam-1, PART=BeamPart\n"
        "*END INSTANCE\n"
        "*NSET, NSET=Fixed, INSTANCE=Other\n"
        "1\n"
        "*END ASSEMBLY\n"};

    const fesa::SetResolutionResult result = parse_and_resolve(input);

    EXPECT_TRUE(result.sets.empty());
    EXPECT_EQ(
        find_diagnostic(result, "abaqus.semantic.wrong_instance", 8U)
            .stage,
        fesa::DiagnosticStage::semantic);
}

}  // namespace
