// Standalone tests for reasampler::util::curve_law — no VST3, no REAPER, no framework. Same
// fast assert loop as the sibling pure tests. This is the ONE law behind the engine's segment
// evaluator, the overlay's knot geometry, and the deck's inner dial, so what it guarantees is
// what all three inherit.
//
// Covers: the LINEAR NEUTRAL (exponent 1.0 returns its input BIT-IDENTICALLY, which is what
// makes a pre-existing instance play unchanged); endpoint exactness at every exponent (no
// segment can overshoot its own endpoint levels); monotonicity and finiteness across the full
// 0.1..10 domain including both endpoints; the mid-level inverse the overlay knot drags
// through, and its round trip against the exponent; and the inner dial's own travel — exact at
// the neutral centre, and reachable there from a real drag grid.

#include "../src/core/util/curve_law.h"

#include <cmath>
#include <cstdio>
#include <initializer_list>

using namespace reasampler::util;

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

// The neutral is not merely "close to linear" — it must be the identity, bit for bit, or a
// blob that loaded at 1.0 would render differently from the engine that wrote it.
static void testNeutralExponentIsTheIdentity() {
    for (int i = 0; i <= 1000; ++i) {
        const double phi = static_cast<double>(i) / 1000.0;
        CHECK(curveMap(phi, kCurveNeutral) == phi);
    }
    // Including the values a fractional stage position actually takes.
    CHECK(curveMap(1.0 / 3.0, 1.0) == 1.0 / 3.0);
    CHECK(curveMap(0.1234567890123, 1.0) == 0.1234567890123);
}

// Both endpoints are exact at every exponent, which is the whole overshoot guarantee: a curved
// stage starts where the previous one ended and ends where the next one starts.
static void testEndpointsAreExactAtEveryExponent() {
    for (int i = 0; i <= 100; ++i) {
        const double e = kCurveMin + (kCurveMax - kCurveMin) * (i / 100.0);
        CHECK(curveMap(0.0, e) == 0.0);
        CHECK(curveMap(1.0, e) == 1.0);
    }
}

// The full domain, both endpoints included: finite, in range, and strictly rising.
static void testSweepIsFiniteMonotoneAndInRange() {
    const double exps[] = {kCurveMin, 0.25, 0.5, kCurveNeutral, 2.0, 4.0, kCurveMax};
    for (double e : exps) {
        double prev = -1.0;
        for (int i = 0; i <= 500; ++i) {
            const double phi = static_cast<double>(i) / 500.0;
            const double v = curveMap(phi, e);
            CHECK(std::isfinite(v));
            CHECK(v >= 0.0 && v <= 1.0);
            CHECK(v > prev - 1e-15);  // non-decreasing
            prev = v;
        }
        CHECK(std::fabs(prev - 1.0) < 1e-12);
    }
}

// Which side of the neutral an exponent falls on is the SHAPE, and the two directions must not
// collapse into each other.
static void testExponentDirectionShapesTheSegment() {
    CHECK(curveMap(0.5, 4.0) < curveMap(0.5, kCurveNeutral));
    CHECK(curveMap(0.5, 0.25) > curveMap(0.5, kCurveNeutral));
    CHECK(std::fabs(curveMap(0.5, kCurveNeutral) - 0.5) < 1e-15);
}

static void testClampCurveHoldsTheDomain() {
    CHECK(clampCurve(-5.0) == kCurveMin);
    CHECK(clampCurve(0.0) == kCurveMin);
    CHECK(clampCurve(1e9) == kCurveMax);
    CHECK(clampCurve(std::nan("")) == kCurveMin);  // a corrupt blob degrades, never propagates
    CHECK(clampCurve(2.5) == 2.5);
}

