// Standalone tests for reasampler::vst::keyboard_strip — no VST3, no REAPER, no framework.
// Same fast assert loop as the sibling pure tests. Assert the capture-first editor's
// keyboard-strip layout, root marker, key mapping, zone-bar hit regions, and the drag-delta
// note resolver directly — the geometry that backs the single-capture root-set and the opt-in
// Zones panel.
//
// Covers: layoutStrip (normal + zero); keyLeftX monotonic across the 128-key span with the
// boundary at 128 == band right; keyRect / rootMarkerRect (rootMarkerRect == keyRect);
// keyAtPoint inverting the mapping and clamping/ missing off-band; zoneBarRect spanning
// [low,high] inclusive and collapsing (not inverting) a malformed low>high; zoneGrabAt
// classifying low-edge / high-edge / body and the narrow-bar midpoint split (low wins the
// tie); zoneBarAtPoint first-match on overlap + null-list rejection; resolveDragNote rounding
// to the nearest key at the key centre, clamping to [0,127], and the zero-delta / zero-width
// no-ops; isNaturalKey across a full octave (C4..B4), at boundary notes 0 and 127, and with
// out-of-range inputs that clamp to [0,127].

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

#include <cstdio>

using namespace reasampler::vst;

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 strip: 1280px wide (10px per key) so key math is exact and easy to reason
// about.
static StripLayout wideStrip() { return layoutStrip(1280, 40); }

// --- layoutStrip --------------------------------------------------------------

static void testLayoutNormalArea() {
    const StripLayout L = layoutStrip(640, 40);
    CHECK(L.keys.left == 0 && L.keys.top == 0);
    CHECK(L.keys.right == 640 && L.keys.bottom == 40);
}

static void testLayoutZeroArea() {
    const StripLayout L = layoutStrip(0, 0);
    CHECK(L.keys.width() == 0 && L.keys.height() == 0);
}

// --- keyLeftX / keyRect / rootMarkerRect --------------------------------------

static void testKeyLeftMonotonicAndBounds() {
    const StripLayout L = wideStrip();
    // Key 0's left edge is the band left; the 128 boundary is the band right.
    CHECK(keyLeftX(L, 0) == L.keys.left);
    CHECK(keyLeftX(L, 128) == L.keys.right);
    // Strictly non-decreasing across the span.
    int prev = keyLeftX(L, 0);
    for (int n = 1; n <= 128; ++n) {
        const int x = keyLeftX(L, n);
        CHECK(x >= prev);
        prev = x;
    }
    // At 10px/key, key 12 (one octave) starts at 120px.
    CHECK(keyLeftX(L, 12) == 120);
}

static void testKeyRectHalfOpen() {
    const StripLayout L = wideStrip();
    const Rect k = keyRect(L, 60);
    CHECK(k.left == keyLeftX(L, 60));
    CHECK(k.right == keyLeftX(L, 61));
    CHECK(k.top == L.keys.top && k.bottom == L.keys.bottom);
    CHECK(k.width() == 10);  // 10px/key
}

static void testRootMarkerEqualsKeyRect() {
    const StripLayout L = wideStrip();
    const Rect m = rootMarkerRect(L, 64);
    const Rect k = keyRect(L, 64);
    CHECK(m.left == k.left && m.right == k.right && m.top == k.top && m.bottom == k.bottom);
}

// --- keyAtPoint ---------------------------------------------------------------

static void testKeyAtPointInverts() {
    const StripLayout L = wideStrip();
    // A point in the middle of key 60's cell resolves to 60.
    const Rect k = keyRect(L, 60);
    CHECK(keyAtPoint(L, k.left + 5, k.top + 2) == 60);
    // The very left of the band is key 0; just inside the right edge is key 127.
    CHECK(keyAtPoint(L, L.keys.left, 2) == 0);
    CHECK(keyAtPoint(L, L.keys.right - 1, 2) == 127);
}

static void testKeyAtPointOffBand() {
    const StripLayout L = wideStrip();
    CHECK(keyAtPoint(L, -5, 2) == -1);            // left of band
    CHECK(keyAtPoint(L, L.keys.right + 5, 2) == -1);  // right of band
    CHECK(keyAtPoint(L, 100, L.keys.bottom + 5) == -1);  // below band
}

// --- zoneBarRect --------------------------------------------------------------

