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

#include <algorithm>
#include <cstddef>
#include <cstdint>
#include <filesystem>
#include <fstream>
#include <optional>
#include <ranges>
#include <sstream>
#include <stdexcept>
#include <string>
#include <string_view>
#include <utility>
#include <vector>

#include <gtest/gtest.h>

namespace {

struct ContractCase final {
    std::string id;
    bool valid;
    std::filesystem::path fixture;
    std::optional<fesa::DiagnosticStage> expected_stage;
    std::string expected_code;
    std::size_t expected_line;
    std::optional<std::size_t> node_count;
    std::optional<std::size_t> element_count;
    std::optional<std::size_t> node_set_count;
    std::optional<std::size_t> element_set_count;
    std::optional<std::size_t> prescribed_dof_count;
    std::optional<std::size_t> nodal_load_count;
    std::string shear_source;
    std::optional<double> shear_area_y;
    std::optional<double> shear_area_z;
    std::string checked_node_set;
    std::string checked_element_set;
};

struct ContractOutcome final {
    std::optional<fesa::Domain> domain;
    std::vector<fesa::Diagnostic> diagnostics;
};

std::vector<std::string> split(
    const std::string_view text,
    const char delimiter) {
    std::vector<std::string> fields;
    std::size_t first = 0;
    while (true) {
        const std::size_t next = text.find(delimiter, first);
        fields.emplace_back(
            text.substr(
                first,
                next == std::string_view::npos
                    ? std::string_view::npos
                    : next - first));
        if (next == std::string_view::npos) {
            break;
        }
        first = next + 1U;
    }
    return fields;
}

std::optional<std::size_t> optional_size(const std::string& field) {
    if (field == "-") {
        return std::nullopt;
    }
    return static_cast<std::size_t>(std::stoull(field));
}

std::optional<double> optional_real(const std::string& field) {
    if (field == "-") {
        return std::nullopt;
    }
    return std::stod(field);
}

std::optional<fesa::DiagnosticStage> optional_stage(
    const std::string& field) {
    if (field == "-") {
        return std::nullopt;
    }
    if (field == "io") {
        return fesa::DiagnosticStage::io;
    }
    if (field == "syntax") {
        return fesa::DiagnosticStage::syntax;
    }
    if (field == "semantic") {
        return fesa::DiagnosticStage::semantic;
    }
    if (field == "model") {
        return fesa::DiagnosticStage::model;
    }
    throw std::runtime_error{"Unknown diagnostic stage in contract manifest."};
}

std::vector<ContractCase> load_contract_cases() {
    const std::filesystem::path path =
        std::filesystem::path{FESA_TEST_SOURCE_DIR} / "fixtures" / "abaqus" /
        "contract.tsv";
    std::ifstream input{path, std::ios::binary};
    if (!input) {
        throw std::runtime_error{"Unable to open Abaqus contract manifest."};
    }

    std::vector<ContractCase> cases;
    std::string line;
    while (std::getline(input, line)) {
        if (line.empty() || line.front() == '#') {
            continue;
        }
        const std::vector<std::string> fields = split(line, '\t');
        if (fields.size() != 17U) {
            throw std::runtime_error{
                "Abaqus contract manifest row must contain 17 fields."};
        }
        const bool valid = fields[1] == "valid";
        if (!valid && fields[1] != "invalid") {
            throw std::runtime_error{
                "Abaqus contract outcome must be valid or invalid."};
        }
        cases.push_back({
            fields[0],
            valid,
            fields[2],
            optional_stage(fields[3]),
            fields[4] == "-" ? std::string{} : fields[4],
            static_cast<std::size_t>(std::stoull(fields[5])),
            optional_size(fields[6]),
            optional_size(fields[7]),
            optional_size(fields[8]),
            optional_size(fields[9]),
            optional_size(fields[10]),
            optional_size(fields[11]),
            fields[12],
            optional_real(fields[13]),
            optional_real(fields[14]),
            fields[15],
            fields[16],
        });
    }
    if (cases.empty()) {
        throw std::runtime_error{"Abaqus contract manifest is empty."};
    }
    return cases;
}

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

std::string diagnostics_text(
    const std::vector<fesa::Diagnostic>& diagnostics) {
    std::ostringstream output;
    for (const auto& diagnostic : diagnostics) {
        output << diagnostic.code;
        if (diagnostic.source.has_value()) {
            output << '@' << diagnostic.source->line;
        }
        output << '\n';
    }
    return output.str();
}

std::pair<std::string, std::vector<std::int64_t>> expected_set(
    const std::string& field) {
    const std::size_t colon = field.find(':');
    if (colon == std::string::npos) {
        throw std::runtime_error{"Set check must use name:label,... format."};
    }
    std::vector<std::int64_t> labels;
    for (const std::string& label :
         split(std::string_view{field}.substr(colon + 1U), ',')) {
        labels.push_back(std::stoll(label));
    }
    return {field.substr(0, colon), std::move(labels)};
}

void expect_node_set(
    const fesa::Domain& domain,
    const std::string& field) {
    if (field == "-") {
        return;
    }
    const auto [name, expected_labels] = expected_set(field);
    const auto found = std::ranges::find(
        domain.node_sets(), name, &fesa::NodeSet::name);
    ASSERT_NE(found, domain.node_sets().end());

    std::vector<std::int64_t> actual_labels;
    for (const fesa::NodeId member : found->members) {
        actual_labels.push_back(domain.node(member).origin.local_label);
    }
    std::ranges::sort(actual_labels);
    EXPECT_EQ(actual_labels, expected_labels);
}

void expect_element_set(
    const fesa::Domain& domain,
    const std::string& field) {
    if (field == "-") {
        return;
    }
    const auto [name, expected_labels] = expected_set(field);
    const auto found = std::ranges::find(
        domain.element_sets(), name, &fesa::ElementSet::name);
    ASSERT_NE(found, domain.element_sets().end());

    std::vector<std::int64_t> actual_labels;
    for (const fesa::ElementId member : found->members) {
        const auto element = std::ranges::find(
            domain.beam_elements(), member, &fesa::BeamElement::id);
        ASSERT_NE(element, domain.beam_elements().end());
        actual_labels.push_back(element->origin.local_label);
    }
    std::ranges::sort(actual_labels);
    EXPECT_EQ(actual_labels, expected_labels);
}

class AbaqusInputContractTest
    : public testing::TestWithParam<ContractCase> {};

TEST_P(AbaqusInputContractTest, FixtureMatchesNormativeContract) {
    const ContractCase& contract = GetParam();
    const std::filesystem::path path =
        std::filesystem::path{FESA_TEST_SOURCE_DIR} / "fixtures" / "abaqus" /
        contract.fixture;
    SCOPED_TRACE(contract.id);
    ASSERT_TRUE(std::filesystem::is_regular_file(path));

    const ContractOutcome outcome = parse_and_map(path);
    if (!contract.valid) {
        EXPECT_FALSE(outcome.domain.has_value());
        ASSERT_TRUE(contract.expected_stage.has_value());
        const auto diagnostic = std::ranges::find_if(
            outcome.diagnostics,
            [&contract](const fesa::Diagnostic& candidate) {
                return candidate.stage == *contract.expected_stage &&
                       candidate.code == contract.expected_code &&
                       candidate.source.has_value() &&
                       candidate.source->line == contract.expected_line;
            });
        EXPECT_NE(diagnostic, outcome.diagnostics.end())
            << "Expected " << contract.expected_code << '@'
            << contract.expected_line << " but received:\n"
            << diagnostics_text(outcome.diagnostics);
        return;
    }

    ASSERT_TRUE(outcome.domain.has_value())
        << diagnostics_text(outcome.diagnostics);
    ASSERT_TRUE(outcome.diagnostics.empty())
        << diagnostics_text(outcome.diagnostics);
    const fesa::Domain& domain = *outcome.domain;
    EXPECT_EQ(domain.nodes().size(), *contract.node_count);
    EXPECT_EQ(domain.beam_elements().size(), *contract.element_count);
    EXPECT_EQ(domain.node_sets().size(), *contract.node_set_count);
    EXPECT_EQ(domain.element_sets().size(), *contract.element_set_count);
    EXPECT_EQ(
        domain.step().prescribed_dofs.size(),
        *contract.prescribed_dof_count);
    EXPECT_EQ(domain.step().nodal_loads.size(), *contract.nodal_load_count);

    ASSERT_EQ(domain.sections().size(), 1U);
    const fesa::BeamSection& section = domain.sections().front();
    const auto expected_source = contract.shear_source == "input"
                                     ? fesa::ShearPropertySource::input
                                     : fesa::ShearPropertySource::phase1_default;
    EXPECT_EQ(section.shear_source, expected_source);
    EXPECT_NEAR(section.shear_area_y, *contract.shear_area_y, 1.0e-12);
    EXPECT_NEAR(section.shear_area_z, *contract.shear_area_z, 1.0e-12);
    expect_node_set(domain, contract.checked_node_set);
    expect_element_set(domain, contract.checked_element_set);
}

INSTANTIATE_TEST_SUITE_P(
    AbaqusInputContract,
    AbaqusInputContractTest,
    testing::ValuesIn(load_contract_cases()),
    [](const testing::TestParamInfo<ContractCase>& info) {
        return info.param.id;
    });

}  // namespace
