// Standalone tests for the running filter's NUMERICAL behaviour and state lifecycle — bounded
// output under a live parameter sweep, full-drive stability and self-oscillation, the softLimit
// shaper's own properties, the denormal flush, DC handling, the impulse response against the
// coefficients, and reset/prepare/per-channel state rules. Split from test_filter.cpp along the
// one seam that costs nothing: none of these need the frequency-response measurement harness, so
// the analytic reference lives in exactly one file and cannot fork.

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

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

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 kBandPass = 0.5f;
static constexpr float kCentre = 0.5f;
static constexpr float kLowPass = 1.0f;

static const MorphLaw kBothLaws[] = {MorphLaw::HighBandLow, MorphLaw::HighNotchLow};
static const char* lawName(MorphLaw law) {
    return law == MorphLaw::HighBandLow ? "HP-BP-LP" : "HP-notch-LP";
}

// The rates the invariance claims are made over.
static const double kRates[] = {44100.0, 48000.0, 88200.0, 96000.0, 192000.0};
static constexpr int kRateCount = 5;

static FilterSettings at(double fcHz, float res, float morph, float drive = 0.0f,
                         MorphLaw law = MorphLaw::HighBandLow) {
    return {filterNormFromCutoffHz(static_cast<float>(fcHz)), res, morph, drive, law};
}

static void testFullRangeCutoffSweepAtAudioRateStaysBounded() {
    unsigned rng = 0x13579bdfu;
    auto noise = [&rng]() {
        rng = rng * 1664525u + 1013904223u;
        return static_cast<float>(static_cast<int>(rng >> 9) - (1 << 22)) /
               static_cast<float>(1 << 22);
    };

    for (int r = 0; r < kRateCount; ++r) {
        const double sr = kRates[r];
        for (MorphLaw law : kBothLaws) {
            for (float morph : {kHighPass, kBandPass, kLowPass}) {
                for (float res : {0.0f, 1.0f}) {
                    for (float drive : {0.0f, 1.0f}) {
                        VoiceFilter f;
                        f.reset();
                        // A fixed WALL-CLOCK sweep: the same cutoff travel per second at every
                        // rate, so the per-sample coefficient step gets no gentler as the rate
                        // rises.
                        const int n = static_cast<int>(sr * 0.25);
                        for (int i = 0; i < n; ++i) {
                            const float t = static_cast<float>(i) / static_cast<float>(n - 1);
                            f.prepare({t, res, morph, drive, law}, sr);
                            const float y = f.process(0, noise());
                            CHECK(std::isfinite(y));
                            CHECK(std::fabs(y) < 100.0f);
                            if (!std::isfinite(y)) return;  // stop before the log floods
                        }
                    }
                }
            }
        }
    }
}

// Drive is bounded by construction, not by tuning: softLimit is a contraction, so the state
// update can only ever shrink the state and the filter cannot gain energy from it. This sweeps
// the corners that would expose a tuned margin instead.
static void testFullDriveStaysBoundedAtEveryCutoffResonanceAndRate() {
    unsigned rng = 0x2468aceu;
    auto noise = [&rng]() {
        rng = rng * 1664525u + 1013904223u;
        return static_cast<float>(static_cast<int>(rng >> 9) - (1 << 22)) /
               static_cast<float>(1 << 22);
    };

    for (int r = 0; r < kRateCount; ++r) {
        const double sr = kRates[r];
        for (MorphLaw law : kBothLaws) {
            for (int ci = 0; ci <= 8; ++ci) {
                for (int mi = 0; mi <= 4; ++mi) {
                    for (float res : {0.0f, 0.5f, 1.0f}) {
                        VoiceFilter f;
                        f.prepare({ci / 8.0f, res, mi / 4.0f, 1.0f, law}, sr);
                        f.reset();
                        for (int i = 0; i < 4000; ++i) {
                            const float y = f.process(0, noise());
                            if (!std::isfinite(y) || std::fabs(y) > 8.0f) {
                                std::printf("FAIL line %d: %s sr=%.0f cutoff=%.2f morph=%.2f "
                                            "res=%.1f full drive produced %g\n",
                                            __LINE__, lawName(law), sr, ci / 8.0, mi / 4.0, res, y);
                                ++g_fail;
                                return;
                            }
                        }
                    }
                }
            }
        }
    }
}

// Full drive at full resonance with no input must still go quiet. A nonlinearity in the loop is
// exactly where a self-oscillator would hide, and softLimit's sub-unit slope is what forbids it.
static void testFullDriveDoesNotSelfOscillate() {
    for (int r = 0; r < kRateCount; ++r) {
        const double sr = kRates[r];
        for (MorphLaw law : kBothLaws) {
            for (float morph : {kHighPass, kBandPass, kLowPass}) {
                VoiceFilter f;
                f.prepare(at(1000.0, 1.0f, morph, 1.0f, law), sr);
                f.reset();
                const int excite = static_cast<int>(sr * 0.01);
                for (int i = 0; i < excite; ++i) {
                    f.process(0, static_cast<float>(std::sin(2.0 * kPi * 1000.0 * i / sr)));
                }
                for (int i = 0; i < static_cast<int>(sr * 0.5); ++i) f.process(0, 0.0f);
                CHECK(f.isSilent());
            }
        }
    }
}

