#include <fesa/constraints/essential_bc.hpp>

#include <algorithm>
#include <array>
#include <cstddef>
#include <cstdint>
#include <stdexcept>
#include <utility>
#include <vector>

#include <fesa/model/domain_builder.hpp>

#include <gtest/gtest.h>

namespace {

fesa::DofManager build_dofs(
    std::vector<fesa::PrescribedDof> prescribed) {
    fesa::DomainBuilder builder;
    builder.add_node({
        fesa::NodeId{0},
        fesa::EntityOrigin{"", "", 1},
        fesa::Vec3{0.0, 0.0, 0.0},
    });
    builder.set_step({"Load", std::move(prescribed), {}});
    auto result = std::move(builder).build();
    if (!result.domain.has_value()) {
        throw std::runtime_error{"Test Domain failed validation."};
    }
    return fesa::DofManager::build(*result.domain);
}

fesa::EquationSystem hand_system() {
    return {
        {
            6,
            {0, 3, 5, 6, 7, 8, 9},
            {0, 1, 2, 1, 2, 2, 3, 4, 5},
            {4.0, 1.0, 2.0, 3.0, 6.0, 5.0, 1.0, 1.0, 1.0},
        },
        {7.0, 8.0, 9.0, 0.0, 0.0, 0.0},
    };
}

TEST(EssentialBc, ZeroPrescribedValueDoesNotShiftReducedForce) {
    const std::vector<fesa::PrescribedDof> prescribed{
        {fesa::NodeId{0}, 2, 0.0},
    };
    const fesa::DofManager dofs = build_dofs(prescribed);

    const fesa::ConstraintResult result =
        fesa::eliminate_essential_bcs(hand_system(), dofs, prescribed);

    ASSERT_TRUE(result.reduced_system.has_value());
    EXPECT_TRUE(result.diagnostics.empty());
    EXPECT_EQ(
        result.reduced_system->force,
        (std::vector<double>{7.0, 9.0, 0.0, 0.0, 0.0}));
}

TEST(
    ConstraintElimination,
    ShiftsNonzeroPrescribedValueAndPreservesOriginalSystem) {
    const std::vector<fesa::PrescribedDof> prescribed{
        {fesa::NodeId{0}, 2, 2.0},
    };
    const fesa::DofManager dofs = build_dofs(prescribed);
    const fesa::EquationSystem original = hand_system();
    const fesa::EquationSystem before = original;

    const fesa::ConstraintResult result =
        fesa::eliminate_essential_bcs(original, dofs, prescribed);

    ASSERT_TRUE(result.reduced_system.has_value());
    EXPECT_TRUE(result.diagnostics.empty());
    const fesa::ReducedSystem& reduced = *result.reduced_system;
    EXPECT_EQ(reduced.stiffness.order, 5);
    EXPECT_EQ(
        reduced.stiffness.row_offsets,
        (std::vector<std::int32_t>{0, 2, 3, 4, 5, 6}));
    EXPECT_EQ(
        reduced.stiffness.column_indices,
        (std::vector<std::int32_t>{0, 1, 1, 2, 3, 4}));
    EXPECT_EQ(
        reduced.stiffness.values,
        (std::vector<double>{4.0, 2.0, 5.0, 1.0, 1.0, 1.0}));
    EXPECT_EQ(
        reduced.force,
        (std::vector<double>{5.0, -3.0, 0.0, 0.0, 0.0}));
    EXPECT_EQ(
        reduced.free_to_full,
        (std::vector<std::size_t>{0, 2, 3, 4, 5}));
    EXPECT_EQ(
        reduced.prescribed_full,
        (std::vector<double>{0.0, 2.0, 0.0, 0.0, 0.0, 0.0}));

    EXPECT_EQ(original.stiffness.order, before.stiffness.order);
    EXPECT_EQ(
        original.stiffness.row_offsets,
        before.stiffness.row_offsets);
    EXPECT_EQ(
        original.stiffness.column_indices,
        before.stiffness.column_indices);
    EXPECT_EQ(original.stiffness.values, before.stiffness.values);
    EXPECT_EQ(original.force, before.force);

    EXPECT_EQ(
        dofs.reconstruct_full(
            std::vector<double>{1.0, 3.0, 0.0, 0.0, 0.0}),
        (std::vector<double>{1.0, 2.0, 3.0, 0.0, 0.0, 0.0}));
}

TEST(ConstraintElimination, AcceptsAllDofsConstrained) {
    const std::vector<fesa::PrescribedDof> prescribed{
        {fesa::NodeId{0}, 1, 1.0},
        {fesa::NodeId{0}, 2, 2.0},
        {fesa::NodeId{0}, 3, 3.0},
        {fesa::NodeId{0}, 4, 4.0},
        {fesa::NodeId{0}, 5, 5.0},
        {fesa::NodeId{0}, 6, 6.0},
    };
    const fesa::DofManager dofs = build_dofs(prescribed);

    const fesa::ConstraintResult result =
        fesa::eliminate_essential_bcs(hand_system(), dofs, prescribed);

    ASSERT_TRUE(result.reduced_system.has_value());
    const fesa::ReducedSystem& reduced = *result.reduced_system;
    EXPECT_EQ(reduced.stiffness.order, 0);
    EXPECT_EQ(
        reduced.stiffness.row_offsets,
        (std::vector<std::int32_t>{0}));
    EXPECT_TRUE(reduced.stiffness.column_indices.empty());
    EXPECT_TRUE(reduced.stiffness.values.empty());
    EXPECT_TRUE(reduced.force.empty());
    EXPECT_TRUE(reduced.free_to_full.empty());
    EXPECT_EQ(
        reduced.prescribed_full,
        (std::vector<double>{1.0, 2.0, 3.0, 4.0, 5.0, 6.0}));
    EXPECT_EQ(
        dofs.reconstruct_full({}),
        reduced.prescribed_full);
}

TEST(ConstraintElimination, RejectsConflictingPrescribedValues) {
    const fesa::DofManager dofs = build_dofs({
        {fesa::NodeId{0}, 2, 2.0},
    });
    const std::vector<fesa::PrescribedDof> conflicting{
        {fesa::NodeId{0}, 2, 2.0},
        {fesa::NodeId{0}, 2, 3.0},
    };

    const fesa::ConstraintResult result =
        fesa::eliminate_essential_bcs(
            hand_system(), dofs, conflicting);

    EXPECT_FALSE(result.reduced_system.has_value());
    ASSERT_FALSE(result.diagnostics.empty());
    EXPECT_NE(
        std::ranges::find(
            result.diagnostics,
            "equation.conflicting_prescribed_dof",
            &fesa::Diagnostic::code),
        result.diagnostics.end());
}

TEST(Reaction, UsesOriginalFullEquilibriumEquation) {
    const std::vector<double> reaction = fesa::recover_reaction(
        hand_system(),
        std::vector<double>{1.0, 2.0, 3.0, 0.0, 0.0, 0.0});

    EXPECT_EQ(
        reaction,
        (std::vector<double>{5.0, 17.0, 20.0, 0.0, 0.0, 0.0}));
}

}  // namespace
