// Tests para fn_ring::compute_ring_layout (cpp/functions/core/compute_ring_layout).
// Pure: sin ImGui context, sin I/O.
// Issue 0109b — skill_tree ring layout.

#define CATCH_CONFIG_MAIN
#include "catch_amalgamated.hpp"

#include "core/compute_ring_layout.h"

#include <algorithm>
#include <cmath>
#include <string>
#include <vector>

using namespace fn_ring;

// ---------------------------------------------------------------------------
// Helpers
// ---------------------------------------------------------------------------

static const LayoutConfig kDefault; // default 5 rings, 18 sectors

static LayoutOutput find_by_id(const std::vector<LayoutOutput>& out,
                                const std::string& id) {
    for (auto& o : out) {
        if (o.id == id) return o;
    }
    // not found — return sentinel with ring=-99
    LayoutOutput bad;
    bad.id = id;
    bad.ring = -99;
    return bad;
}

static float dist2(float x, float y) {
    return std::sqrt(x * x + y * y);
}

// ---------------------------------------------------------------------------
// Test 1: empty input → empty output
// ---------------------------------------------------------------------------
TEST_CASE("empty_input_empty_output") {
    auto out = compute_ring_layout({}, kDefault);
    REQUIRE(out.empty());
}

// ---------------------------------------------------------------------------
// Test 2: un solo nodo cae en centro de su banda radial + centro de su sector
// ---------------------------------------------------------------------------
TEST_CASE("single_node_centered_in_bin") {
    std::vector<LayoutInput> nodes = {{"n1", "completado", "core", 1.0f}};

    // "completado" → ring 0, ring_radii default: [0, 150)
    // r_inner efectivo = 30 (ring0 avoidance), r_outer = 150
    // r_lo = 30 + 14 = 44, r_hi = 150 - 14 = 136, center = (44+136)/2 = 90

    auto out = compute_ring_layout(nodes, kDefault);
    REQUIRE(out.size() == 1);

    auto o = out[0];
    REQUIRE(o.ring   == 0);
    REQUIRE(o.sector == 17); // "core" no esta en DomainOrder vacia → sector n_sectors-1 = 17

    float r = dist2(o.x, o.y);
    // Centro de banda: [44, 136] -> 90.0
    REQUIRE(std::abs(r - 90.0f) < 0.01f);
}

// ---------------------------------------------------------------------------
// Test 3: dos nodos en mismo bin → radios distintos, uniformemente distribuidos
// ---------------------------------------------------------------------------
TEST_CASE("two_nodes_same_bin_radial_distribution") {
    std::vector<LayoutInput> nodes = {
        {"a", "completado", "alpha", 0.5f},
        {"b", "completado", "alpha", 0.5f},
    };

    DomainOrder order = {"alpha"};
    auto out = compute_ring_layout(nodes, kDefault, {}, order);
    REQUIRE(out.size() == 2);

    auto oa = find_by_id(out, "a");
    auto ob = find_by_id(out, "b");

    REQUIRE(oa.ring   == 0);
    REQUIRE(ob.ring   == 0);
    REQUIRE(oa.sector == 0);
    REQUIRE(ob.sector == 0);

    float ra = dist2(oa.x, oa.y);
    float rb = dist2(ob.x, ob.y);

    // Radios deben ser distintos
    REQUIRE(std::abs(ra - rb) > 0.01f);

    // Ambos deben estar en la banda [44, 136]
    float r_lo = 44.0f; // 30 + 14
    float r_hi = 136.0f; // 150 - 14
    REQUIRE(ra >= r_lo - 0.01f);
    REQUIRE(ra <= r_hi + 0.01f);
    REQUIRE(rb >= r_lo - 0.01f);
    REQUIRE(rb <= r_hi + 0.01f);

    // N=2: r_0 = 44 + 0.5*(136-44)/2 = 44+23 = 67; r_1 = 44+1.5*46 = 113
    // (aprox, sin jitter porque bin tiene 2 nodos y capacidad radial = band/18 ~ 5)
    // No verificamos valores exactos porque el jitter angular puede activarse,
    // pero la diferencia de radios debe ser aprox (r_hi-r_lo)/N = 46
    float expected_step = (r_hi - r_lo) / 2.0f; // 46
    REQUIRE(std::abs(std::abs(ra - rb) - expected_step) < 0.5f);
}

