// Standalone tests for reasampler::footer_bar — no REAPER, no test framework. Same fast loop
// as the sibling pure tests (prune_button / mode_switch / action_bar): assert the L4 footer
// LEFT-group layout (toggle . count . Tail) and hit-testing directly.
//
// Covers (L4 brief §footer test cases):
//   * Layout: toggle + count + Tail placed left-to-right in the specified order without
//     overlap, starting at leftPad, vertically inset; the narrow toggle sits at the LEFT.
//   * The group stays clear of the reserved RIGHT region (prune + version) — nothing crosses
//     footer.right - rightReserve.
//   * Suppression: a too-narrow footer drops later affordances (Tail first, then count),
//     the toggle surviving longest; a degenerate footer yields an all-empty layout.
//   * countWidth <= 0 hides the count label and the Tail sits right after the toggle.
//   * Hit-test: Toggle / Tail returned for in-bounds points, None outside AND on the count
//     label (a passive readout, never a control); half-open bounds; suppressed box claims none.

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

#include <cstdio>

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 pixels are hand-checkable.
static FooterBarSpec roundSpec() {
    FooterBarSpec s;
    s.toggleWidth = 130;
    s.countWidth = 60;
    s.tailWidth = 130;
    s.gap = 6;
    s.leftPad = 8;
    s.verticalInset = 4;
    s.rightReserve = 168;
    return s;
}

// --- Layout: wide footer, all three placed, left-to-right ---------------------

// A wide footer fits all three. Verify order, gaps, no overlap, vertical inset, and that the
// Tail (last) stays clear of the reserved right region.
static void testWideFooterAllPlaced() {
    const FooterBarSpec s = roundSpec();
    // Need room for toggle(130)+gap(6)+count(60)+gap(6)+tail(130) = 332 from leftPad(8) => 340,
    // plus rightReserve(168). Width 700 is comfortable.
    FooterRect f{0, 100, 700, 26};
    const FooterBarLayout L = computeFooterBar(f, s);

    const int top = 100 + 4, boxH = 26 - 8;  // 104, 18
    CHECK((L.toggle == FooterBarRect{8, top, 130, boxH}));
    CHECK((L.count == FooterBarRect{8 + 130 + 6, top, 60, boxH}));       // x = 144
    CHECK((L.tail == FooterBarRect{8 + 130 + 6 + 60 + 6, top, 130, boxH}));  // x = 210

    // Order + no overlap: each starts at-or-after the previous end.
    CHECK(L.count.x >= L.toggle.x + L.toggle.width);
    CHECK(L.tail.x >= L.count.x + L.count.width);
    // The whole group clears the reserved right region.
    CHECK(L.tail.x + L.tail.width <= f.x + f.width - s.rightReserve);
}

// Origin offset honoured: the group anchors to THIS footer's left, not 0.
static void testOffsetFooterAnchorsLeft() {
    const FooterBarSpec s = roundSpec();
    FooterRect f{20, 200, 700, 26};
    const FooterBarLayout L = computeFooterBar(f, s);
    CHECK(L.toggle.x == 20 + 8);
    CHECK(!L.toggle.empty() && !L.count.empty() && !L.tail.empty());
}

// --- Suppression: narrowing drops later affordances first ---------------------

// A footer wide enough for toggle + count but not the Tail: Tail suppressed, toggle+count stay.
static void testTailSuppressedWhenTight() {
    const FooterBarSpec s = roundSpec();
    // Right bound = width - 168. Toggle ends at 8+130=138; count ends at 144+60=204.
    // Tail would start at 210 and end at 340; need 340 <= width-168 => width >= 508 for Tail.
    // count needs 204 <= width-168 => width >= 372. Pick width 480: count fits, Tail does not.
    FooterRect f{0, 0, 480, 26};
    const FooterBarLayout L = computeFooterBar(f, s);
    CHECK(!L.toggle.empty());
    CHECK(!L.count.empty());
    CHECK(L.tail.empty());
}

// Narrower still: only the toggle survives (count and Tail both drop). When the count does not
// fit, the Tail is offered the count's slot — but if the Tail also does not fit there, both are
// empty and only the toggle remains.
static void testOnlyToggleSurvives() {
    const FooterBarSpec s = roundSpec();
    // Toggle needs 138 <= width-168 => width >= 306. count needs width >= 372.
    // At the count's slot (x=144) the Tail (130) would end at 274; needs 274 <= width-168 =>
    // width >= 442. Pick width 330: toggle fits (306), count does not (372), Tail-at-count-slot
    // does not (442) -> only toggle.
    FooterRect f{0, 0, 330, 26};
    const FooterBarLayout L = computeFooterBar(f, s);
    CHECK(!L.toggle.empty());
    CHECK(L.count.empty());
    CHECK(L.tail.empty());
}

