//! R E2E: a synthetic bias+price chain through stop-rule + position-management,
//! recorded and folded by `summarize_r`. Also guards the dense-record layout contract
//! (aura-std's `PM_FIELD_NAMES`/`PM_RECORD_KINDS` vs the column indices `summarize_r`
//! reads).
//!
//! Two distinct jobs live here:
//!
//! 1. **The producer -> consumer seam (E2E).** The whole point of the dense
//!    `PositionManagement` record is that one node emits it and a *different*
//!    crate (`summarize_r` in `aura-engine`) folds it. The `summarize_r` unit
//!    tests in `report.rs` feed hand-built rows whose indices are hardcoded
//!    independently of the producer, so they never exercise that seam. Here the
//!    real chain runs node-by-node: a synthetic price drives `FixedStop` and
//!    `PositionManagement` directly (their `eval` is the contract), the dense
//!    records are collected, and `summarize_r` folds them. If the producer's
//!    column layout and the consumer's reads disagreed, the folded R-metrics
//!    would be wrong.
//!
//! 2. **The column-index contract guard.** `report.rs::summarize_r` reads the
//!    dense record by raw index (`CLOSED=0`, `REALIZED_R=1`, `OPEN=11`,
//!    `UNREALIZED_R=12`). `aura-std` declares the authoritative layout in
//!    `PM_FIELD_NAMES` / `PM_RECORD_KINDS` with a fixed `PM_WIDTH`. `aura-std` is
//!    only a dev-dependency of `aura-engine`, so the two sit across a crate
//!    boundary with no compiler link — a reorder of `PM_FIELD_NAMES` (or a
//!    `PM_WIDTH` change) would silently misalign `summarize_r`. The
//!    `r_col_indices_match_producer_field_layout` test pins that contract.

use aura_core::{AnyColumn, Ctx, Node, Scalar, ScalarKind, Timestamp};
use aura_engine::{PositionAction, RMetrics, derive_position_events, summarize_r};
use aura_std::{
    ConstantCost, CostSum, FixedStop, PM_FIELD_NAMES, PM_RECORD_KINDS, PM_WIDTH,
    PositionManagement, VolSlippageCost,
};

// The indices `report.rs::summarize_r` reads the dense record by. Re-declared here
// (the consumer's `r_col` module is private to `aura-engine`) so the guard test can
// assert they line up with the producer's `PM_FIELD_NAMES` / `PM_RECORD_KINDS`.
const CLOSED: usize = 0;
const REALIZED_R: usize = 1;
const OPEN: usize = 11;
const UNREALIZED_R: usize = 12;
const ENTRY_PRICE: usize = 6;
const STOP_PRICE: usize = 7;
const CONVICTION_AT_ENTRY: usize = 9;
const SIZE: usize = 10;
const DIRECTION: usize = 4;
const CUM_REALIZED_R: usize = 13;
// `ConstantCost`'s 3-wide output schema [cost_in_r, cum_cost_in_r, open_cost_in_r] — the
// run-path's in-graph `net_r_equity` tap reads cum (1) + open (2); summarize_r reads 0/2.
const CUM_COST_IN_R: usize = 1;
const OPEN_COST_IN_R: usize = 2;

/// Property: the real producer->consumer seam composes. Driving `FixedStop` ->
/// `PositionManagement` directly (node-by-node, not a bootstrapped graph) over a
/// synthetic price that rises then falls through the stop, with a constant long
/// bias, must produce at least one trade and a finite `RMetrics` — proving
/// `stop -> position-management -> summarize_r` folds the *recorded* dense records
/// into well-defined R-outcomes. The entry sits near 100 and the price gaps down
/// through the 95 stop, so the closed trade is a loss; a fresh long reopens on the
/// last cycle and is force-closed at window end.
#[test]
fn synthetic_long_then_stop_produces_a_sane_rmetric() {
    let mut stop = FixedStop::new(5.0);
    let mut pm = PositionManagement::new();
    let mut sc = vec![AnyColumn::with_capacity(ScalarKind::F64, 1)]; // stop input: price
    let mut pc: Vec<AnyColumn> =
        (0..4).map(|_| AnyColumn::with_capacity(ScalarKind::F64, 1)).collect();
    let mut ledger: Vec<(Timestamp, Vec<Scalar>)> = vec![];
    // price path: up to 110 then down through the 95 stop.
    let prices = [100.0, 104.0, 108.0, 110.0, 102.0, 96.0, 94.0];
    for (i, &p) in prices.iter().enumerate() {
        sc[0].push(Scalar::f64(p)).unwrap();
        let dist = stop.eval(Ctx::new(&sc, Timestamp(i as i64))).map(|c| c[0].f64()).unwrap_or(0.0);
        pc[0].push(Scalar::f64(1.0)).unwrap(); // constant long bias
        pc[1].push(Scalar::f64(p)).unwrap();
        pc[2].push(Scalar::f64(dist)).unwrap();
        pc[3].push(Scalar::f64(1.0)).unwrap(); // flat-1R size; R is size-invariant
        if let Some(row) = pm.eval(Ctx::new(&pc, Timestamp(i as i64))) {
            ledger.push((
                Timestamp(i as i64),
                row.iter()
                    .enumerate()
                    .map(|(j, c)| Scalar::from_cell(PM_RECORD_KINDS[j], *c))
                    .collect(),
            ));
        }
    }
    let m = summarize_r(&ledger, &[]);
    assert!(m.n_trades >= 1, "expected at least one trade (entry then stop), got {m:?}");
    assert!(m.expectancy_r.is_finite());
    // entered ~100, stopped at the 95 level (price gapped to 94 through it) -> a loss.
    assert!(m.expectancy_r < 0.0 || m.n_open_at_end == 1);
}

