// test_puzzle_binding.c — IDF-style host tests for puzzle_binding_from_ir.
#include "unity.h"
#include "puzzle_binding.h"
#include <string.h>

// ── 1. QR step parses fully ──────────────────────────────────────────────────
TEST_CASE("pbind: qr step parses fully", "[pbind]")
{
    const char *ir =
        "{\"steps\":["
        "{\"id\":\"STEP_QR_DETECTOR\","
        "\"puzzle\":{\"id\":3,\"type\":\"qr\","
        "\"codes\":[\"zacus-qr-1\",\"zacus-qr-2\",\"zacus-qr-3\"],"
        "\"fragment\":[5]}}"
        "]}";
    puzzle_binding_t b;
    TEST_ASSERT_EQUAL(ESP_OK, puzzle_binding_from_ir(ir, "STEP_QR_DETECTOR", &b));
    TEST_ASSERT_EQUAL(PB_QR, b.type);
    TEST_ASSERT_EQUAL(3, b.id);
    TEST_ASSERT_EQUAL(3, b.code_count);
    TEST_ASSERT_EQUAL_STRING("zacus-qr-1", b.codes[0]);
    TEST_ASSERT_EQUAL_STRING("zacus-qr-2", b.codes[1]);
    TEST_ASSERT_EQUAL_STRING("zacus-qr-3", b.codes[2]);
    TEST_ASSERT_EQUAL(1, b.fragment_len);
    TEST_ASSERT_EQUAL(5, b.fragment[0]);
}

// ── 2. Sound step parses; tolerance defaults to 1 when absent ────────────────
TEST_CASE("pbind: sound step parses with default tolerance", "[pbind]")
{
    const char *ir =
        "{\"steps\":["
        "{\"id\":\"STEP_LA_DETECTOR\","
        "\"puzzle\":{\"id\":1,\"type\":\"sound\","
        "\"melody\":[60,62,64,65],"
        "\"fragment\":[1,2]}}"
        "]}";
    puzzle_binding_t b;
    TEST_ASSERT_EQUAL(ESP_OK, puzzle_binding_from_ir(ir, "STEP_LA_DETECTOR", &b));
    TEST_ASSERT_EQUAL(PB_SOUND, b.type);
    TEST_ASSERT_EQUAL(1, b.id);
    TEST_ASSERT_EQUAL(4, b.note_count);
    TEST_ASSERT_EQUAL(60, b.melody[0]);
    TEST_ASSERT_EQUAL(65, b.melody[3]);
    TEST_ASSERT_EQUAL(1, b.tolerance);  // default
    TEST_ASSERT_EQUAL(2, b.fragment_len);
    TEST_ASSERT_EQUAL(1, b.fragment[0]);
    TEST_ASSERT_EQUAL(2, b.fragment[1]);
}

// ── 3. Step without puzzle → ESP_OK + PB_NONE ────────────────────────────────
TEST_CASE("pbind: step without puzzle yields PB_NONE", "[pbind]")
{
    const char *ir =
        "{\"steps\":["
        "{\"id\":\"STEP_PLAIN\"}"
        "]}";
    puzzle_binding_t b;
    TEST_ASSERT_EQUAL(ESP_OK, puzzle_binding_from_ir(ir, "STEP_PLAIN", &b));
    TEST_ASSERT_EQUAL(PB_NONE, b.type);
}

// ── 4. Unknown step id → ESP_ERR_NOT_FOUND ───────────────────────────────────
TEST_CASE("pbind: unknown step id yields NOT_FOUND", "[pbind]")
{
    const char *ir =
        "{\"steps\":["
        "{\"id\":\"STEP_A\"}"
        "]}";
    puzzle_binding_t b;
    TEST_ASSERT_EQUAL(ESP_ERR_NOT_FOUND,
                      puzzle_binding_from_ir(ir, "STEP_MISSING", &b));
    TEST_ASSERT_EQUAL(PB_NONE, b.type);
}

