// Standalone tests for reasampler::vst::editor_geometry — no VST3, no REAPER, no test
// framework. Same fast assert loop as the sibling pure tests (mode_switch et al.):
// assert the IPlugView LICE editor's layout math + hit-testing directly.
//
// Covers: contains() half-open convention + degenerate rects; layoutEditor regions on a
// normal view (title band + button + canvas), a tiny view (button clamped to canvas,
// never overhanging), and a zero view (all rects empty, no inversion); hitTest hitting
// the button, missing on the title/canvas, missing outside the surface, and boundary
// pixels; layout<->hit-test agreement (a click on the drawn button rect hits it).

#include "../src/vst/editor_geometry.h"

#include <cstdio>

using namespace reasampler::vst;

static int g_fail = 0;
#define CHECK(cond) do { if(!(cond)) { \
    std::printf("FAIL line %d: %s\n", __LINE__, #cond); ++g_fail; } } while(0)

// --- contains() ---------------------------------------------------------------

static void testContainsHalfOpen() {
    Rect r{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{10, 10, 10, 20}, 10, 15));  // zero width
    CHECK(!contains(Rect{10, 10, 20, 10}, 15, 10));  // zero height
    CHECK(!contains(Rect{20, 10, 10, 20}, 15, 15));  // inverted (right < left)
}

// --- layoutEditor: normal view ------------------------------------------------

static void testLayoutNormalView() {
    // A comfortable 400x260 view: title band spans the top full width; canvas is the
    // rest; button sits inside the canvas, inset by the margin.
    const EditorLayout L = layoutEditor(400, 260);

    CHECK(L.titleBar.left == 0 && L.titleBar.top == 0);
    CHECK(L.titleBar.right == 400);
    CHECK(L.titleBar.height() > 0 && L.titleBar.height() <= 260);

    // Canvas begins right below the title bar and reaches the bottom-right.
    CHECK(L.canvas.top == L.titleBar.bottom);
    CHECK(L.canvas.right == 400 && L.canvas.bottom == 260);

    // Button is inside the canvas (does not overhang any edge).
    CHECK(L.button.left >= L.canvas.left);
    CHECK(L.button.top >= L.canvas.top);
    CHECK(L.button.right <= L.canvas.right);
    CHECK(L.button.bottom <= L.canvas.bottom);
    CHECK(L.button.width() > 0 && L.button.height() > 0);
}

// --- layoutEditor: tiny view (clamping) ---------------------------------------

static void testLayoutTinyViewClampsButton() {
    // A view narrower/shorter than the button's natural size: the button must clamp to
    // the canvas and never produce an inverted or overhanging rect.
    const EditorLayout L = layoutEditor(40, 40);
    CHECK(L.button.right <= L.canvas.right);
    CHECK(L.button.bottom <= L.canvas.bottom);
    CHECK(L.button.right >= L.button.left);   // never inverted
    CHECK(L.button.bottom >= L.button.top);
    // Title bar clamps to the client height when the view is shorter than its height.
    CHECK(L.titleBar.bottom <= 40);
}

// --- layoutEditor: zero view (all empty, no inversion) ------------------------

static void testLayoutZeroView() {
    const EditorLayout L = layoutEditor(0, 0);
    CHECK(L.titleBar.width() <= 0 || L.titleBar.height() <= 0);
    CHECK(L.canvas.width() <= 0 || L.canvas.height() <= 0);
    // No rect is inverted.
    CHECK(L.button.right >= L.button.left);
    CHECK(L.button.bottom >= L.button.top);
    CHECK(L.canvas.right >= L.canvas.left);
    CHECK(L.canvas.bottom >= L.canvas.top);
    // A click anywhere on an empty layout hits nothing.
    CHECK(hitTest(L, 0, 0) == HitTarget::kNone);
    CHECK(hitTest(L, 5, 5) == HitTarget::kNone);
}

// --- hitTest ------------------------------------------------------------------

static void testHitTestButton() {
    const EditorLayout L = layoutEditor(400, 260);
    // Center of the button hits it.
    const int cx = (L.button.left + L.button.right) / 2;
    const int cy = (L.button.top + L.button.bottom) / 2;
    CHECK(hitTest(L, cx, cy) == HitTarget::kButton);
}

