// Standalone tests for reasampler::instrument::ui::waveform_view — no VST3, no REAPER, no framework.
// Same fast assert loop as the sibling pure tests. Assert the waveform band's drawn surface
// (lane split + the full-height overlay contract) and its frame<->pixel mapping, marker grab
// regions, drag-delta frame resolver (with clamps), and zero-crossing snap.
//
// Covers: frameToX / xToFrame (linear map + inverse, edge clamps, degenerate frameCount/width);
// markerAtPoint (grab band, first-match on overlap, off-area + null-array rejection);
// markerHandleRect (the top-strip tab that keeps coincident markers independently grabbable);
// 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); the four marks (per-mark cap
// resolve, the reverse cap order that keeps a coincident pair separable, label sides/nudging,
// the suppression rule and its promoted-first placement, the crossfade wedge ramp);
// waveformSurface (two stacked
// lanes L-over-R in stereo, one lane in mono AND for a mono source, overlay always the full
// stacked height, grabs reaching the lower lane); laneEnvelope (per-lane channel split).

#include "../src/core/instrument/ui/waveform_view.h"
#include "../src/core/instrument/ui/sample_bands.h"  // kWaveformMinHeight, kLaneGap

#include <cstddef>
#include <cstdio>
#include <vector>

using namespace reasampler;
using namespace reasampler::instrument::ui;
using reasampler::audio::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)

static OverlayArea overlayOf(const Rect& r) { return OverlayArea{r}; }

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

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

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

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

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

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