// The mid-level inverse is what a knot drag resolves through: it must be the exact inverse of
// the forward reading over the whole domain, or the knot and the dial could drift.
static void testMidLevelRoundTripsAgainstTheExponent() {
    for (int i = 0; i <= 200; ++i) {
        const double e = kCurveMin + (kCurveMax - kCurveMin) * (i / 200.0);
        const double mid = curveMidLevel(e);
        CHECK(mid > 0.0 && mid < 1.0);
        CHECK(std::fabs(curveFromMidLevel(mid) - e) < 1e-9);
    }
    // The mid-level is strictly DECREASING in the exponent, so a drag has one unambiguous
    // direction at every point of the domain.
    double prev = 1.0;
    for (int i = 0; i <= 200; ++i) {
        const double e = kCurveMin + (kCurveMax - kCurveMin) * (i / 200.0);
        const double mid = curveMidLevel(e);
        CHECK(mid < prev);
        prev = mid;
    }
}

// A knot dragged past what the domain can express saturates rather than producing a
// non-finite exponent.
static void testMidLevelInverseSaturates() {
    CHECK(curveFromMidLevel(0.0) == kCurveMax);
    CHECK(curveFromMidLevel(-1.0) == kCurveMax);
    CHECK(curveFromMidLevel(1.0) == kCurveMin);
    CHECK(curveFromMidLevel(5.0) == kCurveMin);
    CHECK(curveFromMidLevel(std::nan("")) == kCurveMax);
    CHECK(std::fabs(curveFromMidLevel(0.5) - kCurveNeutral) < 1e-12);
}

// curveLevelAt/curveFromLevelAt is the general form a knot's own (possibly off-centre) phi
// needs — curveMidLevel/curveFromMidLevel is the phi = 0.5 case, not a second law.
static void testMidLevelIsThePhiHalfSpecialCase() {
    for (double e : {kCurveMin, 0.3, kCurveNeutral, 2.0, kCurveMax}) {
        CHECK(curveLevelAt(0.5, e) == curveMidLevel(e));
    }
    for (double u : {0.0, 0.2, 0.5, 0.8, 1.0}) {
        CHECK(curveFromLevelAt(0.5, u) == curveFromMidLevel(u));
    }
}

// The round trip must hold at an arbitrary phi, not only 0.5 — this is what a knot whose
// integer x lands off its segment's true midpoint (an odd pixel span) actually exercises.
static void testLevelAtRoundTripsAtArbitraryPhi() {
    for (double phi : {0.1, 0.3, 0.42, 0.5, 0.63, 0.9}) {
        for (int i = 0; i <= 50; ++i) {
            const double e = kCurveMin + (kCurveMax - kCurveMin) * (i / 50.0);
            const double level = curveLevelAt(phi, e);
            CHECK(level > 0.0 && level < 1.0);
            CHECK(std::fabs(curveFromLevelAt(phi, level) - e) < 1e-9);
        }
    }
}

// Saturation holds at an arbitrary phi too, not only the mid-level special case.
static void testLevelAtInverseSaturatesAtArbitraryPhi() {
    for (double phi : {0.2, 0.5, 0.8}) {
        CHECK(curveFromLevelAt(phi, 0.0) == kCurveMax);
        CHECK(curveFromLevelAt(phi, 1.0) == kCurveMin);
        CHECK(curveFromLevelAt(phi, std::nan("")) == kCurveMax);
    }
}

// --- The inner dial's travel ---------------------------------------------------

// The knob drag delivers `start - dy/kKnobDragRangePixels`. param_slider owns that constant and
// this module deliberately does not link it, so the step is restated here; the structural
// assertion below is what keeps the detent wide enough for whatever it is.
static constexpr double kKnobStep = 1.0 / 128.0;

// The dial's centre must reach the neutral EXACTLY, in both directions — an exponent a hair off
// 1.0 costs a std::pow per sample per voice forever on a stage the user believes is at rest.
static void testKnobLawIsExactAtTheNeutralCentre() {
    CHECK(knobNormFromCurve(kCurveNeutral) == 0.5);
    CHECK(curveFromKnobNorm(0.5) == kCurveNeutral);
    // And the identity that exactness buys: curveMap takes its bit-identical fast path.
    for (int i = 0; i <= 100; ++i) {
        const double phi = static_cast<double>(i) / 100.0;
        CHECK(curveMap(phi, curveFromKnobNorm(0.5)) == phi);
    }
}