/// Drive the real cross-crate chain `stop -> PositionManagement` over a `(bias, price)`
/// path, collecting the producer's dense records (decoded by the producer's own
/// `PM_RECORD_KINDS`, the wire contract) into a ledger — the recorded stream an R-yardstick
/// run would persist. Per-cycle bias (not constant) so a fixture can drop bias to 0 to
/// forbid the immediate re-entry after a stop. The fold is left to the caller so the
/// same recorded ledger can be summarized at several `round_trip_cost` values.
fn run_chain_ledger(stop: &mut dyn Node, path: &[(f64, f64)]) -> Vec<(Timestamp, Vec<Scalar>)> {
    let mut sc = vec![AnyColumn::with_capacity(ScalarKind::F64, 1)]; // stop input: price
    let mut pc: Vec<AnyColumn> =
        (0..4).map(|_| AnyColumn::with_capacity(ScalarKind::F64, 1)).collect();
    let mut pm = PositionManagement::new();
    let mut ledger: Vec<(Timestamp, Vec<Scalar>)> = vec![];
    for (i, &(bias, p)) in path.iter().enumerate() {
        let ts = Timestamp(i as i64);
        sc[0].push(Scalar::f64(p)).unwrap();
        let dist = stop.eval(Ctx::new(&sc, ts)).map(|c| c[0].f64()).unwrap_or(0.0);
        pc[0].push(Scalar::f64(bias)).unwrap();
        pc[1].push(Scalar::f64(p)).unwrap();
        pc[2].push(Scalar::f64(dist)).unwrap();
        pc[3].push(Scalar::f64(1.0)).unwrap(); // flat-1R size; R is size-invariant
        if let Some(row) = pm.eval(Ctx::new(&pc, ts)) {
            ledger.push((
                ts,
                row.iter().enumerate().map(|(j, c)| Scalar::from_cell(PM_RECORD_KINDS[j], *c)).collect(),
            ));
        }
    }
    ledger
}

/// Drive the real cross-crate chain `stop -> PositionManagement -> summarize_r`
/// over a `(bias, price)` path. This is the actual producer->consumer seam,
/// node-by-node; the resulting `RMetrics` is what a recorded R-yardstick run would report.
fn run_chain(stop: &mut dyn Node, path: &[(f64, f64)]) -> RMetrics {
    summarize_r(&run_chain_ledger(stop, path), &[])
}

/// The co-temporal cost stream a `ConstantCost(c)` node would emit over `ledger`: per row,
/// the round-trip cost `c` (price units) expressed in R via that row's latched distance
/// (`|entry_price - stop_price|`), placed in BOTH the closed-cost (col 0) and the open-cost
/// (col 2) slots of the producer's 3-wide `[cost_in_r, cum_cost_in_r, open_cost_in_r]` row.
/// A zero-latched (flat) row contributes no cost. `summarize_r` reads col 0 for a closed
/// trade and col 2 for the window-end open trade, so this reproduces the retired scalar
/// `round_trip_cost` argument exactly — the cost recovered from the producer's own
/// `entry_price`/`stop_price` columns, end-to-end. Col 1 (cum) is unread by `summarize_r`.
fn const_cost_stream(ledger: &[(Timestamp, Vec<Scalar>)], c: f64) -> Vec<(Timestamp, Vec<Scalar>)> {
    ledger
        .iter()
        .map(|(ts, row)| {
            let latched = (row[ENTRY_PRICE].as_f64() - row[STOP_PRICE].as_f64()).abs();
            let cir = if latched > 0.0 { c / latched } else { 0.0 };
            (*ts, vec![Scalar::f64(cir), Scalar::f64(0.0), Scalar::f64(cir)])
        })
        .collect()
}

/// A constant-long-bias path (`bias = +1` every cycle) over `prices`.
fn long_path(prices: &[f64]) -> Vec<(f64, f64)> {
    prices.iter().map(|&p| (1.0, p)).collect()
}

