// Standalone tests for the deferred FX-park queue's re-entrancy rule and the
// snapshot-lifecycle contract that rides on it — no REAPER, no test framework.
//
// The properties under test: a mode switch leaves its per-FX offline work here,
// so a second switch arriving before the first drained must leave every track in
// the state the SECOND switch specifies — never the first's, never both replayed;
// and a cancel must not strand the pre-park FX state it was the last record of.

#include "../src/shell/view/view_fx_park.h"

#include "core/view/view_mode_model.h"  // makeRestorePlan — the ops' only producer

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

using namespace reasampler;

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

// -- helpers -----------------------------------------------------------------

// The ops applyMode hands a restore: one per FX captured in the track's snapshot.
static std::vector<FxOfflineOp> ops(const std::string& fxGuid, bool offline) {
    return {FxOfflineOp{"{TRACK}", FxKeying::Identity, fxGuid, 0, offline}};
}

static const FxParkIntent* intentFor(const FxParkQueue& q, const std::string& guid) {
    for (const FxParkIntent& i : q.pending())
        if (i.guid == guid) return &i;
    return nullptr;
}

// -- tests -------------------------------------------------------------------

static void testParkEnqueuesOneIntentCarryingNoOps() {
    FxParkQueue q;
    q.park("{A}");

    CHECK(q.pending().size() == 1);
    const FxParkIntent* held = intentFor(q, "{A}");
    CHECK(held != nullptr);
    CHECK(held && held->park);
    CHECK(held && held->restoreOps.empty());
}

static void testParkReportsNothingCancelledWhenNoIntentWasPending() {
    FxParkQueue q;
    CHECK(q.park("{A}").empty());
}

static void testParkOnItsOwnPendingParkReportsNothingCancelled() {
    FxParkQueue q;
    q.park("{A}");
    CHECK(q.park("{A}").empty());
}

static void testRestoreOnAnUndrainedParkCancelsRatherThanStacks() {
    // The park never ran, so the track's FX still hold their captured state —
    // exactly what the restore would write. Replaying both would unload every
    // plugin only to reload it.
    FxParkQueue q;
    q.park("{A}");
    q.restore("{A}", ops("{FX}", false));

    CHECK(q.empty());
}

static void testParkOnAnUndrainedRestoreCancelsRatherThanStacks() {
    // The mirror case: the restore never ran, so the FX are still parked offline,
    // which is where the new park wants them.
    FxParkQueue q;
    q.restore("{A}", ops("{FX}", false));
    q.park("{A}");

    CHECK(q.empty());
}

static void testRepeatedParkStaysOneIntent() {
    FxParkQueue q;
    q.park("{A}");
    q.park("{A}");
    q.park("{A}");

    CHECK(q.pending().size() == 1);
    CHECK(intentFor(q, "{A}") && intentFor(q, "{A}")->park);
}

static void testLaterRestoreReplacesTheEarlierOnesOps() {
    FxParkQueue q;
    q.restore("{A}", ops("{OLD}", false));
    q.restore("{A}", ops("{NEW}", true));

    CHECK(q.pending().size() == 1);
    const FxParkIntent* held = intentFor(q, "{A}");
    CHECK(held && !held->park);
    CHECK(held && held->restoreOps.size() == 1);
    CHECK(held && held->restoreOps.front().fxGuid == "{NEW}");
    CHECK(held && held->restoreOps.front().offline);
}

static void testOneTracksCancelLeavesEveryOtherTrackAlone() {
    FxParkQueue q;
    q.park("{A}");
    q.park("{B}");
    q.park("{C}");
    q.restore("{B}", ops("{FX}", false));  // cancels B only

    CHECK(q.pending().size() == 2);
    CHECK(intentFor(q, "{A}") != nullptr);
    CHECK(intentFor(q, "{B}") == nullptr);
    CHECK(intentFor(q, "{C}") != nullptr);
    // Order survives the middle erase: the drain applies in enqueue order.
    CHECK(q.pending()[0].guid == "{A}");
    CHECK(q.pending()[1].guid == "{C}");
}

static void testCancelledTrackCanBeQueuedAgain() {
    // Two rapid switches then a third: the third is the one that must land.
    FxParkQueue q;
    q.park("{A}");
    q.restore("{A}", ops("{FX}", false));
    q.park("{A}");

    CHECK(q.pending().size() == 1);
    CHECK(intentFor(q, "{A}") && intentFor(q, "{A}")->park);
}

