// Standalone tests for reasampler::instrument::ui::sample_chrome — no VST3, no REAPER, no
// test framework.
//
// Covers: the chrome band's two rows (toolbar over strip row, tiling the band exactly); the
// toolbar's fixed right-anchored run in order (Hold, bake, preview, velocity cell,
// Loop Off|On, Mono|Stereo, Browse) with the title taking the remainder and still holding its
// text at the editor's floor; the velocity and Hold knobs
// centred in their cells above their labels; the piano strip owning its whole row at every
// width; no rect on the
// toolbar overlapping any other; degenerate bands yielding no inverted rects; and the preview
// button's play-triangle glyph, which sits inside the button without changing its rect.

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

#include <cstdio>
#include <initializer_list>

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 real constant, not a copy: the shell passes kDeckKnobSize into chromeRects, and a
// hand-copied stand-in here had already drifted from it once.
static constexpr int kKnob = kDeckKnobSize;

static Rect chromeBand(int w = kEditorMinWidth, int h = kEditorMinHeight) {
    return computeSampleBands(w, h, 120).chrome;
}

// True when the two rects share at least one pixel.
static bool overlaps(const Rect& a, const Rect& b) {
    if (a.empty() || b.empty()) return false;
    return a.x < b.right() && b.x < a.right() && a.y < b.bottom() && b.y < a.bottom();
}

static void testRowsTileTheBandExactly() {
    const Rect band = chromeBand();
    const ChromeRects r = chromeRects(band, kKnob);
    CHECK(r.toolbar.y == band.y);
    CHECK(r.controls.y == r.toolbar.bottom());
    CHECK(r.controls.bottom() == band.bottom());
    CHECK(r.toolbar.x == band.x && r.toolbar.right() == band.right());
    CHECK(r.controls.x == band.x && r.controls.right() == band.right());
    // The toolbar has to be tall enough for the velocity knob cell it now carries.
    CHECK(r.toolbar.height >= kKnob);
    CHECK(r.controls.height > 0);
}

static void testToolbarRunIsOrderedRightToLeftWithoutOverlap() {
    const Rect band = chromeBand();
    const ChromeRects r = chromeRects(band, kKnob);
    // Rightmost first: Browse, stereo, mono, velocity cell, preview, title.
    CHECK(r.navBrowse.right() == band.right() - kPad);
    CHECK(r.navBrowse.width == kNavButtonWidth);
    CHECK(r.chanStereo.right() <= r.navBrowse.x);
    CHECK(r.chanMono.right() == r.chanStereo.x);
    CHECK(r.loopOn.right() <= r.chanMono.x);   // the enable is immediately left of Mono|Stereo
    CHECK(r.loopOff.right() == r.loopOn.x);    // its two segments abut, like the channel pair
    CHECK(r.loopOff.y == r.chanMono.y && r.loopOff.height == r.chanMono.height);
    CHECK(r.velCell.right() <= r.loopOff.x);
    CHECK(r.preview.right() <= r.velCell.x);
    CHECK(r.bake.right() <= r.preview.x);
    CHECK(r.bake.width == kBakeButtonWidth);
    CHECK(r.bake.y == r.preview.y);              // shares the run's button baseline
    CHECK(r.bake.height == r.preview.height);
    CHECK(r.holdCell.right() <= r.bake.x);
    CHECK(r.title.right() <= r.holdCell.x);      // the title yields to the whole run
    CHECK(r.title.x == band.x + kPad);
    CHECK(r.title.width > 0);

    // Every toolbar rect sits inside the toolbar row.
    const Rect items[] = {r.title, r.holdCell, r.bake, r.preview, r.velCell, r.loopOff,
                          r.loopOn, r.chanMono, r.chanStereo, r.navBrowse};
    for (const Rect& it : items) {
        CHECK(it.y >= r.toolbar.y && it.bottom() <= r.toolbar.bottom());
    }
}