static void testZoneBarSpansInclusive() {
    const StripLayout L = wideStrip();
    const Rect bar = zoneBarRect(L, 12, 23);  // C1..B1 inclusive
    CHECK(bar.left == keyLeftX(L, 12));
    CHECK(bar.right == keyLeftX(L, 24));  // high+1 -> the bar covers key 23 fully
    CHECK(bar.width() == 120);            // 12 keys * 10px
}

static void testZoneBarMalformedCollapses() {
    const StripLayout L = wideStrip();
    // low > high must collapse, never invert.
    const Rect bar = zoneBarRect(L, 80, 40);
    CHECK(bar.width() >= 0);
    CHECK(bar.right >= bar.left);
}

// --- zoneGrabAt ---------------------------------------------------------------

static void testZoneGrabEdgesAndBody() {
    const StripLayout L = wideStrip();
    const Rect bar = zoneBarRect(L, 20, 60);  // wide bar with a clear body
    const int y = L.keys.top + 2;
    // Near the left edge -> low; near the right edge -> high; the middle -> body.
    CHECK(zoneGrabAt(L, 20, 60, bar.left + 1, y) == ZoneGrab::kLowEdge);
    CHECK(zoneGrabAt(L, 20, 60, bar.right - 1, y) == ZoneGrab::kHighEdge);
    CHECK(zoneGrabAt(L, 20, 60, bar.left + bar.width() / 2, y) == ZoneGrab::kBody);
    // Off the bar entirely -> none.
    CHECK(zoneGrabAt(L, 20, 60, bar.right + 20, y) == ZoneGrab::kNone);
}

static void testZoneGrabNarrowBarSplitsAtMidpointLowWins() {
    const StripLayout L = wideStrip();
    // A 1-key bar is narrower than 2*edge: no body; the low edge wins the exact midpoint.
    const Rect bar = zoneBarRect(L, 50, 50);
    const int y = L.keys.top + 2;
    const int mid = bar.left + bar.width() / 2;
    CHECK(zoneGrabAt(L, 50, 50, mid, y) == ZoneGrab::kLowEdge);         // tie -> low
    CHECK(zoneGrabAt(L, 50, 50, bar.right - 1, y) == ZoneGrab::kHighEdge);
}

// --- zoneBarAtPoint -----------------------------------------------------------

static void testZoneBarAtPointFirstMatch() {
    const StripLayout L = wideStrip();
    const int lows[2] = {20, 30};   // zone 0 and zone 1 overlap on [30,50]
    const int highs[2] = {50, 70};
    const Rect overlap = zoneBarRect(L, 30, 50);
    const int y = L.keys.top + 2;
    const int cx = overlap.left + overlap.width() / 2;
    // A point in the overlap resolves to the FIRST covering zone (draw order).
    const ZoneBarHit hit = zoneBarAtPoint(L, lows, highs, 2, cx, y);
    CHECK(hit.zoneIndex == 0);
    CHECK(hit.grab != ZoneGrab::kNone);
}

static void testZoneBarAtPointNullList() {
    const StripLayout L = wideStrip();
    const ZoneBarHit hit = zoneBarAtPoint(L, nullptr, nullptr, 0, 100, 2);
    CHECK(hit.zoneIndex == -1 && hit.grab == ZoneGrab::kNone);
}

// --- resolveDragNote ----------------------------------------------------------

static void testResolveDragRoundsToNearestKey() {
    const StripLayout L = wideStrip();  // 10px/key
    // A +25px drag from key 60 = +2.5 keys -> rounds to +3 (half-key flips at the centre).
    CHECK(resolveDragNote(L, 60, 25) == 63);
    // A +24px drag = +2.4 keys -> rounds to +2.
    CHECK(resolveDragNote(L, 60, 24) == 62);
    // Symmetric for negative deltas.
    CHECK(resolveDragNote(L, 60, -25) == 57);
    CHECK(resolveDragNote(L, 60, -24) == 58);
}

static void testResolveDragClampsAndNoOps() {
    const StripLayout L = wideStrip();
    CHECK(resolveDragNote(L, 60, 0) == 60);          // zero delta -> unchanged
    CHECK(resolveDragNote(L, 2, -1000) == 0);        // clamps at 0
    CHECK(resolveDragNote(L, 120, 1000) == 127);     // clamps at 127
    // Zero-width band -> no motion (pins to startNote, clamped).
    const StripLayout Z = layoutStrip(0, 40);
    CHECK(resolveDragNote(Z, 60, 500) == 60);
}

