use std::fs;
use std::io::Write;
use chrono::{TimeZone, Utc};
use chrono_tz::Tz;
use tempfile::NamedTempFile;
use crate::brain::timezone::{UserTimezoneCache, parse_timezone_heuristic};
#[test]
fn parse_standard_english_timezone() {
let user_md = "\
# USER
- **Name:** Alexey
- **Timezone:** UTC+3 (MSK)
";
let info = parse_timezone_heuristic(user_md).expect("should parse");
assert_eq!(info.label.as_deref(), Some("MSK"));
let dt = Utc.with_ymd_and_hms(2026, 9, 10, 12, 0, 0).unwrap();
let formatted = info.format_dual_time(&dt);
assert_eq!(formatted, "2026-09-10 12:00:00 UTC (user: 15:00:00 MSK)");
}
#[test]
fn parse_russian_timezone_format() {
let user_md = "\
# Профиль
- **Часовой пояс:** Москва (UTC+3)
";
let info = parse_timezone_heuristic(user_md).expect("should parse");
assert_eq!(info.label.as_deref(), Some("Москва"));
let dt = Utc.with_ymd_and_hms(2026, 1, 15, 10, 0, 0).unwrap();
let formatted = info.format_dual_time(&dt);
assert_eq!(formatted, "2026-01-15 10:00:00 UTC (user: 13:00:00 Москва)");
}
#[test]
fn fallback_scan_carries_declared_label() {
let user_md = "\
# Notes
Working hours — Europe/Moscow (МСК)
";
let info = parse_timezone_heuristic(user_md).expect("fallback scan should resolve");
assert_eq!(info.tz, Tz::Europe__Moscow);
assert_eq!(info.label.as_deref(), Some("МСК"));
}
#[test]
fn fallback_scan_ignores_zone_form_parenthetical() {
let user_md = "\
# Notes
Standup — Europe/Moscow (UTC+3)
";
let info = parse_timezone_heuristic(user_md).expect("fallback scan should resolve");
assert_eq!(info.tz, Tz::Europe__Moscow);
assert_eq!(info.label, None);
}
#[test]
fn parse_iana_timezone_with_dst_transitions() {
let user_md = "\
# USER
Timezone: Europe/Paris
";
let info = parse_timezone_heuristic(user_md).expect("should parse");
assert_eq!(info.tz, Tz::Europe__Paris);
let summer_dt = Utc.with_ymd_and_hms(2026, 7, 1, 12, 0, 0).unwrap();
let summer_fmt = info.format_dual_time(&summer_dt);
assert_eq!(
summer_fmt,
"2026-07-01 12:00:00 UTC (user: 14:00:00 Europe/Paris)"
);
let winter_dt = Utc.with_ymd_and_hms(2026, 12, 1, 12, 0, 0).unwrap();
let winter_fmt = info.format_dual_time(&winter_dt);
assert_eq!(
winter_fmt,
"2026-12-01 12:00:00 UTC (user: 13:00:00 Europe/Paris)"
);
}
#[test]
fn test_caching_and_invalidation() {
let mut tmp = NamedTempFile::new().unwrap();
writeln!(tmp, "Timezone: UTC+2 (EET)").unwrap();
tmp.flush().unwrap();
let cache = UserTimezoneCache::new();
let res1 = cache.resolve_from_file(tmp.path()).expect("first resolve");
assert_eq!(res1.label.as_deref(), Some("EET"));
std::thread::sleep(std::time::Duration::from_millis(15));
let mut file = fs::OpenOptions::new()
.write(true)
.truncate(true)
.open(tmp.path())
.unwrap();
writeln!(file, "Timezone: UTC+7 (NOVT)").unwrap();
file.flush().unwrap();
let res2 = cache.resolve_from_file(tmp.path()).expect("second resolve");
assert_eq!(res2.label.as_deref(), Some("NOVT"));
}
#[test]
fn test_negative_caching_on_no_match() {
let mut tmp = NamedTempFile::new().unwrap();
write!(tmp, "# Just notes\nNo timezone mentioned here.\n").unwrap();
tmp.flush().unwrap();
let cache = UserTimezoneCache::new();
assert!(cache.resolve_from_file(tmp.path()).is_none());
assert!(cache.resolve_from_file(tmp.path()).is_none());
}
#[test]
fn prose_mention_of_timezone_mid_sentence_does_not_match() {
let user_md = "\
# Notes
My timezone: is a personal matter, ask me instead.
";
assert!(parse_timezone_heuristic(user_md).is_none());
}
#[test]
fn parse_moscow_city_with_abbreviation_label() {
let user_md = "\
# Профиль
- **Часовой пояс:** Москва (МСК)
";
let info = parse_timezone_heuristic(user_md).expect("should parse");
assert_eq!(info.tz, Tz::Europe__Moscow);
assert_eq!(info.label.as_deref(), Some("МСК"));
}
#[test]
fn parse_bare_moscow_abbreviation() {
let user_md = "\
# Профиль
**Часовой пояс:** МСК
";
let info = parse_timezone_heuristic(user_md).expect("should parse");
assert_eq!(info.tz, Tz::Europe__Moscow);
assert_eq!(info.label.as_deref(), Some("МСК"));
}