/// Property: **R is stop-defined, and the stop choice flows through the whole
/// producer->consumer seam.** The same constant-long-bias price path, folded
/// through two different stop rules, must yield different expectancy_r — because R
/// is `(exit - entry) / latched_distance` and the two rules latch different
/// distances. With a no-gap stop hit at exactly the stop level the fold returns
/// exactly -1R *whatever the distance*, so to observe the stop-defines-R property
/// end-to-end the price must overshoot the stop: then a wider FixedStop (distance
/// 10) realises a shallower R-loss than a tight FixedStop (distance 1). A run that
/// ignored the stop distance (e.g. measured R in raw pips) would collapse these.
#[test]
fn r_is_stop_defined_two_stops_fold_to_different_expectancy() {
    // Open long ~100, then a sharp drop that overshoots both stops.
    // FixedStop(10): stop @90, fill @85 -> R = (85-100)/10 = -1.5.
    let mut fixed = FixedStop::new(10.0);
    let wide = run_chain(&mut fixed, &long_path(&[100.0, 100.0, 85.0]));
    // A TIGHT FixedStop (distance 1.0): entry @100, stop @99; drop to 96 overshoots,
    // fill @96 -> R = (96-100)/1 = -4. A unit (1.0) far tighter than the wide unit (10.0),
    // so for a comparable drop it folds to a ~10x deeper R-loss — R is stop-defined.
    let mut tight = FixedStop::new(1.0);
    let tight_m = run_chain(&mut tight, &long_path(&[100.0, 100.0, 96.0]));
    assert!(wide.n_trades >= 1 && tight_m.n_trades >= 1);
    // R is the (exit-entry)/distance ratio: the tight unit (1.0) yields a far deeper
    // R-loss than the wide unit (10.0) for a comparable price drop.
    assert!(
        tight_m.expectancy_r < wide.expectancy_r,
        "stop choice must change folded R: tight={:?} wide={:?}",
        tight_m.expectancy_r,
        wide.expectancy_r,
    );
}

/// Property: **a position open at window end is force-closed into expectancy at its
/// unrealized R, not silently dropped — through the real fold.** A constant long
/// bias with a monotonically rising price never triggers a stop or bias-exit, so the
/// position is still open on the last record. `summarize_r` must count it
/// (`n_open_at_end == 1`) and fold its unrealized R into expectancy. Entry latches at
/// 100 with FixedStop distance 10; the last mark is 105, so the forced window-end
/// trade is exactly +0.5R and, being the only trade, that is the expectancy.
#[test]
fn open_at_window_end_is_folded_into_expectancy_not_dropped() {
    let mut stop = FixedStop::new(10.0);
    let m = run_chain(&mut stop, &long_path(&[100.0, 102.0, 105.0])); // long @100, never exits, last @105
    assert_eq!(m.n_open_at_end, 1, "the open position must be counted, not hidden");
    assert_eq!(m.n_trades, 1, "exactly the one window-end trade");
    assert!((m.expectancy_r - 0.5).abs() < 1e-9, "window-end R = (105-100)/10; got {}", m.expectancy_r);
}

/// Property: **a clean no-gap stop folds to exactly -1R through the chain (the
/// lagged-fill keystone), and that exact value survives the cross-crate fold.** A
/// long opened at 100 with FixedStop distance 10 (stop @90), price falling to touch
/// 90 exactly, realises -1.0R at the producer and `summarize_r` reports expectancy
/// exactly -1.0 — the R-unit's defining identity (1R = the loss if stopped),
/// asserted end-to-end rather than only on hand-built rows. Bias drops to 0 on the
/// stop cycle so the producer does not immediately re-enter a fresh long (which
/// would add a window-end trade) — isolating the single stopped trade.
#[test]
fn clean_stop_folds_to_exactly_minus_one_r() {
    let mut stop = FixedStop::new(10.0);
    // (bias, price): open long @100, hold, then bias->0 as price touches the 90 stop.
    let m = run_chain(&mut stop, &[(1.0, 100.0), (1.0, 100.0), (0.0, 90.0)]);
    assert_eq!(m.n_trades, 1);
    assert_eq!(m.n_open_at_end, 0, "the stop closed it before window end");
    assert!((m.expectancy_r + 1.0).abs() < 1e-9, "1R = the loss if stopped; got {}", m.expectancy_r);
}

/// Property: **net-of-cost expectancy is gross minus one round-trip cost per trade,
/// charged in R via the `latched_dist` the consumer recovers from the producer's
/// `entry_price`/`stop_price` columns — through the real seam.** A constant long opened
/// at 100 on FixedStop distance 10 (so `latched_dist = |100 - 90| = 10`), price rising to
/// 110, folds to a gross window-end R of +1.0; charging a 2.0 price-unit round-trip cost
/// must lower *net* expectancy to `1.0 - 2.0/10 = 0.8` while leaving gross untouched. The
/// cost is recovered end-to-end (not from a hand-built row): if the producer reordered
/// `entry_price`/`stop_price` or `summarize_r` recovered the distance wrong, net would
/// drift here while gross stayed correct — the failure mode this seam test exists to catch.
#[test]
fn net_of_cost_charges_one_round_trip_per_trade_through_the_recovered_latched_dist() {
    let mut stop = FixedStop::new(10.0);
    let ledger = run_chain_ledger(&mut stop, &long_path(&[100.0, 105.0, 110.0]));
    let gross = summarize_r(&ledger, &[]);
    let net = summarize_r(&ledger, &const_cost_stream(&ledger, 2.0));
    assert_eq!(gross.n_trades, 1);
    assert!((gross.expectancy_r - 1.0).abs() < 1e-9, "gross window-end R = (110-100)/10; got {}", gross.expectancy_r);
    // gross is untouched by the cost; only net_expectancy_r absorbs it.
    assert!((net.expectancy_r - 1.0).abs() < 1e-9, "round_trip_cost must never change gross expectancy");
    assert!(
        (net.net_expectancy_r - 0.8).abs() < 1e-9,
        "net = gross - cost/latched_dist = 1.0 - 2.0/10; got {}",
        net.net_expectancy_r,
    );
}

