// Standalone tests for reasampler::bank_grid — no REAPER, no test framework.
// Same fast loop as the sibling pure tests: assert the grid-layout math and the
// thumbnail cache-key stringification directly.
//
// Covers (M5 Wave A brief §Test cases): column count for a given panel width;
// cell rects for a full grid (row/column wrapping); the partial-last-row case;
// itemCount == 0; a single item; a panel too narrow for even one cell (clamp to
// one column); content-height for exact and partial rows; cache-key stability,
// width/generation/id sensitivity, and length-prefix collision resistance.
//
// Wave B adds: hit-testing (inside / gap / out-of-range / half-open bounds);
// selection updates (plain / ctrl-toggle / shift-range, invariants preserved); and
// keyboard nav (arrow clamp, row moves, shift-extend, partial-last-row clamp,
// fresh-panel focus).

#include "../src/core/ui/bank_grid.h"

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

using namespace reasampler;
using namespace reasampler::ui;

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 spec with round numbers so expected rects are trivial to hand-compute:
// cell 100x50, gap 10. Column pitch = 110, row pitch = 60, margin = 10.
static GridSpec spec() {
    GridSpec s;
    s.cellWidth = 100;
    s.cellHeight = 50;
    s.gap = 10;
    return s;
}

// gap(10) + n*(100+10): 1 col needs 120, 2 need 230, 3 need 340.
static void testColumnsForWidth() {
    const GridSpec s = spec();
    CHECK(columnsForWidth(120, s) == 1);   // exactly one cell + margins
    CHECK(columnsForWidth(229, s) == 1);   // one pixel short of two columns
    CHECK(columnsForWidth(230, s) == 2);   // exactly two
    CHECK(columnsForWidth(345, s) == 3);   // three plus slack
    CHECK(columnsForWidth(1000, s) == 9);  // many
}

// A panel narrower than a single cell still yields one column (clipped by the
// window, never zero — that would drop every sample).
static void testTooNarrowClampsToOneColumn() {
    const GridSpec s = spec();
    CHECK(columnsForWidth(50, s) == 1);
    CHECK(columnsForWidth(0, s) == 1);
    CHECK(columnsForWidth(-20, s) == 1);
}

// Zero items -> no rects (empty state is the panel's concern, not the layout's).
static void testZeroItems() {
    const GridSpec s = spec();
    auto rects = computeCellRects(0, 500, s);
    CHECK(rects.empty());
    CHECK(contentHeight(0, 500, s) == 0);
}

// One item sits at the top-left margin.
static void testSingleItem() {
    const GridSpec s = spec();
    auto rects = computeCellRects(1, 500, s);
    CHECK(rects.size() == 1);
    CHECK(rects[0] == (CellRect{10, 10, 100, 50}));
}

// A full 2x2: width forces exactly two columns, four items fill two rows.
static void testFullGridWrapping() {
    const GridSpec s = spec();
    // Width 230 -> exactly 2 columns.
    auto rects = computeCellRects(4, 230, s);
    CHECK(rects.size() == 4);
    // Row 0: x = 10, 120 ; y = 10.
    CHECK(rects[0] == (CellRect{10, 10, 100, 50}));
    CHECK(rects[1] == (CellRect{120, 10, 100, 50}));
    // Row 1: y = 70 (10 + 60).
    CHECK(rects[2] == (CellRect{10, 70, 100, 50}));
    CHECK(rects[3] == (CellRect{120, 70, 100, 50}));
}

// Partial last row: 5 items in a 2-column grid -> rows of 2,2,1. The lone last
// cell is left-aligned in its row (no centering), same x as column 0.
static void testPartialLastRow() {
    const GridSpec s = spec();
    auto rects = computeCellRects(5, 230, s);  // 2 columns
    CHECK(rects.size() == 5);
    CHECK(rects[4] == (CellRect{10, 130, 100, 50}));  // row 2, col 0: y = 10 + 2*60
    // content height spans 3 rows: 10 + 3*(50+10) = 190.
    CHECK(contentHeight(5, 230, s) == 190);
}

