/*
 * test_recv_noise_reduction — receive-side NR actually denoises a STEREO voice stream, and
 * never touches a screen-audio share. Regression for the bug where a stereo mic (shipped in
 * f72219d) silently bypassed listener-side NR because the decode loop gated on
 * `dec_channels == 1` as a proxy for "is voice" (docs/voice.md §10).
 *
 * White-box: drive AudioEngine in external-playback mode (the mixer-timer thread decodes+mixes
 * with no hardware device, same as test_external_playback) and tap the per-stream pcm_sink, which
 * fires AFTER the receive-side NR pass. We feed deterministic stereo white noise (paced at the
 * 20 ms engine cadence so the jitter buffer's catch-up never drops the backlog) and compare the
 * played-out noise floor across three runs:
 *   • voice stream, NR off  → baseline (decode only)
 *   • voice stream, NR on   → RNNoise folds stereo→mono and collapses the noise floor
 *   • screen-audio (is_voice=false), NR on → NR is skipped; floor ≈ baseline
 *
 * Registered only under VOICECAT_USE_VCPKG_DEPS, where VOICECAT_HAS_NS is defined (a passthrough
 * build would correctly fail the reduction assertion).
 */
#include <atomic>
#include <chrono>
#include <cmath>
#include <cstdint>
#include <cstdio>
#include <thread>
#include <vector>

#if defined(VOICECAT_HAS_AUDIO) && defined(VOICECAT_HAS_OPUS)

#include "audio/audio_engine.h"
#include "codec/opus_codec.h"

