// Standalone tests for reasampler::package::bank_package — no REAPER, no test
// framework. Byte-level suites hand-roll RSBK images with wire::putLE rather
// than calling encodePackage, so a layout regression in encode cannot hide from
// decode (the two sides are pinned against each other AND against raw bytes).

#include "../src/core/package/bank_package.h"

#include <cstdio>
#include <cstdint>
#include <string>
#include <vector>

#include "../src/core/version/app_version.h"
#include "../src/core/wire/bytes.h"

using namespace reasampler::package;
using namespace reasampler::model;
namespace wire = reasampler::wire;
namespace version = reasampler::version;

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

// --- fixtures ----------------------------------------------------------------

// Every Sample field populated, every optional PRESENT.
static Sample fullSample() {
    Sample s;
    s.id = "smp-full";
    s.displayName = "Kick (wet)";
    s.relativePath = "reasampler_bank/kick.wav";
    s.sourceMode = SourceMode::RazorArea;
    s.sourceRange = {1.25, 3.5, 480.0, 1920.0};
    s.trackGuids = {"{AAA}", "{BBB}"};
    s.wetDry = 0.75;
    s.channelCount = 2;
    s.sampleRate = 48000;
    s.lengthSeconds = 2.25;
    s.lengthBeats = 4.5;
    s.captureTempo = 120.5;
    s.captureTimeSigNum = 7;
    s.captureTimeSigDenom = 8;
    s.key = "F#m";
    s.rootNote = 60;
    s.loop = LoopPoints{100, 4800};
    s.levels = {-0.3, -12.7, -14.0};
    s.clipped = true;
    s.tier = Tier::Archive;
    s.contentHash = "W0123456789abcdef";
    s.provenance = Provenance{"smp-parent", "fx-snapshot"};
    s.createdTimestamp = 1754000000;
    return s;
}

// Every Sample optional ABSENT (key, rootNote, loop, provenance).
static Sample bareSample() {
    Sample s;
    s.id = "smp-bare";
    s.displayName = "Snare";
    s.relativePath = "reasampler_bank/snare.wav";
    s.sourceMode = SourceMode::Realtime;
    s.contentHash = "Wfedcba9876543210";
    s.createdTimestamp = 1754000001;
    return s;
}

static PackageManifest fixture(std::uint64_t len0, std::uint64_t len1) {
    PackageManifest m;
    m.bankDisplayName = "Drums \"live\"";
    m.exportTimestamp = 1754100000;
    m.entries.push_back({"kick.wav", len0, "1111222233334444", fullSample()});
    m.entries.push_back({"snare.wav", len1, "5555666677778888", bareSample()});
    m.slots.append("smp-full");
    m.slots.append("smp-bare");
    return m;
}

// A hand-rolled RSBK image: frozen region + a raw tail (manifest framing or
// deliberate garbage), independent of encodePackage.
static std::vector<std::uint8_t> rawHeader(std::uint32_t fv, std::uint32_t mv,
                                           const std::string& semver) {
    std::vector<std::uint8_t> out;
    out.insert(out.end(), kPackageMagic, kPackageMagic + 4);
    wire::putLE(out, fv);
    wire::putLE(out, mv);
    wire::putLE(out, static_cast<std::uint32_t>(semver.size()));
    out.insert(out.end(), semver.begin(), semver.end());
    return out;
}

static void appendManifest(std::vector<std::uint8_t>& out, const std::string& json) {
    wire::putLE(out, static_cast<std::uint32_t>(json.size()));
    out.insert(out.end(), json.begin(), json.end());
}

// One-entry manifest JSON with `extra` spliced in as additional root content
// ("" for none) and `sampleExtra` spliced into the nested Sample object — for
// images a current writer would never emit.
static std::string handManifest(const std::string& name, int length,
                                const std::string& extra,
                                const std::string& sampleExtra = "") {
    return std::string("{") + extra +
           "\"entries\":[{\"name\":\"" + name +
           "\",\"length\":" + std::to_string(length) + ",\"hash\":\"h\","
           "\"index\":{\"version\":1,\"samples\":[{\"id\":\"s1\"," + sampleExtra +
           "\"relativePath\":\"bank/a.wav\"}]}}]}";
}