/// Property: **a wider stop dilutes the same price-unit round-trip cost in R — the cost
/// is per-R, not per-pip — proven through the producer-recovered `latched_dist`.** The same
/// constant cost (2.0 price units) charged against a tight FixedStop (distance 5, latched 5)
/// costs `2.0/5 = 0.4R`, but against a wide FixedStop (distance 20, latched 20) only
/// `2.0/20 = 0.1R`. Both paths open at 100 and rise so the gross window-end R differs by
/// construction, but the *cost component* (gross - net) must be strictly larger for the
/// tighter stop — a run that charged cost in raw pips (ignoring the latched distance) would
/// collapse these to equal, the regression this guards.
#[test]
fn net_of_cost_is_charged_per_r_so_a_wider_stop_dilutes_it() {
    let mut tight = FixedStop::new(5.0);
    let tight_l = run_chain_ledger(&mut tight, &long_path(&[100.0, 102.0, 105.0]));
    let (tg, tn) = (summarize_r(&tight_l, &[]), summarize_r(&tight_l, &const_cost_stream(&tight_l, 2.0)));
    let mut wide = FixedStop::new(20.0);
    let wide_l = run_chain_ledger(&mut wide, &long_path(&[100.0, 102.0, 105.0]));
    let (wg, wn) = (summarize_r(&wide_l, &[]), summarize_r(&wide_l, &const_cost_stream(&wide_l, 2.0)));
    let tight_cost = tg.expectancy_r - tn.net_expectancy_r; // 2.0/5 = 0.4
    let wide_cost = wg.expectancy_r - wn.net_expectancy_r; // 2.0/20 = 0.1
    assert!((tight_cost - 0.4).abs() < 1e-9, "tight cost = 2/5; got {tight_cost}");
    assert!((wide_cost - 0.1).abs() < 1e-9, "wide cost = 2/20; got {wide_cost}");
    assert!(tight_cost > wide_cost, "a tighter stop must pay more R per fixed price-unit cost");
}

/// Drive the REAL `aura_std::ConstantCost(c)` node over a recorded PM `ledger`,
/// node-by-node off the producer's own `closed`/`open`/`entry_price`/`stop_price`
/// columns — the actual cost producer the run-path graph taps (not an algebraic stand-in
/// like `const_cost_stream`). Returns the node's 3-wide `[cost_in_r, cum_cost_in_r,
/// open_cost_in_r]` rows, co-temporal 1:1 with `ledger`. Fresh single-element columns per
/// cycle (the node's lookback is 1), matching the node's own unit-test driving pattern.
fn const_cost_node_stream(
    ledger: &[(Timestamp, Vec<Scalar>)],
    c: f64,
) -> Vec<(Timestamp, Vec<Scalar>)> {
    let mut node = ConstantCost::new(c);
    ledger
        .iter()
        .map(|(ts, row)| {
            let mut cols = vec![
                AnyColumn::with_capacity(ScalarKind::Bool, 1), // closed
                AnyColumn::with_capacity(ScalarKind::Bool, 1), // open
                AnyColumn::with_capacity(ScalarKind::F64, 1),  // entry
                AnyColumn::with_capacity(ScalarKind::F64, 1),  // stop
            ];
            cols[0].push(Scalar::bool(row[CLOSED].as_bool())).unwrap();
            cols[1].push(Scalar::bool(row[OPEN].as_bool())).unwrap();
            cols[2].push(Scalar::f64(row[ENTRY_PRICE].as_f64())).unwrap();
            cols[3].push(Scalar::f64(row[STOP_PRICE].as_f64())).unwrap();
            let out = node.eval(Ctx::new(&cols, *ts)).expect("cost row co-temporal with PM record");
            (*ts, out.iter().map(|cell| Scalar::f64(cell.f64())).collect())
        })
        .collect()
}

