// Standalone tests for reasampler::instrument::ui::knob_deck — no VST3, no REAPER, no framework. Same fast
// assert loop as the sibling pure tests. Assert the r11 deck layout HARD:
//
//  * group width — caption row vs knob row max + padding; row-toggle and caption-toggle widths.
//  * layout — caption toggle right-anchored IN the caption row; cells fixed 48x58 left-to-right
//    inside the box; knob square centered; label band beneath; row toggle after the cells.
//  * wrap — deterministic whole-group wrap at a narrowing width (the r11 "PITCH ENV onto row
//    two at the 560 floor" behavior); the first group of a row always places; deckHeight
//    consistency with deckRowCount.
//  * hit-test — knob cell hit (whole cell), toggle segment 0/1 boundaries, blank (-1) cells
//    and fence padding miss, outside-deck miss.

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

#include <cstdio>
#include <vector>

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)

// A representative deck shaped like the shell's: AMP (5 cells + caption toggle), PITCH
// (1 cell + caption toggle), PITCH ENV (3 cells + caption toggle), VOICE (1 cell + caption
// toggle + row toggle), MASTER (1 cell, no toggle).
static std::vector<DeckGroupDesc> shellLikeDeck() {
    std::vector<DeckGroupDesc> g;
    g.push_back({0, 78, {100, 44}, {1, 2, 3, 4, 5}, {}});
    g.push_back({1, 38, {101, 48}, {6}, {}});
    g.push_back({2, 58, {102, 32}, {7, 8, 9}, {}});
    g.push_back({3, 38, {103, 40}, {10}, {104, 44}});
    g.push_back({4, 46, {}, {11}, {}});
    return g;
}

static void testGroupWidth() {
    // Knob row dominates: 5 cells (240) > caption row (78 + 4 + 88 = 170) -> 240 + 2*6.
    DeckGroupDesc amp{0, 78, {100, 44}, {1, 2, 3, 4, 5}, {}};
    CHECK(deckGroupWidth(amp) == 5 * kDeckCellW + 2 * kDeckGroupPadX);
    // Caption row dominates: 38 + 4 + 96 = 138 > 48 -> 138 + 12.
    DeckGroupDesc pitch{1, 38, {101, 48}, {6}, {}};
    CHECK(deckGroupWidth(pitch) == 38 + kDeckToggleGap + 2 * 48 + 2 * kDeckGroupPadX);
    // Row toggle counts into the knob row: 48 + 4 + 88 = 140 > caption 38+4+80=122.
    DeckGroupDesc voice{3, 38, {103, 40}, {10}, {104, 44}};
    CHECK(deckGroupWidth(voice) ==
          kDeckCellW + kDeckToggleGap + 2 * 44 + 2 * kDeckGroupPadX);
    // No toggles: max(caption, cells) + padding.
    DeckGroupDesc master{4, 46, {}, {11}, {}};
    CHECK(deckGroupWidth(master) == kDeckCellW + 2 * kDeckGroupPadX);
}

static void testShellDeckFitsOneRowAtDefaultWidth() {
    // The r11 default window is 840 with kPad=8 margins -> 824 available. The five shell
    // groups must fit ONE deck row there (the elastic hero keeps ~430px — the layout spec's
    // premise). Locks the constants against accidental growth.
    const auto deck = shellLikeDeck();
    int total = 0;
    for (const auto& g : deck) total += deckGroupWidth(g);
    total += (static_cast<int>(deck.size()) - 1) * kDeckGroupGap;
    CHECK(total <= 824);
    CHECK(deckRowCount(deck, 824) == 1);
    CHECK(deckHeight(deck, 824) == kDeckGroupH);
}

static void testWrapAtNarrowWidthIsDeterministic() {
    // At the 560x460 checkSizeConstraint floor (544 available) the deck wraps to TWO rows,
    // whole trailing groups only.
    const auto deck = shellLikeDeck();
    CHECK(deckRowCount(deck, 544) == 2);
    CHECK(deckHeight(deck, 544) == 2 * kDeckGroupH + kDeckRowGap);
    const DeckLayout dl = layoutDeck(deck, 8, 100, 544);
    CHECK(dl.rowCount == 2);
    CHECK(dl.height == deckHeight(deck, 544));
    CHECK(dl.groups.size() == 5);
    // Row membership: groups on row 1 share the first top; the wrapped groups sit one row
    // pitch lower and restart at the left margin.
    const int row0Top = dl.groups[0].box.y;
    const int row1Top = row0Top + kDeckGroupH + kDeckRowGap;
    CHECK(dl.groups[0].box.y == row0Top);
    CHECK(dl.groups[1].box.y == row0Top);
    bool sawWrap = false;
    for (std::size_t i = 1; i < dl.groups.size(); ++i) {
        if (dl.groups[i].box.y == row1Top && dl.groups[i - 1].box.y == row0Top) {
            CHECK(dl.groups[i].box.x == 8);  // wrapped row restarts at the left edge
            sawWrap = true;
        }
    }
    CHECK(sawWrap);
    // Every box stays within the available width (no group straddles the right edge).
    for (const auto& g : dl.groups) CHECK(g.box.right() <= 8 + 544);
}

static void testFirstGroupAlwaysPlaces() {
    // A group wider than the row still places (degenerate width) — exactly one row per group.
    const auto deck = shellLikeDeck();
    CHECK(deckRowCount(deck, 100) == 5);
    CHECK(deckHeight(deck, 100) == 5 * kDeckGroupH + 4 * kDeckRowGap);
}

