// Standalone tests for reasampler::instrument::ui::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 vs knob circle) and MISSING in the label column, a row gap, and
// off-panel. FA4 adds the radial KNOB: computeKnob inscribing the circle in its cell, the
// circular hit-test (boundary exclusive), the arc angle<->value mapping (min at startDeg, max at
// endDeg, linear midpoint; default = the 7->5 o'clock 300-degree sweep with 50% landing at 12
// o'clock), wrap-boundary + un-normalized arc inputs, the needle endpoint on the circle, and
// the vertical-drag delta->value map (up = increase) with clamping at 0/1.

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

#include <cmath>
#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)

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

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

static void testLayoutStacksRows() {
    const Rect panel = Rect::ltrb(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.y == 100);
    CHECK(rows[0].row.bottom() == 100 + kControlRowHeight);
    CHECK(rows[1].row.y == rows[0].row.bottom() + kControlRowGap);
    CHECK(rows[2].row.y == 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.x == panel.x);
    CHECK(rows[0].control.x == rows[0].label.right());
    CHECK(rows[0].control.right() == panel.right());
    CHECK(rows[0].label.width == kControlLabelWidth);
}

static void testLayoutEmptyAndDegenerate() {
    CHECK(layoutControls(Rect::ltrb(0, 0, 300, 300), {}).empty());
    std::vector<ControlDesc> ctl{{1, ControlKind::Slider}};
    CHECK(layoutControls(Rect::ltrb(0, 0, 0, 0), ctl).empty());
    CHECK(layoutControls(Rect::ltrb(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 = Rect::ltrb(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 = Rect::ltrb(100, 0, 300, 22);  // width 200
    const Rect s0 = toggleSegmentRect(control, 0);
    const Rect s1 = toggleSegmentRect(control, 1);
    CHECK(s0.x == 100 && s0.right() == 200);
    CHECK(s1.x == 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 = Rect::ltrb(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 = Rect::ltrb(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 = Rect::ltrb(100, 0, 300, 22);
    const Rect track = sliderTrackRect(control);
    CHECK(track.x == control.x + kSliderHandleWidth / 2);
    CHECK(track.right() == control.right() - kSliderHandleWidth / 2);
    // A control too narrow for a handle yields an empty track.
    CHECK(sliderTrackRect(Rect::ltrb(0, 0, kSliderHandleWidth - 1, 22)).width == 0);
}

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

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

static void testValueAtPointEndpointsSaturate() {
    const Rect control = Rect::ltrb(100, 0, 300, 22);
    const Rect track = sliderTrackRect(control);
    CHECK(approx(valueAtPoint(control, track.x - 20), 0.0));
    CHECK(approx(valueAtPoint(control, track.x), 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 = Rect::ltrb(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.x + kSliderHandleWidth / 2;
        const double back = valueAtPoint(control, centerX);
        CHECK(back >= v - 0.01 && back <= v + 0.01);
        CHECK(centerX >= track.x && centerX <= track.right());
    }
}

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

// --- knob (FA4) -----------------------------------------------------------------

static bool nearWithin(double a, double b, double tol) { return (a - b) < tol && (b - a) < tol; }

static void testKnobGeometryInscribesCell() {
    // A 44x44 cell at (100,0): center (122,22), radius 22.
    const KnobGeometry g = computeKnob(Rect::ltrb(100, 0, 144, 44));
    CHECK(approx(g.centerX, 122.0));
    CHECK(approx(g.centerY, 22.0));
    CHECK(approx(g.radius, 22.0));
    // A wide cell inscribes on the smaller (vertical) dimension.
    const KnobGeometry w = computeKnob(Rect::ltrb(0, 0, 200, 22));
    CHECK(approx(w.radius, 11.0));
    CHECK(approx(w.centerX, 100.0));
    // Degenerate cells yield radius 0.
    CHECK(computeKnob(Rect::ltrb(0, 0, 0, 22)).radius == 0.0);
    CHECK(computeKnob(Rect::ltrb(0, 0, 22, 0)).radius == 0.0);
}

static void testKnobHitTestCircle() {
    const KnobGeometry g = computeKnob(Rect::ltrb(100, 0, 144, 44));  // center (122,22), r 22
    CHECK(knobHitTest(g, 122, 22));        // center — always hits
    CHECK(!knobHitTest(g, 122 + 22, 22)); // exactly on the boundary — boundary exclusive
    CHECK(!knobHitTest(g, 122 + 22, 44)); // cell corner: inside the rect, outside the circle
    CHECK(!knobHitTest(g, 122, 45));       // just below the circle
    CHECK(knobHitTest(g, 122 + 21, 22));   // one pixel inside the boundary — hits
    CHECK(!knobHitTest(KnobGeometry{}, 0, 0));  // degenerate knob hits nothing
}

static void testKnobDefaultArcIsSevenToFiveOClock() {
    const KnobArc arc;  // default: 210 (7 o'clock) clockwise to 150 (5 o'clock)
    CHECK(approx(knobSweepDeg(arc), 300.0));
    CHECK(approx(knobValueAngleDeg(arc, 0.0), kKnobArcStartDeg));  // min at 7 o'clock (210°)
    CHECK(approx(knobValueAngleDeg(arc, 1.0), kKnobArcEndDeg));    // max at 5 o'clock (150°)
    // Midpoint: 210 + 150 = 360 -> normalized to 0 (12 o'clock, straight up).
    CHECK(approx(knobValueAngleDeg(arc, 0.5), 0.0));
    // Out-of-range values clamp to the arc ends.
    CHECK(approx(knobValueAngleDeg(arc, -0.5), kKnobArcStartDeg));
    CHECK(approx(knobValueAngleDeg(arc, 1.5), kKnobArcEndDeg));
}

static void testKnobArcIsParameterized() {
    // A custom non-wrapping arc: 3 o'clock down to 9 o'clock through 6.
    const KnobArc arc{90.0, 270.0};
    CHECK(approx(knobSweepDeg(arc), 180.0));
    CHECK(approx(knobValueAngleDeg(arc, 0.0), 90.0));
    CHECK(approx(knobValueAngleDeg(arc, 0.5), 180.0));
    CHECK(approx(knobValueAngleDeg(arc, 1.0), 270.0));
    // Equal start/end means a full-circle sweep (end at-or-behind start wraps +360).
    CHECK(approx(knobSweepDeg(KnobArc{0.0, 0.0}), 360.0));
}

static void testKnobArcWrapBoundary() {
    // A 1-degree arc starting at 180: end 181, sweep must be 1, NOT 361.
    const KnobArc tiny{180.0, 181.0};
    CHECK(approx(knobSweepDeg(tiny), 1.0));
    // Un-normalized inputs: start -180 (== 180) sweeping to end 120.
    // normDeg(-180) = 180; normDeg(120) = 120; sweep = 120-180 = -60 <= 0 -> 300.
    const KnobArc unnorm{-180.0, 120.0};
    CHECK(approx(knobSweepDeg(unnorm), 300.0));
    CHECK(approx(knobValueAngleDeg(unnorm, 0.0), 180.0));  // min at 6 o'clock
    CHECK(approx(knobValueAngleDeg(unnorm, 1.0), 120.0));  // max at 4 o'clock
}

static void testKnobNeedlePointOnCircle() {
    const KnobGeometry g = computeKnob(Rect::ltrb(100, 0, 144, 44));  // center (122,22), r 22
    // Default arc, value 0 -> 7 o'clock -> needle points down-left from center.
    const KnobPoint p7 = knobNeedlePoint(g, KnobArc{}, 0.0);
    // 210° clockwise from 12: sin(210°)=-0.5, cos(210°)=-√3/2 -> x = cx - r/2, y = cy + r*√3/2
    CHECK(nearWithin(p7.x, 122.0 + 22.0 * std::sin(210.0 * 3.14159265358979323846 / 180.0), 1e-6));
    CHECK(nearWithin(p7.y, 22.0  - 22.0 * std::cos(210.0 * 3.14159265358979323846 / 180.0), 1e-6));
    // A 12 o'clock needle points straight UP; 3 o'clock points RIGHT.
    const KnobPoint p12 = knobNeedlePoint(g, KnobArc{0.0, 180.0}, 0.0);
    CHECK(nearWithin(p12.x, 122.0, 1e-6) && nearWithin(p12.y, 0.0, 1e-6));
    const KnobPoint p3 = knobNeedlePoint(g, KnobArc{0.0, 180.0}, 0.5);
    CHECK(nearWithin(p3.x, 144.0, 1e-6) && nearWithin(p3.y, 22.0, 1e-6));
    // Every needle endpoint sits ON the circle.
    for (double v : {0.0, 0.25, 0.5, 0.75, 1.0}) {
        const KnobPoint p = knobNeedlePoint(g, KnobArc{}, v);
        const double dx = p.x - g.centerX, dy = p.y - g.centerY;
        CHECK(nearWithin(dx * dx + dy * dy, g.radius * g.radius, 1e-6));
    }
}

static void testKnobDragUpIncreases() {
    // Up (negative dy) increases, down decreases, scaled by the drag range.
    CHECK(approx(knobDragValue(0.5, -32, {}, 128), 0.75));
    CHECK(approx(knobDragValue(0.5, +32, {}, 128), 0.25));
    // A full-range upward drag from 0 lands exactly at 1.
    CHECK(approx(knobDragValue(0.0, -128, {}, 128), 1.0));
    // Default sensitivity applies when the range is omitted.
    CHECK(approx(knobDragValue(0.0, -kKnobDragRangePixels), 1.0));
}

static void testKnobDragClamps() {
    CHECK(approx(knobDragValue(0.9, -64, {}, 128), 1.0));  // over-drag up clamps at 1
    CHECK(approx(knobDragValue(0.1, +64, {}, 128), 0.0));  // over-drag down clamps at 0
    // The start value itself is clamped before the delta applies.
    CHECK(approx(knobDragValue(1.5, 0, {}, 128), 1.0));
    CHECK(approx(knobDragValue(-0.5, 0, {}, 128), 0.0));
    // A degenerate drag range yields the clamped start value.
    CHECK(approx(knobDragValue(0.7, -50, {}, 0), 0.7));
}

// Ctrl scales the drag rate; Shift+Ctrl is Shift, so the rate goes back to coarse. The snap
// itself is not this module's — only the rate is.
static void testCtrlScalesTheDragRateAndShiftOverridesIt() {
    const DragModifiers fine{false, true};
    const DragModifiers both{true, true};
    CHECK(approx(knobDragValue(0.5, -32, fine, 128), 0.5 + 0.25 * kFineDragScale));
    CHECK(approx(knobDragValue(0.5, -32, both, 128), 0.75));
    CHECK(approx(knobDragValue(0.5, -32, DragModifiers{true, false}, 128), 0.75));
    // Continuity across a transition is the CALLER's re-anchor, not this function's: at a
    // zero delta both rates agree, which is exactly the state a re-anchor establishes.
    CHECK(knobDragValue(0.42, 0, fine, 128) == knobDragValue(0.42, 0, {}, 128));
}

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

static void testControlAtPointRoutes() {
    const Rect panel = Rect::ltrb(0, 0, 300, 400);
    std::vector<ControlDesc> ctl{
        {10, ControlKind::Toggle},
        {20, ControlKind::Slider},
    };
    ctl.push_back({30, ControlKind::Knob});
    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.x + tctl.right()) / 2, (tctl.y + 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.x + strack.right()) / 2,
                         (strack.y + strack.bottom()) / 2) == 20);
    // A point at the knob's center routes to the knob id; the control-rect corner (outside
    // the circle) is a miss.
    const KnobGeometry kg = computeKnob(rows[2].control);
    CHECK(controlAtPoint(rows, static_cast<int>(kg.centerX), static_cast<int>(kg.centerY)) == 30);
    CHECK(controlAtPoint(rows, rows[2].control.x + 1, rows[2].control.y + 1) == -1);
}

static void testControlAtPointMisses() {
    const Rect panel = Rect::ltrb(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.x + 2, rows[0].label.y + 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();
    testKnobGeometryInscribesCell();
    testKnobHitTestCircle();
    testKnobDefaultArcIsSevenToFiveOClock();
    testKnobArcIsParameterized();
    testKnobArcWrapBoundary();
    testKnobNeedlePointOnCircle();
    testKnobDragUpIncreases();
    testKnobDragClamps();
    testCtrlScalesTheDragRateAndShiftOverridesIt();
    testControlAtPointRoutes();
    testControlAtPointMisses();

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