// Standalone tests for reasampler::peaks — no REAPER, no test framework.
// Same fast build/run loop as bank_model_tests: feed a known signal, assert the
// envelope.
//
// Covers (PLAN.md M2 / CONTEXT.md §peaks): full-scale sine envelope ~= +/-amp;
// ramp envelope monotonic across bins; DC/silence -> min==max; multi-channel
// independence (no fold); short buffer (fewer frames than bins) and non-divisible
// length (remainder bin); binCount==1 whole-buffer envelope; zero frames / zero
// channels / binCount==0 degenerate inputs. Plus columnMinMax (the display-side
// per-pixel-column collapse, FA3): 1:1 passthrough, upsample fallback, downsample
// merge, steep disjoint-span merge, no-bin-dropped spike sweep, no-column-empty
// coverage, col clamp, degenerate inputs.

#include "../src/core/audio/peaks.h"

#include <cmath>
#include <cstdio>
#include <vector>

using namespace reasampler;
using namespace reasampler::audio;

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 approx(float a, float b, float tol) { return std::fabs(a - b) <= tol; }

// Local pi — M_PI is not a standard macro (MSVC omits it without _USE_MATH_DEFINES).
constexpr double kPi = 3.14159265358979323846;

// A full-scale sine over `frames` frames, mono, `cycles` complete periods so every
// bin sees both a near-peak and a near-trough.
static std::vector<AudioSample> monoSine(std::size_t frames, double cycles, float amp) {
    std::vector<AudioSample> buf(frames);
    for (std::size_t i = 0; i < frames; ++i) {
        const double phase = 2.0 * kPi * cycles * (double)i / (double)frames;
        buf[i] = amp * (float)std::sin(phase);
    }
    return buf;
}

// Full-scale sine: every bin's [min,max] should reach ~ [-amp, +amp].
static void testSineEnvelope() {
    const std::size_t frames = 48000;
    const float amp = 1.0f;
    const std::size_t bins = 64;
    // Many cycles per bin so each bin brackets a full peak and trough.
    auto buf = monoSine(frames, /*cycles=*/128.0, amp);

    Envelope env = computeEnvelope(buf, 1, frames, bins);
    CHECK(env.size() == 1);
    CHECK(env[0].size() == bins);

    for (const MinMax& mm : env[0]) {
        CHECK(mm.min <= mm.max);
        CHECK(approx(mm.max, amp, 0.02f));   // reaches near +amp
        CHECK(approx(mm.min, -amp, 0.02f));  // reaches near -amp
    }
}

// Monotonic ramp 0..1: each bin's max must not decrease across bins, and likewise
// each bin's min, since the signal only ever rises.
static void testRampMonotonic() {
    const std::size_t frames = 10000;
    const std::size_t bins = 50;
    std::vector<AudioSample> buf(frames);
    for (std::size_t i = 0; i < frames; ++i) {
        buf[i] = (float)i / (float)(frames - 1); // 0.0 .. 1.0
    }

    Envelope env = computeEnvelope(buf, 1, frames, bins);
    CHECK(env[0].size() == bins);

    // For a rising signal, per-bin min == the bin's first sample and max == last,
    // and both sequences are non-decreasing across bins.
    for (std::size_t b = 0; b < bins; ++b) {
        CHECK(env[0][b].min <= env[0][b].max);
        if (b > 0) {
            CHECK(env[0][b].min >= env[0][b - 1].min);
            CHECK(env[0][b].max >= env[0][b - 1].max);
        }
    }
    // First bin starts at ~0, last bin ends at ~1.
    CHECK(approx(env[0].front().min, 0.0f, 1e-3f));
    CHECK(approx(env[0].back().max, 1.0f, 1e-3f));

    // Exact-value check on a known bin to confirm min==first sample, max==last sample
    // (not just monotonicity). With frames=10000, bins=50, each bin spans 200 frames.
    // Bin 25: frames [5000,5200); first sample = 5000/9999, last = 5199/9999.
    {
        const float expectedMin = 5000.0f / (float)(frames - 1);
        const float expectedMax = 5199.0f / (float)(frames - 1);
        CHECK(env[0][25].min == expectedMin);
        CHECK(env[0][25].max == expectedMax);
    }
}

