// Tests para `graph_force_layout_gpu` (issue 0049h).
// El layout en GPU vive 100% en compute shaders, asi que no hay logica pura
// que tester sin un contexto GL real. El test crea una ventana GLFW oculta a
// 4.3 core; si la creacion falla (CI sin display, Mesa sin compute, etc.)
// el test SKIPea con WARN para no bloquear CI.

#define CATCH_CONFIG_MAIN
#include "catch_amalgamated.hpp"

#include "viz/graph_types.h"
#include "viz/graph_force_layout.h"
#include "viz/graph_force_layout_gpu.h"
#include "gfx/gl_loader.h"

#include <GLFW/glfw3.h>
#include <cmath>
#include <cstdio>
#include <cstdlib>
#include <vector>

namespace {

// RAII para inicializar GLFW + GL 4.3 hidden context. Si algo falla,
// `ok==false` y el test SKIPea.
struct GLContext {
    GLFWwindow* win = nullptr;
    bool ok = false;

    GLContext() {
        if (!glfwInit()) return;
        glfwWindowHint(GLFW_VISIBLE,                GLFW_FALSE);
        glfwWindowHint(GLFW_CONTEXT_VERSION_MAJOR,  4);
        glfwWindowHint(GLFW_CONTEXT_VERSION_MINOR,  3);
        glfwWindowHint(GLFW_OPENGL_PROFILE,         GLFW_OPENGL_CORE_PROFILE);
        glfwWindowHint(GLFW_OPENGL_FORWARD_COMPAT,  GLFW_TRUE);
        win = glfwCreateWindow(64, 64, "fl_gpu_test", nullptr, nullptr);
        if (!win) { glfwTerminate(); return; }
        glfwMakeContextCurrent(win);
        if (!fn::gfx::gl_loader_init()) { glfwDestroyWindow(win); glfwTerminate(); return; }
        ok = true;
    }
    ~GLContext() {
        if (win) glfwDestroyWindow(win);
        glfwTerminate();
    }
};

// Construye un grafo sintetico (anillo + cuerdas aleatorias) con N nodos y
// E aristas. Usado en los tres tests.
struct Graph {
    std::vector<GraphNode> nodes;
    std::vector<GraphEdge> edges;
    GraphData data{};
};

Graph make_graph(int N, int extra_chords) {
    Graph g;
    g.nodes.reserve(N);
    g.edges.reserve(N + extra_chords);
    unsigned seed = 0xC0FFEEu;
    auto rnd = [&]() {
        seed = seed * 1664525u + 1013904223u;
        return float((seed >> 8) & 0xFFFFFF) / float(1 << 24);
    };
    for (int i = 0; i < N; ++i) {
        float angle = 6.2831853f * i / N;
        GraphNode n = graph_node(80.0f * std::cos(angle) + (rnd() - 0.5f) * 5.0f,
                                 80.0f * std::sin(angle) + (rnd() - 0.5f) * 5.0f);
        g.nodes.push_back(n);
    }
    for (int i = 0; i < N; ++i) {
        g.edges.push_back(graph_edge((uint32_t)i, (uint32_t)((i + 1) % N), 1.0f));
    }
    for (int k = 0; k < extra_chords; ++k) {
        uint32_t a = uint32_t(rnd() * N);
        uint32_t b = uint32_t(rnd() * N);
        if (a == b) b = (b + 1) % N;
        g.edges.push_back(graph_edge(a, b, 0.5f));
    }
    g.data.nodes = g.nodes.data();
    g.data.node_count = (int)g.nodes.size();
    g.data.node_capacity = (int)g.nodes.capacity();
    g.data.edges = g.edges.data();
    g.data.edge_count = (int)g.edges.size();
    g.data.edge_capacity = (int)g.edges.capacity();
    g.data.update_bounds();
    return g;
}

float per_node_energy(float total, int N) {
    return N > 0 ? total / (float)N : 0.0f;
}

} // namespace

