//! Fieldtest c0007 #2 — Exposure shaping: clamp to [-1,+1] and sizing via scale.
//!
//! Axis: "intent/exposure shaping via Exposure". A researcher who has a raw
//! signal score wants to know the exact exposure it maps to, and to confirm the
//! bound is hard (saturation), the sign is preserved, and `scale` is the sizing
//! knob (rustdoc struct.Exposure: clamp(signal/scale, -1, +1); ledger C10:
//! "sizing/risk live in `scale`").
//!
//!   raw-score source --> Exposure(scale=10) --> Recorder
//!
//! We feed the score directly as a source so the mapping is isolated from any
//! indicator warm-up: a score s -> clamp(s/10, -1, +1).

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

use aura_core::{Ctx, Firing, InputSpec, Node, NodeSchema, Scalar, ScalarKind, Timestamp};
use aura_engine::{Edge, Harness, SourceSpec, Target};
use aura_std::Exposure;

struct Recorder {
    tx: Sender<(Timestamp, f64)>,
}
impl Node for Recorder {
    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(), w[0]));
        None
    }
}

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

fn main() {
    let (tx, rx) = mpsc::channel();

    // nodes: 0 = Exposure(scale=10), 1 = Recorder.
    let mut h = Harness::bootstrap(
        vec![Box::new(Exposure::new(10.0)), Box::new(Recorder { tx })],
        vec![SourceSpec { kind: ScalarKind::F64, targets: vec![Target { node: 0, slot: 0 }] }],
        vec![Edge { from: 0, to: 1, slot: 0, from_field: 0 }],
    )
    .expect("valid exposure DAG");

    // Raw scores spanning below, inside, and above the saturation band, both signs.
    let scores: &[(i64, f64)] = &[
        (1, 0.0),    // -> 0.0
        (2, 5.0),    // -> 0.5
        (3, 10.0),   // -> 1.0   (exactly at scale)
        (4, 25.0),   // -> 1.0   (saturated, NOT 2.5)
        (5, -7.0),   // -> -0.7
        (6, -40.0),  // -> -1.0  (saturated negative)
        (7, 3.0),    // -> 0.3
    ];
    h.run(vec![f64_stream(scores)]);

    let exposure: Vec<(Timestamp, f64)> = rx.try_iter().collect();
    println!("recorded exposure = {exposure:?}");

    let expected = vec![
        (Timestamp(1), 0.0),
        (Timestamp(2), 0.5),
        (Timestamp(3), 1.0),
        (Timestamp(4), 1.0),
        (Timestamp(5), -0.7),
        (Timestamp(6), -1.0),
        (Timestamp(7), 0.3),
    ];
    println!("expected exposure = {expected:?}");

    // Every recorded value must lie in [-1, +1].
    assert!(
        exposure.iter().all(|(_, v)| (-1.0..=1.0).contains(v)),
        "every exposure within the hard bound [-1,+1]"
    );
    assert_eq!(exposure, expected, "clamp(score/scale,-1,+1) mapping");
    println!("c0007_2 OK: Exposure clamps and sizes via scale, sign preserved");
}
