// Standalone tests for reasampler::AssignmentRequest — no REAPER, no framework.
// The S8 ingest assignment-request seam: the (bankId, sampleId, generation) value the
// extension writes to ext-state after an ingest-with-assign, decoded by the instrument
// in a later dispatch. Only the wire format lives in this module; test it hard because
// the reader (a different artifact) must decode exactly what this writer produces.
//
// Covers: encode/decode round-trip, ids carrying arbitrary bytes (GUIDs, separators),
// the generation field including zero and negative-guard, and malformed/truncated/
// trailing-garbage input -> nullopt (the reader's "no pending request" fallback hinges
// on it).

#include "../src/core/wire/assignment_request.h"

#include <cstdio>
#include <string>

using namespace reasampler;
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)

// --- round-trip --------------------------------------------------------------

static void testRoundTrip() {
    AssignmentRequest req;
    req.bankId     = "{12345678-1234-1234-1234-1234567890AB}";
    req.sampleId   = "cap-1700000000-kick.wav";
    req.generation = 1700000123;

    const std::string wire = encodeAssignmentRequest(req);
    auto back = decodeAssignmentRequest(wire);
    CHECK(back.has_value());
    CHECK(*back == req);
    // Re-encoding the decoded value is byte-stable (deterministic encoder).
    CHECK(encodeAssignmentRequest(*back) == wire);
}

// The pool bank id and an empty-ish generation must round-trip too (generation 0 is the
// documented pre-S9 default; an assign still carries a real stamp, but 0 must be legal).
static void testRoundTripPoolAndZeroGeneration() {
    AssignmentRequest req;
    req.bankId     = "pool";
    req.sampleId   = "s1";
    req.generation = 0;

    auto back = decodeAssignmentRequest(encodeAssignmentRequest(req));
    CHECK(back.has_value());
    CHECK(back->bankId == "pool");
    CHECK(back->sampleId == "s1");
    CHECK(back->generation == 0);
}

// Ids carrying the wire's own metacharacters (':' the length delimiter, digits that
// could be misread as a length, the magic-tag bytes) must survive whole — the whole
// reason for length-prefixing over a delimiter-split format.
static void testRoundTripAdversarialIds() {
    AssignmentRequest req;
    req.bankId     = "12:34:has-colons";       // ':' is the length delimiter
    req.sampleId   = "rsassign1-lookalike-99";  // embeds the magic tag
    req.generation = -42;                        // negative is representable

    auto back = decodeAssignmentRequest(encodeAssignmentRequest(req));
    CHECK(back.has_value());
    CHECK(*back == req);
    CHECK(back->bankId == "12:34:has-colons");
    CHECK(back->sampleId == "rsassign1-lookalike-99");
    CHECK(back->generation == -42);
}

// Empty ids are structurally valid on the wire (length 0) and must round-trip — the
// decoder must not conflate an empty field with a parse failure.
static void testRoundTripEmptyFields() {
    AssignmentRequest req;
    req.bankId     = "";
    req.sampleId   = "";
    req.generation = 7;

    auto back = decodeAssignmentRequest(encodeAssignmentRequest(req));
    CHECK(back.has_value());
    CHECK(*back == req);
}

// A large generation (past 32-bit) must not truncate — the field is int64.
static void testLargeGeneration() {
    AssignmentRequest req;
    req.bankId     = "b";
    req.sampleId   = "s";
    req.generation = 9007199254740993LL;  // > 2^53, > INT32_MAX

    auto back = decodeAssignmentRequest(encodeAssignmentRequest(req));
    CHECK(back.has_value());
    CHECK(back->generation == 9007199254740993LL);
}

// --- malformed / tolerant parse ----------------------------------------------

