// Standalone tests for reasampler::instrument::ui::sample_bands — no VST3, no REAPER, no
// test framework. Same fast assert loop as the sibling pure tests.
//
// Covers: the shared Rect vocabulary (contains() half-open + degenerate rects); the
// three-band vertical inventory (chrome over waveform over decks, no overlap, no
// inversion) asserted as pure geometry with no paint call; the waveform band's two-lane
// floor and the bands-clip-rather-than-squeeze rule on a short window; the deck band's
// bottom anchor and its exact requested height; the same stack/anchor/degrade properties
// re-anchored AT the allocator's own kEditorMinWidth/kEditorMinHeight floor rather than only
// below it; and the lane split (mono = one full-band lane, stereo = two lanes with the seam
// gap between them).

#include "../src/core/instrument/ui/sample_bands.h"

#include <cstdio>

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)

// --- the shared Rect vocabulary ----------------------------------------------

static void testContainsHalfOpen() {
    Rect r = Rect::ltrb(10, 20, 50, 40);  // [10,50) x [20,40)
    CHECK(contains(r, 10, 20));     // top-left inclusive
    CHECK(contains(r, 49, 39));     // bottom-right exclusive edge, inside
    CHECK(!contains(r, 50, 30));    // right edge excluded
    CHECK(!contains(r, 30, 40));    // bottom edge excluded
    CHECK(!contains(r, 9, 30));     // left of rect
    CHECK(!contains(r, 30, 19));    // above rect
}

static void testContainsDegenerate() {
    CHECK(!contains(Rect::ltrb(10, 10, 10, 20), 10, 15));  // zero width
    CHECK(!contains(Rect::ltrb(10, 10, 20, 10), 15, 10));  // zero height
    CHECK(!contains(Rect::ltrb(20, 10, 10, 20), 15, 15));  // inverted (right < left)
}

// --- the vertical inventory ---------------------------------------------------

static void testBandsStackTopToBottomWithoutOverlap() {
    const SampleBands b = computeSampleBands(840, 620, 120);
    CHECK(b.chrome.y == 0);
    CHECK(b.chrome.height == kTitleHeight + kChromeRowHeight);
    // Strictly ordered, no overlap: each band starts at or after the previous one's bottom.
    CHECK(b.waveform.y >= b.chrome.bottom());
    CHECK(b.decks.y >= b.waveform.bottom());
    // No inversion anywhere.
    CHECK(b.chrome.height > 0 && b.waveform.height > 0 && b.decks.height > 0);
    CHECK(b.chrome.width > 0 && b.waveform.width > 0 && b.decks.width > 0);
}

static void testChromeSpansFullWidthAndLowerBandsAreInset() {
    const SampleBands b = computeSampleBands(840, 620, 120);
    CHECK(b.chrome.x == 0 && b.chrome.right() == 840);
    CHECK(b.waveform.x == kPad && b.waveform.right() == 840 - kPad);
    CHECK(b.decks.x == kPad && b.decks.right() == 840 - kPad);
}

static void testDeckBandIsBottomAnchoredAtItsRequestedHeight() {
    const int deckH = 96;
    const SampleBands b = computeSampleBands(840, 620, deckH);
    CHECK(b.decks.height == deckH);
    CHECK(b.decks.bottom() == 620 - kPad);  // bottom-anchored inside the pad
}

static void testWaveformAbsorbsSlackAsTheWindowGrows() {
    const SampleBands small = computeSampleBands(840, 620, 120);
    const SampleBands big = computeSampleBands(840, 900, 120);
    CHECK(big.waveform.height == small.waveform.height + 280);
    // The fixed bands do not grow with the window.
    CHECK(big.chrome.height == small.chrome.height);
    CHECK(big.decks.height == small.decks.height);
}