// ---------------------------------------------------------------------------
// Test 4: default status map mapea correctamente
// ---------------------------------------------------------------------------
TEST_CASE("default_status_map") {
    std::vector<LayoutInput> nodes = {
        {"c1", "completado",  "d", 0.0f},
        {"c2", "completed",   "d", 0.0f},
        {"c3", "in-progress", "d", 0.0f},
        {"c4", "pendiente",   "d", 0.0f},
        {"c5", "deferred",    "d", 0.0f},
        {"c6", "locked",      "d", 0.0f},
        {"c7", "unlocked",    "d", 0.0f},
        {"c8", "bloqueado",   "d", 0.0f},
    };

    auto out = compute_ring_layout(nodes, kDefault);

    REQUIRE(find_by_id(out, "c1").ring == 0);
    REQUIRE(find_by_id(out, "c2").ring == 0);
    REQUIRE(find_by_id(out, "c3").ring == 1);
    REQUIRE(find_by_id(out, "c4").ring == 3);
    REQUIRE(find_by_id(out, "c5").ring == 4);
    REQUIRE(find_by_id(out, "c6").ring == 3);
    REQUIRE(find_by_id(out, "c7").ring == 2);
    REQUIRE(find_by_id(out, "c8").ring == 4);
}

// ---------------------------------------------------------------------------
// Test 5: status no mapeado → ring == -1, x/y == 0
// ---------------------------------------------------------------------------
TEST_CASE("unmapped_status_returns_ring_minus_one") {
    std::vector<LayoutInput> nodes = {{"x1", "unknown_status", "core", 0.5f}};

    auto out = compute_ring_layout(nodes, kDefault);
    REQUIRE(out.size() == 1);

    auto o = out[0];
    REQUIRE(o.ring == -1);
    REQUIRE(o.x   == 0.0f);
    REQUIRE(o.y   == 0.0f);
}

// ---------------------------------------------------------------------------
// Test 6: domain fuera del orden → sector n_sectors-1
// ---------------------------------------------------------------------------
TEST_CASE("domain_not_in_order_falls_back_to_last_sector") {
    DomainOrder order = {"alpha", "beta", "gamma"};
    std::vector<LayoutInput> nodes = {{"n1", "completado", "unknown_domain", 0.5f}};

    LayoutConfig cfg = kDefault;
    cfg.n_sectors = 5;

    auto out = compute_ring_layout(nodes, cfg, {}, order);
    REQUIRE(out.size() == 1);
    REQUIRE(out[0].sector == 4); // n_sectors-1 = 4
}

// ---------------------------------------------------------------------------
// Test 7: determinismo — dos llamadas identicas producen el mismo output
// ---------------------------------------------------------------------------
TEST_CASE("deterministic_repeated_call") {
    std::vector<LayoutInput> nodes = {
        {"0001", "completado",  "core",     1.0f},
        {"0002", "pendiente",   "infra",    0.5f},
        {"0003", "in-progress", "finance",  0.8f},
        {"0004", "deferred",    "core",     0.1f},
        {"0005", "completado",  "infra",    0.9f},
    };

    auto out1 = compute_ring_layout(nodes, kDefault);
    auto out2 = compute_ring_layout(nodes, kDefault);

    REQUIRE(out1.size() == out2.size());
    for (size_t i = 0; i < out1.size(); ++i) {
        REQUIRE(out1[i].id           == out2[i].id);
        REQUIRE(out1[i].x            == out2[i].x);
        REQUIRE(out1[i].y            == out2[i].y);
        REQUIRE(out1[i].ring         == out2[i].ring);
        REQUIRE(out1[i].sector       == out2[i].sector);
        REQUIRE(out1[i].rank_in_bin  == out2[i].rank_in_bin);
    }
}

