//! `CarryCost` — a flat per-held-cycle carry/financing cost, in R. The simplest
//! ACCRUAL cost node (C10): a cost incurred for every cycle a position is held,
//! accruing over the hold rather than charged once at close. A labelled stress
//! parameter (a constant price-unit carry), the flat base case of the accrual
//! family. A `ConstantCost` twin differing only in `charge_mode() -> PerHeldCycle`;
//! the shared `CostRunner` accrues, dumps at close, and marks the open position so
//! the net_r_equity curve bleeds over the hold.

use aura_core::{Ctx, ParamSpec, PrimitiveBuilder, ScalarKind};

use crate::cost::{cost_node_builder, ChargeMode, CostNode, CostRunner};

/// A flat per-held-cycle carry in price units (`carry_per_cycle`), emitted in R via the
/// shared [`CostRunner`] in its per-held-cycle (accrual) charge mode.
pub struct CarryCost {
    carry_per_cycle: f64,
}

impl CarryCost {
    /// A flat per-held-cycle carry node: the factor wrapped in the shared [`CostRunner`].
    pub fn new(carry_per_cycle: f64) -> CostRunner<CarryCost> {
        assert!(carry_per_cycle >= 0.0, "CarryCost carry_per_cycle must be >= 0");
        CostRunner::new(CarryCost { carry_per_cycle })
    }

    /// The param-generic recipe: one `carry_per_cycle` F64 knob, no extra inputs.
    pub fn builder() -> PrimitiveBuilder {
        cost_node_builder(
            "CarryCost",
            Vec::new(),
            vec![ParamSpec { name: "carry_per_cycle".into(), kind: ScalarKind::F64 }],
            |p| Box::new(CarryCost::new(p[0].f64())),
        )
    }
}

impl CostNode for CarryCost {
    fn label(&self) -> String {
        format!("CarryCost({})", self.carry_per_cycle)
    }
    fn charge_mode(&self) -> ChargeMode {
        ChargeMode::PerHeldCycle
    }
    fn cost_numerator(&mut self, _ctx: &Ctx<'_>) -> f64 {
        self.carry_per_cycle
    }
}

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

    fn cols() -> Vec<AnyColumn> {
        vec![
            AnyColumn::with_capacity(ScalarKind::Bool, 1), // closed
            AnyColumn::with_capacity(ScalarKind::Bool, 1), // open
            AnyColumn::with_capacity(ScalarKind::F64, 1),  // entry
            AnyColumn::with_capacity(ScalarKind::F64, 1),  // stop
        ]
    }

    #[test]
    fn charge_mode_is_per_held_cycle() {
        assert_eq!(CarryCost { carry_per_cycle: 1.0 }.charge_mode(), ChargeMode::PerHeldCycle);
    }

    #[test]
    fn accrues_each_held_cycle_then_dumps_at_close() {
        // per = 1/4 = 0.25. open, open, close -> open_cost 0.25, 0.5, then dump 0.75.
        let mut c = CarryCost::new(1.0);
        let mut a = cols();
        a[0].push(Scalar::bool(false)).unwrap();
        a[1].push(Scalar::bool(true)).unwrap();
        a[2].push(Scalar::f64(100.0)).unwrap();
        a[3].push(Scalar::f64(96.0)).unwrap(); // latched 4
        assert_eq!(
            c.eval(Ctx::new(&a, Timestamp(0))),
            Some([Cell::from_f64(0.0), Cell::from_f64(0.0), Cell::from_f64(0.25)].as_slice())
        );
        let mut b = cols();
        b[0].push(Scalar::bool(false)).unwrap();
        b[1].push(Scalar::bool(true)).unwrap();
        b[2].push(Scalar::f64(100.0)).unwrap();
        b[3].push(Scalar::f64(96.0)).unwrap();
        assert_eq!(
            c.eval(Ctx::new(&b, Timestamp(1))),
            Some([Cell::from_f64(0.0), Cell::from_f64(0.0), Cell::from_f64(0.5)].as_slice())
        );
        let mut d = cols();
        d[0].push(Scalar::bool(true)).unwrap();
        d[1].push(Scalar::bool(false)).unwrap();
        d[2].push(Scalar::f64(100.0)).unwrap();
        d[3].push(Scalar::f64(96.0)).unwrap();
        assert_eq!(
            c.eval(Ctx::new(&d, Timestamp(2))),
            Some([Cell::from_f64(0.75), Cell::from_f64(0.75), Cell::from_f64(0.0)].as_slice())
        );
    }

    #[test]
    fn label_carries_the_carry() {
        assert_eq!(CarryCost::new(0.5).label(), "CarryCost(0.5)");
    }

    #[test]
    fn builder_exposes_one_param_no_extra() {
        let builder = CarryCost::builder();
        let schema = builder.schema();
        assert_eq!(schema.params.len(), 1);
        assert_eq!(schema.params[0].name, "carry_per_cycle");
        // geometry-only inputs (4), no extras.
        assert_eq!(schema.inputs.len(), crate::cost::GEOMETRY_WIDTH);
    }

    #[test]
    #[should_panic(expected = "carry_per_cycle must be >= 0")]
    fn new_panics_on_negative_carry() {
        let _ = CarryCost::new(-1.0);
    }
}
