// Standalone tests for reasampler::vst::waveform_view — no VST3, no REAPER, no framework.
// Same fast assert loop as the sibling pure tests. Assert the S11 waveform surface's
// frame<->pixel mapping, marker grab regions, drag-delta frame resolver (with clamps), and
// the zero-crossing snap — the geometry + snap that back the draggable start/loop markers.
//
// Covers: frameToX / xToFrame (linear map + inverse, edge clamps, degenerate frameCount/width);
// markerAtPoint (grab band, first-match on overlap, off-area + null-array rejection);
// resolveDragFrame (round-to-nearest-frame, clamp to [0,frameCount], zero-delta/zero-width
// no-ops); nearestZeroCrossing (nearest sign-change, sample-on-zero, equidistant-tie-to-lower,
// no-crossing keeps target, target clamp, degenerate buffers).

#include "../src/vst/waveform_view.h"

#include <cstdio>
#include <vector>

using namespace reasampler::vst;
using reasampler::AudioSample;

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

// A comfortable waveform area: 1000px wide, offset so left != 0 (catches origin bugs).
static Rect wideArea() { return Rect{20, 10, 1020, 90}; }  // width 1000

// --- frameToX / xToFrame ------------------------------------------------------

static void testFrameToXEndpoints() {
    const Rect a = wideArea();
    CHECK(frameToX(a, 1000, 0) == a.left);          // frame 0 -> left edge
    CHECK(frameToX(a, 1000, 1000) == a.right);       // frameCount -> right edge
    CHECK(frameToX(a, 1000, 500) == a.left + 500);   // midpoint (1:1 here)
}

static void testFrameToXClampsOutOfRange() {
    const Rect a = wideArea();
    CHECK(frameToX(a, 1000, -50) == a.left);         // below 0 pins left
    CHECK(frameToX(a, 1000, 5000) == a.right);       // above count pins right
}

static void testFrameToXDegenerate() {
    const Rect a = wideArea();
    CHECK(frameToX(a, 0, 100) == a.left);            // no frames -> left
    const Rect z = Rect{5, 5, 5, 45};                // zero width
    CHECK(frameToX(z, 1000, 500) == z.left);
}

static void testXToFrameInverse() {
    const Rect a = wideArea();
    CHECK(xToFrame(a, 1000, a.left) == 0);
    CHECK(xToFrame(a, 1000, a.right) == 1000);
    CHECK(xToFrame(a, 1000, a.left + 250) == 250);   // 1:1 map here
}

static void testXToFrameClampsOutside() {
    const Rect a = wideArea();
    CHECK(xToFrame(a, 1000, a.left - 100) == 0);     // left of area -> 0
    CHECK(xToFrame(a, 1000, a.right + 100) == 1000); // right of area -> frameCount
    CHECK(xToFrame(a, 0, a.left + 10) == 0);         // no frames -> 0
}

static void testFrameToXRoundTrip() {
    // Round-trip at a non-1:1 scale: 800px area over 2000 frames (2.5 frames/px). frameToX then
    // xToFrame should land within a couple frames (rounding both directions).
    const Rect a = Rect{0, 0, 800, 60};
    for (std::int64_t f = 0; f <= 2000; f += 137) {
        const int x = frameToX(a, 2000, f);
        const std::int64_t back = xToFrame(a, 2000, x);
        CHECK(back >= f - 3 && back <= f + 3);
    }
}

// --- markerAtPoint ------------------------------------------------------------

