use std::collections::{BinaryHeap, HashMap, HashSet};
use std::str::FromStr;

use chrono::{DateTime, Datelike, NaiveDate, NaiveTime, TimeZone, Utc, Weekday};
use chrono_tz::Tz;
use uuid::Uuid;

use crate::{ONE_DAY, assign_time, change_time};

///////////////////////////////////////////// RhythmID /////////////////////////////////////////////

one_two_eight::generate_id!(RhythmID, "rhythm:");
crate::generate_id_serde!(RhythmID, RhythmIDVisitor);

impl TryFrom<RhythmID> for Uuid {
    type Error = crate::Error;

    fn try_from(rhythm_id: RhythmID) -> Result<Self, Self::Error> {
        let id_str = rhythm_id.to_string();
        let uuid_str = id_str
            .strip_prefix("rhythm:")
            .ok_or_else(|| crate::Error::Internal("Invalid RhythmID format".to_string()))?;
        Uuid::parse_str(uuid_str).map_err(|e| crate::Error::Internal(e.to_string()))
    }
}

impl TryFrom<Uuid> for RhythmID {
    type Error = crate::Error;

    fn try_from(uuid: Uuid) -> Result<Self, Self::Error> {
        let id_str = format!("rhythm:{}", uuid);
        RhythmID::from_human_readable(&id_str)
            .ok_or_else(|| crate::Error::Internal("Failed to parse RhythmID".to_string()))
    }
}

impl TryFrom<&Uuid> for RhythmID {
    type Error = crate::Error;

    fn try_from(uuid: &Uuid) -> Result<Self, Self::Error> {
        Self::try_from(*uuid)
    }
}

////////////////////////////////////////////// Slider //////////////////////////////////////////////

#[derive(Clone, Copy, Debug, Default, Eq, PartialEq, serde::Deserialize, serde::Serialize)]
pub struct Slider {
    pub before: u32,
}

impl Slider {
    pub fn new(before: u32) -> Self {
        Self { before }
    }
}

////////////////////////////////////////////// Rhythm //////////////////////////////////////////////

#[derive(Clone, Copy, Debug, Eq, PartialEq, serde::Deserialize, serde::Serialize)]
#[serde(tag = "type", rename_all = "snake_case")]
pub enum Rhythm {
    Daily {
        at: NaiveTime,
    },
    Monthly {
        dotm: u32,
        at: NaiveTime,
        #[serde(default)]
        slider: Slider,
    },
    WeekDaily {
        dotw: u8,
        at: NaiveTime,
        #[serde(default)]
        slider: Slider,
    },
    EveryNDays {
        n: u32,
        at: NaiveTime,
        #[serde(default)]
        slider: Slider,
    },
}

impl Rhythm {
    pub fn at(self) -> NaiveTime {
        match self {
            Self::Daily { at } => at,
            Self::Monthly { at, .. } => at,
            Self::WeekDaily { at, .. } => at,
            Self::EveryNDays { at, .. } => at,
        }
    }

    pub fn with_slider(mut self, slider: Slider) -> Self {
        let s = slider;
        match self {
            Self::Daily { .. } => {}
            Self::Monthly { ref mut slider, .. } => {
                *slider = s;
            }
            Self::WeekDaily { ref mut slider, .. } => {
                *slider = s;
            }
            Self::EveryNDays { ref mut slider, .. } => {
                *slider = s;
            }
        }
        self
    }

    pub fn slide_one(mut self) -> Self {
        match self {
            Self::Daily { .. } => self,
            Self::Monthly { ref mut slider, .. } => {
                slider.before = slider.before.saturating_sub(1);
                self
            }
            Self::WeekDaily { ref mut slider, .. } => {
                slider.before = slider.before.saturating_sub(1);
                self
            }
            Self::EveryNDays { ref mut slider, .. } => {
                slider.before = slider.before.saturating_sub(1);
                self
            }
        }
    }

    pub fn slider(self) -> Slider {
        match self {
            Self::Daily { .. } => Slider::default(),
            Self::Monthly { slider, .. } => slider,
            Self::WeekDaily { slider, .. } => slider,
            Self::EveryNDays { slider, .. } => slider,
        }
    }

    pub fn skip_beat_within_slider(self) -> bool {
        match self {
            Self::Daily { .. } => false,
            Self::Monthly { .. } => true,
            Self::WeekDaily { .. } => true,
            Self::EveryNDays { .. } => false,
        }
    }

    pub fn approximate_periodicity(self) -> u32 {
        match self {
            Self::Daily { .. } => 1,
            Self::Monthly { .. } => 31,
            Self::WeekDaily { .. } => 7,
            Self::EveryNDays { n, .. } => n,
        }
    }

    pub fn type_name(&self) -> &'static str {
        match self {
            Self::Daily { .. } => "daily",
            Self::Monthly { .. } => "monthly",
            Self::WeekDaily { .. } => "weekdaily",
            Self::EveryNDays { .. } => "every_n_days",
        }
    }

    pub fn next_time_to_fire<TZ: TimeZone + Clone + Copy>(
        self,
        now: DateTime<TZ>,
        last_fired: Option<DateTime<TZ>>,
    ) -> DateTime<TZ> {
        if let Some(last_fired) = last_fired {
            match self {
                Rhythm::Daily { at } => change_time(last_fired, at) + ONE_DAY,
                Rhythm::Monthly { dotm, at, .. } => {
                    let mut next_fire = last_fired;
                    if next_fire.day0() == dotm {
                        next_fire += ONE_DAY;
                    }
                    while next_fire.day0() != dotm {
                        next_fire += ONE_DAY;
                    }
                    change_time(next_fire, at)
                }
                Rhythm::WeekDaily { dotw, at, .. } => {
                    let mut next_fire = last_fired;
                    let dotw = Weekday::try_from(dotw).unwrap_or(Weekday::Mon);
                    if next_fire.weekday() == dotw {
                        next_fire += ONE_DAY;
                    }
                    while next_fire.weekday() != dotw {
                        next_fire += ONE_DAY;
                    }
                    change_time(next_fire, at)
                }
                Rhythm::EveryNDays { n, at, .. } => change_time(last_fired, at) + n * ONE_DAY,
            }
        } else {
            match self {
                Rhythm::Daily { at } => change_time(now, at),
                Rhythm::Monthly { dotm, at, .. } => {
                    let mut next_fire = now.clone();
                    // NOTE(rescrv): Intentionally bail if on this day of the month.
                    while next_fire.day0() != dotm {
                        next_fire += ONE_DAY;
                    }
                    change_time(next_fire, at)
                }
                Rhythm::WeekDaily { dotw, at, .. } => {
                    let mut next_fire = now;
                    let dotw =
                        Weekday::try_from(dotw).expect("no one expects the end of spacetime");
                    // NOTE(rescrv): Intentionally bail if on this day of the month.
                    while next_fire.weekday() != dotw {
                        next_fire += ONE_DAY;
                    }
                    change_time(next_fire, at)
                }
                Rhythm::EveryNDays { n, at, .. } => {
                    if n >= 7 {
                        return now;
                    }
                    let mut date = now.clone() - 7 * ONE_DAY;
                    while date.weekday().num_days_from_monday() != n {
                        date += ONE_DAY;
                    }
                    while date < now.clone() {
                        date = self.next_time_to_fire(now.clone(), Some(date));
                    }
                    change_time(date, at)
                }
            }
        }
    }
}

impl std::fmt::Display for Rhythm {
    fn fmt(&self, f: &mut std::fmt::Formatter) -> std::fmt::Result {
        match self {
            Self::Daily { at } => write!(f, "daily at {at}"),
            Self::Monthly { dotm, at, slider } => {
                write!(f, "monthly on the {dotm} at {at} slider:{}", slider.before)
            }
            Self::WeekDaily { dotw, at, slider } => {
                let dotw = Weekday::try_from(*dotw).unwrap_or(Weekday::Mon);
                write!(f, "weekly on {dotw} at {at} slider:{}", slider.before)
            }
            Self::EveryNDays { n, at, slider } => {
                write!(f, "every {n} days at {at} slider:{}", slider.before)
            }
        }
    }
}

///////////////////////////////////////// RhythmDefinition /////////////////////////////////////////

#[derive(Clone, Debug, Eq, PartialEq, serde::Deserialize, serde::Serialize)]
pub struct RhythmDefinition {
    pub id: RhythmID,
    pub rhythm: Rhythm,
    pub description: String,
    pub created_at: DateTime<Utc>,
    pub created_at_tz: String,
    pub modified_at: DateTime<Utc>,
    pub modified_at_tz: String,
}

impl RhythmDefinition {
    pub fn slide_one(mut self) -> Self {
        self.rhythm = self.rhythm.slide_one();
        self
    }

    pub fn with_slider(mut self, slider: Slider) -> Self {
        self.rhythm = self.rhythm.with_slider(slider);
        self
    }
}

///////////////////////////////////////////// EventType ////////////////////////////////////////////

#[derive(Clone, Debug, PartialEq, Eq, serde::Deserialize, serde::Serialize)]
#[serde(rename_all = "snake_case")]
pub enum EventType {
    Done,
    Defer,
}

impl EventType {
    pub fn as_str(&self) -> &'static str {
        match self {
            EventType::Done => "done",
            EventType::Defer => "defer",
        }
    }
}

impl FromStr for EventType {
    type Err = String;

    fn from_str(s: &str) -> Result<Self, Self::Err> {
        match s {
            "done" => Ok(EventType::Done),
            "defer" => Ok(EventType::Defer),
            _ => Err(format!("Invalid event type: {s}")),
        }
    }
}

//////////////////////////////////////////// EventRecord ///////////////////////////////////////////

#[derive(Clone, Debug, Eq, PartialEq, serde::Deserialize, serde::Serialize)]
pub struct EventRecord {
    pub rhythm_id: RhythmID,
    pub event_type: EventType,
    pub when: DateTime<Utc>,
    pub when_tz: String,
}

impl EventRecord {
    pub fn local_time(&self) -> Result<DateTime<Tz>, chrono_tz::ParseError> {
        let tz = Tz::from_str(&self.when_tz)?;
        Ok(self.when.with_timezone(&tz))
    }
}

/////////////////////////////////////////// RhythmManager //////////////////////////////////////////