// --- isNaturalKey -------------------------------------------------------------

static void testIsNaturalKeyFullOctave() {
    // Semitone positions 0..11 starting at C4 (MIDI 60):
    //   C=60(nat) C#=61(acc) D=62(nat) D#=63(acc) E=64(nat) F=65(nat)
    //   F#=66(acc) G=67(nat) G#=68(acc) A=69(nat) A#=70(acc) B=71(nat)
    const bool expected[12] = {
        true,  false, true,  false, true,  true,
        false, true,  false, true,  false, true,
    };
    for (int i = 0; i < 12; ++i) {
        CHECK(isNaturalKey(60 + i) == expected[i]);
    }
}

static void testIsNaturalKeyBoundaryNotes() {
    // Note 0 is C (natural); note 127 is G (natural); note 1 is C# (accidental).
    CHECK(isNaturalKey(0)   == true);   // C0  — natural
    CHECK(isNaturalKey(1)   == false);  // C#0 — accidental
    CHECK(isNaturalKey(127) == true);   // G9  — natural (127 % 12 == 7)
    CHECK(isNaturalKey(126) == false);  // F#9 — accidental (126 % 12 == 6)
}

static void testIsNaturalKeyOutOfRangeClamped() {
    // Values outside [0,127] clamp to [0,127]; must not crash/UB.
    // note -1 clamps to 0 (C, natural); note 128 clamps to 127 (G, natural).
    CHECK(isNaturalKey(-1)  == true);
    CHECK(isNaturalKey(128) == true);
    CHECK(isNaturalKey(-100) == true);
    CHECK(isNaturalKey(200)  == true);
}

static void testResolveDragProportionalNonDivisibleWidth() {
    // THE REVIEW FINDING: 544px / 128 = 4.25 (non-integer). Old uniform-keyW math used
    // keyW = 4 (floor), accumulating ~7 keys of drift at the far end. The proportional fix
    // must agree with keyAtPoint at every point — specifically the far-end invariant:
    // a drag from note 0 by (width-1) pixels must land at keyAtPoint(width-1), which is 127.
    const int width = 544;
    const StripLayout L = layoutStrip(width, 40);
    CHECK(keyAtPoint(L, width - 1, L.keys.top + 1) == 127);
    CHECK(resolveDragNote(L, 0, width - 1) == 127);

    // Also verify mid-strip coherence: for each key N, a drag from 0 by N's left-edge
    // pixel offset should land at N (or N-1 at worst — left-edge pixel is a boundary, so
    // rounding may round down). The critical direction is that it must NOT over-shoot by
    // more than 0 (it must reach at least the right key).
    for (int n = 1; n < kStripKeyCount; ++n) {
        const int leftPx = keyRect(L, n).left;
        const int resolved = resolveDragNote(L, 0, leftPx);
        // The left edge of key N is the first pixel "in" that key, so we expect resolved == N.
        // Allow resolved == N-1 only when the pixel is at the exact boundary (keyEdgeToX may
        // produce the same x for adjacent keys when keys share a pixel). Disallow over-shoot.
        const int expected = keyAtPoint(L, leftPx, L.keys.top + 1);
        CHECK(resolved >= expected - 1 && resolved <= expected + 1);
    }
}

int main() {
    testLayoutNormalArea();
    testLayoutZeroArea();
    testKeyLeftMonotonicAndBounds();
    testKeyRectHalfOpen();
    testRootMarkerEqualsKeyRect();
    testKeyAtPointInverts();
    testKeyAtPointOffBand();
    testZoneBarSpansInclusive();
    testZoneBarMalformedCollapses();
    testZoneGrabEdgesAndBody();
    testZoneGrabNarrowBarSplitsAtMidpointLowWins();
    testZoneBarAtPointFirstMatch();
    testZoneBarAtPointNullList();
    testResolveDragRoundsToNearestKey();
    testResolveDragClampsAndNoOps();
    testResolveDragProportionalNonDivisibleWidth();
    testIsNaturalKeyFullOctave();
    testIsNaturalKeyBoundaryNotes();
    testIsNaturalKeyOutOfRangeClamped();

    if (g_fail == 0) std::printf("keyboard_strip: all tests passed\n");
    return g_fail != 0;
}
