// Standalone tests for reasampler::instrument::ui::browser_scroll — no VST3, no REAPER, no framework.
// Same fast assert loop as the sibling pure editor tests: assert the S12 scroll-window +
// scrollbar-thumb + type-to-filter-search geometry LAYERED over the S10 capture_browser.
//
// Covers: scrollContentHeight (ceil rows * card height, 0 for no cards); scrollMaxOffset (0
// when content fits, else content-visible); clampScrollOffset pinning to [0,max]; visibleCardRange
// windowing (top rows only, scrolled window, empty when scrolled past the end); scrolledCardCellRect
// shifting a cell up by the offset; scrollThumbRect (empty when it fits, proportional height +
// position, minimum height, at-max pins to the track bottom); thumbDragToOffset as the position
// inverse (a full-track drag reaches max, round-trips); searchBoxRect; nameMatchesQuery
// (case-insensitive substring, empty-query identity, no-match); filterNameIndices preserving order
// and returning every index for an empty query.

#include "../src/core/instrument/ui/browser_scroll.h"
// The modal reuses the Sample face's chrome metrics (kPad / kTitleHeight); assert against
// those same constants so a metric change can never desync the sheet from what it covers.
#include "../src/core/instrument/ui/sample_bands.h"

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

using namespace reasampler;
using namespace reasampler::instrument::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 layout wide enough for a few columns and tall enough to show a few rows.
static BrowserLayout wideLayout() { return layoutBrowser(560, 300); }

// --- content / max / clamp ----------------------------------------------------

static void testContentHeight() {
    const BrowserLayout L = wideLayout();
    CHECK(scrollContentHeight(L, 0) == 0);
    // One card -> one row -> one card height.
    CHECK(scrollContentHeight(L, 1) == kBrowserCardHeight);
    // columns+1 cards -> two rows.
    CHECK(scrollContentHeight(L, L.columns + 1) == 2 * kBrowserCardHeight);
    // Exactly `columns` cards -> one row.
    CHECK(scrollContentHeight(L, L.columns) == kBrowserCardHeight);
}

static void testMaxOffsetFitsAndOverflows() {
    const BrowserLayout L = wideLayout();
    // A single row fits within the 300px area -> no scroll.
    CHECK(scrollMaxOffset(L, L.columns) == 0);
    // Many rows overflow -> max = content - gridHeight.
    const int many = L.columns * 20;
    const int expect = scrollContentHeight(L, many) - L.grid.height;
    CHECK(scrollMaxOffset(L, many) == expect);
    CHECK(expect > 0);
}

static void testClamp() {
    const BrowserLayout L = wideLayout();
    const int many = L.columns * 20;
    const int maxOff = scrollMaxOffset(L, many);
    CHECK(clampScrollOffset(L, many, -50) == 0);
    CHECK(clampScrollOffset(L, many, maxOff + 500) == maxOff);
    CHECK(clampScrollOffset(L, many, maxOff / 2) == maxOff / 2);
}

// --- visible window -----------------------------------------------------------

static void testVisibleRangeTop() {
    const BrowserLayout L = wideLayout();
    const int many = L.columns * 20;
    const VisibleRange vr = visibleCardRange(L, many, 0);
    CHECK(vr.first == 0);
    // At offset 0, the last visible row is the one containing (gridH-1).
    const int expectedLastRow = (L.grid.height - 1) / kBrowserCardHeight + 1;
    CHECK(vr.last == expectedLastRow * L.columns);
}

static void testVisibleRangeScrolled() {
    const BrowserLayout L = wideLayout();
    const int many = L.columns * 20;
    // Scroll one full card row down.
    const VisibleRange vr = visibleCardRange(L, many, kBrowserCardHeight);
    CHECK(vr.first == L.columns);  // the first row scrolled off the top
}

static void testVisibleRangeEmptyWhenNoCards() {
    const BrowserLayout L = wideLayout();
    const VisibleRange vr = visibleCardRange(L, 0, 0);
    CHECK(vr.first == 0 && vr.last == 0);
}

static void testScrolledCellShiftsUp() {
    const BrowserLayout L = wideLayout();
    const Rect base = cardCellRect(L, 3);
    const Rect shifted = scrolledCardCellRect(L, 3, 40);
    CHECK(shifted.y == base.y - 40);
    CHECK(shifted.bottom() == base.bottom() - 40);
    CHECK(shifted.x == base.x);
}

// --- scrollbar thumb ----------------------------------------------------------

static void testThumbEmptyWhenFits() {
    const BrowserLayout L = wideLayout();
    CHECK(scrollThumbRect(L, L.columns, 0).height == 0);  // one row fits -> no thumb
}

