Skip to main content

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}