//! Fieldtest c0009 #2 — comparison + determinism over the report surface.
//!
//! Axis (carrier): run two harnesses differing in a tuning param (Exposure
//! scale), confirm each run is deterministic (bit-identical metrics across two
//! runs) and the two metrics differ as expected. This is the smallest slice of
//! the World's reason to exist (C21): compare runs by their RunMetrics.
//!
//! Same SMA-cross harness as #1, but the Exposure node's `scale` is the swept
//! tuning param. A smaller scale => larger |exposure| (clamp(signal/scale,..)),
//! so the same price move earns proportionally more pips — until the exposure
//! saturates at +1. So total_pips(scale=2) > total_pips(scale=4) here.

use std::sync::mpsc::{self, Sender};

use aura_core::{Ctx, Firing, InputSpec, Node, NodeSchema, Scalar, ScalarKind, Timestamp};
use aura_engine::{
    Edge, Harness, RunManifest, RunMetrics, RunReport, SourceSpec, Target, f64_field, summarize,
};
use aura_std::{Exposure, SimBroker, Sma, Sub};

struct RowRecorder {
    tx: Sender<(Timestamp, Vec<Scalar>)>,
}
impl Node for RowRecorder {
    fn schema(&self) -> NodeSchema {
        NodeSchema {
            inputs: vec![InputSpec { kind: ScalarKind::F64, lookback: 1, firing: Firing::Any }],
            output: vec![],
        }
    }
    fn eval(&mut self, ctx: Ctx<'_>) -> Option<&[Scalar]> {
        let w = ctx.f64_in(0);
        if w.is_empty() {
            return None;
        }
        let _ = self.tx.send((ctx.now(), vec![Scalar::F64(w[0])]));
        None
    }
}

fn f64_stream(pairs: &[(i64, f64)]) -> Vec<(Timestamp, Scalar)> {
    pairs.iter().map(|&(t, v)| (Timestamp(t), Scalar::F64(v))).collect()
}

const PRICES: &[(i64, f64)] = &[
    (1, 100.0),
    (2, 102.0),
    (3, 104.0),
    (4, 106.0),
    (5, 108.0),
    (6, 110.0),
    (7, 112.0),
];

/// One full run: bootstrap a harness with the given exposure scale, run, drain
/// the two sinks, reduce to a RunReport. This is exactly the World's per-instance
/// step in a tuning sweep (C12/C19/C20), authored in plain Rust.
fn run_one(exposure_scale: f64) -> RunReport {
    let (eq_tx, eq_rx) = mpsc::channel();
    let (exp_tx, exp_rx) = mpsc::channel();

    let mut h = Harness::bootstrap(
        vec![
            Box::new(Sma::new(2)),
            Box::new(Sma::new(4)),
            Box::new(Sub::new()),
            Box::new(Exposure::new(exposure_scale)),
            Box::new(SimBroker::new(1.0)),
            Box::new(RowRecorder { tx: eq_tx }),
            Box::new(RowRecorder { tx: exp_tx }),
        ],
        vec![SourceSpec {
            kind: ScalarKind::F64,
            targets: vec![
                Target { node: 0, slot: 0 },
                Target { node: 1, slot: 0 },
                Target { node: 4, slot: 1 },
            ],
        }],
        vec![
            Edge { from: 0, to: 2, slot: 0, from_field: 0 },
            Edge { from: 1, to: 2, slot: 1, from_field: 0 },
            Edge { from: 2, to: 3, slot: 0, from_field: 0 },
            Edge { from: 3, to: 4, slot: 0, from_field: 0 },
            Edge { from: 3, to: 6, slot: 0, from_field: 0 },
            Edge { from: 4, to: 5, slot: 0, from_field: 0 },
        ],
    )
    .expect("valid harness");

    h.run(vec![f64_stream(PRICES)]);

    let equity = f64_field(&eq_rx.try_iter().collect::<Vec<_>>(), 0);
    let exposure = f64_field(&exp_rx.try_iter().collect::<Vec<_>>(), 0);
    let metrics = summarize(&equity, &exposure);

    RunReport {
        manifest: RunManifest {
            commit: "fieldtest-c0009-synthetic".to_string(),
            params: vec![("exposure_scale".to_string(), exposure_scale)],
            window: (Timestamp(1), Timestamp(7)),
            seed: 0,
            broker: "sim-optimal(pip_size=1)".to_string(),
        },
        metrics,
    }
}

fn main() {
    // Determinism (C1): the same instance run twice yields bit-identical metrics.
    let a1 = run_one(4.0);
    let a2 = run_one(4.0);
    assert_eq!(a1.metrics, a2.metrics, "scale=4 deterministic across runs");
    assert_eq!(a1.to_json(), a2.to_json(), "scale=4 JSON bit-identical");
    println!("determinism OK: scale=4 metrics = {:?}", a1.metrics);

    // Comparison: a second instance with a different tuning param.
    let b = run_one(2.0);
    println!("scale=4 report json = {}", a1.to_json());
    println!("scale=2 report json = {}", b.to_json());

    // Expectation (public model): scale=2 doubles |exposure| (0.5 -> 1.0 saturated)
    // versus scale=4 (0.5), so the same +2/cycle price move earns ~2x the pips.
    let RunMetrics { total_pips: p4, .. } = a1.metrics;
    let RunMetrics { total_pips: p2, .. } = b.metrics;
    println!("total_pips scale=4 = {p4}, scale=2 = {p2}");
    assert!(p2 > p4, "smaller scale => larger exposure => more pips ({p2} > {p4})");

    // Both should be drawdown-free monotonic-up curves on a steadily-rising price,
    // and stay long throughout (no sign flips) — a sanity check the comparison is
    // apples-to-apples, differing only on the swept axis.
    assert_eq!(a1.metrics.max_drawdown, 0.0);
    assert_eq!(b.metrics.max_drawdown, 0.0);
    assert_eq!(a1.metrics.exposure_sign_flips, 0);
    assert_eq!(b.metrics.exposure_sign_flips, 0);

    println!("c0009_2 OK: deterministic + comparable two-run metrics over the report surface");
}
