// Standalone tests for reasampler::tab_strip — no REAPER, no test framework. Same
// fast loop as the sibling pure tests (mode_switch / bank_grid et al.): assert the
// named-banks tab-strip layout, overflow/scroll math, and hit-testing directly.
//
// Covers (B4 brief §unit-test the pure seam): no-overflow tiling (tabs fit, no
// chevrons, track == strip); overflow (chevrons reserved, track shrinks, maxScroll
// = run - track); scroll clamping to [0, maxScroll]; clipped visible rects (a
// partially-scrolled tab is clipped to the track, a fully-scrolled-out tab is
// omitted); scrolling to the end surfaces the last tab; hit-testing (tab hit,
// left/right chevron precedence at the ends, dead space between visible tabs,
// outside the band above/below/left/right, half-open boundary pixels).

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

#include <cstddef>
#include <cstdio>
#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)

// --- No overflow: tabs fit ---------------------------------------------------

// 3 tabs at 96px = 288 fit a 300-wide strip: no overflow, no chevrons, the whole
// strip is the track, maxScroll 0.
static void testFitsNoOverflow() {
    TabStripRect strip{0, 0, 300, 24};
    TabStripSpec spec{96, 20};
    TabStripLayout layout = computeTabStripLayout(strip, 3, spec, 0);
    CHECK(!layout.overflow);
    CHECK(!layout.leftChevron && !layout.rightChevron);
    CHECK(layout.trackX == 0);
    CHECK(layout.trackWidth == 300);
    CHECK(layout.maxScroll == 0);

    auto rects = computeTabRects(strip, 3, spec, 0);
    CHECK(rects.size() == 3);
    CHECK((rects[0] == TabRect{0, 0, 0, 96, 24}));
    CHECK((rects[1] == TabRect{1, 96, 0, 96, 24}));
    CHECK((rects[2] == TabRect{2, 192, 0, 96, 24}));
}

// A fitting strip ignores a stray non-zero scroll offset (maxScroll 0 clamps it).
static void testFitStripIgnoresScroll() {
    TabStripRect strip{0, 0, 300, 24};
    TabStripSpec spec{96, 20};
    auto rects = computeTabRects(strip, 3, spec, /*scrollOffset=*/500);
    CHECK(rects.size() == 3);
    CHECK(rects[0].x == 0);  // offset was clamped to 0
}

// --- Overflow: chevrons reserved, track shrinks ------------------------------

// 10 tabs at 96 = 960 overflow a 300-wide strip. Chevrons (20 each) are reserved,
// so the track is [20, 280) = 260 wide; maxScroll = 960 - 260 = 700.
static void testOverflowReservesChevrons() {
    TabStripRect strip{0, 0, 300, 24};
    TabStripSpec spec{96, 20};
    TabStripLayout layout = computeTabStripLayout(strip, 10, spec, 0);
    CHECK(layout.overflow);
    CHECK(layout.leftChevron && layout.rightChevron);
    CHECK(layout.trackX == 20);
    CHECK(layout.trackWidth == 260);
    CHECK(layout.maxScroll == 700);
}

// At scroll 0 the first tabs are visible from the track's left edge; a tab that
// straddles the right chevron is clipped to the track's right edge, and tabs fully
// past it are omitted.
static void testOverflowScrollZeroClipsRight() {
    TabStripRect strip{0, 0, 300, 24};
    TabStripSpec spec{96, 20};
    auto rects = computeTabRects(strip, 10, spec, 0);
    // Track is [20, 280). Tab 0 at [20,116), tab 1 [116,212), tab 2 [212,308) clipped
    // to [212,280). Tabs 3.. start past 280 -> omitted.
    CHECK(rects.size() == 3);
    CHECK((rects[0] == TabRect{0, 20, 0, 96, 24}));
    CHECK((rects[1] == TabRect{1, 116, 0, 96, 24}));
    CHECK((rects[2] == TabRect{2, 212, 0, 68, 24}));  // clipped at the track's right
}

// Scrolling to maxScroll surfaces the LAST tab flush against the track's right edge
// and drops the earliest tabs off the left. This is the property that makes overflow
// usable: every tab is reachable by scrolling.
static void testScrollToEndSurfacesLastTab() {
    TabStripRect strip{0, 0, 300, 24};
    TabStripSpec spec{96, 20};
    TabStripLayout layout = computeTabStripLayout(strip, 10, spec, 0);
    auto rects = computeTabRects(strip, 10, spec, layout.maxScroll);
    CHECK(!rects.empty());
    const TabRect& last = rects.back();
    CHECK(last.index == 9);                       // the last tab is visible
    CHECK(last.x + last.width == layout.trackX + layout.trackWidth);  // flush right (280)
}

// --- Scroll clamping ---------------------------------------------------------