pub struct RhythmManager {
    user_id: Uuid,
    rhythms: Vec<RhythmDefinition>,
    tz: Tz,
    timezone: String,
    events: Vec<EventRecord>,
    spoons: HashMap<NaiveDate, u8>,
}

impl RhythmManager {
    pub fn new(
        user_id: Uuid,
        timezone: impl AsRef<str>,
        rhythms: Vec<RhythmDefinition>,
        events: Vec<EventRecord>,
        spoons: HashMap<NaiveDate, u8>,
    ) -> Result<Self, crate::Error> {
        let tz =
            Tz::from_str(timezone.as_ref()).map_err(|e| crate::Error::Internal(e.to_string()))?;
        Ok(Self {
            user_id,
            rhythms,
            tz,
            timezone: timezone.as_ref().to_string(),
            events,
            spoons,
        })
    }

    pub fn now(&self) -> DateTime<Tz> {
        Utc::now().with_timezone(&self.tz)
    }

    pub fn set_rhythm(&mut self, mut rhythm: RhythmDefinition) -> Result<(), crate::Error> {
        rhythm.modified_at = Utc::now();
        rhythm.modified_at_tz = self.timezone.clone();
        for r in self.rhythms.iter_mut() {
            if r.id == rhythm.id {
                *r = rhythm;
                return Ok(());
            }
        }
        rhythm.created_at = Utc::now();
        rhythm.created_at_tz = self.timezone.clone();
        self.rhythms.push(rhythm);
        Ok(())
    }

    pub fn unset_rhythm(&mut self, rhythm_id: RhythmID) -> Result<(), crate::Error> {
        self.rhythms.retain(|x| x.id != rhythm_id);
        Ok(())
    }

    pub fn rhythms(&self) -> Vec<RhythmDefinition> {
        self.rhythms.clone()
    }

    pub fn mark_done_at(
        &mut self,
        rhythm_id: RhythmID,
        when: DateTime<Utc>,
    ) -> Result<(), crate::Error> {
        self.events
            .retain(|event| !(event.rhythm_id == rhythm_id && event.event_type == EventType::Done));
        let event = EventRecord {
            rhythm_id,
            event_type: EventType::Done,
            when,
            when_tz: self.timezone.clone(),
        };
        self.events.push(event);
        Ok(())
    }

    pub fn mark_done(&mut self, rhythm_id: RhythmID) -> Result<(), crate::Error> {
        self.mark_done_at(rhythm_id, Utc::now())
    }

    pub fn defer_rhythm_at(
        &mut self,
        rhythm_id: RhythmID,
        when: DateTime<Utc>,
    ) -> Result<(), crate::Error> {
        self.events.retain(|event| {
            !(event.rhythm_id == rhythm_id && event.event_type == EventType::Defer)
        });
        let event = EventRecord {
            rhythm_id,
            event_type: EventType::Defer,
            when,
            when_tz: self.timezone.clone(),
        };
        self.events.push(event);
        Ok(())
    }

    pub fn defer_rhythm(&mut self, rhythm_id: RhythmID) -> Result<(), crate::Error> {
        self.defer_rhythm_at(rhythm_id, Utc::now())
    }

    pub fn last_done(&self, rhythm_id: RhythmID) -> Option<DateTime<Utc>> {
        self.last_done_time(rhythm_id)
    }

    fn last_done_time(&self, rhythm_id: RhythmID) -> Option<DateTime<Utc>> {
        self.events
            .iter()
            .filter(|e| e.rhythm_id == rhythm_id && e.event_type == EventType::Done)
            .map(|e| e.when)
            .max()
    }

    fn continuing_beat<TZ: TimeZone + Clone + Copy>(
        &self,
        rhythm: &RhythmDefinition,
        start: DateTime<TZ>,
        last_seen: Option<DateTime<TZ>>,
    ) -> DateTime<TZ> {
        if let Some(last_seen) = last_seen {
            let mut beat = rhythm
                .rhythm
                .next_time_to_fire(last_seen.clone(), Some(last_seen.clone()));
            if rhythm.rhythm.skip_beat_within_slider() {
                let days_diff = (beat.date_naive() - last_seen.date_naive()).num_days();
                if days_diff <= rhythm.rhythm.slider().before as i64 {
                    beat = rhythm.rhythm.next_time_to_fire(beat.clone(), Some(beat));
                }
            }
            while beat < start {
                beat = rhythm.rhythm.next_time_to_fire(beat.clone(), Some(beat));
            }
            beat
        } else {
            rhythm.rhythm.next_time_to_fire(start, None)
        }
    }

    pub fn schedule(
        &self,
        start: NaiveDate,
        limit: NaiveDate,
    ) -> Vec<(DateTime<Tz>, RhythmDefinition)> {
        self.schedule2(start, limit, true)
    }

    pub fn schedule2(
        &self,
        start: NaiveDate,
        limit: NaiveDate,
        ignore_today: bool,
    ) -> Vec<(DateTime<Tz>, RhythmDefinition)> {
        self.schedule_internal(start, limit, ignore_today, &mut HashMap::default())
    }

    fn schedule_internal(
        &self,
        start: NaiveDate,
        limit: NaiveDate,
        ignore_today: bool,
        watermarks: &mut HashMap<NaiveDate, usize>,
    ) -> Vec<(DateTime<Tz>, RhythmDefinition)> {
        let mut events_by_rhythm: HashMap<RhythmID, Vec<&EventRecord>> = HashMap::new();
        for event in &self.events {
            events_by_rhythm
                .entry(event.rhythm_id)
                .or_default()
                .push(event);
        }

        let today = self.now().date_naive();
        let today_done_deferred_count = if ignore_today {
            self.events
                .iter()
                .filter(|e| {
                    e.local_time()
                        .map(|dt| dt.date_naive() == today)
                        .unwrap_or(false)
                        && (e.event_type == EventType::Done || e.event_type == EventType::Defer)
                })
                .count()
        } else {
            0
        };

        let mut schedule: HashMap<NaiveDate, Vec<(DateTime<Tz>, RhythmDefinition)>> =
            HashMap::new();

        for rhythm in &self.rhythms {
            if let Rhythm::Daily { at } = &rhythm.rhythm {
                let mut day = start;
                while day < limit {
                    let has_done_or_defer = events_by_rhythm
                        .get(&rhythm.id)
                        .map(|events| {
                            events.iter().any(|e| {
                                e.local_time()
                                    .map(|dt| dt.date_naive() == day)
                                    .unwrap_or(false)
                                    && (e.event_type == EventType::Done
                                        || e.event_type == EventType::Defer)
                            })
                        })
                        .unwrap_or(false);

                    if !has_done_or_defer {
                        let when = assign_time(day, *at, self.tz);
                        schedule
                            .entry(day)
                            .or_default()
                            .push((when, rhythm.clone()));
                    }
                    day += chrono::Duration::days(1);
                }
            }
        }

        struct HeapItem {
            rank: u64,
            first_beat: DateTime<Tz>,
            rhythm: RhythmDefinition,
            original_slider: Slider,
            original_beat: DateTime<Tz>,
        }

        impl Eq for HeapItem {}

        impl PartialEq for HeapItem {
            fn eq(&self, other: &Self) -> bool {
                self.cmp(other).is_eq()
            }
        }

        impl Ord for HeapItem {
            fn cmp(&self, other: &Self) -> std::cmp::Ordering {
                self.rank
                    .cmp(&other.rank)
                    .then(self.first_beat.cmp(&other.first_beat))
            }
        }

        impl PartialOrd for HeapItem {
            fn partial_cmp(&self, other: &Self) -> Option<std::cmp::Ordering> {
                Some(self.cmp(other))
            }
        }

        let mut rank = u64::MAX;
        let mut heap = BinaryHeap::new();
        for rhythm in &self.rhythms {
            if matches!(rhythm.rhythm, Rhythm::Daily { .. }) {
                continue;
            }

            let rhythm = rhythm.clone();
            let done_events: Vec<DateTime<Utc>> = events_by_rhythm
                .get(&rhythm.id)
                .map(|events| {
                    events
                        .iter()
                        .filter(|e| e.event_type == EventType::Done)
                        .map(|e| e.when)
                        .collect()
                })
                .unwrap_or_default();

            let defer_dates: HashSet<NaiveDate> = events_by_rhythm
                .get(&rhythm.id)
                .map(|events| {
                    events
                        .iter()
                        .filter(|e| e.event_type == EventType::Defer)
                        .filter_map(|e| e.local_time().ok().map(|dt| dt.date_naive()))
                        .collect()
                })
                .unwrap_or_default();

            let last_done = done_events
                .iter()
                .max()
                .cloned()
                .map(|dt| dt.with_timezone(&self.tz));
            let start_dt = start
                .and_time(NaiveTime::from_hms_opt(0, 0, 0).unwrap())
                .and_local_timezone(self.tz)
                .earliest()
                .unwrap();

            let mut first_beat = self.continuing_beat(&rhythm, start_dt, last_done);
            while defer_dates.contains(&first_beat.date_naive()) {
                first_beat = self.continuing_beat(&rhythm, first_beat, Some(first_beat));
            }

            let original_slider = rhythm.rhythm.slider();
            let original_beat = first_beat;
            heap.push(HeapItem {
                rank,
                first_beat,
                rhythm,
                original_slider,
                original_beat,
            });
            rank -= original_slider.before as u64;
        }

        while let Some(entry) = heap.pop() {
            if entry.first_beat.date_naive() < start {
                if entry.rhythm.rhythm.slider().before > 0 {
                    heap.push(HeapItem {
                        rank: entry.rank,
                        first_beat: entry.first_beat - ONE_DAY,
                        rhythm: entry.rhythm.slide_one(),
                        original_slider: entry.original_slider,
                        original_beat: entry.original_beat,
                    });
                }
                continue;
            }
            if (entry.first_beat - ONE_DAY * entry.rhythm.rhythm.slider().before).date_naive()
                >= limit
            {
                continue;
            }
            let slots_per_day = watermarks
                .get(&entry.first_beat.date_naive())
                .copied()
                .unwrap_or(1);
            let spoons_today = self
                .spoons
                .get(&entry.first_beat.date_naive())
                .copied()
                .unwrap_or(5);
            let spoons_adjusted = (spoons_today as i32 - 5).clamp(-5, 5);
            let slots_per_day =
                ((slots_per_day as f64) * 2_f64.powf(spoons_adjusted as f64 / 5.0)).ceil() as usize;
            let slots_per_day = if entry.first_beat.date_naive() == today && !ignore_today {
                slots_per_day.saturating_sub(today_done_deferred_count)
            } else {
                slots_per_day
            };
            let slots_per_day = slots_per_day.max(1);
            let slots = schedule.entry(entry.first_beat.date_naive()).or_default();
            let defer_today = self.events.iter().any(|e| {
                e.rhythm_id == entry.rhythm.id
                    && e.event_type == EventType::Defer
                    && e.local_time()
                        .map(|dt| dt.date_naive() == entry.first_beat.date_naive())
                        .unwrap_or(false)
            });
            if !defer_today && slots.len() < slots_per_day {
                slots.push((entry.first_beat, entry.rhythm.clone()));
                let next_beat =
                    self.continuing_beat(&entry.rhythm, entry.first_beat, Some(entry.first_beat));
                heap.push(HeapItem {
                    rank: entry.rank,
                    first_beat: next_beat,
                    rhythm: entry.rhythm.with_slider(entry.original_slider),
                    original_slider: entry.original_slider,
                    original_beat: next_beat,
                });
            } else {
                let low_water_mark = watermarks
                    .get(&entry.first_beat.date_naive())
                    .cloned()
                    .unwrap_or(0)
                    + 1;
                for idx in 0..=entry.original_slider.before {
                    let date = (entry.original_beat - ONE_DAY * idx).date_naive();
                    let current = watermarks.entry(date).or_default();
                    *current = (*current).max(low_water_mark);
                }
                return self.schedule_internal(start, limit, ignore_today, watermarks);
            }
        }
        let mut result = schedule.values().flatten().cloned().collect::<Vec<_>>();
        result.sort_by_key(|x| x.0);
        result
    }