static void testXToFrameClampsOutside() {
    const Rect a = wideArea();
    CHECK(xToFrame(overlayOf(a), 1000, a.x - 100) == 0);     // left of area -> 0
    CHECK(xToFrame(overlayOf(a), 1000, a.right() + 100) == 1000); // right of area -> frameCount
    CHECK(xToFrame(overlayOf(a), 0, a.x + 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::ltrb(0, 0, 800, 60);
    for (std::int64_t f = 0; f <= 2000; f += 137) {
        const int x = frameToX(overlayOf(a), 2000, f);
        const std::int64_t back = xToFrame(overlayOf(a), 2000, x);
        CHECK(back >= f - 3 && back <= f + 3);
    }
}

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

static void testMarkerAtPointGrabsWithinBand() {
    const Rect a = wideArea();
    const OverlayArea ov = overlayOf(a);
    // 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.y + a.height / 2;
    CHECK(markerAtPoint(ov, 1000, frames, 3, a.x + 100, midY) == 0);
    CHECK(markerAtPoint(ov, 1000, frames, 3, a.x + 500, midY) == 1);
    CHECK(markerAtPoint(ov, 1000, frames, 3, a.x + 900, midY) == 2);
    // Within the grab band on either side of the line.
    CHECK(markerAtPoint(ov, 1000, frames, 3, a.x + 500 + kMarkerGrabWidth, midY) == 1);
    CHECK(markerAtPoint(ov, 1000, frames, 3, a.x + 500 - kMarkerGrabWidth, midY) == 1);
}

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

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

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

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

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

static void testResolveDragFrameClamps() {
    const Rect a = wideArea();
    const OverlayArea ov = overlayOf(a);
    CHECK(resolveDragFrame(ov, 1000, 50, -500) == 0);      // clamp low
    CHECK(resolveDragFrame(ov, 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 OverlayArea ov = overlayOf(Rect::ltrb(0, 0, 300, 60));  // 1000 frames / 300px = 3.33 frames/px
    // +3px -> 3*1000/300 = 10.0 -> 10 frames.
    CHECK(resolveDragFrame(ov, 1000, 100, 3) == 110);
    // +1px -> 1000/300 = 3.33 -> rounds to 3.
    CHECK(resolveDragFrame(ov, 1000, 100, 1) == 103);
}

static void testResolveDragFrameDegenerate() {
    const Rect z = Rect::ltrb(0, 0, 0, 60);                     // zero width
    CHECK(resolveDragFrame(overlayOf(z), 1000, 300, 100) == 300);    // pinned to start
    const Rect a = wideArea();
    const OverlayArea ov = overlayOf(a);
    CHECK(resolveDragFrame(ov, 0, 300, 100) == 0);         // no frames -> clamp(start)=0
    // startFrame out of range is clamped first.
    CHECK(resolveDragFrame(ov, 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
}

// --- waveformSurface: the lane split + the overlay contract --------------------

// A realistic waveform band: full-width, taller than the two-lane floor.
static Rect band() { return Rect::ltrb(8, 90, 832, 90 + kWaveformMinHeight); }

static void testSurfaceStereoStacksTwoLanes() {
    const Rect b = band();
    const WaveformSurface s = waveformSurface(b, /*stereoMode=*/true, /*sourceChannels=*/2);
    CHECK(s.laneCount == 2);
    CHECK(!s.upper.empty() && !s.lower.empty());
    CHECK(s.upper.y == b.y);                        // L on top
    CHECK(s.lower.y > s.upper.bottom());            // R below, seam between them
    CHECK(s.lower.bottom() == b.bottom());          // together they reach the band's floor
    CHECK(s.upper.x == b.x && s.upper.width == b.width);
    CHECK(s.lower.x == b.x && s.lower.width == b.width);
    // Non-overlapping, and the band is exactly lanes + the one seam gap.
    CHECK(s.lower.y - s.upper.bottom() == kLaneGap);
    CHECK(s.upper.height + kLaneGap + s.lower.height == b.height);
}

static void testSurfaceMonoIsOneLane() {
    const Rect b = band();
    const WaveformSurface s = waveformSurface(b, /*stereoMode=*/false, /*sourceChannels=*/2);
    CHECK(s.laneCount == 1);
    CHECK(s.upper == b);          // the single lane spans the whole band
    CHECK(s.lower.empty());       // no second lane to draw
}

static void testSurfaceMonoSourceInStereoModeStaysOneLane() {
    // Dual-mono: a mono source under stereo mode has no second channel, so a second lane
    // would be a redundant duplicate.
    const Rect b = band();
    const WaveformSurface s = waveformSurface(b, /*stereoMode=*/true, /*sourceChannels=*/1);
    CHECK(s.laneCount == 1);
    CHECK(s.upper == b);
    CHECK(s.lower.empty());
}

static void testSurfaceOverlayIsFullStackedHeightInBothModes() {
    const Rect b = band();
    const WaveformSurface st = waveformSurface(b, /*stereoMode=*/true, 2);
    const WaveformSurface mo = waveformSurface(b, /*stereoMode=*/false, 2);
    // Stereo: ONE overlay rect spanning both lanes, not either lane.
    CHECK(st.overlay.rect == b);
    CHECK(st.overlay.rect.height == st.upper.height + kLaneGap + st.lower.height);
    CHECK(st.overlay.rect != st.upper && st.overlay.rect != st.lower);
    // Mono: the same rect, which is also the single lane.
    CHECK(mo.overlay.rect == b);
    CHECK(mo.overlay.rect == mo.upper);
    // The standalone accessor the hit-test paths use agrees with the resolved surface.
    CHECK(waveformOverlayArea(b) == st.overlay);
    CHECK(waveformOverlayArea(b) == mo.overlay);
}

static void testSurfaceDegenerateBandDrawsNothing() {
    const WaveformSurface s = waveformSurface(Rect{10, 10, 0, 0}, true, 2);
    CHECK(s.laneCount == 0);
    CHECK(s.upper.empty() && s.lower.empty() && s.overlay.rect.empty());
    CHECK(waveformOverlayArea(Rect{10, 10, 0, 0}).rect.empty());
}

static void testSurfaceThinBandRoundsLowerLaneEmpty() {
    // Height 3 is the edge where the stereo split's integer division rounds the lower lane to
    // empty even though the band itself isn't degenerate — pins the laneCount derivation.
    const WaveformSurface s = waveformSurface(Rect{0, 0, 100, 3}, true, 2);
    CHECK(s.laneCount == 1);
    CHECK(!s.upper.empty());
    CHECK(s.lower.empty());
}

// --- Hit-testing across the stacked lanes -------------------------------------

static void testMarkerGrabReachesTheLowerStereoLane() {
    const Rect b = band();
    const WaveformSurface s = waveformSurface(b, /*stereoMode=*/true, 2);
    const std::int64_t frames = 1000;
    const std::int64_t markers[1] = {500};
    const int mx = frameToX(s.overlay, frames, 500);
    // The same marker answers a grab in either lane — overlays span the full stack.
    const int upperY = s.upper.y + s.upper.height / 2;
    const int lowerY = s.lower.y + s.lower.height / 2;
    CHECK(markerAtPoint(s.overlay, frames, markers, 1, mx, upperY) == 0);
    CHECK(markerAtPoint(s.overlay, frames, markers, 1, mx, lowerY) == 0);
    // A lower-lane grab hit-tested against the UPPER LANE would be lost — the miss this
    // contract exists to prevent. (Explicit OverlayArea{} wrap: production code can't do
    // this by accident — markerAtPoint won't accept a bare lane Rect — but the geometry
    // claim still needs proving.)
    CHECK(markerAtPoint(overlayOf(s.upper), frames, markers, 1, mx, lowerY) == -1);
    // Off the marker's x is still a miss at either height.
    CHECK(markerAtPoint(s.overlay, frames, markers, 1, mx + 40, lowerY) == -1);
}

static void testMarkerGrabInMonoSpansTheBand() {
    const Rect b = band();
    const WaveformSurface s = waveformSurface(b, /*stereoMode=*/false, 2);
    const std::int64_t frames = 1000;
    const std::int64_t markers[1] = {250};
    const int mx = frameToX(s.overlay, frames, 250);
    CHECK(markerAtPoint(s.overlay, frames, markers, 1, mx, b.y) == 0);
    CHECK(markerAtPoint(s.overlay, frames, markers, 1, mx, b.bottom() - 1) == 0);
    CHECK(markerAtPoint(s.overlay, frames, markers, 1, mx, b.bottom() + 5) == -1);
}

// --- The marker grab handle ----------------------------------------------------

static void testMarkerHandleIsATopStripCentredOnTheMarker() {
    const Rect a = wideArea();
    const int mx = frameToX(overlayOf(a), 1000, 250);
    const Rect h = markerHandleRect(overlayOf(a), 1000, 250);
    CHECK(h.x == mx - kMarkerHandleHalfWidth);
    CHECK(h.right() == mx + kMarkerHandleHalfWidth + 1);
    CHECK(h.y == a.y);
    CHECK(h.height == kMarkerHandleHeight);
    CHECK(contains(h, mx, a.y));
    CHECK(contains(h, mx, a.y + kMarkerHandleHeight - 1));
    CHECK(!contains(h, mx, a.y + kMarkerHandleHeight));  // below the strip is the column's
}

// The whole reason the handle exists: two markers that share a frame both stay reachable —
// markerAtPoint gives its full-height column to the first in draw order, and the handle owns
// the strip above. Without the split, the loser could never be dragged apart again.
static void testCoincidentMarkersStayIndependentlyGrabbable() {
    const Rect a = wideArea();
    const std::int64_t markers[2] = {250, 250};
    const int mx = frameToX(overlayOf(a), 1000, 250);
    // The column resolves to the first marker at every height, including the top strip.
    CHECK(markerAtPoint(overlayOf(a), 1000, markers, 2, mx, a.y) == 0);
    CHECK(markerAtPoint(overlayOf(a), 1000, markers, 2, mx, a.bottom() - 1) == 0);
    // The handle, asked first, resolves the second one in that same top strip.
    CHECK(contains(markerHandleRect(overlayOf(a), 1000, 250), mx, a.y));
    CHECK(!contains(markerHandleRect(overlayOf(a), 1000, 250), mx, a.bottom() - 1));
}

static void testMarkerHandleClipsIntoTheArea() {
    const Rect a = wideArea();
    // At the last frame the marker maps to right(); an unclipped tab would claim pixels
    // outside the band the caller already hit-tested.
    const Rect hi = markerHandleRect(overlayOf(a), 1000, 1000);
    CHECK(hi.right() == a.right());
    CHECK(!contains(hi, a.right(), a.y));
    CHECK(contains(hi, a.right() - 1, a.y));
    // And at frame 0 it cannot reach left of the band.
    const Rect lo = markerHandleRect(overlayOf(a), 1000, 0);
    CHECK(lo.x == a.x);
    CHECK(!contains(lo, a.x - 1, a.y));
}

static void testMarkerHandleOnDegenerateAreas() {
    CHECK(markerHandleRect(overlayOf(Rect{}), 1000, 0).empty());
    // A band shorter than the strip yields a handle the height of the band, never taller.
    const Rect thin = Rect{0, 0, 100, 4};
    CHECK(markerHandleRect(overlayOf(thin), 1000, 500).height == 4);
}

// The shell (editor_input_waveform.cpp) resolves a mark's CAP before it iterates the ordinary
// marker array, because a zero-length fade puts the crossfade cap exactly on the loop-end
// marker's frame. The same coincidence recurs whenever ANY marker shares that frame, most
// plausibly the START marker dragged up against the fade edge: this module can't exercise the
// shell's check-order itself, but it can prove the geometric ambiguity that makes the ordering
// load-bearing — the array's own first-match rule would otherwise resolve the top strip to the
// START marker, not the fade handle.
static void testStartMarkerSharesTheHandleStripWhenItSitsAtTheFadeEdge() {
    const Rect a = wideArea();
    const std::int64_t loopEnd = 400, crossfade = 30;
    const std::int64_t fadeEdge = loopEnd - crossfade;  // where the crossfade cap sits
    const std::int64_t markers[3] = {fadeEdge, 200, loopEnd};  // start dialled onto the fade edge
    const int mx = frameToX(overlayOf(a), 1000, fadeEdge);
    const int topY = a.y;  // inside the handle's top strip
    // Without the shell's priority check, the array's own first-match rule already resolves the
    // column to the start marker (index 0) at this x/y...
    CHECK(markerAtPoint(overlayOf(a), 1000, markers, 3, mx, topY) == 0);
    // ...and the fade handle's rect claims the exact same pixel — the ambiguity the shell
    // resolves by smallest-target-first (the handle's clipped tab is always the narrower
    // target), same as it does for the zero-fade/loop-end case.
    CHECK(contains(markerHandleRect(overlayOf(a), 1000, fadeEdge), mx, topY));
}

// --- The four marks: cap resolve, labels, suppression, crossfade wedge ----------

static WaveMarks marksAt(std::int64_t start, std::int64_t loopStart, std::int64_t loopEnd,
                         std::int64_t xfade, bool loopPresent) {
    WaveMarks m;
    m.frame[0] = start;
    m.frame[1] = loopStart;
    m.frame[2] = loopEnd;
    m.frame[3] = xfade;
    m.present[0] = true;
    m.present[1] = m.present[2] = m.present[3] = loopPresent;
    return m;
}

static void testEveryMarkAnswersItsOwnCap() {
    const Rect a = wideArea();
    const OverlayArea ov = overlayOf(a);
    const WaveMarks m = marksAt(50, 300, 700, 620, true);
    for (int i = 0; i < kWaveMarkCount; ++i) {
        const int mx = frameToX(ov, 1000, m.frame[i]);
        CHECK(capAtPoint(ov, 1000, m, mx, a.y) == i);
        CHECK(capAtPoint(ov, 1000, m, mx, a.y + kMarkerHandleHeight - 1) == i);
        // Below the cap strip is the column's, never the cap's.
        CHECK(capAtPoint(ov, 1000, m, mx, a.y + kMarkerHandleHeight) == -1);
    }
}

static void testAMarkThatIsNotPresentAnswersNoCap() {
    const Rect a = wideArea();
    const OverlayArea ov = overlayOf(a);
    const WaveMarks m = marksAt(50, 300, 700, 620, /*loopPresent=*/false);
    CHECK(capAtPoint(ov, 1000, m, frameToX(ov, 1000, 300), a.y) == -1);
    CHECK(capAtPoint(ov, 1000, m, frameToX(ov, 1000, 620), a.y) == -1);
    CHECK(capAtPoint(ov, 1000, m, frameToX(ov, 1000, 50), a.y) == 0);  // START stays live
}

// The separability argument the reverse cap order exists for: for a coincident PAIR, one mark
// answers the cap and the OTHER answers the full-height column, so neither is ever stranded.
static void testACoincidentPairStaysSeparableAcrossCapAndColumn() {
    const Rect a = wideArea();
    const OverlayArea ov = overlayOf(a);
    const int midY = a.y + a.height / 2;
    // Zero-length fade: the crossfade mark sits at loopEnd - 0, i.e. exactly on the END marker.
    // This is the live case — the crossfade is anchored to the seam it closes.
    {
        const WaveMarks m = marksAt(50, 300, 700, 700, true);
        const int mx = frameToX(ov, 1000, 700);
        CHECK(capAtPoint(ov, 1000, m, mx, a.y) == static_cast<int>(WaveMark::kCrossfade));
        const std::int64_t cols[3] = {m.frame[0], m.frame[1], m.frame[2]};
        CHECK(markerAtPoint(ov, 1000, cols, 3, mx, midY) == static_cast<int>(WaveMark::kLoopEnd));
    }
    // START dragged onto the loop start: the cap goes to LOOP, the column to START.
    {
        const WaveMarks m = marksAt(300, 300, 700, 100, true);
        const int mx = frameToX(ov, 1000, 300);
        CHECK(capAtPoint(ov, 1000, m, mx, a.y) == static_cast<int>(WaveMark::kLoopStart));
        const std::int64_t cols[3] = {m.frame[0], m.frame[1], m.frame[2]};
        CHECK(markerAtPoint(ov, 1000, cols, 3, mx, midY) == static_cast<int>(WaveMark::kStart));
    }
    // START dragged onto the loop end: the cap goes to END, the column to START.
    {
        const WaveMarks m = marksAt(700, 300, 700, 100, true);
        const int mx = frameToX(ov, 1000, 700);
        CHECK(capAtPoint(ov, 1000, m, mx, a.y) == static_cast<int>(WaveMark::kLoopEnd));
        const std::int64_t cols[3] = {m.frame[0], m.frame[1], m.frame[2]};
        CHECK(markerAtPoint(ov, 1000, cols, 3, mx, midY) == static_cast<int>(WaveMark::kStart));
    }
}

// The crossfade is the one mark with NO full-height column, so it must never lose a cap tie.
static void testTheCrossfadeCapOutranksEveryOtherMark() {
    const Rect a = wideArea();
    const OverlayArea ov = overlayOf(a);
    const WaveMarks m = marksAt(400, 400, 400, 400, true);  // every mark on one frame
    CHECK(capAtPoint(ov, 1000, m, frameToX(ov, 1000, 400), a.y) ==
          static_cast<int>(WaveMark::kCrossfade));
}

static void testLabelSidesKeepEachLabelOutOfTheSpanItBounds() {
    CHECK(!markLabelLeftOfLine(WaveMark::kStart));
    CHECK(!markLabelLeftOfLine(WaveMark::kLoopStart));
    CHECK(markLabelLeftOfLine(WaveMark::kLoopEnd));
    CHECK(markLabelLeftOfLine(WaveMark::kCrossfade));

    const Rect a = wideArea();
    const OverlayArea ov = overlayOf(a);
    const int mx = frameToX(ov, 1000, 500);
    const Rect right = markLabelRect(ov, 1000, 500, /*leftOfLine=*/false, 30);
    const Rect left = markLabelRect(ov, 1000, 500, /*leftOfLine=*/true, 30);
    CHECK(right.x == mx + kMarkLabelGap && right.width == 30);
    CHECK(left.right() == mx - kMarkLabelGap && left.width == 30);
    // Directly under the cap strip, so caps and labels never fight for the same pixels.
    CHECK(right.y == a.y + kMarkerHandleHeight && right.height == kMarkLabelHeight);
    CHECK(left.y == right.y);
}

static void testALabelIsNudgedInsideTheAreaRatherThanClipped() {
    const Rect a = wideArea();
    const OverlayArea ov = overlayOf(a);
    // At frame 0 a right-side label would still fit; at the last frame it would overhang.
    const Rect atEnd = markLabelRect(ov, 1000, 1000, /*leftOfLine=*/false, 40);
    CHECK(atEnd.width == 40);
    CHECK(atEnd.right() == a.right());
    const Rect atStart = markLabelRect(ov, 1000, 0, /*leftOfLine=*/true, 40);
    CHECK(atStart.width == 40);
    CHECK(atStart.x == a.x);
    // Wider than the whole band, or no band to draw in: nothing placed.
    CHECK(markLabelRect(ov, 1000, 500, false, a.width + 1).empty());
    CHECK(markLabelRect(overlayOf(Rect{0, 0, 200, kMarkerHandleHeight}), 1000, 500, false, 20)
              .empty());
}

static void testOverlappingLabelsAreSuppressedInPlacementOrder() {
    const Rect a = wideArea();
    const OverlayArea ov = overlayOf(a);
    // LOOP labels right of its line at 500, XFADE left of its line at 520: the two boxes point
    // at each other and cannot both fit. (LOOP and END never collide however close they get —
    // their labels point away from the span they bound.)
    const WaveMarks m = marksAt(50, 500, 900, 520, true);
    const int w[kWaveMarkCount] = {36, 32, 26, 40};
    const WaveMarkLabels lab = layoutMarkLabels(ov, 1000, m, w, /*promoted=*/-1);
    CHECK(!lab.box[0].empty());  // START, far away, always placed
    CHECK(!lab.box[1].empty());  // LOOP placed before XFADE, so LOOP wins
    CHECK(!lab.box[2].empty());  // END, far away, always placed
    CHECK(lab.box[3].empty());   // XFADE suppressed
    // Every placed box is disjoint from every other.
    for (int i = 0; i < kWaveMarkCount; ++i) {
        for (int j = i + 1; j < kWaveMarkCount; ++j) {
            if (lab.box[i].empty() || lab.box[j].empty()) continue;
            CHECK(lab.box[i].x >= lab.box[j].right() || lab.box[j].x >= lab.box[i].right());
        }
    }
}

// The promoted mark is placed FIRST, so grabbing or hovering a mark always shows its label —
// even the one the resting layout suppresses.
static void testThePromotedMarkIsNeverTheSuppressedOne() {
    const Rect a = wideArea();
    const OverlayArea ov = overlayOf(a);
    const WaveMarks m = marksAt(50, 500, 900, 520, true);
    const int w[kWaveMarkCount] = {36, 32, 26, 40};
    CHECK(layoutMarkLabels(ov, 1000, m, w, -1).box[3].empty());  // XFADE suppressed at rest
    const WaveMarkLabels grabbed =
        layoutMarkLabels(ov, 1000, m, w, static_cast<int>(WaveMark::kCrossfade));
    CHECK(!grabbed.box[3].empty());  // and placed when it is the one being grabbed
    CHECK(grabbed.box[1].empty());   // LOOP yields to it instead
}

static void testAbsentMarksTakeNoLabel() {
    const Rect a = wideArea();
    const OverlayArea ov = overlayOf(a);
    const WaveMarks m = marksAt(50, 300, 700, 620, /*loopPresent=*/false);
    const int w[kWaveMarkCount] = {36, 32, 26, 40};
    const WaveMarkLabels lab = layoutMarkLabels(ov, 1000, m, w, -1);
    CHECK(!lab.box[0].empty());
    CHECK(lab.box[1].empty() && lab.box[2].empty() && lab.box[3].empty());
}

static void testTheCrossfadeWedgeRampsToItsPeakAtTheSeam() {
    // Zero at the fade's start, the peak at its last column, monotone in between.
    CHECK(crossfadeWedgeHeight(100, 200, 100) == 0);
    CHECK(crossfadeWedgeHeight(100, 200, 199) == kCrossfadeWedgePx);
    int prev = -1;
    for (int x = 100; x < 200; ++x) {
        const int h = crossfadeWedgeHeight(100, 200, x);
        CHECK(h >= prev);
        CHECK(h >= 0 && h <= kCrossfadeWedgePx);
        prev = h;
    }
    // Outside the span it contributes nothing, so a caller can sweep a wider range safely.
    CHECK(crossfadeWedgeHeight(100, 200, 99) == 0);
    CHECK(crossfadeWedgeHeight(100, 200, 200) == 0);
    // Degenerate spans: an empty one draws nothing, a one-column one is all peak.
    CHECK(crossfadeWedgeHeight(100, 100, 100) == 0);
    CHECK(crossfadeWedgeHeight(100, 99, 100) == 0);
    CHECK(crossfadeWedgeHeight(100, 101, 100) == kCrossfadeWedgePx);
}

// --- Per-lane envelope content -------------------------------------------------

static void testAsymmetricStereoLanesCarryDifferentContent() {
    // Left is full-scale, right is a tenth of it — the lanes must look materially different.
    const std::size_t frames = 400;
    std::vector<AudioSample> interleaved(frames * 2);
    for (std::size_t f = 0; f < frames; ++f) {
        const AudioSample v = (f % 2 == 0) ? 1.0f : -1.0f;
        interleaved[f * 2 + 0] = v;
        interleaved[f * 2 + 1] = v * 0.1f;
    }
    // ONE pass over the interleaved source, split per lane — what the painter does.
    const reasampler::audio::Envelope env =
        reasampler::audio::computeEnvelope(interleaved, 2, frames, 20);
    const reasampler::audio::Envelope upper = laneEnvelope(env, 0);
    const reasampler::audio::Envelope lower = laneEnvelope(env, 1);
    CHECK(upper.size() == 1 && lower.size() == 1);
    CHECK(upper[0].size() == 20 && lower[0].size() == 20);
    for (std::size_t i = 0; i < 20; ++i) {
        CHECK(upper[0][i].max > 0.9f);          // left near full scale
        CHECK(lower[0][i].max < 0.2f);          // right an order of magnitude down
        CHECK(!(upper[0][i] == lower[0][i]));   // and materially different, bin for bin
    }
}

static void testLaneEnvelopeRejectsOutOfRangeLane() {
    const std::size_t frames = 16;
    std::vector<AudioSample> mono(frames, 0.5f);
    const reasampler::audio::Envelope env =
        reasampler::audio::computeEnvelope(mono, 1, frames, 4);
    CHECK(laneEnvelope(env, 0).size() == 1);
    CHECK(laneEnvelope(env, 1).empty());   // a mono source has no lower lane
    CHECK(laneEnvelope(env, -1).empty());
}

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

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

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

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

    testSurfaceStereoStacksTwoLanes();
    testSurfaceMonoIsOneLane();
    testSurfaceMonoSourceInStereoModeStaysOneLane();
    testSurfaceOverlayIsFullStackedHeightInBothModes();
    testSurfaceDegenerateBandDrawsNothing();
    testSurfaceThinBandRoundsLowerLaneEmpty();

    testMarkerGrabReachesTheLowerStereoLane();
    testMarkerGrabInMonoSpansTheBand();

    testMarkerHandleIsATopStripCentredOnTheMarker();
    testCoincidentMarkersStayIndependentlyGrabbable();
    testMarkerHandleClipsIntoTheArea();
    testMarkerHandleOnDegenerateAreas();
    testStartMarkerSharesTheHandleStripWhenItSitsAtTheFadeEdge();

    testEveryMarkAnswersItsOwnCap();
    testAMarkThatIsNotPresentAnswersNoCap();
    testACoincidentPairStaysSeparableAcrossCapAndColumn();
    testTheCrossfadeCapOutranksEveryOtherMark();
    testLabelSidesKeepEachLabelOutOfTheSpanItBounds();
    testALabelIsNudgedInsideTheAreaRatherThanClipped();
    testOverlappingLabelsAreSuppressedInPlacementOrder();
    testThePromotedMarkIsNeverTheSuppressedOne();
    testAbsentMarksTakeNoLabel();
    testTheCrossfadeWedgeRampsToItsPeakAtTheSeam();

    testAsymmetricStereoLanesCarryDifferentContent();
    testLaneEnvelopeRejectsOutOfRangeLane();

    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;
}
