// Standalone tests for reasampler::instrument::ui::curve_popup — no VST3, no REAPER, no framework. Same
// fast assert loop as the sibling pure tests. Assert the r11 popup-sheet geometry at the size
// clamps (the spec's width clamp(60%, 360..520) / height clamp(55%, 260..380)), the centering,
// the title-row/close-button placement, the curve-box remainder, and the outside-sheet
// dismissal test.

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

#include "../src/core/instrument/engine/velocity_curve.h"

#include <algorithm>
#include <cstdio>

using namespace reasampler;
using namespace reasampler::instrument::ui;
using reasampler::instrument::engine::CurveDomain;
using reasampler::instrument::engine::VelocityCurve;

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 void testDefaultWindowMidClamp() {
    // 840x620: 60% = 504 (inside 360..520), 55% = 341 (inside 260..380).
    const CurvePopupLayout pl = computeCurvePopup(840, 620);
    CHECK(pl.sheet.width == 504);
    CHECK(pl.sheet.height == 341);
    // Centered (within the integer-division pixel).
    CHECK(pl.sheet.x == (840 - 504) / 2);
    CHECK(pl.sheet.y == (620 - 341) / 2);
}

static void testMinClamp() {
    // The 560x460 constraint floor: 60% = 336 -> clamps UP to 360; 55% = 253 -> up to 260.
    const CurvePopupLayout pl = computeCurvePopup(560, 460);
    CHECK(pl.sheet.width == kCurvePopupMinW);
    CHECK(pl.sheet.height == kCurvePopupMinH);
    CHECK(pl.sheet.x >= 0 && pl.sheet.right() <= 560);
    CHECK(pl.sheet.y >= 0 && pl.sheet.bottom() <= 460);
}

static void testMaxClamp() {
    // A large window: 60% of 1600 = 960 -> clamps DOWN to 520; 55% of 900 = 495 -> down to 380.
    const CurvePopupLayout pl = computeCurvePopup(1600, 900);
    CHECK(pl.sheet.width == kCurvePopupMaxW);
    CHECK(pl.sheet.height == kCurvePopupMaxH);
}

static void testDegenerateWindowNeverOverhangs() {
    // A window smaller than the min clamp: the sheet caps at the window dimension (defensive —
    // below checkSizeConstraint, but geometry must stay sane).
    const CurvePopupLayout pl = computeCurvePopup(300, 200);
    CHECK(pl.sheet.width == 300);
    CHECK(pl.sheet.height == 200);
    CHECK(pl.sheet.x == 0 && pl.sheet.y == 0);
}

static void testTitleRowAndCurveBox() {
    const CurvePopupLayout pl = computeCurvePopup(840, 620);
    // Close: 18x18, right-anchored inside the title row.
    CHECK(pl.close.width == kCurvePopupCloseSize && pl.close.height == kCurvePopupCloseSize);
    CHECK(pl.close.right() == pl.sheet.right() - kCurvePopupPad);
    CHECK(pl.close.y >= pl.sheet.y);
    CHECK(pl.close.bottom() <= pl.sheet.y + kCurvePopupTitleH);
    // Title text: left of the close button, in the title row.
    CHECK(pl.title.x == pl.sheet.x + kCurvePopupPad);
    CHECK(pl.title.right() <= pl.close.x);
    // Curve box: fills the remainder below the title row, inside the sheet margins.
    CHECK(pl.curveBox.y >= pl.sheet.y + kCurvePopupTitleH);
    CHECK(pl.curveBox.x == pl.sheet.x + kCurvePopupPad);
    CHECK(pl.curveBox.right() == pl.sheet.right() - kCurvePopupPad);
    CHECK(pl.curveBox.bottom() == pl.sheet.bottom() - kCurvePopupPad);
    CHECK(pl.curveBox.width > 0 && pl.curveBox.height > 0);
}

