// Standalone tests for reasampler::OwnedFileManifest — no REAPER, no framework.
// The owned-file manifest seam (Phase B B-cap): a deduplicated, insertion-ordered
// set of project-relative files the capture path created, with JSON round-trip.
//
// Covers (brief-named): JSON round-trip, dedup of repeated adds, the empty manifest.
// Plus: the relative-paths-only invariant (reject empty / absolute), contains()
// semantics, insertion-order preservation, malformed-parse -> nullopt (the persist
// shell's warn+fallback hinges on it), and round-trip of paths with JSON metacharacters.

#include "../src/core/model/owned_manifest.h"

#include <cstdio>
#include <string>

using namespace reasampler;
using namespace reasampler::model;

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

// --- empty manifest ----------------------------------------------------------

static void testEmptyManifest() {
    OwnedFileManifest m;
    CHECK(m.empty());
    CHECK(m.size() == 0);
    CHECK(m.paths().empty());
    CHECK(!m.contains("anything.wav"));

    // An empty manifest serializes to a well-formed shape and round-trips to empty.
    const std::string json = m.serialize();
    auto back = OwnedFileManifest::deserialize(json);
    CHECK(back.has_value());
    CHECK(*back == m);
    CHECK(back->empty());
}

// Golden byte-literal (Q-W1 follow-up): pins the EXACT serialized bytes for a
// small fixture (two paths), not just self-consistent re-serialization — a
// format drift that both writer and reader agree on would slip past the
// round-trip tests but not this. The format is frozen as-shipped; the literal
// below is the captured current output.
static void testSerializeGoldenLiteral() {
    OwnedFileManifest m;
    m.add("reasampler_bank/a.wav");
    m.add("reasampler_bank/b.wav");
    CHECK(m.serialize() == "{\"owned\":[\"reasampler_bank/a.wav\",\"reasampler_bank/b.wav\"]}");
}

// --- add / contains / order --------------------------------------------------

static void testAddAndContains() {
    OwnedFileManifest m;
    CHECK(m.add("reasampler_bank/a.wav") == ManifestAddResult::Added);
    CHECK(m.add("reasampler_bank/b.wav") == ManifestAddResult::Added);

    CHECK(m.size() == 2);
    CHECK(m.contains("reasampler_bank/a.wav"));
    CHECK(m.contains("reasampler_bank/b.wav"));
    CHECK(!m.contains("reasampler_bank/c.wav"));
    // Exact-string match — not a prefix / substring match.
    CHECK(!m.contains("reasampler_bank/a"));
    CHECK(!m.contains("a.wav"));

    // Insertion order is preserved (deterministic ext-state).
    CHECK(m.paths().size() == 2);
    CHECK(m.paths()[0] == "reasampler_bank/a.wav");
    CHECK(m.paths()[1] == "reasampler_bank/b.wav");
}

// --- dedup of repeated adds --------------------------------------------------

static void testDedupRepeatedAdds() {
    OwnedFileManifest m;
    CHECK(m.add("reasampler_bank/take.wav") == ManifestAddResult::Added);
    // A repeat capture of an identical request must not double-record the file.
    CHECK(m.add("reasampler_bank/take.wav") == ManifestAddResult::AlreadyPresent);
    CHECK(m.add("reasampler_bank/take.wav") == ManifestAddResult::AlreadyPresent);
    CHECK(m.size() == 1);
    CHECK(m.paths().size() == 1);
}

// --- relative-paths-only invariant -------------------------------------------

static void testRejectsEmptyAndAbsolute() {
    OwnedFileManifest m;
    CHECK(m.add("") == ManifestAddResult::RejectedEmptyPath);
    // Every absolute form bank_model rejects, the manifest rejects too.
    CHECK(m.add("/abs/take.wav") == ManifestAddResult::RejectedAbsolutePath);      // POSIX root
    CHECK(m.add("\\\\host\\share\\x.wav") == ManifestAddResult::RejectedAbsolutePath); /* UNC */
    CHECK(m.add("C:/bank/x.wav") == ManifestAddResult::RejectedAbsolutePath);      // Win drive /
    CHECK(m.add("C:\\bank\\x.wav") == ManifestAddResult::RejectedAbsolutePath);    /* Win drive backslash */
    CHECK(m.add("C:x.wav") == ManifestAddResult::RejectedAbsolutePath);            // drive-relative
    // A rejected add never mutates.
    CHECK(m.empty());
    CHECK(!m.contains("/abs/take.wav"));
}

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