    pub fn active(&self) -> Vec<(DateTime<Tz>, RhythmDefinition)> {
        let now = Utc::now();
        let mut schedule = self.schedule(
            now.date_naive(),
            (now + chrono::Duration::days(90)).date_naive(),
        );
        schedule.retain(|x| x.0 < now);
        schedule
    }

    pub fn convergence(&self) -> DateTime<Utc> {
        // Find the maximum time any rhythm will fire in the future
        let now = Utc::now();
        let mut max_fire_time = now;

        for rhythm in &self.rhythms {
            // Get the last done event for this rhythm
            let last_done = self
                .events
                .iter()
                .filter(|e| e.rhythm_id == rhythm.id && e.event_type == EventType::Done)
                .map(|e| e.when)
                .max();

            // Calculate next fire time
            let next_fire = rhythm.rhythm.next_time_to_fire(now, last_done);
            if next_fire > max_fire_time {
                max_fire_time = next_fire;
            }
        }

        max_fire_time
    }

    pub fn delinquent(&self) -> Vec<RhythmDefinition> {
        let now = Utc::now();
        let mut delinquent_rhythms = Vec::new();

        for rhythm in &self.rhythms {
            // Get the last done event for this rhythm
            let last_done = self
                .events
                .iter()
                .filter(|e| e.rhythm_id == rhythm.id && e.event_type == EventType::Done)
                .map(|e| e.when)
                .max();

            // Check if the rhythm should have fired by now
            let next_fire = rhythm.rhythm.next_time_to_fire(
                last_done.unwrap_or(now - chrono::Duration::days(365)),
                last_done,
            );

            // If it should have fired before now, it's delinquent
            if next_fire < now {
                delinquent_rhythms.push(rhythm.clone());
            }
        }

        delinquent_rhythms
    }

    pub fn spoons(&mut self, day: impl Datelike, spoons: u8) {
        let date = NaiveDate::from_ymd_opt(day.year(), day.month(), day.day()).unwrap();
        self.spoons.insert(date, spoons.min(10));
    }

    pub fn show_spoons(&self) -> HashMap<NaiveDate, u8> {
        self.spoons.clone()
    }

    pub fn user_id(&self) -> Uuid {
        self.user_id
    }

    pub fn events(&self) -> &[EventRecord] {
        &self.events
    }

    pub fn timezone(&self) -> &str {
        &self.timezone
    }
}

/////////////////////////////////////////////// tests //////////////////////////////////////////////

#[cfg(test)]
mod tests {
    use chrono::{NaiveTime, TimeZone, Utc};
    use chrono_tz::America::Los_Angeles;

    use super::*;

    const TEST_AT: NaiveTime = NaiveTime::from_hms_opt(13, 37, 42).unwrap();

    fn create_test_manager(timezone: &str) -> RhythmManager {
        RhythmManager::new(
            Uuid::new_v4(),
            timezone,
            Vec::new(),
            Vec::new(),
            HashMap::new(),
        )
        .unwrap()
    }

    #[test]
    fn daily_start_beat() {
        let daily = Rhythm::Daily { at: TEST_AT };
        let now = Utc.with_ymd_and_hms(2023, 8, 19, 0, 0, 0).unwrap();
        let result = daily.next_time_to_fire(now, None);
        assert_eq!(
            result,
            Utc.with_ymd_and_hms(2023, 8, 19, 13, 37, 42).unwrap()
        );
    }

    #[test]
    fn daily_next_beat() {
        let daily = Rhythm::Daily { at: TEST_AT };
        let now = Utc.with_ymd_and_hms(2022, 11, 20, 0, 0, 0).unwrap();
        let last_fired = Utc.with_ymd_and_hms(2022, 11, 20, 0, 0, 0).unwrap();
        let result = daily.next_time_to_fire(now, Some(last_fired));
        assert_eq!(
            result,
            Utc.with_ymd_and_hms(2022, 11, 21, 13, 37, 42).unwrap()
        );
    }

    #[test]
    fn monthly_start_beat_same_day() {
        let monthly = Rhythm::Monthly {
            dotm: 17,
            at: TEST_AT,
            slider: Slider::default(),
        }; // 18th day (0-based)
        let now = Utc.with_ymd_and_hms(2022, 11, 18, 0, 0, 0).unwrap();
        let result = monthly.next_time_to_fire(now, None);
        assert_eq!(
            result,
            Utc.with_ymd_and_hms(2022, 11, 18, 13, 37, 42).unwrap()
        );
    }

    #[test]
    fn monthly_start_beat_different_day() {
        let monthly = Rhythm::Monthly {
            dotm: 17,
            at: TEST_AT,
            slider: Slider::default(),
        }; // 18th day (0-based)
        let now = Utc.with_ymd_and_hms(2022, 11, 20, 0, 0, 0).unwrap();
        let result = monthly.next_time_to_fire(now, None);
        assert_eq!(
            result,
            Utc.with_ymd_and_hms(2022, 12, 18, 13, 37, 42).unwrap()
        );
    }

    #[test]
    fn monthly_next_beat_same_day() {
        let monthly = Rhythm::Monthly {
            dotm: 17,
            at: TEST_AT,
            slider: Slider::default(),
        }; // 18th day (0-based)
        let now = Utc.with_ymd_and_hms(2022, 11, 18, 0, 0, 0).unwrap();
        let last_fired = Utc.with_ymd_and_hms(2022, 11, 18, 0, 0, 0).unwrap();
        let result = monthly.next_time_to_fire(now, Some(last_fired));
        assert_eq!(
            result,
            Utc.with_ymd_and_hms(2022, 12, 18, 13, 37, 42).unwrap()
        );
    }

    #[test]
    fn monthly_next_beat_different_day() {
        let monthly = Rhythm::Monthly {
            dotm: 17,
            at: TEST_AT,
            slider: Slider::default(),
        }; // 18th day (0-based)
        let now = Utc.with_ymd_and_hms(2022, 11, 20, 0, 0, 0).unwrap();
        let last_fired = Utc.with_ymd_and_hms(2022, 11, 20, 0, 0, 0).unwrap();
        let result = monthly.next_time_to_fire(now, Some(last_fired));
        assert_eq!(
            result,
            Utc.with_ymd_and_hms(2022, 12, 18, 13, 37, 42).unwrap()
        );
    }

    #[test]
    fn week_daily_start_beat_same_day() {
        let week_daily = Rhythm::WeekDaily {
            dotw: 5,
            at: TEST_AT,
            slider: Slider::default(),
        }; // Saturday
        let now = Utc.with_ymd_and_hms(2022, 11, 19, 0, 0, 0).unwrap(); // Saturday
        let result = week_daily.next_time_to_fire(now, None);
        assert_eq!(
            result,
            Utc.with_ymd_and_hms(2022, 11, 19, 13, 37, 42).unwrap()
        );
    }

    #[test]
    fn week_daily_start_beat_different_day() {
        let week_daily = Rhythm::WeekDaily {
            dotw: 5,
            at: TEST_AT,
            slider: Slider::default(),
        }; // Saturday
        let now = Utc.with_ymd_and_hms(2022, 11, 20, 0, 0, 0).unwrap(); // Sunday
        let result = week_daily.next_time_to_fire(now, None);
        assert_eq!(
            result,
            Utc.with_ymd_and_hms(2022, 11, 26, 13, 37, 42).unwrap()
        );
    }

    #[test]
    fn week_daily_next_beat_same_day() {
        let week_daily = Rhythm::WeekDaily {
            dotw: 5,
            at: TEST_AT,
            slider: Slider::default(),
        }; // Saturday
        let now = Utc.with_ymd_and_hms(2022, 11, 19, 0, 0, 0).unwrap();
        let last_fired = Utc.with_ymd_and_hms(2022, 11, 19, 0, 0, 0).unwrap();
        let result = week_daily.next_time_to_fire(now, Some(last_fired));
        assert_eq!(
            result,
            Utc.with_ymd_and_hms(2022, 11, 26, 13, 37, 42).unwrap()
        );
    }

    #[test]
    fn week_daily_next_beat_different_day() {
        let week_daily = Rhythm::WeekDaily {
            dotw: 5,
            at: TEST_AT,
            slider: Slider::default(),
        }; // Saturday
        let now = Utc.with_ymd_and_hms(2022, 11, 20, 0, 0, 0).unwrap();
        let last_fired = Utc.with_ymd_and_hms(2022, 11, 20, 0, 0, 0).unwrap();
        let result = week_daily.next_time_to_fire(now, Some(last_fired));
        assert_eq!(
            result,
            Utc.with_ymd_and_hms(2022, 11, 26, 13, 37, 42).unwrap()
        );
    }

