#include <fesa/model/domain_builder.hpp>

#include <algorithm>
#include <array>
#include <limits>
#include <string_view>
#include <utility>

#include <gtest/gtest.h>

namespace {

fesa::Node first_node() {
    return {
        fesa::NodeId{0},
        fesa::EntityOrigin{"BeamPart", "Beam-1", 1},
        fesa::Vec3{0.0, 0.0, 0.0},
    };
}

fesa::Node second_node() {
    return {
        fesa::NodeId{1},
        fesa::EntityOrigin{"BeamPart", "Beam-1", 2},
        fesa::Vec3{2.0, 0.0, 0.0},
    };
}

fesa::IsotropicElastic valid_material() {
    return {
        fesa::MaterialId{0},
        "Steel",
        210.0e9,
        0.3,
    };
}

fesa::BeamSection valid_section() {
    return {
        fesa::SectionId{0},
        "General",
        0.04,
        1.2e-4,
        1.4e-4,
        2.0e-4,
        0.03,
        0.031,
        fesa::ShearPropertySource::input,
        fesa::Vec3{0.0, 1.0, 0.0},
        {{-0.1, 0.0}, {0.1, 0.0}},
    };
}

fesa::BeamElement valid_element() {
    return {
        fesa::ElementId{0},
        fesa::EntityOrigin{"BeamPart", "Beam-1", 1},
        {fesa::NodeId{0}, fesa::NodeId{1}},
        fesa::MaterialId{0},
        fesa::SectionId{0},
    };
}

fesa::StepDefinition valid_step() {
    return {
        "Load",
        {{fesa::NodeId{0}, 1, 0.0}},
        {{fesa::NodeId{1}, {0.0, -100.0, 0.0, 0.0, 0.0, 0.0}}},
    };
}

fesa::DomainBuilder make_builder(
    fesa::IsotropicElastic material,
    fesa::BeamSection section,
    fesa::BeamElement element,
    fesa::StepDefinition step) {
    fesa::DomainBuilder builder;
    builder.add_node(first_node());
    builder.add_node(second_node());
    builder.add_material(std::move(material));
    builder.add_section(std::move(section));
    builder.add_beam_element(std::move(element));
    builder.add_node_set({"Fixed", {fesa::NodeId{0}}});
    builder.add_element_set({"Beam", {fesa::ElementId{0}}});
    builder.set_step(std::move(step));
    return builder;
}

fesa::DomainBuilder make_valid_builder() {
    return make_builder(
        valid_material(), valid_section(), valid_element(), valid_step());
}

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

std::size_t diagnostic_count(
    const fesa::DomainBuildResult& result,
    const std::string_view code) {
    return static_cast<std::size_t>(std::ranges::count_if(
        result.diagnostics,
        [code](const fesa::Diagnostic& diagnostic) {
            return diagnostic.code == code;
        }));
}

TEST(DomainBuilder, BuildsImmutableDomainAndDenseNodeLookups) {
    auto result = std::move(make_valid_builder()).build();

    ASSERT_TRUE(result.domain.has_value());
    EXPECT_TRUE(result.diagnostics.empty());

    const fesa::Domain& domain = *result.domain;
    ASSERT_EQ(domain.nodes().size(), 2);
    ASSERT_EQ(domain.beam_elements().size(), 1);
    ASSERT_EQ(domain.materials().size(), 1);
    ASSERT_EQ(domain.sections().size(), 1);
    ASSERT_EQ(domain.node_sets().size(), 1);
    ASSERT_EQ(domain.element_sets().size(), 1);
    EXPECT_EQ(domain.step().name, "Load");
    EXPECT_EQ(&domain.node(fesa::NodeId{1}), &domain.nodes()[1]);
    EXPECT_EQ(
        &domain.node(fesa::EntityOrigin{"BeamPart", "Beam-1", 2}),
        &domain.nodes()[1]);
}

TEST(DomainValidation, RejectsDuplicateInternalId) {
    auto builder = make_valid_builder();
    builder.add_node({
        fesa::NodeId{1},
        fesa::EntityOrigin{"BeamPart", "Beam-1", 3},
        fesa::Vec3{3.0, 0.0, 0.0},
    });

    const auto result = std::move(builder).build();

    EXPECT_FALSE(result.domain.has_value());
    EXPECT_TRUE(has_diagnostic(result, "model.duplicate_node_id"));
}

TEST(DomainValidation, RejectsDuplicateMaterialId) {
    auto builder = make_valid_builder();
    auto material = valid_material();
    material.name = "Duplicate";
    builder.add_material(std::move(material));

    const auto result = std::move(builder).build();

    EXPECT_FALSE(result.domain.has_value());
    EXPECT_TRUE(has_diagnostic(result, "model.duplicate_material_id"));
}

TEST(DomainValidation, RejectsDuplicateSectionId) {
    auto builder = make_valid_builder();
    auto section = valid_section();
    section.name = "Duplicate";
    builder.add_section(std::move(section));

    const auto result = std::move(builder).build();

    EXPECT_FALSE(result.domain.has_value());
    EXPECT_TRUE(has_diagnostic(result, "model.duplicate_section_id"));
}

TEST(DomainValidation, RejectsDuplicateElementId) {
    auto builder = make_valid_builder();
    auto element = valid_element();
    element.origin.local_label = 2;
    builder.add_beam_element(std::move(element));

    const auto result = std::move(builder).build();

    EXPECT_FALSE(result.domain.has_value());
    EXPECT_TRUE(has_diagnostic(result, "model.duplicate_element_id"));
}

TEST(DomainValidation, RejectsDuplicateOriginWithinEntityKind) {
    auto builder = make_valid_builder();
    builder.add_node({
        fesa::NodeId{2},
        fesa::EntityOrigin{"OtherPart", "Beam-1", 2},
        fesa::Vec3{3.0, 0.0, 0.0},
    });

    const auto result = std::move(builder).build();

    EXPECT_FALSE(result.domain.has_value());
    EXPECT_TRUE(has_diagnostic(result, "model.duplicate_node_origin"));
}

TEST(DomainValidation, RejectsDuplicateElementOriginWithinEntityKind) {
    auto builder = make_valid_builder();
    auto element = valid_element();
    element.id = fesa::ElementId{1};
    element.origin.part_name = "OtherPart";
    builder.add_beam_element(std::move(element));

    const auto result = std::move(builder).build();

    EXPECT_FALSE(result.domain.has_value());
    EXPECT_TRUE(has_diagnostic(result, "model.duplicate_element_origin"));
}

TEST(DomainValidation, CollectsMissingReferencesAcrossSemanticEntities) {
    auto builder = make_valid_builder();
    builder.add_beam_element({
        fesa::ElementId{1},
        fesa::EntityOrigin{"BeamPart", "Beam-1", 2},
        {fesa::NodeId{90}, fesa::NodeId{91}},
        fesa::MaterialId{92},
        fesa::SectionId{93},
    });
    builder.add_node_set({"MissingNodes", {fesa::NodeId{94}}});
    builder.add_element_set({"MissingElements", {fesa::ElementId{95}}});
    builder.set_step({
        "Load",
        {{fesa::NodeId{96}, 1, 0.0}},
        {{fesa::NodeId{97}, {1.0, 0.0, 0.0, 0.0, 0.0, 0.0}}},
    });

    const auto result = std::move(builder).build();

    EXPECT_FALSE(result.domain.has_value());
    EXPECT_TRUE(has_diagnostic(result, "model.missing_node_reference"));
    EXPECT_TRUE(has_diagnostic(result, "model.missing_material_reference"));
    EXPECT_TRUE(has_diagnostic(result, "model.missing_section_reference"));
    EXPECT_TRUE(has_diagnostic(result, "model.missing_element_reference"));
}

TEST(DomainValidation, RejectsNonpositiveYoungsModulus) {
    auto material = valid_material();
    material.young = 0.0;

    const auto result = std::move(make_builder(
                                      material,
                                      valid_section(),
                                      valid_element(),
                                      valid_step()))
                            .build();

    EXPECT_FALSE(result.domain.has_value());
    EXPECT_TRUE(has_diagnostic(result, "model.invalid_material"));
}

TEST(DomainValidation, RejectsPoissonRatioOutsideOpenPhysicalRange) {
    for (const double poisson : {-1.0, 0.5}) {
        auto material = valid_material();
        material.poisson = poisson;

        const auto result = std::move(make_builder(
                                          material,
                                          valid_section(),
                                          valid_element(),
                                          valid_step()))
                                .build();

        EXPECT_FALSE(result.domain.has_value()) << "poisson=" << poisson;
        EXPECT_TRUE(has_diagnostic(result, "model.invalid_material"))
            << "poisson=" << poisson;
    }
}

struct InvalidSectionProperty final {
    const char* name;
    double fesa::BeamSection::*member;
};

class DomainValidationInvalidSection
    : public testing::TestWithParam<InvalidSectionProperty> {};

TEST_P(DomainValidationInvalidSection, RejectsNonpositiveProperty) {
    auto section = valid_section();
    section.*(GetParam().member) = 0.0;

    const auto result = std::move(make_builder(
                                      valid_material(),
                                      section,
                                      valid_element(),
                                      valid_step()))
                            .build();

    EXPECT_FALSE(result.domain.has_value());
    EXPECT_TRUE(has_diagnostic(result, "model.invalid_section"));
}

INSTANTIATE_TEST_SUITE_P(
    SectionProperties,
    DomainValidationInvalidSection,
    testing::Values(
        InvalidSectionProperty{"Area", &fesa::BeamSection::area},
        InvalidSectionProperty{"Iy", &fesa::BeamSection::iy},
        InvalidSectionProperty{"Iz", &fesa::BeamSection::iz},
        InvalidSectionProperty{"TorsionJ", &fesa::BeamSection::torsion_j},
        InvalidSectionProperty{
            "ShearAreaY", &fesa::BeamSection::shear_area_y},
        InvalidSectionProperty{
            "ShearAreaZ", &fesa::BeamSection::shear_area_z}),
    [](const testing::TestParamInfo<InvalidSectionProperty>& info) {
        return info.param.name;
    });

TEST(DomainValidation, RejectsNonfiniteNodeCoordinate) {
    const double nan = std::numeric_limits<double>::quiet_NaN();
    auto builder = make_valid_builder();
    builder.add_node({
        fesa::NodeId{2},
        fesa::EntityOrigin{"BeamPart", "Beam-1", 3},
        fesa::Vec3{nan, 0.0, 0.0},
    });

    const auto result = std::move(builder).build();

    EXPECT_FALSE(result.domain.has_value());
    EXPECT_TRUE(has_diagnostic(result, "model.nonfinite_value"));
}

TEST(DomainValidation, RejectsNonfiniteMaterialConstant) {
    auto material = valid_material();
    material.young = std::numeric_limits<double>::infinity();

    const auto result = std::move(make_builder(
                                      material,
                                      valid_section(),
                                      valid_element(),
                                      valid_step()))
                            .build();

    EXPECT_FALSE(result.domain.has_value());
    EXPECT_TRUE(has_diagnostic(result, "model.nonfinite_value"));
}

TEST(DomainValidation, RejectsNonfinitePoissonRatio) {
    auto material = valid_material();
    material.poisson = std::numeric_limits<double>::quiet_NaN();

    const auto result = std::move(make_builder(
                                      material,
                                      valid_section(),
                                      valid_element(),
                                      valid_step()))
                            .build();

    EXPECT_FALSE(result.domain.has_value());
    EXPECT_TRUE(has_diagnostic(result, "model.nonfinite_value"));
}

TEST(DomainValidation, RejectsNonfiniteSectionProperty) {
    auto section = valid_section();
    section.area = std::numeric_limits<double>::infinity();

    const auto result = std::move(make_builder(
                                      valid_material(),
                                      section,
                                      valid_element(),
                                      valid_step()))
                            .build();

    EXPECT_FALSE(result.domain.has_value());
    EXPECT_TRUE(has_diagnostic(result, "model.nonfinite_value"));
}

TEST(DomainValidation, RejectsNonfiniteSectionOrientation) {
    auto section = valid_section();
    section.orientation.y = std::numeric_limits<double>::quiet_NaN();

    const auto result = std::move(make_builder(
                                      valid_material(),
                                      section,
                                      valid_element(),
                                      valid_step()))
                            .build();

    EXPECT_FALSE(result.domain.has_value());
    EXPECT_TRUE(has_diagnostic(result, "model.nonfinite_value"));
}

TEST(DomainValidation, RejectsNonfiniteRecoveryPoint) {
    auto section = valid_section();
    const double nan = std::numeric_limits<double>::quiet_NaN();
    section.recovery_points = {{nan, 0.0}};

    const auto result = std::move(make_builder(
                                      valid_material(),
                                      section,
                                      valid_element(),
                                      valid_step()))
                            .build();

    EXPECT_FALSE(result.domain.has_value());
    EXPECT_TRUE(has_diagnostic(result, "model.nonfinite_value"));
}

TEST(DomainValidation, RejectsNonfinitePrescribedValue) {
    auto step = valid_step();
    step.prescribed_dofs[0].value =
        std::numeric_limits<double>::infinity();

    const auto result = std::move(make_builder(
                                      valid_material(),
                                      valid_section(),
                                      valid_element(),
                                      step))
                            .build();

    EXPECT_FALSE(result.domain.has_value());
    EXPECT_TRUE(has_diagnostic(result, "model.nonfinite_value"));
}

TEST(DomainValidation, RejectsNonfiniteLoadComponent) {
    auto step = valid_step();
    step.nodal_loads[0].values[0] =
        std::numeric_limits<double>::quiet_NaN();

    const auto result = std::move(make_builder(
                                      valid_material(),
                                      valid_section(),
                                      valid_element(),
                                      step))
                            .build();

    EXPECT_FALSE(result.domain.has_value());
    EXPECT_TRUE(has_diagnostic(result, "model.nonfinite_value"));
}

TEST(DomainValidation, CollectsIndependentMaterialPropertyDiagnostics) {
    auto material = valid_material();
    material.young = std::numeric_limits<double>::quiet_NaN();
    material.poisson = 0.5;

    const auto result = std::move(make_builder(
                                      material,
                                      valid_section(),
                                      valid_element(),
                                      valid_step()))
                            .build();

    EXPECT_FALSE(result.domain.has_value());
    EXPECT_TRUE(has_diagnostic(result, "model.nonfinite_value"));
    EXPECT_TRUE(has_diagnostic(result, "model.invalid_material"));
}

TEST(DomainValidation, CollectsIndependentSectionPropertyDiagnostics) {
    auto section = valid_section();
    section.area = -1.0;
    section.iy = std::numeric_limits<double>::quiet_NaN();

    const auto result = std::move(make_builder(
                                      valid_material(),
                                      section,
                                      valid_element(),
                                      valid_step()))
                            .build();

    EXPECT_FALSE(result.domain.has_value());
    EXPECT_TRUE(has_diagnostic(result, "model.nonfinite_value"));
    EXPECT_TRUE(has_diagnostic(result, "model.invalid_section"));
}

TEST(DomainValidation, CollectsNonfiniteStepValuesIndependently) {
    const double nan = std::numeric_limits<double>::quiet_NaN();
    const double infinity = std::numeric_limits<double>::infinity();
    auto step = fesa::StepDefinition{
        "Load",
        {{fesa::NodeId{0}, 1, infinity}},
        {{fesa::NodeId{1}, {nan, 0.0, 0.0, 0.0, 0.0, 0.0}}},
    };

    const auto result = std::move(make_builder(
                                      valid_material(),
                                      valid_section(),
                                      valid_element(),
                                      step))
                            .build();

    EXPECT_FALSE(result.domain.has_value());
    EXPECT_EQ(diagnostic_count(result, "model.nonfinite_value"), 2);
}

TEST(DomainValidation, RejectsZeroLengthElement) {
    auto element = valid_element();
    element.nodes[1] = fesa::NodeId{0};

    const auto result = std::move(make_builder(
                                      valid_material(),
                                      valid_section(),
                                      element,
                                      valid_step()))
                            .build();

    EXPECT_FALSE(result.domain.has_value());
    EXPECT_TRUE(has_diagnostic(result, "model.zero_length_element"));
}

TEST(DomainValidation, RejectsZeroAndElementParallelOrientation) {
    for (const fesa::Vec3 orientation :
         {fesa::Vec3{0.0, 0.0, 0.0}, fesa::Vec3{1.0, 0.0, 0.0}}) {
        auto section = valid_section();
        section.orientation = orientation;

        const auto result = std::move(make_builder(
                                          valid_material(),
                                          section,
                                          valid_element(),
                                          valid_step()))
                                .build();

        EXPECT_FALSE(result.domain.has_value());
        EXPECT_TRUE(has_diagnostic(result, "model.invalid_orientation"));
    }
}

TEST(DomainValidation, RejectsElementWithoutMaterialAndSectionAssignments) {
    auto element = valid_element();
    element.material = fesa::MaterialId{8};
    element.section = fesa::SectionId{9};

    const auto result = std::move(make_builder(
                                      valid_material(),
                                      valid_section(),
                                      element,
                                      valid_step()))
                            .build();

    EXPECT_FALSE(result.domain.has_value());
    EXPECT_TRUE(has_diagnostic(result, "model.missing_material_reference"));
    EXPECT_TRUE(has_diagnostic(result, "model.missing_section_reference"));
}

TEST(DomainValidation, RejectsConflictingBoundaryConditions) {
    auto step = valid_step();
    step.prescribed_dofs.push_back({fesa::NodeId{0}, 1, 0.25});

    const auto result = std::move(make_builder(
                                      valid_material(),
                                      valid_section(),
                                      valid_element(),
                                      step))
                            .build();

    EXPECT_FALSE(result.domain.has_value());
    EXPECT_TRUE(
        has_diagnostic(result, "model.conflicting_boundary_condition"));
}

TEST(DomainValidation, CollectsIndependentDiagnosticsWithoutEarlyExit) {
    auto material = valid_material();
    material.young = -1.0;
    auto section = valid_section();
    section.area = -1.0;
    section.orientation = fesa::Vec3{1.0, 0.0, 0.0};

    const auto result = std::move(make_builder(
                                      material,
                                      section,
                                      valid_element(),
                                      valid_step()))
                            .build();

    EXPECT_FALSE(result.domain.has_value());
    EXPECT_TRUE(has_diagnostic(result, "model.invalid_material"));
    EXPECT_TRUE(has_diagnostic(result, "model.invalid_section"));
    EXPECT_TRUE(has_diagnostic(result, "model.invalid_orientation"));
}

}  // namespace
