// Standalone tests for reasampler::instrument::ui::loop_marks — no VST3, no REAPER, no
// framework. Asserts the loop enable's whole state machine: the resolve's park rule and its two
// OFF states, the write's collapse fold and crossfade retention, and the four gestures that
// reach `hasLoop` composed end to end (resolve -> edit -> apply -> resolve), which is exactly
// how the editor drives it.

#include "../src/core/instrument/ui/loop_marks.h"
#include "../src/core/instrument/engine/loop/loop_span.h"  // defaultLoopBounds (the park target)

#include <cstdio>

using namespace reasampler::instrument::ui;
using reasampler::SampleLoop;
using reasampler::instrument::engine::loop::LoopBounds;
using reasampler::instrument::engine::loop::defaultLoopBounds;

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

static constexpr std::int64_t kFrames = 10000;

// The editor's own round trip: what the band shows after a marker set is written back.
static LoopMarks writeThenRead(const LoopMarks& edited, std::int64_t frames = kFrames) {
    const LoopWrite w = applyLoopMarks(edited);
    StoredLoop s;
    s.override_ = w.loop;
    s.crossfade = w.crossfade;
    s.startPoint = w.start;
    return resolveLoopMarks(s, frames);
}

static StoredLoop storedSpan(bool on, std::int64_t start, std::int64_t end,
                             std::int64_t crossfade) {
    StoredLoop s;
    s.override_ = SampleLoop{on, start, end};
    s.crossfade = crossfade;
    return s;
}

// --- resolve: the two OFF states -----------------------------------------------

static void testNothingSetParksOnTheDefaultBoundsAndReadsAsNeverSet() {
    const LoopMarks m = resolveLoopMarks(StoredLoop{}, kFrames);
    const LoopBounds d = defaultLoopBounds(kFrames);
    CHECK(!m.hasLoop);
    CHECK(m.parked);  // the "DRAG TO SET LOOP" state
    CHECK(m.loopStart == d.start && m.loopEnd == d.end);
}

static void testAValidSpanSwitchedOffKeepsItsOwnPositions() {
    const LoopMarks m = resolveLoopMarks(storedSpan(false, 4000, 6000, 300), kFrames);
    CHECK(!m.hasLoop);
    CHECK(!m.parked);  // the "LOOP OFF" state — there is nothing to "set"
    CHECK(m.loopStart == 4000 && m.loopEnd == 6000);
    CHECK(m.crossfade == 300);
    // And it is NOT the park position, which is what the off->on restore depends on.
    const LoopBounds d = defaultLoopBounds(kFrames);
    CHECK(!(m.loopStart == d.start && m.loopEnd == d.end));
}

static void testAnUnusableSpanParksWhateverTheEnableSays() {
    const LoopBounds d = defaultLoopBounds(kFrames);
    // Collapsed, inverted, past the PCM, and negative — the four the engine refuses.
    const StoredLoop bad[4] = {storedSpan(true, 500, 500, 0), storedSpan(true, 900, 400, 0),
                               storedSpan(true, 500, kFrames + 1, 0),
                               storedSpan(true, -5, 400, 0)};
    for (const StoredLoop& s : bad) {
        const LoopMarks m = resolveLoopMarks(s, kFrames);
        CHECK(!m.hasLoop);
        CHECK(m.parked);
        CHECK(m.loopStart == d.start && m.loopEnd == d.end);
    }
}

static void testTheOverrideSupersedesTheBankIntrinsic() {
    StoredLoop s;
    s.intrinsic = SampleLoop{true, 100, 200};
    s.override_ = SampleLoop{true, 4000, 6000};
    const LoopMarks m = resolveLoopMarks(s, kFrames);
    CHECK(m.hasLoop && m.loopStart == 4000 && m.loopEnd == 6000);
    // With no override the intrinsic is what the band shows.
    s.override_.reset();
    const LoopMarks i = resolveLoopMarks(s, kFrames);
    CHECK(i.hasLoop && i.loopStart == 100 && i.loopEnd == 200);
    // An intrinsic that itself says "no loop" is not a span to adopt.
    s.intrinsic = SampleLoop{false, 100, 200};
    CHECK(resolveLoopMarks(s, kFrames).parked);
}

// --- the four gestures onto hasLoop --------------------------------------------

// Gesture 1: the enable clicked ON. Span and crossfade retained as-is.
static void testEnableOnKeepsTheSpanAndCrossfade() {
    LoopMarks m = resolveLoopMarks(storedSpan(false, 4000, 6000, 300), kFrames);
    m.hasLoop = true;
    const LoopMarks after = writeThenRead(m);
    CHECK(after.hasLoop);
    CHECK(after.loopStart == 4000 && after.loopEnd == 6000);
    CHECK(after.crossfade == 300);
}

// Gesture 2: the enable clicked OFF. Span and crossfade retained — the whole point of it being
// a toggle rather than a delete button.
static void testEnableOffRetainsTheSpanAndCrossfade() {
    LoopMarks m = resolveLoopMarks(storedSpan(true, 4000, 6000, 300), kFrames);
    m.hasLoop = false;
    const LoopMarks after = writeThenRead(m);
    CHECK(!after.hasLoop);
    CHECK(!after.parked);
    CHECK(after.loopStart == 4000 && after.loopEnd == 6000);
    CHECK(after.crossfade == 300);
}