    #[test]
    fn every_n_days_start_beat() {
        let every_n_days = Rhythm::EveryNDays {
            n: 5,
            at: TEST_AT,
            slider: Slider::default(),
        };
        let now = Utc.with_ymd_and_hms(2023, 8, 19, 0, 0, 0).unwrap();
        let result = every_n_days.next_time_to_fire(now, None);
        assert_eq!(
            result,
            Utc.with_ymd_and_hms(2023, 8, 22, 13, 37, 42).unwrap()
        );
    }

    #[test]
    fn every_n_days_next_beat() {
        let every_n_days = Rhythm::EveryNDays {
            n: 5,
            at: TEST_AT,
            slider: Slider::default(),
        };
        let now = Utc.with_ymd_and_hms(2022, 11, 20, 0, 0, 0).unwrap();
        let last_fired = Utc.with_ymd_and_hms(2022, 11, 20, 0, 0, 0).unwrap();
        let result = every_n_days.next_time_to_fire(now, Some(last_fired));
        assert_eq!(
            result,
            Utc.with_ymd_and_hms(2022, 11, 25, 13, 37, 42).unwrap()
        );
    }

    #[test]
    fn daily_start_beat_la() {
        let daily = Rhythm::Daily { at: TEST_AT };
        let now = Los_Angeles.with_ymd_and_hms(2023, 8, 19, 0, 0, 0).unwrap();
        let result = daily.next_time_to_fire(now, None);
        assert_eq!(
            result,
            Los_Angeles
                .with_ymd_and_hms(2023, 8, 19, 13, 37, 42)
                .unwrap()
        );
    }

    #[test]
    fn daily_next_beat_la() {
        let daily = Rhythm::Daily { at: TEST_AT };
        let now = Los_Angeles.with_ymd_and_hms(2022, 11, 20, 0, 0, 0).unwrap();
        let last_fired = Los_Angeles.with_ymd_and_hms(2022, 11, 20, 0, 0, 0).unwrap();
        let result = daily.next_time_to_fire(now, Some(last_fired));
        assert_eq!(
            result,
            Los_Angeles
                .with_ymd_and_hms(2022, 11, 21, 13, 37, 42)
                .unwrap()
        );
    }

    #[test]
    fn monthly_start_beat_same_day_la() {
        let monthly = Rhythm::Monthly {
            dotm: 17,
            at: TEST_AT,
            slider: Slider::default(),
        }; // 18th day (0-based)
        let now = Los_Angeles.with_ymd_and_hms(2022, 11, 18, 0, 0, 0).unwrap();
        let result = monthly.next_time_to_fire(now, None);
        assert_eq!(
            result,
            Los_Angeles
                .with_ymd_and_hms(2022, 11, 18, 13, 37, 42)
                .unwrap()
        );
    }

    #[test]
    fn monthly_start_beat_different_day_la() {
        let monthly = Rhythm::Monthly {
            dotm: 17,
            at: TEST_AT,
            slider: Slider::default(),
        }; // 18th day (0-based)
        let now = Los_Angeles.with_ymd_and_hms(2022, 11, 20, 0, 0, 0).unwrap();
        let result = monthly.next_time_to_fire(now, None);
        assert_eq!(
            result,
            Los_Angeles
                .with_ymd_and_hms(2022, 12, 18, 13, 37, 42)
                .unwrap()
        );
    }

    #[test]
    fn monthly_next_beat_same_day_la() {
        let monthly = Rhythm::Monthly {
            dotm: 17,
            at: TEST_AT,
            slider: Slider::default(),
        }; // 18th day (0-based)
        let now = Los_Angeles.with_ymd_and_hms(2022, 11, 18, 0, 0, 0).unwrap();
        let last_fired = Los_Angeles.with_ymd_and_hms(2022, 11, 18, 0, 0, 0).unwrap();
        let result = monthly.next_time_to_fire(now, Some(last_fired));
        assert_eq!(
            result,
            Los_Angeles
                .with_ymd_and_hms(2022, 12, 18, 13, 37, 42)
                .unwrap()
        );
    }

    #[test]
    fn monthly_next_beat_different_day_la() {
        let monthly = Rhythm::Monthly {
            dotm: 17,
            at: TEST_AT,
            slider: Slider::default(),
        }; // 18th day (0-based)
        let now = Los_Angeles.with_ymd_and_hms(2022, 11, 20, 0, 0, 0).unwrap();
        let last_fired = Los_Angeles.with_ymd_and_hms(2022, 11, 20, 0, 0, 0).unwrap();
        let result = monthly.next_time_to_fire(now, Some(last_fired));
        assert_eq!(
            result,
            Los_Angeles
                .with_ymd_and_hms(2022, 12, 18, 13, 37, 42)
                .unwrap()
        );
    }

    #[test]
    fn week_daily_start_beat_same_day_la() {
        let week_daily = Rhythm::WeekDaily {
            dotw: 5,
            at: TEST_AT,
            slider: Slider::default(),
        }; // Saturday
        let now = Los_Angeles.with_ymd_and_hms(2022, 11, 19, 0, 0, 0).unwrap(); // Saturday
        let result = week_daily.next_time_to_fire(now, None);
        assert_eq!(
            result,
            Los_Angeles
                .with_ymd_and_hms(2022, 11, 19, 13, 37, 42)
                .unwrap()
        );
    }

    #[test]
    fn week_daily_start_beat_different_day_la() {
        let week_daily = Rhythm::WeekDaily {
            dotw: 5,
            at: TEST_AT,
            slider: Slider::default(),
        }; // Saturday
        let now = Los_Angeles.with_ymd_and_hms(2022, 11, 20, 0, 0, 0).unwrap(); // Sunday
        let result = week_daily.next_time_to_fire(now, None);
        assert_eq!(
            result,
            Los_Angeles
                .with_ymd_and_hms(2022, 11, 26, 13, 37, 42)
                .unwrap()
        );
    }

    #[test]
    fn week_daily_next_beat_same_day_la() {
        let week_daily = Rhythm::WeekDaily {
            dotw: 5,
            at: TEST_AT,
            slider: Slider::default(),
        }; // Saturday
        let now = Los_Angeles.with_ymd_and_hms(2022, 11, 19, 0, 0, 0).unwrap();
        let last_fired = Los_Angeles.with_ymd_and_hms(2022, 11, 19, 0, 0, 0).unwrap();
        let result = week_daily.next_time_to_fire(now, Some(last_fired));
        assert_eq!(
            result,
            Los_Angeles
                .with_ymd_and_hms(2022, 11, 26, 13, 37, 42)
                .unwrap()
        );
    }

    #[test]
    fn week_daily_next_beat_different_day_la() {
        let week_daily = Rhythm::WeekDaily {
            dotw: 5,
            at: TEST_AT,
            slider: Slider::default(),
        }; // Saturday
        let now = Los_Angeles.with_ymd_and_hms(2022, 11, 20, 0, 0, 0).unwrap();
        let last_fired = Los_Angeles.with_ymd_and_hms(2022, 11, 20, 0, 0, 0).unwrap();
        let result = week_daily.next_time_to_fire(now, Some(last_fired));
        assert_eq!(
            result,
            Los_Angeles
                .with_ymd_and_hms(2022, 11, 26, 13, 37, 42)
                .unwrap()
        );
    }

    #[test]
    fn every_n_days_start_beat_la() {
        let every_n_days = Rhythm::EveryNDays {
            n: 5,
            at: TEST_AT,
            slider: Slider::default(),
        };
        let now = Los_Angeles.with_ymd_and_hms(2023, 8, 19, 0, 0, 0).unwrap();
        let result = every_n_days.next_time_to_fire(now, None);
        assert_eq!(
            result,
            Los_Angeles
                .with_ymd_and_hms(2023, 8, 22, 13, 37, 42)
                .unwrap()
        );
    }

    #[test]
    fn every_n_days_next_beat_la() {
        let every_n_days = Rhythm::EveryNDays {
            n: 5,
            at: TEST_AT,
            slider: Slider::default(),
        };
        let now = Los_Angeles.with_ymd_and_hms(2022, 11, 20, 0, 0, 0).unwrap();
        let last_fired = Los_Angeles.with_ymd_and_hms(2022, 11, 20, 0, 0, 0).unwrap();
        let result = every_n_days.next_time_to_fire(now, Some(last_fired));
        assert_eq!(
            result,
            Los_Angeles
                .with_ymd_and_hms(2022, 11, 25, 13, 37, 42)
                .unwrap()
        );
    }

    fn create_test_rhythm(id: &str, rhythm: Rhythm) -> RhythmDefinition {
        RhythmDefinition {
            id: RhythmID::generate().unwrap(),
            rhythm,
            description: format!("Test rhythm {id}"),
            created_at: Utc::now(),
            created_at_tz: "UTC".to_string(),
            modified_at: Utc::now(),
            modified_at_tz: "UTC".to_string(),
        }
    }

    fn group_schedule_by_date(
        schedule: Vec<(DateTime<Tz>, RhythmDefinition)>,
    ) -> HashMap<NaiveDate, Vec<RhythmDefinition>> {
        let mut grouped = HashMap::new();
        for (dt, rhythm) in schedule {
            grouped
                .entry(dt.date_naive())
                .or_insert_with(Vec::new)
                .push(rhythm);
        }
        grouped
    }

    #[test]
    fn schedule_daily_rhythm() {
        let mut manager = create_test_manager("America/Los_Angeles");
        let daily = create_test_rhythm("daily1", Rhythm::Daily { at: TEST_AT });
        manager.set_rhythm(daily.clone()).unwrap();

        let start = NaiveDate::from_ymd_opt(2023, 8, 19).unwrap();
        let limit = NaiveDate::from_ymd_opt(2023, 8, 26).unwrap();
        let schedule_vec = manager.schedule(start, limit);
        let schedule = group_schedule_by_date(schedule_vec);

        // Daily should appear every day
        for day in 0..7 {
            let date = start + chrono::Duration::days(day);
            assert!(schedule.contains_key(&date));
            let rhythms = &schedule[&date];
            assert_eq!(rhythms.len(), 1);
            assert_eq!(rhythms[0].id, daily.id);
        }
    }

