// Standalone tests for reasampler::instrument::engine::meter_ballistics — no VST3, no REAPER,
// no framework. The meter's whole behaviour is asserted here without a host: instantaneous
// rise, the 20 dB/s fall, the 1.5 s peak hold and its release at the same rate, the clip
// latch and its clear, and the dB -> normalized map the bar is drawn against.

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

#include <cmath>
#include <cstdio>

using namespace reasampler::instrument::engine;

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 bool near(double a, double b, double eps = 1e-9) { return std::fabs(a - b) <= eps; }

static double linearFromDb(double db) { return std::pow(10.0, db / 20.0); }

static void testDbFromLinear() {
    CHECK(near(meterDbFromLinear(1.0), 0.0));
    CHECK(near(meterDbFromLinear(0.5), -6.0205999132796239, 1e-9));
    CHECK(near(meterDbFromLinear(2.0), 6.0205999132796239, 1e-9));
    // Silence and anything under the floor read the floor, not -inf: the ballistics subtract
    // from this value, so it has to stay finite.
    CHECK(meterDbFromLinear(0.0) == kMeterFloorDb);
    CHECK(meterDbFromLinear(-1.0) == kMeterFloorDb);
    CHECK(meterDbFromLinear(1e-9) == kMeterFloorDb);
}

static void testNormFromDbIsLinearInDbAndClamped() {
    CHECK(meterNormFromDb(kMeterFloorDb) == 0.0);
    CHECK(meterNormFromDb(kMeterTopDb) == 1.0);
    CHECK(meterNormFromDb(-1000.0) == 0.0);
    CHECK(meterNormFromDb(1000.0) == 1.0);
    // Linear in dB: equal dB steps are equal normalized steps anywhere in the span.
    const double a = meterNormFromDb(-48.0) - meterNormFromDb(-54.0);
    const double b = meterNormFromDb(-6.0) - meterNormFromDb(-12.0);
    CHECK(near(a, b, 1e-12));
    CHECK(near(a, 6.0 / (kMeterTopDb - kMeterFloorDb), 1e-12));
    // The 0 dB tick, which the scale's heavier rule is drawn on.
    CHECK(near(meterNormFromDb(0.0), 60.0 / 66.0, 1e-12));
}

static void testRiseIsInstantaneous() {
    MeterState s;
    s = advanceMeter(s, linearFromDb(-12.0), 1.0 / 30.0);
    CHECK(near(s.levelDb, -12.0, 1e-9));
    // A louder block on the very next frame displays at once, however short the frame.
    s = advanceMeter(s, linearFromDb(-3.0), 1e-6);
    CHECK(near(s.levelDb, -3.0, 1e-9));
}

static void testFallIsTwentyDbPerSecond() {
    MeterState s;
    s = advanceMeter(s, 1.0, 0.0);  // 0 dBFS
    CHECK(near(s.levelDb, 0.0, 1e-9));
    s = advanceMeter(s, 0.0, 0.5);
    CHECK(near(s.levelDb, -10.0, 1e-9));
    s = advanceMeter(s, 0.0, 0.25);
    CHECK(near(s.levelDb, -15.0, 1e-9));
    // The fall never takes the bar under the peak the block itself carried.
    s = advanceMeter(s, linearFromDb(-14.0), 1.0);
    CHECK(near(s.levelDb, -14.0, 1e-9));
    // And it stops at the floor.
    for (int i = 0; i < 20; ++i) s = advanceMeter(s, 0.0, 0.5);
    CHECK(near(s.levelDb, kMeterFloorDb, 1e-9));
}

static void testPeakHoldLatchesForOnePointFiveSecondsThenFallsAtTheSameRate() {
    MeterState s;
    s = advanceMeter(s, 1.0, 0.0);
    CHECK(near(s.holdDb, 0.0, 1e-9));
    CHECK(near(s.holdRemainingSeconds, kMeterPeakHoldSeconds, 1e-12));

    // 1.4 s of silence: the bar has long fallen away, the tick has not moved.
    for (int i = 0; i < 14; ++i) s = advanceMeter(s, 0.0, 0.1);
    CHECK(near(s.holdDb, 0.0, 1e-9));
    CHECK(s.levelDb < -20.0);

    // Past 1.5 s it releases at the bar's own rate — 0.2 s past the latch is 4 dB down.
    s = advanceMeter(s, 0.0, 0.3);
    CHECK(near(s.holdDb, -4.0, 1e-9));
    s = advanceMeter(s, 0.0, 0.1);
    CHECK(near(s.holdDb, -6.0, 1e-9));

    // A new peak re-latches it and restarts the hold.
    s = advanceMeter(s, linearFromDb(-2.0), 0.1);
    CHECK(near(s.holdDb, -2.0, 1e-9));
    CHECK(near(s.holdRemainingSeconds, kMeterPeakHoldSeconds, 1e-12));
}

static void testHoldNeverFallsBelowTheBar() {
    MeterState s;
    s = advanceMeter(s, 1.0, 0.0);
    for (int i = 0; i < 40; ++i) s = advanceMeter(s, linearFromDb(-20.0), 0.1);
    CHECK(near(s.levelDb, -20.0, 1e-9));
    CHECK(near(s.holdDb, -20.0, 1e-9));
}

static void testClipLatchesAtFullScaleAndOnlyClearsOnRequest() {
    MeterState s;
    s = advanceMeter(s, linearFromDb(-0.01), 0.1);
    CHECK(!s.clip);           // under 0 dBFS does not latch
    s = advanceMeter(s, 1.0, 0.1);
    CHECK(s.clip);            // exactly 0 dBFS does
    for (int i = 0; i < 100; ++i) s = advanceMeter(s, 0.0, 0.1);
    CHECK(s.clip);            // and silence does not unlatch it
    s = clearMeterClip(s);
    CHECK(!s.clip);
    s = advanceMeter(s, linearFromDb(-6.0), 0.1);
    CHECK(!s.clip);           // cleared stays cleared while nothing reaches full scale
    CHECK(near(s.levelDb, -6.0, 1e-9));  // and clearing left the ballistics alone
}

static void testNonPositiveElapsedFreezesTheBallistics() {
    MeterState s;
    s = advanceMeter(s, 1.0, 0.0);
    const MeterState frozen = advanceMeter(s, 0.0, -1.0);
    CHECK(near(frozen.levelDb, s.levelDb, 1e-12));
    CHECK(near(frozen.holdDb, s.holdDb, 1e-12));
}

int main() {
    testDbFromLinear();
    testNormFromDbIsLinearInDbAndClamped();
    testRiseIsInstantaneous();
    testFallIsTwentyDbPerSecond();
    testPeakHoldLatchesForOnePointFiveSecondsThenFallsAtTheSameRate();
    testHoldNeverFallsBelowTheBar();
    testClipLatchesAtFullScaleAndOnlyClearsOnRequest();
    testNonPositiveElapsedFreezesTheBallistics();
    if (g_fail) {
        std::printf("%d FAILURE(S)\n", g_fail);
        return 1;
    }
    std::printf("meter_ballistics tests passed\n");
    return 0;
}
