// Standalone tests for reasampler::action_buttons — no REAPER, no test framework.
// Same fast loop as the sibling pure tests (mode_switch / tab_strip et al.): assert the
// action-button strip layout + hit-testing and the label-format logic directly.
//
// Covers (M11 brief §test cases):
//   * Layout: N buttons in a strip — all fit (exact equal tiling), overflow on a narrow
//     panel (only the fitting count laid out, rest hidden, never clipped), zero-width edge.
//   * Hit-test: inside each button, outside the band, half-open boundary, the narrow-panel
//     overflow dead-zone, hit/layout agreement.
//   * Label format: bound ("name  binding"), unbound marker, empty / whitespace-only SDK
//     return -> unbound, over-long binding truncation.

#include "../src/action_buttons.h"

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

using namespace reasampler;

static int g_fail = 0;
#define CHECK(cond) do { if(!(cond)) { \
    std::printf("FAIL line %d: %s\n", __LINE__, #cond); ++g_fail; } } while(0)

// --- Layout: all fit ----------------------------------------------------------

// Strip 300 wide at origin (0, 40), height 24, 3 buttons at min width 80: all fit
// (300/80 = 3), tiled equally -> boundaries floor(i*300/3) = 0,100,200,300, widths 100.
static void testAllButtonsFitEqualTiling() {
    ButtonStripRect s{0, 40, 300, 24};
    ButtonFit fit = computeButtonFit(s, 3, 80);
    CHECK(fit.visibleCount == 3);
    CHECK(fit.hiddenCount == 0);

    auto rects = computeButtonRects(s, 3, 80);
    CHECK(rects.size() == 3);
    CHECK((rects[0] == ActionButtonRect{0, 0, 40, 100, 24}));
    CHECK((rects[1] == ActionButtonRect{1, 100, 40, 100, 24}));
    CHECK((rects[2] == ActionButtonRect{2, 200, 40, 100, 24}));
    // Abut exactly; last reaches strip right edge.
    CHECK(rects[1].x == rects[0].x + rects[0].width);
    CHECK(rects[2].x + rects[2].width == s.x + s.width);
}

// Uneven width absorbed at boundaries: 100 wide / 3 -> edges 0,33,66,100 -> 33,33,34.
static void testUnevenWidthTilesExactly() {
    ButtonStripRect s{7, 3, 100, 16};
    auto rects = computeButtonRects(s, 3, 30);  // 100/30 = 3 fit
    CHECK(rects.size() == 3);
    CHECK(rects[0].width == 33);
    CHECK(rects[1].width == 33);
    CHECK(rects[2].width == 34);
    CHECK(rects.front().x == s.x);
    CHECK(rects.back().x + rects.back().width == s.x + s.width);
}

// --- Layout: overflow on a narrow panel ---------------------------------------

// 5 buttons at min width 80 into a 200-wide strip: only 2 fit (200/80 = 2). The two
// visible buttons share the FULL strip (100 each — never clipped, never below min), and
// 3 are hidden (the overflow the shell degrades, not clipped garbage).
static void testOverflowHidesExcessNotClipped() {
    ButtonStripRect s{0, 0, 200, 24};
    ButtonFit fit = computeButtonFit(s, 5, 80);
    CHECK(fit.visibleCount == 2);
    CHECK(fit.hiddenCount == 3);

    auto rects = computeButtonRects(s, 5, 80);
    CHECK(rects.size() == 2);
    CHECK(rects[0].width == 100);   // >= min width 80, shares full strip
    CHECK(rects[1].width == 100);
    CHECK(rects[1].x + rects[1].width == s.x + s.width);
}

// A strip too narrow for even one min-width button lays out nothing (all hidden). The
// shell draws an empty strip rather than a sub-minimum clipped button.
static void testStripTooNarrowForAny() {
    ButtonStripRect s{0, 0, 50, 24};
    ButtonFit fit = computeButtonFit(s, 3, 80);
    CHECK(fit.visibleCount == 0);
    CHECK(fit.hiddenCount == 3);
    CHECK(computeButtonRects(s, 3, 80).empty());
}

// --- Layout: degenerate --------------------------------------------------------

static void testLayoutDegenerate() {
    CHECK(computeButtonRects(ButtonStripRect{0, 0, 300, 24}, 0, 80).empty());
    CHECK(computeButtonRects(ButtonStripRect{0, 0, 300, 24}, -2, 80).empty());
    CHECK(computeButtonRects(ButtonStripRect{0, 0, 0, 24}, 3, 80).empty());
    CHECK(computeButtonRects(ButtonStripRect{0, 0, -5, 24}, 3, 80).empty());
    CHECK(computeButtonRects(ButtonStripRect{0, 0, 300, 24}, 3, 0).empty());
    CHECK(computeButtonRects(ButtonStripRect{0, 0, 300, 24}, 3, -10).empty());

    ButtonFit fit = computeButtonFit(ButtonStripRect{0, 0, 0, 24}, 3, 80);
    CHECK(fit.visibleCount == 0 && fit.hiddenCount == 0);
}

// A single button fills the whole strip.
static void testSingleButtonFillsStrip() {
    ButtonStripRect s{5, 5, 120, 24};
    auto rects = computeButtonRects(s, 1, 80);
    CHECK(rects.size() == 1);
    CHECK((rects[0] == ActionButtonRect{0, 5, 5, 120, 24}));
}

// --- Hit-test: hits -----------------------------------------------------------

static void testHitTestHitsEachButton() {
    ButtonStripRect s{0, 40, 300, 24};  // 3 buttons, 100 wide each
    CHECK(hitTestButton(0, 40, s, 3, 80) == 0);     // top-left of button 0
    CHECK(hitTestButton(50, 51, s, 3, 80) == 0);    // middle of button 0
    CHECK(hitTestButton(99, 63, s, 3, 80) == 0);    // last pixel of button 0
    CHECK(hitTestButton(100, 50, s, 3, 80) == 1);   // first pixel of button 1
    CHECK(hitTestButton(299, 40, s, 3, 80) == 2);   // last column of button 2
}

// The boundary pixel belongs to exactly ONE button (half-open): px==100 starts button 1.
static void testHitTestBoundaryHalfOpen() {
    ButtonStripRect s{0, 0, 300, 24};
    CHECK(hitTestButton(99, 10, s, 3, 80) == 0);
    CHECK(hitTestButton(100, 10, s, 3, 80) == 1);
    CHECK(hitTestButton(199, 10, s, 3, 80) == 1);
    CHECK(hitTestButton(200, 10, s, 3, 80) == 2);
}

// --- Hit-test: misses ---------------------------------------------------------

static void testHitTestMissesOutsideBand() {
    ButtonStripRect s{10, 40, 200, 24};
    CHECK(hitTestButton(9, 50, s, 3, 60) == -1);    // left of strip
    CHECK(hitTestButton(210, 50, s, 3, 60) == -1);  // right edge (== x+width, excluded)
    CHECK(hitTestButton(50, 39, s, 3, 60) == -1);   // above the band
    CHECK(hitTestButton(50, 64, s, 3, 60) == -1);   // below the band
}

// On a narrow panel the visible buttons fill the whole strip, so there is no in-band
// dead-zone; but when buttonCount is 0 or the strip too narrow, every in-band point
// misses (the shell draws nothing and ignores the click).
static void testHitTestOverflowDeadZone() {
    ButtonStripRect s{0, 0, 50, 24};   // too narrow for any 80-min button
    CHECK(hitTestButton(25, 10, s, 3, 80) == -1);
    CHECK(hitTestButton(0, 0, s, 0, 80) == -1);     // no buttons
}

static void testHitTestDegenerate() {
    ButtonStripRect s{0, 0, 300, 24};
    CHECK(hitTestButton(50, 10, s, 0, 80) == -1);
    CHECK(hitTestButton(50, 10, s, -2, 80) == -1);
    CHECK(hitTestButton(50, 10, ButtonStripRect{0, 0, 0, 24}, 3, 80) == -1);
    CHECK(hitTestButton(50, 10, ButtonStripRect{0, 0, 300, 0}, 3, 80) == -1);
}

// Every point in the strip hit-tests to the button that DREW it (hit-test and layout
// agree — the load-bearing consistency invariant), across an awkward width and count.
static void testHitTestMatchesLayout() {
    ButtonStripRect s{4, 2, 173, 22};
    const int count = 4, minW = 40;   // 173/40 = 4 fit
    auto rects = computeButtonRects(s, count, minW);
    for (int px = s.x; px < s.x + s.width; ++px) {
        const int b = hitTestButton(px, s.y + 1, s, count, minW);
        CHECK(b >= 0);
        const ActionButtonRect& r = rects[static_cast<std::size_t>(b)];
        CHECK(px >= r.x && px < r.x + r.width);
    }
}

// --- Label format -------------------------------------------------------------

static void testLabelBound() {
    CHECK(formatButtonLabel("Capture Item", "Ctrl+Shift+C") ==
          "Capture Item  Ctrl+Shift+C");
    CHECK(formatButtonLabel("Insert", "F3") == "Insert  F3");
}

static void testLabelUnboundEmpty() {
    CHECK(formatButtonLabel("Capture Item", "") == "Capture Item (unbound)");
}

// Whitespace-only SDK returns (spaces, tabs) collapse to the unbound marker — the
// explicit blank-return handling the brief requires.
static void testLabelUnboundBlank() {
    CHECK(formatButtonLabel("Capture Track", "   ") == "Capture Track (unbound)");
    CHECK(formatButtonLabel("Insert", "\t") == "Insert (unbound)");
    CHECK(formatButtonLabel("Insert", " \t ") == "Insert (unbound)");
}

// Leading/trailing whitespace on a real binding is trimmed before formatting.
static void testLabelTrimsSurroundingBlanks() {
    CHECK(formatButtonLabel("Insert", "  F3  ") == "Insert  F3");
}

// An over-long binding is truncated to kMaxBindingChars-1 chars + "~" so the label stays
// bounded (a pathological multi-chord custom binding never blows the button budget).
static void testLabelTruncatesLongBinding() {
    const std::string longB(40, 'X');   // 40 > kMaxBindingChars (24)
    const std::string label = formatButtonLabel("Cancel", longB);
    // "Cancel  " (8) + 23 'X' + "~" (kMaxBindingChars total in the binding portion).
    const std::string expectedBinding =
        std::string(kMaxBindingChars - 1, 'X') + "~";
    CHECK(label == "Cancel  " + expectedBinding);
    CHECK(static_cast<int>(expectedBinding.size()) == kMaxBindingChars);
}

// A binding exactly at the limit is NOT truncated (boundary: <= kMaxBindingChars kept).
static void testLabelAtLimitNotTruncated() {
    const std::string atLimit(kMaxBindingChars, 'Y');
    CHECK(formatButtonLabel("X", atLimit) == "X  " + atLimit);
}

int main() {
    testAllButtonsFitEqualTiling();
    testUnevenWidthTilesExactly();
    testOverflowHidesExcessNotClipped();
    testStripTooNarrowForAny();
    testLayoutDegenerate();
    testSingleButtonFillsStrip();

    testHitTestHitsEachButton();
    testHitTestBoundaryHalfOpen();
    testHitTestMissesOutsideBand();
    testHitTestOverflowDeadZone();
    testHitTestDegenerate();
    testHitTestMatchesLayout();

    testLabelBound();
    testLabelUnboundEmpty();
    testLabelUnboundBlank();
    testLabelTrimsSurroundingBlanks();
    testLabelTruncatesLongBinding();
    testLabelAtLimitNotTruncated();

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