// --- encode / decode round trip ----------------------------------------------

static void testEncodeDecodeRoundTrip() {
    const PackageManifest m = fixture(96000, 48000);
    auto enc = encodePackage(m);
    CHECK(enc.has_value());

    // Layout arithmetic: payloads start at the prefix end, in manifest order.
    CHECK(enc->layout.size() == 2);
    CHECK(enc->layout[0].name == "kick.wav");
    CHECK(enc->layout[0].offset == enc->prefix.size());
    CHECK(enc->layout[0].length == 96000);
    CHECK(enc->layout[1].offset == enc->prefix.size() + 96000);
    CHECK(enc->layout[1].length == 48000);
    CHECK(enc->totalSize == enc->prefix.size() + 96000 + 48000);

    // decodePackage(encodePackage(x)) == x — payloads are never read by the
    // codec, so the prefix plus the true total size is the whole input.
    const DecodedPackage dec = decodePackage(enc->prefix, enc->totalSize);
    CHECK(dec.status == PackageReadability::Readable);
    CHECK(dec.manifest == m);
    CHECK(dec.layout == enc->layout);
    CHECK(dec.prefixSize == enc->prefix.size());

    // The header stamps this build's ladder pair and its informational semver.
    CHECK(dec.header.formatVersion == kPackageFormatVersion);
    CHECK(dec.header.minReaderVersion == kPackageMinReaderVersion);
    CHECK(dec.header.writerVersion == version::stampVersion());
}

static void testEncodeRefusesWhatManifestRefuses() {
    PackageManifest m = fixture(1, 1);
    m.entries[0].fileName = "../evil.wav";
    CHECK(!encodePackage(m).has_value());

    // See src/core/package/CLAUDE.md for the shell seam that forces this.
    PackageManifest zeroLen = fixture(1, 1);
    zeroLen.entries[1].byteLength = 0;
    CHECK(!encodePackage(zeroLen).has_value());
}

// --- truncation: every byte offset -------------------------------------------

static void testTruncationAtEveryByteOffsetIsMalformed() {
    const PackageManifest m = fixture(3, 5);
    auto enc = encodePackage(m);
    CHECK(enc.has_value());

    // The complete on-disk image: prefix + both payloads.
    std::vector<std::uint8_t> file = enc->prefix;
    for (std::uint8_t b : {1, 2, 3, 10, 20, 30, 40, 50}) file.push_back(b);
    CHECK(file.size() == enc->totalSize);
    CHECK(decodePackage(file, file.size()).status == PackageReadability::Readable);

    for (std::size_t cut = 0; cut < file.size(); ++cut) {
        const std::vector<std::uint8_t> truncated(file.begin(), file.begin() + cut);
        const DecodedPackage dec = decodePackage(truncated, truncated.size());
        if (dec.status != PackageReadability::Malformed) {
            std::printf("FAIL: truncation at %zu not Malformed\n", cut);
            ++g_fail;
            break;
        }
        // A refused decode must not half-succeed at any cut either.
        CHECK(dec.manifest.entries.empty());
        CHECK(dec.layout.empty());
    }

    // One byte extra (trailing garbage) is as Malformed as one byte missing.
    std::vector<std::uint8_t> extended = file;
    extended.push_back(0);
    CHECK(decodePackage(extended, extended.size()).status == PackageReadability::Malformed);
    // And a file size that disagrees with the same bytes.
    CHECK(decodePackage(file, file.size() + 1).status == PackageReadability::Malformed);
    CHECK(decodePackage(file, file.size() - 1).status == PackageReadability::Malformed);
}

// --- header defaults ---------------------------------------------------------

