#include "fesa/model/domain.hpp"

#include <gtest/gtest.h>

#include <filesystem>
#include <string>
#include <type_traits>
#include <utility>
#include <vector>

namespace {

fesa::ModelDefinition makeOwnedDefinition() {
    fesa::ModelDefinition definition{};
    definition.sourcePath = "models/owned.inp";
    definition.sourceContentIdentity = "fnv1a64:fedcba9876543210";
    definition.heading = "Stable declaration order";
    definition.nodes = {
        {{"Instance-A", 20, "0020"}, {2.0, 0.0, 0.0}, {"models/owned.inp", 12U}},
        {{"Instance-A", 10, "0010"}, {1.0, 0.0, 0.0}, {"models/owned.inp", 11U}}};
    definition.elements = {{
        {"Instance-A", 5, "0005"},
        {fesa::EntityIndex{1}, fesa::EntityIndex{0}},
        fesa::EntityIndex{0},
        fesa::EntityIndex{0},
        {"models/owned.inp", 20U}}};
    definition.materials = {{
        "Steel", 210.0e9, 0.3, {"models/owned.inp", 30U}}};
    definition.sections = {{
        "Section-1",
        0.02,
        1.0e-5,
        0.0,
        2.0e-5,
        5.0e-6,
        {0.0, 1.0, 0.0},
        {{-0.1, 0.0}, {0.1, 0.0}},
        {"models/owned.inp", 40U}}};
    definition.nodeSets = {{
        "Tip", std::string{"Instance-A"}, {0U}, {"models/owned.inp", 45U}}};
    definition.elementSets = {{
        "Beam", std::string{"Instance-A"}, {0U}, {"models/owned.inp", 46U}}};
    definition.parts = {{
        "BeamPart", {20, 10}, {5}, {"Tip"}, {"Beam"},
        {"models/owned.inp", 2U}}};
    definition.instances = {{
        "Instance-A",
        "BeamPart",
        {{20, 0U}, {10, 1U}},
        {{5, 0U}},
        {"models/owned.inp", 50U}}};
    definition.steps = {{
        "Step-1",
        {{"Root", 1, 6, 0.0, {"models/owned.inp", 60U}}},
        {{"Tip", 3, -1000.0, {"models/owned.inp", 61U}}},
        0.1,
        1.0,
        1.0e-5,
        1.0,
        {"models/owned.inp", 55U}}};
    definition.warnings = {{
        fesa::Severity::warning,
        "ignored-output-request",
        {"models/owned.inp", 70U},
        "*OUTPUT",
        "",
        "Ignored output request."}};
    return definition;
}

} // namespace

TEST(DomainModel, ImmutableOwnershipPreservesStableOrder) {
    auto definition = makeOwnedDefinition();
    auto result = fesa::Domain::create(definition);
    ASSERT_TRUE(result.hasValue());

    definition.sourcePath = "mutated.inp";
    definition.sourceContentIdentity = "mutated";
    definition.nodes[0].sourceId.sourceLabelText = "mutated";
    definition.nodes[0].coordinates[0] = -99.0;
    definition.sections[0].sectionPoints[0][0] = -99.0;
    definition.steps[0].loads[0].magnitude = 99.0;
    definition.warnings[0].code = "mutated";

    const fesa::Domain& domain = result.value();
    const fesa::Node* const firstNodeAddress = domain.nodes().data();
    static_assert(std::is_same_v<
                  decltype(std::declval<const fesa::Domain&>().nodes()),
                  const std::vector<fesa::Node>&>);
    static_assert(std::is_same_v<
                  decltype(std::declval<const fesa::Domain&>().elements()),
                  const std::vector<fesa::EulerBeam3DDefinition>&>);

    EXPECT_EQ(domain.sourcePath(), std::filesystem::path{"models/owned.inp"});
    EXPECT_EQ(domain.sourceContentIdentity(), "fnv1a64:fedcba9876543210");
    ASSERT_EQ(domain.nodes().size(), 2U);
    EXPECT_EQ(domain.nodes()[0].sourceId.sourceLabel, 20);
    EXPECT_EQ(domain.nodes()[0].sourceId.sourceLabelText, "0020");
    EXPECT_DOUBLE_EQ(domain.nodes()[0].coordinates[0], 2.0);
    EXPECT_EQ(domain.nodes()[1].sourceId.sourceLabel, 10);
    EXPECT_EQ(domain.nodes().data(), firstNodeAddress);

    ASSERT_EQ(domain.elements().size(), 1U);
    EXPECT_EQ(domain.elements()[0].nodeIndices[0], 1U);
    EXPECT_EQ(domain.elements()[0].nodeIndices[1], 0U);
    ASSERT_EQ(domain.materials().size(), 1U);
    ASSERT_EQ(domain.sections().size(), 1U);
    EXPECT_DOUBLE_EQ(domain.sections()[0].sectionPoints[0][0], -0.1);
    ASSERT_EQ(domain.nodeSets().size(), 1U);
    EXPECT_EQ(domain.nodeSets()[0].nodeIndices[0], 0U);
    ASSERT_EQ(domain.elementSets().size(), 1U);
    EXPECT_EQ(domain.elementSets()[0].elementIndices[0], 0U);

    ASSERT_EQ(domain.steps().size(), 1U);
    EXPECT_EQ(domain.steps()[0].name, "Step-1");
    EXPECT_DOUBLE_EQ(domain.steps()[0].initialIncrement, 0.1);
    EXPECT_DOUBLE_EQ(domain.steps()[0].timePeriod, 1.0);
    EXPECT_DOUBLE_EQ(domain.steps()[0].minimumIncrement, 1.0e-5);
    EXPECT_DOUBLE_EQ(domain.steps()[0].maximumIncrement, 1.0);
    ASSERT_EQ(domain.steps()[0].boundaries.size(), 1U);
    ASSERT_EQ(domain.steps()[0].loads.size(), 1U);
    EXPECT_DOUBLE_EQ(domain.steps()[0].loads[0].magnitude, -1000.0);

    ASSERT_EQ(domain.warnings().size(), 1U);
    EXPECT_EQ(domain.warnings()[0].code, "ignored-output-request");
}

