use chrono::{DateTime, Duration, Local, TimeZone, Utc};
use std::sync::Arc;
use tiny_counter::{EventStore, TestClock};
fn local_offset_hours(utc_time: DateTime<Utc>) -> f64 {
let offset_seconds = Local
.timestamp_opt(utc_time.timestamp(), 0)
.unwrap()
.offset()
.local_minus_utc();
offset_seconds as f64 / 3600.0
}
#[test]
fn last_seen_very_recent_event() {
let fixed_time = Utc.with_ymd_and_hms(2025, 3, 1, 12, 0, 0).unwrap();
let clock = TestClock::build_for_testing_at(fixed_time);
let store = EventStore::builder()
.track_seconds(60)
.track_minutes(60)
.with_clock(Arc::new(clock.clone()))
.build()
.unwrap();
store.record("event");
let last = store.query("event").last_seen();
assert!(last.is_some());
let duration = last.unwrap();
assert!(duration.num_seconds() < 60);
}
#[test]
fn last_seen_uses_finest_granularity() {
let fixed_time = Utc.with_ymd_and_hms(2025, 3, 1, 12, 0, 0).unwrap();
let clock = TestClock::build_for_testing_at(fixed_time);
let store = EventStore::builder()
.track_minutes(60)
.track_hours(24)
.track_days(7)
.with_clock(Arc::new(clock.clone()))
.build()
.unwrap();
store.record_ago("event", Duration::minutes(5));
let last = store.query("event").last_seen();
assert!(last.is_some());
let expected = Duration::seconds(330);
assert_eq!(
last,
Some(expected),
"Should return exact minute bucket midpoint"
);
}
#[test]
fn last_seen_with_multiple_events_returns_most_recent() {
let fixed_time = Utc.with_ymd_and_hms(2025, 3, 1, 12, 0, 0).unwrap();
let clock = TestClock::build_for_testing_at(fixed_time);
let store = EventStore::builder()
.track_hours(48)
.track_days(7)
.with_clock(Arc::new(clock.clone()))
.build()
.unwrap();
store.record_ago("event", Duration::days(3));
store.record_ago("event", Duration::hours(2));
store.record_ago("event", Duration::minutes(30));
let last = store.query("event").last_seen();
assert!(last.is_some());
let duration = last.unwrap();
assert!(duration.num_hours() <= 2);
}
#[test]
fn last_seen_falls_back_to_coarser_interval() {
let fixed_time = Utc.with_ymd_and_hms(2025, 3, 1, 12, 0, 0).unwrap();
let clock = TestClock::build_for_testing_at(fixed_time);
let store = EventStore::builder()
.track_minutes(60)
.track_hours(48)
.with_clock(Arc::new(clock.clone()))
.build()
.unwrap();
store.record_ago("event", Duration::hours(2));
let last = store.query("event").last_seen();
assert!(last.is_some());
let expected = Duration::seconds(9000);
assert_eq!(
last,
Some(expected),
"Should return exact hour bucket midpoint"
);
}
#[test]
#[cfg(not(feature = "calendar"))]
fn last_seen_in_coarsest_interval_only() {
let fixed_time = Utc.with_ymd_and_hms(2025, 3, 1, 12, 0, 0).unwrap();
let clock = TestClock::build_for_testing_at(fixed_time);
let store = EventStore::builder()
.track_days(30)
.track_weeks(52)
.track_months(24)
.with_clock(Arc::new(clock.clone()))
.build()
.unwrap();
store.record_ago("event", Duration::days(60));
let event_time = fixed_time - Duration::days(60);
let last = store.query("event").last_seen();
assert!(last.is_some());
let duration = last.unwrap();
let days = duration.num_days();
let hours = duration.num_hours();
let seconds = duration.num_seconds();
assert_eq!(
seconds, 5443200,
"Should be exactly 5443200 seconds (63 days)"
);
assert_eq!(days, 63, "Uniform buckets return 63 days (9 weeks exactly)");
}
#[test]
#[cfg(feature = "calendar")]
fn last_seen_in_coarsest_interval_only_calendar() {
let fixed_time = Utc.with_ymd_and_hms(2025, 3, 1, 12, 0, 0).unwrap();
let clock = TestClock::build_for_testing_at(fixed_time);
let store = EventStore::builder()
.track_days(30)
.track_weeks(52)
.track_months(24)
.with_clock(Arc::new(clock.clone()))
.build()
.unwrap();
store.record_ago("event", Duration::days(60));
let last = store.query("event").last_seen();
assert!(last.is_some());
let offset = local_offset_hours(fixed_time);
let duration = last.unwrap();
let hours = duration.num_hours();
let expected_hours = 1560.0 - offset * 1.0;
assert!(
((expected_hours - 1.0) as i64..=(expected_hours + 1.0) as i64).contains(&hours),
"Expected ~{:.0} hours (±1h) based on UTC{:+.1} offset, got {} hours",
expected_hours,
offset,
hours
);
}
#[test]
#[cfg(not(feature = "calendar"))]
fn last_seen_gap_between_intervals() {
let fixed_time = Utc.with_ymd_and_hms(2025, 3, 1, 12, 0, 0).unwrap();
let clock = TestClock::build_for_testing_at(fixed_time);
let store = EventStore::builder()
.track_minutes(60)
.track_days(7)
.with_clock(Arc::new(clock.clone()))
.build()
.unwrap();
store.record_ago("event", Duration::hours(12));
let last = store.query("event").last_seen();
assert!(last.is_some());
let expected = Duration::seconds(45000);
assert_eq!(
last,
Some(expected),
"Should return exact calculated midpoint with gap detection"
);
}
#[test]
#[cfg(feature = "calendar")]
fn last_seen_gap_between_intervals_calendar() {
let fixed_time = Utc.with_ymd_and_hms(2025, 3, 1, 12, 0, 0).unwrap();
let clock = TestClock::build_for_testing_at(fixed_time);
let store = EventStore::builder()
.track_minutes(60)
.track_days(7)
.with_clock(Arc::new(clock.clone()))
.build()
.unwrap();
store.record_ago("event", Duration::hours(12));
let last = store.query("event").last_seen();
assert!(last.is_some());
let offset = local_offset_hours(fixed_time);
let expected_hours = 18.0 - offset * 1.0;
let duration = last.unwrap();
let hours = duration.num_hours();
assert!(
((expected_hours - 1.0) as i64..=(expected_hours + 1.0) as i64).contains(&hours),
"Expected ~{:.0} hours (±1h) based on UTC{:+.1} offset, got {} hours",
expected_hours,
offset,
hours
);
}
#[test]
#[cfg(not(feature = "calendar"))]
fn last_seen_at_exact_interval_boundary() {
let fixed_time = Utc.with_ymd_and_hms(2025, 3, 1, 12, 0, 0).unwrap();
let clock = TestClock::build_for_testing_at(fixed_time);
let store = EventStore::builder()
.track_hours(24)
.track_days(7)
.with_clock(Arc::new(clock.clone()))
.build()
.unwrap();
store.record_ago("event", Duration::hours(24));
let last = store.query("event").last_seen();
assert!(last.is_some());
let expected = Duration::seconds(129600);
assert_eq!(
last,
Some(expected),
"Should return exact boundary midpoint"
);
}
#[test]
#[cfg(feature = "calendar")]
fn last_seen_at_exact_interval_boundary_calendar() {
let fixed_time = Utc.with_ymd_and_hms(2025, 3, 1, 12, 0, 0).unwrap();
let clock = TestClock::build_for_testing_at(fixed_time);
let store = EventStore::builder()
.track_hours(24)
.track_days(7)
.with_clock(Arc::new(clock.clone()))
.build()
.unwrap();
store.record_ago("event", Duration::hours(24));
let last = store.query("event").last_seen();
assert!(last.is_some());
let offset = local_offset_hours(fixed_time);
let expected_hours = 42.0 - offset * 1.0;
let duration = last.unwrap();
let hours = duration.num_hours();
assert!(
((expected_hours - 1.0) as i64..=(expected_hours + 1.0) as i64).contains(&hours),
"Expected ~{:.0} hours (±1h) based on UTC{:+.1} offset, got {} hours",
expected_hours,
offset,
hours
);
}
#[test]
fn last_seen_for_nonexistent_event() {
let store = EventStore::new();
let last = store.query("nonexistent").last_seen();
assert_eq!(last, None);
}
#[test]
fn last_seen_after_event_rotates_out() {
let fixed_time = Utc.with_ymd_and_hms(2025, 3, 1, 12, 0, 0).unwrap();
let clock = TestClock::build_for_testing_at(fixed_time);
let store = EventStore::builder()
.track_hours(24)
.with_clock(Arc::new(clock.clone()))
.build()
.unwrap();
store.record("event");
clock.advance(Duration::hours(48));
store.record("trigger");
let last = store.query("event").last_seen();
assert_eq!(last, None);
}
#[test]
fn last_seen_equals_first_seen_for_single_event() {
let fixed_time = Utc.with_ymd_and_hms(2025, 3, 1, 12, 0, 0).unwrap();
let clock = TestClock::build_for_testing_at(fixed_time);
let store = EventStore::builder()
.track_minutes(60)
.track_hours(24)
.with_clock(Arc::new(clock.clone()))
.build()
.unwrap();
store.record_ago("event", Duration::minutes(15));
let first = store.query("event").first_seen();
let last = store.query("event").last_seen();
assert!(first.is_some());
assert!(last.is_some());
let expected = Duration::seconds(930);
assert_eq!(first, Some(expected), "first_seen should be exact");
assert_eq!(last, Some(expected), "last_seen should be exact");
assert_eq!(
first, last,
"For single event, first and last must be identical"
);
}
#[test]
#[cfg(not(feature = "calendar"))]
fn last_seen_more_recent_than_first_seen() {
let fixed_time = Utc.with_ymd_and_hms(2025, 3, 1, 12, 0, 0).unwrap();
let clock = TestClock::build_for_testing_at(fixed_time);
let store = EventStore::builder()
.track_hours(48)
.track_days(7)
.with_clock(Arc::new(clock.clone()))
.build()
.unwrap();
store.record_ago("event", Duration::days(3));
store.record_ago("event", Duration::hours(2));
let first = store.query("event").first_seen();
let last = store.query("event").last_seen();
let expected_first = Duration::seconds(194400);
let expected_last = Duration::seconds(9000);
assert_eq!(first, Some(expected_first), "first_seen should be exact");
assert_eq!(last, Some(expected_last), "last_seen should be exact");
assert!(
last.unwrap() < first.unwrap(),
"Last must be more recent than first"
);
}
#[test]
#[cfg(feature = "calendar")]
fn last_seen_more_recent_than_first_seen_calendar() {
let fixed_time = Utc.with_ymd_and_hms(2025, 3, 1, 12, 0, 0).unwrap();
let clock = TestClock::build_for_testing_at(fixed_time);
let store = EventStore::builder()
.track_hours(48)
.track_days(7)
.with_clock(Arc::new(clock.clone()))
.build()
.unwrap();
store.record_ago("event", Duration::days(3));
store.record_ago("event", Duration::hours(2));
let first = store.query("event").first_seen();
let last = store.query("event").last_seen();
assert!(first.is_some(), "first_seen should exist");
assert!(last.is_some(), "last_seen should exist");
assert!(
last.unwrap() < first.unwrap(),
"Last must be more recent than first"
);
let offset = local_offset_hours(fixed_time);
let first_hours = first.unwrap().num_hours();
let last_hours = last.unwrap().num_hours();
let expected_first_hours = 66.0 - offset * 1.0;
assert!(
((expected_first_hours - 1.0) as i64..=(expected_first_hours + 1.0) as i64)
.contains(&first_hours),
"Expected first_seen ~{:.0} hours (±1h) based on UTC{:+.1} offset, got {} hours",
expected_first_hours,
offset,
first_hours
);
assert!(
(1..=4).contains(&last_hours),
"Expected last_seen ~2 hours (stable, in hours bucket), got {} hours",
last_hours
);
}
#[test]
fn last_seen_updates_as_time_advances() {
let fixed_time = Utc.with_ymd_and_hms(2025, 3, 1, 12, 0, 0).unwrap();
let clock = TestClock::build_for_testing_at(fixed_time);
let store = EventStore::builder()
.track_minutes(60)
.with_clock(Arc::new(clock.clone()))
.build()
.unwrap();
store.record("event");
let last1 = store.query("event").last_seen();
clock.advance(Duration::minutes(10));
let last2 = store.query("event").last_seen();
assert!(last1.is_some());
assert!(last2.is_some());
assert!(last2.unwrap() > last1.unwrap());
let diff = last2.unwrap().num_minutes() - last1.unwrap().num_minutes();
assert!((8..=12).contains(&diff)); }
#[test]
fn last_seen_bucket_midpoint_estimation() {
let fixed_time = Utc.with_ymd_and_hms(2025, 3, 1, 12, 0, 0).unwrap();
let clock = TestClock::build_for_testing_at(fixed_time);
let store = EventStore::builder()
.track_hours(24)
.with_clock(Arc::new(clock.clone()))
.build()
.unwrap();
store.record("event");
clock.advance(Duration::minutes(90));
let last = store.query("event").last_seen();
assert!(last.is_some());
let expected = Duration::seconds(7200);
assert_eq!(
last,
Some(expected),
"Should return exact bucket midpoint estimation"
);
}