static void testGroupInnerGeometry() {
    const auto deck = shellLikeDeck();
    const DeckLayout dl = layoutDeck(deck, 8, 50, 824);
    const DeckGroupLayout& amp = dl.groups[0];
    // Caption row at the top padding; caption toggle right-anchored inside the box.
    CHECK(amp.caption.y == amp.box.y + kDeckGroupPadY);
    CHECK(amp.captionToggle.id == 100);
    CHECK(amp.captionToggle.seg1.right() == amp.box.right() - kDeckGroupPadX);
    CHECK(amp.captionToggle.seg0.right() == amp.captionToggle.seg1.x);
    CHECK(amp.captionToggle.seg0.width == 44 && amp.captionToggle.seg1.width == 44);
    CHECK(amp.captionToggle.seg0.height == kDeckToggleH);
    // The caption text rect stops before the toggle.
    CHECK(amp.caption.right() <= amp.captionToggle.seg0.x);
    // Cells: five, fixed size, abutting, inside the box, below the caption row.
    CHECK(static_cast<int>(amp.cells.size()) == 5);
    for (std::size_t i = 0; i < amp.cells.size(); ++i) {
        const DeckCellLayout& c = amp.cells[i];
        CHECK(c.cell.width == kDeckCellW && c.cell.height == kDeckCellH);
        CHECK(c.cell.y == amp.box.y + kDeckGroupPadY + kDeckCaptionH + kDeckCaptionGap);
        if (i > 0) CHECK(c.cell.x == amp.cells[i - 1].cell.right());
        // Knob square centered horizontally, label band beneath it, both inside the cell.
        CHECK(c.knob.width == kDeckKnobSize && c.knob.height == kDeckKnobSize);
        CHECK(c.knob.x - c.cell.x == c.cell.right() - c.knob.right());
        CHECK(c.label.y >= c.knob.bottom());
        CHECK(c.label.bottom() <= c.cell.bottom());
    }
    // VOICE group's row toggle sits after its cell, vertically centered in the cell row.
    const DeckGroupLayout& voice = dl.groups[3];
    CHECK(voice.rowToggle.id == 104);
    CHECK(voice.rowToggle.seg0.x == voice.cells[0].cell.right() + kDeckToggleGap);
    CHECK(voice.rowToggle.seg0.height == kDeckToggleH);
    CHECK(voice.rowToggle.seg0.y > voice.cells[0].cell.y);
    // MASTER has no toggles.
    CHECK(dl.groups[4].captionToggle.id == -1);
    CHECK(dl.groups[4].rowToggle.id == -1);
}

static void testHitTest() {
    const auto deck = shellLikeDeck();
    const DeckLayout dl = layoutDeck(deck, 8, 50, 824);
    const DeckGroupLayout& amp = dl.groups[0];

    // Knob hit: anywhere in the cell (including the label band) resolves to the cell id.
    const DeckCellLayout& c0 = amp.cells[0];
    DeckHit h = hitTestDeck(dl, c0.cell.x + 1, c0.cell.y + 1);
    CHECK(h.kind == DeckHitKind::Knob && h.id == 1 && h.segment == -1);
    h = hitTestDeck(dl, c0.label.x + 2, c0.label.y + 2);
    CHECK(h.kind == DeckHitKind::Knob && h.id == 1);

    // Caption toggle segments 0/1 at their boundary: last px of seg0, first px of seg1.
    h = hitTestDeck(dl, amp.captionToggle.seg0.right() - 1, amp.captionToggle.seg0.y + 1);
    CHECK(h.kind == DeckHitKind::CaptionToggle && h.id == 100 && h.segment == 0);
    h = hitTestDeck(dl, amp.captionToggle.seg1.x, amp.captionToggle.seg1.y + 1);
    CHECK(h.kind == DeckHitKind::CaptionToggle && h.id == 100 && h.segment == 1);

    // Row toggle.
    const DeckGroupLayout& voice = dl.groups[3];
    h = hitTestDeck(dl, voice.rowToggle.seg1.x + 1, voice.rowToggle.seg1.y + 1);
    CHECK(h.kind == DeckHitKind::RowToggle && h.id == 104 && h.segment == 1);

    // A blank cell (id -1) misses even though its rect exists.
    std::vector<DeckGroupDesc> trig;
    trig.push_back({0, 78, {100, 44}, {20, 21, 22, -1, -1}, {}});
    const DeckLayout tl = layoutDeck(trig, 0, 0, 824);
    const DeckCellLayout& blank = tl.groups[0].cells[4];
    CHECK(blank.id == -1);
    h = hitTestDeck(tl, blank.cell.x + 5, blank.cell.y + 5);
    CHECK(h.kind == DeckHitKind::None);

    // The fence padding inside the box misses; outside the deck misses.
    h = hitTestDeck(dl, amp.box.x + 1, amp.box.bottom() - 1);
    CHECK(h.kind == DeckHitKind::None);
    h = hitTestDeck(dl, -50, -50);
    CHECK(h.kind == DeckHitKind::None);
}

static void testEmptyDeck() {
    const std::vector<DeckGroupDesc> none;
    CHECK(deckRowCount(none, 800) == 0);
    CHECK(deckHeight(none, 800) == 0);
    const DeckLayout dl = layoutDeck(none, 0, 0, 800);
    CHECK(dl.groups.empty() && dl.rowCount == 0 && dl.height == 0);
}

int main() {
    testGroupWidth();
    testShellDeckFitsOneRowAtDefaultWidth();
    testWrapAtNarrowWidthIsDeterministic();
    testFirstGroupAlwaysPlaces();
    testGroupInnerGeometry();
    testHitTest();
    testEmptyDeck();
    if (g_fail) {
        std::printf("%d FAILURE(S)\n", g_fail);
        return 1;
    }
    std::printf("knob_deck tests passed\n");
    return 0;
}
