// Standalone tests for reasampler::model::SlotMap — no REAPER, no test framework.
// SlotMap is the L7 gap-preserving display-position carrier for one bank, extracted
// from bank_book (Q-W1, T4-05). These are the pure SlotMap-only assertions that
// previously lived inline in test_bank_book.cpp (the L7 "SlotMap unit behaviour"
// block); test_bank_book.cpp keeps its BankBook-level integration coverage
// (reorderSample / reconcileSlots / JSON round-trip WITH a full book), this file
// owns the module's own contract: add/remove/query, reorder gap-preservation,
// resetDense/reconcile, equality/fromEntries, and the serialize wire shape.

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

#include <cstdio>
#include <string>
#include <utility>
#include <vector>

#include "../src/core/json/json.h"

using namespace reasampler::model;
namespace json = reasampler::json;

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

// --- SlotMap unit behaviour --------------------------------------------------

static void testSlotMapDenseAppend() {
    SlotMap m;
    m.append("a");
    m.append("b");
    m.append("c");
    CHECK(m.slotOf("a") == 0);
    CHECK(m.slotOf("b") == 1);
    CHECK(m.slotOf("c") == 2);
    CHECK(m.maxSlot() == 2);
    CHECK((m.orderedIds() == std::vector<std::string>{"a", "b", "c"}));
    CHECK(m.idAt(1) == "b");
    CHECK(m.slotOf("nope") == -1);
}

static void testSlotMapRemoveLeavesGap() {
    SlotMap m;
    m.append("a"); m.append("b"); m.append("c");  // 0,1,2
    CHECK(m.remove("b"));                          // slot 1 now EMPTY (no re-pack)
    CHECK(m.slotOf("a") == 0);
    CHECK(m.slotOf("c") == 2);                     // c did NOT shift down
    CHECK(m.idAt(1).empty());                      // gap preserved
    CHECK((m.orderedIds() == std::vector<std::string>{"a", "c"}));
    CHECK(!m.remove("b"));                         // already gone
}

static void testSlotMapAppendAfterGapGoesToFrontier() {
    SlotMap m;
    m.append("a"); m.append("b"); m.append("c");  // 0,1,2
    m.remove("a");                                 // slot 0 empty
    m.append("d");                                 // append goes AFTER last occupied (2) -> 3
    CHECK(m.slotOf("d") == 3);                      // did NOT fill the slot-0 gap
    CHECK(m.idAt(0).empty());
}

static void testSlotMapReorderIntoEmpty() {
    SlotMap m;
    m.append("a"); m.append("b"); m.append("c");  // 0,1,2
    m.remove("b");                                 // slot 1 empty
    CHECK(m.reorder("c", 1));                       // c -> empty slot 1; its slot 2 empties
    CHECK(m.slotOf("c") == 1);
    CHECK(m.idAt(2).empty());
    CHECK(m.slotOf("a") == 0);                      // untouched
}

static void testSlotMapReorderOntoOccupiedInsertsAndShifts() {
    SlotMap m;
    m.append("a"); m.append("b"); m.append("c"); m.append("d");  // 0,1,2,3
    CHECK(m.reorder("d", 1));   // d onto occupied slot 1 -> insert-before, shift b,c up
    CHECK(m.slotOf("a") == 0);  // before the target: unchanged
    CHECK(m.slotOf("d") == 1);  // took the target slot
    CHECK(m.slotOf("b") == 2);  // shifted +1
    CHECK(m.slotOf("c") == 3);  // shifted +1
    CHECK((m.orderedIds() == std::vector<std::string>{"a", "d", "b", "c"}));
}

static void testSlotMapReorderPreservesInteriorGapAboveTarget() {
    SlotMap m;
    m.append("a"); m.append("b"); m.append("c");  // 0,1,2
    m.remove("b");                                 // gap at 1: a@0, c@2
    m.append("d");                                 // d@3
    CHECK(m.reorder("d", 0));   // d onto occupied slot 0 -> a shifts to 1, c shifts to 3
    CHECK(m.slotOf("d") == 0);
    CHECK(m.slotOf("a") == 1);  // shifted from 0 -> 1
    CHECK(m.slotOf("c") == 3);  // shifted from 2 -> 3 (gap at 2 preserved as a +1 of its own)
    CHECK(m.idAt(2).empty());   // interior gap above the target survives
}

static void testSlotMapReorderUnmappedIsNoOp() {
    SlotMap m;
    m.append("a");
    CHECK(!m.reorder("ghost", 0));  // not mapped -> false, no mutation
    CHECK(m.slotOf("a") == 0);
}

static void testSlotMapNegativeTargetClampsToZero() {
    SlotMap m;
    m.append("a"); m.append("b");  // 0,1
    CHECK(m.reorder("b", -3));      // clamp to 0 -> insert-before a
    CHECK(m.slotOf("b") == 0);
    CHECK(m.slotOf("a") == 1);
}