static void testChromePartsNeverOverlapAtAnyWidth() {
    for (int w = 560; w <= 2400; w += 37) {
        const ChromeRects r = chromeRects(chromeBand(w, 620), kKnob);
        // The strip row and the toolbar row are disjoint by construction; the strip must
        // stay inside its own row, clear of every control.
        CHECK(!overlaps(r.toolbar, r.rootStrip));
        CHECK(r.rootStrip.y >= r.controls.y && r.rootStrip.bottom() <= r.controls.bottom());
        const Rect items[] = {r.holdCell, r.bake, r.preview, r.velCell, r.loopOff, r.loopOn,
                              r.chanMono, r.chanStereo, r.navBrowse};
        for (const Rect& it : items) {
            CHECK(!overlaps(it, r.rootStrip));
            CHECK(!overlaps(it, r.title));
        }
        // The run's own members are pairwise disjoint (the knob/label pairs are inside their
        // cells, so they are checked against the cell's neighbours, not the cell).
        constexpr int kRunCount = static_cast<int>(sizeof(items) / sizeof(items[0]));
        for (int i = 0; i < kRunCount; ++i) {
            for (int j = i + 1; j < kRunCount; ++j) CHECK(!overlaps(items[i], items[j]));
        }
    }
}

static void testStripOwnsItsWholeRowAndGrowsWithTheWindow() {
    const ChromeRects narrow = chromeRects(chromeBand(600, 620), kKnob);
    const ChromeRects wide = chromeRects(chromeBand(1000, 620), kKnob);
    for (const ChromeRects* r : {&narrow, &wide}) {
        // Inset only by the shared band pad — the same inset the waveform band beneath uses,
        // so the two line up. No control shortens it.
        CHECK(r->rootStrip.x == r->controls.x + kPad);
        CHECK(r->rootStrip.right() == r->controls.right() - kPad);
    }
    CHECK(wide.rootStrip.width == narrow.rootStrip.width + 400);
    // The fixed run keeps its size; every extra pixel goes to the title, not the run.
    CHECK(wide.preview.width == narrow.preview.width);
    CHECK(wide.velCell.width == narrow.velCell.width);
    CHECK(wide.title.width == narrow.title.width + 400);
}

static void testVelocityKnobIsCentredInItsCellAboveTheLabel() {
    const ChromeRects r = chromeRects(chromeBand(), kKnob);
    CHECK(r.velKnob.width == kKnob && r.velKnob.height == kKnob);
    CHECK(r.velKnob.y == r.velCell.y);
    const int leftGap = r.velKnob.x - r.velCell.x;
    const int rightGap = r.velCell.right() - r.velKnob.right();
    CHECK(leftGap == rightGap);  // horizontally centred in the cell
    CHECK(r.velLabel.y == r.velKnob.bottom());
    CHECK(r.velLabel.bottom() == r.velCell.bottom());
    CHECK(r.velLabel.height > 0);
    CHECK(r.velLabel.x == r.velCell.x && r.velLabel.right() == r.velCell.right());
}

// The Hold cell reserves its width whether or not the control is drawn, so the title slot
// beside it holds still (sample_chrome.h owns the reasoning). Its interior follows the
// velocity cell's grammar exactly.
static void testHoldCellIsReservedAndFollowsTheVelocityCellGrammar() {
    const ChromeRects r = chromeRects(chromeBand(), kKnob);
    CHECK(r.holdCell.width == r.velCell.width);
    CHECK(r.holdCell.y == r.velCell.y && r.holdCell.height == r.velCell.height);
    CHECK(r.holdKnob.width == kKnob && r.holdKnob.height == kKnob);
    CHECK(r.holdKnob.y == r.holdCell.y);
    CHECK(r.holdKnob.x - r.holdCell.x == r.holdCell.right() - r.holdKnob.right());
    CHECK(r.holdLabel.y == r.holdKnob.bottom());
    CHECK(r.holdLabel.bottom() == r.holdCell.bottom());
    CHECK(r.holdLabel.x == r.holdCell.x && r.holdLabel.right() == r.holdCell.right());

    // Widening the window leaves the reservation alone; the title takes the extra pixels.
    const ChromeRects wide = chromeRects(chromeBand(1000, 620), kKnob);
    CHECK(wide.holdCell.width == r.holdCell.width);
}

// The enable joins a RIGHT-ANCHORED run, so it is charged to the title slot and not to the
// window. kEditorMinWidth itself is guarded by test_deck_groups' derived-floor assertion — this
// is the other half of that contract: the title must still hold its text AT that floor, because
// the agreed remedy if it cannot is to narrow the enable's segments, never to move the floor.
static void testTheControlRunLeavesTheTitleReadableAtTheEditorFloor() {
    const ChromeRects r = chromeRects(chromeBand(), kKnob);
    // "ReaSampler 9000" (15 chars) plus a bracketed 20-char capture name, at an assumed 7 px/char
    // for the toolbar font: 38 * 7. A HEURISTIC, not a measured Segoe UI metric — no font-metric
    // measurement backs this number; it gates a phase-wide acceptance criterion regardless.
    constexpr int kTitleTextFloorPx = 266;
    CHECK(r.title.width >= kTitleTextFloorPx);
    // Nothing in the run reaches into the title's slot.
    CHECK(r.title.right() <= r.holdCell.x);
}

