tiny-counter 0.1.0

Track event counts across time windows with fixed memory and fast queries
Documentation
//! Tests for recording events - all recording methods and operations

use chrono::{Duration, NaiveDate, Utc};
use tiny_counter::EventStore;

// ============================================================================
// Recording Events - Basic Operations
// ============================================================================

#[test]
fn record_single_event() {
    let store = EventStore::new();

    store.record("app_launch");

    let count = store.query("app_launch").last_days(1).sum();
    assert_eq!(count, Some(1));
}

#[test]
fn record_multiple_events_accumulate() {
    let store = EventStore::new();

    store.record("button_click");
    store.record("button_click");
    store.record("button_click");

    let count = store.query("button_click").last_days(1).sum();
    assert_eq!(count, Some(3));
}

#[test]
fn record_count_adds_multiple() {
    let store = EventStore::new();

    store.record_count("api_call", 10);
    store.record_count("api_call", 20);

    let count = store.query("api_call").last_days(1).sum();
    assert_eq!(count, Some(30));
}

#[test]
fn record_different_events_independently() {
    let store = EventStore::new();

    store.record("event_a");
    store.record("event_a");
    store.record("event_b");
    store.record("event_b");
    store.record("event_b");

    assert_eq!(store.query("event_a").last_days(1).sum(), Some(2));
    assert_eq!(store.query("event_b").last_days(1).sum(), Some(3));
}

#[test]
fn record_creates_event_automatically() {
    let store = EventStore::new();

    // No need to "create" event first, recording creates it
    store.record("new_event");

    assert_eq!(store.query("new_event").last_days(1).sum(), Some(1));
}

#[test]
fn record_zero_count_is_noop() {
    let store = EventStore::new();

    store.record_count("event", 0);

    // Zero count creates the event but with 0 count
    assert_eq!(store.query("event").last_days(1).sum(), Some(0));
}

// ============================================================================
// Recording Events - Time-Based Operations
// ============================================================================

#[test]
fn record_at_specific_time() {
    let store = EventStore::new();

    let two_days_ago = Utc::now() - Duration::days(2);
    store.record_at("event", two_days_ago).unwrap();

    // Should not appear in last day
    assert_eq!(store.query("event").last_days(1).sum(), Some(0));

    // Should appear in last 7 days
    assert_eq!(store.query("event").last_days(7).sum(), Some(1));
}

#[test]
fn record_count_at_specific_time() {
    let store = EventStore::new();

    let yesterday = Utc::now() - Duration::days(1);
    store.record_count_at("batch_event", 50, yesterday).unwrap();

    assert_eq!(store.query("batch_event").last_days(7).sum(), Some(50));
}

#[test]
fn record_ago_relative_duration() {
    let store = EventStore::new();

    // Events within the same day might need hours interval
    // Test with both hours and days to verify
    store.record_ago("event", Duration::hours(3));

    // Event should appear in hours interval
    assert_eq!(store.query("event").last_hours(24).sum(), Some(1));

    // Also test with a multi-day event (2 full days old)
    store.record_ago("older_event", Duration::days(2));
    assert_eq!(store.query("older_event").last_days(3).sum(), Some(1));
}

#[test]
fn record_count_ago_relative_duration() {
    let store = EventStore::new();

    // Events within the same day use hours interval
    store.record_count_ago("event", 25, Duration::hours(12));

    assert_eq!(store.query("event").last_hours(24).sum(), Some(25));

    // Also test with multi-day event (2 full days old)
    store.record_count_ago("older_event", 10, Duration::days(2));
    assert_eq!(store.query("older_event").last_days(3).sum(), Some(10));
}

#[test]
fn record_at_rejects_future_time() {
    let store = EventStore::new();

    let future = Utc::now() + Duration::days(1);
    let result = store.record_at("event", future);

    assert!(result.is_err());
}

#[test]
fn record_count_at_rejects_future_time() {
    let store = EventStore::new();

    let future = Utc::now() + Duration::hours(1);
    let result = store.record_count_at("event", 10, future);

    assert!(result.is_err());
}