static void testHitTestMissesNonButton() {
    const EditorLayout L = layoutEditor(400, 260);
    // Title bar is inert in the spike.
    CHECK(hitTest(L, 200, L.titleBar.top + 1) == HitTarget::kNone);
    // Empty canvas away from the button.
    CHECK(hitTest(L, 380, 240) == HitTarget::kNone);
    // Outside the surface entirely.
    CHECK(hitTest(L, -5, -5) == HitTarget::kNone);
    CHECK(hitTest(L, 500, 500) == HitTarget::kNone);
}

static void testHitTestButtonBoundary() {
    const EditorLayout L = layoutEditor(400, 260);
    // Top-left corner of the button is inclusive; the right/bottom edges are excluded.
    CHECK(hitTest(L, L.button.left, L.button.top) == HitTarget::kButton);
    CHECK(hitTest(L, L.button.right, L.button.top) == HitTarget::kNone);
    CHECK(hitTest(L, L.button.left, L.button.bottom) == HitTarget::kNone);
}

// --- layout<->hit-test agreement ----------------------------------------------

// Every pixel inside the drawn button rect must hit the button; this is the
// load-bearing consistency invariant between what the shell draws and what it routes.
static void testHitTestMatchesDrawnButton() {
    const EditorLayout L = layoutEditor(320, 200);
    for (int y = L.button.top; y < L.button.bottom; ++y) {
        for (int x = L.button.left; x < L.button.right; ++x) {
            CHECK(hitTest(L, x, y) == HitTarget::kButton);
        }
    }
}

// --- sample list (S4) ---------------------------------------------------------

static void testSampleRowRectStacks() {
    const EditorLayout L = layoutEditor(400, 260);
    const Rect r0 = sampleRowRect(L, 0);
    const Rect r1 = sampleRowRect(L, 1);
    // Row 0 starts at the canvas top and spans its full width.
    CHECK(r0.top == L.canvas.top);
    CHECK(r0.left == L.canvas.left && r0.right == L.canvas.right);
    CHECK(r0.height() == kSampleRowHeight);
    // Row 1 sits directly below row 0 (no gap, no overlap).
    CHECK(r1.top == r0.bottom);
    CHECK(r1.height() == kSampleRowHeight);
    // A negative index is an empty rect.
    CHECK(sampleRowRect(L, -1).width() == 0 && sampleRowRect(L, -1).height() == 0);
}

static void testSampleRowHitTestMapsClickToRow() {
    const EditorLayout L = layoutEditor(400, 260);
    const int rows = 5;
    // A click in the vertical middle of row 2 resolves to index 2.
    const Rect r2 = sampleRowRect(L, 2);
    const int midY = (r2.top + r2.bottom) / 2;
    CHECK(sampleRowHitTest(L, rows, 200, midY) == 2);
    // Row 0's top-left corner hits row 0.
    const Rect r0 = sampleRowRect(L, 0);
    CHECK(sampleRowHitTest(L, rows, r0.left, r0.top) == 0);
}

static void testSampleRowHitTestMisses() {
    const EditorLayout L = layoutEditor(400, 260);
    const int rows = 3;
    // Above the first row (in the title bar) -> no row.
    CHECK(sampleRowHitTest(L, rows, 200, L.titleBar.top) == -1);
    // Below the last row -> no row.
    const Rect last = sampleRowRect(L, rows - 1);
    CHECK(sampleRowHitTest(L, rows, 200, last.bottom + 1) == -1);
    // Left of the canvas -> no row.
    CHECK(sampleRowHitTest(L, rows, L.canvas.left - 1, last.top) == -1);
    // Zero rows -> always -1.
    CHECK(sampleRowHitTest(L, 0, 200, L.canvas.top + 1) == -1);
    // At or below canvas.bottom -> always -1, even if rowCount would cover that y.
    // This guards paint<->hit-test agreement: sampleRowRect does not clamp to canvas,
    // so without this clip a row that extends past canvas.bottom would hit-test but
    // never be drawn (or vice versa).
    CHECK(sampleRowHitTest(L, rows, 200, L.canvas.bottom) == -1);
    // Use a large rowCount so index arithmetic would return a valid row without the
    // canvas.bottom guard — proving the guard fires independently of rowCount.
    const int bigRows = 1000;
    CHECK(sampleRowHitTest(L, bigRows, 200, L.canvas.bottom) == -1);
    CHECK(sampleRowHitTest(L, bigRows, 200, L.canvas.bottom + 5) == -1);
}