// ---------------------------------------------------------------------------
// Test 8: ring 0 con radio interno 0 → nodos colocados con r >= 30
// ---------------------------------------------------------------------------
TEST_CASE("ring_zero_avoids_origin") {
    // ring_radii[0] == 0 por defecto
    std::vector<LayoutInput> nodes = {
        {"n1", "completado", "d1", 1.0f},
        {"n2", "completed",  "d2", 0.9f},
    };

    auto out = compute_ring_layout(nodes, kDefault);
    for (auto& o : out) {
        if (o.ring == 0) {
            float r = dist2(o.x, o.y);
            REQUIRE(r >= 30.0f - 0.01f); // kRing0InnerMin = 30
            REQUIRE(r >  0.5f);           // definitivamente no en el origen
        }
    }
}

// ---------------------------------------------------------------------------
// Test 9: sector wrap-around — sector 17 (ultimo) con 18 sectores
//         theta ≈ 2*PI*(17+0.5)/18 = 2*PI*17.5/18 ≈ 6.109 rad
// ---------------------------------------------------------------------------
TEST_CASE("sector_wrap_around_last_sector") {
    // Forzamos que el nodo caiga en sector 17 pasando domain_order sin "misc"
    std::vector<LayoutInput> nodes = {{"n1", "completado", "misc_domain", 1.0f}};

    LayoutConfig cfg;
    cfg.n_sectors  = 18;
    cfg.ring_radii = {0.0f, 150.0f, 280.0f, 450.0f, 650.0f, 850.0f};
    cfg.start_angle = 0.0f;
    cfg.bin_padding = 14.0f;

    DomainOrder order; // vacia → "misc_domain" cae en sector n_sectors-1 = 17

    auto out = compute_ring_layout(nodes, cfg, {}, order);
    REQUIRE(out.size() == 1);
    REQUIRE(out[0].sector == 17);

    // theta = start_angle + (17 + 0.5) * (2*PI / 18) = 17.5 / 18 * 2*PI
    const float pi    = 3.14159265358979323846f;
    const float theta = (17.5f / 18.0f) * 2.0f * pi; // ≈ 6.1087 rad

    // Radio del nodo: ring 0, r_inner=30, r_outer=150, padding=14 → [44,136] → 90
    const float r = 90.0f;

    float expected_x = r * std::cos(theta);
    float expected_y = r * std::sin(theta);

    REQUIRE(std::abs(out[0].x - expected_x) < 0.05f);
    REQUIRE(std::abs(out[0].y - expected_y) < 0.05f);
}