// The 0/0 defaults are not the current ladder pair, so a header that never
// parsed cannot be mistaken for a plausible 1/1. They mean "unset", NOT "the
// decode failed": a decode that got past the header reports the real pair
// alongside its Malformed verdict.
static void testHeaderDefaultsMeanUnparsed() {
    CHECK(PackageHeader{}.formatVersion == 0);
    CHECK(PackageHeader{}.minReaderVersion == 0);
    CHECK(PackageHeader{}.writerVersion.empty());

    // Failed before the header: bad magic, and a semver truncated mid-string.
    std::vector<std::uint8_t> badMagic = rawHeader(1, 1, "1.0.0");
    appendManifest(badMagic, "{}");
    badMagic[0] = 'Z';
    const DecodedPackage magic = decodePackage(badMagic, badMagic.size());
    CHECK(magic.status == PackageReadability::Malformed);
    CHECK(magic.header == PackageHeader{});

    std::vector<std::uint8_t> cutSemver = rawHeader(1, 1, "1.0.0");
    cutSemver.resize(18);
    CHECK(decodePackage(cutSemver, cutSemver.size()).header == PackageHeader{});

    // Failed after it: a same-version package with a corrupt manifest is
    // Malformed, and its header is fully populated.
    std::vector<std::uint8_t> corrupt =
        rawHeader(kPackageFormatVersion, kPackageMinReaderVersion, "1.0.0");
    appendManifest(corrupt, "not json");
    const DecodedPackage late = decodePackage(corrupt, corrupt.size());
    CHECK(late.status == PackageReadability::Malformed);
    CHECK(late.header.formatVersion == kPackageFormatVersion);
    CHECK(late.header.minReaderVersion == kPackageMinReaderVersion);
    CHECK(late.header.writerVersion == "1.0.0");
}

// --- version ladder: TooNew refuses whole ------------------------------------

static void testTooNewProducesNoManifest() {
    // A future structural format: only the frozen region is trustworthy, so the
    // tail is deliberate garbage that would crash a parser that kept reading.
    std::vector<std::uint8_t> bytes = rawHeader(9, 9, "9.9.9");
    for (int i = 0; i < 32; ++i) bytes.push_back(0xFF);

    const DecodedPackage dec = decodePackage(bytes, bytes.size());
    CHECK(dec.status == PackageReadability::TooNew);
    // The refusal message's three facts survive...
    CHECK(dec.header.formatVersion == 9);
    CHECK(dec.header.minReaderVersion == 9);
    CHECK(dec.header.writerVersion == "9.9.9");
    // ...and nothing else is produced: no manifest, no layout, no half-success.
    CHECK(dec.manifest.entries.empty());
    CHECK(dec.manifest == PackageManifest{});
    CHECK(dec.layout.empty());
    CHECK(dec.prefixSize == 0);

    // Boundary: minReader exactly one past this build.
    auto boundary = rawHeader(kPackageFormatVersion + 1, kPackageFormatVersion + 1, "2.0.0");
    CHECK(decodePackage(boundary, boundary.size()).status == PackageReadability::TooNew);

    // A TooNew header truncated inside the frozen region cannot name the
    // writer, so it is Malformed, not an unactionable refusal.
    std::vector<std::uint8_t> cut = rawHeader(9, 9, "9.9.9");
    cut.resize(18);  // mid-semver
    CHECK(decodePackage(cut, cut.size()).status == PackageReadability::Malformed);
}