// A dial swept THROUGH the centre has to land on the identity. The raw logarithmic travel does
// not — the drag grid steps by 1/128 and only touches 0.5 by luck — so this is the detent's own
// property, asserted against that raw travel as the reference.
static void testADialSweptThroughNeutralLandsOnTheIdentity() {
    const auto rawTravel = [](double t) {
        return std::exp((2.0 * t - 1.0) * std::log(kCurveMax));
    };
    // A real drag: grabbed at a shaped value, dragged 40 steps down through the centre.
    const double grab = 0.5 + 17.0 * kKnobStep + 0.003;  // deliberately off the grid
    int detented = 0;
    int rawHits = 0;
    for (int step = 0; step <= 40; ++step) {
        const double t = grab - step * kKnobStep;
        if (curveFromKnobNorm(t) == kCurveNeutral) ++detented;
        if (rawTravel(t) == kCurveNeutral) ++rawHits;
    }
    CHECK(detented >= 1);   // the sweep reaches the identity
    CHECK(rawHits == 0);    // and would not have without the detent
    // The structural reason it cannot be skipped: the band is wider than one drag step.
    CHECK(kCurveKnobDetent > kKnobStep);
}

// Outside the detent the pair are inverses, so the dial reads back what it wrote and the
// endpoints saturate on the domain rather than past it.
static void testKnobLawRoundTripsOutsideTheDetent() {
    const double exps[] = {kCurveMin, 0.2, 0.5, 0.8, 1.3, 2.0, 5.0, kCurveMax};
    for (double e : exps) {
        const double back = curveFromKnobNorm(knobNormFromCurve(e));
        CHECK(std::fabs(back - e) < 1e-9);
    }
    // exp(-log(10)) is not guaranteed bit-exact to kCurveMin's literal 0.1 (1-2 ulp either way);
    // both norms below collapse to the same t == 0.0 computation, so both get the same
    // tolerance rather than leaning on clampCurve's floor to land on it by luck.
    CHECK(std::fabs(curveFromKnobNorm(0.0) - kCurveMin) < 1e-12);
    CHECK(std::fabs(curveFromKnobNorm(-3.0) - kCurveMin) < 1e-12);  // out-of-range norm saturates
    CHECK(std::fabs(curveFromKnobNorm(1.0) - kCurveMax) < 1e-12);
    // The ENDS need only land on the domain, not on an exact norm — 0.1 is not exactly 1/10 in
    // binary, so log(kCurveMin) is a hair off -log(kCurveMax). Only the centre carries an
    // exactness requirement, and only because the neutral is a bit-identity.
    CHECK(std::fabs(knobNormFromCurve(kCurveMin)) < 1e-12);
    CHECK(knobNormFromCurve(kCurveMax) == 1.0);
    // Monotone rising across the whole travel, so the dial has one unambiguous direction.
    double prev = 0.0;
    for (int i = 0; i <= 500; ++i) {
        const double v = curveFromKnobNorm(static_cast<double>(i) / 500.0);
        CHECK(v >= prev);
        prev = v;
    }
}

int main() {
    testNeutralExponentIsTheIdentity();
    testEndpointsAreExactAtEveryExponent();
    testSweepIsFiniteMonotoneAndInRange();
    testExponentDirectionShapesTheSegment();
    testClampCurveHoldsTheDomain();
    testMidLevelRoundTripsAgainstTheExponent();
    testMidLevelInverseSaturates();
    testMidLevelIsThePhiHalfSpecialCase();
    testLevelAtRoundTripsAtArbitraryPhi();
    testLevelAtInverseSaturatesAtArbitraryPhi();
    testKnobLawIsExactAtTheNeutralCentre();
    testADialSweptThroughNeutralLandsOnTheIdentity();
    testKnobLawRoundTripsOutsideTheDetent();
    if (g_fail == 0) std::printf("curve_law: all tests passed\n");
    else std::printf("curve_law: %d FAILED\n", g_fail);
    return g_fail == 0 ? 0 : 1;
}