#[test]
fn record_at_midnight_boundary() {
    // Configure with sufficient retention to cover test dates (June 2024)
    let store = EventStore::builder().track_months(24).build().unwrap();

    let midnight = NaiveDate::from_ymd_opt(2024, 6, 15)
        .unwrap()
        .and_hms_opt(0, 0, 0)
        .unwrap()
        .and_utc();

    store.record_at("midnight_event", midnight).unwrap();

    // Should be queryable
    assert_eq!(store.query("midnight_event").ever().sum(), Some(1));
}

#[test]
fn record_at_hour_boundaries() {
    // Configure with sufficient retention to cover test dates (June 2024)
    let store = EventStore::builder().track_months(24).build().unwrap();

    // Record at exact hour boundaries
    for hour in 0..24 {
        let timestamp = NaiveDate::from_ymd_opt(2024, 6, 15)
            .unwrap()
            .and_hms_opt(hour, 0, 0)
            .unwrap()
            .and_utc();

        store.record_at("hourly_event", timestamp).unwrap();
    }

    assert_eq!(store.query("hourly_event").ever().sum(), Some(24));
}

#[test]
fn record_across_day_boundary() {
    // Configure with sufficient retention to cover test dates (June 2024)
    let store = EventStore::builder().track_months(24).build().unwrap();

    // Event just before midnight
    let before_midnight = NaiveDate::from_ymd_opt(2024, 6, 15)
        .unwrap()
        .and_hms_opt(23, 59, 59)
        .unwrap()
        .and_utc();

    // Event just after midnight
    let after_midnight = NaiveDate::from_ymd_opt(2024, 6, 16)
        .unwrap()
        .and_hms_opt(0, 0, 1)
        .unwrap()
        .and_utc();

    store.record_at("boundary_event", before_midnight).unwrap();
    store.record_at("boundary_event", after_midnight).unwrap();

    assert_eq!(store.query("boundary_event").ever().sum(), Some(2));
}

// ============================================================================
// Overflow and Large Values
// ============================================================================

#[test]
fn u64_max_count_clamped_to_u32() {
    let store = EventStore::new();

    // Large counts are clamped to u32::MAX internally
    store.record_count("overflow_test", u32::MAX - 1000);

    let sum = store.query("overflow_test").last_days(1).sum();
    // Records u32::MAX - 1000
    assert_eq!(sum, Some(u32::MAX - 1000));
}

#[test]
fn multiple_large_counts() {
    let store = EventStore::new();

    store.record_count("large_event", u32::MAX / 2);
    store.record_count("large_event", u32::MAX / 2);

    // Sum should not panic (saturating or wrapping)
    let sum = store.query("large_event").last_days(1).sum();
    assert!(sum.is_some());
}

// ============================================================================
// Event ID Edge Cases
// ============================================================================

#[test]
fn long_event_id() {
    let store = EventStore::new();

    let long_id = "a".repeat(1000);
    store.record(&long_id);

    assert_eq!(store.query(&long_id).last_days(1).sum(), Some(1));
}

#[test]
fn special_characters_in_event_id() {
    let store = EventStore::new();

    let special_id = "event:with/special.chars-and_numbers123!@#$%";
    store.record(special_id);

    assert_eq!(store.query(special_id).last_days(1).sum(), Some(1));
}

#[test]
fn unicode_event_id() {
    let store = EventStore::new();

    let unicode_id = "イベント_事件_🎉";
    store.record(unicode_id);

    assert_eq!(store.query(unicode_id).last_days(1).sum(), Some(1));
}

// ============================================================================
// High Volume Recording
// ============================================================================

#[test]
fn many_different_events() {
    let store = EventStore::new();

    for i in 0..1000 {
        store.record(format!("event_{}", i));
    }

    // All events should be tracked
    for i in 0..1000 {
        assert_eq!(
            store.query(format!("event_{}", i)).last_days(1).sum(),
            Some(1)
        );
    }
}

#[test]
fn rapid_recording_same_event() {
    let store = EventStore::new();

    for _ in 0..10_000 {
        store.record("rapid_event");
    }

    assert_eq!(store.query("rapid_event").last_days(1).sum(), Some(10_000));
}