// Unit tests for graph_layouts (issue 0049i).
// Cubre los seis layouts estaticos: grid, circular, random, radial,
// hierarchical, fixed. Verifica:
//   - Centrado y geometria basica de los layouts geometricos.
//   - NF_PINNED se respeta (nodos pinned no se mueven).
//   - radial coloca el root en (0,0) y los hops en anillos correctos.
//   - hierarchical asigna niveles por longest-path.
//   - fixed es no-op.

#define CATCH_CONFIG_MAIN
#include "catch_amalgamated.hpp"

#include "viz/graph_layouts.h"
#include "viz/graph_types.h"

#include <cmath>
#include <vector>

namespace {

// Helper: construir un GraphData a partir de vectores
struct TestGraph {
    std::vector<GraphNode> nodes;
    std::vector<GraphEdge> edges;
    GraphData data{};

    void make(int N) {
        nodes.clear();
        edges.clear();
        for (int i = 0; i < N; ++i) {
            nodes.push_back(graph_node(0.0f, 0.0f));
        }
        sync();
    }

    void add_edge(uint32_t s, uint32_t t) {
        edges.push_back(graph_edge(s, t));
        sync();
    }

    void sync() {
        data.nodes = nodes.data();
        data.node_count = (int)nodes.size();
        data.node_capacity = (int)nodes.capacity();
        data.edges = edges.data();
        data.edge_count = (int)edges.size();
        data.edge_capacity = (int)edges.capacity();
        data.types = nullptr; data.type_count = 0;
        data.rel_types = nullptr; data.rel_type_count = 0;
    }
};

} // namespace

TEST_CASE("layout_grid respects pin and centers", "[viz][layouts][grid]") {
    TestGraph g; g.make(9);
    // Pin nodo 0 con posicion deliberadamente fuera de la cuadricula.
    g.nodes[0].x = 999.0f;
    g.nodes[0].y = 999.0f;
    g.nodes[0].flags |= NF_PINNED;
    g.sync();

    graph::layout_grid(g.data, 10.0f);

    // Pinned no se ha movido
    REQUIRE(g.nodes[0].x == Catch::Approx(999.0f));
    REQUIRE(g.nodes[0].y == Catch::Approx(999.0f));

    // Resto: 9 nodos con cols=3, rows=3 -> centro en (0,0)
    // El nodo 4 esta en (col=1, row=1) -> exactamente (0,0)
    REQUIRE(g.nodes[4].x == Catch::Approx(0.0f));
    REQUIRE(g.nodes[4].y == Catch::Approx(0.0f));

    // Velocidades a cero
    REQUIRE(g.nodes[1].vx == 0.0f);
    REQUIRE(g.nodes[1].vy == 0.0f);
}

TEST_CASE("layout_circular places nodes on circle", "[viz][layouts][circular]") {
    TestGraph g; g.make(8);
    graph::layout_circular(g.data, 50.0f);

    for (int i = 0; i < 8; ++i) {
        float r = std::sqrt(g.nodes[i].x * g.nodes[i].x +
                            g.nodes[i].y * g.nodes[i].y);
        REQUIRE(r == Catch::Approx(50.0f).margin(1e-3f));
    }
}

TEST_CASE("layout_circular respects NF_PINNED", "[viz][layouts][circular]") {
    TestGraph g; g.make(4);
    g.nodes[2].x = 7.0f;
    g.nodes[2].y = -3.0f;
    g.nodes[2].flags |= NF_PINNED;
    g.sync();

    graph::layout_circular(g.data, 100.0f);

    REQUIRE(g.nodes[2].x == Catch::Approx(7.0f));
    REQUIRE(g.nodes[2].y == Catch::Approx(-3.0f));
}

TEST_CASE("layout_random keeps positions in [-spread, spread]", "[viz][layouts][random]") {
    TestGraph g; g.make(50);
    graph::layout_random(g.data, 25.0f);
    for (int i = 0; i < g.data.node_count; ++i) {
        REQUIRE(g.nodes[i].x >= -25.0f);
        REQUIRE(g.nodes[i].x <=  25.0f);
        REQUIRE(g.nodes[i].y >= -25.0f);
        REQUIRE(g.nodes[i].y <=  25.0f);
        REQUIRE(g.nodes[i].vx == 0.0f);
        REQUIRE(g.nodes[i].vy == 0.0f);
    }
}

TEST_CASE("layout_radial root at center, neighbors on first ring", "[viz][layouts][radial]") {
    // Estrella de 5 puntas: nodo 0 -> nodos 1..5
    TestGraph g; g.make(6);
    for (int i = 1; i <= 5; ++i) g.add_edge(0, i);

    graph::layout_radial(g.data, /*root=*/0, /*ring=*/100.0f);

    // root en (0,0)
    REQUIRE(g.nodes[0].x == Catch::Approx(0.0f).margin(1e-3f));
    REQUIRE(g.nodes[0].y == Catch::Approx(0.0f).margin(1e-3f));

    // Vecinos a distancia ~100
    for (int i = 1; i <= 5; ++i) {
        float r = std::sqrt(g.nodes[i].x * g.nodes[i].x +
                            g.nodes[i].y * g.nodes[i].y);
        REQUIRE(r == Catch::Approx(100.0f).margin(1e-3f));
    }
}