// An additively-tagged package (fv > ours, minReader still within reach) whose
// manifest fails to parse: the header classifies Readable, so decode reads
// into the manifest and fails there. That failure must still report TooNew —
// the header is valid and already carries the writer's semver — not the
// unactionable Malformed a genuinely corrupt header produces.
static void testAdditiveUnparseableManifestIsTooNew() {
    std::vector<std::uint8_t> bytes = rawHeader(kPackageFormatVersion + 1, kPackageMinReaderVersion, "1.9.0");
    appendManifest(bytes, "not json");
    const DecodedPackage dec = decodePackage(bytes, bytes.size());
    CHECK(dec.status == PackageReadability::TooNew);
    CHECK(dec.header.formatVersion == kPackageFormatVersion + 1);
    CHECK(dec.header.minReaderVersion == kPackageMinReaderVersion);
    CHECK(dec.header.writerVersion == "1.9.0");
    CHECK(dec.manifest.entries.empty());
    CHECK(dec.layout.empty());

    // Same-version unparseable manifest stays Malformed: nothing "newer"
    // excuses it, so this is not a blanket "unparseable == TooNew" rule.
    std::vector<std::uint8_t> sameVersion =
        rawHeader(kPackageFormatVersion, kPackageMinReaderVersion, "1.0.0");
    appendManifest(sameVersion, "not json");
    CHECK(decodePackage(sameVersion, sameVersion.size()).status == PackageReadability::Malformed);
}

// --- version ladder: additive forward compatibility --------------------------

// The reason two integers exist: a NEWER formatVersion whose minReaderVersion
// still reaches back to this build must read, with its unknown keys skipped —
// at the manifest level AND inside the nested Sample blob, the actual
// motivating case for the two-integer ladder (see this directory's CLAUDE.md).
static void testNewerAdditiveFormatReads() {
    const std::string manifest = handManifest(
        "a.wav", 4,
        "\"instrumentState\":{\"future\":[1,2,3]},\"anotherNewKey\":\"x\",",
        "\"someFutureSampleField\":42,");
    std::vector<std::uint8_t> bytes =
        rawHeader(kPackageFormatVersion + 1, kPackageMinReaderVersion, "1.9.0");
    appendManifest(bytes, manifest);
    const std::uint64_t total = bytes.size() + 4;  // the one entry's payload

    const DecodedPackage dec = decodePackage(bytes, total);
    CHECK(dec.status == PackageReadability::Readable);
    CHECK(dec.header.formatVersion == kPackageFormatVersion + 1);
    CHECK(dec.header.writerVersion == "1.9.0");
    CHECK(dec.manifest.entries.size() == 1);
    CHECK(dec.manifest.entries[0].fileName == "a.wav");
    CHECK(dec.manifest.entries[0].sample.id == "s1");
    CHECK(dec.layout.size() == 1);
    CHECK(dec.layout[0].offset == bytes.size());
    CHECK(dec.layout[0].length == 4);
}

// --- hostile headers ---------------------------------------------------------

static void testHostileHeadersAreMalformed() {
    // Wrong magic.
    std::vector<std::uint8_t> bad = rawHeader(1, 1, "1.0.0");
    appendManifest(bad, "{}");
    bad[0] = 'Z';
    CHECK(decodePackage(bad, bad.size()).status == PackageReadability::Malformed);

    // Incoherent version pairs (the classify rules, proven through the framing).
    for (auto [fv, mv] : {std::pair<std::uint32_t, std::uint32_t>{0, 0}, {0, 1},
                          {1, 0}, {1, 2}}) {
        std::vector<std::uint8_t> b = rawHeader(fv, mv, "1.0.0");
        appendManifest(b, "{}");
        CHECK(decodePackage(b, b.size()).status == PackageReadability::Malformed);
    }

    // A forged semver length over the cap must not read past the buffer.
    std::vector<std::uint8_t> overSemver;
    overSemver.insert(overSemver.end(), kPackageMagic, kPackageMagic + 4);
    wire::putLE(overSemver, std::uint32_t{1});
    wire::putLE(overSemver, std::uint32_t{1});
    wire::putLE(overSemver, kMaxWriterVersionBytes + 1);
    CHECK(decodePackage(overSemver, overSemver.size()).status ==
          PackageReadability::Malformed);

    // A forged manifest length over the cap: refused before any allocation.
    std::vector<std::uint8_t> overManifest = rawHeader(1, 1, "1.0.0");
    wire::putLE(overManifest, kMaxManifestBytes + 1);
    CHECK(decodePackage(overManifest, overManifest.size()).status ==
          PackageReadability::Malformed);

    // A manifest whose entry name expresses a path: rejected on decode even
    // though no current encoder would write it.
    std::vector<std::uint8_t> traversal = rawHeader(1, 1, "1.0.0");
    appendManifest(traversal, handManifest("../evil.wav", 4, ""));
    CHECK(decodePackage(traversal, traversal.size() + 4).status ==
          PackageReadability::Malformed);
}

