// Standalone tests for reasampler::instrument::map::bridge_marshal — no VST3, no REAPER, no test
// framework. Same fast assert loop as the sibling pure tests: assert the REAPER
// bridge-read marshalling (GetProjExtState result decode) directly, so the DAW-facing
// shell only has to invoke the API.
//
// Covers: decodeGetProjExtState hit/absent/zero-return/empty-buffer (the stale-buffer
// guard). The S1 spike's extractJsonStringField string-scan reader was retired in S4
// (the instrument now parses the bank through the shared bank_book JSON path), so its
// cases are gone with it. Q-W5 (rider T2-04) added readProjExtStateGrowing — the ONE
// grow-loop retry policy shared by persist / usage_scan / reaper_bridge, rehomed to
// core/wire/ext_state_read.h in Q-W6 and still pinned here alongside the decode — covered
// against a fake read: absent, small-fit, grow-then-fit, empty-complete (composed with
// the decode guard), and the 16 MB overflow give-up. The overflow case is
// prune-safety-adjacent (usage_scan folds it to abortPrune), so it is pinned here.

#include "../src/core/instrument/map/bridge_marshal.h"
#include "../src/core/wire/ext_state_read.h"  // readProjExtStateGrowing (Q-W6 rehome)

#include <cstdio>
#include <cstring>
#include <string>

using namespace reasampler;
using namespace reasampler::instrument::map;
using namespace reasampler::wire;

static int g_fail = 0;
#define CHECK(cond) do { if(!(cond)) { \
    std::printf("FAIL line %d: %s\n", __LINE__, #cond); ++g_fail; } } while(0)

// --- decodeGetProjExtState ----------------------------------------------------

static void testDecodeHit() {
    // REAPER reports a non-zero length and filled the buffer: that IS the value.
    auto v = decodeGetProjExtState(5, "hello");
    CHECK(v.has_value());
    CHECK(v && *v == "hello");
}

static void testDecodeAbsentKey() {
    // REAPER returns 0 for an absent key. Even if a caller passed a dirty buffer, the
    // decoder must NOT surface it — the zero return means "no value".
    auto v = decodeGetProjExtState(0, "stale-bytes-from-a-prior-read");
    CHECK(!v.has_value());
}

static void testDecodeNegativeReturn() {
    auto v = decodeGetProjExtState(-1, "whatever");
    CHECK(!v.has_value());
}

static void testDecodeEmptyBuffer() {
    // Positive return but empty buffer — treat as no value (defensive).
    auto v = decodeGetProjExtState(3, "");
    CHECK(!v.has_value());
}

// --- readProjExtStateGrowing (the T2-04 shared grow-loop policy) -------------

// A fake GetProjExtState: honors the buffer contract (writes at most cap-1 chars +
// NUL — REAPER clips to the caller's capacity) and returns the stored value's length.
static int fakeRead(const std::string& stored, char* buf, int cap) {
    const std::size_t n =
        stored.size() < static_cast<std::size_t>(cap - 1)
            ? stored.size()
            : static_cast<std::size_t>(cap - 1);
    std::memcpy(buf, stored.data(), n);
    buf[n] = '\0';
    return static_cast<int>(stored.size());
}

static void testGrowingAbsent() {
    // rv <= 0 on the first attempt: the key holds no value — Absent, no retry.
    int calls = 0;
    const auto r = readProjExtStateGrowing([&](char*, int) {
        ++calls;
        return 0;
    });
    CHECK(r.status == GrowingExtStateRead::Status::Absent);
    CHECK(r.apiReturn == 0);
    CHECK(calls == 1);
}

static void testGrowingSmallValueFitsFirstAttempt() {
    const std::string stored = "hello";
    int calls = 0;
    const auto r = readProjExtStateGrowing([&](char* buf, int cap) {
        ++calls;
        return fakeRead(stored, buf, cap);
    });
    CHECK(r.status == GrowingExtStateRead::Status::Complete);
    CHECK(r.value == stored);
    CHECK(r.apiReturn == 5);
    CHECK(calls == 1);
}

static void testGrowingRetriesUntilStrictFit() {
    // A value whose C string exactly fills the first buffer (size+1 == cap) is
    // AMBIGUOUS — it may have been clipped — so the policy must retry at the next
    // capacity, where it fits strictly and returns whole.
    const std::string stored(static_cast<std::size_t>((1 << 16) - 1), 'x');
    int calls = 0;
    const auto r = readProjExtStateGrowing([&](char* buf, int cap) {
        ++calls;
        return fakeRead(stored, buf, cap);
    });
    CHECK(r.status == GrowingExtStateRead::Status::Complete);
    CHECK(r.value == stored);
    CHECK(calls == 2);
}

static void testGrowingEmptyCompleteComposesWithDecodeGuard() {
    // rv > 0 but an empty buffer: the loop reports a Complete empty value, and the
    // bridge path's decodeGetProjExtState(apiReturn, value) still rejects it — the
    // stale/empty-buffer guard survives the T2-04 rewire unchanged.
    const auto r = readProjExtStateGrowing([&](char* buf, int) {
        buf[0] = '\0';
        return 3;
    });
    CHECK(r.status == GrowingExtStateRead::Status::Complete);
    CHECK(r.value.empty());
    CHECK(!decodeGetProjExtState(r.apiReturn, r.value).has_value());
}

static void testGrowingOverflowGivesUpAtCeiling() {
    // Every attempt clips (the value never fits under 16 MB): Overflow — which is
    // "unreadable WHOLE", never Absent. usage_scan folds this to the prune fail-safe
    // abort, so the distinction is load-bearing. Caps run 2^16..2^24 step ×4 = 5 tries.
    int calls = 0;
    const auto r = readProjExtStateGrowing([&](char* buf, int cap) {
        ++calls;
        std::memset(buf, 'a', static_cast<std::size_t>(cap - 1));
        buf[cap - 1] = '\0';
        return cap;  // reports a length that never strictly fits
    });
    CHECK(r.status == GrowingExtStateRead::Status::Overflow);
    CHECK(calls == 5);
}

int main() {
    testDecodeHit();
    testDecodeAbsentKey();
    testDecodeNegativeReturn();
    testDecodeEmptyBuffer();

    testGrowingAbsent();
    testGrowingSmallValueFitsFirstAttempt();
    testGrowingRetriesUntilStrictFit();
    testGrowingEmptyCompleteComposesWithDecodeGuard();
    testGrowingOverflowGivesUpAtCeiling();

    if (g_fail == 0) std::printf("bridge_marshal: all tests passed\n");
    return g_fail != 0;
}
