// Standalone tests for reasampler::vst::param_slider — no VST3, no REAPER, no framework.
// Same fast assert loop as the sibling pure editor tests (capture_browser / keyboard_strip):
// assert the S12/S15/S16 control-surface layout, toggle-segment split + hit-test, slider
// value<->pixel mapping (round-trip + clamping + endpoints), and point->control routing.
//
// Covers: layoutControls stacking rows top-down with the label column + control column and the
// inter-row gap; an empty list / degenerate panel yielding nothing; toggleSegmentRect splitting
// a toggle into two tiling segments (last absorbs the remainder) + toggleSegmentHitTest;
// sliderTrackRect insetting a half-handle at each end; sliderHandleRect at value 0/0.5/1 and
// out-of-range clamping; valueAtPoint mapping x back to 0..1 (endpoints saturate) as the inverse
// of the handle position; controlAtPoint routing a point to the right control id (toggle whole
// area vs slider track) and MISSING in the label column, a row gap, and off-panel.

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

#include <cstdio>
#include <vector>

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)

static bool approx(double a, double b) { return (a - b) < 1e-9 && (b - a) < 1e-9; }

// --- layoutControls -----------------------------------------------------------

static void testLayoutStacksRows() {
    const Rect panel{0, 100, 300, 400};
    std::vector<ControlDesc> ctl{
        {1, ControlKind::Toggle},
        {2, ControlKind::Slider},
        {3, ControlKind::Slider},
    };
    const std::vector<ControlRow> rows = layoutControls(panel, ctl);
    CHECK(rows.size() == 3);
    // Row 0 sits at the panel top; each subsequent row is one row-height + gap below.
    CHECK(rows[0].row.top == 100);
    CHECK(rows[0].row.bottom == 100 + kControlRowHeight);
    CHECK(rows[1].row.top == rows[0].row.bottom + kControlRowGap);
    CHECK(rows[2].row.top == rows[1].row.bottom + kControlRowGap);
    // Ids + kinds carried through in order.
    CHECK(rows[0].id == 1 && rows[0].kind == ControlKind::Toggle);
    CHECK(rows[1].id == 2 && rows[1].kind == ControlKind::Slider);
    // Label column then control column, contiguous, spanning the panel width.
    CHECK(rows[0].label.left == panel.left);
    CHECK(rows[0].control.left == rows[0].label.right);
    CHECK(rows[0].control.right == panel.right);
    CHECK(rows[0].label.width() == kControlLabelWidth);
}

static void testLayoutEmptyAndDegenerate() {
    CHECK(layoutControls(Rect{0, 0, 300, 300}, {}).empty());
    std::vector<ControlDesc> ctl{{1, ControlKind::Slider}};
    CHECK(layoutControls(Rect{0, 0, 0, 0}, ctl).empty());
    CHECK(layoutControls(Rect{0, 0, 300, 0}, ctl).empty());
}

static void testLayoutNarrowPanelClampsLabel() {
    // A panel narrower than 2*labelWidth clamps the label column to half so a control column
    // survives.
    const Rect panel{0, 0, 100, 200};
    const std::vector<ControlRow> rows = layoutControls(panel, {{1, ControlKind::Slider}});
    CHECK(rows.size() == 1);
    CHECK(rows[0].label.width() <= panel.width() / 2 + 1);
    CHECK(rows[0].control.width() > 0);
}

// --- toggle -------------------------------------------------------------------

static void testToggleSegmentsTile() {
    const Rect control{100, 0, 300, 22};  // width 200
    const Rect s0 = toggleSegmentRect(control, 0);
    const Rect s1 = toggleSegmentRect(control, 1);
    CHECK(s0.left == 100 && s0.right == 200);
    CHECK(s1.left == 200 && s1.right == 300);  // last absorbs remainder -> reaches control.right
    // Out of range.
    CHECK(toggleSegmentRect(control, 2).width() == 0);
    CHECK(toggleSegmentRect(control, -1).width() == 0);
}

static void testToggleSegmentRemainderInLast() {
    const Rect control{0, 0, 201, 22};  // odd width -> seg0 = 100, seg1 = 101 (absorbs remainder)
    CHECK(toggleSegmentRect(control, 0).width() == 100);
    CHECK(toggleSegmentRect(control, 1).right == 201);
}

static void testToggleHitTest() {
    const Rect control{100, 0, 300, 22};
    CHECK(toggleSegmentHitTest(control, 150, 10) == 0);
    CHECK(toggleSegmentHitTest(control, 250, 10) == 1);
    CHECK(toggleSegmentHitTest(control, 50, 10) == -1);   // left of control
    CHECK(toggleSegmentHitTest(control, 150, 40) == -1);  // below control
}

// --- slider -------------------------------------------------------------------