/// Property (Task 3, the run-path net seam): **the in-graph `net_r_equity` tap and the
/// post-run `summarize_r` net fold agree on the same recorded streams.** The run-path
/// graph taps a per-cycle `net_r_equity = cum_realized_r + unrealized_r − cum_cost_in_r −
/// open_cost_in_r` (a `LinComb` over the executor + `ConstantCost` outputs), while
/// `summarize_r` independently folds the same PM records + cost stream into
/// `net_expectancy_r`. The FINAL `net_r_equity` sample must equal the post-run net total
/// (`net_expectancy_r × n_trades` = Σ over trades of `r − cost`) — a divergence would mean
/// the charted net-R-equity series and the reported net metric disagree. The path carries
/// one clean stopped loser AND a window-end open trade, so both the cumulative-close-cost
/// (`cum_cost_in_r`) and the window-end open-cost (`open_cost_in_r`) terms bite. The cost is
/// produced by the real `ConstantCost` node, the same producer the run-path wires.
#[test]
fn net_r_equity_final_sample_agrees_with_summarize_r_net_total() {
    let mut stop = FixedStop::new(10.0);
    // open long @100, stop out at the 90 level (bias->0): a clean -1R closed loser; then a
    // fresh long @100 held up to 105, open at window end (+0.5R). Closed-cost charges
    // cum_cost_in_r; the open trade charges open_cost_in_r — both net terms exercised.
    let ledger = run_chain_ledger(
        &mut stop,
        &[(1.0, 100.0), (1.0, 100.0), (0.0, 90.0), (1.0, 100.0), (1.0, 102.0), (1.0, 105.0)],
    );
    let cost = const_cost_node_stream(&ledger, 2.0);
    let m = summarize_r(&ledger, &cost);
    assert_eq!(m.n_trades, 2, "one closed loser + one window-end open trade");
    assert_eq!(m.n_open_at_end, 1, "the second long is still open at window end");
    // post-run net total = mean net R × trade count = Σ (r − cost) over all trades.
    let post_run_net_total = m.net_expectancy_r * m.n_trades as f64;
    // in-graph final net_r_equity sample = the LinComb(1,1,−1,−1) at the last cycle.
    let (_, last_pm) = ledger.last().unwrap();
    let (_, last_cost) = cost.last().unwrap();
    let net_eq_final = last_pm[CUM_REALIZED_R].as_f64() + last_pm[UNREALIZED_R].as_f64()
        - last_cost[CUM_COST_IN_R].as_f64()
        - last_cost[OPEN_COST_IN_R].as_f64();
    assert!(
        (net_eq_final - post_run_net_total).abs() < 1e-9,
        "in-graph net_r_equity {net_eq_final} must equal post-run net total {post_run_net_total}",
    );
}

/// Drive the REAL `aura_std::VolSlippageCost(k)` node over a recorded PM `ledger`
/// with a constant `vol` per cycle — the run-path's vol-slippage producer. Returns
/// the node's 3-wide `[cost_in_r, cum_cost_in_r, open_cost_in_r]` rows, co-temporal
/// 1:1 with `ledger`.
fn vol_slippage_node_stream(
    ledger: &[(Timestamp, Vec<Scalar>)],
    k: f64,
    vol: f64,
) -> Vec<(Timestamp, Vec<Scalar>)> {
    let mut node = VolSlippageCost::new(k);
    ledger
        .iter()
        .map(|(ts, row)| {
            let mut cols = vec![
                AnyColumn::with_capacity(ScalarKind::Bool, 1), // closed
                AnyColumn::with_capacity(ScalarKind::Bool, 1), // open
                AnyColumn::with_capacity(ScalarKind::F64, 1),  // entry
                AnyColumn::with_capacity(ScalarKind::F64, 1),  // stop
                AnyColumn::with_capacity(ScalarKind::F64, 1),  // volatility
            ];
            cols[0].push(Scalar::bool(row[CLOSED].as_bool())).unwrap();
            cols[1].push(Scalar::bool(row[OPEN].as_bool())).unwrap();
            cols[2].push(Scalar::f64(row[ENTRY_PRICE].as_f64())).unwrap();
            cols[3].push(Scalar::f64(row[STOP_PRICE].as_f64())).unwrap();
            cols[4].push(Scalar::f64(vol)).unwrap();
            let out = node.eval(Ctx::new(&cols, *ts)).expect("cost row co-temporal with PM record");
            (*ts, out.iter().map(|cell| Scalar::f64(cell.f64())).collect())
        })
        .collect()
}

/// Drive the REAL `aura_std::CostSum(n)` aggregator over `n` co-temporal cost
/// streams, summing them per-field — the run-path's cost-graph output node.
fn cost_sum_node_stream(streams: &[&[(Timestamp, Vec<Scalar>)]]) -> Vec<(Timestamp, Vec<Scalar>)> {
    let n = streams.len();
    let len = streams[0].len();
    let mut node = CostSum::new(n);
    (0..len)
        .map(|i| {
            let ts = streams[0][i].0;
            let mut cols: Vec<AnyColumn> =
                (0..n * 3).map(|_| AnyColumn::with_capacity(ScalarKind::F64, 1)).collect();
            for (k, s) in streams.iter().enumerate() {
                let (_, row) = &s[i];
                for f in 0..3 {
                    cols[k * 3 + f].push(Scalar::f64(row[f].as_f64())).unwrap();
                }
            }
            let out = node.eval(Ctx::new(&cols, ts)).expect("aggregate co-temporal with cost streams");
            (ts, out.iter().map(|cell| Scalar::f64(cell.f64())).collect())
        })
        .collect()
}