static void testClearDropsEverythingPending() {
    FxParkQueue q;
    q.park("{A}");
    q.restore("{B}", ops("{FX}", true));
    q.clear();

    CHECK(q.empty());
    CHECK(q.pending().empty());
}

// -- snapshot lifecycle ------------------------------------------------------

static void testParkHandsBackTheOpsOfTheRestoreItCancelled() {
    // The cancelled restore is the LAST record of the pre-park FX state: the
    // track's chain still reads the parked values (the restore never ran), and
    // the cancel means no drain will ever put them back. A park that drops these
    // snapshots the park's own offline zeros as if they were the user's state.
    FxParkQueue q;
    q.restore("{A}", ops("{FX}", false));

    const std::vector<FxOfflineOp> cancelled = q.park("{A}");

    CHECK(cancelled.size() == 1);
    CHECK(cancelled.size() == 1 && cancelled.front().fxGuid == "{FX}");
    CHECK(cancelled.size() == 1 && !cancelled.front().offline);
    CHECK(q.empty());  // annihilated: the chain already holds what the park wants
}

static void testCancelledRestoreOpsBecomeTheFreshSnapshotsFxHalf() {
    std::vector<FxOfflineOp> cancelled = ops("{ONE}", true);
    cancelled.push_back(FxOfflineOp{"{TRACK}", FxKeying::Identity, "{TWO}", 1, false});

    const PreParkFx fx = preParkFxFromCancelledRestore(cancelled);

    CHECK(fx.keying == FxKeying::Identity);
    CHECK(fx.states.size() == 2);
    CHECK(fx.states.size() == 2 && fx.states[0].fxGuid == "{ONE}" && fx.states[0].offline == 1);
    CHECK(fx.states.size() == 2 && fx.states[1].fxGuid == "{TWO}" && fx.states[1].offline == 0);
}

static void testNothingCancelledLeavesTheFxHalfToTheCaller() {
    const PreParkFx fx = preParkFxFromCancelledRestore({});

    CHECK(fx.states.empty());  // caller reads the live chain instead
    CHECK(fx.keying == FxKeying::Identity);
}

static void testSlotKeyedRestoreDoesNotBecomeIdentityKeyedWithNoIdentities() {
    // A snapshot lifted from a pre-identity view_state is slot-keyed and carries
    // no fxGuid. Re-labelling it Identity would make resolveFxRestore drop every
    // entry as unidentified instead of writing it by slot.
    std::vector<FxOfflineOp> cancelled = {
        FxOfflineOp{"{TRACK}", FxKeying::Slot, "", 0, true},
        FxOfflineOp{"{TRACK}", FxKeying::Slot, "", 1, false},
    };

    const PreParkFx fx = preParkFxFromCancelledRestore(cancelled);

    CHECK(fx.keying == FxKeying::Slot);
    CHECK(fx.states.size() == 2);
    CHECK(fx.states.size() == 2 && fx.states[0].offline == 1 && fx.states[1].offline == 0);
}

// -- the makeRestorePlan <-> preParkFxFromCancelledRestore round trip ---------
//
// The cancel path's whole premise is that a planned restore's ops are a LOSSLESS
// carrier of the snapshot's FX half. makeRestorePlan is their only producer, so
// the real claim is that the pair composes to the identity on (fxOffline,
// fxKeying). Asserting it against hand-built ops would let a change to
// makeRestorePlan's field mapping or op ordering pass with every test green.

static void testRestorePlanOpsRebuildTheIdentityKeyedSnapshotVerbatim() {
    TrackSnapshot snap;
    snap.fxKeying  = FxKeying::Identity;
    snap.fxOffline = {FxOfflineState{"{ONE}", 1}, FxOfflineState{"{TWO}", 0},
                      FxOfflineState{"{THREE}", 1}};

    const TrackPlan plan = makeRestorePlan("{TRACK}", snap);
    const PreParkFx rebuilt = preParkFxFromCancelledRestore(plan.fxOffline);

    CHECK(rebuilt.keying == FxKeying::Identity);
    // Three DISTINCT entries, compared as a sequence: a dropped fxGuid, a flipped
    // offline, or a reordering each fail here.
    CHECK(rebuilt.states == snap.fxOffline);
}