static void testSliderTrackInsetsHalfHandle() {
    const Rect control{100, 0, 300, 22};
    const Rect track = sliderTrackRect(control);
    CHECK(track.left == control.left + kSliderHandleWidth / 2);
    CHECK(track.right == control.right - kSliderHandleWidth / 2);
    // A control too narrow for a handle yields an empty track.
    CHECK(sliderTrackRect(Rect{0, 0, kSliderHandleWidth - 1, 22}).width() == 0);
}

static void testSliderHandleAtEndpointsAndMid() {
    const Rect control{100, 0, 300, 22};
    const Rect track = sliderTrackRect(control);
    const int half = kSliderHandleWidth / 2;
    // Value 0 -> handle centered at track.left.
    const Rect h0 = sliderHandleRect(control, 0.0);
    CHECK(h0.left + half == track.left);
    // Value 1 -> handle centered at track.right.
    const Rect h1 = sliderHandleRect(control, 1.0);
    CHECK(h1.left + half == track.right);
    // Value 0.5 -> centered at the track middle.
    const Rect hm = sliderHandleRect(control, 0.5);
    CHECK(hm.left + half == track.left + track.width() / 2);
}

static void testSliderHandleClampsOutOfRange() {
    const Rect control{0, 0, 200, 22};
    CHECK(sliderHandleRect(control, -0.5).left == sliderHandleRect(control, 0.0).left);
    CHECK(sliderHandleRect(control, 5.0).left == sliderHandleRect(control, 1.0).left);
}

static void testValueAtPointEndpointsSaturate() {
    const Rect control{100, 0, 300, 22};
    const Rect track = sliderTrackRect(control);
    CHECK(approx(valueAtPoint(control, track.left - 20), 0.0));
    CHECK(approx(valueAtPoint(control, track.left), 0.0));
    CHECK(approx(valueAtPoint(control, track.right + 20), 1.0));
    CHECK(approx(valueAtPoint(control, track.right), 1.0));
}

static void testValueAtPointIsHandleInverse() {
    // Round-trip: a value -> handle center -> valueAtPoint recovers (within one pixel quantum).
    const Rect control{50, 0, 450, 22};  // wide track for pixel resolution
    const Rect track = sliderTrackRect(control);
    for (double v : {0.1, 0.25, 0.5, 0.75, 0.9}) {
        const Rect h = sliderHandleRect(control, v);
        const int centerX = h.left + kSliderHandleWidth / 2;
        const double back = valueAtPoint(control, centerX);
        CHECK(back >= v - 0.01 && back <= v + 0.01);
        CHECK(centerX >= track.left && centerX <= track.right);
    }
}

static void testValueAtPointDegenerateTrack() {
    CHECK(approx(valueAtPoint(Rect{0, 0, kSliderHandleWidth - 1, 22}, 5), 0.0));
}

// --- controlAtPoint routing ---------------------------------------------------

static void testControlAtPointRoutes() {
    const Rect panel{0, 0, 300, 400};
    std::vector<ControlDesc> ctl{
        {10, ControlKind::Toggle},
        {20, ControlKind::Slider},
    };
    const std::vector<ControlRow> rows = layoutControls(panel, ctl);
    // A point in the toggle's control area routes to the toggle id.
    const Rect tctl = rows[0].control;
    CHECK(controlAtPoint(rows, (tctl.left + tctl.right) / 2, (tctl.top + tctl.bottom) / 2) == 10);
    // A point on the slider's track routes to the slider id.
    const Rect strack = sliderTrackRect(rows[1].control);
    CHECK(controlAtPoint(rows, (strack.left + strack.right) / 2,
                         (strack.top + strack.bottom) / 2) == 20);
}

static void testControlAtPointMisses() {
    const Rect panel{0, 0, 300, 400};
    const std::vector<ControlRow> rows =
        layoutControls(panel, {{10, ControlKind::Toggle}, {20, ControlKind::Slider}});
    // The label column is not interactive.
    CHECK(controlAtPoint(rows, rows[0].label.left + 2, rows[0].label.top + 4) == -1);
    // The gap between rows is a miss.
    const int gapY = rows[0].row.bottom + kControlRowGap / 2;
    CHECK(controlAtPoint(rows, 200, gapY) == -1);
    // Off-panel below.
    CHECK(controlAtPoint(rows, 200, 5000) == -1);
}

int main() {
    testLayoutStacksRows();
    testLayoutEmptyAndDegenerate();
    testLayoutNarrowPanelClampsLabel();
    testToggleSegmentsTile();
    testToggleSegmentRemainderInLast();
    testToggleHitTest();
    testSliderTrackInsetsHalfHandle();
    testSliderHandleAtEndpointsAndMid();
    testSliderHandleClampsOutOfRange();
    testValueAtPointEndpointsSaturate();
    testValueAtPointIsHandleInverse();
    testValueAtPointDegenerateTrack();
    testControlAtPointRoutes();
    testControlAtPointMisses();

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