//! End-to-end coverage for the `When` standard cell (#281): the clock-enable
//! `f64 x bool -> f64` driver.
//!
//! The in-module tests in `aura-std/src/when.rs` drive `eval` directly — they
//! never go through a `PrimitiveBuilder` schema, a `GraphBuilder`-resolved
//! wiring, or a compiled+bootstrapped `FlatGraph`. That is the real seam a
//! data-authored op-script or a Rust builder actually exercises (C24); this
//! file proves `When` is reachable from the public `aura_engine`/`aura_std`
//! construction surface, wires cleanly by declared port name/kind (`value:
//! F64`, `gate: Bool`), and — the load-bearing property — that a quiet
//! (gate-false) cycle never reaches a downstream sink: the recorder's own
//! input only advances on a fresh push from `When`, so a quiet cycle leaves
//! it un-fired rather than recording a held/zero value.
//!
//! One `price` source feeds both `When`'s `value` leg and, via
//! `Gt(|price|, price)`, its `gate` leg (true exactly when `price < 0`) — the
//! same single-source-per-cycle shape `select_sign_e2e.rs` uses, so `gate`
//! and `value` are always computed in the same engine cycle (no cross-source
//! phase skew between two independently-clocked inputs).
use std::sync::mpsc;

use aura_core::{Firing, Scalar, ScalarKind, Timestamp};
use aura_engine::{GraphBuilder, VecSource};
use aura_std::{Abs, Gt, Recorder, Resample, When};

/// A deterministic `f64` tick stream, no timestamps/randomness beyond a fixed
/// literal sequence.
fn ticks(values: &[f64]) -> Vec<(Timestamp, Scalar)> {
    values.iter().enumerate().map(|(i, &v)| (Timestamp(i as i64 + 1), Scalar::f64(v))).collect()
}

/// Property: **`When` correctly registers its `PrimitiveBuilder` schema
/// (`value: F64`, `gate: Bool` in, one `f64` out) and dispatches through a
/// compiled+bootstrapped `FlatGraph`, forwarding `value` on gate-true cycles
/// and going quiet — not merely holding or zeroing — on gate-false cycles,
/// all the way to a downstream sink.** A schema/wiring regression (wrong port
/// kind, wrong output arity) would fail at `GraphBuilder::build` or
/// `compile_with_params`, before a single cycle runs; a dispatch regression
/// would show up in the recorded trace below — either the wrong values or,
/// were quiet cycles wrongly forwarding a held/zero value, extra rows.
#[test]
fn when_dispatches_through_a_compiled_graph_and_is_quiet_downstream_on_gate_false() {
    let (tx, rx) = mpsc::channel();
    let mut g = GraphBuilder::new("root");
    let price = g.source_role("price", ScalarKind::F64);
    let abs = g.add(Abs::builder());
    let gt = g.add(Gt::builder());
    let when = g.add(When::builder());
    let rec = g.add(Recorder::builder(vec![ScalarKind::F64], Firing::Any, tx));

    g.feed(price, [abs.input("value"), gt.input("b"), when.input("value")]);
    g.connect(abs.output("value"), gt.input("a"));
    g.connect(gt.output("value"), when.input("gate"));
    g.connect(when.output("value"), rec.input("col[0]"));

    let mut h = g.build().expect("resolves by name").bootstrap_with_params(vec![]).expect("bootstraps");
    h.run(vec![Box::new(VecSource::new(ticks(&[-3.0, 2.5, -1.0, 4.0])))]);
    let trace: Vec<f64> = rx.try_iter().map(|(_, row)| row[0].as_f64()).collect();

    assert_eq!(
        trace,
        vec![-3.0, -1.0],
        "only the two negative-price (gate-true) rows reach the sink; the positive-price rows are quiet, not recorded as held/zero"
    );
}

/// Property: **a quiet `When` (gate false) driving one leg of a `Firing::Barrier(N)`
/// group suppresses the WHOLE group that cycle — not merely its own leg.** This is
/// the composition the `when.rs` module doc calls out directly ("a quiet `When`
/// feeding a `Firing::Barrier(N)` group means the group does not fire that cycle").
/// `Resample`'s four OHLC inputs are `aura-std`'s only `Barrier(0)` consumer; here
/// `When` drives `close` while `open`/`high`/`low` are wired straight from the same
/// source (fresh every cycle, gate or no gate). On a gate-false tick the other three
/// legs still receive a fresh push, but `close` does not — since `Barrier(0)`
/// requires all four co-fresh, the node does not fire at all that cycle, so the
/// gate-false tick's price never enters the OHLC accumulator (not even via the
/// ungated legs), and a bucket rollover due exactly on a gate-false tick is
/// deferred to the next gate-true tick rather than firing early or partially.
#[test]
fn quiet_when_leg_suppresses_the_whole_barrier_group() {
    let (tx, rx) = mpsc::channel();
    let mut g = GraphBuilder::new("root");
    let price = g.source_role("price", ScalarKind::F64);
    let abs = g.add(Abs::builder());
    let gt = g.add(Gt::builder());
    let when = g.add(When::builder());
    let resample = g.add(Resample::builder());
    let rec = g.add(Recorder::builder(vec![ScalarKind::F64; 4], Firing::Any, tx));

    g.feed(price, [
        abs.input("value"),
        gt.input("b"),
        when.input("value"),
        resample.input("open"),
        resample.input("high"),
        resample.input("low"),
    ]);
    g.connect(abs.output("value"), gt.input("a"));
    g.connect(gt.output("value"), when.input("gate"));
    g.connect(when.output("value"), resample.input("close"));
    g.connect(resample.output("open"), rec.input("col[0]"));
    g.connect(resample.output("high"), rec.input("col[1]"));
    g.connect(resample.output("low"), rec.input("col[2]"));
    g.connect(resample.output("close"), rec.input("col[3]"));

    let mut h = g
        .build()
        .expect("resolves by name")
        .bootstrap_with_params(vec![Scalar::i64(1)]) // Resample's sole open param: period_minutes=1
        .expect("bootstraps");

    // gate = price < 0 (Gt(|price|, price)); 1-minute buckets (period_ns = 60e9).
    let feed: [(i64, f64); 5] = [
        (0, -1.0),               // gate true: bucket 0 opens (o=h=l=c=-1.0)
        (10_000_000_000, 2.0),   // gate false: When quiet -> Resample does not fire at all
        (20_000_000_000, -3.0),  // gate true, still bucket 0: high stays -1.0, low->-3.0, close->-3.0
        (60_000_000_000, 4.0),   // gate false exactly on the bucket-1 boundary: rollover DEFERRED
        (70_000_000_000, -6.0),  // gate true, bucket 1: fires -> rolls over the completed bucket-0 bar
    ];
    let ticks: Vec<(Timestamp, Scalar)> =
        feed.iter().map(|&(t, v)| (Timestamp(t), Scalar::f64(v))).collect();
    h.run(vec![Box::new(VecSource::new(ticks))]);
    let trace: Vec<Vec<f64>> =
        rx.try_iter().map(|(_, row)| row.iter().map(|c| c.as_f64()).collect()).collect();

    assert_eq!(
        trace,
        vec![vec![-1.0, -1.0, -3.0, -3.0]],
        "the two gate-false ticks (price 2.0, 4.0) must never enter the OHLC accumulator: \
         a quiet When leg blocks the whole Barrier(0) group, not just its own field, and the \
         bucket-1 rollover due at t=60e9 is deferred to the next gate-true tick (t=70e9)"
    );
}
