// Standalone tests for the filter's control domain — normalized knob position to cutoff Hz, Q,
// and drive depth, plus the exact inverses. No DSP is run here and no sample rate appears, which
// is the point: filter_params is deliberately rate-free. Interior points are pinned against a
// derivation of the intended law written out in-test, so a curve that still hits the anchors but
// bends differently between them fails.

#include "../src/core/instrument/engine/filter/filter_params.h"

#include <cmath>
#include <cstdio>

using namespace reasampler::instrument::engine::filter;

static int g_fail = 0;
#define CHECK(cond) do { if(!(cond)) { \
    std::printf("FAIL line %d: %s\n", __LINE__, #cond); ++g_fail; } } while(0)
#define CHECK_NEAR(a, b, eps) do { const double a_ = (a), b_ = (b); \
    if (!(std::fabs(a_ - b_) <= (eps))) { \
        std::printf("FAIL line %d: %s (%.10f) != %s (%.10f), delta %.3e\n", \
                    __LINE__, #a, a_, #b, b_, std::fabs(a_ - b_)); ++g_fail; } } while(0)

static void testCutoffMapsThreeDecadesLogarithmically() {
    CHECK_NEAR(filterCutoffHzFromNorm(0.0f), 20.0, 1e-3);
    CHECK_NEAR(filterCutoffHzFromNorm(1.0f), 20000.0, 1e-2);

    // Exactly three decades, so the decade midpoints land on round numbers.
    CHECK_NEAR(filterCutoffHzFromNorm(1.0f / 3.0f), 200.0, 1e-3);
    CHECK_NEAR(filterCutoffHzFromNorm(2.0f / 3.0f), 2000.0, 1e-2);

    // Half-decade steps confirm the sweep is log, not linear.
    CHECK_NEAR(filterCutoffHzFromNorm(1.0f / 6.0f), 20.0 * std::sqrt(10.0), 1e-3);
    CHECK_NEAR(filterCutoffHzFromNorm(0.5f), 20.0 * std::sqrt(1000.0), 1e-2);

    CHECK_NEAR(filterCutoffHzFromNorm(-1.0f), 20.0, 1e-3);
    CHECK_NEAR(filterCutoffHzFromNorm(2.0f), 20000.0, 1e-2);
}

static void testCutoffNormRoundTrips() {
    for (int i = 0; i <= 20; ++i) {
        const float n = static_cast<float>(i) / 20.0f;
        CHECK_NEAR(filterNormFromCutoffHz(filterCutoffHzFromNorm(n)), n, 1e-6);
    }
    CHECK_NEAR(filterNormFromCutoffHz(200.0f), 1.0 / 3.0, 1e-6);
    CHECK(filterNormFromCutoffHz(1.0f) == 0.0f);
    CHECK(filterNormFromCutoffHz(48000.0f) == 1.0f);
}

static void testQSpansPointOneToTenWithRootTwoAtCenter() {
    CHECK_NEAR(filterQFromNorm(0.0f), 0.1, 1e-6);
    CHECK_NEAR(filterQFromNorm(0.5f), std::sqrt(2.0), 1e-5);
    CHECK_NEAR(filterQFromNorm(1.0f), 10.0, 1e-4);

    CHECK_NEAR(filterQFromNorm(-1.0f), 0.1, 1e-6);
    CHECK_NEAR(filterQFromNorm(2.0f), 10.0, 1e-4);

    // Pins the single quadratic-in-log-Q curve at two interior points, derived independently by
    // solving log Q = a + b*n + c*n^2 through the three anchors above rather than read out of
    // the implementation. A two-spliced-log-segments curve (log-linear on each half, the design
    // this module doc explicitly rejects for its center-detent slope kink) would give 0.376 and
    // 3.761 here instead — both comfortably outside this tolerance.
    {
        const double lo = std::log(static_cast<double>(kFilterQMin));
        const double mid = std::log(static_cast<double>(kFilterQCenter));
        const double hi = std::log(static_cast<double>(kFilterQMax));
        const double c = 2.0 * lo + 2.0 * hi - 4.0 * mid;
        const double b = hi - lo - c;
        const double a = lo;
        auto qLaw = [&](double n) { return std::exp(a + b * n + c * n * n); };
        CHECK_NEAR(filterQFromNorm(0.25f), qLaw(0.25), 1e-5);
        CHECK_NEAR(filterQFromNorm(0.75f), qLaw(0.75), 1e-5);
    }

    // Strictly monotonic across the whole travel — no fold-back from the quadratic term.
    float prev = -1.0f;
    for (int i = 0; i <= 1000; ++i) {
        const float q = filterQFromNorm(static_cast<float>(i) / 1000.0f);
        CHECK(q > prev);
        prev = q;
    }
}

static void testQNormRoundTrips() {
    for (int i = 0; i <= 20; ++i) {
        const float n = static_cast<float>(i) / 20.0f;
        CHECK_NEAR(filterNormFromQ(filterQFromNorm(n)), n, 1e-5);
    }
    CHECK_NEAR(filterNormFromQ(static_cast<float>(std::sqrt(2.0))), 0.5, 1e-5);
    CHECK(filterNormFromQ(0.0f) == 0.0f);
    CHECK(filterNormFromQ(1000.0f) == 1.0f);
}

static void testDriveDepthIsZeroAtRestAndRisesMonotonically() {
    // Exactly zero, not nearly: the limiter is the identity only at depth 0.
    CHECK(filterDriveDepthFromNorm(0.0f) == 0.0f);
    CHECK(filterDriveDepthFromNorm(-1.0f) == 0.0f);
    CHECK_NEAR(filterDriveDepthFromNorm(1.0f), kFilterDriveDepthMax, 1e-6);
    CHECK_NEAR(filterDriveDepthFromNorm(2.0f), kFilterDriveDepthMax, 1e-6);

    // Pins the SQUARE law at an interior point, not just the anchors: a linear law would give
    // kFilterDriveDepthMax/2 (2.0) here, not kFilterDriveDepthMax/4 (1.0).
    CHECK_NEAR(filterDriveDepthFromNorm(0.5f), kFilterDriveDepthMax * 0.25, 1e-6);

    float prev = -1.0f;
    for (int i = 0; i <= 100; ++i) {
        const float d = filterDriveDepthFromNorm(static_cast<float>(i) / 100.0f);
        CHECK(d > prev);
        prev = d;
    }
}

int main() {
    testCutoffMapsThreeDecadesLogarithmically();
    testCutoffNormRoundTrips();
    testQSpansPointOneToTenWithRootTwoAtCenter();
    testQNormRoundTrips();
    testDriveDepthIsZeroAtRestAndRisesMonotonically();

    if (g_fail == 0) std::printf("filter_params_tests: all passed\n");
    else std::printf("filter_params_tests: %d FAILED\n", g_fail);
    return g_fail == 0 ? 0 : 1;
}
