1#![allow(clippy::cast_precision_loss, clippy::cast_possible_truncation)]
21
22use owo_colors::OwoColorize;
23
24use crate::terminal;
25
26const BAR_WIDTH: usize = 10;
29const NAME_WIDTH: usize = 26;
30const LINE_WIDTH: usize = 86;
31
32pub struct UsageScenario {
36 pub name: &'static str,
37 pub required_mbps: f64,
38 pub icon: &'static str,
39 pub concurrent_label: &'static str,
40}
41
42pub struct ScenarioCategory {
44 pub name: &'static str,
45 pub icon: &'static str,
46 pub scenarios: &'static [UsageScenario],
47}
48
49pub struct ScenarioStatus {
51 pub scenario: &'static UsageScenario,
52 pub concurrent: u32,
53 pub headroom_pct: f64,
54 pub is_met: bool,
55}
56
57#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord)]
59pub enum HeadroomLevel {
60 Green, Yellow, Red, }
64
65static CAT_COMMUNICATION: ScenarioCategory = ScenarioCategory {
68 name: "COMMUNICATION & COLLABORATION",
69 icon: "💬",
70 scenarios: &[
71 UsageScenario {
72 name: "HD Video Calls (Zoom/Teams+Share)",
73 required_mbps: 8.0,
74 icon: "📹",
75 concurrent_label: "calls",
76 },
77 UsageScenario {
78 name: "4K Video Calls (FaceTime/Meet)",
79 required_mbps: 25.0,
80 icon: "📹",
81 concurrent_label: "calls",
82 },
83 UsageScenario {
84 name: "VoIP + Encrypted VPN",
85 required_mbps: 2.0,
86 icon: "🔒",
87 concurrent_label: "sessions",
88 },
89 ],
90};
91
92static CAT_STREAMING: ScenarioCategory = ScenarioCategory {
93 name: "STREAMING & ENTERTAINMENT",
94 icon: "🎬",
95 scenarios: &[
96 UsageScenario {
97 name: "4K HDR Streaming (Netflix/Disney+)",
98 required_mbps: 35.0,
99 icon: "📺",
100 concurrent_label: "streams",
101 },
102 UsageScenario {
103 name: "Cloud Gaming (GeForce Now/Xbox)",
104 required_mbps: 50.0,
105 icon: "🎮",
106 concurrent_label: "sessions",
107 },
108 UsageScenario {
109 name: "Live Broadcast Upload (Twitch/YT)",
110 required_mbps: 30.0,
111 icon: "📡",
112 concurrent_label: "streams",
113 },
114 ],
115};
116
117static CAT_PRODUCTIVITY: ScenarioCategory = ScenarioCategory {
118 name: "WORK & PRODUCTIVITY",
119 icon: "💼",
120 scenarios: &[
121 UsageScenario {
122 name: "Cloud Sync Bulk Upload (Drive/Dropbox)",
123 required_mbps: 50.0,
124 icon: "☁️",
125 concurrent_label: "syncs",
126 },
127 UsageScenario {
128 name: "4K Video Upload (YouTube Creator)",
129 required_mbps: 80.0,
130 icon: "🎥",
131 concurrent_label: "uploads",
132 },
133 UsageScenario {
134 name: "Remote Desktop HD (Parsec/TeamViewer)",
135 required_mbps: 30.0,
136 icon: "🖥️",
137 concurrent_label: "sessions",
138 },
139 ],
140};
141
142static CAT_SMART_HOME: ScenarioCategory = ScenarioCategory {
143 name: "SMART HOME & IOT",
144 icon: "🏠",
145 scenarios: &[
146 UsageScenario {
147 name: "4x 1080p Security Cameras",
148 required_mbps: 20.0,
149 icon: "📷",
150 concurrent_label: "arrays",
151 },
152 UsageScenario {
153 name: "50+ IoT Devices Hub",
154 required_mbps: 5.0,
155 icon: "🔌",
156 concurrent_label: "hubs",
157 },
158 ],
159};
160
161static CAT_NEXTGEN: ScenarioCategory = ScenarioCategory {
162 name: "NEXT-GEN / HEAVY USAGE",
163 icon: "🚀",
164 scenarios: &[
165 UsageScenario {
166 name: "8K Streaming (YouTube 8K/AV1)",
167 required_mbps: 100.0,
168 icon: "🎬",
169 concurrent_label: "streams",
170 },
171 UsageScenario {
172 name: "VR/AR Streaming (Quest 3/Vision Pro)",
173 required_mbps: 80.0,
174 icon: "🥽",
175 concurrent_label: "sessions",
176 },
177 UsageScenario {
178 name: "AI Model Download (7-70GB LLM)",
179 required_mbps: 200.0,
180 icon: "🤖",
181 concurrent_label: "downloads",
182 },
183 UsageScenario {
184 name: "4x Simultaneous 4K Streams",
185 required_mbps: 140.0,
186 icon: "👨👩👧👦",
187 concurrent_label: "households",
188 },
189 ],
190};
191
192const ALL_CATEGORIES: &[&ScenarioCategory] = &[
194 &CAT_COMMUNICATION,
195 &CAT_STREAMING,
196 &CAT_PRODUCTIVITY,
197 &CAT_SMART_HOME,
198 &CAT_NEXTGEN,
199];
200
201#[must_use]
203pub fn all_categories() -> &'static [&'static ScenarioCategory] {
204 ALL_CATEGORIES
205}
206
207#[must_use]
211pub fn compute_all_statuses(dl_mbps: f64) -> Vec<Vec<ScenarioStatus>> {
212 all_categories()
213 .iter()
214 .map(|cat| {
215 cat.scenarios
216 .iter()
217 .map(|s| compute_scenario_status(dl_mbps, s))
218 .collect()
219 })
220 .collect()
221}
222
223fn compute_scenario_status(dl_mbps: f64, scenario: &'static UsageScenario) -> ScenarioStatus {
224 let concurrent = if scenario.required_mbps > 0.0 {
227 (dl_mbps / scenario.required_mbps)
228 .floor()
229 .clamp(0.0, f64::from(u32::MAX)) as u32
230 } else {
231 0
232 };
233 let headroom_pct = if scenario.required_mbps > 0.0 {
234 ((dl_mbps - scenario.required_mbps) / scenario.required_mbps * 100.0).max(0.0)
235 } else {
236 100.0
237 };
238 let is_met = dl_mbps >= scenario.required_mbps;
239
240 ScenarioStatus {
241 scenario,
242 concurrent,
243 headroom_pct,
244 is_met,
245 }
246}
247
248#[must_use]
250pub fn worst_headroom_level(statuses: &[Vec<ScenarioStatus>]) -> HeadroomLevel {
251 let mut worst = HeadroomLevel::Green;
252 for cat in statuses {
253 for s in cat {
254 let level = headroom_level(s.headroom_pct);
255 if level > worst {
256 worst = level;
257 }
258 }
259 }
260 worst
261}
262
263fn headroom_level(pct: f64) -> HeadroomLevel {
264 if pct > 50.0 {
265 HeadroomLevel::Green
266 } else if pct >= 20.0 {
267 HeadroomLevel::Yellow
268 } else {
269 HeadroomLevel::Red
270 }
271}
272
273fn render_capacity_bar(headroom_pct: f64, is_met: bool, _nc: bool, minimal: bool) -> String {
277 let fill = if is_met {
278 ((headroom_pct / 100.0) * BAR_WIDTH as f64)
279 .ceil()
280 .min(BAR_WIDTH as f64) as usize
281 } else {
282 0
283 };
284 let empty = BAR_WIDTH.saturating_sub(fill);
285
286 if minimal {
287 format!("[{}{}]", "#".repeat(fill), "-".repeat(empty))
288 } else if terminal::no_color() {
289 format!("[{}{}]", "█".repeat(fill), "░".repeat(empty))
290 } else {
291 let bar_color = if headroom_pct > 50.0 {
292 "green"
293 } else if headroom_pct >= 20.0 {
294 "yellow"
295 } else {
296 "red"
297 };
298 let filled = "█".repeat(fill);
299 let empty_str = "░".repeat(empty);
300 match bar_color {
301 "green" => format!("[{}{}]", filled.green(), empty_str),
302 "yellow" => format!("[{}{}]", filled.yellow(), empty_str),
303 _ => format!("[{}{}]", filled.red().bold(), empty_str),
304 }
305 }
306}
307
308fn render_status_symbol(headroom_pct: f64, is_met: bool) -> String {
310 let hide_emoji = terminal::no_emoji();
311 if !is_met {
312 if hide_emoji { "FAIL" } else { "❌" }.to_string()
313 } else if headroom_pct > 50.0 {
314 if hide_emoji { "OK" } else { "✅" }.to_string()
315 } else if headroom_pct >= 20.0 {
316 if hide_emoji { "WARN" } else { "⚠️" }.to_string()
317 } else {
318 if hide_emoji { "LOW" } else { "🔴" }.to_string()
319 }
320}
321
322fn render_scenario_row(status: &ScenarioStatus, nc: bool, minimal: bool) -> String {
324 let s = status.scenario;
325 let bar = render_capacity_bar(status.headroom_pct, status.is_met, nc, minimal);
326 let symbol = render_status_symbol(status.headroom_pct, status.is_met);
327 let concurrent = status.concurrent;
328
329 let name_display = if minimal || terminal::no_emoji() {
330 format!("{:<NAME_WIDTH$}", s.name)
331 } else {
332 format!("{} {:<NAME_WIDTH$}", s.icon, s.name)
333 };
334
335 let req_display = format!("{:>6.0} Mbps", s.required_mbps);
336
337 let inner = if minimal || nc {
339 format!("{name_display:<NAME_WIDTH$} {req_display} {bar} {concurrent:>3}x {symbol:<5}",)
340 } else {
341 let req_colored = if status.is_met {
343 req_display.dimmed().to_string()
344 } else {
345 req_display.red().bold().to_string()
346 };
347 format!("{name_display:<NAME_WIDTH$} {req_colored} {bar} {concurrent:>3}x {symbol:<5}",)
348 };
349
350 let content_width = LINE_WIDTH - 2;
352 let padded = format!("{inner:<content_width$}");
353 format!(" │ {padded} │")
354}
355
356fn render_category_header(cat: &ScenarioCategory, nc: bool, minimal: bool) -> String {
358 let content_width = LINE_WIDTH - 2; let title = format!(" {} {} ", cat.icon, cat.name);
360 let dashes = "─".repeat(content_width.saturating_sub(title.len()));
361 let inner = format!("{title}{dashes}");
362 let padded = format!("{inner:<content_width$}");
364 if minimal || nc {
365 format!(" │ {padded} │")
366 } else {
367 let inner_len = title.len() + dashes.len();
369 let pad = " ".repeat(content_width.saturating_sub(inner_len));
370 format!(" │ {}{}{pad} │", title.cyan().bold(), dashes.dimmed())
371 }
372}
373
374fn render_category_box(
376 cat: &ScenarioCategory,
377 statuses: &[ScenarioStatus],
378 nc: bool,
379 minimal: bool,
380) -> String {
381 let mut lines = Vec::new();
382 let content_width = LINE_WIDTH - 2; if minimal || nc {
386 lines.push(format!(" │ {:-<content_width$} │", ""));
387 } else {
388 lines.push(format!(" │ {} │", "─".repeat(content_width).dimmed()));
389 }
390
391 lines.push(render_category_header(cat, nc, minimal));
393
394 for status in statuses {
396 lines.push(render_scenario_row(status, nc, minimal));
397 }
398
399 if minimal || nc {
401 lines.push(format!(" │ {:-<content_width$} │", ""));
402 } else {
403 lines.push(format!(" │ {} │", "─".repeat(content_width).dimmed()));
404 }
405
406 lines.join("\n")
407}
408
409fn render_section_header(dl_mbps: f64, nc: bool, minimal: bool) -> String {
411 let title = format!(" USAGE CAPABILITY — {dl_mbps:.0} Mbps ");
412 let left = (LINE_WIDTH.saturating_sub(title.len())) / 2;
413 let right = LINE_WIDTH.saturating_sub(left).saturating_sub(title.len());
414 if minimal || nc {
415 format!(" +{:─<left$}{}{:─<right$}+", "", title, "")
416 } else {
417 format!(
418 " {}{}{}{}{}",
419 "┌".dimmed(),
420 "─".repeat(left).dimmed(),
421 title.cyan().bold(),
422 "─".repeat(right).dimmed(),
423 "┐".dimmed(),
424 )
425 }
426}
427
428fn render_section_footer(nc: bool, minimal: bool) -> String {
430 if minimal || nc {
431 format!(" +{:─<LINE_WIDTH$}+", "")
432 } else {
433 format!(" {}{:─<LINE_WIDTH$}{}", "└".dimmed(), "", "┘".dimmed(),)
434 }
435}
436
437fn render_summary(
439 statuses: &[Vec<ScenarioStatus>],
440 dl_mbps: f64,
441 nc: bool,
442 minimal: bool,
443) -> String {
444 let mut lines = Vec::new();
445
446 if minimal || nc {
447 lines.push(String::new());
448 lines.push(
449 " ---- SUMMARY --------------------------------------------------------".to_string(),
450 );
451 } else {
452 lines.push(String::new());
453 lines.push(format!(
454 " {}",
455 "──── SUMMARY ────────────────────────────────────────────────".dimmed()
456 ));
457 }
458
459 lines.push(format!(" Your {dl_mbps:.0} Mbps connection supports:"));
460
461 let mut highlights: Vec<(&'static UsageScenario, u32)> = Vec::new();
463 for cat in statuses {
464 for s in cat {
465 if s.concurrent > 0 {
466 highlights.push((s.scenario, s.concurrent));
467 }
468 }
469 }
470 highlights.sort_by_key(|(_, c)| std::cmp::Reverse(*c));
471
472 for (scenario, count) in highlights.iter().take(5) {
474 if minimal || nc {
475 lines.push(format!(
476 " - {:>3}x {} {}",
477 count, scenario.name, scenario.concurrent_label,
478 ));
479 } else {
480 lines.push(format!(
481 " {} {:>3}x {} {}",
482 "•".cyan(),
483 count.to_string().green().bold(),
484 scenario.name,
485 scenario.concurrent_label.dimmed(),
486 ));
487 }
488 }
489
490 lines.join("\n")
491}
492
493fn render_recommendation(
495 statuses: &[Vec<ScenarioStatus>],
496 _dl_mbps: f64,
497 nc: bool,
498 minimal: bool,
499) -> String {
500 let mut worst: Option<&ScenarioStatus> = None;
502 for cat in statuses {
503 for s in cat {
504 if s.is_met {
505 match worst {
506 None => worst = Some(s),
507 Some(w) if s.headroom_pct < w.headroom_pct => worst = Some(s),
508 _ => {}
509 }
510 }
511 }
512 }
513
514 let Some(worst_s) = worst else {
515 let mut lines = Vec::new();
517 lines.push(String::new());
518 if minimal || nc {
519 lines.push(" [!] Your connection speed is insufficient for modern usage.".to_string());
520 lines.push(" Consider upgrading to at least 100 Mbps.".to_string());
521 } else {
522 lines.push(format!(
523 " {} {}",
524 "⚠️".red().bold(),
525 "Your connection speed is insufficient for modern usage."
526 .red()
527 .bold(),
528 ));
529 lines.push(format!(
530 " {} to at least 100 Mbps.",
531 "Consider upgrading".bright_black(),
532 ));
533 }
534 return lines.join("\n");
535 };
536
537 let s = worst_s.scenario;
538 let recommended = (s.required_mbps * 3.0)
540 .ceil()
541 .clamp(0.0, f64::from(u32::MAX)) as u32; let mut lines = Vec::new();
544 lines.push(String::new());
545
546 if minimal || nc {
547 lines.push(format!(
548 " [!] {} has limited headroom at {:.0}%.",
549 s.name, worst_s.headroom_pct,
550 ));
551 lines.push(format!(
552 " Consider upgrading to {recommended}+ Mbps for better performance.",
553 ));
554 } else {
555 let warning_icon = if worst_s.headroom_pct < 20.0 {
556 "🔴"
557 } else {
558 "⚠️"
559 };
560 lines.push(format!(
561 " {} {} {} has limited headroom at {:.0}%.",
562 warning_icon,
563 "Recommendation:".yellow().bold(),
564 s.name.yellow(),
565 worst_s.headroom_pct,
566 ));
567 lines.push(format!(
568 " {} to {}+ Mbps for better performance.",
569 "Consider upgrading".bright_black(),
570 recommended.to_string().cyan().bold(),
571 ));
572 }
573
574 lines.join("\n")
575}
576
577#[must_use]
579pub fn format_scenario_grid(dl_mbps: f64, nc: bool, minimal: bool) -> String {
580 let statuses = compute_all_statuses(dl_mbps);
581 let mut lines = Vec::new();
582
583 lines.push(String::new());
585 lines.push(render_section_header(dl_mbps, nc, minimal));
586 lines.push(String::new());
587
588 for (i, cat) in all_categories().iter().enumerate() {
590 if i > 0 {
591 lines.push(String::new());
592 }
593 lines.push(render_category_box(cat, &statuses[i], nc, minimal));
594 }
595
596 lines.push(String::new());
597
598 lines.push(render_section_footer(nc, minimal));
600
601 lines.push(render_summary(&statuses, dl_mbps, nc, minimal));
603
604 lines.push(render_recommendation(&statuses, 0.0, nc, minimal));
606
607 lines.push(String::new());
608
609 lines.join("\n")
610}
611
612pub fn print_scenario_grid(dl_mbps: f64, minimal: bool) {
614 let nc = terminal::no_color() || minimal;
615 let output = format_scenario_grid(dl_mbps, nc, minimal);
616 eprintln!("{output}");
617}
618
619#[cfg(test)]
620mod tests {
621 use super::*;
622
623 #[test]
624 fn test_compute_scenario_status_met() {
625 let s = &CAT_COMMUNICATION.scenarios[0]; let status = compute_scenario_status(100.0, s);
627 assert!(status.is_met);
628 assert_eq!(status.concurrent, 12); assert!((status.headroom_pct - 1150.0).abs() < 1.0); }
631
632 #[test]
633 fn test_compute_scenario_status_not_met() {
634 let s = &CAT_NEXTGEN.scenarios[2]; let status = compute_scenario_status(50.0, s);
636 assert!(!status.is_met);
637 assert_eq!(status.concurrent, 0);
638 assert!((status.headroom_pct - 0.0).abs() < 0.01);
639 }
640
641 #[test]
642 fn test_headroom_level() {
643 assert_eq!(headroom_level(80.0), HeadroomLevel::Green);
644 assert_eq!(headroom_level(50.1), HeadroomLevel::Green);
645 assert_eq!(headroom_level(50.0), HeadroomLevel::Yellow);
646 assert_eq!(headroom_level(20.0), HeadroomLevel::Yellow);
647 assert_eq!(headroom_level(19.9), HeadroomLevel::Red);
648 assert_eq!(headroom_level(0.0), HeadroomLevel::Red);
649 }
650
651 #[test]
652 fn test_worst_headroom_level_all_green() {
653 let statuses = compute_all_statuses(1000.0); let worst = worst_headroom_level(&statuses);
655 assert_eq!(worst, HeadroomLevel::Green);
656 }
657
658 #[test]
659 fn test_worst_headroom_level_some_red() {
660 let statuses = compute_all_statuses(50.0); let worst = worst_headroom_level(&statuses);
662 assert_eq!(worst, HeadroomLevel::Red);
664 }
665
666 #[test]
667 fn test_render_capacity_bar_full() {
668 let bar = render_capacity_bar(100.0, true, false, false);
669 assert!(bar.contains("##########") || bar.contains("██████████"));
670 }
671
672 #[test]
673 fn test_render_capacity_bar_empty() {
674 let bar = render_capacity_bar(0.0, false, false, false);
675 assert!(bar.contains("----------") || bar.contains("░░░░░░░░░░"));
676 }
677
678 #[test]
679 fn test_render_capacity_bar_half() {
680 let bar = render_capacity_bar(50.0, true, false, false);
681 assert!(bar.contains("#####") || bar.contains("█████"));
683 }
684
685 #[test]
686 fn test_format_scenario_grid_contains_header() {
687 let output = format_scenario_grid(500.0, true, false);
688 assert!(output.contains("USAGE CAPABILITY"));
689 assert!(output.contains("COMMUNICATION"));
690 assert!(output.contains("STREAMING"));
691 }
692
693 #[test]
694 fn test_format_scenario_grid_contains_summary() {
695 let output = format_scenario_grid(500.0, true, false);
696 assert!(output.contains("SUMMARY"));
697 assert!(output.contains("500 Mbps"));
698 }
699
700 #[test]
701 fn test_all_categories_count() {
702 let cats = all_categories();
703 assert_eq!(cats.len(), 5);
704 let total_scenarios: usize = cats.iter().map(|c| c.scenarios.len()).sum();
705 assert_eq!(total_scenarios, 15);
706 }
707
708 #[test]
709 fn test_recommendation_for_fast_connection() {
710 let output = format_scenario_grid(500.0, true, false);
712 assert!(output.contains("SUMMARY"));
713 assert!(output.contains("500 Mbps"));
714 }
715
716 #[test]
717 fn test_recommendation_for_moderate_connection() {
718 let output = format_scenario_grid(100.0, true, false);
720 assert!(
721 output.contains("Recommendation")
722 || output.contains("recommend")
723 || output.contains("100 Mbps")
724 );
725 }
726
727 #[test]
728 fn test_recommendation_for_slow_connection() {
729 let output = format_scenario_grid(5.0, true, false);
730 assert!(output.contains("insufficient") || output.contains("limited"));
731 }
732
733 #[test]
736 fn test_render_status_symbol_met_high_headroom() {
737 let result = render_status_symbol(75.0, true);
739 assert!(result.contains("OK") || result.contains("✅"));
740 }
741
742 #[test]
743 fn test_render_status_symbol_met_medium_headroom() {
744 let result = render_status_symbol(35.0, true);
746 assert!(result.contains("WARN") || result.contains("⚠️"));
747 }
748
749 #[test]
750 fn test_render_status_symbol_met_low_headroom() {
751 let result = render_status_symbol(10.0, true);
753 assert!(result.contains("LOW") || result.contains("🔴"));
754 }
755
756 #[test]
757 fn test_render_status_symbol_not_met() {
758 let result = render_status_symbol(100.0, false);
760 assert!(result.contains("FAIL") || result.contains("❌"));
761 }
762
763 #[test]
764 fn test_render_category_header_colored() {
765 let cat = &CAT_COMMUNICATION;
766 let result = render_category_header(cat, false, false);
767 assert!(result.contains("COMMUNICATION"));
768 assert!(result.contains("│"));
769 }
770
771 #[test]
772 fn test_render_category_header_minimal() {
773 let cat = &CAT_STREAMING;
774 let result = render_category_header(cat, true, true);
775 assert!(result.contains("STREAMING"));
776 assert!(result.contains("│"));
777 }
778
779 #[test]
780 fn test_render_category_box_colored() {
781 let cat = &CAT_SMART_HOME;
782 let statuses = vec![
783 compute_scenario_status(100.0, &cat.scenarios[0]),
784 compute_scenario_status(100.0, &cat.scenarios[1]),
785 ];
786 let result = render_category_box(cat, &statuses, false, false);
787 assert!(result.contains("SMART HOME"));
788 assert!(result.contains("│"));
789 assert!(result.contains("─"));
790 }
791
792 #[test]
793 fn test_render_category_box_minimal() {
794 let cat = &CAT_NEXTGEN;
795 let statuses = vec![compute_scenario_status(1000.0, &cat.scenarios[0])];
796 let result = render_category_box(cat, &statuses, true, true);
797 assert!(result.contains("NEXT-GEN"));
798 assert!(result.contains("│"));
799 }
800
801 #[test]
802 fn test_render_scenario_row_colored_met() {
803 let s = &CAT_COMMUNICATION.scenarios[0]; let status = compute_scenario_status(100.0, s);
805 let result = render_scenario_row(&status, false, false);
806 assert!(result.contains("8 Mbps"));
807 assert!(result.contains("12x"));
808 }
809
810 #[test]
811 fn test_render_scenario_row_minimal_not_met() {
812 let s = &CAT_NEXTGEN.scenarios[2]; let status = compute_scenario_status(50.0, s);
814 let result = render_scenario_row(&status, true, true);
815 assert!(result.contains("200 Mbps"));
816 assert!(result.contains("FAIL") || result.contains("❌"));
818 }
819
820 #[test]
821 fn test_render_section_header_colored() {
822 let result = render_section_header(500.0, false, false);
823 assert!(result.contains("500"));
824 assert!(result.contains("Mbps"));
825 assert!(result.contains("┌"));
826 }
827
828 #[test]
829 fn test_render_section_header_minimal() {
830 let result = render_section_header(100.0, true, true);
831 assert!(result.contains("100"));
832 assert!(result.contains("+"));
833 }
834
835 #[test]
836 fn test_render_section_footer_colored() {
837 let result = render_section_footer(false, false);
838 assert!(result.contains("└"));
839 assert!(result.contains("┘"));
840 }
841
842 #[test]
843 fn test_render_section_footer_minimal() {
844 let result = render_section_footer(true, true);
845 assert!(result.contains("+"));
846 }
847
848 #[test]
849 fn test_render_summary_colored() {
850 let statuses = compute_all_statuses(500.0);
851 let result = render_summary(&statuses, 500.0, false, false);
852 assert!(result.contains("SUMMARY"));
853 assert!(result.contains("500 Mbps"));
854 }
855
856 #[test]
857 fn test_render_summary_minimal() {
858 let statuses = compute_all_statuses(100.0);
859 let result = render_summary(&statuses, 100.0, true, true);
860 assert!(result.contains("SUMMARY"));
861 assert!(result.contains("100"));
862 }
863
864 #[test]
865 fn test_render_recommendation_warning_case() {
866 let statuses = compute_all_statuses(80.0);
868 let result = render_recommendation(&statuses, 80.0, true, true);
869 assert!(result.contains("limited") || result.contains("headroom"));
870 }
871
872 #[test]
873 fn test_render_recommendation_insufficient() {
874 let statuses = compute_all_statuses(5.0);
876 let result = render_recommendation(&statuses, 5.0, true, true);
877 assert!(!result.is_empty());
879 assert!(
881 result.contains("insufficient")
882 || result.contains("upgrading")
883 || result.contains("100 Mbps")
884 || result.contains("[!]")
885 );
886 }
887
888 #[test]
889 fn test_render_recommendation_colored_warning() {
890 let statuses = compute_all_statuses(80.0);
891 let result = render_recommendation(&statuses, 80.0, false, false);
892 assert!(!result.is_empty());
893 assert!(result.contains("headroom") || result.contains("upgrading"));
895 }
896
897 #[test]
898 fn test_render_recommendation_colored_insufficient() {
899 let statuses = compute_all_statuses(5.0);
900 let result = render_recommendation(&statuses, 5.0, false, false);
901 assert!(!result.is_empty());
903 assert!(
905 result.contains("insufficient")
906 || result.contains("upgrading")
907 || result.contains("⚠️")
908 || result.contains("🔴")
909 );
910 }
911
912 #[test]
913 fn test_headroom_level_boundary_green_yellow() {
914 assert_eq!(headroom_level(50.0), HeadroomLevel::Yellow);
916 }
917
918 #[test]
919 fn test_headroom_level_boundary_yellow_red() {
920 assert_eq!(headroom_level(20.0), HeadroomLevel::Yellow);
922 }
923
924 #[test]
925 fn test_headroom_level_just_above_50() {
926 assert_eq!(headroom_level(50.01), HeadroomLevel::Green);
928 }
929
930 #[test]
931 fn test_headroom_level_just_below_20() {
932 assert_eq!(headroom_level(19.99), HeadroomLevel::Red);
934 }
935
936 #[test]
937 fn test_compute_all_statuses_empty_connection() {
938 let statuses = compute_all_statuses(0.0);
939 assert_eq!(statuses.len(), 5); for cat in &statuses {
941 for s in cat {
942 assert!(!s.is_met); assert_eq!(s.concurrent, 0);
944 }
945 }
946 }
947
948 #[test]
949 fn test_compute_all_statuses_high_connection() {
950 let statuses = compute_all_statuses(10000.0); for cat in &statuses {
952 for s in cat {
953 assert!(s.is_met); assert!(s.concurrent > 10); }
956 }
957 }
958
959 #[test]
960 fn test_worst_headroom_level_all_yellow() {
961 let statuses = compute_all_statuses(35.0);
963 let worst = worst_headroom_level(&statuses);
964 assert_eq!(worst, HeadroomLevel::Red);
966 }
967
968 #[test]
969 fn test_worst_headroom_level_mixed() {
970 let statuses = compute_all_statuses(30.0);
972 let worst = worst_headroom_level(&statuses);
973 assert_eq!(worst, HeadroomLevel::Red); }
975
976 #[test]
977 fn test_format_scenario_grid_all_categories() {
978 let output = format_scenario_grid(200.0, true, false);
979 assert!(output.contains("COMMUNICATION"));
981 assert!(output.contains("STREAMING"));
982 assert!(output.contains("PRODUCTIVITY"));
983 assert!(output.contains("SMART HOME"));
984 assert!(output.contains("NEXT-GEN"));
985 }
986
987 #[test]
988 fn test_format_scenario_grid_has_summary() {
989 let output = format_scenario_grid(100.0, true, false);
990 assert!(output.contains("SUMMARY"));
991 }
992
993 #[test]
994 fn test_format_scenario_grid_has_recommendation() {
995 let output = format_scenario_grid(100.0, true, false);
996 assert!(
998 output.contains("Recommendation")
999 || output.contains("recommend")
1000 || output.contains("upgrading")
1001 );
1002 }
1003
1004 #[test]
1005 fn test_print_scenario_grid_runs() {
1006 print_scenario_grid(100.0, false);
1008 }
1009
1010 #[test]
1011 fn test_print_scenario_grid_minimal() {
1012 print_scenario_grid(500.0, true);
1014 }
1015}