/// Property (spec 0082, exact composition): the `CostSum` of the real
/// `ConstantCost` + `VolSlippageCost` node streams folds through `summarize_r` to
/// the exact additive net — `net_both == net_flat + net_vol − gross` (since each
/// single net is `gross − its_mean_cost`, the composed net subtracts BOTH mean
/// costs). The path carries a clean stopped loser AND a window-end open trade, so
/// both the cumulative-close-cost and the window-end open-cost terms bite.
#[test]
fn cost_sum_composes_constant_and_vol_slippage_exactly() {
    let mut stop = FixedStop::new(10.0);
    let ledger = run_chain_ledger(
        &mut stop,
        &[(1.0, 100.0), (1.0, 100.0), (0.0, 90.0), (1.0, 100.0), (1.0, 102.0), (1.0, 105.0)],
    );
    let cc = const_cost_node_stream(&ledger, 2.0);
    let vs = vol_slippage_node_stream(&ledger, 0.5, 3.0);
    let summed = cost_sum_node_stream(&[&cc, &vs]);

    let gross = summarize_r(&ledger, &[]).expectancy_r;
    let net_flat = summarize_r(&ledger, &cc).net_expectancy_r;
    let net_vol = summarize_r(&ledger, &vs).net_expectancy_r;
    let net_both = summarize_r(&ledger, &summed).net_expectancy_r;

    // additive identity: mean(r − cc − vs) == mean(r − cc) + mean(r − vs) − mean(r)
    assert!(
        (net_both - (net_flat + net_vol - gross)).abs() < 1e-9,
        "composition is exact + additive: net_both {net_both} == net_flat {net_flat} + net_vol {net_vol} − gross {gross}",
    );
    // both costs bite: the composed net is strictly below either single-cost net.
    assert!(net_both < net_flat && net_both < net_vol, "both costs bite");
}

/// Property (spec 0082): the aggregate `CostSum` stream agrees with the in-graph
/// net seam — the in-graph final `net_r_equity` sample (a LinComb over the executor
/// outputs and the AGGREGATE cost) equals the post-run `summarize_r` net total. The
/// aggregate is the one cost stream the net tap and `summarize_r` both read.
#[test]
fn aggregate_net_r_equity_final_sample_agrees_with_summarize_r_net_total() {
    let mut stop = FixedStop::new(10.0);
    let ledger = run_chain_ledger(
        &mut stop,
        &[(1.0, 100.0), (1.0, 100.0), (0.0, 90.0), (1.0, 100.0), (1.0, 102.0), (1.0, 105.0)],
    );
    let cc = const_cost_node_stream(&ledger, 2.0);
    let vs = vol_slippage_node_stream(&ledger, 0.5, 3.0);
    let summed = cost_sum_node_stream(&[&cc, &vs]);

    let m = summarize_r(&ledger, &summed);
    let post_run_net_total = m.net_expectancy_r * m.n_trades as f64;
    let (_, last_pm) = ledger.last().unwrap();
    let (_, last_cost) = summed.last().unwrap();
    let net_eq_final = last_pm[CUM_REALIZED_R].as_f64() + last_pm[UNREALIZED_R].as_f64()
        - last_cost[CUM_COST_IN_R].as_f64()
        - last_cost[OPEN_COST_IN_R].as_f64();
    assert!(
        (net_eq_final - post_run_net_total).abs() < 1e-9,
        "in-graph aggregate net_r_equity {net_eq_final} must equal post-run net total {post_run_net_total}",
    );
}

/// Property (spec 0082): `CostSum(1)` is the identity — a lone vol-slippage stream
/// folds through the aggregator to exactly the un-aggregated net (the run path is
/// uniform whether one or several cost nodes are wired).
#[test]
fn cost_sum_of_one_is_identity_for_vol_slippage() {
    let mut stop = FixedStop::new(10.0);
    let ledger = run_chain_ledger(&mut stop, &long_path(&[100.0, 102.0, 105.0]));
    let vs = vol_slippage_node_stream(&ledger, 0.5, 3.0);
    let single = cost_sum_node_stream(&[&vs]);
    assert_eq!(
        summarize_r(&ledger, &single).net_expectancy_r,
        summarize_r(&ledger, &vs).net_expectancy_r,
        "CostSum(1) does not move the net",
    );
}

/// Property (#130): **`sqn` and `sqn_normalized` (SQN100) are computed from the
/// producer's *recorded* dense records, not hand-built rows, and below the
/// 100-trade cap the two are identical.** Every `summarize_r` test that touches
/// `sqn_normalized` lives in `report.rs` and feeds rows whose column indices are
/// hardcoded independently of the producer — the exact seam this file's header
/// warns about. Here a real `FixedStop -> PositionManagement` chain produces four
/// closed trades (three +1R winners, one -1R loser) whose recorded `realized_r`
/// the consumer reads back: mean_R = 0.5, sample-sd = 1.0, so q = mean/sd = 0.5
/// and `sqn = √4·q = 1.0`. n = 4 ≤ SQN_CAP (100), so the cap is inactive and
/// `sqn_normalized = √(min(4,100))·q = sqn` *exactly*. A producer-layout drift
/// that misaligned `realized_r` would break both values here even though the
/// hand-built `report.rs` units stayed green.
#[test]
fn sqn_and_sqn_normalized_fold_from_recorded_records_and_agree_below_cap() {
    let mut stop = FixedStop::new(10.0);
    let mut path: Vec<(f64, f64)> = vec![];
    // three winners: open long @100 (FixedStop dist 10 -> latched 10), exit @110
    // via bias->0 -> realized_r = (110-100)/10 = +1R each.
    for _ in 0..3 {
        path.push((1.0, 100.0));
        path.push((1.0, 105.0));
        path.push((0.0, 110.0));
    }
    // one loser: open @100, then bias->0 as price touches the 90 stop -> -1R.
    path.push((1.0, 100.0));
    path.push((1.0, 100.0));
    path.push((0.0, 90.0));
    let m = run_chain(&mut stop, &path);
    assert_eq!(m.n_trades, 4, "three winners + one loser, all closed before window end");
    assert_eq!(m.n_open_at_end, 0, "every trade exits via bias->0; nothing open at end");
    assert!((m.expectancy_r - 0.5).abs() < 1e-9, "mean R = (1+1+1-1)/4; got {}", m.expectancy_r);
    // sqn = √n · mean/sd = √4 · (0.5/1.0) = 1.0, folded from the recorded realized_r.
    assert!((m.sqn - 1.0).abs() < 1e-9, "raw sqn = √4·(0.5/1.0); got {}", m.sqn);
    // n = 4 <= SQN_CAP(100): the cap is inactive, so SQN100 == raw sqn exactly.
    assert!(
        (m.sqn_normalized - m.sqn).abs() < 1e-12,
        "below the cap, sqn_normalized must equal sqn (got {} vs {})",
        m.sqn_normalized,
        m.sqn,
    );
}