// ── 5. Malformed JSON → ESP_ERR_INVALID_ARG ──────────────────────────────────
TEST_CASE("pbind: malformed JSON yields INVALID_ARG", "[pbind]")
{
    puzzle_binding_t b;
    TEST_ASSERT_EQUAL(ESP_ERR_INVALID_ARG,
                      puzzle_binding_from_ir("{not valid json", "STEP_A", &b));
    TEST_ASSERT_EQUAL(PB_NONE, b.type);
}

// ── 6a. Puzzle id out of range (0) → ESP_ERR_INVALID_ARG ─────────────────────
TEST_CASE("pbind: puzzle id 0 yields INVALID_ARG", "[pbind]")
{
    const char *ir =
        "{\"steps\":["
        "{\"id\":\"STEP_X\","
        "\"puzzle\":{\"id\":0,\"type\":\"qr\","
        "\"codes\":[\"c1\"],\"fragment\":[1]}}"
        "]}";
    puzzle_binding_t b;
    TEST_ASSERT_EQUAL(ESP_ERR_INVALID_ARG,
                      puzzle_binding_from_ir(ir, "STEP_X", &b));
    TEST_ASSERT_EQUAL(PB_NONE, b.type);
}

// ── 6b. Fragment value 12 (non-single-digit) → ESP_ERR_INVALID_ARG ───────────
TEST_CASE("pbind: fragment value 12 yields INVALID_ARG", "[pbind]")
{
    const char *ir =
        "{\"steps\":["
        "{\"id\":\"STEP_X\","
        "\"puzzle\":{\"id\":2,\"type\":\"qr\","
        "\"codes\":[\"c1\"],\"fragment\":[12]}}"
        "]}";
    puzzle_binding_t b;
    TEST_ASSERT_EQUAL(ESP_ERR_INVALID_ARG,
                      puzzle_binding_from_ir(ir, "STEP_X", &b));
    TEST_ASSERT_EQUAL(PB_NONE, b.type);
}

// ── 8-10. Scene metadata (lenient parser) ────────────────────────────────────
TEST_CASE("scene parses title/subtitle/symbol/effect", "[pbind]")
{
    const char *ir =
        "{\"steps\":["
        "{\"id\":\"STEP_X\",\"scene\":{\"title\":\"MISSION\","
        "\"subtitle\":\"trouvez les QR\",\"symbol\":\"RUN\","
        "\"effect\":\"gyro\"}}"
        "]}";
    scene_binding_t s;
    TEST_ASSERT_EQUAL(ESP_OK, scene_binding_from_ir(ir, "STEP_X", &s));
    TEST_ASSERT_TRUE(s.present);
    TEST_ASSERT_EQUAL_STRING("MISSION", s.title);
    TEST_ASSERT_EQUAL_STRING("trouvez les QR", s.subtitle);
    TEST_ASSERT_EQUAL_STRING("RUN", s.symbol);
    TEST_ASSERT_EQUAL(SB_FX_GYRO, s.effect);
}

TEST_CASE("scene absent leaves present=false, defaults intact", "[pbind]")
{
    const char *ir = "{\"steps\":[{\"id\":\"STEP_X\"}]}";
    scene_binding_t s;
    TEST_ASSERT_EQUAL(ESP_OK, scene_binding_from_ir(ir, "STEP_X", &s));
    TEST_ASSERT_FALSE(s.present);
    TEST_ASSERT_EQUAL(SB_FX_PULSE, s.effect);
}

TEST_CASE("scene unknown effect -> pulse; long title truncated", "[pbind]")
{
    const char *ir =
        "{\"steps\":["
        "{\"id\":\"STEP_X\",\"scene\":{\"effect\":\"discoball\","
        "\"title\":\"0123456789012345678901234567890123456789012345678901234\"}}"
        "]}";
    scene_binding_t s;
    TEST_ASSERT_EQUAL(ESP_OK, scene_binding_from_ir(ir, "STEP_X", &s));
    TEST_ASSERT_TRUE(s.present);
    TEST_ASSERT_EQUAL(SB_FX_PULSE, s.effect);
    TEST_ASSERT_EQUAL(SB_MAX_TITLE - 1, (int) strlen(s.title));
}
