// Standalone tests for reasampler::card_drag — no REAPER, no test framework. Asserts the
// L7 in-grid reorder drag decision logic + sparse-aware slot layout/hit-test.
//
// Covers: gesture precedence (no-drag/empty -> None; leave-client -> OsDragOut wins first;
// other-bank -> Move/Copy on Ctrl; same-bank grid empty vs occupied+no-mod -> Reorder;
// same-bank occupied+Alt -> Replace; Alt over EMPTY slot -> Reorder not Replace; dead space
// -> None); cursor-cue mapping (incl. Replace only for Replace); slot rects include empties
// (gap layout), dense layout matches a plain grid, slot hit-test returns slot index + miss.

#include "../src/core/ui/card_drag.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 200x200 client at origin. A pointer at (10,10) is inside; (-5,10) / (250,10) are outside.
static const PanelClientRect kClient{0, 0, 200, 200};
static const DragState kLiveDrag{/*dragging=*/true, /*hasArmedSamples=*/true};

static DragModifiers mods(DropRegion region, int slot, bool occupied, bool ctrl, bool alt) {
    DragModifiers m;
    m.region = region;
    m.targetSlot = slot;
    m.slotOccupied = occupied;
    m.ctrl = ctrl;
    m.alt = alt;
    return m;
}

// --- Gesture: guard cases ----------------------------------------------------

static void testNoDragIsNone() {
    const DragState idle{/*dragging=*/false, /*hasArmedSamples=*/true};
    CHECK(decideCardGesture(10, 10, kClient, idle,
                            mods(DropRegion::SameBankGrid, 0, true, false, false)) ==
          CardGesture::None);
}

static void testEmptyPayloadIsNone() {
    const DragState noSamples{/*dragging=*/true, /*hasArmedSamples=*/false};
    CHECK(decideCardGesture(10, 10, kClient, noSamples,
                            mods(DropRegion::SameBankGrid, 0, true, false, false)) ==
          CardGesture::None);
}

// --- Gesture: precedence 1 — leave client wins first -------------------------

static void testLeaveClientIsOsDragOut() {
    // Pointer outside the client -> OsDragOut EVEN when the region verdict says same-bank
    // and Alt is held (leave-client wins first — invariant #4 boundary).
    CHECK(decideCardGesture(-5, 10, kClient, kLiveDrag,
                            mods(DropRegion::SameBankGrid, 0, true, /*ctrl=*/true, /*alt=*/true)) ==
          CardGesture::OsDragOut);
    CHECK(decideCardGesture(250, 10, kClient, kLiveDrag,
                            mods(DropRegion::OtherBankOrTab, -1, false, false, false)) ==
          CardGesture::OsDragOut);
}

// --- Gesture: precedence 2 — other bank/tab = move/copy ----------------------

static void testOtherBankIsMove() {
    CHECK(decideCardGesture(10, 10, kClient, kLiveDrag,
                            mods(DropRegion::OtherBankOrTab, -1, false, /*ctrl=*/false, false)) ==
          CardGesture::Move);
}

static void testOtherBankCtrlIsCopy() {
    CHECK(decideCardGesture(10, 10, kClient, kLiveDrag,
                            mods(DropRegion::OtherBankOrTab, -1, false, /*ctrl=*/true, false)) ==
          CardGesture::Copy);
}

// --- Gesture: precedence 3 — same-bank grid reorder / replace ----------------

static void testSameBankEmptySlotIsReorder() {
    CHECK(decideCardGesture(10, 10, kClient, kLiveDrag,
                            mods(DropRegion::SameBankGrid, 3, /*occupied=*/false, false, false)) ==
          CardGesture::Reorder);
}

static void testSameBankOccupiedNoModIsReorder() {
    // Occupied + no modifier = insert-before-and-shift, which is still Reorder.
    CHECK(decideCardGesture(10, 10, kClient, kLiveDrag,
                            mods(DropRegion::SameBankGrid, 2, /*occupied=*/true, false, false)) ==
          CardGesture::Reorder);
}

static void testSameBankOccupiedAltIsReplace() {
    CHECK(decideCardGesture(10, 10, kClient, kLiveDrag,
                            mods(DropRegion::SameBankGrid, 2, /*occupied=*/true, false, /*alt=*/true)) ==
          CardGesture::Replace);
}

static void testAltOverEmptySlotIsReorderNotReplace() {
    // Alt over an EMPTY slot must NOT be Replace (replace needs an occupant).
    CHECK(decideCardGesture(10, 10, kClient, kLiveDrag,
                            mods(DropRegion::SameBankGrid, 5, /*occupied=*/false, false, /*alt=*/true)) ==
          CardGesture::Reorder);
}