// DC / silence: min == max == the DC level in every bin (no spurious spread).
static void testDcAndSilence() {
    const std::size_t frames = 1000;
    const std::size_t bins = 16;

    std::vector<AudioSample> silence(frames, 0.0f);
    Envelope se = computeEnvelope(silence, 1, frames, bins);
    for (const MinMax& mm : se[0]) {
        CHECK(mm.min == 0.0f);
        CHECK(mm.max == 0.0f);
    }

    std::vector<AudioSample> dc(frames, 0.5f);
    Envelope de = computeEnvelope(dc, 1, frames, bins);
    for (const MinMax& mm : de[0]) {
        CHECK(mm.min == 0.5f);
        CHECK(mm.max == 0.5f); // min == max: DC has no envelope spread
    }
}

// Multi-channel independence: ch0 full-scale sine, ch1 silent. Proves no fold:
// ch1 must stay flat zero regardless of ch0's swing.
static void testMultiChannelNoFold() {
    const std::size_t frames = 8000;
    const std::size_t bins = 32;
    const float amp = 0.9f;

    // Interleave: [ch0, ch1] per frame; ch0 = sine, ch1 = 0.
    auto sine = monoSine(frames, /*cycles=*/64.0, amp);
    std::vector<AudioSample> buf(frames * 2);
    for (std::size_t i = 0; i < frames; ++i) {
        buf[i * 2 + 0] = sine[i];
        buf[i * 2 + 1] = 0.0f;
    }

    Envelope env = computeEnvelope(buf, 2, frames, bins);
    CHECK(env.size() == 2);
    CHECK(env[0].size() == bins);
    CHECK(env[1].size() == bins);

    for (const MinMax& mm : env[0]) {
        CHECK(approx(mm.max, amp, 0.05f));
        CHECK(approx(mm.min, -amp, 0.05f));
    }
    for (const MinMax& mm : env[1]) {
        CHECK(mm.min == 0.0f); // silent channel stays silent — not averaged with ch0
        CHECK(mm.max == 0.0f);
    }
}

// A distinct-per-channel check that would visibly fail under any averaging: ch0
// constant +1, ch1 constant -1. A fold would give 0; independence keeps +1 / -1.
static void testChannelsNotAveraged() {
    const std::size_t frames = 100;
    const std::size_t bins = 4;
    std::vector<AudioSample> buf(frames * 2);
    for (std::size_t i = 0; i < frames; ++i) {
        buf[i * 2 + 0] = 1.0f;
        buf[i * 2 + 1] = -1.0f;
    }
    Envelope env = computeEnvelope(buf, 2, frames, bins);
    for (std::size_t b = 0; b < bins; ++b) {
        CHECK(env[0][b].min == 1.0f && env[0][b].max == 1.0f);
        CHECK(env[1][b].min == -1.0f && env[1][b].max == -1.0f);
    }
}

// Short buffer: fewer frames than bins. Frames that land in a bin are correct;
// trailing bins with no frame are {0,0}. No OOB.
static void testShortBuffer() {
    const std::size_t frames = 3;
    const std::size_t bins = 8;
    std::vector<AudioSample> buf = {0.25f, -0.5f, 0.75f};

    Envelope env = computeEnvelope(buf, 1, frames, bins);
    CHECK(env[0].size() == bins);

    // With 3 frames over 8 bins, spans [b*3/8,(b+1)*3/8) place one frame in bins
    // 2, 5, 7 and leave the rest empty. Assert exactly which bins are populated so
    // the partition (not just "no crash") is verified.
    int populated = 0;
    for (std::size_t b = 0; b < bins; ++b) {
        const MinMax& mm = env[0][b];
        if (mm.min != 0.0f || mm.max != 0.0f) ++populated;
        CHECK(mm.min <= mm.max);
    }
    CHECK(populated == 3); // every input frame landed in exactly one bin, none lost
    // Bin spans: floor(b*3/8): b=2 -> frame0(0.25), b=5 -> frame1(-0.5), b=7 -> frame2(0.75).
    CHECK(env[0][2].min == 0.25f && env[0][2].max == 0.25f);
    CHECK(env[0][5].min == -0.5f && env[0][5].max == -0.5f);
    CHECK(env[0][7].min == 0.75f && env[0][7].max == 0.75f);
}