// When the count does NOT fit but the Tail WOULD fit at the count's slot, the Tail takes it
// (a passive label yields to the interactive button). Right after the toggle, no count gap.
static void testTailTakesCountSlotWhenCountDrops() {
    const FooterBarSpec s = roundSpec();
    // Want: count does not fit, Tail-at-count-slot fits. count fits at width >= 372.
    // Tail at count slot x=144, ends 274, fits at width >= 442. Choose width 420:
    // 372 > 420? no — count fits at 420. Need width in [442-eps? ] hmm: count fits iff
    // width>=372, Tail-at-slot fits iff width>=442. For "count drops but tail-at-slot fits"
    // we need width < 372 AND width >= 442 — impossible with countWidth==tailWidth region.
    // So shrink countWidth to make its slot further right than tail's need. Use a spec where
    // countWidth is large so it drops before a smaller Tail.
    FooterBarSpec s2 = s;
    s2.countWidth = 200;  // large passive label
    s2.tailWidth = 80;    // small button
    // toggle ends 138. count slot x = 144, count ends 344; fits iff 344 <= width-168 =>
    // width >= 512. Tail at count slot x=144 ends 224; fits iff width >= 392.
    // Choose width 420: count drops (needs 512), Tail-at-slot fits (needs 392).
    FooterRect f{0, 0, 420, 26};
    const FooterBarLayout L = computeFooterBar(f, s2);
    CHECK(!L.toggle.empty());
    CHECK(L.count.empty());
    CHECK(!L.tail.empty());
    // Tail sits at the count's slot (right after the toggle + gap), NOT past a phantom count.
    CHECK(L.tail.x == L.toggle.x + L.toggle.width + s2.gap);
}

// A degenerate footer yields an all-empty layout (nothing placed, no crash).
static void testDegenerateFooterEmpty() {
    const FooterBarSpec s = roundSpec();
    const FooterBarLayout a = computeFooterBar(FooterRect{0, 0, 0, 26}, s);
    CHECK(a.toggle.empty() && a.count.empty() && a.tail.empty());
    const FooterBarLayout b = computeFooterBar(FooterRect{0, 0, 700, 0}, s);
    CHECK(b.toggle.empty() && b.count.empty() && b.tail.empty());
    // Height <= 2*verticalInset -> boxH <= 0 -> all empty.
    const FooterBarLayout c = computeFooterBar(FooterRect{0, 0, 700, 8}, s);
    CHECK(c.toggle.empty() && c.count.empty() && c.tail.empty());
}

// A footer too narrow even for the toggle: all empty (toggle drops last, so nothing survives).
static void testTooNarrowForToggle() {
    const FooterBarSpec s = roundSpec();
    // Toggle fits iff 8+130 <= width-168 => width >= 306. Width 200 suppresses everything.
    const FooterBarLayout L = computeFooterBar(FooterRect{0, 0, 200, 26}, s);
    CHECK(L.toggle.empty() && L.count.empty() && L.tail.empty());
}

// countWidth <= 0 hides the count label and the Tail sits right after the toggle (no count gap).
static void testCountHiddenByZeroWidth() {
    FooterBarSpec s = roundSpec();
    s.countWidth = 0;
    FooterRect f{0, 0, 700, 26};
    const FooterBarLayout L = computeFooterBar(f, s);
    CHECK(!L.toggle.empty());
    CHECK(L.count.empty());
    CHECK(!L.tail.empty());
    CHECK(L.tail.x == L.toggle.x + L.toggle.width + s.gap);  // right after the toggle
}

// --- Hit-test -----------------------------------------------------------------

static void testHitToggleAndTail() {
    const FooterBarSpec s = roundSpec();
    FooterRect f{0, 100, 700, 26};
    const FooterBarLayout L = computeFooterBar(f, s);

    // Toggle interior.
    CHECK(hitTestFooterBar(L.toggle.x, L.toggle.y, L) == FooterHit::Toggle);
    CHECK(hitTestFooterBar(L.toggle.x + L.toggle.width - 1,
                           L.toggle.y + L.toggle.height - 1, L) == FooterHit::Toggle);
    // Tail interior.
    CHECK(hitTestFooterBar(L.tail.x + L.tail.width / 2,
                           L.tail.y + L.tail.height / 2, L) == FooterHit::Tail);
    // The count label is a passive readout — never a hit target.
    CHECK(hitTestFooterBar(L.count.x + L.count.width / 2,
                           L.count.y + L.count.height / 2, L) == FooterHit::None);
    // Between the toggle and the count (the gap) is a clean miss.
    CHECK(hitTestFooterBar(L.toggle.x + L.toggle.width, L.toggle.y, L) == FooterHit::None);
}

// Half-open bounds: far edges excluded; points outside every box miss.
static void testHitEdgesAndOutside() {
    const FooterBarSpec s = roundSpec();
    FooterRect f{0, 100, 700, 26};
    const FooterBarLayout L = computeFooterBar(f, s);
    CHECK(hitTestFooterBar(L.toggle.x - 1, L.toggle.y, L) == FooterHit::None);
    CHECK(hitTestFooterBar(L.tail.x + L.tail.width, L.tail.y, L) == FooterHit::None);   // right edge excl
    CHECK(hitTestFooterBar(L.toggle.x, L.toggle.y - 1, L) == FooterHit::None);          // above
    CHECK(hitTestFooterBar(L.toggle.x, L.toggle.y + L.toggle.height, L) == FooterHit::None);  // below excl
    // Far right (the prune/version region) is not this module's — a clean None here.
    CHECK(hitTestFooterBar(f.x + f.width - 10, L.toggle.y, L) == FooterHit::None);
}