static void testMalformedParse() {
    // Absence / total garbage — the reader maps these to "no pending request".
    CHECK(!decodeAssignmentRequest("").has_value());
    CHECK(!decodeAssignmentRequest("not a request").has_value());
    // Wrong magic tag.
    CHECK(!decodeAssignmentRequest("rsprov1" "1:b1:s1:7").has_value());
    // Magic only, no fields.
    CHECK(!decodeAssignmentRequest("rsassign1").has_value());
    // Truncated mid-record (missing the generation field).
    CHECK(!decodeAssignmentRequest("rsassign1" "4:pool2:s1").has_value());
    // A length that runs past the end.
    CHECK(!decodeAssignmentRequest("rsassign1" "99:short").has_value());
    // Non-numeric length token.
    CHECK(!decodeAssignmentRequest("rsassign1" "x:pool2:s11:7").has_value());
    // A non-numeric generation field.
    CHECK(!decodeAssignmentRequest("rsassign1" "4:pool2:s13:abc").has_value());
    // A bare "-" generation.
    CHECK(!decodeAssignmentRequest("rsassign1" "4:pool2:s11:-").has_value());
}

// Trailing garbage after a well-formed record must be rejected — a partial/padded blob
// is not a valid request, and the reader must never accept the prefix and ignore the rest.
static void testTrailingGarbageRejected() {
    AssignmentRequest req;
    req.bankId     = "pool";
    req.sampleId   = "s1";
    req.generation = 7;
    const std::string wire = encodeAssignmentRequest(req);

    // The clean value parses.
    CHECK(decodeAssignmentRequest(wire).has_value());
    // The same value with any trailing byte does not.
    CHECK(!decodeAssignmentRequest(wire + "X").has_value());
    CHECK(!decodeAssignmentRequest(wire + "0:").has_value());
}

// --- overflow / adversarial integer inputs ------------------------------------

// A 21-digit length field overflows SIZE_MAX and must be rejected safely (no UB,
// no wrap-around that could make a huge length appear small and pass the bounds check).
static void testOverflowFieldLength() {
    // Craft a wire where the bankId length token is 21 digits that exceed SIZE_MAX.
    // The decoder must fail cleanly, not access memory out of bounds.
    // "rsassign1" + "999999999999999999999:" (21 nines) + junk: rejects before OOB.
    const std::string wire = std::string("rsassign1") + "999999999999999999999:junk";
    CHECK(!decodeAssignmentRequest(wire).has_value());
}

// A 20-digit generation (exceeds the 19-digit cap) must be rejected safely.
static void testOverflowFieldInt64() {
    // Encode a valid record then manually substitute the generation with a 20-digit value.
    // We cannot use encode (it would produce a correct 19-digit generation), so we
    // build the wire manually. Generation "99999999999999999999" (20 nines) exceeds cap.
    // bankId = "pool" (4 bytes), sampleId = "s1" (2 bytes).
    const std::string wire = std::string("rsassign1")
                             + "4:pool"
                             + "2:s1"
                             + "20:99999999999999999999";
    CHECK(!decodeAssignmentRequest(wire).has_value());
}

// SIZE_MAX as a length field (20 digits, within the digit-count cap) must not UB or
// wrap. The overflow-guard in field() caps the multiplication; even if the value itself
// does not trigger the multiply guard (SIZE_MAX accumulates cleanly digit by digit),
// the subsequent "len > s_.size() - start" bounds check catches it because the actual
// string is tiny — no OOB access, no wraparound, clean rejection.
static void testOverflowExactSizeMax() {
    // 18446744073709551615 = SIZE_MAX on 64-bit. 20 digits: within the digit cap, but the
    // trailing bounds check rejects it because the wire string is far smaller than SIZE_MAX.
    const std::string wire = std::string("rsassign1") + "18446744073709551615:X";
    CHECK(!decodeAssignmentRequest(wire).has_value());
}

int main() {
    testRoundTrip();
    testRoundTripPoolAndZeroGeneration();
    testRoundTripAdversarialIds();
    testRoundTripEmptyFields();
    testLargeGeneration();
    testMalformedParse();
    testTrailingGarbageRejected();
    testOverflowFieldLength();
    testOverflowFieldInt64();
    testOverflowExactSizeMax();

    if (g_fail == 0) std::printf("All tests passed.\n");
    return g_fail ? 1 : 0;
}