// The softLimit shaper's own properties, independent of any running filter: bit-exact at depth 0,
// odd, a contraction over the whole excursion range, bounded by the knee, and increasing where the
// shaping actually happens.
static void testSoftLimitIsOddMonotoneBoundedAndExactAtZeroDepth() {
    for (double x : {-3.0, -0.5, 0.0, 1e-9, 0.25, 7.0}) {
        // Depth 0 is the identity by algebra, so correctness doesn't require special-casing it —
        // voice_filter.h gates the call anyway, but as a perf optimization (see its comment).
        CHECK(softLimit(static_cast<float>(x), 0.0f) == static_cast<float>(x));
    }
    CHECK_NEAR(softLimit(1.5f, 2.0f), -softLimit(-1.5f, 2.0f), 1e-9);

    for (float depth : {0.5f, 4.0f, 64.0f}) {
        // The two properties the stability argument rests on, over the whole excursion range a
        // resonating state can reach. Monotonicity is NOT asserted here: far past the knee the
        // curve is asymptotically flat, so the true increment between adjacent samples falls
        // below float epsilon and rounding can walk it backwards by an ulp.
        for (int i = -400; i <= 400; ++i) {
            const float x = static_cast<float>(i) * 0.05f;
            const float y = softLimit(x, depth);
            CHECK(std::fabs(y) <= std::fabs(x));        // a contraction — the stability argument
            CHECK(std::fabs(y) < 1.0f / depth + 1e-6f); // bounded by the knee
        }
        // Strictly increasing across the knee, which is where the shaping actually happens.
        const float knee = 1.0f / depth;
        float prev = -1e30f;
        for (int i = -20; i <= 20; ++i) {
            const float y = softLimit(static_cast<float>(i) * 0.1f * knee, depth);
            CHECK(y > prev);
            prev = y;
        }
    }
}

// The flush tests the ENVELOPE — both integrators — not one sample: isSilent() means "both are
// exactly zero," so both have to reach zero for that check to mean anything, and the conjunctive
// test is the cheapest guarantee of that (see voice_filter.h's flush comment). The stronger
// limit-cycle rationale belongs to the retired Direct Form I state, where the flushed variables
// were the actual filter OUTPUT rather than integrator state — it does not reproduce here.
static void checkFlushGoesSilent(double sr, float morph, float drive, MorphLaw law) {
    // The decay to the floor is a fixed WALL-CLOCK time, so the budget scales with the rate.
    const int budget = static_cast<int>(sr * 0.5);
    VoiceFilter f;
    f.prepare(at(1000.0, 1.0f, morph, drive, law), sr);
    f.reset();

    // Excite, then hard-cut to silence the way a released voice does.
    const int excite = static_cast<int>(sr * 0.01);
    for (int i = 0; i < excite; ++i) {
        f.process(0, 0.5f * static_cast<float>(std::sin(2.0 * kPi * 1000.0 * i / sr)));
    }

    int subnormalSamples = 0, silentAt = -1;
    for (int i = 0; i < budget; ++i) {
        f.process(0, 0.0f);
        const VoiceFilter::State& s = f.state(0);
        if ((s.ic1 != 0.0f && std::fabs(s.ic1) < FLT_MIN) ||
            (s.ic2 != 0.0f && std::fabs(s.ic2) < FLT_MIN)) {
            ++subnormalSamples;
        }
        if (silentAt < 0 && f.isSilent()) silentAt = i;
    }
    // Without the flush the state grinds down through the subnormal range for thousands of
    // samples; a stray sample or two at a zero crossing is not a stall.
    CHECK(subnormalSamples <= 2);
    CHECK(silentAt >= 0);
    CHECK(silentAt < budget);
    // And it stays silent — a flush that perturbs the loop would re-excite it.
    for (int i = 0; i < 1000; ++i) CHECK(f.process(0, 0.0f) == 0.0f);
    CHECK(f.isSilent());
}

static void testStateFlushesToZeroWithoutStallingInDenormals() {
    for (int r = 0; r < kRateCount; ++r) {
        for (MorphLaw law : kBothLaws) {
            for (float morph : {kHighPass, kBandPass, kLowPass}) {
                for (float drive : {0.0f, 1.0f}) checkFlushGoesSilent(kRates[r], morph, drive, law);
            }
        }
    }
}

// A high-pass under sustained DC must settle to zero and STAY there. Sampling only the final
// value is not enough: a resonator swings through zero twice a cycle, so a single late sample
// can land near zero while the envelope still rings well above it. This regressed a click train
// on the retired topology, where flushing the FIR history discarded the pinned DC and the next
// sample recomputed a full-amplitude step. TPT has no FIR history to discard, so the hazard is
// structural rather than a tuning — but the assertion is cheap and pins the outcome. Morph 0 is
// the same pure high-pass under either law, so this needs no law loop.
static void testHighPassSustainedDCDoesNotReRing() {
    for (int r = 0; r < kRateCount; ++r) {
        const double sr = kRates[r];
        for (float drive : {0.0f, 1.0f}) {
            VoiceFilter f;
            f.prepare(at(1000.0, 1.0f, kHighPass, drive), sr);
            f.reset();
            const int settle = static_cast<int>(sr * 0.05);
            float worstAfterSettle = 0.0f;
            for (int i = 0; i < static_cast<int>(sr * 0.5); ++i) {
                const float y = f.process(0, 1.0f);
                if (i >= settle) worstAfterSettle = std::fmax(worstAfterSettle, std::fabs(y));
            }
            CHECK(worstAfterSettle < 1e-3f);
        }
    }
}

