use chrono::{TimeZone, Utc};
use truth_engine::expander::ExpandedEvent;
use truth_engine::freebusy::{find_first_free_slot, find_free_slots};
fn event(
year: i32,
month: u32,
day: u32,
start_hour: u32,
start_min: u32,
end_hour: u32,
end_min: u32,
) -> ExpandedEvent {
ExpandedEvent {
start: Utc
.with_ymd_and_hms(year, month, day, start_hour, start_min, 0)
.unwrap(),
end: Utc
.with_ymd_and_hms(year, month, day, end_hour, end_min, 0)
.unwrap(),
}
}
#[test]
fn single_event_produces_two_free_slots() {
let events = vec![event(2026, 3, 1, 10, 0, 11, 0)];
let window_start = Utc.with_ymd_and_hms(2026, 3, 1, 8, 0, 0).unwrap();
let window_end = Utc.with_ymd_and_hms(2026, 3, 1, 17, 0, 0).unwrap();
let slots = find_free_slots(&events, window_start, window_end);
assert_eq!(slots.len(), 2, "single event should produce 2 free slots");
assert_eq!(
slots[0].start,
Utc.with_ymd_and_hms(2026, 3, 1, 8, 0, 0).unwrap()
);
assert_eq!(
slots[0].end,
Utc.with_ymd_and_hms(2026, 3, 1, 10, 0, 0).unwrap()
);
assert_eq!(slots[0].duration_minutes, 120);
assert_eq!(
slots[1].start,
Utc.with_ymd_and_hms(2026, 3, 1, 11, 0, 0).unwrap()
);
assert_eq!(
slots[1].end,
Utc.with_ymd_and_hms(2026, 3, 1, 17, 0, 0).unwrap()
);
assert_eq!(slots[1].duration_minutes, 360);
}
#[test]
fn overlapping_events_merged_correctly() {
let events = vec![
event(2026, 3, 1, 10, 0, 11, 30),
event(2026, 3, 1, 11, 0, 12, 0),
];
let window_start = Utc.with_ymd_and_hms(2026, 3, 1, 8, 0, 0).unwrap();
let window_end = Utc.with_ymd_and_hms(2026, 3, 1, 17, 0, 0).unwrap();
let slots = find_free_slots(&events, window_start, window_end);
assert_eq!(
slots.len(),
2,
"overlapping events should merge into one busy block"
);
assert_eq!(
slots[0].start,
Utc.with_ymd_and_hms(2026, 3, 1, 8, 0, 0).unwrap()
);
assert_eq!(
slots[0].end,
Utc.with_ymd_and_hms(2026, 3, 1, 10, 0, 0).unwrap()
);
assert_eq!(slots[0].duration_minutes, 120);
assert_eq!(
slots[1].start,
Utc.with_ymd_and_hms(2026, 3, 1, 12, 0, 0).unwrap()
);
assert_eq!(
slots[1].end,
Utc.with_ymd_and_hms(2026, 3, 1, 17, 0, 0).unwrap()
);
assert_eq!(slots[1].duration_minutes, 300);
}
#[test]
fn no_events_entire_window_is_free() {
let window_start = Utc.with_ymd_and_hms(2026, 3, 1, 8, 0, 0).unwrap();
let window_end = Utc.with_ymd_and_hms(2026, 3, 1, 17, 0, 0).unwrap();
let slots = find_free_slots(&[], window_start, window_end);
assert_eq!(slots.len(), 1, "no events should produce one free slot");
assert_eq!(slots[0].start, window_start);
assert_eq!(slots[0].end, window_end);
assert_eq!(slots[0].duration_minutes, 540); }
#[test]
fn find_first_free_slot_with_minimum_duration() {
let events = vec![
event(2026, 3, 1, 8, 0, 8, 30),
event(2026, 3, 1, 9, 0, 12, 0),
];
let window_start = Utc.with_ymd_and_hms(2026, 3, 1, 8, 0, 0).unwrap();
let window_end = Utc.with_ymd_and_hms(2026, 3, 1, 17, 0, 0).unwrap();
let slot = find_first_free_slot(&events, window_start, window_end, 60);
assert!(slot.is_some(), "should find a free slot of at least 60 min");
let slot = slot.unwrap();
assert_eq!(
slot.start,
Utc.with_ymd_and_hms(2026, 3, 1, 12, 0, 0).unwrap()
);
assert_eq!(
slot.end,
Utc.with_ymd_and_hms(2026, 3, 1, 17, 0, 0).unwrap()
);
assert_eq!(slot.duration_minutes, 300);
}
#[test]
fn events_filling_entire_window_no_free_slots() {
let events = vec![event(2026, 3, 1, 9, 0, 12, 0)];
let window_start = Utc.with_ymd_and_hms(2026, 3, 1, 9, 0, 0).unwrap();
let window_end = Utc.with_ymd_and_hms(2026, 3, 1, 12, 0, 0).unwrap();
let slots = find_free_slots(&events, window_start, window_end);
assert!(
slots.is_empty(),
"events filling entire window should produce no free slots"
);
}
#[test]
fn find_first_free_slot_no_gap_large_enough() {
let events = vec![
event(2026, 3, 1, 9, 0, 10, 0),
event(2026, 3, 1, 10, 15, 12, 0),
];
let window_start = Utc.with_ymd_and_hms(2026, 3, 1, 9, 0, 0).unwrap();
let window_end = Utc.with_ymd_and_hms(2026, 3, 1, 12, 0, 0).unwrap();
let slot = find_first_free_slot(&events, window_start, window_end, 60);
assert!(slot.is_none(), "no gap large enough should return None");
}
#[test]
fn multiple_gaps_between_events() {
let events = vec![
event(2026, 3, 1, 9, 0, 10, 0),
event(2026, 3, 1, 12, 0, 13, 0),
event(2026, 3, 1, 15, 0, 16, 0),
];
let window_start = Utc.with_ymd_and_hms(2026, 3, 1, 8, 0, 0).unwrap();
let window_end = Utc.with_ymd_and_hms(2026, 3, 1, 18, 0, 0).unwrap();
let slots = find_free_slots(&events, window_start, window_end);
assert_eq!(slots.len(), 4, "should find 4 free slots between 3 events");
assert_eq!(slots[0].duration_minutes, 60); assert_eq!(slots[1].duration_minutes, 120); assert_eq!(slots[2].duration_minutes, 120); assert_eq!(slots[3].duration_minutes, 120); }