#include <gtest/gtest.h>

#include <algorithm>
#include <array>
#include <cmath>
#include <limits>
#include <optional>
#include <span>
#include <string>
#include <string_view>
#include <utility>
#include <vector>

#include <fesa/validation/comparison.hpp>

namespace {

std::array<double, 6> values(
    const double first,
    const double second,
    const double third,
    const double fourth,
    const double fifth,
    const double sixth) {
    return {first, second, third, fourth, fifth, sixth};
}

fesa::ResultFrame result_frame() {
    fesa::NodalFrame nodal{
        {fesa::NodeId{10}, fesa::NodeId{11}, fesa::NodeId{12}},
        {
            fesa::EntityOrigin{"BeamPart", "Part-1-1", 101},
            fesa::EntityOrigin{"BeamPart", "Part-1-1", 102},
            fesa::EntityOrigin{"BeamPart", "Part-1-1", 103},
        },
        {
            values(1.0, 2.0, 3.0, 4.0, 5.0, 6.0),
            values(7.0, 8.0, 9.0, 10.0, 11.0, 12.0),
            values(0.0, 0.0, 0.0, 0.0, 0.0, 0.0),
        },
        {
            values(13.0, 14.0, 15.0, 16.0, 17.0, 18.0),
            values(19.0, 20.0, 21.0, 22.0, 23.0, 24.0),
            values(0.0, 0.0, 0.0, 0.0, 0.0, 0.0),
        },
    };
    fesa::BeamElementFrame beam{
        fesa::ElementId{20},
        fesa::EntityOrigin{"BeamPart", "Part-1-1", 501},
        {
            fesa::Vec3{1.0, 0.0, 0.0},
            fesa::Vec3{0.0, 1.0, 0.0},
            fesa::Vec3{0.0, 0.0, 1.0},
        },
        {{
            {
                -1.0,
                fesa::NodeId{10},
                values(0.0, 0.0, 0.0, 0.0, 0.0, 0.0),
                values(25.0, 26.0, 27.0, 28.0, 29.0, 30.0),
                31.0,
                {},
            },
            {
                1.0,
                fesa::NodeId{11},
                values(0.0, 0.0, 0.0, 0.0, 0.0, 0.0),
                values(32.0, 33.0, 34.0, 35.0, 36.0, 37.0),
                38.0,
                {},
            },
        }},
    };
    return {1.0, std::move(nodal), {{std::move(beam)}}, {}};
}

fesa::ComparisonSample sample(
    const fesa::ReferenceQuantity quantity,
    fesa::ResultPosition position,
    std::vector<double> reference,
    std::vector<double> actual,
    const fesa::Tolerance tolerance) {
    return {
        quantity,
        std::move(position),
        std::move(reference),
        std::move(actual),
        tolerance,
    };
}

bool has_failure(
    const std::vector<fesa::Diagnostic>& failures,
    const std::string_view code) {
    return std::ranges::any_of(
        failures,
        [code](const fesa::Diagnostic& failure) {
            return failure.stage == fesa::DiagnosticStage::validation &&
                   failure.severity == fesa::Severity::error &&
                   failure.code == code;
        });
}

TEST(ComparisonMetric, RejectsNearZeroErrorBeyondAbsoluteScale) {
    const auto input = sample(
        fesa::ReferenceQuantity::displacement,
        {"Part-1-1", 101, std::nullopt},
        {0.0, 0.0, 0.0, 0.0, 0.0, 0.0},
        {2.0e-9, 0.0, 0.0, 0.0, 0.0, 0.0},
        {1.0e-5, 1.0e-9});

    const auto report = fesa::compare_samples(
        std::span<const fesa::ComparisonSample>{&input, 1});

    ASSERT_FALSE(report.passed);
    EXPECT_DOUBLE_EQ(report.maximum_normalized_error, 2.0);
    ASSERT_EQ(report.failures.size(), 1U);
    EXPECT_EQ(report.failures[0].code, "validation.tolerance_exceeded");
    EXPECT_NE(
        report.failures[0].message.find("quantity=displacement"),
        std::string::npos);
    EXPECT_NE(
        report.failures[0].message.find("entity=101"),
        std::string::npos);
    EXPECT_NE(
        report.failures[0].message.find("component=Ux"),
        std::string::npos);
    EXPECT_NE(
        report.failures[0].message.find("reference="),
        std::string::npos);
    EXPECT_NE(
        report.failures[0].message.find("actual="),
        std::string::npos);
    EXPECT_NE(
        report.failures[0].message.find("normalized_error="),
        std::string::npos);
    EXPECT_NE(
        report.failures[0].message.find("relative_tolerance="),
        std::string::npos);
    EXPECT_NE(
        report.failures[0].message.find("absolute_scale="),
        std::string::npos);
}

TEST(ComparisonMetric, RejectsRepresentativeLargeRelativeError) {
    const auto input = sample(
        fesa::ReferenceQuantity::reaction,
        {"Part-1-1", 101, std::nullopt},
        {1.0e6, 1.0e6, 1.0e6, 1.0e6, 1.0e6, 1.0e6},
        {1.0e6 + 20.0, 1.0e6, 1.0e6, 1.0e6, 1.0e6, 1.0e6},
        {1.0e-5, 0.0});

    const auto report = fesa::compare_samples(
        std::span<const fesa::ComparisonSample>{&input, 1});

    EXPECT_FALSE(report.passed);
    EXPECT_DOUBLE_EQ(report.maximum_normalized_error, 2.0);
    EXPECT_TRUE(has_failure(
        report.failures, "validation.tolerance_exceeded"));
}

TEST(ComparisonMetric, AcceptsErrorsAtTheExplicitToleranceBoundary) {
    const std::vector<fesa::ComparisonSample> inputs{
        sample(
            fesa::ReferenceQuantity::displacement,
            {"Part-1-1", 101, std::nullopt},
            {0.0, 0.0, 0.0, 0.0, 0.0, 0.0},
            {1.0e-9, 0.0, 0.0, 0.0, 0.0, 0.0},
            {1.0e-5, 1.0e-9}),
        sample(
            fesa::ReferenceQuantity::reaction,
            {"Part-1-1", 101, std::nullopt},
            {1.0e6, 1.0e6, 1.0e6, 1.0e6, 1.0e6, 1.0e6},
            {1.0e6 + 10.0, 1.0e6, 1.0e6, 1.0e6, 1.0e6, 1.0e6},
            {1.0e-5, 0.0}),
    };

    const auto report = fesa::compare_samples(inputs);

    EXPECT_TRUE(report.passed);
    EXPECT_DOUBLE_EQ(report.maximum_normalized_error, 1.0);
    EXPECT_TRUE(report.failures.empty());
}

TEST(ComparisonMetric, RejectsNonfiniteValues) {
    const auto input = sample(
        fesa::ReferenceQuantity::centroid_stress,
        {"Part-1-1", 501, 101},
        {10.0},
        {std::numeric_limits<double>::quiet_NaN()},
        {1.0e-5, 1.0e-6});

    const auto report = fesa::compare_samples(
        std::span<const fesa::ComparisonSample>{&input, 1});

    EXPECT_FALSE(report.passed);
    EXPECT_TRUE(std::isinf(report.maximum_normalized_error));
    EXPECT_TRUE(has_failure(
        report.failures, "validation.nonfinite_comparison_value"));
}

TEST(ComparisonMetric, RejectsDuplicateQuantityAndPosition) {
    const std::vector<fesa::ComparisonSample> inputs{
        sample(
            fesa::ReferenceQuantity::displacement,
            {"Part-1-1", 101, std::nullopt},
            {0.0, 0.0, 0.0, 0.0, 0.0, 0.0},
            {0.0, 0.0, 0.0, 0.0, 0.0, 0.0},
            {1.0e-5, 1.0e-9}),
        sample(
            fesa::ReferenceQuantity::displacement,
            {"Part-1-1", 101, std::nullopt},
            {1.0, 1.0, 1.0, 1.0, 1.0, 1.0},
            {1.0, 1.0, 1.0, 1.0, 1.0, 1.0},
            {1.0e-5, 1.0e-9}),
    };

    const auto report = fesa::compare_samples(inputs);

    EXPECT_FALSE(report.passed);
    EXPECT_TRUE(std::isinf(report.maximum_normalized_error));
    EXPECT_TRUE(has_failure(
        report.failures, "validation.duplicate_result_position"));
}

TEST(ComparisonMetric, RejectsComponentCountMismatch) {
    const auto input = sample(
        fesa::ReferenceQuantity::internal_force,
        {"Part-1-1", 501, 101},
        {1.0, 2.0, 3.0, 4.0, 5.0, 6.0},
        {1.0, 2.0, 3.0, 4.0, 5.0},
        {1.0e-5, 1.0e-9});

    const auto report = fesa::compare_samples(
        std::span<const fesa::ComparisonSample>{&input, 1});

    EXPECT_FALSE(report.passed);
    EXPECT_TRUE(std::isinf(report.maximum_normalized_error));
    EXPECT_TRUE(has_failure(
        report.failures, "validation.component_count_mismatch"));
}

TEST(CorrelationMetric, ComputesComponentWiseRmseAndRelativeL2) {
    const std::vector<fesa::ComparisonSample> inputs{
        sample(
            fesa::ReferenceQuantity::displacement,
            {"Part-1-1", 101, std::nullopt},
            {3.0, 0.0, 0.0, 0.0, 0.0, 0.0},
            {0.0, 0.0, 0.0, 0.0, 0.0, 0.0},
            {0.0, 1.0e-9}),
        sample(
            fesa::ReferenceQuantity::displacement,
            {"Part-1-1", 102, std::nullopt},
            {4.0, 0.0, 0.0, 0.0, 0.0, 0.0},
            {0.0, 0.0, 0.0, 0.0, 0.0, 0.0},
            {0.0, 1.0e-9}),
    };

    const auto report = fesa::correlate_samples(inputs);

    ASSERT_TRUE(report.evaluable);
    ASSERT_TRUE(report.failures.empty());
    ASSERT_EQ(report.metrics.size(), 6U);
    EXPECT_EQ(report.metrics[0].quantity, fesa::ReferenceQuantity::displacement);
    EXPECT_EQ(report.metrics[0].component_index, 0U);
    EXPECT_EQ(report.metrics[0].value_count, 2U);
    EXPECT_DOUBLE_EQ(
        report.metrics[0].root_mean_square_error,
        std::sqrt(12.5));
    EXPECT_DOUBLE_EQ(report.metrics[0].relative_l2_error, 1.0);
    EXPECT_DOUBLE_EQ(report.metrics[1].root_mean_square_error, 0.0);
    EXPECT_DOUBLE_EQ(report.metrics[1].relative_l2_error, 0.0);
}

TEST(CorrelationMetric, UsesAbsoluteScaleNormForNearZeroReference) {
    const std::vector<fesa::ComparisonSample> inputs{
        sample(
            fesa::ReferenceQuantity::reaction,
            {"Part-1-1", 101, std::nullopt},
            {0.0, 0.0, 0.0, 0.0, 0.0, 0.0},
            {3.0e-9, 0.0, 0.0, 0.0, 0.0, 0.0},
            {0.0, 1.0e-9}),
        sample(
            fesa::ReferenceQuantity::reaction,
            {"Part-1-1", 102, std::nullopt},
            {0.0, 0.0, 0.0, 0.0, 0.0, 0.0},
            {4.0e-9, 0.0, 0.0, 0.0, 0.0, 0.0},
            {0.0, 1.0e-9}),
    };

    const auto report = fesa::correlate_samples(inputs);

    ASSERT_TRUE(report.evaluable);
    ASSERT_EQ(report.metrics.size(), 6U);
    EXPECT_DOUBLE_EQ(
        report.metrics[0].root_mean_square_error,
        std::sqrt(12.5) * 1.0e-9);
    EXPECT_DOUBLE_EQ(
        report.metrics[0].relative_l2_error,
        5.0 / std::sqrt(2.0));
}

TEST(EntityMatching, MatchesNodalResultByInstanceAndExternalLabel) {
    const auto frame = result_frame();
    const std::array reference{0.0, 0.0, 0.0, 0.0, 0.0, 0.0};

    const auto match = fesa::make_comparison_sample(
        frame,
        fesa::ReferenceQuantity::displacement,
        {"Part-1-1", 102, std::nullopt},
        reference,
        {1.0e-5, 1.0e-9});

    ASSERT_TRUE(match.sample.has_value());
    EXPECT_TRUE(match.failures.empty());
    EXPECT_EQ(
        match.sample->actual,
        (std::vector<double>{7.0, 8.0, 9.0, 10.0, 11.0, 12.0}));
}

TEST(EntityMatching, MatchesReactionComponentsWithoutUsingDisplacement) {
    const auto frame = result_frame();
    const std::array reference{0.0, 0.0, 0.0, 0.0, 0.0, 0.0};

    const auto match = fesa::make_comparison_sample(
        frame,
        fesa::ReferenceQuantity::reaction,
        {"Part-1-1", 101, std::nullopt},
        reference,
        {1.0e-5, 1.0e-9});

    ASSERT_TRUE(match.sample.has_value());
    EXPECT_TRUE(match.failures.empty());
    EXPECT_EQ(
        match.sample->actual,
        (std::vector<double>{13.0, 14.0, 15.0, 16.0, 17.0, 18.0}));
}

TEST(EntityMatching, MatchesElementResultByElementAndEndNodeLabels) {
    const auto frame = result_frame();
    const std::array reference{0.0, 0.0, 0.0, 0.0, 0.0, 0.0};

    const auto match = fesa::make_comparison_sample(
        frame,
        fesa::ReferenceQuantity::internal_force,
        {"Part-1-1", 501, 102},
        reference,
        {1.0e-5, 1.0e-9});

    ASSERT_TRUE(match.sample.has_value());
    EXPECT_TRUE(match.failures.empty());
    EXPECT_EQ(
        match.sample->actual,
        (std::vector<double>{32.0, 33.0, 34.0, 35.0, 36.0, 37.0}));
}

TEST(EntityMatching, MatchesCentroidStressAtTheRequestedElementEnd) {
    const auto frame = result_frame();
    const std::array reference{0.0};

    const auto match = fesa::make_comparison_sample(
        frame,
        fesa::ReferenceQuantity::centroid_stress,
        {"Part-1-1", 501, 101},
        reference,
        {1.0e-5, 1.0e-9});

    ASSERT_TRUE(match.sample.has_value());
    EXPECT_TRUE(match.failures.empty());
    EXPECT_EQ(match.sample->actual, (std::vector<double>{31.0}));
}

TEST(EntityMatching, RejectsUnknownResultOrigin) {
    const auto frame = result_frame();
    const std::array reference{0.0, 0.0, 0.0, 0.0, 0.0, 0.0};

    const auto match = fesa::make_comparison_sample(
        frame,
        fesa::ReferenceQuantity::reaction,
        {"Part-1-1", 999, std::nullopt},
        reference,
        {1.0e-5, 1.0e-9});

    EXPECT_FALSE(match.sample.has_value());
    EXPECT_TRUE(has_failure(
        match.failures, "validation.unknown_result_origin"));
}

TEST(EntityMatching, RejectsNodeThatIsNotAnEndOfTheElement) {
    const auto frame = result_frame();
    const std::array reference{0.0};

    const auto match = fesa::make_comparison_sample(
        frame,
        fesa::ReferenceQuantity::centroid_stress,
        {"Part-1-1", 501, 103},
        reference,
        {1.0e-5, 1.0e-9});

    EXPECT_FALSE(match.sample.has_value());
    EXPECT_TRUE(has_failure(
        match.failures, "validation.invalid_element_node_pair"));
}

}  // namespace