static void testDegenerateBandYieldsNoInvertedRects() {
    const ChromeRects empty = chromeRects(Rect{}, kKnob);
    CHECK(empty.toolbar.empty() && empty.controls.empty());
    CHECK(empty.rootStrip.empty() && empty.preview.empty());

    // A band far too narrow for the fixed run: everything collapses left, nothing inverts.
    const ChromeRects tiny = chromeRects(Rect::ltrb(0, 0, 40, kTitleHeight + kChromeRowHeight),
                                         kKnob);
    const Rect items[] = {tiny.title, tiny.holdCell, tiny.holdKnob, tiny.holdLabel,
                          tiny.bake, tiny.preview, tiny.velCell, tiny.velKnob, tiny.velLabel,
                          tiny.loopOff, tiny.loopOn, tiny.chanMono, tiny.chanStereo,
                          tiny.navBrowse, tiny.rootStrip};
    for (const Rect& it : items) CHECK(it.right() >= it.x && it.bottom() >= it.y);
}

static void testPreviewGlyphSitsInsideTheButtonAndPointsRight() {
    const ChromeRects r = chromeRects(chromeBand(), kKnob);
    const PreviewGlyph g = previewGlyph(r.preview);
    CHECK(!g.empty());
    // Wholly inside the button — the glyph replaces the label, it does not resize the target.
    CHECK(g.leftX >= r.preview.x && g.apexX <= r.preview.right());
    CHECK(g.topY >= r.preview.y && g.bottomY <= r.preview.bottom());
    // Right-pointing, and the apex on the button's own centre line so it reads as balanced.
    CHECK(g.apexX > g.leftX);
    CHECK(g.apexY == r.preview.y + r.preview.height / 2);
    CHECK(g.apexY - g.topY == g.bottomY - g.apexY);  // isosceles about the centre line
    // The button's rect is what the hit-test uses, and the glyph must not have moved it: the
    // preview button still sits at the run's fixed size, exactly where the text button did.
    CHECK(r.preview.width == 64 && r.preview.height == 24);
}

static void testPreviewGlyphDegradesRatherThanOverflowing() {
    // An unusably small button yields an empty glyph (draw nothing) rather than a triangle
    // spilling past the button edge.
    CHECK(previewGlyph(Rect{}).empty());
    CHECK(previewGlyph(Rect::ltrb(0, 0, 6, 6)).empty());
    // A tall, wide button caps the glyph instead of scaling without bound.
    const PreviewGlyph big = previewGlyph(Rect::ltrb(0, 0, 400, 200));
    CHECK(!big.empty());
    CHECK(big.bottomY - big.topY <= 14);
}

static void testToolbarOnlyBandStillPlacesTheNav() {
    // A band clipped to just the toolbar row: the strip row is empty but Browse still
    // resolves, so the empty state's call-to-action is never unreachable.
    const ChromeRects r = chromeRects(Rect::ltrb(0, 0, 400, kTitleHeight), kKnob);
    CHECK(r.toolbar.height == kTitleHeight);
    CHECK(r.controls.empty());
    CHECK(r.rootStrip.empty());
    CHECK(r.navBrowse.width == kNavButtonWidth);
}

int main() {
    testRowsTileTheBandExactly();
    testToolbarRunIsOrderedRightToLeftWithoutOverlap();
    testChromePartsNeverOverlapAtAnyWidth();
    testStripOwnsItsWholeRowAndGrowsWithTheWindow();
    testVelocityKnobIsCentredInItsCellAboveTheLabel();
    testHoldCellIsReservedAndFollowsTheVelocityCellGrammar();
    testTheControlRunLeavesTheTitleReadableAtTheEditorFloor();
    testDegenerateBandYieldsNoInvertedRects();
    testPreviewGlyphSitsInsideTheButtonAndPointsRight();
    testPreviewGlyphDegradesRatherThanOverflowing();
    testToolbarOnlyBandStillPlacesTheNav();

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