static void testWaveformNeverShrinksBelowTheTwoLaneFloor() {
    // A window far too short for chrome + two lanes + deck: the floor wins and the deck band
    // is pushed past the bottom (clipped) rather than squeezing the waveform.
    const SampleBands b = computeSampleBands(840, 160, 120);
    CHECK(b.waveform.height == kWaveformMinHeight);
    CHECK(b.decks.y >= b.waveform.bottom());
    CHECK(b.decks.bottom() > 160);  // deliberately clipped below the window
}

static void testTwoLaneFloorHoldsTwoUsableLanes() {
    // The floor is exactly what two minimum lanes plus their seam need — not an arbitrary
    // number, so a lane can never be allocated below its own minimum.
    CHECK(kWaveformMinHeight == 2 * kLaneMinHeight + kLaneGap);
    const SampleBands b = computeSampleBands(840, 160, 120);
    const WaveformLanes lanes = waveformLanes(b.waveform, LaneSplit::Stereo);
    CHECK(lanes.upper.height >= kLaneMinHeight);
    CHECK(lanes.lower.height >= kLaneMinHeight);
}

static void testDegenerateWindowYieldsNoInvertedRects() {
    const SampleBands z = computeSampleBands(0, 0, 0);
    CHECK(z.chrome.width == 0 && z.chrome.height == 0);
    CHECK(z.waveform.width <= 0 || z.waveform.height >= 0);
    CHECK(z.waveform.right() >= z.waveform.x);
    CHECK(z.decks.right() >= z.decks.x);
    const SampleBands tiny = computeSampleBands(20, 20, 4);
    CHECK(tiny.waveform.right() >= tiny.waveform.x);
    CHECK(tiny.decks.right() >= tiny.decks.x);
}

// --- the allocator at its own floor (kEditorMinWidth x kEditorMinHeight) -------

// The allocator's own client-area floor is a fact IT owns (kEditorMinWidth/kEditorMinHeight
// above); the fixtures above validate the general shape entirely below that floor (840x620,
// 840x160). These anchor the same properties AT the floor itself, so a floor move that broke
// the stack there would have nothing else in this file to catch it.

static void testBandsStackWithoutOverlapAtTheEditorFloor() {
    const SampleBands b = computeSampleBands(kEditorMinWidth, kEditorMinHeight, 120);
    CHECK(b.chrome.y == 0);
    CHECK(b.chrome.width == kEditorMinWidth);
    CHECK(b.chrome.height == kTitleHeight + kChromeRowHeight);
    CHECK(b.waveform.y >= b.chrome.bottom());
    CHECK(b.decks.y >= b.waveform.bottom());
    CHECK(b.waveform.x == kPad && b.waveform.right() == kEditorMinWidth - kPad);
    CHECK(b.decks.x == kPad && b.decks.right() == kEditorMinWidth - kPad);
}

static void testDeckBandIsBottomAnchoredAtTheEditorFloor() {
    constexpr int deckH = 120;
    const SampleBands b = computeSampleBands(kEditorMinWidth, kEditorMinHeight, deckH);
    CHECK(b.decks.height == deckH);
    CHECK(b.decks.bottom() == kEditorMinHeight - kPad);
}

// At the shipped two-row deck height (216px — what test_deck_groups.cpp pins the deck to by
// construction, at and above the floor width), the waveform gets exactly what the floor's own
// height leaves it: an equality, not a bound, so a floor-height change that quietly ate into
// the waveform's slack would fail here rather than only widen/narrow a `>=`.
static void testWaveformGetsExactlyTheFloorsRemainingHeightAtATwoRowDeck() {
    constexpr int twoRowDeckH = 216;
    const SampleBands b = computeSampleBands(kEditorMinWidth, kEditorMinHeight, twoRowDeckH);
    const int expected = kEditorMinHeight - kPad - twoRowDeckH - 2 * kBandGap -
                          (kTitleHeight + kChromeRowHeight);
    CHECK(b.waveform.height == expected);
    CHECK(b.waveform.height == 358);
    CHECK(b.waveform.height >= kWaveformMinHeight);
}