static void testDeadSpaceIsNone() {
    CHECK(decideCardGesture(10, 10, kClient, kLiveDrag,
                            mods(DropRegion::DeadSpace, -1, false, false, true)) ==
          CardGesture::None);
}

// --- Cursor cue mapping ------------------------------------------------------

static void testCursorCueMapping() {
    CHECK(cursorForGesture(CardGesture::None)      == CursorCue::Default);
    CHECK(cursorForGesture(CardGesture::OsDragOut) == CursorCue::OsDragOut);
    CHECK(cursorForGesture(CardGesture::Move)      == CursorCue::Move);
    CHECK(cursorForGesture(CardGesture::Copy)      == CursorCue::Copy);
    CHECK(cursorForGesture(CardGesture::Reorder)   == CursorCue::Reorder);
    CHECK(cursorForGesture(CardGesture::Replace)   == CursorCue::Replace);
}

static void testReplaceCueOnlyFromReplace() {
    // The Replace cue is produced by NO gesture other than Replace (which itself requires
    // Alt-over-occupied) — the "replace cursor only while Alt over occupied" guarantee.
    CHECK(cursorForGesture(CardGesture::Reorder) != CursorCue::Replace);
    CHECK(cursorForGesture(CardGesture::Move)    != CursorCue::Replace);
    CHECK(cursorForGesture(CardGesture::Copy)    != CursorCue::Replace);
    CHECK(cursorForGesture(CardGesture::None)    != CursorCue::Replace);
}

// --- Sparse slot layout + hit-test -------------------------------------------

// Spec: cell 100x40, gap 10. In a 340-wide panel: usable = 340-10 = 330; cell+gap = 110;
// cols = 330/110 = 3.
static const GridSpec kSpec{/*cellWidth=*/100, /*cellHeight=*/40, /*gap=*/10};

static void testSlotRectsIncludeEmpties() {
    // maxSlot = 4 -> 5 rects, slots 0..4, three per row.
    const std::vector<SlotCellRect> r = computeSlotRects(4, 340, kSpec);
    CHECK(r.size() == 5);
    CHECK(r[0].slot == 0);
    CHECK(r[4].slot == 4);
    // Slot 0: x = gap = 10, y = gap = 10.
    CHECK((r[0] == SlotCellRect{0, 10, 10, 100, 40}));
    // Slot 3 wraps to row 1, col 0: y = gap + 1*(40+10) = 60.
    CHECK((r[3] == SlotCellRect{3, 10, 60, 100, 40}));
}

static void testSlotRectsEmptyWhenNoOccupied() {
    CHECK(computeSlotRects(-1, 340, kSpec).empty());
}

static void testSlotRectsDenseMatchesGrid() {
    // A dense slot layout (slots 0..N-1) lays out identically to bank_grid's item tiling.
    const std::vector<SlotCellRect> s = computeSlotRects(2, 340, kSpec);
    const std::vector<CellRect> g = computeCellRects(3, 340, kSpec);
    CHECK(s.size() == g.size());
    for (std::size_t i = 0; i < s.size(); ++i) {
        CHECK(s[i].x == g[i].x && s[i].y == g[i].y);
        CHECK(s[i].width == g[i].width && s[i].height == g[i].height);
    }
}

static void testHitTestSlotReturnsSlotIndex() {
    const std::vector<SlotCellRect> r = computeSlotRects(4, 340, kSpec);
    // A point inside slot 3's rect (x=10,y=60) returns slot 3, not vector index 3 (they
    // coincide here, but the point maps by geometry).
    CHECK(hitTestSlot(15, 65, r) == 3);
    // Inside slot 1 (x = gap + 1*(100+10) = 120).
    CHECK(hitTestSlot(125, 15, r) == 1);
}

static void testHitTestSlotMiss() {
    const std::vector<SlotCellRect> r = computeSlotRects(4, 340, kSpec);
    CHECK(hitTestSlot(0, 0, r) == -1);       // top-left margin (gap) is a miss
    CHECK(hitTestSlot(115, 15, r) == -1);   // inter-cell gap between slot 0 (ends x=110) and slot 1 (starts x=120)
}

// --- computeSlotRectsForDrop — beyond-extent trailing row ---------------------
// Spec: kSpec, 340px wide, 3 cols. maxSlot=2 -> render rects for 0..2.
// Drop rects extend one trailing row: slots 0..2 (existing) + 3..5 (trailing).

