// Standalone tests for reasampler::instrument::ui::master_meter — no VST3, no REAPER, no
// framework. Assert:
//
//  * column interior — the 22/4/36 decomposition, the exported column width the deck reserves,
//    the mono bar taking the whole field, the two stereo bars, all inside the column.
//  * bar count — the SAME LaneSplit resolveLaneSplit folds, over channel mode x source
//    channel count, so it can never become a second rule.
//  * the editor's dirty-rect fast path — meterFastPathEligible's wholly-inside test, and
//    meterBarsWithinField, the self-containment invariant the fast path rests on.
//  * the dB axis — top/floor on the field's edges, an interior value, and the clamps.
//  * ballistics — instantaneous rise, 20 dB/s fall, the 1.5 s hold and its release AT RATE;
//    the audio thread's clip latch surviving a UI frame; the per-field single-lane fold.
//  * the GR lamp — lit only while the limiter reduces, held, and surviving the 500 ms tick the
//    editor actually runs it at.

#include "../src/core/instrument/ui/master_meter.h"
#include "../src/core/instrument/ui/waveform_view.h"

#include <cmath>
#include <cstdio>

using namespace reasampler;
using namespace reasampler::instrument::ui;
using reasampler::instrument::engine::kMeterFallDbPerSecond;
using reasampler::instrument::engine::kMeterFloorDb;
using reasampler::instrument::engine::kMeterPeakHoldSeconds;
using reasampler::instrument::engine::kMeterTopDb;

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 shipped column: 62 px wide, 186 tall (knob_deck's spanning geometry).
static const Rect kColumn = Rect::ltrb(1114, 40, 1176, 226);

static void testColumnDividesIntoGutterAndBarField() {
    const MeterRects m = meterRects(kColumn, LaneSplit::Single);
    CHECK(m.labels.x == kColumn.x);
    CHECK(m.labels.width == kMeterLabelW);
    CHECK(m.field.x == m.labels.right() + kMeterLabelGap);
    CHECK(m.field.width == kMeterFieldW);
    // The three parts account for the column exactly — a residue would leave dead pixels the
    // scale's numerals would then be centred against. Asserted against the EXPORTED width the
    // deck reserves, not against this fixture's literal rect: the deck reading the same
    // constant is what keeps the reserve and the interior from drifting apart.
    CHECK(kMeterLabelW + kMeterLabelGap + kMeterFieldW == kMeterColumnW);
    CHECK(kMeterColumnW == kColumn.width);
    CHECK(m.field.right() == kColumn.right());
    // Full height in both rects: the column spans both row baselines as ONE readout.
    CHECK(m.labels.y == kColumn.y && m.labels.bottom() == kColumn.bottom());
    CHECK(m.field.y == kColumn.y && m.field.bottom() == kColumn.bottom());
}

static void testMonoDrawsOneWideBarAndStereoDrawsTwo() {
    const MeterRects mono = meterRects(kColumn, LaneSplit::Single);
    CHECK(mono.barA == mono.field);  // the one bar IS the field
    CHECK(mono.barB.empty());

    const MeterRects st = meterRects(kColumn, LaneSplit::Stereo);
    CHECK(!st.barB.empty());
    CHECK(st.barA.width == st.barB.width);
    CHECK(st.barA.width == (kMeterFieldW - kMeterBarGap) / 2);
    CHECK(st.barA.width == 17);
    CHECK(st.barB.x - st.barA.right() == kMeterBarGap);
    // Both bars inside the field, and the pair fills it to the pixel.
    CHECK(st.barA.x == st.field.x);
    CHECK(st.barB.right() == st.field.right());
    CHECK(st.barA.y == st.field.y && st.barB.bottom() == st.field.bottom());
}

// The bar count is NOT a second rule: it is resolveLaneSplit's answer for the same (mode,
// source) pair the waveform asks about. A mono source under stereo mode is dual-mono — one
// source, two views.
static void testBarCountFollowsTheWaveformsOwnLaneSplit() {
    const Rect band = Rect::ltrb(8, 100, 1182, 458);
    for (bool stereoMode : {false, true}) {
        for (int sourceChannels : {1, 2}) {
            const LaneSplit split = resolveLaneSplit(stereoMode, sourceChannels);
            const MeterRects m = meterRects(kColumn, split);
            const int bars = m.barB.empty() ? 1 : 2;
            // The waveform's own surface folds the SAME call, so on a band tall enough to
            // divide the two answers agree by construction rather than by coincidence.
            CHECK(bars == waveformSurface(band, stereoMode, sourceChannels).laneCount);
            // Spelled out per combination so a regression names which one broke.
            const bool expectTwo = stereoMode && sourceChannels >= 2;
            CHECK(bars == (expectTwo ? 2 : 1));
        }
    }
}

static void testDbAxisSpansTheFieldAndClamps() {
    const MeterRects m = meterRects(kColumn, LaneSplit::Single);
    CHECK(meterDbToY(m.field, kMeterTopDb) == m.field.y);
    CHECK(meterDbToY(m.field, kMeterFloorDb) == m.field.bottom());
    // Monotone downward as the level falls.
    int prev = m.field.y;
    for (double db = kMeterTopDb; db >= kMeterFloorDb; db -= 6.0) {
        const int y = meterDbToY(m.field, db);
        CHECK(y >= prev);
        prev = y;
    }
    // Clamped outside the scale rather than drawn off the field.
    CHECK(meterDbToY(m.field, kMeterTopDb + 40.0) == m.field.y);
    CHECK(meterDbToY(m.field, kMeterFloorDb - 40.0) == m.field.bottom());

    // One INTERIOR point, because endpoints plus monotonicity are satisfied by any log or
    // piecewise map through them, and the scale is specified LINEAR in dB. −27 is the
    // midpoint of −60…+6, so it must land on the field's own midpoint: 186 x 0.5 = 93.
    CHECK(meterDbToY(m.field, -27.0) == m.field.bottom() - 93);
    // And a quarter of the way up, which fixes the slope rather than just the centre.
    CHECK(meterDbToY(m.field, -43.5) == m.field.bottom() - 47);  // round(0.25 x 186) = 47
}

// The numeral SET is spec-pinned (0, −12, −24, −36, −48, −60) as a property of the scale, so it
// is asserted here rather than left as a modulo inside the painter.
static void testEveryOtherTickCarriesANumeral() {
    const int expected[] = {6, -6, -18, -30, -42, -54};
    for (int db : expected) CHECK(!meterTickNumeralled(db));
    const int numeralled[] = {0, -12, -24, -36, -48, -60};
    for (int db : numeralled) CHECK(meterTickNumeralled(db));
}

// The floor tick sits ON the field's bottom edge, so an unclamped y±5 numeral box hangs below
// the column and into the deck's bottom padding.
static void testTheFloorNumeralStaysInsideTheGutter() {
    const MeterRects m = meterRects(kColumn, LaneSplit::Single);
    const Rect floorLabel = meterNumeralRect(m.labels, meterDbToY(m.field, kMeterFloorDb));
    CHECK(floorLabel.bottom() <= m.labels.bottom());
    CHECK(floorLabel.y >= m.labels.y);
    CHECK(floorLabel.height == 10);  // clamped, not squashed — the numeral still has its band
    const Rect topLabel = meterNumeralRect(m.labels, meterDbToY(m.field, kMeterTopDb));
    CHECK(topLabel.y >= m.labels.y);
    CHECK(topLabel.height == 10);
    // An interior tick is centred on its rule, which is the case the clamp must not disturb.
    const int midY = meterDbToY(m.field, -24.0);
    CHECK(meterNumeralRect(m.labels, midY).y == midY - 5);
}

// A column narrower than the interior needs yields NOTHING rather than a field overrunning it.
// Reachable only if the deck's reserve and this module's interior ever disagree — which is
// exactly what kMeterColumnW exists to prevent.
static void testAColumnTooNarrowForTheInteriorDrawsNothing() {
    const Rect narrow = Rect::ltrb(0, 0, kMeterColumnW - 1, 186);
    const MeterRects m = meterRects(narrow, LaneSplit::Stereo);
    CHECK(m.field.empty() && m.barA.empty() && m.barB.empty());
    // Exactly the needed width still lays out.
    CHECK(!meterRects(Rect::ltrb(0, 0, kMeterColumnW, 186), LaneSplit::Stereo).field.empty());
}

static void testPeakRisesAtOnceAndFallsAtTwentyDbPerSecond() {
    MasterMeterUi s;
    // Unity on the left, silence on the right: the two channels are independent.
    s = advanceMasterMeter(s, {1.0, 0.0, 1.0, false}, 0.1);
    CHECK(std::fabs(s.left.levelDb - 0.0) < 1e-9);   // rise is instantaneous, this very frame
    CHECK(s.right.levelDb == kMeterFloorDb);

    // One second of silence: exactly kMeterFallDbPerSecond of fall, not a smoothed decay.
    s = advanceMasterMeter(s, {0.0, 0.0, 1.0, false}, 1.0);
    CHECK(std::fabs(s.left.levelDb - -kMeterFallDbPerSecond) < 1e-9);
}

static void testPeakHoldSitsForItsFullWindowThenReleases() {
    MasterMeterUi s;
    s = advanceMasterMeter(s, {1.0, 1.0, 1.0, false}, 0.1);
    const double held = s.left.holdDb;
    CHECK(std::fabs(held - 0.0) < 1e-9);

    // Just under the hold window: the bar has fallen a long way, the tick has not moved.
    s = advanceMasterMeter(s, {0.0, 0.0, 1.0, false}, kMeterPeakHoldSeconds - 0.01);
    CHECK(s.left.levelDb < held - 20.0);
    CHECK(std::fabs(s.left.holdDb - held) < 1e-9);

    // Past it, the tick releases at the SAME 20 dB/s the bar uses — pinned by value, not as an
    // inequality: a slower release would satisfy "it fell" and still be the wrong meter. The
    // frame spends the 0.01 s of hold it had left and releases for the remaining 0.49 s, which
    // is also what proves the release does not quantize to whole UI frames.
    s = advanceMasterMeter(s, {0.0, 0.0, 1.0, false}, 0.5);
    CHECK(std::fabs(s.left.holdDb - (held - kMeterFallDbPerSecond * 0.49)) < 1e-9);
    CHECK(s.left.holdDb >= s.left.levelDb);
}

// The published latch is the authoritative one: a clip between two UI frames never appears in
// the block peak this frame samples, so dropping it would silently lose the report.
static void testClipLatchesFromThePublishedFlagAndClearsOnDemand() {
    MasterMeterUi s;
    CHECK(!meterClipped(s));
    s = advanceMasterMeter(s, {0.25, 0.25, 1.0, /*clip=*/true}, 0.1);
    CHECK(meterClipped(s));
    // Latched: quiet frames do not lower it.
    s = advanceMasterMeter(s, {0.0, 0.0, 1.0, false}, 5.0);
    CHECK(meterClipped(s));
    s = clearMasterMeterClip(s);
    CHECK(!meterClipped(s));
    // And the UI's own sample latches it too, when the loud block IS the one sampled.
    s = advanceMasterMeter(s, {1.0, 0.0, 1.0, false}, 0.1);
    CHECK(meterClipped(s));
}

static void testGrLampLitOnlyWhileTheLimiterReduces() {
    MasterMeterUi s;
    CHECK(!grLampLit(s));
    // A gain of 1 is no reduction, however long it is held.
    s = advanceMasterMeter(s, {0.5, 0.5, 1.0, false}, 0.1);
    CHECK(s.reductionDb == 0.0);
    CHECK(!grLampLit(s));

    // ~6 dB of reduction lights it, and arms the hold.
    s = advanceMasterMeter(s, {0.5, 0.5, 0.5, false}, 0.1);
    CHECK(std::fabs(s.reductionDb - 6.0206) < 1e-3);
    CHECK(grLampLit(s));
    CHECK(s.reductionHoldSeconds == kMeterPeakHoldSeconds);

    // Held flat, not decaying, for its whole window — the peak tick's own contract.
    s = advanceMasterMeter(s, {0.5, 0.5, 1.0, false}, kMeterPeakHoldSeconds - 0.01);
    CHECK(std::fabs(s.reductionDb - 6.0206) < 1e-3);
    CHECK(grLampLit(s));

    // Past the window it releases at the meter's 20 dB/s, and 6 dB of catch is gone inside a
    // third of a second of release.
    s = advanceMasterMeter(s, {0.5, 0.5, 1.0, false}, 1.0);
    CHECK(!grLampLit(s));
    CHECK(s.reductionDb == 0.0);
}

// The cadence the lamp ACTUALLY runs at is editor_platform's 500 ms sync tick, and the whole
// point of the hold is that the lamp survives it. Without one, a 6 dB catch decays 20 x 0.5 =
// 10 dB on the very next frame and clamps to 0 — lit for exactly one repaint. Pinned in frames,
// because "how many times does this draw lit" is arithmetic, not a look.
static void testGrLampSurvivesTheFiveHundredMillisecondTick() {
    constexpr double kTick = 0.5;  // editor_platform.cpp's kSyncTimerIntervalMs
    MasterMeterUi s = advanceMasterMeter(MasterMeterUi{}, {0.5, 0.5, 0.5, false}, kTick);
    CHECK(grLampLit(s));

    int litFrames = 1;
    for (int i = 0; i < 20 && grLampLit(s); ++i) {
        s = advanceMasterMeter(s, {0.5, 0.5, 1.0, false}, kTick);
        if (grLampLit(s)) ++litFrames;
    }
    // 1.5 s of hold spans the tick that armed it plus three more, and the release then takes
    // 6.02 dB below the 0.5 dB floor within one further 10 dB step.
    CHECK(litFrames == 4);
    CHECK(!grLampLit(s));

    // A catch the previous frame does not shorten: a SECOND catch re-arms the full window.
    MasterMeterUi t = advanceMasterMeter(MasterMeterUi{}, {0.5, 0.5, 0.5, false}, kTick);
    t = advanceMasterMeter(t, {0.5, 0.5, 1.0, false}, kTick);
    t = advanceMasterMeter(t, {0.5, 0.5, 0.5, false}, kTick);
    CHECK(t.reductionHoldSeconds == kMeterPeakHoldSeconds);
}

// The one bar a single-lane column draws folds the two channels per FIELD. Picking whichever
// channel won on level would draw the OTHER channel's hold tick and clip nowhere.
static void testSingleLaneStateFoldsBothChannelsPerField() {
    MasterMeterUi m;
    m.left.levelDb = -30.0;
    m.left.holdDb = -2.0;          // left is quieter now but held the loudest peak
    m.right.levelDb = -10.0;
    m.right.holdDb = -8.0;
    m.left.clip = true;            // and only left ever clipped
    m.right.clip = false;

    const instrument::engine::MeterState s = meterSingleLaneState(m);
    CHECK(s.levelDb == -10.0);     // the louder channel's bar
    CHECK(s.holdDb == -2.0);       // but the higher hold tick, which is the other channel's
    CHECK(s.clip);                 // and the clip, which a level pick would have dropped
}

// The tick repaints only on a change, so what counts as a change has to cover every drawn
// quantity — and only those.
static void testDrawEqualityCoversTheDrawnQuantities() {
    MasterMeterUi a;
    CHECK(meterDrawEqual(a, a));

    MasterMeterUi loud = advanceMasterMeter(a, {1.0, 0.0, 1.0, false}, 0.1);
    CHECK(!meterDrawEqual(a, loud));           // bar + hold tick moved

    MasterMeterUi clipped = a;
    clipped.left.clip = true;
    CHECK(!meterDrawEqual(a, clipped));        // the cap appeared

    MasterMeterUi lamp = a;
    lamp.reductionDb = kGrLampFloorDb;
    CHECK(!meterDrawEqual(a, lamp));           // the lamp lit

    // Reduction that does not cross the lamp's floor draws identically — the state differs,
    // the picture does not, and a repaint there would be pure cost.
    MasterMeterUi graze = a;
    graze.reductionDb = kGrLampFloorDb / 2.0;
    CHECK(meterDrawEqual(a, graze));
}

// The editor's WM_PAINT fast-path test: a dirty rect wholly inside the field takes the
// meter-only redraw; anything that pokes outside it, or a field that hasn't been laid out yet,
// falls back to the full compose.
static void testFastPathEligibleOnlyForADirtyRectInsideTheField() {
    const MeterRects m = meterRects(kColumn, LaneSplit::Single);
    CHECK(meterFastPathEligible(m, m.field));                    // the whole field
    CHECK(meterFastPathEligible(
        m, Rect::ltrb(m.field.x + 2, m.field.y + 2, m.field.right() - 2, m.field.bottom() - 2)));
    CHECK(!meterFastPathEligible(m, Rect::ltrb(m.field.x - 1, m.field.y, m.field.right(),
                                               m.field.bottom())));  // pokes left of the field
    CHECK(!meterFastPathEligible(
        m, Rect::ltrb(kColumn.x, kColumn.y, kColumn.right(), kColumn.bottom())));  // whole column
    CHECK(!meterFastPathEligible(MeterRects{}, m.field));  // no cached layout at all
}

// The self-containment invariant drawMeterField's redraw-the-field-alone shortcut rests on:
// every bar meterRects() hands back stays inside the field, in both lane splits.
static void testFieldBoundsEveryBarItPaints() {
    CHECK(meterBarsWithinField(meterRects(kColumn, LaneSplit::Single)));
    CHECK(meterBarsWithinField(meterRects(kColumn, LaneSplit::Stereo)));
    CHECK(meterBarsWithinField(MeterRects{}));  // degenerate: nothing to bound, nothing drawn
}

static void testDegenerateColumnYieldsNothing() {
    const MeterRects m = meterRects(Rect::ltrb(0, 0, 0, 0), LaneSplit::Stereo);
    CHECK(m.field.empty() && m.barA.empty() && m.barB.empty());
}

int main() {
    testColumnDividesIntoGutterAndBarField();
    testMonoDrawsOneWideBarAndStereoDrawsTwo();
    testBarCountFollowsTheWaveformsOwnLaneSplit();
    testDbAxisSpansTheFieldAndClamps();
    testEveryOtherTickCarriesANumeral();
    testTheFloorNumeralStaysInsideTheGutter();
    testAColumnTooNarrowForTheInteriorDrawsNothing();
    testFastPathEligibleOnlyForADirtyRectInsideTheField();
    testFieldBoundsEveryBarItPaints();
    testPeakRisesAtOnceAndFallsAtTwentyDbPerSecond();
    testPeakHoldSitsForItsFullWindowThenReleases();
    testClipLatchesFromThePublishedFlagAndClearsOnDemand();
    testSingleLaneStateFoldsBothChannelsPerField();
    testGrLampLitOnlyWhileTheLimiterReduces();
    testGrLampSurvivesTheFiveHundredMillisecondTick();
    testDrawEqualityCoversTheDrawnQuantities();
    testDegenerateColumnYieldsNothing();
    if (g_fail) {
        std::printf("%d FAILURE(S)\n", g_fail);
        return 1;
    }
    std::printf("master_meter tests passed\n");
    return 0;
}