// ---------------------------------------------------------------------------
// Test 10: golden snapshot — 30 nodos, mezcla de status/domain
//          Verifica primer y ultimo nodo con tolerancia 0.001
// ---------------------------------------------------------------------------
TEST_CASE("golden_snapshot_30_nodes") {
    // Input fijo: 30 nodos con mix de status/domain/recency
    const std::vector<LayoutInput> nodes = {
        {"0001", "completado",         "core",         1.00f},
        {"0002", "completado",         "infra",        0.95f},
        {"0003", "in-progress",        "finance",      0.90f},
        {"0004", "pendiente",          "core",         0.85f},
        {"0005", "deferred",           "datascience",  0.80f},
        {"0006", "completado",         "core",         0.75f},
        {"0007", "locked",             "infra",        0.70f},
        {"0008", "unlocked",           "finance",      0.65f},
        {"0009", "pendiente",          "core",         0.60f},
        {"0010", "in-progress",        "datascience",  0.55f},
        {"0011", "completado",         "infra",        0.50f},
        {"0012", "bloqueado",          "core",         0.45f},
        {"0013", "deferred",           "finance",      0.40f},
        {"0014", "pendiente",          "infra",        0.35f},
        {"0015", "completado",         "datascience",  0.30f},
        {"0016", "unknown_status",     "core",         0.25f}, // descartado
        {"0017", "in-progress",        "infra",        0.20f},
        {"0018", "pendiente_unlocked", "finance",      0.15f},
        {"0019", "completed",          "datascience",  0.10f},
        {"0020", "locked",             "core",         0.05f},
        {"0021", "completado",         "core",         1.00f},
        {"0022", "completado",         "infra",        0.92f},
        {"0023", "pendiente",          "finance",      0.88f},
        {"0024", "in-progress",        "core",         0.84f},
        {"0025", "deferred",           "infra",        0.80f},
        {"0026", "unlocked",           "datascience",  0.76f},
        {"0027", "completado",         "finance",      0.72f},
        {"0028", "locked",             "datascience",  0.68f},
        {"0029", "bloqueado",          "core",         0.64f},
        {"0030", "pendiente",          "finance",      0.60f},
    };

    DomainOrder order = {"core", "infra", "finance", "datascience"};
    LayoutConfig cfg;
    cfg.n_sectors   = 18;
    cfg.ring_radii  = {0.0f, 150.0f, 280.0f, 450.0f, 650.0f, 850.0f};
    cfg.start_angle = 0.0f;
    cfg.bin_padding = 14.0f;
    cfg.center_x    = 0.0f;
    cfg.center_y    = 0.0f;

    auto out = compute_ring_layout(nodes, cfg, {}, order);
    REQUIRE(out.size() == 30);

    // Nodo "0016" tiene status desconocido → debe ser descartado
    auto o16 = find_by_id(out, "0016");
    REQUIRE(o16.ring == -1);
    REQUIRE(o16.x    == 0.0f);
    REQUIRE(o16.y    == 0.0f);

    // Verificar "0001": completado→ring0, core→sector0
    auto o1 = find_by_id(out, "0001");
    REQUIRE(o1.ring   == 0);
    REQUIRE(o1.sector == 0);
    // theta = 0 + (0 + 0.5) * (2*PI / 18) = PI/18 ≈ 0.17453 rad
    // En el bin (ring=0, sector=0) hay "0001", "0006", "0021", "0024"
    // ordenados por (recency desc): recency 1.00, 0.75, 1.00 (0021), 0.84
    // -> "0001"(1.0), "0021"(1.0, id mayor), "0024"(0.84), "0006"(0.75)
    // rank "0001" = 0 si su id < "0021": "0001" < "0021" → rank 0
    REQUIRE(o1.rank_in_bin == 0);
    // El radio y angulo exactos dependen del jitter deterministico.
    // Verificamos que esta dentro de la banda del ring 0: [44, 136]
    float r1 = dist2(o1.x, o1.y);
    REQUIRE(r1 >= 44.0f - 0.01f);
    REQUIRE(r1 <= 136.0f + 0.01f);

    // Verificar "0030": pendiente→ring3, finance→sector2
    auto o30 = find_by_id(out, "0030");
    REQUIRE(o30.ring   == 3);
    REQUIRE(o30.sector == 2);
    // ring 3: [450, 650), r_lo = 450+14 = 464, r_hi = 650-14 = 636
    float r30 = dist2(o30.x, o30.y);
    REQUIRE(r30 >= 464.0f - 0.01f);
    REQUIRE(r30 <= 636.0f + 0.01f);

    // El output es deterministico: dos llamadas dan resultado identico
    auto out2 = compute_ring_layout(nodes, cfg, {}, order);
    for (size_t i = 0; i < out.size(); ++i) {
        REQUIRE(out[i].x == out2[i].x);
        REQUIRE(out[i].y == out2[i].y);
    }
}