TEST_CASE("graph_force_layout_gpu — smoke + decreasing energy", "[graph_force_layout_gpu]") {
    GLContext gl;
    if (!gl.ok) {
        WARN("No GL 4.3 context (CI/headless?). Skipping GPU layout test.");
        SUCCEED("no GL context");
        return;
    }

    auto g = make_graph(100, 100);

    auto* ctx = graph_force_layout_gpu_create(g.data.node_count + 16,
                                              g.data.edge_count + 16);
    if (!ctx) {
        WARN("Compute shaders no soportados por el driver. Skipping.");
        SUCCEED("no compute support");
        return;
    }
    REQUIRE(graph_force_layout_gpu_available());

    graph_force_layout_gpu_upload(ctx, g.data);

    ForceLayoutConfig cfg;
    cfg.repulsion    = 200.0f;
    cfg.attraction   = 0.05f;
    cfg.damping      = 0.85f;
    cfg.gravity      = 0.01f;
    cfg.max_velocity = 20.0f;
    cfg.iterations   = 1;

    // Warmup + medicion ventana.
    float e_warm = 0.0f;
    for (int i = 0; i < 5; ++i) e_warm = graph_force_layout_gpu_step(ctx, cfg);

    float e_after = e_warm;
    for (int i = 0; i < 100; ++i) e_after = graph_force_layout_gpu_step(ctx, cfg);

    // Tras 100 pasos la energia por nodo deberia ser <= a la del warmup.
    // Comparamos por nodo para ser robustos al N concreto.
    INFO("warm=" << e_warm << " after=" << e_after);
    REQUIRE(per_node_energy(e_after, g.data.node_count) <=
            per_node_energy(e_warm, g.data.node_count) + 1.0f);

    graph_force_layout_gpu_readback(ctx, g.data);
    // Ningun NaN tras readback.
    for (const auto& n : g.nodes) {
        REQUIRE(std::isfinite(n.x));
        REQUIRE(std::isfinite(n.y));
    }

    graph_force_layout_gpu_destroy(ctx);
}

TEST_CASE("graph_force_layout_gpu — pinned nodes no se mueven", "[graph_force_layout_gpu]") {
    GLContext gl;
    if (!gl.ok) {
        WARN("No GL 4.3 context. Skipping.");
        SUCCEED("no GL context");
        return;
    }

    auto g = make_graph(50, 30);
    // Pinear nodo 0 en (0, 0)
    g.nodes[0].x = 0.0f;
    g.nodes[0].y = 0.0f;
    g.nodes[0].vx = 0.0f;
    g.nodes[0].vy = 0.0f;
    g.nodes[0].flags |= NF_PINNED;

    auto* ctx = graph_force_layout_gpu_create(g.data.node_count + 16,
                                              g.data.edge_count + 16);
    if (!ctx) {
        WARN("No compute support. Skipping.");
        SUCCEED("no compute");
        return;
    }
    graph_force_layout_gpu_upload(ctx, g.data);

    ForceLayoutConfig cfg;
    cfg.repulsion = 500.0f;
    cfg.attraction = 0.05f;
    cfg.iterations = 1;

    for (int i = 0; i < 100; ++i) graph_force_layout_gpu_step(ctx, cfg);
    graph_force_layout_gpu_readback(ctx, g.data, /*include_velocities=*/true);

    REQUIRE(g.nodes[0].x == Catch::Approx(0.0f).margin(1e-4));
    REQUIRE(g.nodes[0].y == Catch::Approx(0.0f).margin(1e-4));
    REQUIRE(g.nodes[0].vx == Catch::Approx(0.0f).margin(1e-4));
    REQUIRE(g.nodes[0].vy == Catch::Approx(0.0f).margin(1e-4));

    graph_force_layout_gpu_destroy(ctx);
}

TEST_CASE("graph_force_layout_gpu — CPU vs GPU (energia comparable)",
          "[graph_force_layout_gpu]") {
    GLContext gl;
    if (!gl.ok) {
        WARN("No GL 4.3 context. Skipping.");
        SUCCEED("no GL context");
        return;
    }

    // Mismo grafo en dos copias: una para CPU, otra para GPU.
    auto g_cpu = make_graph(50, 60);
    auto g_gpu = make_graph(50, 60);

    auto* ctx = graph_force_layout_gpu_create(g_gpu.data.node_count + 16,
                                              g_gpu.data.edge_count + 16);
    if (!ctx) {
        WARN("No compute support. Skipping.");
        SUCCEED("no compute");
        return;
    }
    graph_force_layout_gpu_upload(ctx, g_gpu.data);

    ForceLayoutConfig cfg;
    cfg.repulsion    = 300.0f;
    cfg.attraction   = 0.03f;
    cfg.damping      = 0.85f;
    cfg.gravity      = 0.005f;
    cfg.max_velocity = 20.0f;
    cfg.iterations   = 1;

    float e_cpu = 0.0f, e_gpu = 0.0f;
    for (int i = 0; i < 80; ++i) {
        e_cpu = graph_force_layout_step(g_cpu.data, cfg);
        e_gpu = graph_force_layout_gpu_step(ctx, cfg);
    }

    INFO("e_cpu=" << e_cpu << "  e_gpu=" << e_gpu);

    // No exigimos igualdad — Barnes-Hut (CPU) y spatial-hash (GPU) son
    // aproximaciones distintas. Solo verificamos que ambas convergen al mismo
    // orden de magnitud (factor 50x da margen para grafos pequenos donde la
    // varianza es alta).
    if (e_cpu > 1e-3f) {
        float ratio = e_gpu / e_cpu;
        REQUIRE(ratio > 0.001f);
        REQUIRE(ratio < 50.0f);
    }

    graph_force_layout_gpu_destroy(ctx);
}
