// Cycle-0049 fieldtest — example 2: the typed VALIDATION GATE.
//
// Declare invalid ranges and observe the typed `SweepError` returned by
// `RandomSpace::new` BEFORE any run (axis-hint 2). Exercises every reachable
// SweepError construction fault:
//   - Arity              (wrong number of ranges vs. param-space slots)
//   - RangeKindMismatch  (F64 range on an I64 slot)
//   - EmptyRange (I64)   (lo > hi)
//   - EmptyRange (F64)   (lo >= hi — the half-open lo == hi case)
//   - the valid I64 lo == hi single-point range (accepted, NOT an error)
//   - NonNumericRange    (a range on a Bool slot)
//
// The NonNumericRange case needs a node with a Bool param. No shipped aura-std
// node has one (all knobs are I64 `length` / F64 `scale`), so — exactly as a
// C16 research project would (it authors its own nodes in Rust) — this fixture
// authors a tiny custom pass-through node `GateNode` whose single knob is a Bool.
//
// PUBLIC INTERFACE ONLY: ledger + glossary + spec 0049 + rustdoc. No
// crates/*/src was read.

use aura_core::{
    Cell, Ctx, FieldSpec, Firing, Node, NodeSchema, ParamSpec, PortSpec, PrimitiveBuilder,
    ScalarKind,
};
use aura_engine::{
    BlueprintNode, Composite, Edge, OutField, ParamRange, RandomSpace, Role, SweepError, Target,
};
use aura_std::{Exposure, Sma, Sub};

// ---- A minimal custom node with a BOOL param (what a real project would write
// to need the NonNumericRange gate). It passes its f64 input through unchanged;
// the `enabled: bool` knob is declared purely so a Bool slot exists in the
// param-space. -----------------------------------------------------------------
struct GateNode {
    out: [Cell; 1],
}
impl Node for GateNode {
    fn lookbacks(&self) -> Vec<usize> {
        vec![1]
    }
    fn eval(&mut self, ctx: Ctx<'_>) -> Option<&[Cell]> {
        let w = ctx.f64_in(0);
        if w.is_empty() {
            return None;
        }
        self.out[0] = Cell::from_f64(w[0]);
        Some(&self.out)
    }
}
fn gate_builder() -> PrimitiveBuilder {
    PrimitiveBuilder::new(
        "gate",
        NodeSchema {
            inputs: vec![PortSpec { kind: ScalarKind::F64, firing: Firing::Any, name: "in".into() }],
            output: vec![FieldSpec { name: "out".into(), kind: ScalarKind::F64 }],
            params: vec![ParamSpec { name: "enabled".into(), kind: ScalarKind::Bool }],
        },
        |_params: &[Cell]| Box::new(GateNode { out: [Cell::from_f64(0.0)] }) as Box<dyn Node>,
    )
}

/// SMA-cross: param-space = [sma_cross.fast.length: I64, sma_cross.slow.length:
/// I64, exposure.scale: F64]. Used for the numeric error variants.
fn numeric_space() -> Vec<ParamSpec> {
    let cross = Composite::new(
        "sma_cross",
        vec![
            Sma::builder().named("fast").into(),
            Sma::builder().named("slow").into(),
            Sub::builder().into(),
        ],
        vec![
            Edge { from: 0, to: 2, slot: 0, from_field: 0 },
            Edge { from: 1, to: 2, slot: 1, from_field: 0 },
        ],
        vec![Role {
            name: "price".into(),
            targets: vec![Target { node: 0, slot: 0 }, Target { node: 1, slot: 0 }],
            source: None,
        }],
        vec![OutField { node: 2, field: 0, name: "out".into() }],
    );
    Composite::new(
        "harness",
        vec![BlueprintNode::Composite(cross), Exposure::builder().into()],
        vec![Edge { from: 0, to: 1, slot: 0, from_field: 0 }],
        vec![Role {
            name: "price".into(),
            targets: vec![Target { node: 0, slot: 0 }],
            source: Some(ScalarKind::F64),
        }],
        vec![OutField { node: 1, field: 0, name: "exposure".into() }],
    )
    .param_space()
}

