//! Bed-exit (overnight) primitive (§3.12.1 row 8).
//!
//! Edge-triggered event: fires once when "someone sleeping" transitions
//! to "no presence in any bed-tagged zone" between 22:00 and 06:00
//! local time.
//!
//! Inputs:
//! - `sleeping` from upstream (the someone_sleeping primitive — wired
//!   into the bus output so we don't re-derive it here)
//! - `active_zones` — list of zones currently reporting presence
//! - `bed_zones` — config list of zones tagged as bed-areas
//! - `local_seconds_since_midnight` — local-time of day
//!
//! For v1 we don't have direct cross-primitive wiring, so we
//! approximate "sleeping" with: was-presence-in-bed-zone, then
//! exited-bed-zone. Refine in v2 when the bus exposes `sleeping`
//! state to other primitives.

use super::common::{in_window, PrimitiveConfig, PrimitiveState, RawSnapshot, Reason};

#[derive(Debug, Default, Clone)]
pub struct BedExit {
    in_bed: bool,
}

impl BedExit {
    pub fn new() -> Self { Self::default() }

    fn in_bed_zone(snap: &RawSnapshot) -> bool {
        !snap.bed_zones.is_empty()
            && snap.active_zones.iter().any(|z| snap.bed_zones.contains(z))
    }

    pub fn tick(&mut self, snap: &RawSnapshot, cfg: &PrimitiveConfig) -> PrimitiveState {
        if snap.since_start < cfg.warmup {
            return PrimitiveState::Idle;
        }
        let now_in_bed = snap.presence && Self::in_bed_zone(snap);
        let was_in_bed = self.in_bed;
        self.in_bed = now_in_bed;

        if was_in_bed && !now_in_bed {
            // Only fire during overnight window.
            let (start, end) = cfg.bed_exit_window;
            if in_window(snap.local_seconds_since_midnight, start, end) {
                return PrimitiveState::Event {
                    event_type: "bed_exit",
                    reason: Reason::new(&[
                        "left_bed_zone",
                        "overnight_window",
                    ]),
                };
            }
        }
        PrimitiveState::Idle
    }
}

#[cfg(test)]
mod tests {
    use super::*;
    use std::time::Duration;

    fn cfg() -> PrimitiveConfig { PrimitiveConfig::default() }

    fn in_bed_overnight(t: u64) -> RawSnapshot {
        RawSnapshot {
            since_start: Duration::from_secs(120 + t),
            presence: true,
            active_zones: vec!["bedroom".into()],
            bed_zones: vec!["bedroom".into()],
            local_seconds_since_midnight: 2 * 3600, // 02:00
            ..Default::default()
        }
    }

    fn out_of_bed_overnight(t: u64) -> RawSnapshot {
        RawSnapshot {
            since_start: Duration::from_secs(120 + t),
            presence: true,
            active_zones: vec!["hall".into()],
            bed_zones: vec!["bedroom".into()],
            local_seconds_since_midnight: 2 * 3600,
            ..Default::default()
        }
    }

    #[test]
    fn fires_on_bed_to_non_bed_overnight() {
        let mut p = BedExit::new();
        let _ = p.tick(&in_bed_overnight(10), &cfg());
        let state = p.tick(&out_of_bed_overnight(20), &cfg());
        assert!(matches!(state, PrimitiveState::Event { event_type: "bed_exit", .. }));
    }

    #[test]
    fn does_not_fire_during_day() {
        let mut p = BedExit::new();
        let mut s_in = in_bed_overnight(10);
        s_in.local_seconds_since_midnight = 14 * 3600; // 14:00
        let _ = p.tick(&s_in, &cfg());
        let mut s_out = out_of_bed_overnight(20);
        s_out.local_seconds_since_midnight = 14 * 3600;
        let state = p.tick(&s_out, &cfg());
        assert!(matches!(state, PrimitiveState::Idle));
    }

    #[test]
    fn does_not_fire_without_prior_in_bed() {
        let mut p = BedExit::new();
        // Person never was in bed.
        let state = p.tick(&out_of_bed_overnight(20), &cfg());
        assert!(matches!(state, PrimitiveState::Idle));
    }

    #[test]
    fn warmup_blocks_initial_transitions() {
        let mut p = BedExit::new();
        let mut s_in = in_bed_overnight(0);
        s_in.since_start = Duration::from_secs(30);
        assert!(matches!(p.tick(&s_in, &cfg()), PrimitiveState::Idle));
    }

    #[test]
    fn does_not_fire_when_bed_zones_unconfigured() {
        let mut p = BedExit::new();
        let mut s_in = in_bed_overnight(10);
        s_in.bed_zones.clear();
        let _ = p.tick(&s_in, &cfg());
        let mut s_out = out_of_bed_overnight(20);
        s_out.bed_zones.clear();
        let state = p.tick(&s_out, &cfg());
        assert!(matches!(state, PrimitiveState::Idle));
    }

    #[test]
    fn fires_just_after_midnight_window_start() {
        let mut p = BedExit::new();
        let mut s_in = in_bed_overnight(10);
        s_in.local_seconds_since_midnight = 22 * 3600 + 5; // 22:00:05
        let _ = p.tick(&s_in, &cfg());
        let mut s_out = out_of_bed_overnight(20);
        s_out.local_seconds_since_midnight = 22 * 3600 + 10;
        let state = p.tick(&s_out, &cfg());
        assert!(matches!(state, PrimitiveState::Event { .. }));
    }
}
