// Standalone tests for the pure morph domain: normalized position -> tap weights under both
// morph laws, and the fold of those weights into the kernel's three multipliers. Algebra only —
// no filter is run here. Most interior expectations are derived from the intended law in radicals,
// sharing not even a trig call with the implementation; one check evaluates std::cos/std::sin
// directly at the same argument the implementation does, but a radical-derived check of the same
// leg sits right beside it, so no coverage rests solely on the shared call.
// The MEASURED consequences of each law — HP-BP-LP's flat corner, HP-notch-LP's null — live in
// test_filter.cpp, where a filter is actually driven.

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

#include <cmath>
#include <cstdio>
#include <initializer_list>
#include <limits>
#include <type_traits>

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 constexpr double kPi = 3.14159265358979323846;

static constexpr float kHighPass = 0.0f;
static constexpr float kCentre = 0.5f;
static constexpr float kLowPass = 1.0f;

// cos and sin of pi/8, from the half-angle identity in radicals: cos(pi/8) = sqrt((1+cos(pi/4))/2)
// with cos(pi/4) = sqrt(2)/2. No trig call, so nothing here is shared with filter_morph's cos/sin.
static double cosPi8() { return std::sqrt((1.0 + std::sqrt(2.0) / 2.0) / 2.0); }  // 0.9238795325
static double sinPi8() { return std::sqrt((1.0 - std::sqrt(2.0) / 2.0) / 2.0); }  // 0.3826834324

// ---------------------------------------------------------------------------
// Shared across both laws
// ---------------------------------------------------------------------------

// HighBandLow is enumerator 0 by design: a zero-initialized field, or one absent from an older
// persisted blob and left default-constructed, must land on the default law rather than the SEM
// leg. A codec written against this enum depends on that.
static void testHighBandLowIsTheZeroEnumerator() {
    CHECK(static_cast<std::underlying_type_t<MorphLaw>>(MorphLaw::HighBandLow) == 0);
    CHECK(MorphLaw{} == MorphLaw::HighBandLow);
}

// The endpoints are pure taps EXACTLY under BOTH laws, not to within a rounding of cos/sin — the
// laws differ only in the interior. Asserted on the folded mix, where "pure" is an exact
// statement about three floats.
static void testMorphEndpointMixesAreExactlyPureTapsUnderBothLaws() {
    const float k = 1.0f / filterQFromNorm(0.5f);

    for (MorphLaw law : {MorphLaw::HighBandLow, MorphLaw::HighNotchLow}) {
        const MorphMix hp = morphMix(morphWeights(kHighPass, law), k);
        CHECK(hp.m0 == 1.0f && hp.m1 == -k && hp.m2 == -1.0f);  // v0 - k*v1 - v2

        const MorphMix lp = morphMix(morphWeights(kLowPass, law), k);
        CHECK(lp.m0 == 0.0f && lp.m1 == 0.0f && lp.m2 == 1.0f);  // v2

        // Out-of-range clamps to the endpoints rather than extrapolating.
        CHECK(morphWeights(-1.0f, law).hp == 1.0f);
        CHECK(morphWeights(2.0f, law).lp == 1.0f);
    }

    // Only the centre differs: a band-pass under one law, an HP+LP sum under the other.
    const MorphMix bp = morphMix(morphWeights(kCentre, MorphLaw::HighBandLow), k);
    CHECK(bp.m0 == 0.0f && bp.m1 == 1.0f && bp.m2 == 0.0f);  // v1

    const MorphMix notch = morphMix(morphWeights(kCentre, MorphLaw::HighNotchLow), k);
    CHECK(notch.m0 != 0.0f && notch.m1 != 0.0f);
}

// NaN clamps to neither endpoint (every comparison against it is false) and lands on each law's
// degenerate — no crash, a sane fallback rather than an extrapolation. HighNotchLow has no band
// tap to fall back to, so it lands on the leg-zero endpoint instead.
static void testNaNFallsBackToASaneTapPerLaw() {
    const float nan = std::numeric_limits<float>::quiet_NaN();

    const MorphWeights band = morphWeights(nan, MorphLaw::HighBandLow);
    CHECK(band.hp == 0.0f && band.bp == 1.0f && band.lp == 0.0f);

    const MorphWeights sem = morphWeights(nan, MorphLaw::HighNotchLow);
    CHECK(sem.hp == 1.0f && sem.bp == 0.0f && sem.lp == 0.0f);
}

// ---------------------------------------------------------------------------
// HighBandLow — adjacent taps only
// ---------------------------------------------------------------------------

// Pins the cos/sin curve at an interior point, not just the endpoints and the quadrature
// identity (hp^2+bp^2+lp^2=1, which any equal-power reparameterization would also satisfy).
// theta=0.5*pi*t^2 (quadratic in the leg fraction, still equal-power, still exact at both
// ends) would give hp=0.9239/bp=0.3827 here instead of the cos/sin pair's 0.7071/0.7071.
static void testHighBandLowInteriorMatchesCosSinNotAnAlternateEqualPowerCurve() {
    const MorphWeights w = morphWeights(0.25f, MorphLaw::HighBandLow);  // HP->BP leg, t = 0.5
    const double theta = 0.5 * kPi * 0.5;
    CHECK_NEAR(w.hp, std::cos(theta), 1e-6);
    CHECK_NEAR(w.bp, std::sin(theta), 1e-6);
    CHECK(w.lp == 0.0f);

    // Each leg is half the sweep, so a leg reaches at 0.125 what the SEM's single crossfade
    // reaches at 0.25 — the crispest algebraic statement of how the two laws differ.
    const MorphWeights eighth = morphWeights(0.125f, MorphLaw::HighBandLow);
    CHECK_NEAR(eighth.hp, cosPi8(), 1e-6);
    CHECK_NEAR(eighth.bp, sinPi8(), 1e-6);
}

