tokrs 0.3.0

A CLI tool for counting tokens
use super::*;

fn entry(app: AppKind, created_at: i64, input: u64, output: u64) -> UsageEntry {
    UsageEntry::new(
        app,
        "test-model".to_string(),
        None,
        created_at,
        input,
        output,
        0,
        0,
        None,
    )
}

fn local_noon(year: i32, month: u32, day: u32) -> i64 {
    Local
        .with_ymd_and_hms(year, month, day, 12, 0, 0)
        .single()
        .unwrap()
        .timestamp()
}

#[test]
fn test_aggregate_by_app_and_total() {
    let entries = vec![
        entry(AppKind::Claude, 1, 10, 5),
        entry(AppKind::Claude, 2, 20, 5),
        entry(AppKind::Codex, 3, 100, 50),
    ];
    let by_app = aggregate_by_app(&entries);
    assert_eq!(by_app.len(), 2);
    assert_eq!(by_app[&AppKind::Claude].requests, 2);
    assert_eq!(by_app[&AppKind::Claude].input_tokens, 30);
    assert_eq!(by_app[&AppKind::Codex].total_tokens(), 150);
    let total = grand_total(&entries);
    assert_eq!(total.requests, 3);
    assert_eq!(total.input_tokens, 130);
}

#[test]
fn test_aggregate_by_model() {
    let mut e1 = entry(AppKind::Claude, 1, 10, 5);
    e1.model = "claude-sonnet-4".to_string();
    let mut e2 = entry(AppKind::Claude, 2, 7, 3);
    e2.model = "gpt-5".to_string();
    let by_model = aggregate_by_model(&[e1.clone(), e2.clone(), e1.clone()]);
    assert_eq!(by_model.len(), 2);
    assert_eq!(
        by_model[&(AppKind::Claude, "claude-sonnet-4".to_string())].requests,
        2
    );
}

#[test]
fn test_aggregate_by_day_groups_same_local_day() {
    let ts = local_noon(2026, 9, 1);
    let entries = vec![
        entry(AppKind::Claude, ts, 1, 1),
        entry(AppKind::Claude, ts + 3600, 2, 2),
    ];
    let by_day = aggregate_by_day(&entries);
    assert_eq!(by_day.len(), 1);
    assert_eq!(by_day.values().next().unwrap().requests, 2);
}

#[test]
fn test_two_entries_days_apart_are_different_groups() {
    let entries = vec![
        entry(AppKind::Claude, local_noon(2026, 9, 1), 1, 1),
        entry(AppKind::Codex, local_noon(2026, 9, 3), 1, 1),
    ];
    assert_eq!(aggregate_by_day(&entries).len(), 2);
}

#[test]
fn test_filter_by_range() {
    let entries = vec![
        entry(AppKind::Claude, local_noon(2026, 9, 1), 1, 1),
        entry(AppKind::Codex, local_noon(2026, 9, 5), 2, 2),
    ];
    let since = NaiveDate::from_ymd_opt(2026, 9, 2).unwrap();
    let filtered = filter_by_range(entries.clone(), Some(since), None);
    assert_eq!(filtered.len(), 1);
    assert_eq!(filtered[0].app, AppKind::Codex);

    let until = NaiveDate::from_ymd_opt(2026, 9, 2).unwrap();
    let filtered = filter_by_range(entries, None, Some(until));
    assert_eq!(filtered.len(), 1);
    assert_eq!(filtered[0].app, AppKind::Claude);
}

#[test]
fn test_filter_by_range_noop() {
    let entries = vec![entry(AppKind::Claude, 1, 1, 1)];
    let filtered = filter_by_range(entries, None, None);
    assert_eq!(filtered.len(), 1);
}

#[test]
fn test_today_total_counts_only_today() {
    let today = Local::now().date_naive();
    let today_noon = today
        .and_hms_opt(12, 0, 0)
        .unwrap()
        .and_local_timezone(Local)
        .single()
        .unwrap()
        .timestamp();
    let old_noon = today_noon - 10 * 86_400;
    let entries = vec![
        entry(AppKind::Claude, today_noon, 10, 5),
        entry(AppKind::Codex, old_noon, 100, 50),
    ];
    let today = today_total(&entries);
    assert_eq!(today.requests, 1);
    assert_eq!(today.input_tokens, 10);
    assert_eq!(today.output_tokens, 5);
    assert_eq!(today_total(&[]), TokenTotals::default());
}

#[test]
fn test_totals_accumulate_cost_and_unpriced() {
    let mut priced = entry(AppKind::Claude, 1, 10, 5);
    priced.cost_usd = Some(0.25);
    let unpriced = entry(AppKind::Claude, 2, 10, 5);
    let total = grand_total(&[priced, unpriced]);
    assert_eq!(total.requests, 2);
    assert!((total.cost_usd - 0.25).abs() < f64::EPSILON);
    assert_eq!(total.unpriced, 1);
}