// Microbenchmarks for the `validate_voice_template` double-compile claim
// (project_backlog.md: "validate_voice_template double-compile").
//
// What we measure
// ---------------
// 1. `set_template`  — exercises `validate_voice_template` in isolation:
//    pool exists, slots already allocated, so the call cost is dominated
//    by `CompiledDsp::compile(...)` whose result is discarded plus a
//    template-reference swap.
// 2. `note_on`       — the per-voice compile we already pay (max_voices=1
//    keeps slot stealing stable across iterations so each call recompiles).
// 3. `voicepool_new` — full constructor (validate + max_voices=32 slot
//    allocations), the user-perceptible cost of first authoring.
//
// The decision number is the per-fixture ratio `set_template / note_on`.
// Near 1.0 = double-compile waste is real; near 0 = it is noise.
//
// Default `moon bench` runs wasm-gc (browser AudioWorklet deployment
// path). The compile path is allocation-heavy (`graph/graph_compile.mbt`
// and `dsp/delay.mbt` allocate fresh buffers per call), so a wasm-gc
// number does not automatically generalize across targets. Verified
// 2026-05-19 that `moon bench --target native` (C-native deployment
// for future CLAP plugin) reports the same ratio and 25-30% lower
// absolute cost — no RC-overhead blowup. If a JS-target deployment
// is added, re-measure there before extending any "defer" conclusion.

///|
const BENCH_SAMPLE_RATE : Double = 48000.0

///|
const BENCH_BLOCK_SIZE : Int = 128

///|
/// Minimal wired-ADSR voice (4 nodes). Exercises the orphan-ADSR guard
/// in the well-formed path; matches the smallest real voice in production.
fn bench_minimal_voice_nodes() -> Array[@graph.DspNode] {
  [
    @graph.DspNode::oscillator(@dsp.Waveform::Sine, 440.0),
    @graph.DspNode::adsr(
      attack_ms=5.0,
      decay_ms=10.0,
      sustain=0.6,
      release_ms=50.0,
    ),
    @graph.DspNode::mul(0, 1),
    @graph.DspNode::output(2),
  ]
}

///|
/// FM voice (11 nodes). Genuine FM: a modulator oscillator (whose
/// frequency is supplied by an upstream constant) is scaled by an FM
/// depth gain and mixed into the carrier's frequency input via
/// `oscillator_from`. Without `oscillator_from`, the modulator nodes
/// would be dead-code-eliminated by `analyze` and this row would only
/// measure a carrier-plus-env chain.
fn bench_fm_voice_nodes() -> Array[@graph.DspNode] {
  [
    @graph.DspNode::constant(5.0), // 0: modulator base freq (Hz)
    @graph.DspNode::oscillator_from(0, @dsp.Waveform::Sine), // 1: modulator
    @graph.DspNode::gain(1, 200.0), // 2: FM depth
    @graph.DspNode::constant(220.0), // 3: carrier base freq
    @graph.DspNode::mix(3, 2), // 4: carrier_freq + modulation
    @graph.DspNode::oscillator_from(4, @dsp.Waveform::Saw), // 5: carrier
    @graph.DspNode::adsr(
      attack_ms=5.0,
      decay_ms=10.0,
      sustain=0.6,
      release_ms=50.0,
    ), // 6: env
    @graph.DspNode::mul(5, 6), // 7: amp-modulated carrier
    @graph.DspNode::biquad(
      input=7,
      mode=@graph.BiquadMode::LowPass,
      cutoff_hz=1200.0,
      q=0.707,
    ), // 8: filter
    @graph.DspNode::gain(8, 0.7), // 9: trim
    @graph.DspNode::output(9), // 10: output
  ]
}

///|
/// Full voice (10 nodes): osc + noise → ADSR → filter → gain → delay → output.
/// Largest realistic voice topology; the longest compile is the upper bound
/// on the cost we're worried about.
fn bench_full_voice_nodes() -> Array[@graph.DspNode] {
  [
    @graph.DspNode::oscillator(@dsp.Waveform::Saw, 220.0), // 0: osc
    @graph.DspNode::noise(42U), // 1: noise
    @graph.DspNode::gain(1, 0.2), // 2: noise gain
    @graph.DspNode::mix(0, 2), // 3: mix
    @graph.DspNode::adsr(
      attack_ms=5.0,
      decay_ms=10.0,
      sustain=0.6,
      release_ms=50.0,
    ), // 4: env
    @graph.DspNode::mul(3, 4), // 5: modulated
    @graph.DspNode::biquad(
      input=5,
      mode=@graph.BiquadMode::LowPass,
      cutoff_hz=1200.0,
      q=0.707,
    ), // 6: filter
    @graph.DspNode::gain(6, 0.7), // 7: trim
    @graph.DspNode::delay(
      input=7,
      max_delay_samples=4800,
      delay_samples=2400,
      feedback=0.3,
    ), // 8: delay
    @graph.DspNode::output(8), // 9: output
  ]
}

// ---- Validation tests ----
// WHY: a failed .unwrap() inside a bench closure aborts with no diagnostic.
// These guard the fixtures so a broken topology is caught with a clear
// assertion before the benchmark runner sees it.

///|
test "bench voice: minimal_voice template compiles + pool accepts" {
  let ctx = @dsp.DspContext::new(
    sample_rate=BENCH_SAMPLE_RATE,
    block_size=BENCH_BLOCK_SIZE,
  )
  let template = @graph.CompiledTemplate::analyze(bench_minimal_voice_nodes())
  assert_true(@graph.CompiledDsp::compile(template, ctx) is Some(_))
  assert_true(VoicePool::new(template, ctx) is Ok(_))
}