TEST(DomainModel, MultipleIdentityInstancesDoNotMerge) {
    fesa::ModelDefinition definition{};
    definition.sourcePath = "models/two-instances.inp";
    definition.sourceContentIdentity = "fnv1a64:0011223344556677";
    definition.nodes = {
        {{"Instance-A", 1, "1"}, {0.0, 0.0, 0.0}, {"models/two-instances.inp", 10U}},
        {{"Instance-A", 2, "2"}, {1.0, 0.0, 0.0}, {"models/two-instances.inp", 11U}},
        {{"Instance-B", 1, "1"}, {0.0, 0.0, 0.0}, {"models/two-instances.inp", 10U}},
        {{"Instance-B", 2, "2"}, {1.0, 0.0, 0.0}, {"models/two-instances.inp", 11U}}};
    definition.elements = {
        {{"Instance-A", 1, "1"}, {0U, 1U}, 0U, 0U, {"models/two-instances.inp", 20U}},
        {{"Instance-B", 1, "1"}, {2U, 3U}, 0U, 0U, {"models/two-instances.inp", 20U}}};
    definition.materials = {{
        "Steel", 210.0e9, 0.3, {"models/two-instances.inp", 30U}}};
    definition.sections = {{
        "Section-1",
        0.02,
        1.0e-5,
        0.0,
        2.0e-5,
        5.0e-6,
        {0.0, 1.0, 0.0},
        {},
        {"models/two-instances.inp", 40U}}};
    definition.parts = {{
        "BeamPart", {1, 2}, {1}, {}, {}, {"models/two-instances.inp", 2U}}};
    definition.instances = {
        {"Instance-A", "BeamPart", {{1, 0U}, {2, 1U}}, {{1, 0U}},
         {"models/two-instances.inp", 50U}},
        {"Instance-B", "BeamPart", {{1, 2U}, {2, 3U}}, {{1, 1U}},
         {"models/two-instances.inp", 51U}}};
    definition.steps = {{
        "Step-1", {}, {}, 0.1, 1.0, 1.0e-5, 1.0,
        {"models/two-instances.inp", 60U}}};

    auto result = fesa::Domain::create(std::move(definition));
    ASSERT_TRUE(result.hasValue());
    const fesa::Domain& domain = result.value();

    ASSERT_EQ(domain.nodes().size(), 4U);
    EXPECT_EQ(domain.nodes()[0].sourceId.sourceLabel, 1);
    EXPECT_EQ(domain.nodes()[2].sourceId.sourceLabel, 1);
    EXPECT_EQ(domain.nodes()[0].sourceId.sourceLabelText, "1");
    EXPECT_EQ(domain.nodes()[2].sourceId.sourceLabelText, "1");
    EXPECT_NE(
        domain.nodes()[0].sourceId.instanceName,
        domain.nodes()[2].sourceId.instanceName);

    ASSERT_EQ(domain.elements().size(), 2U);
    EXPECT_EQ(domain.elements()[0].sourceId.sourceLabel, 1);
    EXPECT_EQ(domain.elements()[1].sourceId.sourceLabel, 1);
    EXPECT_EQ(domain.elements()[0].sourceId.instanceName, "Instance-A");
    EXPECT_EQ(domain.elements()[1].sourceId.instanceName, "Instance-B");
    EXPECT_EQ(domain.elements()[0].nodeIndices[0], 0U);
    EXPECT_EQ(domain.elements()[1].nodeIndices[0], 2U);
}