TEST_CASE("layout_radial places hop2 on second ring", "[viz][layouts][radial]") {
    // Cadena: 0 -> 1 -> 2 -> 3
    TestGraph g; g.make(4);
    g.add_edge(0, 1);
    g.add_edge(1, 2);
    g.add_edge(2, 3);

    graph::layout_radial(g.data, 0, 50.0f);

    auto radius = [&](int i) {
        return std::sqrt(g.nodes[i].x * g.nodes[i].x +
                         g.nodes[i].y * g.nodes[i].y);
    };
    REQUIRE(radius(0) == Catch::Approx(0.0f).margin(1e-3f));
    REQUIRE(radius(1) == Catch::Approx(50.0f).margin(1e-3f));
    REQUIRE(radius(2) == Catch::Approx(100.0f).margin(1e-3f));
    REQUIRE(radius(3) == Catch::Approx(150.0f).margin(1e-3f));
}

TEST_CASE("layout_radial respects pin", "[viz][layouts][radial]") {
    TestGraph g; g.make(3);
    g.add_edge(0, 1);
    g.add_edge(0, 2);
    g.nodes[1].x = 42.0f; g.nodes[1].y = 7.0f;
    g.nodes[1].flags |= NF_PINNED;
    g.sync();

    graph::layout_radial(g.data, 0, 50.0f);
    REQUIRE(g.nodes[1].x == Catch::Approx(42.0f));
    REQUIRE(g.nodes[1].y == Catch::Approx(7.0f));
}

TEST_CASE("layout_hierarchical levels by longest path (LR)", "[viz][layouts][hierarchical]") {
    // DAG: 0 -> 1, 0 -> 2, 1 -> 3, 2 -> 3 (diamond)
    // longest-path levels: 0=0, 1=1, 2=1, 3=2
    TestGraph g; g.make(4);
    g.add_edge(0, 1);
    g.add_edge(0, 2);
    g.add_edge(1, 3);
    g.add_edge(2, 3);

    graph::layout_hierarchical(g.data, /*direction=*/0,
                                /*layer=*/100.0f,
                                /*node=*/40.0f);

    // Nivel 0 (nodo 0) en x=0
    REQUIRE(g.nodes[0].x == Catch::Approx(0.0f).margin(1e-3f));
    // Nivel 1 (nodos 1,2) en x=100
    REQUIRE(g.nodes[1].x == Catch::Approx(100.0f).margin(1e-3f));
    REQUIRE(g.nodes[2].x == Catch::Approx(100.0f).margin(1e-3f));
    // Nivel 2 (nodo 3) en x=200
    REQUIRE(g.nodes[3].x == Catch::Approx(200.0f).margin(1e-3f));
}

TEST_CASE("layout_hierarchical TB swaps axes", "[viz][layouts][hierarchical]") {
    TestGraph g; g.make(3);
    g.add_edge(0, 1);
    g.add_edge(1, 2);

    graph::layout_hierarchical(g.data, /*direction=*/2,
                                /*layer=*/50.0f,
                                /*node=*/30.0f);

    // TB: y crece con el nivel, x es transverse
    REQUIRE(g.nodes[0].y == Catch::Approx(0.0f).margin(1e-3f));
    REQUIRE(g.nodes[1].y == Catch::Approx(50.0f).margin(1e-3f));
    REQUIRE(g.nodes[2].y == Catch::Approx(100.0f).margin(1e-3f));
}

TEST_CASE("layout_hierarchical respects pin", "[viz][layouts][hierarchical]") {
    TestGraph g; g.make(3);
    g.add_edge(0, 1);
    g.add_edge(1, 2);
    g.nodes[1].x = -999.0f; g.nodes[1].y = 555.0f;
    g.nodes[1].flags |= NF_PINNED;
    g.sync();

    graph::layout_hierarchical(g.data, 0, 100.0f, 40.0f);

    REQUIRE(g.nodes[1].x == Catch::Approx(-999.0f));
    REQUIRE(g.nodes[1].y == Catch::Approx(555.0f));
}

TEST_CASE("layout_fixed is a no-op", "[viz][layouts][fixed]") {
    TestGraph g; g.make(5);
    for (int i = 0; i < 5; ++i) {
        g.nodes[i].x  = (float)(i * 10);
        g.nodes[i].y  = (float)(i * -3);
        g.nodes[i].vx = 0.5f;
        g.nodes[i].vy = -0.5f;
    }
    g.sync();

    graph::layout_fixed(g.data);

    for (int i = 0; i < 5; ++i) {
        REQUIRE(g.nodes[i].x == Catch::Approx((float)(i * 10)));
        REQUIRE(g.nodes[i].y == Catch::Approx((float)(i * -3)));
        REQUIRE(g.nodes[i].vx == Catch::Approx(0.5f));
        REQUIRE(g.nodes[i].vy == Catch::Approx(-0.5f));
    }
}

TEST_CASE("layouts handle empty graph", "[viz][layouts][empty]") {
    TestGraph g; g.make(0);
    REQUIRE_NOTHROW(graph::layout_grid(g.data, 10.0f));
    REQUIRE_NOTHROW(graph::layout_circular(g.data, 10.0f));
    REQUIRE_NOTHROW(graph::layout_random(g.data, 10.0f));
    REQUIRE_NOTHROW(graph::layout_radial(g.data, 0, 10.0f));
    REQUIRE_NOTHROW(graph::layout_hierarchical(g.data, 0, 10.0f, 10.0f));
    REQUIRE_NOTHROW(graph::layout_fixed(g.data));
}
