#include "fesa/analysis/analysis_state.hpp"

#include <gtest/gtest.h>

#include <filesystem>
#include <iterator>
#include <type_traits>
#include <utility>

namespace {

template<class T, class = void>
struct HasVelocity : std::false_type {};

template<class T>
struct HasVelocity<T, std::void_t<decltype(std::declval<T&>().velocity())>>
    : std::true_type {};

template<class T, class = void>
struct HasAcceleration : std::false_type {};

template<class T>
struct HasAcceleration<
    T, std::void_t<decltype(std::declval<T&>().acceleration())>>
    : std::true_type {};

template<class T, class = void>
struct HasTemperature : std::false_type {};

template<class T>
struct HasTemperature<T, std::void_t<decltype(std::declval<T&>().temperature())>>
    : std::true_type {};

template<class T, class = void>
struct HasIterationHistory : std::false_type {};

template<class T>
struct HasIterationHistory<
    T, std::void_t<decltype(std::declval<T&>().iterationHistory())>>
    : std::true_type {};

template<class T, class = void>
struct HasNonlinearState : std::false_type {};

template<class T>
struct HasNonlinearState<
    T, std::void_t<decltype(std::declval<T&>().nonlinearState())>>
    : std::true_type {};

fesa::DofManager makeDofs() {
    fesa::ModelDefinition definition{};
    definition.sourcePath = "models/analysis-state.inp";
    definition.sourceContentIdentity = "fnv1a64:0123456789abcdef";
    definition.nodes = {
        {{"Beam-1", 1, "1"}, {0.0, 0.0, 0.0}, {definition.sourcePath, 10U}},
        {{"Beam-1", 2, "2"}, {1.0, 0.0, 0.0}, {definition.sourcePath, 11U}}};
    definition.steps = {{
        "Step-1",
        {{"1", 1, 6, 0.0, {definition.sourcePath, 20U}}},
        {},
        0.1,
        1.0,
        0.01,
        1.0,
        {definition.sourcePath, 19U}}};

    auto domain = fesa::Domain::create(std::move(definition));
    EXPECT_TRUE(domain.hasValue());
    auto model = fesa::AnalysisModel::create(domain.value());
    EXPECT_TRUE(model.hasValue());
    auto dofs = fesa::DofManager::create(model.value());
    EXPECT_TRUE(dofs.hasValue());
    return std::move(dofs.value());
}

void expectAllZero(const fesa::Vector& vector) {
    for (std::size_t index = 0U; index < vector.size(); ++index) {
        EXPECT_DOUBLE_EQ(vector[index], 0.0);
    }
}

} // namespace

TEST(AnalysisState, AllocatesOnlyV0FullVectors) {
    const auto dofs = makeDofs();
    ASSERT_EQ(dofs.fullDofCount(), 12U);
    ASSERT_EQ(dofs.constrainedDofCount(), 6U);

    auto state = fesa::AnalysisState::create(dofs, {"Step-1", 0U});
    const fesa::AnalysisState& constState = state;
    const fesa::Vector* const vectors[] = {
        &constState.displacement(),
        &constState.externalForce(),
        &constState.internalForce(),
        &constState.residual(),
        &constState.reaction()};

    for (const auto* vector : vectors) {
        EXPECT_EQ(vector->size(), dofs.fullDofCount());
        expectAllZero(*vector);
    }
    for (std::size_t left = 0U; left < std::size(vectors); ++left) {
        for (std::size_t right = left + 1U; right < std::size(vectors); ++right) {
            EXPECT_NE(vectors[left]->data(), vectors[right]->data());
        }
    }

    state.displacement()[0] = 4.0;
    state.reaction()[11] = -9.0;
    EXPECT_DOUBLE_EQ(state.displacement()[0], 4.0);
    EXPECT_DOUBLE_EQ(state.reaction()[11], -9.0);
    EXPECT_DOUBLE_EQ(state.externalForce()[0], 0.0);
    EXPECT_DOUBLE_EQ(state.internalForce()[0], 0.0);
    EXPECT_DOUBLE_EQ(state.residual()[0], 0.0);

    EXPECT_FALSE(HasVelocity<fesa::AnalysisState>::value);
    EXPECT_FALSE(HasAcceleration<fesa::AnalysisState>::value);
    EXPECT_FALSE(HasTemperature<fesa::AnalysisState>::value);
    EXPECT_FALSE(HasIterationHistory<fesa::AnalysisState>::value);
    EXPECT_FALSE(HasNonlinearState<fesa::AnalysisState>::value);
}

TEST(AnalysisState, CopiesOrMovesWithoutAliasing) {
    static_assert(std::is_copy_constructible_v<fesa::AnalysisState>);
    static_assert(std::is_copy_assignable_v<fesa::AnalysisState>);
    static_assert(std::is_move_constructible_v<fesa::AnalysisState>);
    static_assert(std::is_move_assignable_v<fesa::AnalysisState>);

    const auto dofs = makeDofs();
    auto original = fesa::AnalysisState::create(dofs, {"Step-1", 0U});
    original.displacement()[0] = 3.0;
    original.reaction()[11] = -2.0;
    original.endpointResults().push_back({
        0U,
        -1,
        {"Beam-1", 1, "1"},
        {1.0, 0.0, 0.0, 0.0, 0.0, 0.0},
        {2.0, 0.0, 0.0, 0.0}});
    original.gaussResults().push_back(
        {0U, 1, {0.1, 0.0, 0.0, 0.0}, {10.0, 0.0, 0.0, 0.0}});
    original.stressResults().push_back(
        {0U, 1, 0U, 0.0, 0.0, 10.0, "fesa-default"});

    auto copied = original;
    EXPECT_NE(copied.displacement().data(), original.displacement().data());
    EXPECT_NE(copied.reaction().data(), original.reaction().data());
    EXPECT_NE(copied.endpointResults().data(), original.endpointResults().data());
    EXPECT_NE(copied.gaussResults().data(), original.gaussResults().data());
    EXPECT_NE(copied.stressResults().data(), original.stressResults().data());
    copied.displacement()[0] = 30.0;
    copied.endpointResults()[0].endAction[0] = 20.0;
    EXPECT_DOUBLE_EQ(original.displacement()[0], 3.0);
    EXPECT_DOUBLE_EQ(original.endpointResults()[0].endAction[0], 1.0);

    auto moved = std::move(copied);
    EXPECT_EQ(moved.identity().stepName, "Step-1");
    EXPECT_DOUBLE_EQ(moved.displacement()[0], 30.0);
    EXPECT_DOUBLE_EQ(moved.endpointResults()[0].endAction[0], 20.0);
    EXPECT_NE(moved.displacement().data(), original.displacement().data());
    EXPECT_NE(moved.endpointResults().data(), original.endpointResults().data());

    auto copyAssigned = fesa::AnalysisState::create(dofs, {"Other", 3U});
    copyAssigned = original;
    copyAssigned.reaction()[11] = -20.0;
    EXPECT_DOUBLE_EQ(original.reaction()[11], -2.0);
    EXPECT_EQ(copyAssigned.identity().stepName, "Step-1");

    auto moveAssigned = fesa::AnalysisState::create(dofs, {"Other", 4U});
    moveAssigned = std::move(copyAssigned);
    EXPECT_EQ(moveAssigned.identity().stepName, "Step-1");
    EXPECT_DOUBLE_EQ(moveAssigned.reaction()[11], -20.0);
    EXPECT_NE(moveAssigned.reaction().data(), original.reaction().data());
}
