netspeed_cli/profiles.rs
1//! User profiles/roles that customize output based on use case.
2//!
3//! Each profile adjusts:
4//! - Metric scoring weights (what matters most)
5//! - Usage check targets (relevant benchmarks)
6//! - Output section visibility
7//! - Rating thresholds
8
9use serde::{Deserialize, Serialize};
10
11/// Pre-defined user profiles.
12///
13/// # Example
14///
15/// ```
16/// use netspeed_cli::profiles::UserProfile;
17///
18/// // Parse from a string name
19/// assert_eq!(UserProfile::from_name("gamer"), Some(UserProfile::Gamer));
20/// assert_eq!(UserProfile::from_name("streamer"), Some(UserProfile::Streamer));
21/// assert_eq!(UserProfile::from_name("invalid"), None);
22///
23/// // Round-trip: name() → from_name()
24/// assert_eq!(UserProfile::from_name(UserProfile::RemoteWorker.name()), Some(UserProfile::RemoteWorker));
25///
26/// // Default is PowerUser
27/// assert_eq!(UserProfile::default(), UserProfile::PowerUser);
28/// ```
29#[derive(Debug, Clone, Copy, PartialEq, Eq, Default, Serialize, Deserialize)]
30pub enum UserProfile {
31 /// Tech-savvy users who want all metrics and detailed analysis.
32 #[default]
33 PowerUser,
34 /// Online gamers focused on latency, jitter, and bufferbloat.
35 Gamer,
36 /// Content consumers (Netflix, `YouTube`, etc.) focused on download speed.
37 Streamer,
38 /// Work-from-home professionals focused on upload and stability.
39 RemoteWorker,
40 /// Basic users who want a simple pass/fail assessment.
41 Casual,
42}
43
44impl UserProfile {
45 /// Get profile from string name (case-insensitive).
46 ///
47 /// Returns `Some(UserProfile)` for valid names (including aliases like
48 /// `"poweruser"` and `"remote"`), or `None` for unrecognized names.
49 ///
50 /// # Example
51 ///
52 /// ```
53 /// use netspeed_cli::profiles::UserProfile;
54 ///
55 /// // Canonical names
56 /// assert_eq!(UserProfile::from_name("power-user"), Some(UserProfile::PowerUser));
57 /// assert_eq!(UserProfile::from_name("gamer"), Some(UserProfile::Gamer));
58 /// assert_eq!(UserProfile::from_name("streamer"), Some(UserProfile::Streamer));
59 /// assert_eq!(UserProfile::from_name("remote-worker"), Some(UserProfile::RemoteWorker));
60 /// assert_eq!(UserProfile::from_name("casual"), Some(UserProfile::Casual));
61 ///
62 /// // Aliases
63 /// assert_eq!(UserProfile::from_name("poweruser"), Some(UserProfile::PowerUser));
64 /// assert_eq!(UserProfile::from_name("remote"), Some(UserProfile::RemoteWorker));
65 ///
66 /// // Case-insensitive
67 /// assert_eq!(UserProfile::from_name("GAMER"), Some(UserProfile::Gamer));
68 /// assert_eq!(UserProfile::from_name("Casual"), Some(UserProfile::Casual));
69 ///
70 /// // Invalid names return None
71 /// assert_eq!(UserProfile::from_name("admin"), None);
72 /// ```
73 #[must_use]
74 pub fn from_name(name: &str) -> Option<Self> {
75 Self::is_valid_name(name).then_some(Self::from_name_unchecked(name))
76 }
77
78 /// Check if a profile name is valid without returning the profile.
79 ///
80 /// # Example
81 ///
82 /// ```
83 /// use netspeed_cli::profiles::UserProfile;
84 ///
85 /// // Canonical names are valid
86 /// assert!(UserProfile::is_valid_name("power-user"));
87 /// assert!(UserProfile::is_valid_name("gamer"));
88 ///
89 /// // Aliases are also valid
90 /// assert!(UserProfile::is_valid_name("poweruser"));
91 /// assert!(UserProfile::is_valid_name("remote"));
92 ///
93 /// // Case-insensitive
94 /// assert!(UserProfile::is_valid_name("GAMER"));
95 ///
96 /// // Invalid names
97 /// assert!(!UserProfile::is_valid_name("admin"));
98 /// assert!(!UserProfile::is_valid_name(""));
99 /// ```
100 #[must_use]
101 pub fn is_valid_name(name: &str) -> bool {
102 matches!(
103 name.to_lowercase().as_str(),
104 "power-user"
105 | "poweruser"
106 | "gamer"
107 | "streamer"
108 | "remote-worker"
109 | "remoteworker"
110 | "remote"
111 | "casual"
112 )
113 }
114
115 /// Internal: convert validated name to profile (assumes valid input).
116 fn from_name_unchecked(name: &str) -> Self {
117 match name.to_lowercase().as_str() {
118 "power-user" | "poweruser" => Self::PowerUser,
119 "gamer" => Self::Gamer,
120 "streamer" => Self::Streamer,
121 "remote-worker" | "remoteworker" | "remote" => Self::RemoteWorker,
122 "casual" => Self::Casual,
123 _ => Self::PowerUser, // Safe default
124 }
125 }
126
127 /// Validate this profile name and return error message if invalid.
128 ///
129 /// Returns `Ok(())` if valid, `Err(msg)` with the list of valid options if invalid.
130 /// Use this for config-file validation where you need an error message;
131 /// use [`from_name()`](UserProfile::from_name) if you just need the `UserProfile` value.
132 ///
133 /// # Example
134 ///
135 /// ```
136 /// use netspeed_cli::profiles::UserProfile;
137 ///
138 /// // Valid names pass validation
139 /// assert!(UserProfile::validate("power-user").is_ok());
140 /// assert!(UserProfile::validate("gamer").is_ok());
141 /// assert!(UserProfile::validate("streamer").is_ok());
142 /// assert!(UserProfile::validate("remote-worker").is_ok());
143 /// assert!(UserProfile::validate("casual").is_ok());
144 ///
145 /// // Invalid names produce a descriptive error
146 /// let err = UserProfile::validate("admin").unwrap_err();
147 /// assert!(err.contains("Invalid profile"));
148 /// assert!(err.contains("admin"));
149 /// assert!(err.contains("gamer")); // lists valid options
150 /// ```
151 pub fn validate(name: &str) -> Result<(), String> {
152 if Self::is_valid_name(name) {
153 Ok(())
154 } else {
155 Err(format!(
156 "Invalid profile '{}'. Valid options: {}",
157 name,
158 Self::VALID_NAMES.join(", ")
159 ))
160 }
161 }
162
163 /// Type identifier for error messages (DIP: shared validation pattern).
164 pub const TYPE_NAME: &'static str = "profile";
165
166 /// List of valid profile names for error messages.
167 pub const VALID_NAMES: &'static [&'static str] =
168 &["power-user", "gamer", "streamer", "remote-worker", "casual"];
169
170 /// CLI-friendly name for the profile.
171 ///
172 /// # Example
173 ///
174 /// ```
175 /// use netspeed_cli::profiles::UserProfile;
176 ///
177 /// assert_eq!(UserProfile::PowerUser.name(), "power-user");
178 /// assert_eq!(UserProfile::Gamer.name(), "gamer");
179 /// assert_eq!(UserProfile::Streamer.name(), "streamer");
180 /// assert_eq!(UserProfile::RemoteWorker.name(), "remote-worker");
181 /// assert_eq!(UserProfile::Casual.name(), "casual");
182 /// ```
183 #[must_use]
184 pub fn name(&self) -> &'static str {
185 match self {
186 Self::PowerUser => "power-user",
187 Self::Gamer => "gamer",
188 Self::Streamer => "streamer",
189 Self::RemoteWorker => "remote-worker",
190 Self::Casual => "casual",
191 }
192 }
193
194 /// Display name with emoji for headers.
195 ///
196 /// # Example
197 ///
198 /// ```
199 /// use netspeed_cli::profiles::UserProfile;
200 ///
201 /// assert_eq!(UserProfile::PowerUser.display_name(), "⚙️ Power User");
202 /// assert_eq!(UserProfile::Gamer.display_name(), "🎮 Gamer");
203 /// assert_eq!(UserProfile::Streamer.display_name(), "📺 Streamer");
204 /// assert_eq!(UserProfile::RemoteWorker.display_name(), "💼 Remote Worker");
205 /// assert_eq!(UserProfile::Casual.display_name(), "👤 Casual");
206 /// ```
207 #[must_use]
208 pub fn display_name(&self) -> &'static str {
209 match self {
210 Self::PowerUser => "⚙️ Power User",
211 Self::Gamer => "🎮 Gamer",
212 Self::Streamer => "📺 Streamer",
213 Self::RemoteWorker => "💼 Remote Worker",
214 Self::Casual => "👤 Casual",
215 }
216 }
217
218 /// Description for help text.
219 ///
220 /// # Example
221 ///
222 /// ```
223 /// use netspeed_cli::profiles::UserProfile;
224 ///
225 /// // Each description highlights the profile's focus
226 /// assert!(UserProfile::Gamer.description().contains("Latency"));
227 /// assert!(UserProfile::Gamer.description().contains("jitter"));
228 ///
229 /// assert!(UserProfile::Streamer.description().contains("Download"));
230 /// assert!(UserProfile::Streamer.description().contains("streaming"));
231 ///
232 /// assert!(UserProfile::RemoteWorker.description().contains("Upload"));
233 /// assert!(UserProfile::RemoteWorker.description().contains("video calls"));
234 ///
235 /// assert!(UserProfile::Casual.description().contains("pass/fail"));
236 ///
237 /// assert!(UserProfile::PowerUser.description().contains("All metrics"));
238 /// ```
239 #[must_use]
240 pub fn description(&self) -> &'static str {
241 match self {
242 Self::PowerUser => "All metrics, historical trends, percentiles, stability analysis",
243 Self::Gamer => "Latency, jitter, bufferbloat — optimized for gaming performance",
244 Self::Streamer => "Download speed, consistency — optimized for streaming quality",
245 Self::RemoteWorker => {
246 "Upload speed, stability — optimized for video calls and cloud work"
247 }
248 Self::Casual => "Simple pass/fail with overall rating only",
249 }
250 }
251
252 /// Scoring weights for overall connection rating (ping, jitter, download, upload).
253 ///
254 /// Returns `(ping_weight, jitter_weight, download_weight, upload_weight)`.
255 /// Weights always sum to ~1.0, but the distribution reflects each profile's
256 /// priorities.
257 ///
258 /// # Example
259 ///
260 /// ```
261 /// use netspeed_cli::profiles::UserProfile;
262 ///
263 /// // Gamer prioritizes latency and jitter
264 /// let (ping, jitter, dl, ul) = UserProfile::Gamer.scoring_weights();
265 /// assert!(ping > dl, "gamer weights ping over download");
266 /// assert!(jitter > ul, "gamer weights jitter over upload");
267 ///
268 /// // Streamer prioritizes download speed
269 /// let (_, _, dl, _) = UserProfile::Streamer.scoring_weights();
270 /// assert!(dl >= 0.5, "streamer weights download highest");
271 ///
272 /// // RemoteWorker prioritizes upload speed
273 /// let (_, _, _, ul) = UserProfile::RemoteWorker.scoring_weights();
274 /// assert!(ul >= 0.35, "remote-worker weights upload highest");
275 ///
276 /// // All profiles' weights sum to ~1.0
277 /// for profile in [UserProfile::PowerUser, UserProfile::Gamer,
278 /// UserProfile::Streamer, UserProfile::RemoteWorker,
279 /// UserProfile::Casual] {
280 /// let (p, j, d, u) = profile.scoring_weights();
281 /// assert!((p + j + d + u - 1.0).abs() < 0.01,
282 /// "weights must sum to ~1.0 for {profile:?}");
283 /// }
284 /// ```
285 #[must_use]
286 pub fn scoring_weights(&self) -> (f64, f64, f64, f64) {
287 match self {
288 Self::PowerUser => (0.25, 0.20, 0.30, 0.25), // Balanced
289 Self::Gamer => (0.40, 0.30, 0.15, 0.15), // Latency-focused
290 Self::Streamer => (0.15, 0.15, 0.55, 0.15), // Download-focused
291 Self::RemoteWorker => (0.20, 0.15, 0.25, 0.40), // Upload-focused
292 Self::Casual => (0.25, 0.15, 0.35, 0.25), // Simplified balanced
293 }
294 }
295
296 /// Speed rating thresholds for "Excellent" (in Mbps).
297 ///
298 /// Lower values = easier to achieve. PowerUser demands the highest
299 /// bandwidth; Casual is satisfied with the least.
300 ///
301 /// # Example
302 ///
303 /// ```
304 /// use netspeed_cli::profiles::UserProfile;
305 ///
306 /// // PowerUser requires 500 Mbps for "Excellent"
307 /// assert_eq!(UserProfile::PowerUser.excellent_speed_threshold(), 500.0);
308 ///
309 /// // Gamer and RemoteWorker need only 100 Mbps (latency matters more)
310 /// assert_eq!(UserProfile::Gamer.excellent_speed_threshold(), 100.0);
311 /// assert_eq!(UserProfile::RemoteWorker.excellent_speed_threshold(), 100.0);
312 ///
313 /// // Streamer needs 200 Mbps (4K streaming headroom)
314 /// assert_eq!(UserProfile::Streamer.excellent_speed_threshold(), 200.0);
315 ///
316 /// // Casual is happy with 50 Mbps
317 /// assert_eq!(UserProfile::Casual.excellent_speed_threshold(), 50.0);
318 /// ```
319 #[must_use]
320 pub fn excellent_speed_threshold(&self) -> f64 {
321 match self {
322 Self::PowerUser => 500.0,
323 Self::Gamer | Self::RemoteWorker => 100.0, // Gamers/remote workers don't need massive bandwidth
324 Self::Streamer => 200.0, // 4K streaming needs ~50 Mbps, 200 gives headroom
325 Self::Casual => 50.0,
326 }
327 }
328
329 /// Ping rating thresholds for "Excellent" (in ms).
330 ///
331 /// Lower values = harder to achieve. Gamer demands ultra-low latency;
332 /// Casual/Streamer tolerate higher ping.
333 ///
334 /// # Example
335 ///
336 /// ```
337 /// use netspeed_cli::profiles::UserProfile;
338 ///
339 /// // Gamer needs ≤5 ms for "Excellent" ping
340 /// assert_eq!(UserProfile::Gamer.excellent_ping_threshold(), 5.0);
341 ///
342 /// // PowerUser needs ≤10 ms
343 /// assert_eq!(UserProfile::PowerUser.excellent_ping_threshold(), 10.0);
344 ///
345 /// // RemoteWorker tolerates ≤20 ms
346 /// assert_eq!(UserProfile::RemoteWorker.excellent_ping_threshold(), 20.0);
347 ///
348 /// // Streamer and Casual are fine with ≤30 ms
349 /// assert_eq!(UserProfile::Streamer.excellent_ping_threshold(), 30.0);
350 /// assert_eq!(UserProfile::Casual.excellent_ping_threshold(), 30.0);
351 /// ```
352 #[must_use]
353 pub fn excellent_ping_threshold(&self) -> f64 {
354 match self {
355 Self::PowerUser => 10.0,
356 Self::Gamer => 5.0, // Gamers need ultra-low latency
357 Self::RemoteWorker => 20.0,
358 Self::Streamer | Self::Casual => 30.0, // Streaming buffers / casual users tolerate higher ping
359 }
360 }
361
362 /// Jitter rating thresholds for "Excellent" (in ms).
363 ///
364 /// Lower values = harder to achieve. Gamer needs the most consistent
365 /// latency; Casual/Streamer tolerate more variation.
366 ///
367 /// # Example
368 ///
369 /// ```
370 /// use netspeed_cli::profiles::UserProfile;
371 ///
372 /// // Gamer needs ≤1 ms jitter for "Excellent"
373 /// assert_eq!(UserProfile::Gamer.excellent_jitter_threshold(), 1.0);
374 ///
375 /// // PowerUser needs ≤2 ms
376 /// assert_eq!(UserProfile::PowerUser.excellent_jitter_threshold(), 2.0);
377 ///
378 /// // RemoteWorker tolerates ≤3 ms
379 /// assert_eq!(UserProfile::RemoteWorker.excellent_jitter_threshold(), 3.0);
380 ///
381 /// // Streamer and Casual are fine with ≤5 ms
382 /// assert_eq!(UserProfile::Streamer.excellent_jitter_threshold(), 5.0);
383 /// assert_eq!(UserProfile::Casual.excellent_jitter_threshold(), 5.0);
384 /// ```
385 #[must_use]
386 pub fn excellent_jitter_threshold(&self) -> f64 {
387 match self {
388 Self::PowerUser => 2.0,
389 Self::Gamer => 1.0, // Gamers need consistent latency
390 Self::RemoteWorker => 3.0,
391 Self::Streamer | Self::Casual => 5.0,
392 }
393 }
394
395 /// Whether to show detailed latency section.
396 ///
397 /// All profiles except [`Casual`](UserProfile::Casual) show latency details.
398 ///
399 /// # Example
400 ///
401 /// ```
402 /// use netspeed_cli::profiles::UserProfile;
403 ///
404 /// assert!(UserProfile::PowerUser.show_latency_details());
405 /// assert!(UserProfile::Gamer.show_latency_details());
406 /// assert!(!UserProfile::Casual.show_latency_details()); // minimal output
407 /// ```
408 #[must_use]
409 pub fn show_latency_details(&self) -> bool {
410 !matches!(self, Self::Casual)
411 }
412
413 /// Whether to show bufferbloat grade.
414 ///
415 /// Only [`PowerUser`](UserProfile::PowerUser) and [`Gamer`](UserProfile::Gamer)
416 /// care about bufferbloat.
417 ///
418 /// # Example
419 ///
420 /// ```
421 /// use netspeed_cli::profiles::UserProfile;
422 ///
423 /// assert!(UserProfile::PowerUser.show_bufferbloat());
424 /// assert!(UserProfile::Gamer.show_bufferbloat());
425 /// assert!(!UserProfile::Streamer.show_bufferbloat());
426 /// assert!(!UserProfile::Casual.show_bufferbloat());
427 /// ```
428 #[must_use]
429 pub fn show_bufferbloat(&self) -> bool {
430 matches!(self, Self::PowerUser | Self::Gamer)
431 }
432
433 /// Whether to show stability analysis (CV%).
434 ///
435 /// [`PowerUser`](UserProfile::PowerUser) and [`RemoteWorker`](UserProfile::RemoteWorker)
436 /// need consistent connections for their use cases.
437 ///
438 /// # Example
439 ///
440 /// ```
441 /// use netspeed_cli::profiles::UserProfile;
442 ///
443 /// assert!(UserProfile::PowerUser.show_stability());
444 /// assert!(UserProfile::RemoteWorker.show_stability());
445 /// assert!(!UserProfile::Gamer.show_stability());
446 /// assert!(!UserProfile::Casual.show_stability());
447 /// ```
448 #[must_use]
449 pub fn show_stability(&self) -> bool {
450 matches!(self, Self::PowerUser | Self::RemoteWorker)
451 }
452
453 /// Whether to show latency percentiles.
454 ///
455 /// Only [`PowerUser`](UserProfile::PowerUser) sees percentile detail.
456 ///
457 /// # Example
458 ///
459 /// ```
460 /// use netspeed_cli::profiles::UserProfile;
461 ///
462 /// assert!(UserProfile::PowerUser.show_percentiles());
463 /// assert!(!UserProfile::Gamer.show_percentiles());
464 /// assert!(!UserProfile::Casual.show_percentiles());
465 /// ```
466 #[must_use]
467 pub fn show_percentiles(&self) -> bool {
468 matches!(self, Self::PowerUser)
469 }
470
471 /// Whether to show usage check targets.
472 ///
473 /// All profiles except [`Casual`](UserProfile::Casual) show usage checks.
474 ///
475 /// # Example
476 ///
477 /// ```
478 /// use netspeed_cli::profiles::UserProfile;
479 ///
480 /// assert!(UserProfile::PowerUser.show_usage_check());
481 /// assert!(UserProfile::Gamer.show_usage_check());
482 /// assert!(!UserProfile::Casual.show_usage_check()); // minimal output
483 /// ```
484 #[must_use]
485 pub fn show_usage_check(&self) -> bool {
486 !matches!(self, Self::Casual)
487 }
488
489 /// Whether to show download time estimates.
490 ///
491 /// [`PowerUser`](UserProfile::PowerUser) wants all metrics;
492 /// [`Casual`](UserProfile::Casual) benefits from practical time estimates.
493 ///
494 /// # Example
495 ///
496 /// ```
497 /// use netspeed_cli::profiles::UserProfile;
498 ///
499 /// assert!(UserProfile::PowerUser.show_estimates());
500 /// assert!(UserProfile::Casual.show_estimates());
501 /// assert!(!UserProfile::Gamer.show_estimates());
502 /// assert!(!UserProfile::Streamer.show_estimates());
503 /// ```
504 #[must_use]
505 pub fn show_estimates(&self) -> bool {
506 matches!(self, Self::PowerUser | Self::Casual)
507 }
508
509 /// Whether to show historical comparison.
510 ///
511 /// [`PowerUser`](UserProfile::PowerUser) tracks trends;
512 /// [`RemoteWorker`](UserProfile::RemoteWorker) monitors connection reliability over time.
513 ///
514 /// # Example
515 ///
516 /// ```
517 /// use netspeed_cli::profiles::UserProfile;
518 ///
519 /// assert!(UserProfile::PowerUser.show_history());
520 /// assert!(UserProfile::RemoteWorker.show_history());
521 /// assert!(!UserProfile::Gamer.show_history());
522 /// assert!(!UserProfile::Casual.show_history());
523 /// ```
524 #[must_use]
525 pub fn show_history(&self) -> bool {
526 matches!(self, Self::PowerUser | Self::RemoteWorker)
527 }
528
529 /// Whether to show UL/DL ratio.
530 ///
531 /// [`PowerUser`](UserProfile::PowerUser) wants all metrics;
532 /// [`RemoteWorker`](UserProfile::RemoteWorker) cares about upload relative to download.
533 ///
534 /// # Example
535 ///
536 /// ```
537 /// use netspeed_cli::profiles::UserProfile;
538 ///
539 /// assert!(UserProfile::PowerUser.show_ul_dl_ratio());
540 /// assert!(UserProfile::RemoteWorker.show_ul_dl_ratio());
541 /// assert!(!UserProfile::Streamer.show_ul_dl_ratio());
542 /// assert!(!UserProfile::Casual.show_ul_dl_ratio());
543 /// ```
544 #[must_use]
545 pub fn show_ul_dl_ratio(&self) -> bool {
546 matches!(self, Self::PowerUser | Self::RemoteWorker)
547 }
548
549 /// Whether to show peak speeds.
550 ///
551 /// All profiles except [`Casual`](UserProfile::Casual) show peak speeds.
552 ///
553 /// # Example
554 ///
555 /// ```
556 /// use netspeed_cli::profiles::UserProfile;
557 ///
558 /// assert!(UserProfile::PowerUser.show_peaks());
559 /// assert!(UserProfile::Gamer.show_peaks());
560 /// assert!(!UserProfile::Casual.show_peaks()); // minimal output
561 /// ```
562 #[must_use]
563 pub fn show_peaks(&self) -> bool {
564 !matches!(self, Self::Casual)
565 }
566
567 /// Whether to show latency under load.
568 ///
569 /// [`PowerUser`](UserProfile::PowerUser) wants all metrics;
570 /// [`Gamer`](UserProfile::Gamer) needs to know if loaded latency spikes.
571 ///
572 /// # Example
573 ///
574 /// ```
575 /// use netspeed_cli::profiles::UserProfile;
576 ///
577 /// assert!(UserProfile::PowerUser.show_latency_under_load());
578 /// assert!(UserProfile::Gamer.show_latency_under_load());
579 /// assert!(!UserProfile::Streamer.show_latency_under_load());
580 /// assert!(!UserProfile::Casual.show_latency_under_load());
581 /// ```
582 #[must_use]
583 pub fn show_latency_under_load(&self) -> bool {
584 matches!(self, Self::PowerUser | Self::Gamer)
585 }
586}
587
588/// Profile-specific usage check targets.
589///
590/// Each target represents a real-world use case (e.g., "4K streaming", "video calls")
591/// with the minimum bandwidth required to support it.
592///
593/// # Example
594///
595/// ```
596/// use netspeed_cli::profiles::{UserProfile, profile_usage_targets};
597///
598/// let targets = profile_usage_targets(UserProfile::Gamer);
599/// assert!(!targets.is_empty());
600///
601/// // Each target has a name, required bandwidth, and icon
602/// let first = &targets[0];
603/// assert!(!first.name.is_empty());
604/// assert!(first.required_mbps > 0.0);
605/// assert!(!first.icon.is_empty());
606/// ```
607pub struct UsageTarget {
608 /// Human-readable name of the use case (e.g., `"4K streaming"`, `"Video calls (1080p)"`).
609 ///
610 /// # Example
611 ///
612 /// ```
613 /// use netspeed_cli::profiles::{UserProfile, profile_usage_targets};
614 ///
615 /// let targets = profile_usage_targets(UserProfile::Streamer);
616 /// let four_k = targets.iter().find(|t| t.name.contains("4K")).unwrap();
617 /// assert_eq!(four_k.name, "4K streaming");
618 ///
619 /// let casual = profile_usage_targets(UserProfile::Casual);
620 /// assert_eq!(casual[0].name, "Web browsing");
621 /// ```
622 pub name: &'static str,
623
624 /// Minimum bandwidth in Mbps required for a good experience.
625 ///
626 /// Always positive. Higher values indicate more demanding use cases.
627 ///
628 /// # Example
629 ///
630 /// ```
631 /// use netspeed_cli::profiles::{UserProfile, profile_usage_targets};
632 ///
633 /// // 4K streaming needs at least 25 Mbps
634 /// let streamer = profile_usage_targets(UserProfile::Streamer);
635 /// let four_k = streamer.iter().find(|t| t.name.contains("4K")).unwrap();
636 /// assert_eq!(four_k.required_mbps, 25.0);
637 ///
638 /// // Web browsing is lightweight — only 1 Mbps
639 /// let casual = profile_usage_targets(UserProfile::Casual);
640 /// assert_eq!(casual[0].required_mbps, 1.0);
641 ///
642 /// // Voice chat is extremely lightweight
643 /// let gamer = profile_usage_targets(UserProfile::Gamer);
644 /// let voice = gamer.iter().find(|t| t.name.contains("Voice")).unwrap();
645 /// assert!(voice.required_mbps < 1.0);
646 /// ```
647 pub required_mbps: f64,
648
649 /// Emoji icon for visual display in the usage check section.
650 ///
651 /// Always a single emoji character (e.g., `"📺"`, `"📹"`, `"☁️"`).
652 ///
653 /// # Example
654 ///
655 /// ```
656 /// use netspeed_cli::profiles::{UserProfile, profile_usage_targets};
657 ///
658 /// let streamer = profile_usage_targets(UserProfile::Streamer);
659 /// let four_k = streamer.iter().find(|t| t.name.contains("4K")).unwrap();
660 /// assert_eq!(four_k.icon, "🎬");
661 ///
662 /// let casual = profile_usage_targets(UserProfile::Casual);
663 /// assert_eq!(casual[0].icon, "🌐"); // Web browsing
664 ///
665 /// // All targets have non-empty icons
666 /// for profile in [UserProfile::PowerUser, UserProfile::Gamer,
667 /// UserProfile::Streamer, UserProfile::RemoteWorker,
668 /// UserProfile::Casual] {
669 /// for target in &profile_usage_targets(profile) {
670 /// assert!(!target.icon.is_empty(),
671 /// "icon must not be empty for {}", target.name);
672 /// }
673 /// }
674 /// ```
675 pub icon: &'static str,
676}
677
678/// Get usage check targets for a profile.
679///
680/// Returns a list of [`UsageTarget`] entries relevant to the profile's use case.
681/// Each profile has different targets reflecting its priorities.
682///
683/// # Example
684///
685/// ```
686/// use netspeed_cli::profiles::{UserProfile, profile_usage_targets};
687///
688/// // Gamer targets include voice chat and cloud gaming
689/// let gamer = profile_usage_targets(UserProfile::Gamer);
690/// assert!(gamer.len() >= 3);
691/// assert!(gamer.iter().any(|t| t.name.contains("gaming")));
692///
693/// // Streamer targets include streaming quality levels
694/// let streamer = profile_usage_targets(UserProfile::Streamer);
695/// assert!(streamer.iter().any(|t| t.name.contains("4K")));
696///
697/// // RemoteWorker targets include video calls and file uploads
698/// let remote = profile_usage_targets(UserProfile::RemoteWorker);
699/// assert!(remote.iter().any(|t| t.name.contains("Video calls")));
700///
701/// // Casual has the fewest targets
702/// let casual = profile_usage_targets(UserProfile::Casual);
703/// assert!(casual.len() < gamer.len());
704///
705/// // All profiles have at least one target
706/// for profile in [UserProfile::PowerUser, UserProfile::Gamer,
707/// UserProfile::Streamer, UserProfile::RemoteWorker,
708/// UserProfile::Casual] {
709/// assert!(!profile_usage_targets(profile).is_empty());
710/// }
711/// ```
712#[must_use]
713pub fn profile_usage_targets(profile: UserProfile) -> Vec<UsageTarget> {
714 match profile {
715 UserProfile::Gamer => vec![
716 UsageTarget {
717 name: "Online gaming (1080p)",
718 required_mbps: 3.0,
719 icon: "🎮",
720 },
721 UsageTarget {
722 name: "Game downloads (50 GB)",
723 required_mbps: 100.0,
724 icon: "💿",
725 },
726 UsageTarget {
727 name: "Game updates (5 GB)",
728 required_mbps: 50.0,
729 icon: "🔄",
730 },
731 UsageTarget {
732 name: "Cloud gaming (Stadia)",
733 required_mbps: 35.0,
734 icon: "☁️",
735 },
736 UsageTarget {
737 name: "Voice chat (Discord)",
738 required_mbps: 0.1,
739 icon: "🎙️",
740 },
741 ],
742 UserProfile::Streamer => vec![
743 UsageTarget {
744 name: "SD streaming (480p)",
745 required_mbps: 3.0,
746 icon: "📺",
747 },
748 UsageTarget {
749 name: "HD streaming (1080p)",
750 required_mbps: 5.0,
751 icon: "📺",
752 },
753 UsageTarget {
754 name: "4K streaming",
755 required_mbps: 25.0,
756 icon: "🎬",
757 },
758 UsageTarget {
759 name: "8K streaming",
760 required_mbps: 80.0,
761 icon: "🎬",
762 },
763 UsageTarget {
764 name: "Multiple streams (3x)",
765 required_mbps: 75.0,
766 icon: "📺",
767 },
768 ],
769 UserProfile::RemoteWorker => vec![
770 UsageTarget {
771 name: "Video calls (1080p)",
772 required_mbps: 3.0,
773 icon: "📹",
774 },
775 UsageTarget {
776 name: "Video calls (4K)",
777 required_mbps: 8.0,
778 icon: "📹",
779 },
780 UsageTarget {
781 name: "Screen sharing",
782 required_mbps: 5.0,
783 icon: "🖥️",
784 },
785 UsageTarget {
786 name: "Large file upload",
787 required_mbps: 50.0,
788 icon: "📤",
789 },
790 UsageTarget {
791 name: "Cloud backup",
792 required_mbps: 20.0,
793 icon: "☁️",
794 },
795 ],
796 UserProfile::PowerUser => vec![
797 UsageTarget {
798 name: "Video calls (1080p)",
799 required_mbps: 3.0,
800 icon: "📹",
801 },
802 UsageTarget {
803 name: "HD streaming",
804 required_mbps: 5.0,
805 icon: "📺",
806 },
807 UsageTarget {
808 name: "4K streaming",
809 required_mbps: 25.0,
810 icon: "🎬",
811 },
812 UsageTarget {
813 name: "Cloud gaming",
814 required_mbps: 35.0,
815 icon: "☁️",
816 },
817 UsageTarget {
818 name: "Large file transfers",
819 required_mbps: 100.0,
820 icon: "📤",
821 },
822 ],
823 UserProfile::Casual => vec![
824 UsageTarget {
825 name: "Web browsing",
826 required_mbps: 1.0,
827 icon: "🌐",
828 },
829 UsageTarget {
830 name: "Email",
831 required_mbps: 0.5,
832 icon: "📧",
833 },
834 UsageTarget {
835 name: "SD video",
836 required_mbps: 3.0,
837 icon: "📺",
838 },
839 ],
840 }
841}
842
843#[cfg(test)]
844mod tests {
845 use super::*;
846
847 #[test]
848 fn test_is_valid_name() {
849 assert!(UserProfile::is_valid_name("gamer"));
850 assert!(UserProfile::is_valid_name("GAMER"));
851 assert!(UserProfile::is_valid_name("power-user"));
852 // Aliases
853 assert!(UserProfile::is_valid_name("remote")); // alias for remote-worker
854 assert!(UserProfile::is_valid_name("poweruser")); // alias without hyphen
855 assert!(!UserProfile::is_valid_name("invalid"));
856 }
857
858 #[test]
859 fn test_validate_valid() {
860 assert!(UserProfile::validate("gamer").is_ok());
861 assert!(UserProfile::validate("streamer").is_ok());
862 assert!(UserProfile::validate("casual").is_ok());
863 }
864
865 #[test]
866 fn test_validate_invalid() {
867 let result = UserProfile::validate("invalid");
868 assert!(result.is_err());
869 let err = result.unwrap_err();
870 assert!(err.contains("Invalid profile"));
871 assert!(err.contains("valid"));
872 }
873
874 #[test]
875 fn test_profile_from_name() {
876 assert!(UserProfile::from_name("gamer").is_some());
877 assert!(UserProfile::from_name("GAMER").is_some());
878 assert!(UserProfile::from_name("streamer").is_some());
879 assert!(UserProfile::from_name("remote-worker").is_some());
880 assert!(UserProfile::from_name("power-user").is_some());
881 assert!(UserProfile::from_name("casual").is_some());
882 assert!(UserProfile::from_name("invalid").is_none());
883 }
884
885 #[test]
886 fn test_profile_name_roundtrip() {
887 for profile in [
888 UserProfile::PowerUser,
889 UserProfile::Gamer,
890 UserProfile::Streamer,
891 UserProfile::RemoteWorker,
892 UserProfile::Casual,
893 ] {
894 assert_eq!(UserProfile::from_name(profile.name()), Some(profile));
895 }
896 }
897
898 #[test]
899 fn test_scoring_weights_sum() {
900 for profile in [
901 UserProfile::PowerUser,
902 UserProfile::Gamer,
903 UserProfile::Streamer,
904 UserProfile::RemoteWorker,
905 UserProfile::Casual,
906 ] {
907 let (ping_w, jitter_w, dl_w, ul_w) = profile.scoring_weights();
908 assert!(
909 (ping_w + jitter_w + dl_w + ul_w - 1.0).abs() < 0.01,
910 "Weights should sum to ~1.0 for {profile:?}"
911 );
912 }
913 }
914
915 #[test]
916 fn test_gamer_profile_priorities() {
917 let gamer = UserProfile::Gamer;
918 let (ping_w, jitter_w, dl_w, ul_w) = gamer.scoring_weights();
919 assert!(
920 ping_w > dl_w,
921 "Gamer: ping should weight more than download"
922 );
923 assert!(
924 jitter_w > ul_w,
925 "Gamer: jitter should weight more than upload"
926 );
927 assert!((gamer.excellent_ping_threshold() - 5.0).abs() < f64::EPSILON);
928 assert!(gamer.show_bufferbloat());
929 }
930
931 #[test]
932 fn test_streamer_profile_priorities() {
933 let streamer = UserProfile::Streamer;
934 let (_, _, dl_w, _) = streamer.scoring_weights();
935 assert!(dl_w >= 0.5, "Streamer: download should have highest weight");
936 assert!(streamer.show_usage_check());
937 }
938
939 #[test]
940 fn test_remote_worker_profile_priorities() {
941 let remote_worker = UserProfile::RemoteWorker;
942 let (_, _, _, ul_w) = remote_worker.scoring_weights();
943 assert!(ul_w >= 0.35, "RemoteWorker: upload should have high weight");
944 assert!(remote_worker.show_stability());
945 assert!(remote_worker.show_ul_dl_ratio());
946 }
947
948 #[test]
949 fn test_casual_profile_minimal() {
950 let casual = UserProfile::Casual;
951 assert!(!casual.show_latency_details());
952 assert!(!casual.show_bufferbloat());
953 assert!(!casual.show_stability());
954 assert!(!casual.show_percentiles());
955 assert!(!casual.show_history());
956 assert!(casual.show_estimates());
957 }
958
959 #[test]
960 fn test_power_user_shows_all() {
961 let power_user = UserProfile::PowerUser;
962 assert!(power_user.show_latency_details());
963 assert!(power_user.show_bufferbloat());
964 assert!(power_user.show_stability());
965 assert!(power_user.show_percentiles());
966 assert!(power_user.show_history());
967 }
968
969 #[test]
970 fn test_profile_usage_targets_not_empty() {
971 for profile in [
972 UserProfile::PowerUser,
973 UserProfile::Gamer,
974 UserProfile::Streamer,
975 UserProfile::RemoteWorker,
976 UserProfile::Casual,
977 ] {
978 let targets = profile_usage_targets(profile);
979 assert!(!targets.is_empty(), "{profile:?} should have usage targets");
980 }
981 }
982}