1use crate::compliance::risk::RiskAssessment;
4#[derive(Clone, Copy, Debug, Default, clap::ValueEnum)]
9pub enum OutputFormat {
10 #[default]
11 Table,
12 Json,
13 Yaml,
14 Markdown,
15}
16
17pub fn format_risk_report(
19 assessment: &RiskAssessment,
20 format: OutputFormat,
21 detailed: bool,
22) -> String {
23 match format {
24 OutputFormat::Table => format_risk_table(assessment, detailed),
25 OutputFormat::Json => serde_json::to_string_pretty(assessment).unwrap_or_default(),
26 OutputFormat::Yaml => serde_yaml::to_string(assessment).unwrap_or_default(),
27 OutputFormat::Markdown => format_risk_markdown(assessment, detailed),
28 }
29}
30
31fn format_risk_table(assessment: &RiskAssessment, detailed: bool) -> String {
33 let mut output = String::new();
34
35 output.push_str(&format!(
37 "\n{} Risk Assessment Report\n",
38 assessment.risk_level.emoji()
39 ));
40 output.push_str(&"ā".repeat(60));
41 output.push('\n');
42
43 output.push_str(&format!("{:<20} {}\n", "Address:", assessment.address));
45 output.push_str(&format!("{:<20} {}\n", "Chain:", assessment.chain));
46 output.push_str(&format!(
47 "{:<20} {:.1}/10\n",
48 "Risk Score:", assessment.overall_score
49 ));
50 output.push_str(&format!(
51 "{:<20} {} {:?}\n",
52 "Risk Level:",
53 assessment.risk_level.emoji(),
54 assessment.risk_level
55 ));
56 output.push_str(&format!(
57 "{:<20} {}\n",
58 "Assessed At:",
59 assessment.assessed_at.format("%Y-%m-%d %H:%M UTC")
60 ));
61
62 if detailed {
64 output.push_str("\nš Risk Factor Breakdown\n");
65 output.push_str(&"ā".repeat(60));
66 output.push('\n');
67 output.push_str(&format!(
68 "{:<25} {:<12} {:<8} {:<8} {:<10}\n",
69 "Factor", "Category", "Score", "Weight", "Weighted"
70 ));
71 output.push_str(&"ā".repeat(60));
72 output.push('\n');
73
74 for factor in &assessment.factors {
75 let weighted = factor.score * factor.weight;
76 output.push_str(&format!(
77 "{:<25} {:<12} {:<8.1} {:<8.0}% {:<10.2}\n",
78 factor.name.chars().take(24).collect::<String>(),
79 format!("{:?}", factor.category)
80 .chars()
81 .take(11)
82 .collect::<String>(),
83 factor.score,
84 factor.weight * 100.0,
85 weighted
86 ));
87 }
88 }
89
90 if !assessment.recommendations.is_empty() {
92 output.push_str("\nš” Recommendations\n");
93 output.push_str(&"ā".repeat(60));
94 output.push('\n');
95
96 for (i, rec) in assessment.recommendations.iter().enumerate() {
97 output.push_str(&format!("{}. {}\n", i + 1, rec));
98 }
99 }
100
101 output
102}
103
104fn format_risk_markdown(assessment: &RiskAssessment, detailed: bool) -> String {
106 let mut md = String::new();
107
108 md.push_str("# Risk Assessment Report\n\n");
109 md.push_str(&format!("**Address:** `{}`\n\n", assessment.address));
110 md.push_str(&format!("**Chain:** {}\n\n", assessment.chain));
111 md.push_str(&format!(
112 "**Risk Score:** {:.1}/10\n\n",
113 assessment.overall_score
114 ));
115 md.push_str(&format!(
116 "**Risk Level:** {} {:?}\n\n",
117 assessment.risk_level.emoji(),
118 assessment.risk_level
119 ));
120 md.push_str(&format!(
121 "**Assessed At:** {}\n\n",
122 assessment.assessed_at.format("%Y-%m-%d %H:%M UTC")
123 ));
124
125 if detailed {
126 md.push_str("## Risk Factor Breakdown\n\n");
127 md.push_str("| Factor | Category | Score | Weight | Weighted |\n");
128 md.push_str("|--------|----------|-------|--------|----------|\n");
129
130 for factor in &assessment.factors {
131 let weighted = factor.score * factor.weight;
132 md.push_str(&format!(
133 "| {} | {:?} | {:.1} | {:.0}% | {:.2} |\n",
134 factor.name,
135 factor.category,
136 factor.score,
137 factor.weight * 100.0,
138 weighted
139 ));
140 }
141
142 md.push('\n');
143
144 md.push_str("## Factor Details\n\n");
146 for factor in &assessment.factors {
147 md.push_str(&format!("### {} ({:?})\n\n", factor.name, factor.category));
148 md.push_str(&format!("{}\n\n", factor.description));
149
150 if !factor.evidence.is_empty() {
151 md.push_str("**Evidence:**\n");
152 for ev in &factor.evidence {
153 md.push_str(&format!("- {}\n", ev));
154 }
155 md.push('\n');
156 }
157 }
158 }
159
160 if !assessment.recommendations.is_empty() {
161 md.push_str("## Recommendations\n\n");
162 for rec in &assessment.recommendations {
163 md.push_str(&format!("- {}\n", rec));
164 }
165 md.push('\n');
166 }
167
168 md.push_str("---\n\n");
169 md.push_str("*This report was generated automatically. Always verify data from primary sources before making compliance decisions.*\n");
170
171 md
172}
173
174#[cfg(test)]
175mod tests {
176 use super::*;
177 use crate::compliance::risk::{RiskAssessment, RiskCategory, RiskFactor, RiskLevel};
178 use chrono::Utc;
179
180 fn create_test_assessment() -> RiskAssessment {
181 RiskAssessment {
182 address: "0x742d35Cc6634C0532925a3b844Bc9e7595f1b3c2".to_string(),
183 chain: "ethereum".to_string(),
184 overall_score: 4.5,
185 risk_level: RiskLevel::Medium,
186 factors: vec![
187 RiskFactor {
188 name: "Behavioral".to_string(),
189 category: RiskCategory::Behavioral,
190 score: 3.0,
191 weight: 0.25,
192 description: "Test behavioral".to_string(),
193 evidence: vec!["Evidence 1".to_string()],
194 },
195 RiskFactor {
196 name: "Association".to_string(),
197 category: RiskCategory::Association,
198 score: 6.0,
199 weight: 0.30,
200 description: "Test association".to_string(),
201 evidence: vec!["Evidence 2".to_string()],
202 },
203 ],
204 assessed_at: Utc::now(),
205 recommendations: vec!["Monitor closely".to_string()],
206 }
207 }
208
209 #[test]
210 fn test_format_risk_report_table() {
211 let assessment = create_test_assessment();
212 let output = format_risk_report(&assessment, OutputFormat::Table, false);
213 assert!(output.contains("Risk Assessment Report"));
214 assert!(output.contains("0x742d35Cc6634C0532925a3b844Bc9e7595f1b3c2"));
215 assert!(output.contains("ethereum"));
216 }
217
218 #[test]
219 fn test_format_risk_report_detailed() {
220 let assessment = create_test_assessment();
221 let output = format_risk_report(&assessment, OutputFormat::Table, true);
222 assert!(output.contains("Risk Factor Breakdown"));
223 assert!(output.contains("Behavioral"));
224 assert!(output.contains("Association"));
225 }
226
227 #[test]
228 fn test_format_risk_report_json() {
229 let assessment = create_test_assessment();
230 let output = format_risk_report(&assessment, OutputFormat::Json, false);
231 assert!(output.contains("address"));
232 assert!(output.contains("ethereum"));
233 assert!(output.contains("overall_score"));
234 }
235
236 #[test]
237 fn test_format_risk_report_yaml() {
238 let assessment = create_test_assessment();
239 let output = format_risk_report(&assessment, OutputFormat::Yaml, false);
240 assert!(output.contains("address:"));
241 assert!(output.contains("chain:"));
242 }
243
244 #[test]
245 fn test_format_risk_report_markdown() {
246 let assessment = create_test_assessment();
247 let output = format_risk_report(&assessment, OutputFormat::Markdown, true);
248 assert!(output.contains("# Risk Assessment Report"));
249 assert!(output.contains("## Risk Factor Breakdown"));
250 assert!(output.contains("## Recommendations"));
251 }
252
253 #[test]
254 fn test_format_low_risk() {
255 let mut assessment = create_test_assessment();
256 assessment.risk_level = RiskLevel::Low;
257 assessment.overall_score = 2.0;
258
259 let output = format_risk_report(&assessment, OutputFormat::Table, false);
260 assert!(output.contains("š¢"));
261 }
262
263 #[test]
264 fn test_format_high_risk() {
265 let mut assessment = create_test_assessment();
266 assessment.risk_level = RiskLevel::High;
267 assessment.overall_score = 7.5;
268
269 let output = format_risk_report(&assessment, OutputFormat::Table, false);
270 assert!(output.contains("š“"));
271 }
272
273 #[test]
274 fn test_format_critical_risk() {
275 let mut assessment = create_test_assessment();
276 assessment.risk_level = RiskLevel::Critical;
277 assessment.overall_score = 9.0;
278
279 let output = format_risk_report(&assessment, OutputFormat::Table, false);
280 assert!(output.contains("ā«"));
281 }
282
283 #[test]
284 fn test_empty_recommendations() {
285 let mut assessment = create_test_assessment();
286 assessment.recommendations = vec![];
287
288 let output = format_risk_report(&assessment, OutputFormat::Table, false);
289 assert!(output.contains("Risk Assessment Report"));
291 }
292
293 #[test]
294 fn test_markdown_no_detailed() {
295 let assessment = create_test_assessment();
296 let output = format_risk_report(&assessment, OutputFormat::Markdown, false);
297 assert!(!output.contains("## Risk Factor Breakdown"));
299 }
300
301 #[test]
306 fn test_format_risk_report_no_factors() {
307 let mut assessment = create_test_assessment();
308 assessment.factors = vec![];
309 let output = format_risk_report(&assessment, OutputFormat::Table, true);
311 assert!(output.contains("Risk Assessment Report"));
312 assert!(output.contains("Risk Factor Breakdown"));
313 }
314
315 #[test]
316 fn test_format_risk_markdown_no_factors() {
317 let mut assessment = create_test_assessment();
318 assessment.factors = vec![];
319 let output = format_risk_report(&assessment, OutputFormat::Markdown, true);
320 assert!(output.contains("# Risk Assessment Report"));
321 assert!(output.contains("## Risk Factor Breakdown"));
322 }
323
324 #[test]
325 fn test_format_risk_json_roundtrip() {
326 let assessment = create_test_assessment();
327 let json = format_risk_report(&assessment, OutputFormat::Json, false);
328 let parsed: serde_json::Value = serde_json::from_str(&json).unwrap();
330 assert_eq!(
331 parsed["address"],
332 "0x742d35Cc6634C0532925a3b844Bc9e7595f1b3c2"
333 );
334 assert_eq!(parsed["chain"], "ethereum");
335 }
336
337 #[test]
338 fn test_format_risk_yaml_roundtrip() {
339 let assessment = create_test_assessment();
340 let yaml = format_risk_report(&assessment, OutputFormat::Yaml, false);
341 let parsed: serde_yaml::Value = serde_yaml::from_str(&yaml).unwrap();
343 assert!(parsed["address"].as_str().unwrap().starts_with("0x742d"));
344 }
345
346 #[test]
347 fn test_format_risk_table_no_recommendations() {
348 let mut assessment = create_test_assessment();
349 assessment.recommendations = vec![];
350 let output = format_risk_report(&assessment, OutputFormat::Table, false);
351 assert!(!output.contains("Recommendations"));
352 }
353
354 #[test]
355 fn test_format_risk_table_many_recommendations() {
356 let mut assessment = create_test_assessment();
357 assessment.recommendations = (0..10).map(|i| format!("Recommendation {}", i)).collect();
358 let output = format_risk_report(&assessment, OutputFormat::Table, false);
359 assert!(output.contains("1."));
360 assert!(output.contains("10."));
361 }
362
363 #[test]
364 fn test_format_risk_markdown_with_evidence() {
365 let mut assessment = create_test_assessment();
366 assessment.factors[0].evidence = vec![
367 "Evidence A".to_string(),
368 "Evidence B".to_string(),
369 "Evidence C".to_string(),
370 ];
371 let output = format_risk_report(&assessment, OutputFormat::Markdown, true);
372 assert!(output.contains("Evidence A"));
373 assert!(output.contains("Evidence B"));
374 assert!(output.contains("Evidence C"));
375 }
376
377 #[test]
378 fn test_format_risk_markdown_empty_evidence() {
379 let mut assessment = create_test_assessment();
380 for factor in &mut assessment.factors {
381 factor.evidence = vec![];
382 }
383 let output = format_risk_report(&assessment, OutputFormat::Markdown, true);
384 assert!(!output.contains("**Evidence:**"));
386 }
387
388 #[test]
389 fn test_format_risk_table_long_factor_name() {
390 let mut assessment = create_test_assessment();
391 assessment.factors[0].name =
392 "A Very Long Factor Name That Exceeds 24 Characters".to_string();
393 let output = format_risk_report(&assessment, OutputFormat::Table, true);
394 assert!(output.contains("A Very Long Factor Name "));
396 }
397
398 #[test]
399 fn test_all_output_formats_no_panic() {
400 let assessment = create_test_assessment();
401 for format in [
402 OutputFormat::Table,
403 OutputFormat::Json,
404 OutputFormat::Yaml,
405 OutputFormat::Markdown,
406 ] {
407 for detailed in [true, false] {
408 let output = format_risk_report(&assessment, format, detailed);
409 assert!(!output.is_empty());
410 }
411 }
412 }
413
414 #[test]
415 fn test_output_format_default() {
416 let format = OutputFormat::default();
417 assert!(matches!(format, OutputFormat::Table));
418 }
419
420 #[test]
421 fn test_markdown_contains_disclaimer() {
422 let assessment = create_test_assessment();
423 let output = format_risk_report(&assessment, OutputFormat::Markdown, false);
424 assert!(output.contains("generated automatically"));
425 }
426
427 #[test]
428 fn test_format_all_risk_levels() {
429 let mut assessment = create_test_assessment();
430 for (level, emoji) in [
431 (RiskLevel::Low, "š¢"),
432 (RiskLevel::Medium, "š”"),
433 (RiskLevel::High, "š“"),
434 (RiskLevel::Critical, "ā«"),
435 ] {
436 assessment.risk_level = level;
437 let output = format_risk_report(&assessment, OutputFormat::Table, false);
438 assert!(output.contains(emoji));
439 let md = format_risk_report(&assessment, OutputFormat::Markdown, false);
440 assert!(md.contains(emoji));
441 }
442 }
443
444 #[test]
445 fn test_format_risk_markdown_all_categories() {
446 let mut assessment = create_test_assessment();
447 assessment.factors = vec![
448 RiskFactor {
449 name: "Behavioral".to_string(),
450 category: RiskCategory::Behavioral,
451 score: 3.0,
452 weight: 0.2,
453 description: "Behavioral analysis".to_string(),
454 evidence: vec!["evidence".to_string()],
455 },
456 RiskFactor {
457 name: "Association".to_string(),
458 category: RiskCategory::Association,
459 score: 4.0,
460 weight: 0.2,
461 description: "Association analysis".to_string(),
462 evidence: vec![],
463 },
464 RiskFactor {
465 name: "Source".to_string(),
466 category: RiskCategory::Source,
467 score: 2.0,
468 weight: 0.2,
469 description: "Source analysis".to_string(),
470 evidence: vec![],
471 },
472 RiskFactor {
473 name: "Destination".to_string(),
474 category: RiskCategory::Destination,
475 score: 1.0,
476 weight: 0.2,
477 description: "Destination analysis".to_string(),
478 evidence: vec![],
479 },
480 RiskFactor {
481 name: "Entity".to_string(),
482 category: RiskCategory::Entity,
483 score: 5.0,
484 weight: 0.2,
485 description: "Entity analysis".to_string(),
486 evidence: vec![],
487 },
488 ];
489 let output = format_risk_report(&assessment, OutputFormat::Markdown, true);
490 assert!(output.contains("Behavioral"));
491 assert!(output.contains("Association"));
492 assert!(output.contains("Source"));
493 assert!(output.contains("Destination"));
494 assert!(output.contains("Entity"));
495 }
496}