static void testDropRectsExtendOneTrailingRow() {
    // computeSlotRects(2, ...) gives 3 rects (0..2). Drop rects should give 6 (0..5).
    const std::vector<SlotCellRect> render = computeSlotRects(2, 340, kSpec);
    const std::vector<SlotCellRect> drop   = computeSlotRectsForDrop(2, 340, kSpec);
    CHECK(render.size() == 3);
    CHECK(drop.size() == 6);
    // First three match (existing slots preserved).
    for (std::size_t i = 0; i < 3; ++i) {
        CHECK(drop[i].slot == render[i].slot);
        CHECK(drop[i].x == render[i].x && drop[i].y == render[i].y);
    }
    // Slot 3 starts the trailing row: row 1, col 0. y = gap + 1*(40+10) = 60.
    CHECK(drop[3].slot == 3);
    CHECK(drop[3].x == 10 && drop[3].y == 60);
}

static void testDropRectsEmptyBankHasTrailingRow() {
    // maxSlot=-1 (no occupied slots): drop rects start at slot 0 (one trailing row).
    const std::vector<SlotCellRect> drop = computeSlotRectsForDrop(-1, 340, kSpec);
    CHECK(drop.size() == 3);   // 3 cols = one trailing row
    CHECK(drop[0].slot == 0);
    CHECK(drop[1].slot == 1);
    CHECK(drop[2].slot == 2);
}

static void testBeyondExtentResolvesToTrailingSlot() {
    // A pointer placed in the trailing row (below the last occupied card row) resolves
    // to a valid slot via the drop rects, but returns -1 via the render rects — this is
    // the exact gap the fix closes.
    const std::vector<SlotCellRect> render = computeSlotRects(2, 340, kSpec);
    const std::vector<SlotCellRect> drop   = computeSlotRectsForDrop(2, 340, kSpec);
    // Slot 3 rect: row 1, col 0. A point at (15, 65) is inside it (y=60..99, x=10..109).
    CHECK(hitTestSlot(15, 65, render) == -1);   // render rects: miss — bug confirmed
    CHECK(hitTestSlot(15, 65, drop)   ==  3);   // drop rects: hits trailing slot 3 — fix
}

static void testGapPixelStillMissesInDropRects() {
    // Inter-cell pixel gaps should still be misses even in the drop rects.
    const std::vector<SlotCellRect> drop = computeSlotRectsForDrop(4, 340, kSpec);
    // x=115 is in the gap between slot 0 (x=10..109) and slot 1 (x=120..219).
    CHECK(hitTestSlot(115, 15, drop) == -1);
    // Top-left margin (before any cell) is still a miss.
    CHECK(hitTestSlot(0, 0, drop) == -1);
}

static void testEmptySlotWithinExtentHitsViaRenderRects() {
    // Slot 1 is empty (gap), slot 0 and slot 2 are "occupied" (just position — the
    // render rects cover ALL slots 0..maxSlot including empties). A pointer at slot 1's
    // cell center returns slot 1 from both render AND drop rects.
    const std::vector<SlotCellRect> render = computeSlotRects(2, 340, kSpec);
    const std::vector<SlotCellRect> drop   = computeSlotRectsForDrop(2, 340, kSpec);
    // Slot 1: x = gap + 1*(100+10) = 120, y = gap = 10. Center: (170, 30).
    CHECK(hitTestSlot(170, 30, render) == 1);  // works via render rects (existing)
    CHECK(hitTestSlot(170, 30, drop)   == 1);  // also works via drop rects
}

int main() {
    testNoDragIsNone();
    testEmptyPayloadIsNone();
    testLeaveClientIsOsDragOut();
    testOtherBankIsMove();
    testOtherBankCtrlIsCopy();
    testSameBankEmptySlotIsReorder();
    testSameBankOccupiedNoModIsReorder();
    testSameBankOccupiedAltIsReplace();
    testAltOverEmptySlotIsReorderNotReplace();
    testDeadSpaceIsNone();
    testCursorCueMapping();
    testReplaceCueOnlyFromReplace();
    testSlotRectsIncludeEmpties();
    testSlotRectsEmptyWhenNoOccupied();
    testSlotRectsDenseMatchesGrid();
    testHitTestSlotReturnsSlotIndex();
    testHitTestSlotMiss();
    testDropRectsExtendOneTrailingRow();
    testDropRectsEmptyBankHasTrailingRow();
    testBeyondExtentResolvesToTrailingSlot();
    testGapPixelStillMissesInDropRects();
    testEmptySlotWithinExtentHitsViaRenderRects();

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