// MITC4-MODEL-002
TEST(DomainModel, ShellOwnershipPreservesResolvedAssignmentsAndOptionalFrames) {
    fesa::ModelDefinition definition{};
    definition.sourcePath = "models/shell-owned.inp";
    definition.sourceContentIdentity = "fnv1a64:1234567890abcdef";
    definition.materials = {
        {"Material-B", 70.0e9, 0.33, {"models/shell-owned.inp", 31U}},
        {"Material-A", 210.0e9, 0.3, {"models/shell-owned.inp", 30U}}};
    definition.shellSections = {
        {"Section-B", 0.02, 0U, {"models/shell-owned.inp", 41U}},
        {"Section-A", 0.01, 1U, {"models/shell-owned.inp", 40U}}};
    definition.shellElements = {
        {{"Shell-Instance", 20, "0020"},
         fesa::ShellSourceElementType::s4r,
         {4U, 5U, 6U, 7U},
         0U,
         0U,
         {"models/shell-owned.inp", 21U}},
        {{"Shell-Instance", 10, "0010"},
         fesa::ShellSourceElementType::s4,
         {0U, 1U, 2U, 3U},
         1U,
         1U,
         {"models/shell-owned.inp", 20U}}};
    definition.shellNodeInitialFrames = {
        {4U, {0.0, 0.0, 1.0}, {1.0, 0.0, 0.0}, {0.0, 1.0, 0.0}},
        {0U, {0.0, 1.0, 0.0}, {1.0, 0.0, 0.0}, {0.0, 0.0, -1.0}}};

    auto result = fesa::Domain::create(definition);
    ASSERT_TRUE(result.hasValue());

    definition.shellSections[0].thickness = -1.0;
    definition.shellElements[0].sourceId.sourceLabelText = "mutated";
    definition.shellNodeInitialFrames[0].director[2] = -1.0;

    const fesa::Domain& domain = result.value();
    static_assert(std::is_same_v<
                  decltype(std::declval<const fesa::Domain&>().shellElements()),
                  const std::vector<fesa::Mitc4ShellDefinition>&>);
    static_assert(std::is_same_v<
                  decltype(std::declval<const fesa::Domain&>().shellSections()),
                  const std::vector<fesa::ShellSection>&>);
    static_assert(std::is_same_v<
                  decltype(std::declval<const fesa::Domain&>().shellNodeInitialFrames()),
                  const std::vector<fesa::ShellNodeInitialFrame>&>);

    ASSERT_EQ(domain.shellElements().size(), 2U);
    EXPECT_EQ(domain.shellElements()[0].sourceId.sourceLabel, 20);
    EXPECT_EQ(domain.shellElements()[0].sourceId.sourceLabelText, "0020");
    EXPECT_EQ(domain.shellElements()[1].sourceId.sourceLabel, 10);
    EXPECT_EQ(domain.shellElements()[0].sourceType, fesa::ShellSourceElementType::s4r);
    EXPECT_EQ(domain.shellElements()[1].sourceType, fesa::ShellSourceElementType::s4);
    EXPECT_EQ(domain.shellElements()[0].nodeIndices[3], 7U);
    EXPECT_EQ(domain.shellElements()[0].materialIndex, 0U);
    EXPECT_EQ(domain.shellElements()[0].sectionIndex, 0U);

    ASSERT_EQ(domain.shellSections().size(), 2U);
    EXPECT_EQ(domain.shellSections()[0].name, "Section-B");
    EXPECT_DOUBLE_EQ(domain.shellSections()[0].thickness, 0.02);
    EXPECT_EQ(domain.shellSections()[0].materialIndex, 0U);
    EXPECT_EQ(domain.shellSections()[1].name, "Section-A");

    ASSERT_EQ(domain.shellNodeInitialFrames().size(), 2U);
    EXPECT_EQ(domain.shellNodeInitialFrames()[0].nodeIndex, 4U);
    EXPECT_EQ(
        domain.shellNodeInitialFrames()[0].director,
        (std::array<double, 3>{0.0, 0.0, 1.0}));
    EXPECT_EQ(
        domain.shellNodeInitialFrames()[0].tangentA,
        (std::array<double, 3>{1.0, 0.0, 0.0}));
    EXPECT_EQ(
        domain.shellNodeInitialFrames()[0].tangentB,
        (std::array<double, 3>{0.0, 1.0, 0.0}));

    auto noFramesResult = fesa::Domain::create(fesa::ModelDefinition{});
    ASSERT_TRUE(noFramesResult.hasValue());
    EXPECT_TRUE(noFramesResult.value().shellNodeInitialFrames().empty());
}