// Non-divisible length: 10 frames over 3 bins. Spans are [0,3),[3,6),[6,10) — the
// remainder (last) bin absorbs the extra frames; no sample dropped.
static void testNonDivisibleRemainderBin() {
    const std::size_t frames = 10;
    const std::size_t bins = 3;
    std::vector<AudioSample> buf(frames);
    for (std::size_t i = 0; i < frames; ++i) buf[i] = (float)i; // 0..9

    Envelope env = computeEnvelope(buf, 1, frames, bins);
    CHECK(env[0].size() == bins);
    // [0,3): {0..2} -> min 0, max 2
    CHECK(env[0][0].min == 0.0f && env[0][0].max == 2.0f);
    // [3,6): {3..5} -> min 3, max 5
    CHECK(env[0][1].min == 3.0f && env[0][1].max == 5.0f);
    // [6,10): {6..9} -> min 6, max 9 — remainder frames 6..9 all included
    CHECK(env[0][2].min == 6.0f && env[0][2].max == 9.0f);
}

// binCount == 1: the whole buffer collapses to a single min/max.
static void testSingleBinWholeBuffer() {
    std::vector<AudioSample> buf = {-0.3f, 0.8f, -0.9f, 0.1f, 0.4f};
    Envelope env = computeEnvelope(buf, 1, buf.size(), 1);
    CHECK(env[0].size() == 1);
    CHECK(env[0][0].min == -0.9f);
    CHECK(env[0][0].max == 0.8f);
}

// Degenerate inputs: defined behavior, no UB, no throw.
static void testDegenerateInputs() {
    std::vector<AudioSample> buf = {0.1f, 0.2f, 0.3f, 0.4f};

    // Zero frames -> binCount bins, all {0,0}.
    Envelope zf = computeEnvelope(buf, 1, 0, 4);
    CHECK(zf.size() == 1 && zf[0].size() == 4);
    for (const MinMax& mm : zf[0]) CHECK(mm.min == 0.0f && mm.max == 0.0f);

    // Zero channels -> empty envelope.
    Envelope zc = computeEnvelope(buf, 0, 4, 4);
    CHECK(zc.empty());

    // binCount == 0 -> one channel, empty bin vector.
    Envelope zb = computeEnvelope(buf, 1, 4, 0);
    CHECK(zb.size() == 1 && zb[0].empty());

    // frameCount overstates the buffer: clamps to available frames, no OOB.
    // buf holds 4 mono frames; ask for 100. Must not read past the buffer.
    Envelope over = computeEnvelope(buf, 1, 100, 2);
    CHECK(over.size() == 1 && over[0].size() == 2);
    // [0,2) of the 4 real frames -> min .1 max .2 ; [2,4) -> min .3 max .4
    CHECK(over[0][0].min == 0.1f && over[0][0].max == 0.2f);
    CHECK(over[0][1].min == 0.3f && over[0][1].max == 0.4f);

    // Empty buffer, non-zero request -> all-zero bins, no crash.
    std::vector<AudioSample> empty;
    Envelope eb = computeEnvelope(empty, 2, 10, 3);
    CHECK(eb.size() == 2);
    for (const auto& chenv : eb) {
        CHECK(chenv.size() == 3);
        for (const MinMax& mm : chenv) CHECK(mm.min == 0.0f && mm.max == 0.0f);
    }
}

