//! Acceptance pin for the harness-input-binding spec (#231): a MULTI-COLUMN
//! blueprint (open/high/low/close input roles) composes from blueprint data
//! and runs over four inline `VecSource` columns to hand-computed values —
//! pinning the role-declaration/merge-order contract (role i is fed by source
//! i; same-timestamp ties break by source index, i.e. canonical column order —
//! C4) without any archive. Mirrors `rsi_from_blueprint_data.rs` (the same
//! public seam: a JSON document + `blueprint_from_json` + an injected
//! vocabulary, C24). Pure engine machinery — no CLI, no binding module.

use std::sync::mpsc;

use aura_core::{Firing, Scalar, ScalarKind, Timestamp};
use aura_engine::{blueprint_from_json, BlueprintNode, Composite, Edge, Role, Target, VecSource};
use aura_std::{std_vocabulary, Recorder};

// value = (high - low) + (close - open): every column is load-bearing, so any
// mis-pairing of roles to sources (or a wrong merge tie-break) changes the
// recorded rows. Node indices: 0 Sub(range = high - low) ·
// 1 Sub(body = close - open) · 2 Add(range + body).
const OHLC_BLUEPRINT_JSON: &str = r#"{
  "format_version": 1,
  "blueprint": {
    "name": "ohlc_probe",
    "nodes": [
      {"primitive":{"type":"Sub"}},
      {"primitive":{"type":"Sub"}},
      {"primitive":{"type":"Add"}}
    ],
    "edges": [
      {"from":0,"to":2,"slot":0,"from_field":0},
      {"from":1,"to":2,"slot":1,"from_field":0}
    ],
    "input_roles": [
      {"name":"open","targets":[{"node":1,"slot":1}]},
      {"name":"high","targets":[{"node":0,"slot":0}]},
      {"name":"low","targets":[{"node":0,"slot":1}]},
      {"name":"close","targets":[{"node":1,"slot":0}]}
    ],
    "output": [{"node":2,"field":0,"name":"value"}]
  }
}"#;

// Nest the loaded signal under a Rust root that declares the four source roles
// in canonical column order (open, high, low, close) and records the single
// output (the rsi_from_blueprint_data idiom — sinks are outside the std
// vocabulary, so the recorder is added in Rust). Root role slot i targets the
// nested composite's role i (input_roles order).
fn run_recording(
    signal: Composite,
    columns: [Vec<(Timestamp, Scalar)>; 4],
) -> Vec<(Timestamp, Vec<Scalar>)> {
    let (tx, rx) = mpsc::channel();
    let roles: Vec<Role> = ["open", "high", "low", "close"]
        .iter()
        .enumerate()
        .map(|(slot, name)| Role {
            name: (*name).to_string(),
            targets: vec![Target { node: 0, slot }],
            source: Some(ScalarKind::F64),
        })
        .collect();
    let root = Composite::new(
        "h",
        vec![
            BlueprintNode::Composite(signal),
            Recorder::builder(vec![ScalarKind::F64], Firing::Any, tx).into(),
        ],
        vec![Edge { from: 0, to: 1, slot: 0, from_field: 0 }],
        roles,
        vec![],
    );
    let mut h = root.bootstrap_with_params(vec![]).expect("bootstraps (no open params)");
    let sources: Vec<Box<dyn aura_engine::Source>> = columns
        .into_iter()
        .map(|c| Box::new(VecSource::new(c)) as Box<dyn aura_engine::Source>)
        .collect();
    h.run(sources);
    rx.try_iter().collect()
}

fn col(values: &[f64]) -> Vec<(Timestamp, Scalar)> {
    values
        .iter()
        .enumerate()
        .map(|(i, &v)| (Timestamp(i as i64 + 1), Scalar::f64(v)))
        .collect()
}

/// Hand-computed contract. The four sources at one bar timestamp arrive in
/// FOUR consecutive engine cycles (the k-way merge breaks the timestamp tie
/// by source index = canonical column order: open, high, low, close); every
/// `Firing::Any` node re-evaluates per fresh input over the newest value of
/// each leg (sample-and-hold on the stale leg). Bars (o,h,l,c):
/// (1,10,5,3), (2,20,8,6), (4,40,16,12).
///
/// Bar 1: open=1 -> body fires, close leg cold -> None; high=10 -> range
/// fires, low leg cold -> None; low=5 -> range = 10-5 = 5, add fires, body
/// leg cold -> None; close=3 -> body = 3-1 = 2, add = 5+2 = 7 -> (t1, 7).
/// Bar 2: open=2 -> body = 3-2 = 1 (close held at 3), add = 5+1 = 6;
/// high=20 -> range = 20-5 = 15 (low held), add = 15+1 = 16;
/// low=8 -> range = 20-8 = 12, add = 12+1 = 13;
/// close=6 -> body = 6-2 = 4, add = 12+4 = 16.
/// Bar 3: open=4 -> body = 6-4 = 2, add = 12+2 = 14; high=40 -> range =
/// 40-8 = 32, add = 32+2 = 34; low=16 -> range = 40-16 = 24, add = 26;
/// close=12 -> body = 12-4 = 8, add = 24+8 = 32.
#[test]
fn ohlc_blueprint_runs_over_four_columns_in_canonical_order() {
    let signal = blueprint_from_json(OHLC_BLUEPRINT_JSON, &|t| std_vocabulary(t))
        .expect("loads through the std vocabulary");
    let trace = run_recording(
        signal,
        [
            col(&[1.0, 2.0, 4.0]),   // open
            col(&[10.0, 20.0, 40.0]), // high
            col(&[5.0, 8.0, 16.0]),  // low
            col(&[3.0, 6.0, 12.0]),  // close
        ],
    );
    let want: Vec<(Timestamp, Vec<Scalar>)> = [
        (1, 7.0),
        (2, 6.0),
        (2, 16.0),
        (2, 13.0),
        (2, 16.0),
        (3, 14.0),
        (3, 34.0),
        (3, 26.0),
        (3, 32.0),
    ]
    .iter()
    .map(|&(t, v)| (Timestamp(t), vec![Scalar::f64(v)]))
    .collect();
    assert_eq!(
        trace, want,
        "role i must be fed by source i in canonical column order",
    );
}
