// Cycle-0031 fieldtest — scenario 1 (axis a): the named single-run authoring path.
//
// Promise probed (spec 0031 acceptance #1, #2, #4): a downstream author writes
// `Sma::builder().named("fast")`, inspects `param_space()` (now uniformly
// `<node>.<param>` at every level, including the root `exposure.scale`), binds
// `sma_cross.fast.length` BY NAME with NO ParamAlias and NO index counting, and
// runs. This is the exact pain the milestone-0030 fixture (mw_1) had to work
// around with two hand-counted ParamAlias overlays.
//
// Public interface only: API discovered from the ledger (C9/C23), spec 0031,
// and `cargo doc`. No crates/*/src was read.
//
// The harness: source -> sma_cross composite { Sma("fast"), Sma("slow"), Sub }
//              -> Exposure -> SimBroker -> Recorder.

use std::sync::mpsc;

use aura_core::{Firing, Scalar, ScalarKind, Timestamp};
use aura_engine::{BlueprintNode, Composite, Edge, OutField, Role, Target};
use aura_std::{Exposure, Recorder, SimBroker, Sma, Sub};

/// The reusable inner 2/4-SMA-cross composite. The two SMA legs are now named
/// inline via `.named()` — the single act that (a) distinguishes the fan-in and
/// (b) qualifies the two param paths. No ParamAlias argument exists anymore
/// (Composite::new is 4-arg).
fn sma_cross() -> Composite {
    Composite::new(
        "sma_cross",
        vec![
            Sma::builder().named("fast").into(),
            Sma::builder().named("slow").into(),
            Sub::builder().into(),
        ],
        vec![
            Edge { from: 0, to: 2, slot: 0, from_field: 0 },
            Edge { from: 1, to: 2, slot: 1, from_field: 0 },
        ],
        vec![Role {
            name: "price".into(),
            targets: vec![
                Target { node: 0, slot: 0 },
                Target { node: 1, slot: 0 },
            ],
            source: None,
        }],
        vec![OutField { node: 2, field: 0, name: "out".into() }],
    )
}

/// The root harness over the named cross. The root-level Exposure node now also
/// carries a name (default "exposure"), so its knob surfaces as `exposure.scale`.
fn harness(tx: mpsc::Sender<(Timestamp, Vec<Scalar>)>) -> Composite {
    Composite::new(
        "harness",
        vec![
            BlueprintNode::Composite(sma_cross()),
            Exposure::builder().into(),
            SimBroker::builder(1e-4).into(),
            Recorder::builder(vec![ScalarKind::F64], Firing::Any, tx).into(),
        ],
        vec![
            Edge { from: 0, to: 1, slot: 0, from_field: 0 },
            Edge { from: 1, to: 2, slot: 0, from_field: 0 },
            Edge { from: 2, to: 3, slot: 0, from_field: 0 },
        ],
        vec![Role {
            name: "price".into(),
            targets: vec![
                Target { node: 0, slot: 0 },
                Target { node: 2, slot: 1 },
            ],
            source: Some(ScalarKind::F64),
        }],
        vec![OutField { node: 0, field: 0, name: "exposure".into() }],
    )
}

fn synthetic_prices() -> Vec<(Timestamp, Scalar)> {
    let prices = [
        1.00, 1.01, 1.02, 1.03, 1.05, 1.08, 1.06, 1.04, 1.02, 1.01, 1.03, 1.07,
    ];
    prices
        .iter()
        .enumerate()
        .map(|(i, &p)| (Timestamp(60_000_000_000 * i as i64), Scalar::F64(p)))
        .collect()
}

fn main() {
    // --- 1. Inspect the param-space: are the names what the spec promises? -----
    let (tx0, _rx0) = mpsc::channel();
    let space = harness(tx0).param_space();
    println!("param_space() knobs (name : kind):");
    for p in &space {
        println!("  {} : {:?}", p.name, p.kind);
    }
    let names: Vec<&str> = space.iter().map(|p| p.name.as_str()).collect();
    // spec 0031 acceptance #1/#4: uniform <node>.<param>, distinct named legs.
    assert!(
        names.contains(&"sma_cross.fast.length"),
        "expected sma_cross.fast.length, got {names:?}"
    );
    assert!(
        names.contains(&"sma_cross.slow.length"),
        "expected sma_cross.slow.length, got {names:?}"
    );
    assert!(
        names.contains(&"exposure.scale"),
        "expected root-level exposure.scale, got {names:?}"
    );

    // --- 2. Bind BY NAME, with no ParamAlias and no index counting. ------------
    let (tx, rx) = mpsc::channel::<(Timestamp, Vec<Scalar>)>();
    let mut h = harness(tx)
        .with("sma_cross.fast.length", 2)
        .with("sma_cross.slow.length", 4)
        .with("exposure.scale", 0.5)
        .bootstrap()
        .expect("named harness binds by the uniform <node>.<param> names and bootstraps");

    h.run(vec![synthetic_prices()]);
    drop(h);

    let rows: Vec<(Timestamp, Vec<Scalar>)> = rx.iter().collect();
    println!("\nnamed-bound run — recorded rows: {}", rows.len());
    for (ts, row) in &rows {
        if let Scalar::F64(x) = row[0] {
            println!("  ts={:>14} exposure={:+.4}", ts.0, x);
        }
    }
    assert!(!rows.is_empty(), "named-bound harness must record a populated trace");

    // --- 3. Determinism (C1): a disjoint second run is bit-identical. ----------
    let (tx2, rx2) = mpsc::channel::<(Timestamp, Vec<Scalar>)>();
    let mut h2 = harness(tx2)
        .with("sma_cross.fast.length", 2)
        .with("sma_cross.slow.length", 4)
        .with("exposure.scale", 0.5)
        .bootstrap()
        .expect("second disjoint bootstrap");
    h2.run(vec![synthetic_prices()]);
    drop(h2);
    let rows2: Vec<(Timestamp, Vec<Scalar>)> = rx2.iter().collect();
    assert_eq!(rows, rows2, "two disjoint named runs must be bit-identical (C1)");

    println!("\nOK: param-space uniform <node>.<param>, named bind ran, deterministic.");
}