// binCount > frameCount with frames >= 2: under the old un-guarded formula,
// b*frames (or (b+1)*frames) overflows size_t for b near SIZE_MAX/frames.
// This test exercises the sparse binCount > frames code path with a concrete
// allocatable binCount and verifies populated bins have correct values and
// all empty bins remain {0,0}.
static void testLargeBinCountOverflowGuard() {
    // frames=4, binCount=9 (> frames, sparse). The overflow guard protects the
    // same loop iteration path that would UB for pathological binCount near SIZE_MAX.
    const std::size_t frames = 4;
    const std::size_t binCount = 9;
    std::vector<AudioSample> buf = {0.1f, 0.2f, 0.3f, 0.4f};

    Envelope env = computeEnvelope(buf, 1, frames, binCount);
    CHECK(env.size() == 1);
    CHECK(env[0].size() == binCount);

    // Partition [b*4/9, (b+1)*4/9):
    // b=2: [0,1) -> frame 0 = 0.1   b=4: [1,2) -> frame 1 = 0.2
    // b=6: [2,3) -> frame 2 = 0.3   b=8: [3,4) -> frame 3 = 0.4
    // b=0,1,3,5,7: empty spans -> {0,0}
    CHECK(env[0][2].min == 0.1f && env[0][2].max == 0.1f);
    CHECK(env[0][4].min == 0.2f && env[0][4].max == 0.2f);
    CHECK(env[0][6].min == 0.3f && env[0][6].max == 0.3f);
    CHECK(env[0][8].min == 0.4f && env[0][8].max == 0.4f);
    CHECK(env[0][0].min == 0.0f && env[0][0].max == 0.0f);
    CHECK(env[0][1].min == 0.0f && env[0][1].max == 0.0f);
    CHECK(env[0][3].min == 0.0f && env[0][3].max == 0.0f);
    CHECK(env[0][5].min == 0.0f && env[0][5].max == 0.0f);
    CHECK(env[0][7].min == 0.0f && env[0][7].max == 0.0f);
}

// --- lastFrameAboveThreshold: the realtime tail's decay-scan boundary primitive --

// A mono decaying ramp: frame i has amplitude that falls linearly to zero. With a
// threshold set between two frames' levels, the last frame above it is deterministic.
static void testLastFrameDecayingRamp() {
    // 10 mono frames, amplitude 1.0 - i*0.1: frame0=1.0 ... frame9=0.1.
    std::vector<AudioSample> buf(10);
    for (std::size_t i = 0; i < 10; ++i) buf[i] = 1.0f - 0.1f * (float)i;

    // Threshold 0.35: frames 0..6 (levels 1.0..0.4) exceed it; frame 6 is the last
    // (level 0.4 > 0.35), frame 7 (0.3) does not. Strict > semantics.
    CHECK(lastFrameAboveThreshold(buf, 1, 10, 0.35f) == 6);

    // Threshold just under frame 9's level (0.1): the very last frame stays.
    CHECK(lastFrameAboveThreshold(buf, 1, 10, 0.05f) == 9);

    // Threshold above the loudest frame: nothing survives.
    CHECK(lastFrameAboveThreshold(buf, 1, 10, 1.5f) == kNoFrameAboveThreshold);
}

// Pure silence at or below the threshold -> sentinel (the "trim back to end" case:
// no frame in the tail window exceeds -72 dB).
static void testLastFrameSilence() {
    std::vector<AudioSample> zeros(20, 0.0f);
    CHECK(lastFrameAboveThreshold(zeros, 2, 10, 0.001f) == kNoFrameAboveThreshold);

    // A DC level exactly AT the threshold does not count (strict >).
    std::vector<AudioSample> atThresh(8, 0.25f);
    CHECK(lastFrameAboveThreshold(atThresh, 1, 8, 0.25f) == kNoFrameAboveThreshold);
}

// Every frame above the threshold (a non-decaying source): the last frame is the
// boundary — the caller keeps the whole window (the 8 s cap did its job).
static void testLastFrameAllAbove() {
    std::vector<AudioSample> loud(12, 0.8f);  // 6 stereo frames
    CHECK(lastFrameAboveThreshold(loud, 2, 6, 0.1f) == 5);
}

// Per-frame peak is the MAX abs across channels (no fold): a frame with one loud
// channel and one silent channel is "above" on the strength of the loud one, and a
// negative sample is compared by magnitude.
static void testLastFramePerChannelMaxAbs() {
    // 3 stereo frames. Frame0: (0.9, 0.0) loud L. Frame1: (0.0, -0.9) loud R (negative
    // -> abs). Frame2: (0.05, -0.05) both quiet.
    std::vector<AudioSample> buf = {0.9f, 0.0f,  0.0f, -0.9f,  0.05f, -0.05f};
    // Threshold 0.5: frame2 is below (peak 0.05), frame1 is above (|-0.9|=0.9).
    CHECK(lastFrameAboveThreshold(buf, 2, 3, 0.5f) == 1);
    // If both channels of the last frame mattered independently, a fold-average
    // (0.9+0.0)/2 = 0.45 on frame0 would fall below 0.5 — but frame0's L alone (0.9)
    // is above, proving max-abs, not average. Lower the threshold to isolate frame0.
    std::vector<AudioSample> f0 = {0.9f, 0.0f};
    CHECK(lastFrameAboveThreshold(f0, 2, 1, 0.5f) == 0);
}