/// Contract guard (the lockstep cross-crate column-index agreement `report.rs`
/// names): `PM_WIDTH` is the fixed dense-record arity, the indices `summarize_r`
/// reads the dense record by must equal the indices the producer's authoritative
/// `PM_FIELD_NAMES` assigns those columns, and the producer's `PM_RECORD_KINDS` at
/// those indices must be the kinds `summarize_r` assumes when it reads them as
/// bool / f64. `aura-std` is only a dev-dependency, so nothing but this test stops
/// a `PM_WIDTH` change or a `PM_FIELD_NAMES` reorder from silently breaking the fold.
#[test]
fn r_col_indices_match_producer_field_layout() {
    assert_eq!(PM_WIDTH, 14);
    assert_eq!(PM_FIELD_NAMES.len(), PM_WIDTH);
    assert_eq!(PM_RECORD_KINDS.len(), PM_WIDTH);

    // names: the consumer's read indices land on the columns it intends.
    assert_eq!(PM_FIELD_NAMES[CLOSED], "closed_this_cycle");
    assert_eq!(PM_FIELD_NAMES[REALIZED_R], "realized_r");
    assert_eq!(PM_FIELD_NAMES[OPEN], "open");
    assert_eq!(PM_FIELD_NAMES[UNREALIZED_R], "unrealized_r");

    // kinds: summarize_r reads CLOSED/OPEN as bool and REALIZED_R/UNREALIZED_R as f64.
    assert_eq!(PM_RECORD_KINDS[CLOSED], ScalarKind::Bool);
    assert_eq!(PM_RECORD_KINDS[OPEN], ScalarKind::Bool);
    assert_eq!(PM_RECORD_KINDS[REALIZED_R], ScalarKind::F64);
    assert_eq!(PM_RECORD_KINDS[UNREALIZED_R], ScalarKind::F64);

    // iter-2 reads: entry_price (6), stop_price (7), conviction_at_entry (9) — the geometry
    // summarize_r recovers latched_dist (net-of-cost) and conviction (terciles) from.
    assert_eq!(PM_FIELD_NAMES[ENTRY_PRICE], "entry_price");
    assert_eq!(PM_FIELD_NAMES[STOP_PRICE], "stop_price");
    assert_eq!(PM_FIELD_NAMES[CONVICTION_AT_ENTRY], "conviction_at_entry");
    assert_eq!(PM_RECORD_KINDS[ENTRY_PRICE], ScalarKind::F64);
    assert_eq!(PM_RECORD_KINDS[STOP_PRICE], ScalarKind::F64);
    assert_eq!(PM_RECORD_KINDS[CONVICTION_AT_ENTRY], ScalarKind::F64);

    // iter-2 size column (10): the Sizer's `size` flows here, and the sibling
    // `risk_executor.rs` fixture asserts its R-invariance by reading this index — so the
    // size column's position is pinned against the producer layout too, not just the reads
    // `summarize_r` makes (the lockstep claim in that fixture's header relies on this).
    assert_eq!(PM_FIELD_NAMES[SIZE], "size");
    assert_eq!(PM_RECORD_KINDS[SIZE], ScalarKind::F64);

    // iter-2 direction column (4): the signed bias sign `derive_position_events` reads to
    // decide Buy vs Sell. Pinned against the producer layout so a reorder can't silently
    // flip the derived event-table's action — the lockstep claim now covers the derive too.
    assert_eq!(PM_FIELD_NAMES[DIRECTION], "direction");
    assert_eq!(PM_RECORD_KINDS[DIRECTION], ScalarKind::I64);
}

