1use crate::error::AiError;
7use crate::fraud::RiskLevel;
8use serde::{Deserialize, Serialize};
9use std::collections::HashMap;
10use std::fmt::Write as _;
11
12#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
14pub enum ReportFormat {
15 Markdown,
17 Json,
19 Csv,
21}
22
23#[derive(Debug, Clone, Serialize, Deserialize)]
25pub struct CostAnalysisReport {
26 pub title: String,
28 pub period: String,
30 pub total_cost: f64,
32 pub by_provider: HashMap<String, f64>,
34 pub by_operation: HashMap<String, f64>,
36 pub total_requests: usize,
38 pub avg_cost_per_request: f64,
40 pub cost_trend: Option<f64>,
42 pub recommendations: Vec<String>,
44}
45
46impl CostAnalysisReport {
47 #[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 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 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 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 pub fn add_recommendation(&mut self, recommendation: String) {
84 self.recommendations.push(recommendation);
85 }
86
87 #[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 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 #[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#[derive(Debug, Clone, Serialize, Deserialize)]
190pub struct PerformanceBenchmarkReport {
191 pub title: String,
193 pub date: String,
195 pub operations: HashMap<String, OperationBenchmark>,
197 pub summary: BenchmarkSummary,
199}
200
201#[derive(Debug, Clone, Serialize, Deserialize)]
203pub struct OperationBenchmark {
204 pub name: String,
206 pub avg_latency_ms: f64,
208 pub median_latency_ms: f64,
210 pub p95_latency_ms: f64,
212 pub p99_latency_ms: f64,
214 pub total_ops: usize,
216 pub success_rate: f64,
218}
219
220#[derive(Debug, Clone, Serialize, Deserialize)]
222pub struct BenchmarkSummary {
223 pub total_operations: usize,
225 pub overall_avg_latency_ms: f64,
227 pub overall_success_rate: f64,
229 pub slowest_operation: Option<String>,
231 pub fastest_operation: Option<String>,
233}
234
235impl PerformanceBenchmarkReport {
236 #[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 pub fn add_operation(&mut self, name: String, benchmark: OperationBenchmark) {
255 self.operations.insert(name, benchmark);
256 }
257
258 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 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 #[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 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#[derive(Debug, Clone, Serialize, Deserialize)]
379pub struct FraudSummaryReport {
380 pub title: String,
382 pub period: String,
384 pub total_cases: usize,
386 pub by_risk_level: HashMap<String, usize>,
388 pub common_fraud_types: Vec<(String, usize)>,
390 pub accuracy: Option<f64>,
392 pub insights: Vec<String>,
394}
395
396impl FraudSummaryReport {
397 #[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 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 pub fn set_common_fraud_types(&mut self, types: Vec<(String, usize)>) {
420 self.common_fraud_types = types;
421 }
422
423 pub fn add_insight(&mut self, insight: String) {
425 self.insights.push(insight);
426 }
427
428 #[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 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
488pub struct ReportGenerator;
490
491impl ReportGenerator {
492 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
518pub enum ReportType {
520 CostAnalysis(CostAnalysisReport),
522 PerformanceBenchmark(PerformanceBenchmarkReport),
524 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}