#include "fesa/solvers/linear/linear_solver.hpp"
#include "fesa/solvers/linear/mkl_pardiso_solver.hpp"

#include "fesa/fem/dof_manager.hpp"
#include "fesa/math/sparse_matrix.hpp"

#include <gtest/gtest.h>

#include <type_traits>
#include <utility>
#include <vector>

namespace {

fesa::SparseMatrix makeDenseCsr(
    const std::size_t rows,
    const std::size_t columns,
    const std::vector<double>& values) {
    EXPECT_EQ(values.size(), rows * columns);

    fesa::SparsePattern pattern;
    std::vector<fesa::CooContribution> contributions;
    pattern.rowOffsets.reserve(rows + 1U);
    pattern.rowOffsets.push_back(0U);
    for (std::size_t row = 0U; row < rows; ++row) {
        for (std::size_t column = 0U; column < columns; ++column) {
            pattern.columnIndices.push_back(column);
            contributions.push_back({
                row,
                column,
                values[row * columns + column],
                row,
                column});
        }
        pattern.rowOffsets.push_back(pattern.columnIndices.size());
    }

    auto matrix = fesa::SparseMatrix::fromCoo(
        rows, columns, std::move(contributions), pattern);
    EXPECT_TRUE(matrix.hasValue());
    return std::move(matrix.value());
}

void expectSolverFailure(const fesa::Status& status) {
    EXPECT_FALSE(status.isOk());
    EXPECT_EQ(status.failureCategory(), fesa::FailureCategory::solver);
    ASSERT_FALSE(status.diagnostics().empty());
    EXPECT_EQ(status.diagnostics().front().severity, fesa::Severity::error);
}

} // namespace

TEST(MklPardisoSolver, RejectsInvalidCsrStateAndDimensions) {
    static_assert(std::is_base_of_v<fesa::LinearSolver, fesa::MklPardisoSolver>);
    static_assert(std::has_virtual_destructor_v<fesa::LinearSolver>);

    fesa::MklPardisoSolver solver;
    fesa::Vector untouched{2U};
    untouched[0U] = 17.0;
    untouched[1U] = -4.0;
    const auto beforeFactorize =
        solver.solve(fesa::Vector{2U, 1.0}, untouched);
    expectSolverFailure(beforeFactorize);
    EXPECT_EQ(
        beforeFactorize.diagnostics().front().code,
        "solver-not-factorized");
    EXPECT_DOUBLE_EQ(untouched[0U], 17.0);
    EXPECT_DOUBLE_EQ(untouched[1U], -4.0);

    // A fully constrained model has a valid 0x0 Kff. It still observes the
    // factorize-then-solve lifecycle without invoking a numerical backend.
    const auto empty = makeDenseCsr(0U, 0U, {});
    ASSERT_TRUE(solver.factorize(empty).isOk());
    fesa::Vector emptySolution{0U};
    EXPECT_TRUE(solver.solve(fesa::Vector{0U}, emptySolution).isOk());
    EXPECT_EQ(emptySolution.size(), 0U);

    // Refactorization from the trivial state must establish ordinary PARDISO
    // state rather than retaining a zero-equation shortcut.
    const auto spd = makeDenseCsr(2U, 2U, {4.0, 1.0, 1.0, 3.0});
    ASSERT_TRUE(solver.factorize(spd).isOk());
    fesa::Vector solution{2U};
    ASSERT_TRUE(solver.solve(fesa::Vector{2U, 1.0}, solution).isOk());
    EXPECT_NEAR(solution[0U], 2.0 / 11.0, 1.0e-14);
    EXPECT_NEAR(solution[1U], 3.0 / 11.0, 1.0e-14);

    const double solvedFirst = solution[0U];
    const double solvedSecond = solution[1U];
    expectSolverFailure(solver.solve(fesa::Vector{1U, 1.0}, solution));
    EXPECT_DOUBLE_EQ(solution[0U], solvedFirst);
    EXPECT_DOUBLE_EQ(solution[1U], solvedSecond);

    fesa::Vector wrongSolution{1U};
    wrongSolution[0U] = 41.0;
    expectSolverFailure(solver.solve(fesa::Vector{2U, 1.0}, wrongSolution));
    EXPECT_DOUBLE_EQ(wrongSolution[0U], 41.0);

    const auto rectangular = makeDenseCsr(
        2U, 3U, {2.0, 0.0, 0.0, 0.0, 3.0, 0.0});
    const auto rectangularStatus = solver.factorize(rectangular);
    expectSolverFailure(rectangularStatus);
    EXPECT_EQ(
        rectangularStatus.diagnostics().front().code,
        "solver-matrix-not-square");

    fesa::SparsePattern invalidPattern{{0U, 2U}, {0U}};
    auto invalidCsr = fesa::SparseMatrix::fromCoo(
        1U,
        1U,
        {{0U, 0U, 1.0, 0U, 0U}},
        invalidPattern);
    EXPECT_FALSE(invalidCsr.hasValue());

    const auto nonsymmetric = makeDenseCsr(2U, 2U, {2.0, 1.0, 0.0, 3.0});
    const auto nonsymmetricStatus = solver.factorize(nonsymmetric);
    expectSolverFailure(nonsymmetricStatus);
    EXPECT_EQ(
        nonsymmetricStatus.diagnostics().front().code,
        "solver-matrix-not-symmetric");

    const auto scaledNonsymmetric = makeDenseCsr(
        2U, 2U, {2.0e-20, 1.0e-20, 1.1e-20, 3.0e-20});
    const auto scaledNonsymmetricStatus =
        solver.factorize(scaledNonsymmetric);
    // Stop this case before inspecting diagnostics when the production code
    // incorrectly accepts the matrix; this keeps the RED failure deterministic.
    ASSERT_FALSE(scaledNonsymmetricStatus.isOk());
    expectSolverFailure(scaledNonsymmetricStatus);
    EXPECT_EQ(
        scaledNonsymmetricStatus.diagnostics().front().code,
        "solver-matrix-not-symmetric");

    fesa::SparsePattern noDiagonalPattern{{0U, 1U, 2U}, {1U, 0U}};
    auto noDiagonal = fesa::SparseMatrix::fromCoo(
        2U,
        2U,
        {{0U, 1U, 1.0, 0U, 0U}, {1U, 0U, 1.0, 1U, 0U}},
        noDiagonalPattern);
    ASSERT_TRUE(noDiagonal.hasValue());
    const auto noDiagonalStatus = solver.factorize(noDiagonal.value());
    expectSolverFailure(noDiagonalStatus);
    EXPECT_EQ(
        noDiagonalStatus.diagnostics().front().code,
        "solver-missing-diagonal");

}
