//! `LongOnly` — a long-only exposure gate. The bool param `enabled`: when true,
//! short (negative) exposure is clamped to 0 (a long-only constraint); when
//! false, exposure passes through unchanged. One f64 input, one f64 output.
//! Emits `None` until its input is present (warm-up filter, C8).

use aura_core::{
    Cell, Ctx, FieldSpec, Firing, Node, NodeSchema, ParamSpec, PortSpec, PrimitiveBuilder,
    ScalarKind,
};

/// Long-only exposure gate driven by a bool param. `enabled=true` →
/// `max(exposure, 0.0)`; `enabled=false` → `exposure` unchanged.
pub struct LongOnly {
    enabled: bool,
    out: [Cell; 1],
}

impl LongOnly {
    /// Build a long-only gate. `enabled` toggles the short-clamp.
    pub fn new(enabled: bool) -> Self {
        Self { enabled, out: [Cell::from_f64(0.0)] }
    }

    /// The param-generic recipe: declares the bool param `enabled` and builds
    /// through `LongOnly::new` (the slice is kind-checked before `build`, so the
    /// typed `p[0].bool()` read is total). First aura-std node with a bool param.
    pub fn builder() -> PrimitiveBuilder {
        PrimitiveBuilder::new(
            "LongOnly",
            NodeSchema {
                inputs: vec![PortSpec { kind: ScalarKind::F64, firing: Firing::Any, name: "exposure".into() }],
                output: vec![FieldSpec { name: "exposure".into(), kind: ScalarKind::F64 }],
                params: vec![ParamSpec { name: "enabled".into(), kind: ScalarKind::Bool }],
            },
            |p| Box::new(LongOnly::new(p[0].bool())),
        )
    }
}

impl Node for LongOnly {
    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; // not yet warmed up (C8 filter)
        }
        self.out[0] = Cell::from_f64(if self.enabled { w[0].max(0.0) } else { w[0] });
        Some(&self.out)
    }

    fn label(&self) -> String {
        format!("LongOnly({})", self.enabled)
    }
}

#[cfg(test)]
mod tests {
    use super::*;
    use aura_core::{AnyColumn, Scalar, Timestamp};

    #[test]
    fn long_only_gates_negatives_when_enabled() {
        let mut g = LongOnly::new(true);
        let mut inputs = vec![AnyColumn::with_capacity(ScalarKind::F64, 1)];
        // (input exposure, expected output) for enabled=true: negatives clamp to 0.
        let cases = [(0.6_f64, 0.6_f64), (-0.6, 0.0), (1.0, 1.0), (-1.0, 0.0)];
        for (sig, want) in cases {
            inputs[0].push(Scalar::f64(sig)).unwrap();
            assert_eq!(
                g.eval(Ctx::new(&inputs, Timestamp(0))),
                Some([Cell::from_f64(want)].as_slice())
            );
        }
    }

    #[test]
    fn long_only_passes_through_when_disabled() {
        let mut g = LongOnly::new(false);
        let mut inputs = vec![AnyColumn::with_capacity(ScalarKind::F64, 1)];
        let cases = [(0.6_f64, 0.6_f64), (-0.6, -0.6)];
        for (sig, want) in cases {
            inputs[0].push(Scalar::f64(sig)).unwrap();
            assert_eq!(
                g.eval(Ctx::new(&inputs, Timestamp(0))),
                Some([Cell::from_f64(want)].as_slice())
            );
        }
    }

    #[test]
    fn long_only_is_none_until_input_present() {
        let mut g = LongOnly::new(true);
        let inputs = vec![AnyColumn::with_capacity(ScalarKind::F64, 1)];
        assert_eq!(g.eval(Ctx::new(&inputs, Timestamp(0))), None);
    }

    #[test]
    fn long_only_param_is_a_bool_named_enabled() {
        let builder = LongOnly::builder();
        let schema = builder.schema();
        assert_eq!(schema.params.len(), 1);
        assert_eq!(schema.params[0].name, "enabled");
        assert_eq!(schema.params[0].kind, ScalarKind::Bool);
    }
}