// --- requiredPrefixSize ------------------------------------------------------

static void testRequiredPrefixSizeGrowsToTheFullPrefix() {
    auto enc = encodePackage(fixture(3, 5));
    CHECK(enc.has_value());
    const auto& prefix = enc->prefix;

    // Empty: the fixed region first.
    CHECK(requiredPrefixSize({}) == std::uint64_t{16});

    // With the fixed region: asks through the semver + manifest-length field.
    const std::string& semver = version::stampVersion();
    std::vector<std::uint8_t> first16(prefix.begin(), prefix.begin() + 16);
    CHECK(requiredPrefixSize(first16) == std::uint64_t{16 + semver.size() + 4});

    // With that much: the full prefix size. And the answer is a fixpoint.
    std::vector<std::uint8_t> upToManifestLen(
        prefix.begin(), prefix.begin() + 20 + static_cast<long>(semver.size()));
    CHECK(requiredPrefixSize(upToManifestLen) == std::uint64_t{prefix.size()});
    CHECK(requiredPrefixSize(prefix) == std::uint64_t{prefix.size()});

    // The returned count is exactly enough for decodePackage.
    CHECK(decodePackage(prefix, enc->totalSize).status == PackageReadability::Readable);
}

static void testRequiredPrefixSizeRefusals() {
    // Bad magic: stop reading.
    std::vector<std::uint8_t> bad(16, 0);
    CHECK(!requiredPrefixSize(bad).has_value());

    // Incoherent versions: stop reading.
    std::vector<std::uint8_t> zeroed = rawHeader(0, 0, "1.0.0");
    CHECK(!requiredPrefixSize(zeroed).has_value());

    // TooNew: asks only through the frozen region — the manifest-length field
    // belongs to the newer format and is never trusted.
    std::vector<std::uint8_t> tooNew = rawHeader(9, 9, "9.9.9");
    for (int i = 0; i < 8; ++i) tooNew.push_back(0xFF);  // garbage where M would be
    CHECK(requiredPrefixSize(tooNew) == std::uint64_t{16 + 5});

    // Oversize length fields: stop reading.
    std::vector<std::uint8_t> overSemver;
    overSemver.insert(overSemver.end(), kPackageMagic, kPackageMagic + 4);
    wire::putLE(overSemver, std::uint32_t{1});
    wire::putLE(overSemver, std::uint32_t{1});
    wire::putLE(overSemver, kMaxWriterVersionBytes + 1);
    CHECK(!requiredPrefixSize(overSemver).has_value());

    std::vector<std::uint8_t> overManifest = rawHeader(1, 1, "1.0.0");
    wire::putLE(overManifest, kMaxManifestBytes + 1);
    CHECK(!requiredPrefixSize(overManifest).has_value());
}

int main() {
    testEncodeDecodeRoundTrip();
    testEncodeRefusesWhatManifestRefuses();
    testTruncationAtEveryByteOffsetIsMalformed();
    testHeaderDefaultsMeanUnparsed();
    testTooNewProducesNoManifest();
    testAdditiveUnparseableManifestIsTooNew();
    testNewerAdditiveFormatReads();
    testHostileHeadersAreMalformed();
    testRequiredPrefixSizeRefusals();
    testRequiredPrefixSizeGrowsToTheFullPrefix();

    if (g_fail == 0) {
        std::printf("bank_package_tests: all passed\n");
        return 0;
    }
    std::printf("bank_package_tests: %d failure(s)\n", g_fail);
    return 1;
}