// A suppressed box claims no point — a Tail hit-test where the Tail was dropped is None.
static void testSuppressedClaimsNothing() {
    const FooterBarSpec s = roundSpec();
    FooterRect f{0, 0, 480, 26};  // Tail suppressed (see testTailSuppressedWhenTight)
    const FooterBarLayout L = computeFooterBar(f, s);
    CHECK(L.tail.empty());
    // A point where the Tail WOULD have been claims nothing.
    CHECK(hitTestFooterBar(210 + 10, 104, L) != FooterHit::Tail);
}

// --- Resize sweep: no overlap or cross-into-reserved across a width range -----

static void testResizeSweepNoOverlap() {
    const FooterBarSpec s = roundSpec();
    const int rightBoundFor = -s.rightReserve;  // + width applied per iteration
    for (int w = 40; w <= 1200; w += 7) {
        FooterRect f{0, 0, w, 26};
        const FooterBarLayout L = computeFooterBar(f, s);
        const int rightBound = f.x + w + rightBoundFor;  // = w - rightReserve
        // Order + no overlap among placed boxes.
        if (!L.toggle.empty() && !L.count.empty())
            CHECK(L.count.x >= L.toggle.x + L.toggle.width);
        if (!L.count.empty() && !L.tail.empty())
            CHECK(L.tail.x >= L.count.x + L.count.width);
        if (!L.toggle.empty() && L.count.empty() && !L.tail.empty())
            CHECK(L.tail.x >= L.toggle.x + L.toggle.width);
        // Nothing placed crosses into the reserved right region.
        if (!L.toggle.empty()) CHECK(L.toggle.x + L.toggle.width <= rightBound);
        if (!L.count.empty())  CHECK(L.count.x + L.count.width <= rightBound);
        if (!L.tail.empty())   CHECK(L.tail.x + L.tail.width <= rightBound);
        // A later affordance is never placed while an earlier one is dropped (greedy order):
        // count/tail present implies toggle present.
        if (!L.count.empty() || !L.tail.empty()) CHECK(!L.toggle.empty());
    }
}

// --- Mode-segment enablement (the playback gate's visible half) ---------------

static void testModeSegmentsAreLiveWithTheTransportStopped() {
    CHECK(modeSegmentEnabled(/*isActiveSegment=*/false, /*transportRunning=*/false, /*routable=*/true));
    CHECK(modeSegmentEnabled(/*isActiveSegment=*/true, /*transportRunning=*/false, /*routable=*/true));
}

static void testInactiveSegmentGoesDeadWhileTheTransportRuns() {
    // The one that would fire a real switch — refused while playing/recording, so it
    // must read dead rather than invite a click that silently does nothing.
    CHECK(!modeSegmentEnabled(/*isActiveSegment=*/false, /*transportRunning=*/true, /*routable=*/true));
}

static void testActiveSegmentStaysLiveWhileTheTransportRuns() {
    // Clicking the mode you are already in is a reapply, which is never gated;
    // dimming it would read as "this mode is unavailable".
    CHECK(modeSegmentEnabled(/*isActiveSegment=*/true, /*transportRunning=*/true, /*routable=*/false));
}

static void testInactiveSegmentGoesDeadWhenUnroutable() {
    // A third registered mode with no seeded command id (the model is N-mode, the UI
    // ships two ids) must read dead, not live-but-silently-inert on click.
    CHECK(!modeSegmentEnabled(/*isActiveSegment=*/false, /*transportRunning=*/false, /*routable=*/false));
}

static void testActiveSegmentStaysLiveEvenWhenUnroutable() {
    // The active segment always fires a reapply through its own already-resolved id in
    // practice, but the predicate's active bypass does not consult routable either way.
    CHECK(modeSegmentEnabled(/*isActiveSegment=*/true, /*transportRunning=*/false, /*routable=*/false));
}

int main() {
    testWideFooterAllPlaced();
    testOffsetFooterAnchorsLeft();
    testTailSuppressedWhenTight();
    testOnlyToggleSurvives();
    testTailTakesCountSlotWhenCountDrops();
    testDegenerateFooterEmpty();
    testTooNarrowForToggle();
    testCountHiddenByZeroWidth();
    testHitToggleAndTail();
    testHitEdgesAndOutside();
    testSuppressedClaimsNothing();
    testResizeSweepNoOverlap();
    testModeSegmentsAreLiveWithTheTransportStopped();
    testInactiveSegmentGoesDeadWhileTheTransportRuns();
    testActiveSegmentStaysLiveWhileTheTransportRuns();
    testInactiveSegmentGoesDeadWhenUnroutable();
    testActiveSegmentStaysLiveEvenWhenUnroutable();

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