static void testMarkerAtPointGrabsWithinBand() {
    const Rect a = wideArea();
    // Markers at frames 100, 500, 900 -> x = left+100, left+500, left+900.
    const std::int64_t frames[3] = {100, 500, 900};
    const int midY = a.top + a.height() / 2;
    CHECK(markerAtPoint(a, 1000, frames, 3, a.left + 100, midY) == 0);
    CHECK(markerAtPoint(a, 1000, frames, 3, a.left + 500, midY) == 1);
    CHECK(markerAtPoint(a, 1000, frames, 3, a.left + 900, midY) == 2);
    // Within the grab band on either side of the line.
    CHECK(markerAtPoint(a, 1000, frames, 3, a.left + 500 + kMarkerGrabWidth, midY) == 1);
    CHECK(markerAtPoint(a, 1000, frames, 3, a.left + 500 - kMarkerGrabWidth, midY) == 1);
}

static void testMarkerAtPointMissesBetween() {
    const Rect a = wideArea();
    const std::int64_t frames[3] = {100, 500, 900};
    const int midY = a.top + a.height() / 2;
    // Well away from any marker line.
    CHECK(markerAtPoint(a, 1000, frames, 3, a.left + 300, midY) == -1);
    // Off the area vertically.
    CHECK(markerAtPoint(a, 1000, frames, 3, a.left + 500, a.top - 5) == -1);
}

static void testMarkerAtPointFirstMatchOnOverlap() {
    const Rect a = wideArea();
    // Two markers at the same frame -> first in order wins.
    const std::int64_t frames[2] = {400, 400};
    const int midY = a.top + a.height() / 2;
    CHECK(markerAtPoint(a, 1000, frames, 2, a.left + 400, midY) == 0);
}

static void testMarkerAtPointRejectsNullEmpty() {
    const Rect a = wideArea();
    const int midY = a.top + a.height() / 2;
    CHECK(markerAtPoint(a, 1000, nullptr, 3, a.left + 100, midY) == -1);
    const std::int64_t frames[1] = {100};
    CHECK(markerAtPoint(a, 1000, frames, 0, a.left + 100, midY) == -1);
}

// --- resolveDragFrame ---------------------------------------------------------

static void testResolveDragFrameShift() {
    const Rect a = wideArea();  // 1:1 (1000px / 1000 frames)
    CHECK(resolveDragFrame(a, 1000, 300, 0) == 300);      // zero delta -> unchanged
    CHECK(resolveDragFrame(a, 1000, 300, 100) == 400);    // +100px -> +100 frames
    CHECK(resolveDragFrame(a, 1000, 300, -50) == 250);    // -50px -> -50 frames
}

static void testResolveDragFrameClamps() {
    const Rect a = wideArea();
    CHECK(resolveDragFrame(a, 1000, 50, -500) == 0);      // clamp low
    CHECK(resolveDragFrame(a, 1000, 950, 500) == 1000);   // clamp high (== frameCount)
}

static void testResolveDragFrameRounds() {
    // 500px area over 1000 frames -> 2 frames/px. A +3px drag -> round(6.0)=6; the rounding is
    // at the frame centre. Use a scale where a fractional result appears.
    const Rect a = Rect{0, 0, 300, 60};  // 1000 frames / 300px = 3.33 frames/px
    // +3px -> 3*1000/300 = 10.0 -> 10 frames.
    CHECK(resolveDragFrame(a, 1000, 100, 3) == 110);
    // +1px -> 1000/300 = 3.33 -> rounds to 3.
    CHECK(resolveDragFrame(a, 1000, 100, 1) == 103);
}

static void testResolveDragFrameDegenerate() {
    const Rect z = Rect{0, 0, 0, 60};                     // zero width
    CHECK(resolveDragFrame(z, 1000, 300, 100) == 300);    // pinned to start
    const Rect a = wideArea();
    CHECK(resolveDragFrame(a, 0, 300, 100) == 0);         // no frames -> clamp(start)=0
    // startFrame out of range is clamped first.
    CHECK(resolveDragFrame(a, 1000, 5000, 0) == 1000);
}

// --- nearestZeroCrossing ------------------------------------------------------