static void testThumbProportionalAndClamped() {
    const BrowserLayout L = wideLayout();
    const int many = L.columns * 20;
    const Rect atTop = scrollThumbRect(L, many, 0);
    CHECK(atTop.height > 0);
    CHECK(atTop.y == L.grid.y);  // at offset 0 the thumb starts at the track top
    CHECK(atTop.width == kScrollbarWidth);
    CHECK(atTop.right() == L.grid.right());
    // At max offset, the thumb bottom reaches the grid bottom (pinned to the end).
    const int maxOff = scrollMaxOffset(L, many);
    const Rect atMax = scrollThumbRect(L, many, maxOff);
    CHECK(atMax.bottom() == L.grid.y + L.grid.height);
}

static void testThumbDragIsInverse() {
    const BrowserLayout L = wideLayout();
    const int many = L.columns * 20;
    const int maxOff = scrollMaxOffset(L, many);
    // A zero drag holds the start offset.
    CHECK(thumbDragToOffset(L, many, 0, 0) == 0);
    // A large positive drag pins to max; a large negative drag pins to 0.
    CHECK(thumbDragToOffset(L, many, 0, 100000) == maxOff);
    CHECK(thumbDragToOffset(L, many, maxOff, -100000) == 0);
    // Dragging the thumb by the whole track span from top reaches (near) max.
    const Rect thumb = scrollThumbRect(L, many, 0);
    const int trackSpan = L.grid.height - thumb.height;
    const int off = thumbDragToOffset(L, many, 0, trackSpan);
    CHECK(off >= maxOff - 2 && off <= maxOff);
}

// --- search -------------------------------------------------------------------

static void testSearchBoxRect() {
    const Rect r = searchBoxRect(200);
    CHECK(r.x == 0 && r.y == 0 && r.right() == 200 && r.height == kSearchBoxHeight);
    CHECK(searchBoxRect(0).width == 0);
}

static void testNameMatch() {
    CHECK(nameMatchesQuery("Kick Drum 01", ""));         // empty query matches all
    CHECK(nameMatchesQuery("Kick Drum 01", "drum"));      // case-insensitive substring
    CHECK(nameMatchesQuery("Kick Drum 01", "KICK"));
    CHECK(!nameMatchesQuery("Kick Drum 01", "snare"));
    CHECK(!nameMatchesQuery("ab", "abc"));                // query longer than name
}

static void testFilterIndices() {
    std::vector<std::string> names{"Kick", "Snare", "Kick Sub", "Hat"};
    // Empty query -> every index, in order.
    const std::vector<int> all = filterNameIndices(names, "");
    CHECK(all.size() == 4 && all[0] == 0 && all[3] == 3);
    // "kick" -> indices 0 and 2, order preserved.
    const std::vector<int> kicks = filterNameIndices(names, "kick");
    CHECK(kicks.size() == 2 && kicks[0] == 0 && kicks[1] == 2);
    // No match -> empty.
    CHECK(filterNameIndices(names, "zzz").empty());
}

// --- The Browse-modal regions (Q-W2v hoist, T2-06) ---------------------------

// Title / search / content / footer partition the window top-to-bottom without gaps;
// Back right-anchors in the title band; Cancel/Load flank the footer.
static void testBrowseModalPartition() {
    const BrowseModal m = computeBrowseModal(840, 620);
    CHECK(m.title.y == 0 && m.title.width == 840 && m.title.height == kTitleHeight);
    CHECK(m.back.right() == 840 - kPad && m.back.bottom() <= m.title.bottom());
    CHECK(m.search.y == m.title.bottom());
    CHECK(m.search.height == searchBoxRect(840).height);
    CHECK(m.content.y == m.search.bottom());
    CHECK(m.content.bottom() == 620 - 30);  // the footer band (kBrowseFooterH)
    CHECK(m.cancel.x == kPad);
    CHECK(m.confirm.right() == 840 - kPad);
    CHECK(m.cancel.y == m.content.bottom() + 3 && m.cancel.y == m.confirm.y);
    // Degenerate short window: the footer clamps below the search box (no inversion).
    const BrowseModal s = computeBrowseModal(840, 40);
    CHECK(s.content.bottom() >= s.content.y);
}

int main() {
    testContentHeight();
    testMaxOffsetFitsAndOverflows();
    testClamp();
    testVisibleRangeTop();
    testVisibleRangeScrolled();
    testVisibleRangeEmptyWhenNoCards();
    testScrolledCellShiftsUp();
    testThumbEmptyWhenFits();
    testThumbProportionalAndClamped();
    testThumbDragIsInverse();
    testSearchBoxRect();
    testNameMatch();
    testFilterIndices();
    testBrowseModalPartition();

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