// Milestone fieldtest — Topology-as-data (C24), Example 4.
//
// The forward-compatibility discipline the milestone promises (#156 / C24
// two-tier), exercised from the loader's seat, plus cross-surface interop:
//
//   0. Cross-surface interop: the blueprint emitted by the `aura graph build`
//      CLI (mt_2_blueprint.cli.json, trailing newline and all) loads cleanly via
//      the library blueprint_from_json and runs — proving CLI-authored topology
//      is library-consumable.
//   1. Tier-1 (additive-optional tolerance): a future writer adds an UNKNOWN
//      optional field — at the envelope level AND inside a node — and an older
//      reader must (a) load it, (b) drop it on re-serialize back to the SAME
//      canonical form, and (c) run BIT-IDENTICALLY to the clean blueprint.
//   2. Tier-2 (must-understand refusal): a too-new format_version and an
//      out-of-vocabulary node type must each fail CLEANLY BY NAME
//      (LoadError::UnsupportedVersion / UnknownNodeType) — never a panic, never
//      a silently-wrong graph.
//
// Public interface only: blueprint_to_json / blueprint_from_json / LoadError /
// std_vocabulary, learned from `cargo doc` + the C24 ledger. No crates/*/src.

use std::sync::mpsc;

use aura_core::{Firing, Scalar, ScalarKind, Timestamp};
use aura_engine::{
    blueprint_from_json, blueprint_to_json, BlueprintNode, Composite, Edge, GraphBuilder, LoadError,
    OutField, Role, Target, VecSource,
};
use aura_std::{std_vocabulary, Bias, Recorder, Sma, Sub};

fn author_signal() -> Composite {
    let mut gb = GraphBuilder::new("graph");
    let price = gb.source_role("price", ScalarKind::F64);
    let fast = gb.add(Sma::builder().named("fast").bind("length", Scalar::I64(2)));
    let slow = gb.add(Sma::builder().named("slow").bind("length", Scalar::I64(4)));
    gb.feed(price, [fast.input("series"), slow.input("series")]);
    let sub = gb.add(Sub::builder().named("sub"));
    gb.connect(fast.output("value"), sub.input("lhs"));
    gb.connect(slow.output("value"), sub.input("rhs"));
    let bias = gb.add(Bias::builder().named("bias"));
    gb.connect(sub.output("value"), bias.input("signal"));
    gb.expose(bias.output("bias"), "bias");
    gb.build().expect("signal composite should build")
}

fn run_signal(signal: Composite) -> Vec<(i64, f64)> {
    let prices: [f64; 12] = [
        1.00, 1.01, 1.02, 1.03, 1.05, 1.08, 1.06, 1.04, 1.02, 1.01, 1.03, 1.07,
    ];
    let (tx, rx) = mpsc::channel::<(Timestamp, Vec<Scalar>)>();
    let harness = Composite::new(
        "harness",
        vec![
            BlueprintNode::Composite(signal),
            Recorder::builder(vec![ScalarKind::F64], Firing::Any, tx).into(),
        ],
        vec![Edge { from: 0, to: 1, slot: 0, from_field: 0 }],
        vec![Role {
            name: "price".into(),
            targets: vec![Target { node: 0, slot: 0 }],
            source: Some(ScalarKind::F64),
        }],
        vec![] as Vec<OutField>,
    );
    let stream: Vec<(Timestamp, Scalar)> = prices
        .iter()
        .enumerate()
        .map(|(i, &p)| (Timestamp(60_000_000_000 * i as i64), Scalar::F64(p)))
        .collect();
    let mut h = harness
        .bootstrap_with_params(vec![Scalar::F64(1.0)])
        .expect("bootstrap");
    h.run(vec![Box::new(VecSource::new(stream))]);
    drop(h);
    rx.iter()
        .map(|(ts, row)| match row[0] {
            Scalar::F64(x) => (ts.0, x),
            other => panic!("expected f64 bias, got {other:?}"),
        })
        .collect()
}

fn main() {
    let canonical = blueprint_to_json(&author_signal()).expect("serialize");
    let baseline_run = run_signal(author_signal());

    // --- 0. cross-surface interop: load the CLI `graph build` output ----------
    match std::fs::read_to_string("mt_2_blueprint.cli.json") {
        Ok(cli_json) => {
            let from_cli = blueprint_from_json(&cli_json, &std_vocabulary)
                .expect("CLI-emitted blueprint must load via the library loader");
            assert_eq!(
                run_signal(from_cli),
                baseline_run,
                "CLI-authored topology must run identically to the Rust-authored twin"
            );
            let trailing_nl = cli_json.ends_with('\n');
            println!(
                "0. cross-surface interop: CLI blueprint loaded + ran identically \
                 (loader tolerated trailing newline = {trailing_nl})"
            );
        }
        Err(_) => println!("0. (skipped: run mt_2 CLI build first to produce mt_2_blueprint.cli.json)"),
    }

    // --- 1. Tier-1: unknown optional fields are tolerated ---------------------
    // envelope-level unknown key
    let env_unknown = canonical.replacen('{', r#"{"x_future_envelope":{"any":[1,2]},"#, 1);
    // node-level unknown key (inject inside the first primitive object)
    let node_unknown = canonical.replacen(
        r#"{"type":"SMA","name":"fast""#,
        r#"{"x_future_node_flag":true,"type":"SMA","name":"fast""#,
        1,
    );
    for (label, mutated) in [("envelope", &env_unknown), ("node", &node_unknown)] {
        let loaded = blueprint_from_json(mutated, &std_vocabulary)
            .unwrap_or_else(|e| panic!("Tier-1 {label} unknown field must be tolerated, got {e:?}"));
        let reser = blueprint_to_json(&loaded).expect("re-serialize");
        assert_eq!(
            reser, canonical,
            "Tier-1 {label}: re-serialized form must drop the unknown field and match canonical"
        );
        assert_eq!(
            run_signal(loaded),
            baseline_run,
            "Tier-1 {label}: must run bit-identically to the clean blueprint"
        );
        println!("1. Tier-1 {label} unknown field: tolerated, dropped on re-emit, ran identical.");
    }

    // --- 2. Tier-2: must-understand refusals, by name, no panic ---------------
    // (Composite has no Debug impl, so we cannot {:?} the Ok arm — match explicitly.)
    let too_new = canonical.replacen(r#""format_version":1"#, r#""format_version":999"#, 1);
    match blueprint_from_json(&too_new, &std_vocabulary) {
        Err(e @ LoadError::UnsupportedVersion { .. }) => {
            println!("2a. too-new format_version (999): refused -> {e:?}")
        }
        Err(other) => panic!("2a. expected UnsupportedVersion, got {other:?}"),
        Ok(_) => panic!("2a. expected refusal, but a graph was silently loaded"),
    }

    let bad_type = canonical.replacen(r#""type":"Sub""#, r#""type":"FOO""#, 1);
    match blueprint_from_json(&bad_type, &std_vocabulary) {
        Err(e @ LoadError::UnknownNodeType(..)) => {
            println!("2b. out-of-vocabulary node type (FOO): refused -> {e:?}")
        }
        Err(other) => panic!("2b. expected UnknownNodeType, got {other:?}"),
        Ok(_) => panic!("2b. expected refusal, but a graph was silently loaded"),
    }

    println!("PASS: Tier-1 tolerance + Tier-2 by-name refusal (no panic, no silent wrong graph).");
}
