kaccy_ai/
reports.rs

1//! Professional report generation for AI operations
2//!
3//! This module provides utilities for generating comprehensive reports
4//! from AI analysis results in multiple formats (Markdown, JSON, CSV).
5
6use crate::error::AiError;
7use crate::fraud::RiskLevel;
8use serde::{Deserialize, Serialize};
9use std::collections::HashMap;
10use std::fmt::Write as _;
11
12/// Output format for reports
13#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
14pub enum ReportFormat {
15    /// Markdown format for documentation
16    Markdown,
17    /// JSON format for APIs and data exchange
18    Json,
19    /// CSV format for spreadsheet analysis
20    Csv,
21}
22
23/// Cost analysis report
24#[derive(Debug, Clone, Serialize, Deserialize)]
25pub struct CostAnalysisReport {
26    /// Report title
27    pub title: String,
28    /// Time period covered
29    pub period: String,
30    /// Total cost in USD
31    pub total_cost: f64,
32    /// Cost breakdown by provider
33    pub by_provider: HashMap<String, f64>,
34    /// Cost breakdown by operation type
35    pub by_operation: HashMap<String, f64>,
36    /// Number of requests
37    pub total_requests: usize,
38    /// Average cost per request
39    pub avg_cost_per_request: f64,
40    /// Cost trend (percentage change from previous period)
41    pub cost_trend: Option<f64>,
42    /// Recommendations for cost optimization
43    pub recommendations: Vec<String>,
44}
45
46impl CostAnalysisReport {
47    /// Create a new cost analysis report
48    #[must_use]
49    pub fn new(title: String, period: String) -> Self {
50        Self {
51            title,
52            period,
53            total_cost: 0.0,
54            by_provider: HashMap::new(),
55            by_operation: HashMap::new(),
56            total_requests: 0,
57            avg_cost_per_request: 0.0,
58            cost_trend: None,
59            recommendations: Vec::new(),
60        }
61    }
62
63    /// Add provider cost
64    pub fn add_provider_cost(&mut self, provider: String, cost: f64) {
65        *self.by_provider.entry(provider).or_insert(0.0) += cost;
66        self.total_cost += cost;
67    }
68
69    /// Add operation cost
70    pub fn add_operation_cost(&mut self, operation: String, cost: f64) {
71        *self.by_operation.entry(operation).or_insert(0.0) += cost;
72    }
73
74    /// Set total requests and calculate average
75    pub fn set_total_requests(&mut self, count: usize) {
76        self.total_requests = count;
77        if count > 0 {
78            self.avg_cost_per_request = self.total_cost / count as f64;
79        }
80    }
81
82    /// Add a cost optimization recommendation
83    pub fn add_recommendation(&mut self, recommendation: String) {
84        self.recommendations.push(recommendation);
85    }
86
87    /// Generate markdown report
88    #[must_use]
89    pub fn to_markdown(&self) -> String {
90        let mut report = String::new();
91
92        let _ = writeln!(report, "# {}", self.title);
93        let _ = writeln!(report, "**Period:** {}", self.period);
94
95        report.push_str("## Summary\n\n");
96        let _ = writeln!(report, "- **Total Cost:** ${:.2}", self.total_cost);
97        let _ = writeln!(report, "- **Total Requests:** {}", self.total_requests);
98        let _ = writeln!(
99            report,
100            "- **Average Cost per Request:** ${:.4}",
101            self.avg_cost_per_request
102        );
103
104        if let Some(trend) = self.cost_trend {
105            let trend_symbol = if trend > 0.0 { "↑" } else { "↓" };
106            let _ = writeln!(
107                report,
108                "- **Cost Trend:** {}{:.1}%",
109                trend_symbol,
110                trend.abs()
111            );
112        }
113        report.push('\n');
114
115        if !self.by_provider.is_empty() {
116            report.push_str("## Cost by Provider\n\n");
117            report.push_str("| Provider | Cost | Percentage |\n");
118            report.push_str("|----------|------|------------|\n");
119
120            let mut providers: Vec<_> = self.by_provider.iter().collect();
121            providers.sort_by(|a, b| b.1.partial_cmp(a.1).unwrap());
122
123            for (provider, cost) in providers {
124                let percentage = (cost / self.total_cost) * 100.0;
125                let _ = writeln!(report, "| {provider} | ${cost:.2} | {percentage:.1}% |");
126            }
127            report.push('\n');
128        }
129
130        if !self.by_operation.is_empty() {
131            report.push_str("## Cost by Operation\n\n");
132            report.push_str("| Operation | Cost | Percentage |\n");
133            report.push_str("|-----------|------|------------|\n");
134
135            let mut operations: Vec<_> = self.by_operation.iter().collect();
136            operations.sort_by(|a, b| b.1.partial_cmp(a.1).unwrap());
137
138            for (operation, cost) in operations {
139                let percentage = (cost / self.total_cost) * 100.0;
140                let _ = writeln!(report, "| {operation} | ${cost:.2} | {percentage:.1}% |");
141            }
142            report.push('\n');
143        }
144
145        if !self.recommendations.is_empty() {
146            report.push_str("## Cost Optimization Recommendations\n\n");
147            for (i, rec) in self.recommendations.iter().enumerate() {
148                let _ = writeln!(report, "{}. {}", i + 1, rec);
149            }
150            report.push('\n');
151        }
152
153        report
154    }
155
156    /// Generate JSON report
157    pub fn to_json(&self) -> Result<String, AiError> {
158        serde_json::to_string_pretty(self)
159            .map_err(|e| AiError::InvalidInput(format!("Failed to serialize report: {e}")))
160    }
161
162    /// Generate CSV report
163    #[must_use]
164    pub fn to_csv(&self) -> String {
165        let mut csv = String::new();
166
167        csv.push_str("Category,Item,Value\n");
168        let _ = writeln!(csv, "Summary,Total Cost,{:.2}", self.total_cost);
169        let _ = writeln!(csv, "Summary,Total Requests,{}", self.total_requests);
170        let _ = writeln!(
171            csv,
172            "Summary,Avg Cost per Request,{:.4}",
173            self.avg_cost_per_request
174        );
175
176        for (provider, cost) in &self.by_provider {
177            let _ = writeln!(csv, "Provider,{provider},{cost:.2}");
178        }
179
180        for (operation, cost) in &self.by_operation {
181            let _ = writeln!(csv, "Operation,{operation},{cost:.2}");
182        }
183
184        csv
185    }
186}
187
188/// Performance benchmark report
189#[derive(Debug, Clone, Serialize, Deserialize)]
190pub struct PerformanceBenchmarkReport {
191    /// Report title
192    pub title: String,
193    /// Benchmark date
194    pub date: String,
195    /// Performance metrics by operation
196    pub operations: HashMap<String, OperationBenchmark>,
197    /// Overall statistics
198    pub summary: BenchmarkSummary,
199}
200
201/// Benchmark data for a single operation
202#[derive(Debug, Clone, Serialize, Deserialize)]
203pub struct OperationBenchmark {
204    /// Operation name
205    pub name: String,
206    /// Average latency in milliseconds
207    pub avg_latency_ms: f64,
208    /// Median latency in milliseconds
209    pub median_latency_ms: f64,
210    /// 95th percentile latency
211    pub p95_latency_ms: f64,
212    /// 99th percentile latency
213    pub p99_latency_ms: f64,
214    /// Total operations
215    pub total_ops: usize,
216    /// Success rate (0-100)
217    pub success_rate: f64,
218}
219
220/// Overall benchmark summary
221#[derive(Debug, Clone, Serialize, Deserialize)]
222pub struct BenchmarkSummary {
223    /// Total operations benchmarked
224    pub total_operations: usize,
225    /// Overall average latency
226    pub overall_avg_latency_ms: f64,
227    /// Overall success rate
228    pub overall_success_rate: f64,
229    /// Slowest operation
230    pub slowest_operation: Option<String>,
231    /// Fastest operation
232    pub fastest_operation: Option<String>,
233}
234
235impl PerformanceBenchmarkReport {
236    /// Create a new performance benchmark report
237    #[must_use]
238    pub fn new(title: String, date: String) -> Self {
239        Self {
240            title,
241            date,
242            operations: HashMap::new(),
243            summary: BenchmarkSummary {
244                total_operations: 0,
245                overall_avg_latency_ms: 0.0,
246                overall_success_rate: 0.0,
247                slowest_operation: None,
248                fastest_operation: None,
249            },
250        }
251    }
252
253    /// Add operation benchmark
254    pub fn add_operation(&mut self, name: String, benchmark: OperationBenchmark) {
255        self.operations.insert(name, benchmark);
256    }
257
258    /// Calculate summary statistics
259    pub fn calculate_summary(&mut self) {
260        if self.operations.is_empty() {
261            return;
262        }
263
264        let total_ops: usize = self.operations.values().map(|b| b.total_ops).sum();
265        let total_latency: f64 = self
266            .operations
267            .values()
268            .map(|b| b.avg_latency_ms * b.total_ops as f64)
269            .sum();
270
271        let total_success: f64 = self
272            .operations
273            .values()
274            .map(|b| b.success_rate * b.total_ops as f64)
275            .sum();
276
277        self.summary.total_operations = total_ops;
278        self.summary.overall_avg_latency_ms = if total_ops > 0 {
279            total_latency / total_ops as f64
280        } else {
281            0.0
282        };
283        self.summary.overall_success_rate = if total_ops > 0 {
284            total_success / total_ops as f64
285        } else {
286            0.0
287        };
288
289        // Find slowest and fastest operations
290        let mut slowest: Option<(&String, &OperationBenchmark)> = None;
291        let mut fastest: Option<(&String, &OperationBenchmark)> = None;
292
293        for (name, bench) in &self.operations {
294            if slowest.is_none() || bench.avg_latency_ms > slowest.unwrap().1.avg_latency_ms {
295                slowest = Some((name, bench));
296            }
297            if fastest.is_none() || bench.avg_latency_ms < fastest.unwrap().1.avg_latency_ms {
298                fastest = Some((name, bench));
299            }
300        }
301
302        self.summary.slowest_operation = slowest.map(|(name, _)| name.clone());
303        self.summary.fastest_operation = fastest.map(|(name, _)| name.clone());
304    }
305
306    /// Generate markdown report
307    #[must_use]
308    pub fn to_markdown(&self) -> String {
309        let mut report = String::new();
310
311        let _ = writeln!(report, "# {}", self.title);
312        let _ = writeln!(report, "**Date:** {}", self.date);
313
314        report.push_str("## Summary\n\n");
315        let _ = writeln!(
316            report,
317            "- **Total Operations:** {}",
318            self.summary.total_operations
319        );
320        let _ = writeln!(
321            report,
322            "- **Overall Avg Latency:** {:.2}ms",
323            self.summary.overall_avg_latency_ms
324        );
325        let _ = writeln!(
326            report,
327            "- **Overall Success Rate:** {:.1}%",
328            self.summary.overall_success_rate
329        );
330
331        if let Some(ref slowest) = self.summary.slowest_operation {
332            let _ = writeln!(report, "- **Slowest Operation:** {slowest}");
333        }
334        if let Some(ref fastest) = self.summary.fastest_operation {
335            let _ = writeln!(report, "- **Fastest Operation:** {fastest}");
336        }
337        report.push('\n');
338
339        if !self.operations.is_empty() {
340            report.push_str("## Operation Benchmarks\n\n");
341            report.push_str(
342                "| Operation | Avg (ms) | Median (ms) | P95 (ms) | P99 (ms) | Ops | Success % |\n",
343            );
344            report.push_str(
345                "|-----------|----------|-------------|----------|----------|-----|----------|\n",
346            );
347
348            let mut ops: Vec<_> = self.operations.iter().collect();
349            ops.sort_by(|a, b| b.1.avg_latency_ms.partial_cmp(&a.1.avg_latency_ms).unwrap());
350
351            for (name, bench) in ops {
352                let _ = writeln!(
353                    report,
354                    "| {} | {:.2} | {:.2} | {:.2} | {:.2} | {} | {:.1}% |",
355                    name,
356                    bench.avg_latency_ms,
357                    bench.median_latency_ms,
358                    bench.p95_latency_ms,
359                    bench.p99_latency_ms,
360                    bench.total_ops,
361                    bench.success_rate
362                );
363            }
364            report.push('\n');
365        }
366
367        report
368    }
369
370    /// Generate JSON report
371    pub fn to_json(&self) -> Result<String, AiError> {
372        serde_json::to_string_pretty(self)
373            .map_err(|e| AiError::InvalidInput(format!("Failed to serialize report: {e}")))
374    }
375}
376
377/// Fraud analysis summary report
378#[derive(Debug, Clone, Serialize, Deserialize)]
379pub struct FraudSummaryReport {
380    /// Report title
381    pub title: String,
382    /// Analysis period
383    pub period: String,
384    /// Total cases analyzed
385    pub total_cases: usize,
386    /// Cases by risk level
387    pub by_risk_level: HashMap<String, usize>,
388    /// Most common fraud types
389    pub common_fraud_types: Vec<(String, usize)>,
390    /// Detection accuracy (if available)
391    pub accuracy: Option<f64>,
392    /// Key insights
393    pub insights: Vec<String>,
394}
395
396impl FraudSummaryReport {
397    /// Create a new fraud summary report
398    #[must_use]
399    pub fn new(title: String, period: String) -> Self {
400        Self {
401            title,
402            period,
403            total_cases: 0,
404            by_risk_level: HashMap::new(),
405            common_fraud_types: Vec::new(),
406            accuracy: None,
407            insights: Vec::new(),
408        }
409    }
410
411    /// Add a fraud case
412    pub fn add_case(&mut self, risk_level: RiskLevel) {
413        self.total_cases += 1;
414        let level_str = format!("{risk_level:?}");
415        *self.by_risk_level.entry(level_str).or_insert(0) += 1;
416    }
417
418    /// Set common fraud types
419    pub fn set_common_fraud_types(&mut self, types: Vec<(String, usize)>) {
420        self.common_fraud_types = types;
421    }
422
423    /// Add an insight
424    pub fn add_insight(&mut self, insight: String) {
425        self.insights.push(insight);
426    }
427
428    /// Generate markdown report
429    #[must_use]
430    pub fn to_markdown(&self) -> String {
431        let mut report = String::new();
432
433        let _ = writeln!(report, "# {}", self.title);
434        let _ = writeln!(report, "**Period:** {}", self.period);
435
436        report.push_str("## Summary\n\n");
437        let _ = writeln!(report, "- **Total Cases Analyzed:** {}", self.total_cases);
438
439        if let Some(accuracy) = self.accuracy {
440            let _ = writeln!(report, "- **Detection Accuracy:** {accuracy:.1}%");
441        }
442        report.push('\n');
443
444        if !self.by_risk_level.is_empty() {
445            report.push_str("## Risk Level Distribution\n\n");
446            report.push_str("| Risk Level | Count | Percentage |\n");
447            report.push_str("|------------|-------|------------|\n");
448
449            let mut levels: Vec<_> = self.by_risk_level.iter().collect();
450            levels.sort_by(|a, b| b.1.cmp(a.1));
451
452            for (level, count) in levels {
453                let percentage = (*count as f64 / self.total_cases as f64) * 100.0;
454                let _ = writeln!(report, "| {level} | {count} | {percentage:.1}% |");
455            }
456            report.push('\n');
457        }
458
459        if !self.common_fraud_types.is_empty() {
460            report.push_str("## Common Fraud Types\n\n");
461            report.push_str("| Fraud Type | Occurrences |\n");
462            report.push_str("|------------|-------------|\n");
463
464            for (fraud_type, count) in &self.common_fraud_types {
465                let _ = writeln!(report, "| {fraud_type} | {count} |");
466            }
467            report.push('\n');
468        }
469
470        if !self.insights.is_empty() {
471            report.push_str("## Key Insights\n\n");
472            for (i, insight) in self.insights.iter().enumerate() {
473                let _ = writeln!(report, "{}. {}", i + 1, insight);
474            }
475            report.push('\n');
476        }
477
478        report
479    }
480
481    /// Generate JSON report
482    pub fn to_json(&self) -> Result<String, AiError> {
483        serde_json::to_string_pretty(self)
484            .map_err(|e| AiError::InvalidInput(format!("Failed to serialize report: {e}")))
485    }
486}
487
488/// Report generator for all report types
489pub struct ReportGenerator;
490
491impl ReportGenerator {
492    /// Generate a report in the specified format
493    pub fn generate(report_type: ReportType, format: ReportFormat) -> Result<String, AiError> {
494        match format {
495            ReportFormat::Markdown => match report_type {
496                ReportType::CostAnalysis(ref report) => Ok(report.to_markdown()),
497                ReportType::PerformanceBenchmark(ref report) => Ok(report.to_markdown()),
498                ReportType::FraudSummary(ref report) => Ok(report.to_markdown()),
499            },
500            ReportFormat::Json => match report_type {
501                ReportType::CostAnalysis(ref report) => report.to_json(),
502                ReportType::PerformanceBenchmark(ref report) => report.to_json(),
503                ReportType::FraudSummary(ref report) => report.to_json(),
504            },
505            ReportFormat::Csv => match report_type {
506                ReportType::CostAnalysis(ref report) => Ok(report.to_csv()),
507                ReportType::PerformanceBenchmark(_) => Err(AiError::InvalidInput(
508                    "CSV format not supported for performance benchmarks".to_string(),
509                )),
510                ReportType::FraudSummary(_) => Err(AiError::InvalidInput(
511                    "CSV format not supported for fraud summaries".to_string(),
512                )),
513            },
514        }
515    }
516}
517
518/// Report type enum
519pub enum ReportType {
520    /// Cost analysis report
521    CostAnalysis(CostAnalysisReport),
522    /// Performance benchmark report
523    PerformanceBenchmark(PerformanceBenchmarkReport),
524    /// Fraud summary report
525    FraudSummary(FraudSummaryReport),
526}
527
528#[cfg(test)]
529mod tests {
530    use super::*;
531
532    #[test]
533    fn test_cost_analysis_report_creation() {
534        let mut report = CostAnalysisReport::new(
535            "Cost Analysis Q1 2026".to_string(),
536            "January - March 2026".to_string(),
537        );
538
539        report.add_provider_cost("OpenAI".to_string(), 150.50);
540        report.add_provider_cost("Anthropic".to_string(), 120.25);
541        report.add_provider_cost("Gemini".to_string(), 45.00);
542
543        report.add_operation_cost("code_evaluation".to_string(), 180.00);
544        report.add_operation_cost("verification".to_string(), 95.75);
545        report.add_operation_cost("fraud_detection".to_string(), 40.00);
546
547        report.set_total_requests(1_250);
548        report.add_recommendation(
549            "Consider using Gemini for simple tasks to reduce costs by 40%".to_string(),
550        );
551
552        assert_eq!(report.total_cost, 315.75);
553        assert_eq!(report.total_requests, 1_250);
554        assert!((report.avg_cost_per_request - 0.2526).abs() < 1e-4);
555        assert_eq!(report.by_provider.len(), 3);
556        assert_eq!(report.by_operation.len(), 3);
557        assert_eq!(report.recommendations.len(), 1);
558    }
559
560    #[test]
561    fn test_cost_analysis_markdown_generation() {
562        let mut report =
563            CostAnalysisReport::new("Test Report".to_string(), "January 2026".to_string());
564
565        report.add_provider_cost("OpenAI".to_string(), 100.0);
566        report.set_total_requests(500);
567
568        let markdown = report.to_markdown();
569
570        assert!(markdown.contains("# Test Report"));
571        assert!(markdown.contains("**Period:** January 2026"));
572        assert!(markdown.contains("**Total Cost:** $100.00"));
573        assert!(markdown.contains("**Total Requests:** 500"));
574        assert!(markdown.contains("## Cost by Provider"));
575    }
576
577    #[test]
578    fn test_cost_analysis_json_generation() {
579        let mut report =
580            CostAnalysisReport::new("Test Report".to_string(), "January 2026".to_string());
581
582        report.add_provider_cost("OpenAI".to_string(), 100.0);
583        report.set_total_requests(500);
584
585        let json = report.to_json().unwrap();
586
587        assert!(json.contains("\"title\""));
588        assert!(json.contains("\"Test Report\""));
589        assert!(json.contains("\"total_cost\""));
590    }
591
592    #[test]
593    fn test_cost_analysis_csv_generation() {
594        let mut report =
595            CostAnalysisReport::new("Test Report".to_string(), "January 2026".to_string());
596
597        report.add_provider_cost("OpenAI".to_string(), 100.0);
598        report.add_operation_cost("evaluation".to_string(), 50.0);
599        report.set_total_requests(500);
600
601        let csv = report.to_csv();
602
603        assert!(csv.contains("Category,Item,Value"));
604        assert!(csv.contains("Summary,Total Cost,100.00"));
605        assert!(csv.contains("Provider,OpenAI,100.00"));
606        assert!(csv.contains("Operation,evaluation,50.00"));
607    }
608
609    #[test]
610    fn test_performance_benchmark_report_creation() {
611        let mut report = PerformanceBenchmarkReport::new(
612            "Performance Benchmarks".to_string(),
613            "2026-01-09".to_string(),
614        );
615
616        let bench1 = OperationBenchmark {
617            name: "code_evaluation".to_string(),
618            avg_latency_ms: 250.5,
619            median_latency_ms: 240.0,
620            p95_latency_ms: 350.0,
621            p99_latency_ms: 450.0,
622            total_ops: 1_000,
623            success_rate: 99.5,
624        };
625
626        let bench2 = OperationBenchmark {
627            name: "fraud_detection".to_string(),
628            avg_latency_ms: 180.2,
629            median_latency_ms: 170.0,
630            p95_latency_ms: 250.0,
631            p99_latency_ms: 320.0,
632            total_ops: 500,
633            success_rate: 98.8,
634        };
635
636        report.add_operation("code_evaluation".to_string(), bench1);
637        report.add_operation("fraud_detection".to_string(), bench2);
638        report.calculate_summary();
639
640        assert_eq!(report.operations.len(), 2);
641        assert_eq!(report.summary.total_operations, 1_500);
642        assert!(report.summary.slowest_operation.is_some());
643        assert!(report.summary.fastest_operation.is_some());
644    }
645
646    #[test]
647    fn test_performance_benchmark_markdown_generation() {
648        let mut report = PerformanceBenchmarkReport::new(
649            "Test Benchmarks".to_string(),
650            "2026-01-09".to_string(),
651        );
652
653        let bench = OperationBenchmark {
654            name: "test_op".to_string(),
655            avg_latency_ms: 100.0,
656            median_latency_ms: 95.0,
657            p95_latency_ms: 120.0,
658            p99_latency_ms: 150.0,
659            total_ops: 100,
660            success_rate: 99.0,
661        };
662
663        report.add_operation("test_op".to_string(), bench);
664        report.calculate_summary();
665
666        let markdown = report.to_markdown();
667
668        assert!(markdown.contains("# Test Benchmarks"));
669        assert!(markdown.contains("**Date:** 2026-01-09"));
670        assert!(markdown.contains("## Summary"));
671        assert!(markdown.contains("## Operation Benchmarks"));
672    }
673
674    #[test]
675    fn test_fraud_summary_report_creation() {
676        let mut report =
677            FraudSummaryReport::new("Fraud Analysis".to_string(), "Q1 2026".to_string());
678
679        report.add_case(RiskLevel::Low);
680        report.add_case(RiskLevel::Low);
681        report.add_case(RiskLevel::Medium);
682        report.add_case(RiskLevel::High);
683        report.add_case(RiskLevel::Critical);
684
685        report.set_common_fraud_types(vec![
686            ("Sybil Attack".to_string(), 15),
687            ("Wash Trading".to_string(), 8),
688        ]);
689
690        report
691            .add_insight("Sybil attacks increased by 25% compared to previous quarter".to_string());
692        report.accuracy = Some(94.5);
693
694        assert_eq!(report.total_cases, 5);
695        assert_eq!(report.common_fraud_types.len(), 2);
696        assert_eq!(report.insights.len(), 1);
697        assert_eq!(report.accuracy, Some(94.5));
698    }
699
700    #[test]
701    fn test_fraud_summary_markdown_generation() {
702        let mut report =
703            FraudSummaryReport::new("Test Fraud Report".to_string(), "January 2026".to_string());
704
705        report.add_case(RiskLevel::Low);
706        report.add_case(RiskLevel::High);
707        report.accuracy = Some(95.0);
708        report.add_insight("Test insight".to_string());
709
710        let markdown = report.to_markdown();
711
712        assert!(markdown.contains("# Test Fraud Report"));
713        assert!(markdown.contains("**Period:** January 2026"));
714        assert!(markdown.contains("**Total Cases Analyzed:** 2"));
715        assert!(markdown.contains("**Detection Accuracy:** 95.0%"));
716        assert!(markdown.contains("## Key Insights"));
717    }
718
719    #[test]
720    fn test_report_generator_markdown() {
721        let report = CostAnalysisReport::new("Test".to_string(), "2026".to_string());
722
723        let result =
724            ReportGenerator::generate(ReportType::CostAnalysis(report), ReportFormat::Markdown);
725
726        assert!(result.is_ok());
727        let markdown = result.unwrap();
728        assert!(markdown.contains("# Test"));
729    }
730
731    #[test]
732    fn test_report_generator_json() {
733        let report = CostAnalysisReport::new("Test".to_string(), "2026".to_string());
734
735        let result =
736            ReportGenerator::generate(ReportType::CostAnalysis(report), ReportFormat::Json);
737
738        assert!(result.is_ok());
739        let json = result.unwrap();
740        assert!(json.contains("\"title\""));
741    }
742
743    #[test]
744    fn test_report_generator_csv_unsupported() {
745        let report = PerformanceBenchmarkReport::new("Test".to_string(), "2026".to_string());
746
747        let result =
748            ReportGenerator::generate(ReportType::PerformanceBenchmark(report), ReportFormat::Csv);
749
750        assert!(result.is_err());
751    }
752}