    #[test]
    fn schedule_monthly_rhythm() {
        let mut manager = create_test_manager("America/Los_Angeles");
        let monthly = create_test_rhythm(
            "monthly1",
            Rhythm::Monthly {
                dotm: 14, // 15th day (0-based)
                at: TEST_AT,
                slider: Slider::default(),
            },
        );
        manager.set_rhythm(monthly.clone()).unwrap();

        let start = NaiveDate::from_ymd_opt(2023, 8, 1).unwrap();
        let limit = NaiveDate::from_ymd_opt(2023, 10, 1).unwrap();
        let schedule_vec = manager.schedule(start, limit);
        let schedule = group_schedule_by_date(schedule_vec);

        // Should appear on the 15th of August and September
        let aug_15 = NaiveDate::from_ymd_opt(2023, 8, 15).unwrap();
        let sep_15 = NaiveDate::from_ymd_opt(2023, 9, 15).unwrap();

        assert!(schedule.contains_key(&aug_15));
        assert!(schedule.contains_key(&sep_15));

        let aug_rhythms = &schedule[&aug_15];
        assert_eq!(aug_rhythms.len(), 1);
        assert_eq!(aug_rhythms[0].id, monthly.id);

        let sep_rhythms = &schedule[&sep_15];
        assert_eq!(sep_rhythms.len(), 1);
        assert_eq!(sep_rhythms[0].id, monthly.id);
    }

    #[test]
    fn schedule_weekly_rhythm() {
        let mut manager = create_test_manager("America/Los_Angeles");
        let weekly = create_test_rhythm(
            "weekly1",
            Rhythm::WeekDaily {
                dotw: 1, // Tuesday
                at: TEST_AT,
                slider: Slider::default(),
            },
        );
        manager.set_rhythm(weekly.clone()).unwrap();

        let start = NaiveDate::from_ymd_opt(2023, 8, 14).unwrap(); // Monday
        let limit = NaiveDate::from_ymd_opt(2023, 8, 28).unwrap(); // Two weeks
        let schedule_vec = manager.schedule(start, limit);
        let schedule = group_schedule_by_date(schedule_vec);

        // Should appear on Tuesdays: Aug 15 and Aug 22
        let tuesday1 = NaiveDate::from_ymd_opt(2023, 8, 15).unwrap();
        let tuesday2 = NaiveDate::from_ymd_opt(2023, 8, 22).unwrap();

        assert!(schedule.contains_key(&tuesday1));
        assert!(schedule.contains_key(&tuesday2));

        let rhythms1 = &schedule[&tuesday1];
        assert_eq!(rhythms1.len(), 1);
        assert_eq!(rhythms1[0].id, weekly.id);

        let rhythms2 = &schedule[&tuesday2];
        assert_eq!(rhythms2.len(), 1);
        assert_eq!(rhythms2[0].id, weekly.id);
    }

    #[test]
    fn schedule_every_n_days_rhythm() {
        let mut manager = create_test_manager("America/Los_Angeles");
        let every3 = create_test_rhythm(
            "every3",
            Rhythm::EveryNDays {
                n: 3,
                at: TEST_AT,
                slider: Slider::default(),
            },
        );
        manager.set_rhythm(every3.clone()).unwrap();

        let start = NaiveDate::from_ymd_opt(2023, 8, 19).unwrap();
        let limit = NaiveDate::from_ymd_opt(2023, 8, 29).unwrap();
        let schedule_vec = manager.schedule(start, limit);

        // Should appear every 3 days starting from a predictable day
        // Based on the algorithm, it aligns to weekday for n<7
        let mut found_dates = Vec::new();
        for (dt, rhythm) in &schedule_vec {
            if rhythm.id == every3.id {
                found_dates.push(dt.date_naive());
            }
        }
        found_dates.sort();

        // Verify spacing between dates
        for i in 1..found_dates.len() {
            let diff = (found_dates[i] - found_dates[i - 1]).num_days();
            assert_eq!(diff, 3);
        }
    }

    #[test]
    fn schedule_with_done_event() {
        let mut manager = create_test_manager("America/Los_Angeles");
        let daily = create_test_rhythm("daily2", Rhythm::Daily { at: TEST_AT });
        manager.set_rhythm(daily.clone()).unwrap();

        // Mark today as done
        let today = NaiveDate::from_ymd_opt(2023, 8, 20).unwrap();
        manager.events.push(EventRecord {
            rhythm_id: daily.id,
            event_type: EventType::Done,
            when: today
                .and_time(TEST_AT)
                .and_local_timezone(Utc)
                .earliest()
                .unwrap(),
            when_tz: "UTC".to_string(),
        });

        let start = NaiveDate::from_ymd_opt(2023, 8, 19).unwrap();
        let limit = NaiveDate::from_ymd_opt(2023, 8, 22).unwrap();
        let schedule_vec = manager.schedule(start, limit);
        let schedule = group_schedule_by_date(schedule_vec);

        // Should not appear on the day marked as done
        let empty_vec = Vec::new();
        let today_rhythms = schedule.get(&today).unwrap_or(&empty_vec);
        assert_eq!(today_rhythms.len(), 0);

        // Should appear on other days
        let yesterday = today - chrono::Duration::days(1);
        let tomorrow = today + chrono::Duration::days(1);

        assert!(
            schedule
                .get(&yesterday)
                .unwrap_or(&empty_vec)
                .iter()
                .any(|r| r.id == daily.id)
        );
        assert!(
            schedule
                .get(&tomorrow)
                .unwrap_or(&empty_vec)
                .iter()
                .any(|r| r.id == daily.id)
        );
    }

    #[test]
    fn schedule_with_defer_event() {
        let mut manager = create_test_manager("America/Los_Angeles");
        let weekly = create_test_rhythm(
            "weekly2",
            Rhythm::WeekDaily {
                dotw: 3, // Thursday
                at: TEST_AT,
                slider: Slider::new(1), // Can move 1 day before
            },
        );
        manager.set_rhythm(weekly.clone()).unwrap();

        // Defer Thursday Aug 17
        let deferred_day = NaiveDate::from_ymd_opt(2023, 8, 17).unwrap();
        manager.events.push(EventRecord {
            rhythm_id: weekly.id,
            event_type: EventType::Defer,
            when: deferred_day
                .and_time(TEST_AT)
                .and_local_timezone(Utc)
                .earliest()
                .unwrap(),
            when_tz: "UTC".to_string(),
        });

        let start = NaiveDate::from_ymd_opt(2023, 8, 14).unwrap();
        let limit = NaiveDate::from_ymd_opt(2023, 8, 25).unwrap();
        let schedule_vec = manager.schedule(start, limit);
        println!("schedule_vec: {:?}", schedule_vec);
        let schedule = group_schedule_by_date(schedule_vec);

        // Should not appear on deferred day
        let empty_vec = Vec::new();
        let deferred_day_rhythms = schedule.get(&deferred_day).unwrap_or(&empty_vec);
        assert!(!deferred_day_rhythms.iter().any(|r| r.id == weekly.id));

        // Slider only allows moving earlier, so check day before or the rhythm moves to next week
        let day_before = deferred_day - chrono::Duration::days(1);

        let appears_before = schedule
            .get(&day_before)
            .map(|rhythms| rhythms.iter().any(|r| r.id == weekly.id))
            .unwrap_or(false);

        // When deferred, rhythm either slides earlier (if slider allows) or moves to next beat
        // With slider=1, it can appear on Wed Aug 16 (day before Thursday)
        // Or it skips to next Thursday Aug 24
        let next_thursday = NaiveDate::from_ymd_opt(2023, 8, 24).unwrap();
        let appears_next_week = schedule
            .get(&next_thursday)
            .map(|rhythms| rhythms.iter().any(|r| r.id == weekly.id))
            .unwrap_or(false);

        assert!(
            appears_before || appears_next_week,
            "Weekly rhythm should appear on day before (slider) or next week"
        );
    }

    #[test]
    fn schedule_with_slider() {
        let mut manager = create_test_manager("America/Los_Angeles");

        // Add two monthly rhythms that would normally conflict
        let monthly1 = create_test_rhythm(
            "monthly_slider1",
            Rhythm::Monthly {
                dotm: 14, // 15th
                at: TEST_AT,
                slider: Slider::new(2), // Can move 2 days before
            },
        );
        let monthly2 = create_test_rhythm(
            "monthly_slider2",
            Rhythm::Monthly {
                dotm: 14, // Also 15th
                at: TEST_AT,
                slider: Slider::new(2),
            },
        );

        manager.set_rhythm(monthly1.clone()).unwrap();
        manager.set_rhythm(monthly2.clone()).unwrap();

        let start = NaiveDate::from_ymd_opt(2023, 8, 10).unwrap();
        let limit = NaiveDate::from_ymd_opt(2023, 8, 20).unwrap();
        let schedule_vec = manager.schedule(start, limit);

        // Both should be scheduled, potentially on different days due to smoothing
        let mut found1 = false;
        let mut found2 = false;

        for (_, rhythm) in &schedule_vec {
            if rhythm.id == monthly1.id {
                found1 = true;
            }
            if rhythm.id == monthly2.id {
                found2 = true;
            }
        }

        assert!(found1);
        assert!(found2);
    }

    #[test]
    fn schedule_with_spoons() {
        let mut manager = create_test_manager("America/Los_Angeles");

        // Add multiple daily rhythms
        for i in 0..5 {
            let daily =
                create_test_rhythm(&format!("daily_spoons_{i}"), Rhythm::Daily { at: TEST_AT });
            manager.set_rhythm(daily).unwrap();
        }

        let test_day = NaiveDate::from_ymd_opt(2023, 8, 20).unwrap();

        // Set low spoons (should reduce capacity)
        manager.spoons(test_day, 2);

        let start = NaiveDate::from_ymd_opt(2023, 8, 19).unwrap();
        let limit = NaiveDate::from_ymd_opt(2023, 8, 22).unwrap();
        let schedule_vec = manager.schedule(start, limit);
        let schedule = group_schedule_by_date(schedule_vec);

        // All 5 dailies should still appear (dailies bypass spoons system)
        let empty_vec = Vec::new();
        let test_day_rhythms = schedule.get(&test_day).unwrap_or(&empty_vec);
        assert_eq!(test_day_rhythms.len(), 5);
    }