static void testRoundTrip() {
    OwnedFileManifest m;
    m.add("reasampler_bank/one.wav");
    m.add("reasampler_bank/two.wav");
    m.add("reasampler_bank/three.wav");

    const std::string json = m.serialize();
    auto back = OwnedFileManifest::deserialize(json);
    CHECK(back.has_value());
    CHECK(*back == m);
    // Order + membership survive.
    CHECK(back->paths().size() == 3);
    CHECK(back->paths()[0] == "reasampler_bank/one.wav");
    CHECK(back->paths()[2] == "reasampler_bank/three.wav");

    // serialize(deserialize(serialize(x))) is stable.
    CHECK(back->serialize() == json);
}

// A path carrying JSON metacharacters must survive the escape/unescape round-trip.
static void testRoundTripEscaping() {
    OwnedFileManifest m;
    m.add("reasampler_bank/od\"d name.wav");     // embedded quote
    m.add("reasampler_bank/back\\slash.wav");     // embedded backslash
    m.add("reasampler_bank/tab\tafter.wav");      // control char

    auto back = OwnedFileManifest::deserialize(m.serialize());
    CHECK(back.has_value());
    CHECK(*back == m);
    CHECK(back->contains("reasampler_bank/od\"d name.wav"));
    CHECK(back->contains("reasampler_bank/back\\slash.wav"));
    CHECK(back->contains("reasampler_bank/tab\tafter.wav"));
}

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

static void testMalformedParse() {
    // The persist shell's warn+fallback hinges on nullopt for a corrupt blob.
    CHECK(!OwnedFileManifest::deserialize("").has_value());              // empty string
    CHECK(!OwnedFileManifest::deserialize("not json").has_value());
    CHECK(!OwnedFileManifest::deserialize("{\"owned\":[").has_value());  // unterminated array
    CHECK(!OwnedFileManifest::deserialize("{\"owned\":[1,2]}").has_value()); // non-string element
    CHECK(!OwnedFileManifest::deserialize("{\"owned\":\"x\"}").has_value()); // wrong value type

    // An explicit empty array parses to an empty manifest.
    auto empty = OwnedFileManifest::deserialize("{\"owned\":[]}");
    CHECK(empty.has_value());
    CHECK(empty->empty());

    // An unknown sibling key is tolerated (forward-compat) — the owned array still loads.
    auto fwd = OwnedFileManifest::deserialize(
        "{\"future\":{\"nested\":[1,2]},\"owned\":[\"reasampler_bank/x.wav\"]}");
    CHECK(fwd.has_value());
    CHECK(fwd->size() == 1);
    CHECK(fwd->contains("reasampler_bank/x.wav"));

    // A stored blob cannot smuggle a duplicate or absolute path past the load-time
    // invariant re-assertion (deserialize routes each element through add()).
    auto dupe = OwnedFileManifest::deserialize(
        "{\"owned\":[\"reasampler_bank/x.wav\",\"reasampler_bank/x.wav\"]}");
    CHECK(dupe.has_value());
    CHECK(dupe->size() == 1);

    auto absolute = OwnedFileManifest::deserialize(
        "{\"owned\":[\"reasampler_bank/ok.wav\",\"/etc/evil.wav\"]}");
    CHECK(absolute.has_value());
    CHECK(absolute->size() == 1);
    CHECK(absolute->contains("reasampler_bank/ok.wav"));
    CHECK(!absolute->contains("/etc/evil.wav"));
}

int main() {
    testSerializeGoldenLiteral();
    testEmptyManifest();
    testAddAndContains();
    testDedupRepeatedAdds();
    testRejectsEmptyAndAbsolute();
    testRoundTrip();
    testRoundTripEscaping();
    testMalformedParse();

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