static void testClampScroll() {
    TabStripRect strip{0, 0, 300, 24};
    TabStripSpec spec{96, 20};
    TabStripLayout layout = computeTabStripLayout(strip, 10, spec, 0);
    CHECK(clampTabScroll(-50, layout) == 0);
    CHECK(clampTabScroll(0, layout) == 0);
    CHECK(clampTabScroll(300, layout) == 300);
    CHECK(clampTabScroll(layout.maxScroll, layout) == layout.maxScroll);
    CHECK(clampTabScroll(layout.maxScroll + 999, layout) == layout.maxScroll);

    TabStripLayout fits = computeTabStripLayout(strip, 2, spec, 0);
    CHECK(clampTabScroll(123, fits) == 0);  // no overflow -> everything clamps to 0
}

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

// No overflow: a point in a tab returns that tab; the gap-free tiling means every
// x in the strip band lands on some tab.
static void testHitFitStrip() {
    TabStripRect strip{0, 0, 300, 24};
    TabStripSpec spec{96, 20};
    CHECK((hitTestTabStrip(10, 12, strip, 3, spec, 0) == TabHit{TabHitKind::Tab, 0}));
    CHECK((hitTestTabStrip(100, 12, strip, 3, spec, 0) == TabHit{TabHitKind::Tab, 1}));
    CHECK((hitTestTabStrip(250, 12, strip, 3, spec, 0) == TabHit{TabHitKind::Tab, 2}));
    // Outside the band: above, below, left, right all miss.
    CHECK((hitTestTabStrip(10, -1, strip, 3, spec, 0) == TabHit{TabHitKind::None, -1}));
    CHECK((hitTestTabStrip(10, 24, strip, 3, spec, 0) == TabHit{TabHitKind::None, -1}));
    CHECK((hitTestTabStrip(-1, 12, strip, 3, spec, 0) == TabHit{TabHitKind::None, -1}));
    CHECK((hitTestTabStrip(300, 12, strip, 3, spec, 0) == TabHit{TabHitKind::None, -1}));
}

// Overflow: the reserved chevron bands hit-test to the scroll affordances and take
// precedence over any tab that would otherwise sit there.
static void testHitChevrons() {
    TabStripRect strip{0, 0, 300, 24};
    TabStripSpec spec{96, 20};
    // Left chevron band [0,20).
    CHECK((hitTestTabStrip(5, 12, strip, 10, spec, 0) ==
           TabHit{TabHitKind::ScrollLeft, -1}));
    CHECK((hitTestTabStrip(19, 12, strip, 10, spec, 0) ==
           TabHit{TabHitKind::ScrollLeft, -1}));
    // Right chevron band [280,300).
    CHECK((hitTestTabStrip(280, 12, strip, 10, spec, 0) ==
           TabHit{TabHitKind::ScrollRight, -1}));
    CHECK((hitTestTabStrip(299, 12, strip, 10, spec, 0) ==
           TabHit{TabHitKind::ScrollRight, -1}));
    // Just inside the track (x=20) is the first tab, not the left chevron.
    CHECK((hitTestTabStrip(20, 12, strip, 10, spec, 0) == TabHit{TabHitKind::Tab, 0}));
}

// A hit in the track matches the drawn (clipped) rects; a point in track dead space
// (no visible tab under it) is None. With overflow at scroll 0 the visible tabs are
// 0,1,2 (2 clipped to [212,280)); everything in [20,280) is covered here, so we test
// dead space by scrolling so a tab boundary leaves no gap — instead assert the hit
// agrees with computeTabRects for a mid-scroll offset.
static void testHitMatchesRectsMidScroll() {
    TabStripRect strip{0, 0, 300, 24};
    TabStripSpec spec{96, 20};
    const int offset = 150;
    auto rects = computeTabRects(strip, 10, spec, offset);
    CHECK(!rects.empty());
    for (const TabRect& r : rects) {
        // A point at the rect's left edge and one just inside its right edge both
        // resolve to this tab (half-open bounds).
        CHECK((hitTestTabStrip(r.x, 12, strip, 10, spec, offset) ==
               TabHit{TabHitKind::Tab, r.index}));
        CHECK((hitTestTabStrip(r.x + r.width - 1, 12, strip, 10, spec, offset) ==
               TabHit{TabHitKind::Tab, r.index}));
    }
}

// --- Degenerate inputs -------------------------------------------------------

static void testDegenerate() {
    TabStripRect strip{0, 0, 300, 24};
    TabStripSpec spec{96, 20};
    CHECK(computeTabRects(strip, 0, spec, 0).empty());
    CHECK((hitTestTabStrip(10, 12, strip, 0, spec, 0) == TabHit{TabHitKind::None, -1}));

    TabStripRect empty{0, 0, 0, 24};
    CHECK(computeTabRects(empty, 3, spec, 0).empty());
    TabStripLayout layout = computeTabStripLayout(empty, 3, spec, 0);
    CHECK(!layout.overflow);
    CHECK(layout.maxScroll == 0);
}

int main() {
    testFitsNoOverflow();
    testFitStripIgnoresScroll();
    testOverflowReservesChevrons();
    testOverflowScrollZeroClipsRight();
    testScrollToEndSurfacesLastTab();
    testClampScroll();
    testHitFitStrip();
    testHitChevrons();
    testHitMatchesRectsMidScroll();
    testDegenerate();

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