static void testSlotMapResetDenseSkipsDupesAndEmpties() {
    SlotMap m;
    m.resetDense({"a", "", "b", "a", "c"});  // "" and the second "a" dropped
    CHECK((m.orderedIds() == std::vector<std::string>{"a", "b", "c"}));
    CHECK(m.slotOf("a") == 0);
    CHECK(m.slotOf("c") == 2);
}

static void testSlotMapReconcileDropsStaleAppendsNew() {
    SlotMap m;
    m.append("a"); m.append("b"); m.append("c");  // 0,1,2
    m.reconcile({"a", "c", "d"});  // b left the index (drop), d is new (append)
    CHECK(m.slotOf("a") == 0);      // kept at its slot
    CHECK(m.slotOf("c") == 2);      // kept at its slot (gap where b was)
    CHECK(m.slotOf("b") == -1);     // stale marker dropped
    CHECK(m.slotOf("d") == 3);      // appended after the frontier
    CHECK(m.idAt(1).empty());       // b's slot stays empty
}

static void testSlotMapEqualityAndFromEntries() {
    SlotMap a;
    a.append("x"); a.append("y");
    SlotMap b = SlotMap::fromEntries({{"x", 0}, {"y", 1}});
    CHECK(a == b);
    // Defensive repair: duplicate id (first wins), slot conflict (later dropped),
    // empty id / negative slot dropped.
    SlotMap c = SlotMap::fromEntries({{"x", 0}, {"x", 5}, {"y", 0}, {"", 9}, {"z", -1}, {"w", 2}});
    CHECK(c.slotOf("x") == 0);   // first x wins
    CHECK(c.slotOf("y") == -1);  // slot 0 already taken -> dropped
    CHECK(c.slotOf("w") == 2);   // valid
    CHECK(c.slotOf("z") == -1);  // negative slot dropped
}

// --- serialize: golden byte-literal + round-trip -----------------------------

// Pins the exact wire shape (an array of {"id":..,"slot":..} objects, ascending
// slot, no whitespace) so a future format drift is caught here rather than only
// as a downstream bank_book diff. Mirrors the pre-extraction bank_book writer
// byte-for-byte (core/json emit helpers are shared, not reimplemented).
static void testSlotMapSerializeGoldenLiteral() {
    SlotMap empty;
    CHECK(empty.serialize() == "[]");

    SlotMap m;
    m.append("a");
    m.append("b");
    CHECK(m.serialize() == "[{\"id\":\"a\",\"slot\":0},{\"id\":\"b\",\"slot\":1}]");
}

// A local mirror of bank_book's private parseSlots (the "slots" array grammar):
// [{id, slot}, ...]. slot_map.cpp itself only emits — JSON parsing is a consumer
// concern (see slot_map.h) — so the round-trip proof below parses the emitted
// text back into pairs the same way bank_book does, then rebuilds via
// SlotMap::fromEntries and checks equality against the original.
static bool parseSlotsArray(json::Reader& r, std::vector<std::pair<std::string, int>>& out) {
    out.clear();
    if (!r.consume('[')) return false;
    r.skipWs();
    if (r.consume(']')) return true;  // empty array
    do {
        if (!r.consume('{')) return false;
        std::string id;
        int slot = 0;
        bool haveId = false, haveSlot = false;
        do {
            std::string k;
            if (!r.parseKey(k)) return false;
            if (k == "id") { if (!r.parseString(id)) return false; haveId = true; }
            else if (k == "slot") { if (!r.parseInt(slot)) return false; haveSlot = true; }
            else { if (!r.skipValue()) return false; }
        } while (r.consume(','));
        if (!r.consume('}')) return false;
        if (!haveId || !haveSlot) return false;
        out.emplace_back(std::move(id), slot);
    } while (r.consume(','));
    return r.consume(']');
}

static void testSlotMapSerializeRoundTrip() {
    SlotMap m;
    m.append("a"); m.append("b"); m.append("c");
    m.remove("b");        // leave a gap: a@0, c@2
    m.append("d");        // d@3

    const std::string blob = m.serialize();
    json::Reader r(blob);
    std::vector<std::pair<std::string, int>> pairs;
    CHECK(parseSlotsArray(r, pairs));

    SlotMap round = SlotMap::fromEntries(pairs);
    CHECK(round == m);
    CHECK(round.slotOf("a") == 0);
    CHECK(round.idAt(1).empty());  // gap survives the round trip
    CHECK(round.slotOf("c") == 2);
    CHECK(round.slotOf("d") == 3);
}

int main() {
    testSlotMapDenseAppend();
    testSlotMapRemoveLeavesGap();
    testSlotMapAppendAfterGapGoesToFrontier();
    testSlotMapReorderIntoEmpty();
    testSlotMapReorderOntoOccupiedInsertsAndShifts();
    testSlotMapReorderPreservesInteriorGapAboveTarget();
    testSlotMapReorderUnmappedIsNoOp();
    testSlotMapNegativeTargetClampsToZero();
    testSlotMapResetDenseSkipsDupesAndEmpties();
    testSlotMapReconcileDropsStaleAppendsNew();
    testSlotMapEqualityAndFromEntries();
    testSlotMapSerializeGoldenLiteral();
    testSlotMapSerializeRoundTrip();

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