terraphim_usage 1.20.3

Usage tracking and analytics for Terraphim AI
Documentation
use crate::{MetricLine, ProviderUsage};
use std::fmt::Write;

pub fn format_usage_text(usage: &ProviderUsage) -> String {
    let mut output = String::new();
    writeln!(
        output,
        "{} ({})",
        usage.display_name,
        usage.plan.as_deref().unwrap_or("Unknown")
    )
    .unwrap();

    for line in &usage.lines {
        match line {
            MetricLine::Text { label, value, .. } => {
                writeln!(output, "  {}: {}", label, value).unwrap();
            }
            MetricLine::Progress {
                label,
                used,
                limit,
                format,
                resets_at,
                ..
            } => {
                let pct = if *limit > 0.0 {
                    (*used / *limit) * 100.0
                } else {
                    0.0
                };
                let bar = progress_bar(pct);
                let display = match format {
                    crate::ProgressFormat::Percent => format!("{:.0}%", pct),
                    crate::ProgressFormat::Dollars => format!("${:.2}", used / 100.0),
                    crate::ProgressFormat::Count { suffix } => format!("{:.0} {}", used, suffix),
                };
                let reset_info = resets_at
                    .as_ref()
                    .map(|r| format!(" (resets {})", r))
                    .unwrap_or_default();
                writeln!(output, "  {}: {} {}{}", label, bar, display, reset_info).unwrap();
            }
            MetricLine::Badge { label, text, .. } => {
                writeln!(output, "  {}: {}", label, text).unwrap();
            }
        }
    }
    output
}

fn progress_bar(pct: f64) -> String {
    let filled = (pct / 5.0).round() as usize;
    let filled = filled.min(20);
    let empty = 20 - filled;
    format!("{}{}", "".repeat(filled), "".repeat(empty))
}

pub fn format_usage_json(usage: &ProviderUsage) -> Result<String, serde_json::Error> {
    serde_json::to_string_pretty(usage)
}

pub fn format_usage_csv(usages: &[ProviderUsage]) -> String {
    let mut csv =
        String::from("provider,plan,line_type,label,value,used,limit,resets_at,fetched_at\n");
    for usage in usages {
        for line in &usage.lines {
            match line {
                MetricLine::Text { label, value, .. } => {
                    writeln!(
                        csv,
                        "{},{},text,\"{}\",\"{}\",,,{}",
                        usage.display_name,
                        usage.plan.as_deref().unwrap_or(""),
                        label,
                        value,
                        usage.fetched_at
                    )
                    .unwrap();
                }
                MetricLine::Progress {
                    label,
                    used,
                    limit,
                    resets_at,
                    ..
                } => {
                    writeln!(
                        csv,
                        "{},{},progress,\"{}\",{},{},{},{}",
                        usage.display_name,
                        usage.plan.as_deref().unwrap_or(""),
                        label,
                        used,
                        limit,
                        resets_at.as_deref().unwrap_or(""),
                        usage.fetched_at
                    )
                    .unwrap();
                }
                MetricLine::Badge { label, text, .. } => {
                    writeln!(
                        csv,
                        "{},{},badge,\"{}\",\"{}\",,,{}",
                        usage.display_name,
                        usage.plan.as_deref().unwrap_or(""),
                        label,
                        text,
                        usage.fetched_at
                    )
                    .unwrap();
                }
            }
        }
    }
    csv
}

#[cfg(test)]
mod tests {
    use super::*;
    use crate::ProgressFormat;

    #[test]
    fn test_format_usage_text_with_progress() {
        let usage = ProviderUsage {
            provider_id: "test".to_string(),
            display_name: "Test Provider".to_string(),
            plan: Some("Pro".to_string()),
            lines: vec![MetricLine::Progress {
                label: "Session".to_string(),
                used: 42.0,
                limit: 100.0,
                format: ProgressFormat::Percent,
                resets_at: Some("2026-04-02T15:00:00Z".to_string()),
                period_duration_ms: None,
                color: None,
            }],
            fetched_at: "2026-04-02T10:00:00Z".to_string(),
        };
        let output = format_usage_text(&usage);
        assert!(output.contains("Test Provider (Pro)"));
        assert!(output.contains("Session"));
        assert!(output.contains("42%"));
    }

    #[test]
    fn test_format_usage_json() {
        let usage = ProviderUsage {
            provider_id: "test".to_string(),
            display_name: "Test".to_string(),
            plan: None,
            lines: vec![],
            fetched_at: "2026-04-02T10:00:00Z".to_string(),
        };
        let json = format_usage_json(&usage).unwrap();
        assert!(json.contains("Test"));
    }

    #[test]
    fn test_format_usage_csv() {
        let usages = vec![ProviderUsage {
            provider_id: "test".to_string(),
            display_name: "Test".to_string(),
            plan: Some("Pro".to_string()),
            lines: vec![MetricLine::Text {
                label: "Account".to_string(),
                value: "user@test.com".to_string(),
                color: None,
                subtitle: None,
            }],
            fetched_at: "2026-04-02T10:00:00Z".to_string(),
        }];
        let csv = format_usage_csv(&usages);
        assert!(csv.contains("provider,plan,line_type"));
        assert!(csv.contains("Test"));
        assert!(csv.contains("Account"));
    }

    #[test]
    fn test_progress_bar_full() {
        assert_eq!(progress_bar(100.0), "████████████████████");
    }

    #[test]
    fn test_progress_bar_half() {
        assert_eq!(progress_bar(50.0), "██████████░░░░░░░░░░");
    }

    #[test]
    fn test_progress_bar_empty() {
        assert_eq!(progress_bar(0.0), "░░░░░░░░░░░░░░░░░░░░");
    }
}