// Cycle-0031 fieldtest — scenario 3 (axis c): default names + the paramless
// "interchangeable stays legal" corner + node-name visibility.
//
// Probes (spec 0031 §"Default name", §"Paramless interchangeable stays legal",
// and the carrier's optional 3rd axis):
//  (a) a single un-named primitive emits `<lowercased-type>.<param>` — `sma.length`;
//  (b) a root-level Exposure (un-named) emits `exposure.scale`;
//  (c) the default lowercasing is verbatim, no snake_case insertion
//      ("SimBroker" -> "simbroker", not "sim_broker");
//  (d) two un-named PARAMLESS same-type siblings under a fan-in stay legal
//      (no IndistinguishableFanIn), as the spec preserves.
//
// Public interface only (ledger C9/C23 + spec 0031 + cargo doc). No src read.

use std::sync::mpsc;

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

/// A trivial harness: one un-named SMA feeding Exposure -> SimBroker -> Recorder.
/// No composite nesting, so every param surfaces at the ROOT level — the case
/// that exercises the "root segment is now present too" rule.
fn single_unnamed_harness(tx: mpsc::Sender<(Timestamp, Vec<Scalar>)>) -> Composite {
    Composite::new(
        "harness",
        vec![
            Sma::builder().into(), // un-named -> default "sma"
            Exposure::builder().into(), // un-named -> default "exposure"
            SimBroker::builder(1e-4).into(), // un-named -> default "simbroker"
            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: 1, field: 0, name: "exposure".into() }],
    )
}

/// Two un-named PARAMLESS same-type siblings (Add has no params) under a fan-in
/// (Sub). Spec: this stays LEGAL — interchangeable paramless legs do not fire
/// IndistinguishableFanIn. Built as a self-contained ROOT (the single price
/// source feeds both Adds' slots) so the role is bound and the only thing under
/// test is the paramless same-name fan-in rule, not role-binding.
fn paramless_fanin() -> Composite {
    Composite::new(
        "paramless",
        vec![
            Add::builder().into(), // un-named -> "add"
            Add::builder().into(), // un-named -> "add" — same name, but paramless
            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: "x".into(),
            targets: vec![
                Target { node: 0, slot: 0 },
                Target { node: 0, slot: 1 },
                Target { node: 1, slot: 0 },
                Target { node: 1, slot: 1 },
            ],
            // bound root source — keeps the role satisfied so the compile only
            // exercises the paramless fan-in rule.
            source: Some(ScalarKind::F64),
        }],
        vec![OutField { node: 2, field: 0, name: "out".into() }],
    )
}

fn main() {
    // --- (a)(b)(c) default names appear in param_space at the root level. ------
    let (tx0, _rx0) = mpsc::channel();
    let space = single_unnamed_harness(tx0).param_space();
    println!("default-name param_space():");
    for p in &space {
        println!("  {} : {:?}", p.name, p.kind);
    }
    let names: Vec<&str> = space.iter().map(|p| p.name.as_str()).collect();
    assert!(names.contains(&"sma.length"), "expected sma.length, got {names:?}");
    assert!(names.contains(&"exposure.scale"), "expected exposure.scale, got {names:?}");
    // SimBroker's spread arg is a constructor value, not a knob; if SimBroker has
    // a param it must surface lowercased verbatim. Record whatever it emits.
    let simbroker_knobs: Vec<&&str> =
        names.iter().filter(|n| n.starts_with("sim")).collect();
    println!("simbroker-prefixed knobs (verbatim-lowercase check): {simbroker_knobs:?}");
    assert!(
        !names.iter().any(|n| n.starts_with("sim_broker")),
        "default name must be verbatim-lowercased 'simbroker', never snake_cased 'sim_broker' ({names:?})"
    );

    // Bind the default names and run, to prove they are real bindable knobs.
    let (tx, rx) = mpsc::channel::<(Timestamp, Vec<Scalar>)>();
    let mut h = single_unnamed_harness(tx)
        .with("sma.length", 3)
        .with("exposure.scale", 1.0)
        .bootstrap()
        .expect("default-named knobs bind by their lowercased-type paths");
    let prices: Vec<(Timestamp, Scalar)> = (0..10)
        .map(|i| (Timestamp(60_000_000_000 * i), Scalar::F64(1.0 + 0.02 * i as f64)))
        .collect();
    h.run(vec![prices]);
    drop(h);
    let rows: Vec<_> = rx.iter().collect();
    println!("\ndefault-named run recorded {} rows", rows.len());
    assert!(!rows.is_empty());

    // --- (d) paramless interchangeable same-name fan-in stays LEGAL. ----------
    let compiled = paramless_fanin().compile_with_params(&[]);
    println!("\nparamless same-name fan-in compile ok: {:?}", compiled.is_ok());
    assert!(
        compiled.is_ok(),
        "two un-named PARAMLESS same-type legs must stay legal (no IndistinguishableFanIn): err={:?}",
        compiled.err()
    );

    println!("\nOK: default names = lowercased type label, root-level qualified, paramless legal.");
}