// The drawn-row <-> hit-test agreement: every pixel inside a row rect must resolve to
// that row's index (the same load-bearing invariant as the button).
static void testSampleRowHitTestMatchesDrawnRows() {
    const EditorLayout L = layoutEditor(320, 200);
    const int rows = 4;
    for (int i = 0; i < rows; ++i) {
        const Rect r = sampleRowRect(L, i);
        if (r.top >= L.canvas.bottom) break;  // clipped rows aren't clickable targets
        const int y = (r.top + r.bottom) / 2;
        if (y >= L.canvas.bottom) continue;
        CHECK(sampleRowHitTest(L, rows, r.left + 1, y) == i);
    }
}

// --- keymap editor (S5 Tier-1 UI) --------------------------------------------

static void testKeymapLayoutSplitsCanvas() {
    const KeymapEditorLayout L = layoutKeymapEditor(600, 300);
    // The left sample list and right zone panel partition the canvas with no overlap and
    // no gap: the list's right edge is the panel's left edge.
    CHECK(L.sampleList.left == L.base.canvas.left);
    CHECK(L.sampleList.right == L.zonePanel.left);
    CHECK(L.zonePanel.right == L.base.canvas.right);
    CHECK(L.sampleList.top == L.base.canvas.top);
    CHECK(L.zonePanel.top == L.base.canvas.top);
    CHECK(L.sampleList.bottom == L.base.canvas.bottom);
    CHECK(L.zonePanel.bottom == L.base.canvas.bottom);
    CHECK(L.sampleList.width() > 0 && L.zonePanel.width() > 0);
    // Add-Zone button caps the panel; zone rows stack below it.
    CHECK(L.addZoneButton.top == L.zonePanel.top);
    CHECK(L.addZoneButton.left == L.zonePanel.left && L.addZoneButton.right == L.zonePanel.right);
    CHECK(L.zoneRowArea.top == L.addZoneButton.bottom);
    CHECK(L.zoneRowArea.bottom == L.zonePanel.bottom);
}

static void checkNoInversion(const KeymapEditorLayout& L) {
    CHECK(L.sampleList.right >= L.sampleList.left);
    CHECK(L.zonePanel.right >= L.zonePanel.left);
    CHECK(L.addZoneButton.right >= L.addZoneButton.left);
    CHECK(L.addZoneButton.bottom >= L.addZoneButton.top);
    CHECK(L.zoneRowArea.right >= L.zoneRowArea.left);
    CHECK(L.zoneRowArea.bottom >= L.zoneRowArea.top);
    // Regions stay within the client area.
    CHECK(L.zonePanel.right <= L.base.canvas.right);
}

static void testKeymapLayoutTinyAndZeroNoInversion() {
    checkNoInversion(layoutKeymapEditor(30, 30));
    checkNoInversion(layoutKeymapEditor(0, 0));
    // A click anywhere on a zero layout hits no zone and no Add button.
    const KeymapEditorLayout Z = layoutKeymapEditor(0, 0);
    CHECK(zoneHitTest(Z, 3, 0, 0).zoneIndex == -1);
    CHECK(!addZoneHitTest(Z, 0, 0));
}

static void testKeymapSampleRowInLeftColumn() {
    const KeymapEditorLayout L = layoutKeymapEditor(600, 300);
    const Rect r0 = keymapSampleRowRect(L, 0);
    // Rows live in the LEFT column (not the full canvas width).
    CHECK(r0.left == L.sampleList.left && r0.right == L.sampleList.right);
    CHECK(r0.right < L.base.canvas.right);  // strictly left of the zone panel
    CHECK(r0.top == L.sampleList.top && r0.height() == kSampleRowHeight);
    // Hit-test maps a left-column click to the row and rejects a click in the zone panel.
    const int midY = (r0.top + r0.bottom) / 2;
    CHECK(keymapSampleRowHitTest(L, 3, r0.left + 2, midY) == 0);
    CHECK(keymapSampleRowHitTest(L, 3, L.zonePanel.left + 2, midY) == -1);
}

