// Standalone tests for reasampler::instrument::engine::StretchCursor — the Preserve read's
// source-feed schedule. No VST3, no REAPER, no vendor, no test framework.
//
// Covers:
//   1. rate 1.0 is EXACTLY one source frame per output frame, forever and with no residue —
//      the mechanism behind the "unity is bit-identical to the shipped Preserve read" gate.
//   2. the schedule tracks the rate: over N output frames the cursor consumes N*rate source
//      frames to within one, at rates either side of unity and at irrational ones.
//   3. the per-output-frame feed count never exceeds kMaxFeedPerFrame — the bound that makes
//      a variable-length feed loop RT-safe.
//   4. the clamp: out-of-range folds to the bounds, unusable input folds to unity (never to a
//      silent stall or a quarter-speed surprise).
//   5. the sustain loop wraps the cursor and never lets it leave [start, end) — "loop the
//      source" holds at every rate, including one that steps over the loop end.

#include "../src/core/instrument/engine/time_stretch.h"

#include <cmath>
#include <cstdio>
#include <vector>

using namespace reasampler::instrument::engine;
using reasampler::instrument::engine::loop::ResolvedLoop;

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 ResolvedLoop noLoop() { return ResolvedLoop{}; }

static ResolvedLoop loopSpan(std::int64_t start, std::int64_t end) {
    ResolvedLoop lp;
    lp.active = true;
    lp.start = start;
    lp.end = end;
    lp.length = end - start;
    return lp;
}

// --- 1. Unity is exactly one frame per output frame, with no drifting residue. ---
static void testUnityRateFeedsExactlyOneFramePerOutputFrame() {
    StretchCursor c;
    c.start(100);
    const ResolvedLoop lp = noLoop();
    for (std::int64_t i = 0; i < 200000; ++i) {
        CHECK(c.due(1.0) == 1);
        CHECK(c.next(lp) == 100 + i);
    }
    // No accumulated debt after 200k frames: the source frame the cursor is about to feed is
    // exactly the one an un-stretched integer walk would be at. A residue of even one frame
    // over a long note would move the shipped Preserve output.
    CHECK(c.frame() == 100 + 200000);
}

// --- 2. The schedule tracks the rate. ---
static void testTotalConsumedTracksTheRate() {
    const ResolvedLoop lp = noLoop();
    // Includes a rate with no exact binary representation, where a naive per-frame rounding
    // would drift without bound rather than carrying the residue.
    for (double rate : {0.5, 0.75, 1.0, 1.3333333333333333, 2.0, 1.0 / 3.0 + 1.0}) {
        StretchCursor c;
        c.start(0);
        const std::int64_t outFrames = 100000;
        for (std::int64_t i = 0; i < outFrames; ++i) {
            const std::int64_t due = c.due(rate);
            for (std::int64_t k = 0; k < due; ++k) (void)c.next(lp);
        }
        const double expected = static_cast<double>(outFrames) * clampStretchRate(rate);
        CHECK(std::fabs(static_cast<double>(c.frame()) - expected) <= 1.0);
    }
}

// --- 3. The feed count is bounded — the RT-safety argument for a variable-length loop. ---
static void testFeedPerOutputFrameIsBounded() {
    const ResolvedLoop lp = noLoop();
    // Drive at, above and around the ceiling; an unclamped rate would run the caller's loop
    // for as many iterations as the rate names.
    for (double rate : {kStretchRateMax, kStretchRateMax * 100.0, 3.99, 2.5}) {
        StretchCursor c;
        c.start(0);
        std::int64_t worst = 0;
        for (std::int64_t i = 0; i < 20000; ++i) {
            const std::int64_t due = c.due(rate);
            if (due > worst) worst = due;
            for (std::int64_t k = 0; k < due; ++k) (void)c.next(lp);
        }
        CHECK(worst <= kMaxFeedPerFrame);
        CHECK(worst >= 1);  // and the bound is not vacuous — frames genuinely fell due
    }
}

// --- 4. The clamp. ---
static void testRateClamp() {
    CHECK(clampStretchRate(1.0) == 1.0);            // exact: the unity read depends on it
    CHECK(clampStretchRate(0.5) == 0.5);
    CHECK(clampStretchRate(2.0) == 2.0);
    CHECK(clampStretchRate(0.001) == kStretchRateMin);
    CHECK(clampStretchRate(1000.0) == kStretchRateMax);
    // Unusable input plays at speed rather than stalling or quarter-speeding.
    CHECK(clampStretchRate(0.0) == 1.0);
    CHECK(clampStretchRate(-2.0) == 1.0);
    CHECK(clampStretchRate(std::nan("")) == 1.0);
    // ...and the cursor honours it rather than looping on the raw value.
    StretchCursor c;
    c.start(0);
    CHECK(c.due(-5.0) == 1);   // folded to unity
    StretchCursor d;
    d.start(0);
    CHECK(d.due(50.0) <= kMaxFeedPerFrame);
}

// --- 5. The loop wraps the SOURCE cursor, at every rate. ---
static void testCursorStaysInsideTheLoopSpan() {
    const ResolvedLoop lp = loopSpan(1000, 1040);  // a 40-frame loop: rate 4 steps 10% of it
    for (double rate : {0.5, 1.0, 2.0, 4.0}) {
        StretchCursor c;
        c.start(1000);
        std::int64_t lowest = 1 << 30, highest = -1;
        for (std::int64_t i = 0; i < 50000; ++i) {
            const std::int64_t due = c.due(rate);
            for (std::int64_t k = 0; k < due; ++k) {
                const std::int64_t q = c.next(lp);
                if (q < lowest) lowest = q;
                if (q > highest) highest = q;
            }
        }
        // Never reads outside the span — the "loop the source, shift the output" contract does
        // not weaken under a stretch, because the span is a source-frame fact.
        CHECK(lowest >= lp.start);
        CHECK(highest < lp.end);
        CHECK(highest == lp.end - 1);   // and it genuinely covered the span
        CHECK(lowest == lp.start);
    }
    // A cursor started BEYOND the loop end (the start-point-past-the-loop case) is pulled in on
    // its first take rather than reading off the end.
    StretchCursor c;
    c.start(5000);
    const std::int64_t q = c.next(lp);
    CHECK(q >= lp.start && q < lp.end);
}

int main() {
    testUnityRateFeedsExactlyOneFramePerOutputFrame();
    testTotalConsumedTracksTheRate();
    testFeedPerOutputFrameIsBounded();
    testRateClamp();
    testCursorStaysInsideTheLoopSpan();

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