///|
test "bench voice: fm_voice template compiles + pool accepts" {
  let ctx = @dsp.DspContext::new(
    sample_rate=BENCH_SAMPLE_RATE,
    block_size=BENCH_BLOCK_SIZE,
  )
  let template = @graph.CompiledTemplate::analyze(bench_fm_voice_nodes())
  assert_true(@graph.CompiledDsp::compile(template, ctx) is Some(_))
  assert_true(VoicePool::new(template, ctx) is Ok(_))
}

///|
test "bench voice: full_voice template compiles + pool accepts" {
  let ctx = @dsp.DspContext::new(
    sample_rate=BENCH_SAMPLE_RATE,
    block_size=BENCH_BLOCK_SIZE,
  )
  let template = @graph.CompiledTemplate::analyze(bench_full_voice_nodes())
  assert_true(@graph.CompiledDsp::compile(template, ctx) is Some(_))
  assert_true(VoicePool::new(template, ctx) is Ok(_))
}

// ---- set_template (isolates the discarded compile) ----
// WHY this isolates the validator: at iteration time the pool already
// exists, slots are allocated, so the only work is validate_voice_template
// (which IS CompiledDsp::compile in the well-formed case) plus one
// template-pointer assignment and an adsr_authoring_indices() snapshot.
// That snapshot is O(node count); a single 4-10-node array scan is far
// below the compile cost we are measuring.

///|
test "bench/voice/set_template" (b : @bench.T) {
  let ctx = @dsp.DspContext::new(
    sample_rate=BENCH_SAMPLE_RATE,
    block_size=BENCH_BLOCK_SIZE,
  )
  let minimal = @graph.CompiledTemplate::analyze(bench_minimal_voice_nodes())
  let fm = @graph.CompiledTemplate::analyze(bench_fm_voice_nodes())
  let full = @graph.CompiledTemplate::analyze(bench_full_voice_nodes())
  let pool_min = VoicePool::new(minimal, ctx).unwrap()
  let pool_fm = VoicePool::new(fm, ctx).unwrap()
  let pool_full = VoicePool::new(full, ctx).unwrap()
  b.bench(name="minimal_voice", fn() {
    let r = pool_min.set_template(minimal)
    b.keep(r)
  })
  b.bench(name="fm_voice", fn() {
    let r = pool_fm.set_template(fm)
    b.keep(r)
  })
  b.bench(name="full_voice", fn() {
    let r = pool_full.set_template(full)
    b.keep(r)
  })
}

// ---- note_on (the productive per-voice compile) ----
// WHY max_voices=1: every iter steals slot 0 and recompiles. This keeps
// `find_slot` cost constant and bounded, isolating compile cost as the
// dominant per-call work. The slot's pre-existing CompiledDsp from the
// previous iter is replaced; no leak.
// WHY params=[]: skips the apply_controls branch so we measure pure compile
// overhead, not control validation.

///|
test "bench/voice/note_on" (b : @bench.T) {
  let ctx = @dsp.DspContext::new(
    sample_rate=BENCH_SAMPLE_RATE,
    block_size=BENCH_BLOCK_SIZE,
  )
  let minimal = @graph.CompiledTemplate::analyze(bench_minimal_voice_nodes())
  let fm = @graph.CompiledTemplate::analyze(bench_fm_voice_nodes())
  let full = @graph.CompiledTemplate::analyze(bench_full_voice_nodes())
  let pool_min = VoicePool::new(minimal, ctx, max_voices=1).unwrap()
  let pool_fm = VoicePool::new(fm, ctx, max_voices=1).unwrap()
  let pool_full = VoicePool::new(full, ctx, max_voices=1).unwrap()
  let empty : Array[@graph.GraphControl] = []
  b.bench(name="minimal_voice", fn() {
    let h = pool_min.note_on(empty)
    b.keep(h)
  })
  b.bench(name="fm_voice", fn() {
    let h = pool_fm.note_on(empty)
    b.keep(h)
  })
  b.bench(name="full_voice", fn() {
    let h = pool_full.note_on(empty)
    b.keep(h)
  })
}

// ---- voicepool_new (constructor cost, post-analyze) ----
// WHY: measures validate + 32 × VoiceSlot::new (each allocates a
// mono_buffer of block_size doubles). `CompiledTemplate::analyze` is
// hoisted OUT of the bench closure because real authoring re-uses one
// analyzed template across many pool constructions; the bench therefore
// reports the steady-state per-call cost, not first-edit cost from raw
// authoring nodes. If `set_template` and `voicepool_new` differ by
// roughly the slot-allocation constant across all fixtures, that confirms
// the compile is what dominates `set_template`.

///|
test "bench/voice/voicepool_new" (b : @bench.T) {
  let ctx = @dsp.DspContext::new(
    sample_rate=BENCH_SAMPLE_RATE,
    block_size=BENCH_BLOCK_SIZE,
  )
  let minimal = @graph.CompiledTemplate::analyze(bench_minimal_voice_nodes())
  let fm = @graph.CompiledTemplate::analyze(bench_fm_voice_nodes())
  let full = @graph.CompiledTemplate::analyze(bench_full_voice_nodes())
  b.bench(name="minimal_voice", fn() {
    let pool = VoicePool::new(minimal, ctx)
    b.keep(pool)
  })
  b.bench(name="fm_voice", fn() {
    let pool = VoicePool::new(fm, ctx)
    b.keep(pool)
  })
  b.bench(name="full_voice", fn() {
    let pool = VoicePool::new(full, ctx)
    b.keep(pool)
  })
}