// HP and LP never carry weight at the same time under THIS law. That is what keeps its centre a
// band-pass: the two are antiphase at the corner and would otherwise cancel into a notch. This
// assertion is law-specific and is deliberately inverted for HighNotchLow below — do not relax
// it to cover both, which would give up the guarantee entirely.
static void testHighBandLowNeverBlendsHighAgainstLowPass() {
    for (int i = 0; i <= 200; ++i) {
        const MorphWeights w = morphWeights(static_cast<float>(i) / 200.0f, MorphLaw::HighBandLow);
        CHECK(w.hp == 0.0f || w.lp == 0.0f);
        CHECK(w.hp >= 0.0f && w.bp >= 0.0f && w.lp >= 0.0f);
        // Equal power: the active pair sums in quadrature to unity.
        CHECK_NEAR(w.hp * w.hp + w.bp * w.bp + w.lp * w.lp, 1.0, 1e-6);
    }
}

// ---------------------------------------------------------------------------
// HighNotchLow — HP against LP, which is the whole mechanism
// ---------------------------------------------------------------------------

// The exact inverse of the HighBandLow assertion above: blending HP against LP is not a defect
// to be avoided here, it is what produces the notch. The band tap is silent throughout.
static void testHighNotchLowBlendsHighAgainstLowPassWithNoBandTap() {
    for (int i = 0; i <= 200; ++i) {
        const float n = static_cast<float>(i) / 200.0f;
        const MorphWeights w = morphWeights(n, MorphLaw::HighNotchLow);
        CHECK(w.bp == 0.0f);
        CHECK(w.hp >= 0.0f && w.lp >= 0.0f);
        // Both taps carry weight everywhere strictly between the endpoints.
        if (i > 0 && i < 200) CHECK(w.hp > 0.0f && w.lp > 0.0f);
        // Equal power, which is what keeps the legs from sagging away from the notch frequency.
        CHECK_NEAR(w.hp * w.hp + w.lp * w.lp, 1.0, 1e-6);
    }
}

// Pins the single equal-power crossfade at interior points against radical-derived values, so a
// law that still hits both endpoints but bends differently between them fails. Discriminators at
// n=0.25: a LINEAR crossfade gives 0.75/0.25; a two-leg construction (HighBandLow's spacing
// applied to an HP/LP pair) gives 0.7071/0.7071. Both are far outside this tolerance.
static void testHighNotchLowInteriorWeightsMatchTheSingleEqualPowerCrossfade() {
    // cos/sin of pi/8 and 3pi/8; the latter pair is the former swapped.
    const double c8 = cosPi8(), s8 = sinPi8();
    CHECK_NEAR(c8, 0.9238795325112867, 1e-15);
    CHECK_NEAR(s8, 0.3826834323650898, 1e-15);

    const MorphWeights quarter = morphWeights(0.25f, MorphLaw::HighNotchLow);
    CHECK_NEAR(quarter.hp, c8, 1e-6);
    CHECK_NEAR(quarter.lp, s8, 1e-6);

    const MorphWeights threeQuarters = morphWeights(0.75f, MorphLaw::HighNotchLow);
    CHECK_NEAR(threeQuarters.hp, s8, 1e-6);
    CHECK_NEAR(threeQuarters.lp, c8, 1e-6);

    const MorphWeights centre = morphWeights(kCentre, MorphLaw::HighNotchLow);
    CHECK_NEAR(centre.hp, std::sqrt(2.0) / 2.0, 1e-6);
    CHECK_NEAR(centre.lp, std::sqrt(2.0) / 2.0, 1e-6);

    // Symmetric about the centre, so the sweep reads the same in either direction.
    for (int i = 0; i <= 100; ++i) {
        const float n = static_cast<float>(i) / 100.0f;
        const MorphWeights a = morphWeights(n, MorphLaw::HighNotchLow);
        const MorphWeights b = morphWeights(1.0f - n, MorphLaw::HighNotchLow);
        CHECK_NEAR(a.hp, b.lp, 1e-6);
    }
}

// The centre's cancellation is STRUCTURAL, not a runtime near-miss of two large numbers. The fold
// is m2 = lp - hp, and at the centre the two weights are the same float — cos and sin of pi/4
// differ by about an ulp of DOUBLE, ~1e-16, which is nine orders below float's ~6e-8 spacing
// there, so they round to one value. m2 is therefore exactly 0 and the output reduces to
// hp*(x - k*v1): the high and low taps cannot drift apart by a rounding.
static void testHighNotchLowCentreFoldsToAnExactlyCancellingMix() {
    for (float res : {0.0f, 0.5f, 1.0f}) {
        const float k = 1.0f / filterQFromNorm(res);
        const MorphWeights w = morphWeights(kCentre, MorphLaw::HighNotchLow);
        CHECK(w.hp == w.lp);

        const MorphMix m = morphMix(w, k);
        CHECK(m.m2 == 0.0f);
        CHECK(m.m0 == w.hp);
        CHECK(m.m1 == -w.hp * k);
    }
}

int main() {
    testHighBandLowIsTheZeroEnumerator();
    testMorphEndpointMixesAreExactlyPureTapsUnderBothLaws();
    testNaNFallsBackToASaneTapPerLaw();

    testHighBandLowInteriorMatchesCosSinNotAnAlternateEqualPowerCurve();
    testHighBandLowNeverBlendsHighAgainstLowPass();

    testHighNotchLowBlendsHighAgainstLowPassWithNoBandTap();
    testHighNotchLowInteriorWeightsMatchTheSingleEqualPowerCrossfade();
    testHighNotchLowCentreFoldsToAnExactlyCancellingMix();

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