// Content height for an exact-fill grid: 4 items / 2 cols = 2 rows.
static void testContentHeightExactRows() {
    const GridSpec s = spec();
    CHECK(contentHeight(4, 230, s) == 130);  // 10 + 2*60
    CHECK(contentHeight(2, 230, s) == 70);   // 10 + 1*60
}

// The cache key is stable and sensitive to every field.
static void testCacheKeyStabilityAndSensitivity() {
    ThumbnailKey a{"sample-1", 120, 7};
    ThumbnailKey aSame{"sample-1", 120, 7};
    CHECK(thumbnailKeyString(a) == thumbnailKeyString(aSame));  // deterministic

    ThumbnailKey diffWidth{"sample-1", 121, 7};
    ThumbnailKey diffGen{"sample-1", 120, 8};
    ThumbnailKey diffId{"sample-2", 120, 7};
    CHECK(thumbnailKeyString(a) != thumbnailKeyString(diffWidth));
    CHECK(thumbnailKeyString(a) != thumbnailKeyString(diffGen));
    CHECK(thumbnailKeyString(a) != thumbnailKeyString(diffId));
}

// A sampleId containing the delimiter byte must not forge a collision with a
// different key. Without the length prefix, id "x|9" with width 0 and id "x" with
// width 9 would both tail-concatenate through the '|' delimiter and could match;
// the length prefix on the id disambiguates them.
static void testCacheKeyDelimiterCollisionResistance() {
    // The canonical would-be collision: moving a "|9" fragment from the id into
    // the width field. Length-prefixing the id makes the two forms distinct.
    ThumbnailKey a{"x|9", 0, 0};
    ThumbnailKey b{"x", 9, 0};
    CHECK(thumbnailKeyString(a) != thumbnailKeyString(b));

    // A second pair in the same spirit, delimiters in both id and adjacent fields.
    ThumbnailKey k1{"1|2", 3, 0};
    ThumbnailKey k2{"1", 23, 0};
    CHECK(thumbnailKeyString(k1) != thumbnailKeyString(k2));
}

// --- Hit-testing --------------------------------------------------------------

// A 2x2 grid of the round spec: rects at (10,10),(120,10),(10,70),(120,70), each
// 100x50. Gaps sit at x in [110,120) and y in [60,70) and the outer margin < 10.
static std::vector<CellRect> grid2x2() {
    return computeCellRects(4, 230, spec());  // 2 columns, 4 items
}

// A point inside a cell returns that cell's index; the top-left corner is inside
// (half-open lower bound), the bottom-right corner is NOT (half-open upper bound).
static void testHitTestInside() {
    auto rects = grid2x2();
    CHECK(hitTestCell(10, 10, rects) == 0);     // top-left corner of cell 0: inside
    CHECK(hitTestCell(60, 35, rects) == 0);     // center of cell 0
    CHECK(hitTestCell(120, 10, rects) == 1);    // top-left of cell 1
    CHECK(hitTestCell(10, 70, rects) == 2);     // top-left of cell 2
    CHECK(hitTestCell(120, 70, rects) == 3);    // top-left of cell 3
    // One pixel inside the far edge of cell 0 (x=109,y=59) still hits it.
    CHECK(hitTestCell(109, 59, rects) == 0);
}

// The exclusive far edge (x+width, y+height) is a MISS — belongs to no cell, so
// adjacent gapless rects would never double-claim it.
static void testHitTestHalfOpenBounds() {
    auto rects = grid2x2();
    CHECK(hitTestCell(110, 35, rects) == -1);   // x == cell0.x+width: past cell 0
    CHECK(hitTestCell(60, 60, rects) == -1);    // y == cell0.y+height: past cell 0
}

// A click in the inter-cell gap or the outer margin hits nothing.
static void testHitTestGapAndMargin() {
    auto rects = grid2x2();
    CHECK(hitTestCell(115, 35, rects) == -1);   // horizontal gap between cols
    CHECK(hitTestCell(60, 65, rects) == -1);    // vertical gap between rows
    CHECK(hitTestCell(0, 0, rects) == -1);      // top-left margin
    CHECK(hitTestCell(5, 35, rects) == -1);     // left margin
}