static void testOutsideSheetDismissTest() {
    const CurvePopupLayout pl = computeCurvePopup(840, 620);
    // On the wash: outside.
    CHECK(popupOutsideSheet(pl, 0, 0));
    CHECK(popupOutsideSheet(pl, pl.sheet.x - 1, pl.sheet.y + 10));
    CHECK(popupOutsideSheet(pl, pl.sheet.right(), pl.sheet.y + 10));  // half-open right edge
    // On the sheet (title row, curve box, padding): inside.
    CHECK(!popupOutsideSheet(pl, pl.sheet.x, pl.sheet.y));
    CHECK(!popupOutsideSheet(pl, pl.curveBox.x + 5, pl.curveBox.y + 5));
    CHECK(!popupOutsideSheet(pl, pl.sheet.right() - 1, pl.sheet.bottom() - 1));
}

// The sheet hosts all three curves, and its box is domain-agnostic: the SAME curveBox drives a
// unipolar and a bipolar editor, and only the curve's own y map differs. Asserted here, against
// the popup's real geometry, because that is what makes one popup code path legitimate.
static void testTheSameCurveBoxHostsBothDomains() {
    const CurvePopupLayout pl = computeCurvePopup(840, 620);
    // The editor never maps against the raw curveBox — shell/instrument/editor_internal.h's
    // curveBoxFromRect insets it first (kVelCurveInset == 14, mirrored here since this pure
    // target cannot link the shell). Applying it, not the raw rect, is what exercises the
    // actual box shape paint/hit-test/drag agree on.
    constexpr int kInset = 14;
    const VelocityCurve::Box box{pl.curveBox.x + kInset, pl.curveBox.y + kInset,
                                 std::max(0, pl.curveBox.width - 2 * kInset),
                                 std::max(0, pl.curveBox.height - 2 * kInset)};
    CHECK(box.width > 1 && box.height > 1);

    const VelocityCurve amp = VelocityCurve::flat();
    const VelocityCurve mod = VelocityCurve::zero();
    const int top = box.top;
    const int bottom = box.top + box.height - 1;

    // Both domains put their MAX on the top row and their MIN on the bottom row...
    CHECK(amp.pixelFromPoint(box, {0.0, 1.0}).y == top);
    CHECK(amp.pixelFromPoint(box, {0.0, 0.0}).y == bottom);
    CHECK(mod.pixelFromPoint(box, {0.0, 1.0}).y == top);
    CHECK(mod.pixelFromPoint(box, {0.0, -1.0}).y == bottom);
    // ...so value 0 is the FLOOR for the amp curve and the MIDLINE for a modulation curve. The
    // expected row replicates valueToY's own HALF-UP rounding on the inverted fraction (velocity_
    // curve.cpp) rather than a plain integer bisection of top/bottom: the two agree only when
    // box.height-1 is even, so a naive (top+bottom)/2 silently depends on this box's parity.
    const int midY = top + static_cast<int>(0.5 * static_cast<double>(box.height - 1) + 0.5);
    CHECK(mod.pixelFromPoint(box, {0.0, 0.0}).y == midY);

    // And a click at the vertical centre adds a point at 0 in the bipolar editor, at 0.5 in
    // the unipolar one — one hit-test path, two correct answers.
    CHECK(mod.pointFromPixel(box, box.left, midY).value == 0.0);
    CHECK(amp.pointFromPixel(box, box.left, midY).value > 0.49);
    CHECK(amp.pointFromPixel(box, box.left, midY).value < 0.51);
}

int main() {
    testTheSameCurveBoxHostsBothDomains();
    testDefaultWindowMidClamp();
    testMinClamp();
    testMaxClamp();
    testDegenerateWindowNeverOverhangs();
    testTitleRowAndCurveBox();
    testOutsideSheetDismissTest();
    if (g_fail) {
        std::printf("%d FAILURE(S)\n", g_fail);
        return 1;
    }
    std::printf("curve_popup tests passed\n");
    return 0;
}
