// Cycle-0031 fieldtest — scenario 2 (axis b): the forcing function.
//
// Promise probed (spec 0031 acceptance #3): author the 2-SMA cross WITHOUT
// names -> both legs default to "sma" -> they collide and the fan-in is
// indistinguishable -> `IndistinguishableFanIn { node }`. The recovery is the
// single act of naming the colliding legs. The question this fixture answers as
// a downstream consumer: is the diagnostic DISCOVERABLE and HELPFUL from the
// public surface alone — does it name the node, and does the error type/text
// point the author at "give the legs distinct names"?
//
// 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::{
    BlueprintNode, CompileError, Composite, Edge, OutField, Role, Target,
};
use aura_std::{Exposure, Recorder, SimBroker, Sma, Sub};

/// Build the cross with the two SMA legs given (caller decides whether to name
/// them). `name_them = false` reproduces the minimal un-named authoring.
fn sma_cross(name_them: bool) -> Composite {
    let (fast, slow) = if name_them {
        (Sma::builder().named("fast"), Sma::builder().named("slow"))
    } else {
        (Sma::builder(), Sma::builder())
    };
    Composite::new(
        "sma_cross",
        vec![fast.into(), 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() }],
    )
}

fn harness(
    name_them: bool,
    tx: mpsc::Sender<(Timestamp, Vec<Scalar>)>,
) -> Composite {
    Composite::new(
        "harness",
        vec![
            BlueprintNode::Composite(sma_cross(name_them)),
            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 main() {
    // --- 0. What does param_space() emit for the UN-NAMED cross? --------------
    // Both legs default to "sma" -> sma_cross.sma.length appears TWICE
    // (a collision). This is what makes a by-name bind ambiguous/unknown.
    let (txs, _rxs) = mpsc::channel();
    let unnamed_space = harness(false, txs).param_space();
    println!("[0] un-named cross param_space():");
    for p in &unnamed_space {
        println!("    {} : {:?}", p.name, p.kind);
    }

    // --- 1. The minimal un-named authoring is correctly REJECTED. -------------
    let (txu, _rxu) = mpsc::channel();
    let unnamed = harness(false, txu)
        .with("sma_cross.length", 2) // best guess at the colliding bare name
        .bootstrap();
    // Harness (Ok branch) may not implement Debug; inspect the error only.
    println!("[1] un-named cross bootstrap is_err: {}", unnamed.is_err());
    match unnamed.err() {
        // The binder may reject first (AmbiguousKnob on the colliding
        // sma_cross.length), OR the compile may reject first
        // (IndistinguishableFanIn). Record whichever wall the author hits.
        Some(other) => println!("    -> diagnostic the author sees: {other:?}"),
        None => panic!("BUG: un-named param-bearing fan-in should be rejected"),
    }

    // --- 1a. Bind the name param_space() ACTUALLY emits (the duplicate). ------
    // sma_cross.sma.length is emitted TWICE -> binding it is AmbiguousKnob, NOT
    // the spec's headline IndistinguishableFanIn. So the by-name author never
    // sees "name the colliding legs"; they see a knob-name diagnostic.
    let (txa, _rxa) = mpsc::channel();
    let ambiguous = harness(false, txa)
        .with("sma_cross.sma.length", 2)
        .bootstrap();
    println!(
        "[1a] bind the real emitted (duplicate) name -> {:?}",
        ambiguous.err()
    );

    // --- 1b. The bare bootstrap (no binds at all) isolates the COMPILE wall. ---
    // This removes the binder from the picture so we see the fan-in error itself.
    let (txc, _rxc) = mpsc::channel();
    let compiled = harness(false, txc).compile_with_params(&[
        Scalar::I64(2),
        Scalar::I64(4),
        Scalar::F64(0.5),
    ]);
    // FlatGraph (the Ok branch) does not implement Debug, so we cannot
    // `{:?}`-print the whole Result; inspect via .err() instead.
    println!("\n[1b] un-named cross compile_with_params err: {:?}", compiled.as_ref().err());
    match compiled.err() {
        Some(CompileError::IndistinguishableFanIn { node }) => {
            println!(
                "    -> IndistinguishableFanIn at interior node index {node} \
                 (the Sub that fans in both un-named SMAs)"
            );
        }
        other => println!(
            "    -> NOTE: expected IndistinguishableFanIn, got {other:?}"
        ),
    }

    // --- 2. The recovery: name the legs -> the SAME topology now compiles. -----
    let (tx, rx) = mpsc::channel::<(Timestamp, Vec<Scalar>)>();
    let named = harness(true, tx)
        .with("sma_cross.fast.length", 2)
        .with("sma_cross.slow.length", 4)
        .with("exposure.scale", 0.5)
        .bootstrap();
    println!("\n[2] named cross bootstrap ok: {}", named.is_ok());
    let mut h = named.expect("naming the colliding legs resolves the fan-in");
    let prices: Vec<(Timestamp, Scalar)> = (0..12)
        .map(|i| (Timestamp(60_000_000_000 * i), Scalar::F64(1.0 + 0.01 * i as f64)))
        .collect();
    h.run(vec![prices]);
    drop(h);
    let rows: Vec<_> = rx.iter().collect();
    println!("[2] named run recorded {} rows", rows.len());
    assert!(!rows.is_empty(), "named cross must run and record");

    println!(
        "\nOK: un-named fan-in rejected, naming the legs is the single recovering act."
    );
}