static int g_failures = 0;
#define CHECK(cond)                                                       \
    do {                                                                  \
        if (!(cond)) {                                                    \
            std::printf("FAIL [%s:%d]: %s\n", __FILE__, __LINE__, #cond); \
            ++g_failures;                                                 \
        }                                                                 \
    } while (0)

namespace vca = voicecat::audio;
namespace vcc = voicecat::codec;

// pcm_sink accumulator (written on the mixer-timer thread, read by main after stop()).
struct Sink {
    std::atomic<int>       frames{0};
    std::atomic<long long> sumsq{0};
    std::atomic<long long> count{0};
    std::atomic<uint32_t>  last_ch{0};
    int                    warmup = 0;  // skip RNNoise's recurrent-state settle window
};

static void sink_cb(void* user, uint32_t, uint32_t, const int16_t* pcm, size_t spc, uint32_t ch,
                    uint32_t) {
    auto* s = static_cast<Sink*>(user);
    int   f = s->frames.fetch_add(1, std::memory_order_relaxed);
    s->last_ch.store(ch, std::memory_order_relaxed);
    if (f < s->warmup) return;
    const size_t total = spc * ch;
    long long    sq    = 0;
    for (size_t i = 0; i < total; ++i) {
        long long v = pcm[i];
        sq += v * v;
    }
    s->sumsq.fetch_add(sq, std::memory_order_relaxed);
    s->count.fetch_add(static_cast<long long>(total), std::memory_order_relaxed);
}

// Run one playout scenario and return the RMS of the played-out PCM, or -1 if nothing played.
static double run_scenario(bool is_voice, bool nr_on,
                           const std::vector<std::vector<uint8_t>>& frames, const vcc::OpusParams& op,
                           int frame_samples) {
    vca::AudioEngine engine;
    engine.set_external_playback(true);
    Sink sink;
    sink.warmup = 20;
    engine.set_pcm_sink(&sink_cb, &sink);

    vca::AudioParams p;
    p.sample_rate      = 48000;
    p.capture_channels = 1;
    p.playback_channels = 2;
    p.frame_ms         = 20;
    if (!engine.start(p)) return -1;

    const uint32_t ssrc = 1;
    engine.init_recv_stream(ssrc, op, /*user_id=*/7, /*stream_id=*/3, is_voice);
    if (nr_on) engine.set_stream_noise_reduction(ssrc, true);

    uint32_t ts = 0;
    for (size_t i = 0; i < frames.size(); ++i) {
        vca::JitterBuffer::Frame f;
        f.seq         = static_cast<uint64_t>(i);
        f.timestamp   = ts;
        f.fec_present = false;
        f.payload     = frames[i];
        engine.push_recv_frame(ssrc, std::move(f));
        ts += static_cast<uint32_t>(frame_samples);
        // Pace at the engine cadence so production ≈ consumption: the jitter buffer never builds a
        // backlog big enough to trigger catch-up frame-dropping, so every frame is decoded.
        std::this_thread::sleep_for(std::chrono::milliseconds(20));
    }
    std::this_thread::sleep_for(std::chrono::milliseconds(120));  // drain
    engine.stop();

    CHECK(sink.last_ch.load(std::memory_order_relaxed) == 2);  // stereo throughout
    long long c = sink.count.load(std::memory_order_relaxed);
    if (c == 0) return -1;
    return std::sqrt(static_cast<double>(sink.sumsq.load(std::memory_order_relaxed)) /
                     static_cast<double>(c));
}

int main() {
    // Stereo Opus params (a stereo mic with send-side NR off arrives exactly like this).
    vcc::OpusParams op;
    op.sample_rate = 48000;
    op.frame_ms    = 20;
    op.stereo      = true;
    int frame_samples = vcc::opus_frame_samples(op);  // 960 per channel

    // Encode a run of deterministic interleaved-stereo white noise (L and R independent so the
    // bitstream is genuinely stereo, not L==R).
    uint32_t rng       = 0xC0FFEEu;
    auto     next_noise = [&]() -> int16_t {
        rng ^= rng << 13;
        rng ^= rng >> 17;
        rng ^= rng << 5;
        return static_cast<int16_t>((static_cast<int32_t>(rng % 6001)) - 3000);  // ~[-3000,3000]
    };

    vcc::OpusEncoder enc;
    CHECK(enc.init(op));
    const int                         kFrames = 80;  // ~1.6 s; warmup 20 leaves ~60 measured
    std::vector<std::vector<uint8_t>> frames;
    frames.reserve(kFrames);
    std::vector<int16_t> interleaved(static_cast<size_t>(frame_samples) * 2);
    uint8_t              opus_buf[1500];
    for (int f = 0; f < kFrames; ++f) {
        for (int i = 0; i < frame_samples * 2; ++i) interleaved[i] = next_noise();
        int len = enc.encode(interleaved.data(), frame_samples, opus_buf, sizeof(opus_buf));
        CHECK(len > 0);
        frames.emplace_back(opus_buf, opus_buf + len);
    }
    enc.destroy();

    double voice_off = run_scenario(/*is_voice=*/true, /*nr_on=*/false, frames, op, frame_samples);
    double voice_on  = run_scenario(/*is_voice=*/true, /*nr_on=*/true, frames, op, frame_samples);
    double screen_on = run_scenario(/*is_voice=*/false, /*nr_on=*/true, frames, op, frame_samples);

    std::printf("recv_nr: voice_off_rms=%.1f voice_on_rms=%.1f screen_on_rms=%.1f\n", voice_off,
                voice_on, screen_on);

    CHECK(voice_off > 0.0);
    CHECK(voice_on > 0.0);
    CHECK(screen_on > 0.0);

    // The fix: enabling NR on the stereo VOICE stream must collapse the noise floor. RNNoise drops
    // pure noise ~99%; require a clear, unambiguous reduction (the pre-fix bug left it unchanged).
    CHECK(voice_on < voice_off * 0.6);

    // A screen-audio share is not voice: NR must be a no-op there, so its floor stays ≈ the
    // undenoised baseline (same decode path, no RNNoise). Loose band absorbs run-to-run timing.
    CHECK(screen_on > voice_off * 0.7);

    if (g_failures == 0) {
        std::printf("recv_noise_reduction: OK\n");
        return 0;
    }
    std::printf("recv_noise_reduction: %d failure(s)\n", g_failures);
    return 1;
}

#else

int main() {
    std::printf("recv_noise_reduction: SKIP (VOICECAT_HAS_AUDIO or VOICECAT_HAS_OPUS not defined)\n");
    return 0;
}

#endif