// A click well outside the grid (below the last row / right of the last col) and
// an empty rect list both miss.
static void testHitTestOutOfRange() {
    auto rects = grid2x2();
    CHECK(hitTestCell(1000, 1000, rects) == -1);
    CHECK(hitTestCell(60, 35, {}) == -1);       // no cells at all
    CHECK(hitTestCell(-5, -5, rects) == -1);    // negative coords
}

// --- Selection updates --------------------------------------------------------

static bool selEq(const Selection& s, std::vector<int> idx, int focus, int anchor) {
    return s.indices == idx && s.focus == focus && s.anchor == anchor;
}

// A plain click selects only that cell; focus and anchor both land on it,
// replacing any prior multi-selection.
static void testClickPlainReplaces() {
    Selection start{{0, 1, 2}, 2, 0};
    Selection s = applyClick(start, 4, /*ctrl=*/false, /*shift=*/false, 6);
    CHECK(selEq(s, {4}, 4, 4));
}

// Ctrl-click adds an unselected cell (keeping the set sorted) and moves focus.
static void testClickCtrlAdds() {
    Selection start{{1, 3}, 3, 1};
    Selection s = applyClick(start, 2, /*ctrl=*/true, /*shift=*/false, 6);
    CHECK(selEq(s, {1, 2, 3}, 2, 2));           // inserted in sorted position
}

// Ctrl-click on an already-selected cell removes it (toggle out); focus still
// moves to the clicked cell even though it left the set.
static void testClickCtrlRemoves() {
    Selection start{{1, 2, 3}, 3, 1};
    Selection s = applyClick(start, 2, /*ctrl=*/true, /*shift=*/false, 6);
    CHECK(selEq(s, {1, 3}, 2, 2));
}

// Shift-click selects the inclusive range from the existing anchor to the click,
// leaving the anchor put; order-agnostic (anchor above or below the click).
static void testClickShiftRange() {
    Selection start{{2}, 2, 2};                 // anchor at 2
    Selection s = applyClick(start, 5, /*ctrl=*/false, /*shift=*/true, 8);
    CHECK(selEq(s, {2, 3, 4, 5}, 5, 2));
    // Downward range (click above the anchor) yields the same inclusive set.
    Selection s2 = applyClick(start, 0, false, true, 8);
    CHECK(selEq(s2, {0, 1, 2}, 0, 2));
}

// Shift-click with no prior anchor behaves like a plain click (anchor seeds at the
// clicked cell).
static void testClickShiftNoAnchor() {
    Selection start{};                          // focus/anchor == -1
    Selection s = applyClick(start, 3, false, true, 6);
    CHECK(selEq(s, {3}, 3, 3));
}

// Shift takes precedence over ctrl when both are held (range select, documented).
static void testClickShiftBeatsCtrl() {
    Selection start{{1}, 1, 1};
    Selection s = applyClick(start, 3, /*ctrl=*/true, /*shift=*/true, 6);
    CHECK(selEq(s, {1, 2, 3}, 3, 1));           // range, not toggle
}

// An out-of-range index (or empty grid) returns the selection unchanged.
static void testClickOutOfRangeNoop() {
    Selection start{{1, 2}, 2, 1};
    CHECK(applyClick(start, 9, false, false, 6) == start);
    CHECK(applyClick(start, -1, false, false, 6) == start);
    CHECK(applyClick(start, 0, false, false, 0) == start);
}

// --- Keyboard navigation ------------------------------------------------------

// Right/Left move by one in linear order; Down/Up move by a row (cols cells).
static void testNavArrowsMoveOneAndRow() {
    // 6 items, 3 columns: rows [0,1,2],[3,4,5]. Focus at 1.
    Selection start{{1}, 1, 1};
    CHECK(selEq(navigate(start, NavKey::Right, 3, 6, false), {2}, 2, 2));
    CHECK(selEq(navigate(start, NavKey::Left, 3, 6, false), {0}, 0, 0));
    CHECK(selEq(navigate(start, NavKey::Down, 3, 6, false), {4}, 4, 4));
    // Up from row 1 back to row 0.
    Selection row1{{4}, 4, 4};
    CHECK(selEq(navigate(row1, NavKey::Up, 3, 6, false), {1}, 1, 1));
}