    #[test]
    fn convergence_test() {
        let mut manager = create_test_manager("America/Los_Angeles");

        let weekly = create_test_rhythm(
            "conv_weekly",
            Rhythm::WeekDaily {
                dotw: 1, // Tuesday
                at: TEST_AT,
                slider: Slider::default(),
            },
        );
        let monthly = create_test_rhythm(
            "conv_monthly",
            Rhythm::Monthly {
                dotm: 24, // 25th
                at: TEST_AT,
                slider: Slider::default(),
            },
        );

        manager.set_rhythm(weekly).unwrap();
        manager.set_rhythm(monthly).unwrap();

        // Mark weekly as done recently
        manager.events.push(EventRecord {
            rhythm_id: manager.rhythms[0].id,
            event_type: EventType::Done,
            when: Utc::now() - chrono::Duration::days(1),
            when_tz: "UTC".to_string(),
        });

        let convergence = manager.convergence();

        // Convergence should be in the future
        assert!(convergence > Utc::now());

        // Should be within a year
        assert!(convergence < Utc::now() + chrono::Duration::days(365));
    }

    #[test]
    fn delinquent_test() {
        let mut manager = create_test_manager("America/Los_Angeles");

        let daily1 = create_test_rhythm("delinquent1", Rhythm::Daily { at: TEST_AT });
        let daily2 = create_test_rhythm("delinquent2", Rhythm::Daily { at: TEST_AT });

        manager.set_rhythm(daily1.clone()).unwrap();
        manager.set_rhythm(daily2.clone()).unwrap();

        // Mark daily1 as done today (not delinquent)
        manager.events.push(EventRecord {
            rhythm_id: daily1.id,
            event_type: EventType::Done,
            when: Utc::now() - chrono::Duration::hours(1),
            when_tz: "UTC".to_string(),
        });

        // Mark daily2 as done 3 days ago (delinquent)
        manager.events.push(EventRecord {
            rhythm_id: daily2.id,
            event_type: EventType::Done,
            when: Utc::now() - chrono::Duration::days(3),
            when_tz: "UTC".to_string(),
        });

        let delinquent = manager.delinquent();

        // Only daily2 should be delinquent
        assert_eq!(delinquent.len(), 1);
        assert_eq!(delinquent[0].id, daily2.id);
    }

    #[test]
    fn complex_mixed_schedule() {
        let mut manager = create_test_manager("America/Los_Angeles");

        // Add various rhythm types
        let daily = create_test_rhythm("mixed_daily", Rhythm::Daily { at: TEST_AT });
        let weekly = create_test_rhythm(
            "mixed_weekly",
            Rhythm::WeekDaily {
                dotw: 3, // Thursday
                at: TEST_AT,
                slider: Slider::new(1),
            },
        );
        let monthly = create_test_rhythm(
            "mixed_monthly",
            Rhythm::Monthly {
                dotm: 9, // 10th
                at: TEST_AT,
                slider: Slider::new(2),
            },
        );
        let every5 = create_test_rhythm(
            "mixed_every5",
            Rhythm::EveryNDays {
                n: 5,
                at: TEST_AT,
                slider: Slider::new(1),
            },
        );

        manager.set_rhythm(daily.clone()).unwrap();
        manager.set_rhythm(weekly.clone()).unwrap();
        manager.set_rhythm(monthly.clone()).unwrap();
        manager.set_rhythm(every5.clone()).unwrap();

        // Add some events
        let base_date = NaiveDate::from_ymd_opt(2023, 8, 8).unwrap();

        // Mark daily as done on Aug 8
        manager.events.push(EventRecord {
            rhythm_id: daily.id,
            event_type: EventType::Done,
            when: base_date
                .and_time(TEST_AT)
                .and_local_timezone(Utc)
                .earliest()
                .unwrap(),
            when_tz: "UTC".to_string(),
        });

        // Defer weekly on Aug 10 (Thursday)
        let thursday = NaiveDate::from_ymd_opt(2023, 8, 10).unwrap();
        manager.events.push(EventRecord {
            rhythm_id: weekly.id,
            event_type: EventType::Defer,
            when: thursday
                .and_time(TEST_AT)
                .and_local_timezone(Utc)
                .earliest()
                .unwrap(),
            when_tz: "UTC".to_string(),
        });

        let start = NaiveDate::from_ymd_opt(2023, 8, 7).unwrap();
        let limit = NaiveDate::from_ymd_opt(2023, 8, 14).unwrap();
        let schedule_vec = manager.schedule(start, limit);
        let schedule = group_schedule_by_date(schedule_vec.clone());

        // Verify daily appears every day except Aug 8
        for day in 0..7 {
            let date = start + chrono::Duration::days(day);
            let has_daily = schedule
                .get(&date)
                .map(|rhythms| rhythms.iter().any(|r| r.id == daily.id))
                .unwrap_or(false);

            if date == base_date {
                assert!(!has_daily, "Daily should not appear on done day");
            } else {
                assert!(has_daily, "Daily should appear on {date}");
            }
        }

        // Verify weekly doesn't appear on deferred Thursday
        let empty_vec = Vec::new();
        let thursday_rhythms = schedule.get(&thursday).unwrap_or(&empty_vec);
        assert!(!thursday_rhythms.iter().any(|r| r.id == weekly.id));

        // Verify monthly appears within its slider range (Aug 8-10)
        // dotm: 9 means 10th (0-based), slider: 2 means can move up to 2 days earlier
        let mut monthly_date = None;
        for (dt, rhythm) in &schedule_vec {
            if rhythm.id == monthly.id {
                monthly_date = Some(dt.date_naive());
                break;
            }
        }
        let tenth = NaiveDate::from_ymd_opt(2023, 8, 10).unwrap();
        let eighth = NaiveDate::from_ymd_opt(2023, 8, 8).unwrap();
        assert!(monthly_date.is_some(), "Monthly should appear in schedule");
        let mdate = monthly_date.unwrap();
        assert!(
            mdate >= eighth && mdate <= tenth,
            "Monthly should appear between Aug 8 and Aug 10 (slider range), got {mdate}"
        );

        // Verify every5 appears somewhere
        let mut every5_found = false;
        for (_, rhythm) in &schedule_vec {
            if rhythm.id == every5.id {
                every5_found = true;
                break;
            }
        }
        assert!(every5_found);
    }

    #[test]
    fn timezone_boundary_daily_scheduling() {
        let mut manager = create_test_manager("America/Los_Angeles");

        let daily = create_test_rhythm("tz_daily2", Rhythm::Daily { at: TEST_AT });
        manager.set_rhythm(daily.clone()).unwrap();

        let la_date = NaiveDate::from_ymd_opt(2023, 8, 20).unwrap();
        let la_time = NaiveTime::from_hms_opt(23, 45, 0).unwrap();
        let la_datetime = la_date
            .and_time(la_time)
            .and_local_timezone(Los_Angeles)
            .earliest()
            .unwrap();

        manager.events.push(EventRecord {
            rhythm_id: daily.id,
            event_type: EventType::Done,
            when: la_datetime.with_timezone(&Utc),
            when_tz: "America/Los_Angeles".to_string(),
        });

        let start = la_date;
        let limit = la_date + chrono::Duration::days(3);
        let schedule_vec = manager.schedule(start, limit);
        let schedule = group_schedule_by_date(schedule_vec);

        let empty_vec = Vec::new();
        let done_day_rhythms = schedule.get(&la_date).unwrap_or(&empty_vec);
        assert!(
            !done_day_rhythms.iter().any(|r| r.id == daily.id),
            "Daily should not appear on done day (Aug 20 LA time)"
        );

        let next_day = la_date + chrono::Duration::days(1);
        let next_day_rhythms = schedule.get(&next_day).unwrap_or(&empty_vec);
        assert!(
            next_day_rhythms.iter().any(|r| r.id == daily.id),
            "Daily should appear on Aug 21"
        );
    }

    #[test]
    fn timezone_boundary_defer_dates() {
        let mut manager = create_test_manager("America/Los_Angeles");

        let weekly = create_test_rhythm(
            "tz_weekly",
            Rhythm::WeekDaily {
                dotw: 6,
                at: TEST_AT,
                slider: Slider::new(1),
            },
        );
        manager.set_rhythm(weekly.clone()).unwrap();

        let la_date = NaiveDate::from_ymd_opt(2023, 8, 20).unwrap();
        let la_time = NaiveTime::from_hms_opt(23, 55, 0).unwrap();
        let la_datetime = la_date
            .and_time(la_time)
            .and_local_timezone(Los_Angeles)
            .earliest()
            .unwrap();

        manager.events.push(EventRecord {
            rhythm_id: weekly.id,
            event_type: EventType::Defer,
            when: la_datetime.with_timezone(&Utc),
            when_tz: "America/Los_Angeles".to_string(),
        });

        let start = la_date;
        let limit = la_date + chrono::Duration::days(10);
        let schedule_vec = manager.schedule(start, limit);
        let schedule = group_schedule_by_date(schedule_vec);

        let empty_vec = Vec::new();
        let deferred_day_rhythms = schedule.get(&la_date).unwrap_or(&empty_vec);
        assert!(
            !deferred_day_rhythms.iter().any(|r| r.id == weekly.id),
            "Weekly should not appear on deferred day (Aug 20 LA time, even though UTC is Aug 21)"
        );

        // Slider only allows moving earlier (day before) or rhythm moves to next week
        let day_before = la_date - chrono::Duration::days(1);

        let appears_before = schedule
            .get(&day_before)
            .map(|rhythms| rhythms.iter().any(|r| r.id == weekly.id))
            .unwrap_or(false);

        // Next Sunday is 7 days after Aug 20
        let next_sunday = la_date + chrono::Duration::days(7);
        let appears_next_week = schedule
            .get(&next_sunday)
            .map(|rhythms| rhythms.iter().any(|r| r.id == weekly.id))
            .unwrap_or(false);

        assert!(
            appears_before || appears_next_week,
            "Weekly should appear on day before (slider) or next week"
        );
    }

