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