static void testAddZoneHitTest() {
    const KeymapEditorLayout L = layoutKeymapEditor(600, 300);
    const int cx = (L.addZoneButton.left + L.addZoneButton.right) / 2;
    const int cy = (L.addZoneButton.top + L.addZoneButton.bottom) / 2;
    CHECK(addZoneHitTest(L, cx, cy));
    // A click in the zone-row area below the button is NOT the Add button.
    CHECK(!addZoneHitTest(L, cx, L.zoneRowArea.top + 2));
    // A click in the left list is NOT the Add button.
    CHECK(!addZoneHitTest(L, L.sampleList.left + 2, L.sampleList.top + 2));
}

static void testZoneRowStacksAndSelects() {
    const KeymapEditorLayout L = layoutKeymapEditor(600, 300);
    const Rect z0 = zoneRowRect(L, 0);
    const Rect z1 = zoneRowRect(L, 1);
    CHECK(z0.top == L.zoneRowArea.top && z0.height() == kZoneRowHeight);
    CHECK(z1.top == z0.bottom);  // stacked, no gap
    CHECK(z0.left == L.zoneRowArea.left && z0.right == L.zoneRowArea.right);
    // A click on the LABEL area (left part of a zone row) selects the zone with no field.
    const int labelX = z0.left + 2;  // far left = label, not a control
    const int midY = (z0.top + z0.bottom) / 2;
    const ZoneHit h = zoneHitTest(L, 2, labelX, midY);
    CHECK(h.zoneIndex == 0 && h.field == ZoneField::kZoneNone);
}

static void testZoneRowControlsMapToFields() {
    const KeymapEditorLayout L = layoutKeymapEditor(600, 300);
    const Rect row = zoneRowRect(L, 0);
    const int midY = (row.top + row.bottom) / 2;
    // The seven controls occupy the rightmost 7*kZoneCtrlWidth px, left-to-right:
    // low-, low+, high-, high+, root-, root+, delete.
    const int block = row.right - 7 * kZoneCtrlWidth;
    const ZoneField expected[7] = {
        ZoneField::kLowDown, ZoneField::kLowUp,  ZoneField::kHighDown,
        ZoneField::kHighUp,  ZoneField::kRootDown, ZoneField::kRootUp,
        ZoneField::kDelete,
    };
    for (int s = 0; s < 7; ++s) {
        const int x = block + s * kZoneCtrlWidth + kZoneCtrlWidth / 2;  // center of slot s
        const ZoneHit h = zoneHitTest(L, 1, x, midY);
        CHECK(h.zoneIndex == 0);
        CHECK(h.zoneIndex == 0 && h.field == expected[s]);
    }
}

static void testZoneHitTestMisses() {
    const KeymapEditorLayout L = layoutKeymapEditor(600, 300);
    const Rect row = zoneRowRect(L, 0);
    const int midY = (row.top + row.bottom) / 2;
    // Zero zones -> always miss.
    CHECK(zoneHitTest(L, 0, row.left + 2, midY).zoneIndex == -1);
    // Below the last zone row -> miss.
    const Rect last = zoneRowRect(L, 2);
    CHECK(zoneHitTest(L, 3, row.left + 2, last.bottom + 1).zoneIndex == -1);
    // Left of the zone panel (in the sample list) -> miss.
    CHECK(zoneHitTest(L, 3, L.sampleList.left + 2, midY).zoneIndex == -1);
}

int main() {
    testContainsHalfOpen();
    testContainsDegenerate();
    testLayoutNormalView();
    testLayoutTinyViewClampsButton();
    testLayoutZeroView();
    testHitTestButton();
    testHitTestMissesNonButton();
    testHitTestButtonBoundary();
    testHitTestMatchesDrawnButton();
    testSampleRowRectStacks();
    testSampleRowHitTestMapsClickToRow();
    testSampleRowHitTestMisses();
    testSampleRowHitTestMatchesDrawnRows();
    testKeymapLayoutSplitsCanvas();
    testKeymapLayoutTinyAndZeroNoInversion();
    testKeymapSampleRowInLeftColumn();
    testAddZoneHitTest();
    testZoneRowStacksAndSelects();
    testZoneRowControlsMapToFields();
    testZoneHitTestMisses();

    if (g_fail == 0) std::printf("editor_geometry: all tests passed\n");
    return g_fail != 0;
}