/// A param-space with a Bool slot at index 0 (the GateNode's `enabled` knob),
/// followed by an F64 slot (exposure.scale).
fn bool_space() -> Vec<ParamSpec> {
    Composite::new(
        "harness",
        vec![gate_builder().into(), Exposure::builder().into()],
        vec![Edge { from: 0, to: 1, slot: 0, from_field: 0 }],
        vec![Role {
            name: "price".into(),
            targets: vec![Target { node: 0, slot: 0 }],
            source: Some(ScalarKind::F64),
        }],
        vec![OutField { node: 1, field: 0, name: "exposure".into() }],
    )
    .param_space()
}

fn main() {
    let space = numeric_space();
    println!("numeric param-space:");
    for (i, p) in space.iter().enumerate() {
        println!("  slot {i}: {} : {:?}", p.name, p.kind);
    }
    println!();

    // 1. Arity — two ranges for a three-slot space.
    let e = RandomSpace::new(&space, vec![ParamRange::i64(2, 8), ParamRange::i64(10, 30)], 50, 1);
    report("Arity (2 ranges, 3 slots)", &e);
    assert!(matches!(e, Err(SweepError::Arity { expected: 3, got: 2 })));

    // 2. RangeKindMismatch — an F64 range on the I64 fast.length slot (slot 0).
    let e = RandomSpace::new(
        &space,
        vec![ParamRange::f64(2.0, 8.0), ParamRange::i64(10, 30), ParamRange::f64(0.5, 4.0)],
        50,
        1,
    );
    report("RangeKindMismatch (F64 range on I64 slot 0)", &e);
    assert!(matches!(
        e,
        Err(SweepError::RangeKindMismatch { slot: 0, expected: ScalarKind::I64, got: ScalarKind::F64 })
    ));

    // 3. EmptyRange (I64) — lo > hi on slot 0.
    let e = RandomSpace::new(
        &space,
        vec![ParamRange::i64(8, 2), ParamRange::i64(10, 30), ParamRange::f64(0.5, 4.0)],
        50,
        1,
    );
    report("EmptyRange I64 (lo=8 > hi=2 on slot 0)", &e);
    assert!(matches!(e, Err(SweepError::EmptyRange { slot: 0 })));

    // 4. EmptyRange (F64) — half-open [lo, hi) with lo == hi is empty (slot 2).
    let e = RandomSpace::new(
        &space,
        vec![ParamRange::i64(2, 8), ParamRange::i64(10, 30), ParamRange::f64(1.5, 1.5)],
        50,
        1,
    );
    report("EmptyRange F64 (lo == hi == 1.5 on slot 2, half-open)", &e);
    assert!(matches!(e, Err(SweepError::EmptyRange { slot: 2 })));

    // 5. The VALID I64 lo == hi single-point range (NOT an error — inclusive).
    let ok = RandomSpace::new(
        &space,
        vec![ParamRange::i64(5, 5), ParamRange::i64(10, 30), ParamRange::f64(0.5, 4.0)],
        7,
        1,
    );
    match &ok {
        Ok(rs) => println!(
            "\nVALID  | I64 lo == hi == 5 single point accepted -> {} points",
            rs.len()
        ),
        Err(err) => panic!("I64 lo == hi must be a valid single point, got {err:?}"),
    }

    // 6. NonNumericRange — any range on the Bool slot 0 of the custom node.
    let bspace = bool_space();
    println!("\nbool param-space:");
    for (i, p) in bspace.iter().enumerate() {
        println!("  slot {i}: {} : {:?}", p.name, p.kind);
    }
    // We must still pass *some* range per slot to even reach the per-slot check;
    // an i64 range on the Bool slot. The kind-check ordering decides which error
    // wins (NonNumericRange vs RangeKindMismatch) — record whichever surfaces.
    let e = RandomSpace::new(
        &bspace,
        vec![ParamRange::i64(0, 1), ParamRange::f64(0.5, 4.0)],
        50,
        1,
    );
    report("range on Bool slot 0", &e);

    println!("\nOK: every fault was caught by RandomSpace::new before any run.");
}

fn report(label: &str, e: &Result<RandomSpace, SweepError>) {
    match e {
        Ok(rs) => println!("{label:<48} -> Ok({} points)  [UNEXPECTED]", rs.len()),
        Err(err) => println!("{label:<48} -> Err({err:?})"),
    }
}
