//! End-to-end coverage for #246 tasks 1-2: `PrimitiveBuilder::bind`/`unbind`
//! storing the bound value as data merged back at build time (aura-core), and
//! `Composite::reopen` re-opening a bound param at its `param_space()` path
//! (aura-engine). The in-crate unit tests (`node.rs`/`blueprint.rs` `mod
//! tests`) already pin the mechanism directly against `PrimitiveBuilder`; this
//! file instead drives the **full public compile pipeline**
//! (`Composite::reopen` -> `compile_with_params` -> `FlatGraph`) through only
//! `aura_engine`'s exported surface — the seam the future CLI axis-override
//! path (the rest of #246) will call. A regression that broke the merge only
//! when routed through `compile_with_params` (as opposed to calling
//! `PrimitiveBuilder::build` directly) would pass every existing unit test
//! while breaking this.

use aura_core::{Scalar, ScalarKind};
use aura_engine::{Composite, Role, Target};
use aura_std::{Bias, Sma};

/// A single bound `Sma`, wired from a root role so it compiles standalone.
fn one_bound_sma() -> Composite {
    Composite::new(
        "sig",
        vec![Sma::builder().named("sma").bind("length", Scalar::i64(2)).into()],
        vec![],
        vec![Role {
            name: "series".into(),
            targets: vec![Target { node: 0, slot: 0 }],
            source: Some(ScalarKind::F64),
        }],
        vec![],
    )
}

/// Property: a bound param, once reopened, is genuinely re-bindable through
/// the full `compile_with_params` pipeline — the compiled node reflects the
/// value supplied AT COMPILE TIME, not the stale bound default. This is the
/// build-time merge (`PrimitiveBuilder::build`, aura-core #246 task 1) proven
/// under the compile boundary it must survive (aura-engine #246 task 2), not
/// just called directly.
#[test]
fn reopened_param_rebuilds_with_the_freshly_supplied_value_not_the_stale_default() {
    // Before reopen: fully bound, nothing to supply, and the default (2) is
    // what gets built. (A fresh `Composite` — `reopen`/`compile_with_params`
    // both consume `self`, so the "before" and "after" instances are built
    // separately from the same constructor rather than cloned.)
    let flat = one_bound_sma().compile_with_params(&[]).expect("fully-bound composite compiles with no params");
    assert_eq!(flat.nodes[0].label(), "SMA(2)", "the bound default builds unchanged");

    let reopened = one_bound_sma().reopen("sma.length").expect("bound knob reopens");
    let space = reopened.param_space();
    assert_eq!(space.len(), 1, "reopen returns exactly the one knob to the open surface");
    assert_eq!(space[0].name, "sma.length");

    let flat2 = reopened
        .compile_with_params(&[Scalar::i64(9)])
        .expect("reopened composite compiles once the freed slot is supplied");
    assert_eq!(
        flat2.nodes[0].label(),
        "SMA(9)",
        "the compiled node must be built from the freshly supplied value (9), not the stale bound default (2)"
    );
}

/// Property: reopening one node's bound param does not disturb a SIBLING
/// node's own still-bound default. `BoundParam`'s per-node bookkeeping
/// (`pos`, restored on `unbind`) must stay node-local through the compile
/// pipeline — a bug that let one node's reopen leak into another's merge
/// would build the sibling from the wrong cell.
#[test]
fn reopening_one_node_leaves_a_sibling_bound_default_intact_through_compile() {
    let signal = Composite::new(
        "sig",
        vec![
            Sma::builder().named("fast").bind("length", Scalar::i64(2)).into(),
            Bias::builder().named("b").bind("scale", Scalar::f64(0.5)).into(),
        ],
        vec![],
        vec![
            Role {
                name: "series".into(),
                targets: vec![Target { node: 0, slot: 0 }],
                source: Some(ScalarKind::F64),
            },
            Role {
                name: "signal".into(),
                targets: vec![Target { node: 1, slot: 0 }],
                source: Some(ScalarKind::F64),
            },
        ],
        vec![],
    );

    let reopened = signal.reopen("fast.length").expect("fast.length reopens");
    let flat = reopened
        .compile_with_params(&[Scalar::i64(7)])
        .expect("compiles once the one freed slot is supplied");

    assert_eq!(flat.nodes[0].label(), "SMA(7)", "the reopened node builds from the freshly supplied value");
    assert_eq!(
        flat.nodes[1].label(),
        "Bias(0.5)",
        "the sibling's still-bound default must build unchanged — reopen must not perturb it"
    );
}