// The acceptance criterion in one assertion: off then on restores the loop EXACTLY.
static void testOffThenOnRestoresTheLoopExactly() {
    const LoopMarks before = resolveLoopMarks(storedSpan(true, 4000, 6000, 300), kFrames);
    LoopMarks off = before;
    off.hasLoop = false;
    LoopMarks mid = writeThenRead(off);
    mid.hasLoop = true;
    const LoopMarks back = writeThenRead(mid);
    CHECK(back.hasLoop == before.hasLoop);
    CHECK(back.loopStart == before.loopStart && back.loopEnd == before.loopEnd);
    CHECK(back.crossfade == before.crossfade);
    CHECK(back.parked == before.parked);
}

// Gesture 3: the span collapsed onto itself. OFF, span destroyed and re-parked, crossfade zeroed.
static void testCollapsingTheSpanTurnsItOffReparksAndZeroesTheCrossfade() {
    LoopMarks m = resolveLoopMarks(storedSpan(true, 4000, 6000, 300), kFrames);
    m.loopEnd = m.loopStart;  // the drag that lands one mark on the other
    const LoopWrite w = applyLoopMarks(m);
    CHECK(!w.loop.hasLoop);
    CHECK(w.crossfade == 0);
    const LoopMarks after = writeThenRead(m);
    const LoopBounds d = defaultLoopBounds(kFrames);
    CHECK(!after.hasLoop && after.parked);
    CHECK(after.loopStart == d.start && after.loopEnd == d.end);
    CHECK(after.crossfade == 0);
}

// Gesture 4: dragging a loop mark while OFF turns it on, in BOTH off-states.
static void testDraggingALoopMarkWhileOffTurnsItOn() {
    // Never set: the pair is parked, and the drag takes it off the park.
    LoopMarks parked = resolveLoopMarks(StoredLoop{}, kFrames);
    CHECK(parked.parked && !parked.hasLoop);
    parked.loopStart = 3000;  // the drag
    parked.hasLoop = true;
    const LoopMarks fromParked = writeThenRead(parked);
    CHECK(fromParked.hasLoop && !fromParked.parked);
    CHECK(fromParked.loopStart == 3000);

    // Span retained: the drag turns it on at the dragged positions, crossfade retained.
    LoopMarks retained = resolveLoopMarks(storedSpan(false, 4000, 6000, 300), kFrames);
    CHECK(!retained.parked && !retained.hasLoop);
    retained.loopEnd = 7000;
    retained.hasLoop = true;
    const LoopMarks fromRetained = writeThenRead(retained);
    CHECK(fromRetained.hasLoop);
    CHECK(fromRetained.loopStart == 4000 && fromRetained.loopEnd == 7000);
    CHECK(fromRetained.crossfade == 300);
}

// --- the write's own rules ------------------------------------------------------

// Dragging the START marker with the enable off must not turn the loop on, and must not
// destroy the crossfade travelling with the retained span.
static void testEditingTheStartMarkerWhileOffLeavesTheEnableAndCrossfadeAlone() {
    LoopMarks m = resolveLoopMarks(storedSpan(false, 4000, 6000, 300), kFrames);
    m.start = 512;
    const LoopWrite w = applyLoopMarks(m);
    CHECK(!w.loop.hasLoop);
    CHECK(w.loop.start == 4000 && w.loop.end == 6000);
    CHECK(w.crossfade == 300);
    CHECK(w.start == 512);
}

// Dragging the START marker while parked (never set) must not turn "DRAG TO SET LOOP" into
// "LOOP OFF" — the ordinary gesture that exposed the bug, distinct from the retained-span case
// above.
static void testEditingTheStartMarkerWhileParkedStaysParked() {
    LoopMarks m = resolveLoopMarks(StoredLoop{}, kFrames);
    CHECK(m.parked && !m.hasLoop);
    m.start = 512;
    const LoopMarks after = writeThenRead(m);
    CHECK(!after.hasLoop);
    CHECK(after.parked);
    const LoopBounds d = defaultLoopBounds(kFrames);
    CHECK(after.loopStart == d.start && after.loopEnd == d.end);
    CHECK(after.start == 512);
}

static void testANegativeCrossfadeNeverReachesTheStore() {
    LoopMarks m = resolveLoopMarks(storedSpan(true, 4000, 6000, 0), kFrames);
    m.crossfade = -1;
    CHECK(applyLoopMarks(m).crossfade == 0);
    StoredLoop s = storedSpan(true, 4000, 6000, -1);
    CHECK(resolveLoopMarks(s, kFrames).crossfade == 0);
}

int main() {
    testNothingSetParksOnTheDefaultBoundsAndReadsAsNeverSet();
    testAValidSpanSwitchedOffKeepsItsOwnPositions();
    testAnUnusableSpanParksWhateverTheEnableSays();
    testTheOverrideSupersedesTheBankIntrinsic();

    testEnableOnKeepsTheSpanAndCrossfade();
    testEnableOffRetainsTheSpanAndCrossfade();
    testOffThenOnRestoresTheLoopExactly();
    testCollapsingTheSpanTurnsItOffReparksAndZeroesTheCrossfade();
    testDraggingALoopMarkWhileOffTurnsItOn();

    testEditingTheStartMarkerWhileOffLeavesTheEnableAndCrossfadeAlone();
    testEditingTheStartMarkerWhileParkedStaysParked();
    testANegativeCrossfadeNeverReachesTheStore();

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