static void testImpulseResponseMatchesTheKernel() {
    VoiceFilter f;
    f.prepare(at(1000.0, 0.5f, kLowPass), 48000.0);
    f.reset();
    const SvfCoeffs c = f.coeffs();
    // From a cleared state the first sample reduces to the coefficients alone: v1 == a2, v2 == a3.
    CHECK_NEAR(f.process(0, 1.0f), c.a3, 1e-7);

    VoiceFilter bp;
    bp.prepare(at(1000.0, 0.5f, kBandPass), 48000.0);
    bp.reset();
    CHECK_NEAR(bp.process(0, 1.0f), c.a2, 1e-7);

    VoiceFilter hp;
    hp.prepare(at(1000.0, 0.5f, kHighPass), 48000.0);
    hp.reset();
    CHECK_NEAR(hp.process(0, 1.0f), 1.0 - c.k * c.a2 - c.a3, 1e-7);

    // Under HighNotchLow the centre's first sample is the SUM of the high and low taps, scaled by
    // the shared weight — the same algebra the null rests on, seen one sample in.
    VoiceFilter sem;
    sem.prepare(at(1000.0, 0.5f, kCentre, 0.0f, MorphLaw::HighNotchLow), 48000.0);
    sem.reset();
    const double w = morphWeights(kCentre, MorphLaw::HighNotchLow).hp;
    const double highTap = 1.0 - c.k * c.a2 - c.a3;
    const double lowTap = c.a3;
    CHECK_NEAR(sem.process(0, 1.0f), w * (highTap + lowTap), 1e-6);
}

static void testLowpassStepSettlesToUnityAndHighpassRejectsDC() {
    const double sr = 48000.0;
    VoiceFilter f;
    f.prepare(at(1000.0, 0.0f, kLowPass), sr);
    f.reset();
    float y = 0.0f;
    for (int i = 0; i < 48000; ++i) y = f.process(0, 1.0f);
    CHECK_NEAR(y, 1.0, 1e-3);  // DC passes a lowpass at unity

    VoiceFilter hp;
    hp.prepare(at(1000.0, 0.0f, kHighPass), sr);
    hp.reset();
    float worstAfterSettle = 0.0f;
    for (int i = 0; i < 48000; ++i) {
        y = hp.process(0, 1.0f);
        if (i >= 200) worstAfterSettle = std::fmax(worstAfterSettle, std::fabs(y));
    }
    CHECK(worstAfterSettle < 1e-3f);
}

static void testResetClearsStateButPrepareKeepsIt() {
    VoiceFilter f;
    f.prepare({0.5f, 0.5f, kLowPass, 0.0f}, 48000.0);
    f.process(0, 1.0f);
    CHECK(!f.isSilent());

    // A live parameter move must not zero the state — that is what would click. Switching the
    // morph law is a parameter move like any other: it only recomputes the mix.
    f.prepare({0.6f, 0.5f, kLowPass, 0.0f}, 48000.0);
    CHECK(!f.isSilent());
    f.prepare({0.6f, 0.5f, kBandPass, 1.0f}, 48000.0);
    CHECK(!f.isSilent());
    f.prepare({0.6f, 0.5f, kCentre, 1.0f, MorphLaw::HighNotchLow}, 48000.0);
    CHECK(!f.isSilent());

    f.reset();
    CHECK(f.isSilent());
}

static void testChannelStateIsIndependent() {
    VoiceFilter f;
    f.prepare({0.5f, 0.5f, kLowPass, 0.0f}, 48000.0);
    f.reset();
    f.process(0, 1.0f);
    CHECK(f.state(0).ic2 != 0.0f);
    CHECK(f.state(1).ic2 == 0.0f);

    float frame[2] = {1.0f, -1.0f};
    f.processFrame(frame, 2);
    CHECK(f.state(1).ic2 < 0.0f);
    CHECK(frame[0] != frame[1]);
}

int main() {
    testFullRangeCutoffSweepAtAudioRateStaysBounded();
    testFullDriveStaysBoundedAtEveryCutoffResonanceAndRate();
    testFullDriveDoesNotSelfOscillate();
    testSoftLimitIsOddMonotoneBoundedAndExactAtZeroDepth();
    testStateFlushesToZeroWithoutStallingInDenormals();
    testHighPassSustainedDCDoesNotReRing();
    testImpulseResponseMatchesTheKernel();
    testLowpassStepSettlesToUnityAndHighpassRejectsDC();
    testResetClearsStateButPrepareKeepsIt();
    testChannelStateIsIndependent();

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