/// Property (#114, derived not hand-built): **a bias reversal in the REAL producer
/// derives Close-then-opposite-open at one `event_ts`, close first.** The reversal
/// contract is the central acceptance criterion of the derive, but the `report.rs`
/// unit sets the `closed`/`direction`/`open`/`size` columns by hand, independently
/// of `PositionManagement` — so it can stay green while the producer's actual
/// reversal row (one record carrying `closed_this_cycle=true` AND `open=true` AND a
/// flipped `direction`) disagrees with the column indices the derive reads. Here the
/// real `FixedStop -> PositionManagement` chain emits that one reversal record (long
/// opened @100, bias flips to short @110 with no stop hit -> ReversalLeg + reopen),
/// and the derive must turn it into exactly Buy@t0, then at the SAME flip instant
/// Close(the long) before Sell(the short). A producer-layout drift that misplaced
/// `direction`, or a derive that opened before it closed, breaks this even though the
/// hand-built `report.rs` reversal unit stays green — the cross-crate seam this guards.
#[test]
fn reversal_in_real_producer_derives_close_then_opposite_open_at_one_ts() {
    let mut stop = FixedStop::new(10.0);
    // (bias, price): open long @100, hold @105, then bias flips short @110 (above the
    // 90 stop -> no stop hit, a clean reversal leg).
    let ledger = run_chain_ledger(&mut stop, &[(1.0, 100.0), (1.0, 105.0), (-1.0, 110.0)]);
    let events = derive_position_events(&ledger, 3);

    // open long; reversal at the flip cycle = Close(long) then Sell(short), same ts;
    // the short is open at window end, so no synthetic Close after it.
    assert_eq!(events.len(), 3, "Buy, Close, Sell; got {events:?}");
    assert_eq!(events[0].action, PositionAction::Buy);
    assert_eq!(events[0].position_id, 0);

    // close before open, at one and the same instant (the #114 ordering contract).
    assert_eq!(events[1].action, PositionAction::Close);
    assert_eq!(events[1].position_id, 0, "the Close references the long it closes");
    assert_eq!(events[2].action, PositionAction::Sell, "opposite-direction reopen");
    assert_eq!(events[2].position_id, 1, "a fresh position id for the reopened leg");
    assert_eq!(
        events[1].event_ts, events[2].event_ts,
        "Close and the opposite open share the reversal instant",
    );
    // close strictly before open in emission order (the table is ordered, close first).
    let (close_idx, sell_idx) = (1usize, 2usize);
    assert!(close_idx < sell_idx, "Close must be emitted before the opposite open");

    assert!(events.iter().all(|e| e.instrument_id == 3));
}

/// Property: **a short entry in the REAL producer derives a `Sell`, reading the
/// producer's signed `direction` column across the crate seam.** Every other E2E
/// path opens longs only, so the derive's `dir < 0 => Sell` branch — and its read of
/// the real `PositionManagement` `direction` column (i64, index 4) — is exercised
/// nowhere else end-to-end. A constant `bias = -1` path opens a short directly; the
/// derive must emit `Sell` (not `Buy`). A producer that wrote `direction` to a
/// different index, or a derive that defaulted to `Buy`, breaks this.
#[test]
fn short_entry_in_real_producer_derives_sell() {
    let mut stop = FixedStop::new(10.0);
    // constant short bias: open short @100, hold while price falls (a winning short).
    let ledger = run_chain_ledger(&mut stop, &[(-1.0, 100.0), (-1.0, 98.0), (-1.0, 96.0)]);
    let events = derive_position_events(&ledger, 1);
    assert!(!events.is_empty(), "a short must open");
    assert_eq!(events[0].action, PositionAction::Sell, "negative direction derives Sell");
    assert_eq!(events[0].position_id, 0);
}

/// Property: the derived position-event table agrees with the R-metrics fold over
/// the SAME recorded ledger — one Close per closed round-trip, every open either
/// closed in-window or still open at window end, every Close referencing an earlier
/// open, and the caller's instrument_id threaded onto every event.
#[test]
fn derived_event_table_agrees_with_r_metrics_over_one_ledger() {
    let mut stop = FixedStop::new(5.0);
    let ledger =
        run_chain_ledger(&mut stop, &long_path(&[100.0, 104.0, 108.0, 110.0, 102.0, 96.0, 94.0]));
    let m: RMetrics = summarize_r(&ledger, &[]);
    let events = derive_position_events(&ledger, 7);

    let closes = events.iter().filter(|e| e.action == PositionAction::Close).count() as u64;
    let opens = events
        .iter()
        .filter(|e| matches!(e.action, PositionAction::Buy | PositionAction::Sell))
        .count() as u64;
    // one Close per closed round-trip; a window-end open position has no Close.
    assert_eq!(closes, m.n_trades - m.n_open_at_end);
    // every open is either closed in-window or still open at window end.
    assert_eq!(opens, closes + m.n_open_at_end);
    assert!(opens >= 1, "scenario must open at least one position");

    // referential integrity: every Close references a position_id opened earlier.
    let mut opened: std::collections::HashSet<i64> = std::collections::HashSet::new();
    for e in &events {
        match e.action {
            PositionAction::Buy | PositionAction::Sell => {
                opened.insert(e.position_id);
            }
            PositionAction::Close => assert!(opened.contains(&e.position_id)),
        }
    }
    // the caller-supplied instrument_id is threaded onto every event.
    assert!(events.iter().all(|e| e.instrument_id == 7));
}