static void testZeroCrossingNearest() {
    // Crossings (sign change from i-1 to i): i=4 (1->-1), i=5 (-1->1), i=10 (1->-1).
    std::vector<AudioSample> pcm = {1, 1, 1, 1, -1, 1, 1, 1, 1, 1, -1, -1};
    // Target 4 is itself a crossing -> 4.
    CHECK(nearestZeroCrossing(pcm.data(), (std::int64_t)pcm.size(), 4) == 4);
    // Nearest to 6: crossing 5 (dist 1) beats 4 (dist 2) and 10 (dist 4) -> 5.
    CHECK(nearestZeroCrossing(pcm.data(), (std::int64_t)pcm.size(), 6) == 5);
    // Nearest to 9: crossing 10 (dist 1) beats 5 (dist 4) -> 10.
    CHECK(nearestZeroCrossing(pcm.data(), (std::int64_t)pcm.size(), 9) == 10);
}

static void testZeroCrossingSampleOnZero() {
    // A sample exactly 0 is its own crossing (frame index of the zero sample).
    std::vector<AudioSample> pcm = {1, 1, 0, 1, 1};
    CHECK(nearestZeroCrossing(pcm.data(), (std::int64_t)pcm.size(), 2) == 2);
    CHECK(nearestZeroCrossing(pcm.data(), (std::int64_t)pcm.size(), 3) == 2);
}

static void testZeroCrossingEquidistantTieToLower() {
    // Crossings at i=2 (1->-1) and i=6 (-1->1). Target 4 is equidistant (dist 2) -> lower (2).
    std::vector<AudioSample> pcm = {1, 1, -1, -1, -1, -1, 1, 1};
    CHECK(nearestZeroCrossing(pcm.data(), (std::int64_t)pcm.size(), 4) == 2);
}

static void testZeroCrossingNoneKeepsTarget() {
    // All one sign -> no crossing -> the (clamped) target comes back unchanged.
    std::vector<AudioSample> pcm = {0.5f, 0.6f, 0.7f, 0.8f};
    CHECK(nearestZeroCrossing(pcm.data(), (std::int64_t)pcm.size(), 2) == 2);
}

static void testZeroCrossingClampsTarget() {
    std::vector<AudioSample> pcm = {1, -1, 1, -1};  // crossings at 1,2,3
    // Target beyond the end clamps to frames-1 (3) then finds crossing at 3.
    CHECK(nearestZeroCrossing(pcm.data(), (std::int64_t)pcm.size(), 999) == 3);
    // Negative target clamps to 0; nearest crossing is 1.
    CHECK(nearestZeroCrossing(pcm.data(), (std::int64_t)pcm.size(), -999) == 1);
}

static void testZeroCrossingDegenerate() {
    CHECK(nearestZeroCrossing(nullptr, 0, 5) == 0);
    std::vector<AudioSample> one = {1};
    CHECK(nearestZeroCrossing(one.data(), 1, 0) == 0);  // <2 frames -> clamped target
}

int main() {
    testFrameToXEndpoints();
    testFrameToXClampsOutOfRange();
    testFrameToXDegenerate();
    testXToFrameInverse();
    testXToFrameClampsOutside();
    testFrameToXRoundTrip();

    testMarkerAtPointGrabsWithinBand();
    testMarkerAtPointMissesBetween();
    testMarkerAtPointFirstMatchOnOverlap();
    testMarkerAtPointRejectsNullEmpty();

    testResolveDragFrameShift();
    testResolveDragFrameClamps();
    testResolveDragFrameRounds();
    testResolveDragFrameDegenerate();

    testZeroCrossingNearest();
    testZeroCrossingSampleOnZero();
    testZeroCrossingEquidistantTieToLower();
    testZeroCrossingNoneKeepsTarget();
    testZeroCrossingClampsTarget();
    testZeroCrossingDegenerate();

    if (g_fail == 0) std::printf("waveform_view: all tests passed\n");
    else std::printf("waveform_view: %d FAILED\n", g_fail);
    return g_fail == 0 ? 0 : 1;
}
