//! `EqConst` — a stateless `i64 -> bool` equality gate: `out = (value == target)`.
//!
//! The `i64 -> bool` analogue of a relational comparator (`Gt` is the `f64 -> bool`
//! cousin). Its purpose in the session-breakout strategy is to turn `Session`'s
//! `bars_since_open: i64` into per-bar booleans (`isBar3`, `isBar5Close`) via two
//! instances bound to `3` and `5`.
//!
//! The **operator is topology** — `EqConst` is a concrete node type, not an op
//! selected by a swept param. Only `target` is a knob (C11/C12); the `==` is fixed
//! by the type. (`!=`, `<`, `>` would each be their own node type, never a param.)
//! One `i64` input (`Firing::Any`), one `bool` output, allocation-free on the hot
//! path (the single-cell output buffer is sized once at construction, C7).

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

/// Stateless `i64 -> bool` equality gate: emits `value == target` each cycle.
/// Emits `None` until its input is present (warm-up filter, C8).
pub struct EqConst {
    target: i64,
    out: [Cell; 1],
}

impl EqConst {
    /// Build an equality gate that fires `true` exactly when its input equals
    /// `target`.
    pub fn new(target: i64) -> Self {
        Self { target, out: [Cell::from_bool(false)] }
    }

    /// The param-generic recipe for a blueprint primitive: declares `target` and
    /// builds through `EqConst::new` (the slice is kind-checked before `build`
    /// runs, so the typed read is total).
    pub fn builder() -> PrimitiveBuilder {
        PrimitiveBuilder::new(
            "EqConst",
            NodeSchema {
                inputs: vec![PortSpec { kind: ScalarKind::I64, firing: Firing::Any, name: "value".into() }],
                output: vec![FieldSpec { name: "value".into(), kind: ScalarKind::Bool }],
                params: vec![ParamSpec { name: "target".into(), kind: ScalarKind::I64 }],
            },
            |p| Box::new(EqConst::new(p[0].i64())),
        )
    }
}

impl Node for EqConst {
    fn lookbacks(&self) -> Vec<usize> {
        vec![1]
    }

    fn eval(&mut self, ctx: Ctx<'_>) -> Option<&[Cell]> {
        let w = ctx.i64_in(0);
        if w.is_empty() {
            return None; // not yet warmed up (C8 filter)
        }
        self.out[0] = Cell::from_bool(w[0] == self.target);
        Some(&self.out)
    }

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

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

    #[test]
    fn eqconst_gates_on_equality_with_target() {
        // The core property: out == (value == target). Bound to target=3, the gate
        // fires true exactly on the matching input and false on every mismatch —
        // this is what makes `bars_since_open == 3` the isBar3 boolean.
        let node_for_depth = EqConst::new(3);

        // size the input column from the node's lookback, as bootstrap will at wiring
        let mut inputs =
            vec![AnyColumn::with_capacity(ScalarKind::I64, node_for_depth.lookbacks()[0])];
        let mut node = node_for_depth;

        let feed = [2_i64, 3, 5];
        let expect = [Some(false), Some(true), Some(false)];

        for (v, want) in feed.iter().zip(expect) {
            inputs[0].push(Scalar::i64(*v)).unwrap();
            let got = node.eval(Ctx::new(&inputs, Timestamp(0)));
            match want {
                Some(b) => assert_eq!(got, Some([Cell::from_bool(b)].as_slice())),
                None => assert_eq!(got, None),
            }
        }
    }

    #[test]
    fn eqconst_is_none_until_input_present() {
        // a warm-up / empty-input cycle filters (C8) — like every other stateless node
        let mut node = EqConst::new(3);
        let inputs = vec![AnyColumn::with_capacity(ScalarKind::I64, 1)];
        assert_eq!(node.eval(Ctx::new(&inputs, Timestamp(0))), None);
    }
}