// Degenerate: zero channels, zero frames, and a frameCount that overstates the
// buffer (must clamp to available frames, no OOB read).
static void testLastFrameDegenerate() {
    std::vector<AudioSample> buf = {0.5f, 0.5f, 0.5f, 0.5f};  // 2 stereo frames

    CHECK(lastFrameAboveThreshold(buf, 0, 2, 0.1f) == kNoFrameAboveThreshold);
    CHECK(lastFrameAboveThreshold(buf, 2, 0, 0.1f) == kNoFrameAboveThreshold);

    std::vector<AudioSample> empty;
    CHECK(lastFrameAboveThreshold(empty, 2, 10, 0.1f) == kNoFrameAboveThreshold);

    // frameCount=100 but only 2 real stereo frames: clamps to frame 1 (the last real
    // frame), which is above -> index 1, no read past the buffer.
    CHECK(lastFrameAboveThreshold(buf, 2, 100, 0.1f) == 1);
}

// --- columnMinMax (display-side per-pixel-column collapse, FA3) ----------------

// Build a ChannelEnvelope from parallel min/max arrays.
static ChannelEnvelope makeEnvelope(const std::vector<float>& mins,
                                    const std::vector<float>& maxs) {
    ChannelEnvelope env(mins.size());
    for (std::size_t i = 0; i < mins.size(); ++i) env[i] = MinMax{mins[i], maxs[i]};
    return env;
}

static void testColumnOneToOne() {
    // 4 bins, 4 columns: each column is a pure passthrough of its one bin.
    ChannelEnvelope env = makeEnvelope({-1.0f, -0.5f, 0.0f, 0.5f},
                                       { 0.5f,  0.0f, 0.5f, 1.0f});
    CHECK(columnMinMax(env, 4, 0).min == -1.0f && columnMinMax(env, 4, 0).max == 0.5f);
    CHECK(columnMinMax(env, 4, 1).min == -0.5f && columnMinMax(env, 4, 1).max == 0.0f);
    CHECK(columnMinMax(env, 4, 2).min ==  0.0f && columnMinMax(env, 4, 2).max == 0.5f);
    CHECK(columnMinMax(env, 4, 3).min ==  0.5f && columnMinMax(env, 4, 3).max == 1.0f);
}

static void testColumnUpsampleFallback() {
    // 2 bins, 4 columns: columns 0,1 fall back to enclosing bin 0; 2,3 to bin 1 —
    // no column left empty when there are more columns than bins.
    ChannelEnvelope env = makeEnvelope({-1.0f, 0.5f}, {0.0f, 1.0f});
    CHECK(columnMinMax(env, 4, 0).min == -1.0f && columnMinMax(env, 4, 0).max == 0.0f);
    CHECK(columnMinMax(env, 4, 1).min == -1.0f && columnMinMax(env, 4, 1).max == 0.0f);
    CHECK(columnMinMax(env, 4, 2).min ==  0.5f && columnMinMax(env, 4, 2).max == 1.0f);
    CHECK(columnMinMax(env, 4, 3).min ==  0.5f && columnMinMax(env, 4, 3).max == 1.0f);
}

static void testColumnDownsampleMerge() {
    // 4 bins, 2 columns: each column is the true min/max union of its 2 bins.
    ChannelEnvelope env = makeEnvelope({-1.0f, -0.5f, 0.0f, 0.5f},
                                       { 0.5f,  0.0f, 0.5f, 1.0f});
    CHECK(columnMinMax(env, 2, 0).min == -1.0f && columnMinMax(env, 2, 0).max == 0.5f);
    CHECK(columnMinMax(env, 2, 1).min ==  0.0f && columnMinMax(env, 2, 1).max == 1.0f);
}