    #[test]
    fn timezone_early_morning_utc() {
        let mut manager = create_test_manager("America/Los_Angeles");

        let daily = create_test_rhythm("tz_early", Rhythm::Daily { at: TEST_AT });
        manager.set_rhythm(daily.clone()).unwrap();

        let la_date = NaiveDate::from_ymd_opt(2023, 8, 20).unwrap();
        let la_time = NaiveTime::from_hms_opt(2, 0, 0).unwrap();
        let la_datetime = la_date
            .and_time(la_time)
            .and_local_timezone(Los_Angeles)
            .earliest()
            .unwrap();

        manager.events.push(EventRecord {
            rhythm_id: daily.id,
            event_type: EventType::Done,
            when: la_datetime.with_timezone(&Utc),
            when_tz: "America/Los_Angeles".to_string(),
        });

        let start = la_date;
        let limit = la_date + chrono::Duration::days(2);
        let schedule_vec = manager.schedule(start, limit);
        let schedule = group_schedule_by_date(schedule_vec);

        let empty_vec = Vec::new();
        let done_day_rhythms = schedule.get(&la_date).unwrap_or(&empty_vec);
        assert!(
            !done_day_rhythms.iter().any(|r| r.id == daily.id),
            "Event at 2 AM LA time should be recognized as done on Aug 20 LA"
        );
    }

    #[test]
    fn schedule2_ignore_today_false_reduces_capacity() {
        let mut manager = create_test_manager("America/Los_Angeles");

        let every2 = create_test_rhythm(
            "sched2_every2",
            Rhythm::EveryNDays {
                n: 2,
                at: TEST_AT,
                slider: Slider::new(1),
            },
        );
        let every3 = create_test_rhythm(
            "sched2_every3",
            Rhythm::EveryNDays {
                n: 3,
                at: TEST_AT,
                slider: Slider::new(1),
            },
        );

        manager.set_rhythm(every2.clone()).unwrap();
        manager.set_rhythm(every3.clone()).unwrap();

        let today = manager.now().date_naive();

        manager.events.push(EventRecord {
            rhythm_id: every2.id,
            event_type: EventType::Done,
            when: manager.now().with_timezone(&Utc),
            when_tz: manager.timezone().to_string(),
        });

        let limit = today + chrono::Duration::days(10);

        let schedule_ignore = manager.schedule2(today, limit, true);
        let schedule_adjust = manager.schedule2(today, limit, false);

        let today_count_ignore = schedule_ignore
            .iter()
            .filter(|(dt, _)| dt.date_naive() == today)
            .count();
        let today_count_adjust = schedule_adjust
            .iter()
            .filter(|(dt, _)| dt.date_naive() == today)
            .count();

        println!("schedule2 ignore_today=true: {today_count_ignore} items today");
        println!("schedule2 ignore_today=false: {today_count_adjust} items today");

        assert!(
            today_count_adjust <= today_count_ignore,
            "ignore_today=false should reduce today's capacity. ignore={today_count_ignore}, adjust={today_count_adjust}"
        );
    }

    #[test]
    fn schedule2_ignore_today_true_does_not_reduce_capacity() {
        let mut manager = create_test_manager("America/Los_Angeles");

        let daily = create_test_rhythm("sched2_daily", Rhythm::Daily { at: TEST_AT });
        manager.set_rhythm(daily.clone()).unwrap();

        let today = manager.now().date_naive();

        manager.events.push(EventRecord {
            rhythm_id: daily.id,
            event_type: EventType::Done,
            when: manager.now().with_timezone(&Utc),
            when_tz: manager.timezone().to_string(),
        });

        let limit = today + chrono::Duration::days(5);

        let schedule_ignore = manager.schedule2(today, limit, true);
        let schedule_no_ignore = manager.schedule2(today, limit, false);

        let tomorrow = today + chrono::Duration::days(1);

        let tomorrow_count_ignore = schedule_ignore
            .iter()
            .filter(|(dt, _)| dt.date_naive() == tomorrow)
            .count();
        let tomorrow_count_no_ignore = schedule_no_ignore
            .iter()
            .filter(|(dt, _)| dt.date_naive() == tomorrow)
            .count();

        assert_eq!(
            tomorrow_count_ignore, tomorrow_count_no_ignore,
            "Future days should be unaffected by ignore_today flag"
        );
    }

    #[test]
    fn watermarks_increase_slots_when_rhythms_overflow() {
        let mut manager = create_test_manager("America/Los_Angeles");

        for i in 0..5 {
            let rhythm = create_test_rhythm(
                &format!("watermark_every2_{i}"),
                Rhythm::EveryNDays {
                    n: 2,
                    at: TEST_AT,
                    slider: Slider::new(1),
                },
            );
            manager.set_rhythm(rhythm).unwrap();
        }

        let start = NaiveDate::from_ymd_opt(2023, 8, 19).unwrap();
        let limit = NaiveDate::from_ymd_opt(2023, 8, 26).unwrap();
        let schedule_vec = manager.schedule(start, limit);
        let schedule = group_schedule_by_date(schedule_vec);

        let mut total_scheduled = 0;
        for day in 0..7 {
            let date = start + chrono::Duration::days(day);
            let count = schedule.get(&date).map(|v| v.len()).unwrap_or(0);
            total_scheduled += count;
            println!("Day {date}: {count} rhythms scheduled");
        }

        assert!(
            total_scheduled >= 5 * 3,
            "All 5 rhythms should fire at least 3 times over 7 days (every 2 days). Got {total_scheduled}"
        );
    }

    #[test]
    fn slider_allows_rhythm_to_shift_earlier() {
        let mut manager = create_test_manager("America/Los_Angeles");

        let monthly1 = create_test_rhythm(
            "slider_monthly1",
            Rhythm::Monthly {
                dotm: 14,
                at: TEST_AT,
                slider: Slider::new(3),
            },
        );
        let monthly2 = create_test_rhythm(
            "slider_monthly2",
            Rhythm::Monthly {
                dotm: 14,
                at: TEST_AT,
                slider: Slider::new(3),
            },
        );

        manager.set_rhythm(monthly1.clone()).unwrap();
        manager.set_rhythm(monthly2.clone()).unwrap();

        let start = NaiveDate::from_ymd_opt(2023, 8, 10).unwrap();
        let limit = NaiveDate::from_ymd_opt(2023, 8, 20).unwrap();
        let schedule_vec = manager.schedule(start, limit);

        let mut monthly1_date = None;
        let mut monthly2_date = None;

        for (dt, rhythm) in &schedule_vec {
            if rhythm.id == monthly1.id && monthly1_date.is_none() {
                monthly1_date = Some(dt.date_naive());
            }
            if rhythm.id == monthly2.id && monthly2_date.is_none() {
                monthly2_date = Some(dt.date_naive());
            }
        }

        let target_date = NaiveDate::from_ymd_opt(2023, 8, 15).unwrap();
        let earliest_allowed = target_date - chrono::Duration::days(3);

        if let Some(date) = monthly1_date {
            assert!(
                date >= earliest_allowed && date <= target_date,
                "monthly1 should be within slider range: {date}"
            );
        }
        if let Some(date) = monthly2_date {
            assert!(
                date >= earliest_allowed && date <= target_date,
                "monthly2 should be within slider range: {date}"
            );
        }

        if monthly1_date.is_some() && monthly2_date.is_some() {
            println!(
                "monthly1 scheduled on {:?}, monthly2 on {:?}",
                monthly1_date, monthly2_date
            );
        }
    }

    #[test]
    fn multiple_done_events_today_reduce_capacity_proportionally() {
        let mut manager = create_test_manager("America/Los_Angeles");

        for i in 0..4 {
            let rhythm = create_test_rhythm(
                &format!("multi_done_{i}"),
                Rhythm::EveryNDays {
                    n: 3,
                    at: TEST_AT,
                    slider: Slider::new(2),
                },
            );
            manager.set_rhythm(rhythm).unwrap();
        }

        let today = manager.now().date_naive();

        manager.events.push(EventRecord {
            rhythm_id: manager.rhythms[0].id,
            event_type: EventType::Done,
            when: manager.now().with_timezone(&Utc),
            when_tz: manager.timezone().to_string(),
        });
        manager.events.push(EventRecord {
            rhythm_id: manager.rhythms[1].id,
            event_type: EventType::Done,
            when: manager.now().with_timezone(&Utc),
            when_tz: manager.timezone().to_string(),
        });

        let limit = today + chrono::Duration::days(10);

        let schedule_no_adjust = manager.schedule2(today, limit, true);
        let schedule_with_adjust = manager.schedule2(today, limit, false);

        let today_no_adjust = schedule_no_adjust
            .iter()
            .filter(|(dt, _)| dt.date_naive() == today)
            .count();
        let today_with_adjust = schedule_with_adjust
            .iter()
            .filter(|(dt, _)| dt.date_naive() == today)
            .count();

        println!("2 done events: no_adjust={today_no_adjust}, with_adjust={today_with_adjust}");

        assert!(
            today_with_adjust <= today_no_adjust,
            "Capacity should be reduced by done events"
        );
    }

    #[test]
    fn defer_events_count_toward_capacity_reduction() {
        let mut manager = create_test_manager("America/Los_Angeles");

        for i in 0..3 {
            let rhythm = create_test_rhythm(
                &format!("defer_cap_{i}"),
                Rhythm::EveryNDays {
                    n: 4,
                    at: TEST_AT,
                    slider: Slider::new(1),
                },
            );
            manager.set_rhythm(rhythm).unwrap();
        }

        let today = manager.now().date_naive();

        manager.events.push(EventRecord {
            rhythm_id: manager.rhythms[0].id,
            event_type: EventType::Defer,
            when: manager.now().with_timezone(&Utc),
            when_tz: manager.timezone().to_string(),
        });

        let limit = today + chrono::Duration::days(10);

        let schedule_no_adjust = manager.schedule2(today, limit, true);
        let schedule_with_adjust = manager.schedule2(today, limit, false);

        let today_no_adjust = schedule_no_adjust
            .iter()
            .filter(|(dt, _)| dt.date_naive() == today)
            .count();
        let today_with_adjust = schedule_with_adjust
            .iter()
            .filter(|(dt, _)| dt.date_naive() == today)
            .count();

        println!("1 defer event: no_adjust={today_no_adjust}, with_adjust={today_with_adjust}");

        assert!(
            today_with_adjust <= today_no_adjust,
            "Defer events should reduce capacity"
        );
    }

