//! `And` — a stateless `bool × bool -> bool` conjunction: `out = (a && b)`.
//!
//! The **bool-input twin** of `Gt` (which *emits* a bool from two `f64` legs);
//! `And` *consumes* two bools and ANDs them. Its purpose in the session-breakout
//! strategy is `entry = breakout && isBar3`: a fresh-15m-close breakout that
//! lands exactly on the third bar of the session.
//!
//! The **seed of the logic family** — `Or` and `Not` would each be their own
//! node type, never a swept op param (the operator is topology, like the
//! relational comparators). Two `bool` inputs (`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, PortSpec, PrimitiveBuilder, ScalarKind};

/// Stateless `bool × bool -> bool` conjunction: emits `a && b` each cycle.
/// Emits `None` until **both** inputs have a value (warm-up gate, C8).
pub struct And {
    out: [Cell; 1],
}

impl And {
    /// Build an `And` node.
    pub fn new() -> Self {
        Self { out: [Cell::from_bool(false)] }
    }

    /// The param-generic recipe for a blueprint primitive: paramless, builds
    /// through `And::new`.
    pub fn builder() -> PrimitiveBuilder {
        PrimitiveBuilder::new(
            "And",
            NodeSchema {
                inputs: vec![
                    PortSpec { kind: ScalarKind::Bool, firing: Firing::Any, name: "a".into() },
                    PortSpec { kind: ScalarKind::Bool, firing: Firing::Any, name: "b".into() },
                ],
                output: vec![FieldSpec { name: "value".into(), kind: ScalarKind::Bool }],
                params: vec![],
            },
            |_| Box::new(And::new()),
        )
    }
}

impl Default for And {
    fn default() -> Self {
        Self::new()
    }
}

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

    fn eval(&mut self, ctx: Ctx<'_>) -> Option<&[Cell]> {
        let a = ctx.bool_in(0);
        let b = ctx.bool_in(1);
        if a.is_empty() || b.is_empty() {
            return None; // not yet warmed up — both legs required (C8 filter)
        }
        self.out[0] = Cell::from_bool(a[0] && b[0]);
        Some(&self.out)
    }

    fn label(&self) -> String {
        "And".to_string()
    }
}

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

    #[test]
    fn and_is_conjunction_once_both_inputs_present() {
        // The core property: out == (a && b). The full truth table is pinned —
        // true exactly when BOTH legs are true, false on every other row. This is
        // what makes `entry = breakout && isBar3` fire only on a breakout that
        // lands on the third bar.
        let mut node = And::new();
        let mut inputs = vec![
            AnyColumn::with_capacity(ScalarKind::Bool, 1),
            AnyColumn::with_capacity(ScalarKind::Bool, 1),
        ];

        let a_feed = [false, true, false, true];
        let b_feed = [false, false, true, true];
        //            F,F    T,F    F,T    T,T
        let expect = [false, false, false, true];

        for ((av, bv), want) in a_feed.iter().zip(b_feed.iter()).zip(expect) {
            inputs[0].push(Scalar::bool(*av)).unwrap();
            inputs[1].push(Scalar::bool(*bv)).unwrap();
            let got = node.eval(Ctx::new(&inputs, Timestamp(0)));
            assert_eq!(got, Some([Cell::from_bool(want)].as_slice()));
        }
    }

    #[test]
    fn and_is_none_until_both_inputs_present() {
        // Both-inputs warm-up gate (C8), like `Gt`: only one leg present -> None.
        let mut node = And::new();
        let mut inputs = vec![
            AnyColumn::with_capacity(ScalarKind::Bool, 1),
            AnyColumn::with_capacity(ScalarKind::Bool, 1),
        ];

        // only input 0 present -> None
        inputs[0].push(Scalar::bool(true)).unwrap();
        assert_eq!(node.eval(Ctx::new(&inputs, Timestamp(0))), None);

        // both present -> the conjunction (true && true == true)
        inputs[1].push(Scalar::bool(true)).unwrap();
        assert_eq!(node.eval(Ctx::new(&inputs, Timestamp(0))), Some([Cell::from_bool(true)].as_slice()));
    }

    #[test]
    fn input_slots_are_named_a_b() {
        let a = And::builder();
        let names: Vec<&str> = a.schema().inputs.iter().map(|p| p.name.as_str()).collect();
        assert_eq!(names, ["a", "b"]);
    }
}