static void testRestorePlanOpsRebuildTheSlotKeyedSnapshotVerbatim() {
    TrackSnapshot snap;
    snap.fxKeying  = FxKeying::Slot;
    snap.fxOffline = {FxOfflineState{"", 0}, FxOfflineState{"", 1}, FxOfflineState{"", 1}};

    const TrackPlan plan = makeRestorePlan("{TRACK}", snap);

    // Slot keying addresses by POSITION, so the identity holds only while the op
    // at index i carries slot i.
    CHECK(plan.fxOffline.size() == 3);
    CHECK(plan.fxOffline.size() == 3 && plan.fxOffline[0].slot == 0 &&
          plan.fxOffline[1].slot == 1 && plan.fxOffline[2].slot == 2);

    const PreParkFx rebuilt = preParkFxFromCancelledRestore(plan.fxOffline);

    CHECK(rebuilt.keying == FxKeying::Slot);
    CHECK(rebuilt.states == snap.fxOffline);
}

// -- re-entrancy -------------------------------------------------------------

static void testTakeDetachesEverythingAndLeavesTheQueueEmpty() {
    FxParkQueue q;
    q.park("{A}");
    q.restore("{B}", ops("{FX}", true));

    const std::vector<FxParkIntent> taken = q.take();

    CHECK(taken.size() == 2);
    CHECK(taken.size() == 2 && taken[0].guid == "{A}" && taken[0].park);
    CHECK(taken.size() == 2 && taken[1].guid == "{B}" && !taken[1].park);
    CHECK(q.empty());
}

static void testIntentsArrivingDuringADrainSurviveIt() {
    // [verify — DAW] applying an intent loads/unloads plugins, which is ASSUMED to
    // pump the message loop, so a switch can re-enter and enqueue mid-drain. Those
    // intents belong to the NEXT drain — the one in progress must neither see them
    // nor discard them.
    FxParkQueue q;
    q.park("{A}");

    const std::vector<FxParkIntent> draining = q.take();
    q.restore("{B}", ops("{FX}", false));  // arrives while {A} is being applied

    CHECK(draining.size() == 1);
    CHECK(draining.size() == 1 && draining.front().guid == "{A}");
    CHECK(q.pending().size() == 1);
    CHECK(intentFor(q, "{B}") != nullptr);
}

static void testAReEntrantParkCancelsOnlyWhatIsStillPending() {
    // {A}'s restore was already taken for the in-flight drain, so a park arriving
    // mid-drain has nothing to cancel — it must queue as a fresh park rather than
    // silently annihilate against an intent that has already been applied.
    FxParkQueue q;
    q.restore("{A}", ops("{FX}", false));
    q.take();

    const std::vector<FxOfflineOp> cancelled = q.park("{A}");

    CHECK(cancelled.empty());
    CHECK(q.pending().size() == 1);
    CHECK(intentFor(q, "{A}") && intentFor(q, "{A}")->park);
}

int main() {
    testParkEnqueuesOneIntentCarryingNoOps();
    testParkReportsNothingCancelledWhenNoIntentWasPending();
    testParkOnItsOwnPendingParkReportsNothingCancelled();
    testRestoreOnAnUndrainedParkCancelsRatherThanStacks();
    testParkOnAnUndrainedRestoreCancelsRatherThanStacks();
    testRepeatedParkStaysOneIntent();
    testLaterRestoreReplacesTheEarlierOnesOps();
    testOneTracksCancelLeavesEveryOtherTrackAlone();
    testCancelledTrackCanBeQueuedAgain();
    testClearDropsEverythingPending();
    testParkHandsBackTheOpsOfTheRestoreItCancelled();
    testCancelledRestoreOpsBecomeTheFreshSnapshotsFxHalf();
    testNothingCancelledLeavesTheFxHalfToTheCaller();
    testSlotKeyedRestoreDoesNotBecomeIdentityKeyedWithNoIdentities();
    testRestorePlanOpsRebuildTheIdentityKeyedSnapshotVerbatim();
    testRestorePlanOpsRebuildTheSlotKeyedSnapshotVerbatim();
    testTakeDetachesEverythingAndLeavesTheQueueEmpty();
    testIntentsArrivingDuringADrainSurviveIt();
    testAReEntrantParkCancelsOnlyWhatIsStillPending();

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