    #[test]
    fn rhythm_skipped_on_deferred_day_moves_to_next_beat() {
        let mut manager = create_test_manager("America/Los_Angeles");

        let every3 = create_test_rhythm(
            "defer_skip",
            Rhythm::EveryNDays {
                n: 3,
                at: TEST_AT,
                slider: Slider::default(),
            },
        );
        manager.set_rhythm(every3.clone()).unwrap();

        let start = NaiveDate::from_ymd_opt(2023, 8, 19).unwrap();
        let limit = NaiveDate::from_ymd_opt(2023, 8, 30).unwrap();

        let schedule_before = manager.schedule(start, limit);
        let first_occurrence = schedule_before
            .iter()
            .find(|(_, r)| r.id == every3.id)
            .map(|(dt, _)| dt.date_naive());

        if let Some(deferred_date) = first_occurrence {
            manager.events.push(EventRecord {
                rhythm_id: every3.id,
                event_type: EventType::Defer,
                when: deferred_date
                    .and_time(TEST_AT)
                    .and_local_timezone(Utc)
                    .earliest()
                    .unwrap(),
                when_tz: "UTC".to_string(),
            });

            let schedule_after = manager.schedule(start, limit);

            let still_on_deferred = schedule_after
                .iter()
                .any(|(dt, r)| r.id == every3.id && dt.date_naive() == deferred_date);

            assert!(
                !still_on_deferred,
                "Rhythm should not appear on deferred date {deferred_date}"
            );

            let appears_elsewhere = schedule_after.iter().any(|(_, r)| r.id == every3.id);
            assert!(
                appears_elsewhere,
                "Rhythm should still appear on other days"
            );

            println!("Deferred date: {deferred_date}, rhythm rescheduled successfully");
        }
    }

    #[test]
    fn schedule_handles_empty_rhythms() {
        let manager = create_test_manager("America/Los_Angeles");

        let start = NaiveDate::from_ymd_opt(2023, 8, 19).unwrap();
        let limit = NaiveDate::from_ymd_opt(2023, 8, 26).unwrap();
        let schedule = manager.schedule(start, limit);

        assert!(
            schedule.is_empty(),
            "Empty manager should produce empty schedule"
        );
    }

    #[test]
    fn schedule_handles_start_equals_limit() {
        let mut manager = create_test_manager("America/Los_Angeles");
        let daily = create_test_rhythm("edge_daily", Rhythm::Daily { at: TEST_AT });
        manager.set_rhythm(daily).unwrap();

        let date = NaiveDate::from_ymd_opt(2023, 8, 19).unwrap();
        let schedule = manager.schedule(date, date);

        assert!(
            schedule.is_empty(),
            "Schedule with start == limit should be empty"
        );
    }

    #[test]
    fn schedule_handles_limit_before_start() {
        let mut manager = create_test_manager("America/Los_Angeles");
        let daily = create_test_rhythm("edge_daily2", Rhythm::Daily { at: TEST_AT });
        manager.set_rhythm(daily).unwrap();

        let start = NaiveDate::from_ymd_opt(2023, 8, 26).unwrap();
        let limit = NaiveDate::from_ymd_opt(2023, 8, 19).unwrap();
        let schedule = manager.schedule(start, limit);

        assert!(
            schedule.is_empty(),
            "Schedule with limit < start should be empty"
        );
    }

    #[test]
    fn high_spoons_increases_daily_capacity() {
        let mut manager = create_test_manager("America/Los_Angeles");

        for i in 0..6 {
            let rhythm = create_test_rhythm(
                &format!("high_spoons_{i}"),
                Rhythm::EveryNDays {
                    n: 2,
                    at: TEST_AT,
                    slider: Slider::new(1),
                },
            );
            manager.set_rhythm(rhythm).unwrap();
        }

        let test_day = NaiveDate::from_ymd_opt(2023, 8, 20).unwrap();

        let schedule_normal = manager.schedule(test_day, test_day + chrono::Duration::days(5));
        let normal_count = schedule_normal
            .iter()
            .filter(|(dt, _)| dt.date_naive() == test_day)
            .count();

        manager.spoons(test_day, 10);

        let schedule_high = manager.schedule(test_day, test_day + chrono::Duration::days(5));
        let high_count = schedule_high
            .iter()
            .filter(|(dt, _)| dt.date_naive() == test_day)
            .count();

        println!("Normal spoons (5): {normal_count} items, High spoons (10): {high_count} items");

        assert!(
            high_count >= normal_count,
            "High spoons should allow at least as many items. normal={normal_count}, high={high_count}"
        );
    }

    #[test]
    fn low_spoons_decreases_daily_capacity() {
        let mut manager = create_test_manager("America/Los_Angeles");

        for i in 0..6 {
            let rhythm = create_test_rhythm(
                &format!("low_spoons_{i}"),
                Rhythm::EveryNDays {
                    n: 2,
                    at: TEST_AT,
                    slider: Slider::new(1),
                },
            );
            manager.set_rhythm(rhythm).unwrap();
        }

        let test_day = NaiveDate::from_ymd_opt(2023, 8, 20).unwrap();

        let schedule_normal = manager.schedule(test_day, test_day + chrono::Duration::days(5));
        let normal_count = schedule_normal
            .iter()
            .filter(|(dt, _)| dt.date_naive() == test_day)
            .count();

        manager.spoons(test_day, 1);

        let schedule_low = manager.schedule(test_day, test_day + chrono::Duration::days(5));
        let low_count = schedule_low
            .iter()
            .filter(|(dt, _)| dt.date_naive() == test_day)
            .count();

        println!("Normal spoons (5): {normal_count} items, Low spoons (1): {low_count} items");

        assert!(
            low_count <= normal_count,
            "Low spoons should allow fewer or equal items. normal={normal_count}, low={low_count}"
        );
    }

    #[test]
    fn schedule_sorts_by_datetime() {
        let mut manager = create_test_manager("America/Los_Angeles");

        let early = NaiveTime::from_hms_opt(8, 0, 0).unwrap();
        let mid = NaiveTime::from_hms_opt(12, 0, 0).unwrap();
        let late = NaiveTime::from_hms_opt(20, 0, 0).unwrap();

        let rhythm1 = create_test_rhythm("sort_late", Rhythm::Daily { at: late });
        let rhythm2 = create_test_rhythm("sort_early", Rhythm::Daily { at: early });
        let rhythm3 = create_test_rhythm("sort_mid", Rhythm::Daily { at: mid });

        manager.set_rhythm(rhythm1).unwrap();
        manager.set_rhythm(rhythm2).unwrap();
        manager.set_rhythm(rhythm3).unwrap();

        let start = NaiveDate::from_ymd_opt(2023, 8, 19).unwrap();
        let limit = NaiveDate::from_ymd_opt(2023, 8, 20).unwrap();
        let schedule = manager.schedule(start, limit);

        for i in 1..schedule.len() {
            assert!(
                schedule[i].0 >= schedule[i - 1].0,
                "Schedule should be sorted by datetime: {:?} should come after {:?}",
                schedule[i].0,
                schedule[i - 1].0
            );
        }
    }

    #[test]
    fn skip_beat_within_slider_for_monthly() {
        let mut manager = create_test_manager("America/Los_Angeles");

        let monthly = create_test_rhythm(
            "skip_beat_monthly",
            Rhythm::Monthly {
                dotm: 14,
                at: TEST_AT,
                slider: Slider::new(3),
            },
        );
        manager.set_rhythm(monthly.clone()).unwrap();

        let aug_14 = NaiveDate::from_ymd_opt(2023, 8, 15).unwrap();
        manager.events.push(EventRecord {
            rhythm_id: monthly.id,
            event_type: EventType::Done,
            when: aug_14
                .and_time(TEST_AT)
                .and_local_timezone(Utc)
                .earliest()
                .unwrap(),
            when_tz: "UTC".to_string(),
        });

        let start = NaiveDate::from_ymd_opt(2023, 8, 16).unwrap();
        let limit = NaiveDate::from_ymd_opt(2023, 10, 1).unwrap();
        let schedule = manager.schedule(start, limit);

        let next_occurrence = schedule
            .iter()
            .find(|(_, r)| r.id == monthly.id)
            .map(|(dt, _)| dt.date_naive());

        if let Some(date) = next_occurrence {
            let sep_15 = NaiveDate::from_ymd_opt(2023, 9, 15).unwrap();
            let sep_12 = NaiveDate::from_ymd_opt(2023, 9, 12).unwrap();

            assert!(
                date >= sep_12 && date <= sep_15,
                "Next monthly should be in September (within slider range), got {date}"
            );
            println!("After Aug 15 done, next monthly scheduled on {date}");
        }
    }

    #[test]
    fn every_n_days_large_n_starts_immediately() {
        let manager = create_test_manager("America/Los_Angeles");

        let every10 = Rhythm::EveryNDays {
            n: 10,
            at: TEST_AT,
            slider: Slider::default(),
        };

        let now = manager.now();
        let first_beat = every10.next_time_to_fire(now, None);

        assert_eq!(
            first_beat, now,
            "EveryNDays with n >= 7 should start immediately"
        );
    }

    #[test]
    fn rhythm_definition_slide_one_reduces_slider() {
        let rhythm = create_test_rhythm(
            "slide_test",
            Rhythm::Monthly {
                dotm: 14,
                at: TEST_AT,
                slider: Slider::new(3),
            },
        );

        let slid = rhythm.slide_one();
        assert_eq!(slid.rhythm.slider().before, 2);

        let slid2 = slid.slide_one();
        assert_eq!(slid2.rhythm.slider().before, 1);

        let slid3 = slid2.slide_one();
        assert_eq!(slid3.rhythm.slider().before, 0);

        let slid4 = slid3.slide_one();
        assert_eq!(
            slid4.rhythm.slider().before,
            0,
            "Slider should not go below 0"
        );
    }

    #[test]
    fn daily_rhythm_ignores_slider() {
        let daily = Rhythm::Daily { at: TEST_AT };

        assert_eq!(daily.slider().before, 0);

        let with_slider = daily.with_slider(Slider::new(5));
        assert_eq!(
            with_slider.slider().before,
            0,
            "Daily should ignore slider assignment"
        );
    }
}