// Movement clamps at every edge (no wrap): Left on cell 0, Right on the last cell,
// Up on the top row, Down past the last cell all stay put.
static void testNavClampsAtEdges() {
    CHECK(selEq(navigate(Selection{{0}, 0, 0}, NavKey::Left, 3, 6, false), {0}, 0, 0));
    CHECK(selEq(navigate(Selection{{5}, 5, 5}, NavKey::Right, 3, 6, false), {5}, 5, 5));
    CHECK(selEq(navigate(Selection{{2}, 2, 2}, NavKey::Up, 3, 6, false), {2}, 2, 2));
    CHECK(selEq(navigate(Selection{{5}, 5, 5}, NavKey::Down, 3, 6, false), {5}, 5, 5));
}

// Down from a cell above a MISSING last-row cell clamps to the last cell rather
// than overshooting past itemCount. 5 items, 3 cols: rows [0,1,2],[3,4]. Down from
// 2 would be 5 (absent) -> clamps to 4.
static void testNavDownPartialLastRowClamps() {
    Selection start{{2}, 2, 2};
    CHECK(selEq(navigate(start, NavKey::Down, 3, 5, false), {4}, 4, 4));
}

// Home/End jump to the first/last cell.
static void testNavHomeEnd() {
    Selection start{{3}, 3, 3};
    CHECK(selEq(navigate(start, NavKey::Home, 3, 6, false), {0}, 0, 0));
    CHECK(selEq(navigate(start, NavKey::End, 3, 6, false), {5}, 5, 5));
}

// Shift+arrow extends the range from the anchor; the anchor stays put as focus
// walks. Repeated shift-right grows the set.
static void testNavShiftExtends() {
    Selection start{{1}, 1, 1};                 // anchor at 1
    Selection s1 = navigate(start, NavKey::Right, 3, 6, true);
    CHECK(selEq(s1, {1, 2}, 2, 1));
    Selection s2 = navigate(s1, NavKey::Right, 3, 6, true);
    CHECK(selEq(s2, {1, 2, 3}, 3, 1));
    // Shift-down from the grown range extends by a whole row from the anchor.
    Selection s3 = navigate(s1, NavKey::Down, 3, 6, true);  // focus 2 -> 5
    CHECK(selEq(s3, {1, 2, 3, 4, 5}, 5, 1));
}

// Shift-extending back toward the anchor shrinks the range (focus crosses the
// anchor without moving it).
static void testNavShiftShrinksAndCrosses() {
    Selection start{{1, 2, 3}, 3, 1};           // anchor 1, focus 3
    Selection s = navigate(start, NavKey::Left, 3, 6, true);   // focus 3 -> 2
    CHECK(selEq(s, {1, 2}, 2, 1));
    Selection s2 = navigate(s, NavKey::Left, 3, 6, true);      // focus 2 -> 1 (anchor)
    CHECK(selEq(s2, {1}, 1, 1));
    Selection s3 = navigate(s2, NavKey::Left, 3, 6, true);     // cross below anchor
    CHECK(selEq(s3, {0, 1}, 0, 1));
}

// A fresh panel (empty selection) focuses cell 0 on the first arrow without
// stepping, with and without shift.
static void testNavFromEmptyFocusesFirst() {
    Selection empty{};
    CHECK(selEq(navigate(empty, NavKey::Down, 3, 6, false), {0}, 0, 0));
    CHECK(selEq(navigate(empty, NavKey::Right, 3, 6, true), {0}, 0, 0));
}