static void testColumnSteepDisjointSpans() {
    // THE anti-alias case: two adjacent bins holding DISJOINT spans (a steep edge —
    // all-positive then all-negative). Merged into one column the result must bridge
    // both extremes as one full span {-1.0, 1.0}. The old per-bin overdraw could never
    // produce a wrong value here — only the merge path exercises this.
    ChannelEnvelope env = makeEnvelope({0.9f, -1.0f}, {1.0f, -0.9f});
    const MinMax mm = columnMinMax(env, 1, 0);
    CHECK(mm.min == -1.0f && mm.max == 1.0f);
}

static void testColumnNoBinDropped() {
    // Downsample coverage: EVERY bin must land in some column (union of columns ==
    // union of bins). For several non-integer ratios, plant a lone {-1,+1} spike in
    // bin j (all other bins {0,0}) and assert some column reports it — a partition
    // that skipped bin j would lose the spike entirely.
    const struct { int nbins; int cols; } cases[] = {{7, 3}, {10, 4}, {9, 2}, {6, 10}};
    for (const auto& c : cases) {
        for (int j = 0; j < c.nbins; ++j) {
            ChannelEnvelope env(static_cast<std::size_t>(c.nbins));  // all {0,0}
            env[static_cast<std::size_t>(j)] = MinMax{-1.0f, 1.0f};
            bool found = false;
            for (int col = 0; col < c.cols; ++col) {
                const MinMax mm = columnMinMax(env, c.cols, col);
                if (mm.min == -1.0f && mm.max == 1.0f) { found = true; break; }
            }
            CHECK(found);
        }
    }
}

static void testColumnNoColumnEmpty() {
    // Gap-free coverage: 6 bins over 10 columns (non-integer ratio) — every column
    // must carry a real bin's values (all bins strictly positive, so a default {0,0}
    // would expose a skipped column).
    ChannelEnvelope env = makeEnvelope({0.1f, 0.2f, 0.3f, 0.4f, 0.5f, 0.6f},
                                       {0.2f, 0.3f, 0.4f, 0.5f, 0.6f, 0.7f});
    for (int col = 0; col < 10; ++col) {
        const MinMax mm = columnMinMax(env, 10, col);
        CHECK(mm.min >= 0.1f && mm.max <= 0.7f);
        CHECK(mm.min <= mm.max);
    }
}

static void testColumnColClamp() {
    // col outside [0, columnCount-1] clamps: negative to the first, large to the last.
    ChannelEnvelope env = makeEnvelope({-0.5f, 0.5f}, {-0.1f, 0.9f});
    CHECK(columnMinMax(env, 2, -5).min == -0.5f);
    CHECK(columnMinMax(env, 2, 999).max == 0.9f);
}

static void testColumnDegenerate() {
    // Empty envelope, columnCount <= 0 -> {0, 0}.
    ChannelEnvelope empty;
    const MinMax z = columnMinMax(empty, 4, 0);
    CHECK(z.min == 0.0f && z.max == 0.0f);
    ChannelEnvelope env = makeEnvelope({0.3f}, {0.7f});
    const MinMax z2 = columnMinMax(env, 0, 0);
    CHECK(z2.min == 0.0f && z2.max == 0.0f);
    const MinMax z3 = columnMinMax(env, -1, 0);
    CHECK(z3.min == 0.0f && z3.max == 0.0f);
}

int main() {
    testSineEnvelope();
    testRampMonotonic();
    testDcAndSilence();
    testMultiChannelNoFold();
    testChannelsNotAveraged();
    testShortBuffer();
    testNonDivisibleRemainderBin();
    testSingleBinWholeBuffer();
    testDegenerateInputs();
    testLargeBinCountOverflowGuard();
    testLastFrameDecayingRamp();
    testLastFrameSilence();
    testLastFrameAllAbove();
    testLastFramePerChannelMaxAbs();
    testLastFrameDegenerate();
    testColumnOneToOne();
    testColumnUpsampleFallback();
    testColumnDownsampleMerge();
    testColumnSteepDisjointSpans();
    testColumnNoBinDropped();
    testColumnNoColumnEmpty();
    testColumnColClamp();
    testColumnDegenerate();

    if (g_fail == 0) std::printf("All tests passed.\n");
    return g_fail ? 1 : 0;
}