// The floor is not immune to the degrade path: a deck grown too tall for the floor's OWN
// height still hits the floor-wins rule instead of squeezing the waveform below its usable
// minimum — the sub-floor fixtures above cover the general rule; this is the same rule
// exercised at the allocator's own minimum width.
static void testAnOversizedDeckAtTheFloorStillDegradesGracefully() {
    const SampleBands b = computeSampleBands(kEditorMinWidth, kEditorMinHeight, 500);
    CHECK(b.waveform.height == kWaveformMinHeight);
    CHECK(b.decks.y >= b.waveform.bottom());
    CHECK(b.decks.bottom() > kEditorMinHeight);  // clipped below the floor's own window
}

// --- the waveform band's lanes ------------------------------------------------

static void testMonoUsesOneFullBandLane() {
    const Rect band = Rect::ltrb(8, 100, 832, 300);
    const WaveformLanes lanes = waveformLanes(band, LaneSplit::Single);
    CHECK(lanes.upper == band);
    CHECK(lanes.lower.empty());  // no redundant duplicate lane in mono
}

static void testStereoSplitsIntoTwoLanesWithTheSeamGap() {
    const Rect band = Rect::ltrb(8, 100, 832, 300);  // height 200
    const WaveformLanes lanes = waveformLanes(band, LaneSplit::Stereo);
    CHECK(lanes.upper.y == band.y);
    CHECK(lanes.lower.bottom() == band.bottom());
    // Full width each, seam exactly kLaneGap, no overlap.
    CHECK(lanes.upper.x == band.x && lanes.upper.right() == band.right());
    CHECK(lanes.lower.x == band.x && lanes.lower.right() == band.right());
    CHECK(lanes.lower.y - lanes.upper.bottom() == kLaneGap);
    CHECK(lanes.upper.height + lanes.lower.height + kLaneGap == band.height);
}

static void testStereoOddRemainderGoesToTheUpperLane() {
    const Rect band = Rect::ltrb(0, 0, 100, 201);  // usable 199 -> 100 / 99
    const WaveformLanes lanes = waveformLanes(band, LaneSplit::Stereo);
    CHECK(lanes.upper.height == 100);
    CHECK(lanes.lower.height == 99);
    CHECK(lanes.lower.bottom() == band.bottom());
}

static void testEmptyBandYieldsEmptyLanes() {
    const WaveformLanes lanes = waveformLanes(Rect{}, LaneSplit::Stereo);
    CHECK(lanes.upper.empty());
    CHECK(lanes.lower.empty());
}

int main() {
    testContainsHalfOpen();
    testContainsDegenerate();
    testBandsStackTopToBottomWithoutOverlap();
    testChromeSpansFullWidthAndLowerBandsAreInset();
    testDeckBandIsBottomAnchoredAtItsRequestedHeight();
    testWaveformAbsorbsSlackAsTheWindowGrows();
    testWaveformNeverShrinksBelowTheTwoLaneFloor();
    testTwoLaneFloorHoldsTwoUsableLanes();
    testDegenerateWindowYieldsNoInvertedRects();
    testBandsStackWithoutOverlapAtTheEditorFloor();
    testDeckBandIsBottomAnchoredAtTheEditorFloor();
    testWaveformGetsExactlyTheFloorsRemainingHeightAtATwoRowDeck();
    testAnOversizedDeckAtTheFloorStillDegradesGracefully();
    testMonoUsesOneFullBandLane();
    testStereoSplitsIntoTwoLanesWithTheSeamGap();
    testStereoOddRemainderGoesToTheUpperLane();
    testEmptyBandYieldsEmptyLanes();

    if (g_fail == 0) {
        std::printf("sample_bands: all tests passed\n");
        return 0;
    }
    std::printf("sample_bands: %d failure(s)\n", g_fail);
    return 1;
}
