caldir-cli 0.10.1

CLI for interacting with your local caldir directory and syncing with external calendar providers
use crate::commands::doctor::warning::DoctorWarning;
use caldir_core::{CalendarEvent, EventInstanceId};
use std::collections::HashMap;
use std::path::PathBuf;

pub(crate) fn duplicate_file_warnings(events: &[CalendarEvent]) -> Vec<DoctorWarning> {
    duplicate_file_sets(events)
        .into_iter()
        .map(DoctorWarning::DuplicateFiles)
        .collect()
}

fn duplicate_file_sets(events: &[CalendarEvent]) -> Vec<Vec<PathBuf>> {
    let mut by_id: HashMap<EventInstanceId, Vec<PathBuf>> = HashMap::new();
    for ce in events {
        by_id
            .entry(ce.event().event_instance_id())
            .or_default()
            .push(ce.path().to_path_buf());
    }

    let mut sets: Vec<Vec<PathBuf>> = by_id
        .into_values()
        .filter(|paths| paths.len() > 1)
        .collect();

    for paths in &mut sets {
        paths.sort();
    }
    sets.sort();
    sets
}

#[cfg(test)]
mod tests {
    use super::duplicate_file_warnings;
    use crate::commands::doctor::warning::DoctorWarning;
    use caldir_core::{Calendar, Event, EventTime};
    use chrono::NaiveDate;
    use pretty_assertions::assert_eq;

    fn test_calendar() -> (tempfile::TempDir, Calendar) {
        let tmp = tempfile::tempdir().unwrap();
        let calendar = Calendar::create(&tmp.path().join("work"), None).unwrap();
        (tmp, calendar)
    }

    fn test_event(summary: &str) -> Event {
        Event::new(
            summary,
            EventTime::DateTimeFloating(
                NaiveDate::from_ymd_opt(2026, 1, 1)
                    .unwrap()
                    .and_hms_opt(12, 0, 0)
                    .unwrap(),
            ),
        )
    }

    #[test]
    fn warns_about_multiple_files_with_the_same_event_identity() {
        let (_tmp, calendar) = test_calendar();
        let event = test_event("Standup");

        calendar.create_event(event.clone()).unwrap();
        calendar.create_event(event).unwrap();

        let warnings = duplicate_file_warnings(&calendar.events().unwrap());

        assert_eq!(warnings.len(), 1);
        let DoctorWarning::DuplicateFiles(paths) = &warnings[0] else {
            panic!("expected duplicate file warning");
        };
        assert_eq!(paths.len(), 2);
    }

    #[test]
    fn ignores_distinct_events_with_the_same_filename_slug() {
        let (_tmp, calendar) = test_calendar();

        calendar.create_event(test_event("Standup")).unwrap();
        calendar.create_event(test_event("Standup")).unwrap();

        let warnings = duplicate_file_warnings(&calendar.events().unwrap());

        assert!(warnings.is_empty());
    }
}