use cfait::model::parser::parse_duration_range;
use cfait::model::{DateType, Task};
use chrono::{TimeZone, Utc};
use std::collections::HashMap;
fn parse(input: &str) -> Task {
let aliases = HashMap::new();
Task::new(input, &aliases, None)
}
#[test]
fn test_parse_duration_range_single_value() {
assert_eq!(parse_duration_range("30m"), Some((30, None)));
assert_eq!(parse_duration_range("1h"), Some((60, None)));
assert_eq!(parse_duration_range("2d"), Some((2880, None)));
}
#[test]
fn test_parse_duration_range_valid_range() {
assert_eq!(parse_duration_range("30m-1h"), Some((30, Some(60))));
assert_eq!(parse_duration_range("1h-2h"), Some((60, Some(120))));
assert_eq!(parse_duration_range("30m-90m"), Some((30, Some(90))));
}
#[test]
fn test_parse_duration_range_equal_values() {
assert_eq!(parse_duration_range("30m-30m"), Some((30, Some(30))));
}
#[test]
fn test_parse_duration_range_invalid_order() {
assert_eq!(parse_duration_range("1h-30m"), Some((60, None)));
}
#[test]
fn test_parse_duration_range_invalid_input() {
assert_eq!(parse_duration_range(""), None);
assert_eq!(parse_duration_range("invalid"), None);
assert_eq!(parse_duration_range("-"), None);
}
#[test]
fn test_parse_duration_range_smart_input_single() {
let t = parse("Task ~30m");
assert_eq!(t.estimated_duration, Some(30));
assert_eq!(t.estimated_duration_max, None);
}
#[test]
fn test_parse_duration_range_smart_input_valid_range() {
let t = parse("Task ~30m-1h");
assert_eq!(t.estimated_duration, Some(30));
assert_eq!(t.estimated_duration_max, Some(60));
}
#[test]
fn test_parse_duration_range_smart_input_with_est() {
let t = parse("Task est:30m-1h");
assert_eq!(t.estimated_duration, Some(30));
assert_eq!(t.estimated_duration_max, Some(60));
}
#[test]
fn test_duration_filter_point_query_in_range() {
let mut t = parse("Task");
t.estimated_duration = Some(30);
t.estimated_duration_max = Some(60);
assert!(
t.matches_search_term("~30m"),
"Should match ~30m (in range)"
);
assert!(
t.matches_search_term("~45m"),
"Should match ~45m (in range)"
);
assert!(t.matches_search_term("~1h"), "Should match ~1h (in range)");
}
#[test]
fn test_duration_filter_point_query_outside_range() {
let mut t = parse("Task");
t.estimated_duration = Some(30);
t.estimated_duration_max = Some(60);
assert!(
!t.matches_search_term("~15m"),
"Should NOT match ~15m (below range)"
);
assert!(
!t.matches_search_term("~2h"),
"Should NOT match ~2h (above range)"
);
}
#[test]
fn test_duration_filter_point_estimate() {
let mut t = parse("Task");
t.estimated_duration = Some(30);
t.estimated_duration_max = None;
assert!(t.matches_search_term("~30m"), "Should match ~30m (exact)");
assert!(!t.matches_search_term("~29m"), "Should NOT match ~29m");
assert!(!t.matches_search_term("~31m"), "Should NOT match ~31m");
}
#[test]
fn test_duration_filter_less_than() {
let mut t = parse("Task");
t.estimated_duration = Some(30);
t.estimated_duration_max = Some(60);
assert!(t.matches_search_term("~<1h"), "Task [30m-1h] can be < 1h");
}
#[test]
fn test_duration_filter_less_than_fail() {
let mut t = parse("Task");
t.estimated_duration = Some(30);
t.estimated_duration_max = Some(60);
assert!(
!t.matches_search_term("~<30m"),
"Task [30m-1h] cannot be < 30m"
);
}
#[test]
fn test_duration_filter_greater_than() {
let mut t = parse("Task");
t.estimated_duration = Some(30);
t.estimated_duration_max = Some(60);
assert!(t.matches_search_term("~>30m"), "Task [30m-1h] can be > 30m");
}
#[test]
fn test_duration_filter_greater_than_fail() {
let mut t = parse("Task");
t.estimated_duration = Some(30);
t.estimated_duration_max = Some(60);
assert!(
!t.matches_search_term("~>1h"),
"Task [30m-1h] cannot be > 1h"
);
}
#[test]
fn test_duration_filter_less_than_or_equal() {
let mut t = parse("Task");
t.estimated_duration = Some(30);
t.estimated_duration_max = Some(60);
assert!(t.matches_search_term("~<=1h"), "Task [30m-1h] can be <= 1h");
}
#[test]
fn test_duration_filter_less_than_or_equal_fail() {
let mut t = parse("Task");
t.estimated_duration = Some(31);
t.estimated_duration_max = Some(60);
assert!(
!t.matches_search_term("~<=30m"),
"Task [31m-1h] cannot be <= 30m"
);
}
#[test]
fn test_duration_filter_greater_than_or_equal() {
let mut t = parse("Task");
t.estimated_duration = Some(30);
t.estimated_duration_max = Some(60);
assert!(
t.matches_search_term("~>=30m"),
"Task [30m-1h] can be >= 30m"
);
}
#[test]
fn test_duration_filter_greater_than_or_equal_fail() {
let mut t = parse("Task");
t.estimated_duration = Some(30);
t.estimated_duration_max = Some(59);
assert!(
!t.matches_search_term("~>=1h"),
"Task [30m-59m] cannot be >= 1h"
);
}
#[test]
fn test_duration_filter_no_duration() {
let t = parse("Task");
assert!(
!t.matches_search_term("~30m"),
"Task without duration should NOT match ~30m"
);
assert!(
!t.matches_search_term("~<1h"),
"Task without duration should NOT match ~<1h"
);
}
#[test]
fn test_ics_roundtrip_with_max_duration() {
let mut original = parse("Task with range");
original.estimated_duration = Some(30);
original.estimated_duration_max = Some(60);
let ics = original.to_ics();
assert!(
ics.contains("DURATION:PT30M"),
"ICS should contain DURATION:PT30M. Got:\n{}",
ics
);
assert!(
ics.contains("X-CFAIT-ESTIMATED-DURATION-MAX:PT60M"),
"ICS should contain X-CFAIT-ESTIMATED-DURATION-MAX:PT60M. Got:\n{}",
ics
);
let restored =
Task::from_ics(&ics, "e1".to_string(), "h1".to_string(), "c1".to_string()).unwrap();
assert_eq!(
restored.estimated_duration,
Some(30),
"Restored estimated_duration should be 30"
);
assert_eq!(
restored.estimated_duration_max,
Some(60),
"Restored estimated_duration_max should be 60"
);
}
#[test]
fn test_ics_roundtrip_without_max_duration() {
let mut original = parse("Task with single duration");
original.estimated_duration = Some(30);
original.estimated_duration_max = None;
let ics = original.to_ics();
assert!(
!ics.contains("X-CFAIT-ESTIMATED-DURATION-MAX"),
"ICS should NOT contain X-CFAIT-ESTIMATED-DURATION-MAX when max is None. Got:\n{}",
ics
);
assert!(
ics.contains("DURATION:PT30M"),
"ICS should contain DURATION:PT30M. Got:\n{}",
ics
);
let restored =
Task::from_ics(&ics, "e1".to_string(), "h1".to_string(), "c1".to_string()).unwrap();
assert_eq!(restored.estimated_duration, Some(30));
assert_eq!(restored.estimated_duration_max, None);
}
#[test]
fn test_parse_ics_with_max_duration() {
let ics = r#"BEGIN:VCALENDAR
VERSION:2.0
PRODID:-//Cfait//Test//EN
BEGIN:VTODO
UID:test123
SUMMARY:Task with range
X-ESTIMATED-DURATION:PT30M
X-CFAIT-ESTIMATED-DURATION-MAX:PT60M
STATUS:NEEDS-ACTION
END:VTODO
END:VCALENDAR"#;
let task = Task::from_ics(ics, "e1".to_string(), "h1".to_string(), "c1".to_string()).unwrap();
assert_eq!(task.summary, "Task with range");
assert_eq!(task.estimated_duration, Some(30));
assert_eq!(task.estimated_duration_max, Some(60));
}
#[test]
fn test_parse_ics_without_max_duration() {
let ics = r#"BEGIN:VCALENDAR
VERSION:2.0
PRODID:-//Cfait//Test//EN
BEGIN:VTODO
UID:test123
SUMMARY:Task with single duration
X-ESTIMATED-DURATION:PT30M
STATUS:NEEDS-ACTION
END:VTODO
END:VCALENDAR"#;
let task = Task::from_ics(ics, "e1".to_string(), "h1".to_string(), "c1".to_string()).unwrap();
assert_eq!(task.summary, "Task with single duration");
assert_eq!(task.estimated_duration, Some(30));
assert_eq!(task.estimated_duration_max, None);
}
#[test]
fn test_ics_roundtrip_with_max_and_due_date() {
let mut original = parse("Task with range and due");
original.estimated_duration = Some(30);
original.estimated_duration_max = Some(60);
original.due = Some(DateType::Specific(
Utc.with_ymd_and_hms(2025, 2, 15, 14, 0, 0).unwrap(),
));
let ics = original.to_ics();
assert!(
ics.contains("X-ESTIMATED-DURATION:PT30M"),
"ICS should contain X-ESTIMATED-DURATION:PT30M when due is present. Got:\n{}",
ics
);
assert!(
ics.contains("X-CFAIT-ESTIMATED-DURATION-MAX:PT60M"),
"ICS should contain X-CFAIT-ESTIMATED-DURATION-MAX:PT60M. Got:\n{}",
ics
);
assert!(
!ics.contains("\nDURATION:"),
"ICS should NOT contain DURATION when due is present. Got:\n{}",
ics
);
let restored =
Task::from_ics(&ics, "e1".to_string(), "h1".to_string(), "c1".to_string()).unwrap();
assert_eq!(restored.estimated_duration, Some(30));
assert_eq!(restored.estimated_duration_max, Some(60));
}
#[test]
fn test_to_smart_string_with_range() {
let mut t = parse("Task");
t.summary = "Task with range".to_string();
t.estimated_duration = Some(30);
t.estimated_duration_max = Some(60);
let smart = t.to_smart_string();
assert!(
smart.contains("~30m-1h"),
"Smart string should contain ~30m-1h. Got: {}",
smart
);
}
#[test]
fn test_to_smart_string_single_duration() {
let mut t = parse("Task");
t.summary = "Task with single".to_string();
t.estimated_duration = Some(30);
t.estimated_duration_max = None;
let smart = t.to_smart_string();
assert!(
smart.contains("~30m") && !smart.contains("~30m-"),
"Smart string should contain ~30m but NOT range syntax. Got: {}",
smart
);
}
#[test]
fn test_to_smart_string_no_duration() {
let t = parse("Task without duration");
let smart = t.to_smart_string();
assert!(
!smart.contains("~"),
"Smart string should NOT contain ~ when no duration. Got: {}",
smart
);
}
#[test]
fn test_format_duration_short_range() {
let mut t = parse("Task");
t.estimated_duration = Some(30);
t.estimated_duration_max = Some(60);
let formatted = t.format_duration_short();
assert_eq!(formatted, "[~30m-1h]");
}
#[test]
fn test_format_duration_short_single() {
let mut t = parse("Task");
t.estimated_duration = Some(30);
t.estimated_duration_max = None;
let formatted = t.format_duration_short();
assert_eq!(formatted, "[~30m]");
}
#[test]
fn test_format_duration_short_none() {
let t = parse("Task");
let formatted = t.format_duration_short();
assert_eq!(formatted, "");
}