use super::*; use crate::models::{BlockContent, ProgrammingBlock, ScheduleConfig}; use crate::value_objects::{FillStrategy, InterstitialRule, MediaFilter, MidRollRule, Weekday}; use chrono::NaiveTime; use std::collections::HashMap; fn make_config_with_blocks( entries: Vec<(Weekday, Vec)>, ) -> ScheduleConfig { let day_blocks: HashMap> = entries.into_iter().collect(); ScheduleConfig::from_day_blocks(day_blocks) } #[test] fn vcalendar_header() { let config = ScheduleConfig::new(); let ical = generate_ical("Test Channel", "Europe/Warsaw", &config); assert!(ical.contains("BEGIN:VCALENDAR\r\n")); assert!(ical.contains("VERSION:2.0\r\n")); assert!(ical.contains("PRODID:-//K-TV//Schedule Export//EN\r\n")); assert!(ical.contains("CALSCALE:GREGORIAN\r\n")); assert!(ical.contains("X-WR-CALNAME:Test Channel\r\n")); assert!(ical.contains("X-WR-TIMEZONE:Europe/Warsaw\r\n")); assert!(ical.contains("END:VCALENDAR\r\n")); } #[test] fn empty_config_produces_no_events() { let config = ScheduleConfig::new(); let ical = generate_ical("Empty", "UTC", &config); assert!(!ical.contains("BEGIN:VEVENT")); } #[test] fn algorithmic_block_produces_vevent() { let block = ProgrammingBlock::new_algorithmic( "Morning Cartoons", NaiveTime::from_hms_opt(8, 0, 0).unwrap(), 120, MediaFilter::default(), FillStrategy::Random, ); let config = make_config_with_blocks(vec![(Weekday::Monday, vec![block.clone()])]); let ical = generate_ical("Kids TV", "UTC", &config); assert!(ical.contains("BEGIN:VEVENT\r\n")); assert!(ical.contains("END:VEVENT\r\n")); assert!(ical.contains("DTSTART:080000\r\n")); assert!(ical.contains("DURATION:PT2H\r\n")); assert!(ical.contains("RRULE:FREQ=WEEKLY;BYDAY=MO\r\n")); assert!(ical.contains("SUMMARY:Morning Cartoons\r\n")); assert!(ical.contains(&format!("UID:{}\r\n", block.id()))); assert!(ical.contains("X-KTV-STRATEGY:random\r\n")); assert!(ical.contains("X-KTV-CONTENT:")); } #[test] fn different_days_produce_different_byday() { let mon_block = ProgrammingBlock::new_algorithmic( "Monday Block", NaiveTime::from_hms_opt(10, 0, 0).unwrap(), 60, MediaFilter::default(), FillStrategy::Sequential, ); let fri_block = ProgrammingBlock::new_algorithmic( "Friday Block", NaiveTime::from_hms_opt(20, 0, 0).unwrap(), 90, MediaFilter::default(), FillStrategy::BestFit, ); let config = make_config_with_blocks(vec![ (Weekday::Monday, vec![mon_block]), (Weekday::Friday, vec![fri_block]), ]); let ical = generate_ical("Multi-Day", "America/New_York", &config); assert!(ical.contains("RRULE:FREQ=WEEKLY;BYDAY=MO\r\n")); assert!(ical.contains("RRULE:FREQ=WEEKLY;BYDAY=FR\r\n")); } #[test] fn manual_block_has_no_strategy() { let block = ProgrammingBlock::new_manual( "Manual Show", NaiveTime::from_hms_opt(14, 30, 0).unwrap(), 45, vec![], ); let config = make_config_with_blocks(vec![(Weekday::Wednesday, vec![block])]); let ical = generate_ical("Manual Ch", "UTC", &config); assert!(ical.contains("SUMMARY:Manual Show\r\n")); assert!(ical.contains("DTSTART:143000\r\n")); assert!(ical.contains("DURATION:PT45M\r\n")); assert!(ical.contains("RRULE:FREQ=WEEKLY;BYDAY=WE\r\n")); assert!(!ical.contains("X-KTV-STRATEGY:")); } #[test] fn duration_with_hours_and_minutes() { let block = ProgrammingBlock::new_manual( "Mixed Duration", NaiveTime::from_hms_opt(0, 0, 0).unwrap(), 150, vec![], ); let config = make_config_with_blocks(vec![(Weekday::Sunday, vec![block])]); let ical = generate_ical("Test", "UTC", &config); assert!(ical.contains("DURATION:PT2H30M\r\n")); assert!(ical.contains("RRULE:FREQ=WEEKLY;BYDAY=SU\r\n")); } #[test] fn all_weekdays_mapped() { let days_and_byday = [ (Weekday::Monday, "MO"), (Weekday::Tuesday, "TU"), (Weekday::Wednesday, "WE"), (Weekday::Thursday, "TH"), (Weekday::Friday, "FR"), (Weekday::Saturday, "SA"), (Weekday::Sunday, "SU"), ]; for (day, byday) in days_and_byday { let block = ProgrammingBlock::new_manual( "Test", NaiveTime::from_hms_opt(12, 0, 0).unwrap(), 60, vec![], ); let config = make_config_with_blocks(vec![(day, vec![block])]); let ical = generate_ical("Test", "UTC", &config); assert!( ical.contains(&format!("BYDAY={}\r\n", byday)), "Expected BYDAY={} for {:?}", byday, day ); } } #[test] fn roundtrip_algorithmic_block() { let block = ProgrammingBlock::new_algorithmic( "Morning Cartoons", NaiveTime::from_hms_opt(8, 0, 0).unwrap(), 120, MediaFilter::default(), FillStrategy::Random, ); let original = make_config_with_blocks(vec![(Weekday::Monday, vec![block.clone()])]); let ical = generate_ical("Test", "UTC", &original); let parsed = parse_ical(&ical).unwrap(); let blocks = parsed.blocks_for(Weekday::Monday); assert_eq!(blocks.len(), 1); let b = &blocks[0]; assert_eq!(b.name(), "Morning Cartoons"); assert_eq!(b.start_time(), NaiveTime::from_hms_opt(8, 0, 0).unwrap()); assert_eq!(b.duration_mins(), 120); assert_eq!(b.id(), block.id()); assert!(matches!(b.content(), BlockContent::Algorithmic { strategy: FillStrategy::Random, .. })); } #[test] fn roundtrip_multi_day() { let mon = ProgrammingBlock::new_algorithmic( "Mon Block", NaiveTime::from_hms_opt(10, 0, 0).unwrap(), 60, MediaFilter::default(), FillStrategy::Sequential, ); let fri = ProgrammingBlock::new_algorithmic( "Fri Block", NaiveTime::from_hms_opt(20, 0, 0).unwrap(), 90, MediaFilter::default(), FillStrategy::BestFit, ); let original = make_config_with_blocks(vec![ (Weekday::Monday, vec![mon]), (Weekday::Friday, vec![fri]), ]); let ical = generate_ical("Multi", "UTC", &original); let parsed = parse_ical(&ical).unwrap(); assert_eq!(parsed.blocks_for(Weekday::Monday).len(), 1); assert_eq!(parsed.blocks_for(Weekday::Friday).len(), 1); assert_eq!(parsed.blocks_for(Weekday::Monday)[0].name(), "Mon Block"); assert_eq!(parsed.blocks_for(Weekday::Friday)[0].name(), "Fri Block"); } #[test] fn roundtrip_manual_block() { let block = ProgrammingBlock::new_manual( "Manual Show", NaiveTime::from_hms_opt(14, 30, 0).unwrap(), 45, vec![], ); let original = make_config_with_blocks(vec![(Weekday::Wednesday, vec![block])]); let ical = generate_ical("Manual", "UTC", &original); let parsed = parse_ical(&ical).unwrap(); let blocks = parsed.blocks_for(Weekday::Wednesday); assert_eq!(blocks.len(), 1); assert_eq!(blocks[0].name(), "Manual Show"); assert_eq!(blocks[0].duration_mins(), 45); assert!(matches!(blocks[0].content(), BlockContent::Manual { .. })); } #[test] fn roundtrip_with_interstitial_and_midroll() { let mut block = ProgrammingBlock::new_algorithmic( "Full Block", NaiveTime::from_hms_opt(20, 0, 0).unwrap(), 180, MediaFilter::default(), FillStrategy::Random, ); let interstitial = InterstitialRule::new(MediaFilter::default(), FillStrategy::Random, 30); let midroll = MidRollRule::new(true, 15, 60, MediaFilter::default()); block = ProgrammingBlock::from_parts( block.id(), block.name().to_string(), block.start_time(), block.duration_mins(), block.content().clone(), Some(interstitial), Some(midroll), ); let original = make_config_with_blocks(vec![(Weekday::Saturday, vec![block])]); let ical = generate_ical("Full", "UTC", &original); let parsed = parse_ical(&ical).unwrap(); let blocks = parsed.blocks_for(Weekday::Saturday); assert_eq!(blocks.len(), 1); assert!(blocks[0].interstitial_rule().is_some()); assert!(blocks[0].mid_roll_rule().is_some()); assert_eq!(blocks[0].interstitial_rule().unwrap().min_gap_secs(), 30); assert!(blocks[0].mid_roll_rule().unwrap().prefer_chapters()); } #[test] fn import_without_ktv_properties_uses_defaults() { let ical = "BEGIN:VCALENDAR\r\n\ VERSION:2.0\r\n\ BEGIN:VEVENT\r\n\ SUMMARY:Generic Event\r\n\ DTSTART:090000\r\n\ DURATION:PT1H\r\n\ RRULE:FREQ=WEEKLY;BYDAY=TU\r\n\ END:VEVENT\r\n\ END:VCALENDAR\r\n"; let parsed = parse_ical(ical).unwrap(); let blocks = parsed.blocks_for(Weekday::Tuesday); assert_eq!(blocks.len(), 1); let b = &blocks[0]; assert_eq!(b.name(), "Generic Event"); assert_eq!(b.start_time(), NaiveTime::from_hms_opt(9, 0, 0).unwrap()); assert_eq!(b.duration_mins(), 60); assert!(matches!( b.content(), BlockContent::Algorithmic { strategy: FillStrategy::Random, .. } )); assert!(b.interstitial_rule().is_none()); assert!(b.mid_roll_rule().is_none()); } #[test] fn import_invalid_ical_missing_vcalendar() { let result = parse_ical("not an ical file"); assert!(result.is_err()); let err = result.unwrap_err().to_string(); assert!(err.contains("BEGIN:VCALENDAR"), "error: {err}"); } #[test] fn import_invalid_ical_bad_duration() { let ical = "BEGIN:VCALENDAR\r\n\ BEGIN:VEVENT\r\n\ SUMMARY:Bad\r\n\ DTSTART:090000\r\n\ DURATION:INVALID\r\n\ RRULE:FREQ=WEEKLY;BYDAY=MO\r\n\ END:VEVENT\r\n\ END:VCALENDAR\r\n"; let result = parse_ical(ical); assert!(result.is_err()); } #[test] fn import_handles_line_folding() { let long_summary = "A".repeat(100); let block = ProgrammingBlock::new_algorithmic( &long_summary, NaiveTime::from_hms_opt(10, 0, 0).unwrap(), 60, MediaFilter::default(), FillStrategy::Random, ); let original = make_config_with_blocks(vec![(Weekday::Thursday, vec![block])]); let ical = generate_ical("Fold Test", "UTC", &original); let parsed = parse_ical(&ical).unwrap(); let blocks = parsed.blocks_for(Weekday::Thursday); assert_eq!(blocks.len(), 1); } #[test] fn import_multiple_byday() { let ical = "BEGIN:VCALENDAR\r\n\ VERSION:2.0\r\n\ BEGIN:VEVENT\r\n\ SUMMARY:Weekday Show\r\n\ DTSTART:180000\r\n\ DURATION:PT2H\r\n\ RRULE:FREQ=WEEKLY;BYDAY=MO,WE,FR\r\n\ END:VEVENT\r\n\ END:VCALENDAR\r\n"; let parsed = parse_ical(ical).unwrap(); assert_eq!(parsed.blocks_for(Weekday::Monday).len(), 1); assert_eq!(parsed.blocks_for(Weekday::Wednesday).len(), 1); assert_eq!(parsed.blocks_for(Weekday::Friday).len(), 1); assert_eq!(parsed.blocks_for(Weekday::Tuesday).len(), 0); }