// itemCount <= 0 returns the selection unchanged; cols < 1 is treated as 1.
static void testNavDegenerate() {
    Selection start{{1}, 1, 1};
    CHECK(navigate(start, NavKey::Right, 3, 0, false) == start);
    // cols coerced to 1: Down moves by 1 in a single-column grid.
    CHECK(selEq(navigate(Selection{{0}, 0, 0}, NavKey::Down, 0, 4, false), {1}, 1, 1));
}

// --- compressAmplitudeForDisplay ----------------------------------------------

// Full scale: magnitude 1.0 must reach the full display fraction exactly.
static void testCompressFullScale() {
    CHECK(compressAmplitudeForDisplay(1.0f) == 1.0f);
    CHECK(compressAmplitudeForDisplay(-1.0f) == -1.0f);
}

// Exact zero must stay on the midline (no log of zero; guards the singularity).
static void testCompressZeroIsMidline() {
    CHECK(compressAmplitudeForDisplay(0.0f) == 0.0f);
}

// -20 dB (0.1 linear) and -40 dB (0.01 linear) must both produce clearly visible
// (non-zero) fractions, with -20 dB > -40 dB (monotonic), and both well above
// the midline (arbitrary threshold of 0.15 chosen conservatively — at a -60 dB
// floor, -20 dB normalizes to 2/3 and -40 dB to 1/3).
static void testCompressMidValuesVisible() {
    const float f20 = compressAmplitudeForDisplay(0.1f);   // -20 dBFS
    const float f40 = compressAmplitudeForDisplay(0.01f);  // -40 dBFS
    CHECK(f20 > 0.15f);    // clearly non-zero
    CHECK(f40 > 0.15f);    // clearly non-zero
    CHECK(f20 > f40);      // monotonic: louder -> taller bar
}

// At and below the floor (-60 dB = 0.001 linear) the result is ~0 (silence).
// We test at exactly the floor magnitude and well below it.
static void testCompressAtAndBelowFloor() {
    // 0.001 == 10^(-60/20) is the floor ratio. Magnitude at or below it -> 0.
    const float floorMag = std::pow(10.0f, kDisplayFloorDb / 20.0f);  // ~0.001
    CHECK(compressAmplitudeForDisplay(floorMag) == 0.0f);
    CHECK(compressAmplitudeForDisplay(floorMag * 0.5f) == 0.0f);
    CHECK(compressAmplitudeForDisplay(0.0001f) == 0.0f);
}

// Sign is preserved: negative input produces a negative fraction of the same
// magnitude as its positive counterpart.
static void testCompressSignPreserved() {
    const float pos = compressAmplitudeForDisplay(0.1f);
    const float neg = compressAmplitudeForDisplay(-0.1f);
    CHECK(neg < 0.0f);
    // Magnitudes must be equal (sign-symmetric).
    const float diff = pos + neg;  // pos - |neg|
    CHECK(diff > -0.001f && diff < 0.001f);
}

int main() {
    testColumnsForWidth();
    testTooNarrowClampsToOneColumn();
    testZeroItems();
    testSingleItem();
    testFullGridWrapping();
    testPartialLastRow();
    testContentHeightExactRows();
    testCacheKeyStabilityAndSensitivity();
    testCacheKeyDelimiterCollisionResistance();

    testHitTestInside();
    testHitTestHalfOpenBounds();
    testHitTestGapAndMargin();
    testHitTestOutOfRange();

    testClickPlainReplaces();
    testClickCtrlAdds();
    testClickCtrlRemoves();
    testClickShiftRange();
    testClickShiftNoAnchor();
    testClickShiftBeatsCtrl();
    testClickOutOfRangeNoop();

    testNavArrowsMoveOneAndRow();
    testNavClampsAtEdges();
    testNavDownPartialLastRowClamps();
    testNavHomeEnd();
    testNavShiftExtends();
    testNavShiftShrinksAndCrosses();
    testNavFromEmptyFocusesFirst();
    testNavDegenerate();

    testCompressFullScale();
    testCompressZeroIsMidline();
    testCompressMidValuesVisible();
    testCompressAtAndBelowFloor();
    testCompressSignPreserved();

    if (g_fail == 0) std::printf("All tests passed.\n");
    return g_fail ? 1 : 0;
}
