Skip to main content

fortress_rollback/sessions/
config.rs

1//! Configuration types for Fortress Rollback sessions.
2//!
3//! This module contains configuration structs that control various aspects of
4//! session behavior including synchronization, network protocol, spectator
5//! settings, and input queue sizing.
6//!
7//! # Overview
8//!
9//! | Config Type | Purpose | Key Presets |
10//! |-------------|---------|-------------|
11//! | `SyncConfig` | Sync handshake behavior | `lan()`, `mobile()`, `competitive()` |
12//! | `ProtocolConfig` | Network protocol settings | `debug()`, `mobile()` |
13//! | `SpectatorConfig` | Spectator session behavior | `broadcast()`, `fast_paced()` |
14//! | `InputQueueConfig` | Input queue sizing | `high_latency()`, `minimal()` |
15//! | `SaveMode` | Game state save strategy | `EveryFrame`, `Sparse` |
16//!
17//! # Example
18//!
19//! ```
20//! use fortress_rollback::{SyncConfig, ProtocolConfig, SessionBuilder, Config};
21//!
22//! # struct MyConfig;
23//! # impl Config for MyConfig {
24//! #     type Input = u32;
25//! #     type State = ();
26//! #     type Address = std::net::SocketAddr;
27//! # }
28//! // Use presets for common scenarios
29//! let builder = SessionBuilder::<MyConfig>::new()
30//!     .with_sync_config(SyncConfig::mobile())
31//!     .with_protocol_config(ProtocolConfig::mobile());
32//! ```
33
34use web_time::Duration;
35
36use crate::input_queue::INPUT_QUEUE_LENGTH;
37use crate::{FortressError, InvalidRequestKind};
38
39/// Configuration for the synchronization protocol.
40///
41/// This struct allows fine-tuning the sync handshake behavior for different
42/// network conditions. The defaults work well for typical networks with <15%
43/// packet loss and <100ms RTT.
44///
45/// # Forward Compatibility
46///
47/// New fields may be added to this struct in future versions. To ensure your
48/// code continues to compile, always use the `..Default::default()` or
49/// `..SyncConfig::default()` pattern when constructing instances.
50///
51/// # Example
52///
53/// ```
54/// use fortress_rollback::SyncConfig;
55/// use web_time::Duration;
56///
57/// // For high-latency networks, increase retry intervals
58/// let high_latency_config = SyncConfig {
59///     sync_retry_interval: Duration::from_millis(500),
60///     running_retry_interval: Duration::from_millis(500),
61///     keepalive_interval: Duration::from_millis(500),
62///     ..SyncConfig::default()
63/// };
64///
65/// // For lossy networks, increase required roundtrips
66/// let lossy_config = SyncConfig {
67///     num_sync_packets: 8,
68///     ..SyncConfig::default()
69/// };
70/// ```
71#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
72#[must_use = "SyncConfig has no effect unless passed to SessionBuilder::with_sync_config()"]
73pub struct SyncConfig {
74    /// Number of successful sync roundtrips required before considering
75    /// the connection synchronized. Higher values provide more confidence
76    /// but take longer to synchronize.
77    ///
78    /// Default: 5
79    pub num_sync_packets: u32,
80
81    /// Time between sync request retries during the synchronization phase.
82    /// If a sync request doesn't receive a reply within this interval,
83    /// another request is sent.
84    ///
85    /// Default: 200ms
86    pub sync_retry_interval: Duration,
87
88    /// Maximum time to wait for synchronization to complete. If sync takes
89    /// longer than this, a `SyncTimeout` event is emitted.
90    ///
91    /// Default: `None` (no timeout)
92    pub sync_timeout: Option<Duration>,
93
94    /// Time between input retries during the running phase. If we haven't
95    /// received an ack for our inputs within this interval, resend them.
96    ///
97    /// Default: 200ms
98    pub running_retry_interval: Duration,
99
100    /// Time between keepalive packets when idle. Keepalives prevent
101    /// disconnect timeouts during periods of no input.
102    ///
103    /// Default: 200ms
104    pub keepalive_interval: Duration,
105}
106
107impl Default for SyncConfig {
108    fn default() -> Self {
109        Self {
110            num_sync_packets: 5,
111            sync_retry_interval: Duration::from_millis(200),
112            sync_timeout: None,
113            running_retry_interval: Duration::from_millis(200),
114            keepalive_interval: Duration::from_millis(200),
115        }
116    }
117}
118
119impl std::fmt::Display for SyncConfig {
120    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
121        // Destructure to ensure all fields are included when new fields are added.
122        let Self {
123            num_sync_packets,
124            sync_retry_interval,
125            sync_timeout,
126            running_retry_interval,
127            keepalive_interval,
128        } = self;
129
130        write!(
131            f,
132            "SyncConfig {{ num_sync_packets: {}, sync_retry: {:?}, timeout: {}, running_retry: {:?}, keepalive: {:?} }}",
133            num_sync_packets,
134            sync_retry_interval,
135            sync_timeout.map_or_else(|| "None".to_string(), |d| format!("{:?}", d)),
136            running_retry_interval,
137            keepalive_interval,
138        )
139    }
140}
141
142impl SyncConfig {
143    /// Creates a new `SyncConfig` with default values.
144    pub fn new() -> Self {
145        Self::default()
146    }
147
148    /// Configuration preset for high-latency networks (100-200ms RTT).
149    ///
150    /// Uses longer intervals to avoid flooding the network with retries.
151    pub fn high_latency() -> Self {
152        Self {
153            num_sync_packets: 5,
154            sync_retry_interval: Duration::from_millis(400),
155            sync_timeout: Some(Duration::from_secs(10)),
156            running_retry_interval: Duration::from_millis(400),
157            keepalive_interval: Duration::from_millis(400),
158        }
159    }
160
161    /// Configuration preset for lossy networks (5-15% unidirectional packet loss).
162    ///
163    /// Uses more sync packets for higher confidence and a sync timeout.
164    ///
165    /// # Note on Packet Loss Math
166    ///
167    /// When packet loss is applied bidirectionally (both send and receive),
168    /// the effective loss rate compounds. For example:
169    /// - 10% bidirectional loss = ~19% effective (1 - 0.9 × 0.9)
170    /// - 15% bidirectional loss = ~28% effective (1 - 0.85 × 0.85)
171    /// - 20% bidirectional loss = ~36% effective (1 - 0.8 × 0.8)
172    /// - 30% bidirectional loss = ~51% effective (1 - 0.7 × 0.7)
173    ///
174    /// For bidirectional loss rates > 20%, consider using [`Self::mobile()`] instead.
175    pub fn lossy() -> Self {
176        Self {
177            num_sync_packets: 8,
178            sync_retry_interval: Duration::from_millis(200),
179            sync_timeout: Some(Duration::from_secs(10)),
180            running_retry_interval: Duration::from_millis(200),
181            keepalive_interval: Duration::from_millis(200),
182        }
183    }
184
185    /// Configuration preset for local network / LAN play.
186    ///
187    /// Uses shorter intervals and fewer sync packets for faster connection.
188    pub fn lan() -> Self {
189        Self {
190            num_sync_packets: 3,
191            sync_retry_interval: Duration::from_millis(100),
192            sync_timeout: Some(Duration::from_secs(5)),
193            running_retry_interval: Duration::from_millis(100),
194            keepalive_interval: Duration::from_millis(100),
195        }
196    }
197
198    /// Configuration preset for mobile/cellular networks.
199    ///
200    /// Mobile networks have high variability, intermittent connectivity,
201    /// and often switch between WiFi and cellular. This preset combines
202    /// aspects of high_latency and lossy with additional tolerance.
203    ///
204    /// Characteristics addressed:
205    /// - High jitter (50-150ms variation)
206    /// - Intermittent packet loss (5-20%)
207    /// - Connection handoff during WiFi/cellular switches
208    /// - Variable RTT (60-200ms)
209    pub fn mobile() -> Self {
210        Self {
211            // More sync packets to handle intermittent loss
212            num_sync_packets: 10,
213            // Longer retry interval to avoid flooding during handoffs
214            sync_retry_interval: Duration::from_millis(350),
215            // Generous timeout for connection establishment
216            sync_timeout: Some(Duration::from_secs(15)),
217            // Longer retry interval during gameplay
218            running_retry_interval: Duration::from_millis(350),
219            // More frequent keepalives to detect connection issues
220            keepalive_interval: Duration::from_millis(300),
221        }
222    }
223
224    /// Configuration preset for competitive/esports scenarios.
225    ///
226    /// Prioritizes quick detection of network issues over tolerance.
227    /// Assumes good network conditions and fails fast on problems.
228    ///
229    /// Characteristics:
230    /// - Fast sync handshake
231    /// - Quick failure detection
232    /// - Strict timeout for connection
233    pub fn competitive() -> Self {
234        Self {
235            // Fewer sync packets for faster connection
236            num_sync_packets: 4,
237            // Fast retry for quick connection
238            sync_retry_interval: Duration::from_millis(100),
239            // Strict timeout - fail fast if network is bad
240            sync_timeout: Some(Duration::from_secs(3)),
241            // Fast retries during gameplay
242            running_retry_interval: Duration::from_millis(100),
243            // Frequent keepalives for quick disconnect detection
244            keepalive_interval: Duration::from_millis(100),
245        }
246    }
247
248    /// Configuration preset for extreme/hostile network conditions (testing).
249    ///
250    /// Designed for testing scenarios with very high packet loss, aggressive
251    /// burst loss, or other extreme network impairments. Uses significantly
252    /// more sync packets and longer timeouts to maximize chance of success.
253    ///
254    /// This preset is **not recommended for production use** as it has very
255    /// long timeouts that could delay error detection in real scenarios.
256    ///
257    /// Characteristics addressed:
258    /// - High burst loss (10%+ probability, 8+ packet bursts)
259    /// - Combined high packet loss (>15%)
260    /// - Extreme jitter and latency variation
261    /// - Scenarios where multiple consecutive sync attempts may fail
262    ///
263    /// # Example
264    ///
265    /// ```
266    /// use fortress_rollback::SyncConfig;
267    ///
268    /// // For testing with aggressive burst loss
269    /// let config = SyncConfig::extreme();
270    /// assert_eq!(config.num_sync_packets, 20);
271    /// ```
272    pub fn extreme() -> Self {
273        Self {
274            // Many more sync packets to survive multiple burst losses
275            // With 10% burst probability and 8-packet bursts, we need enough
276            // retries to statistically guarantee success
277            num_sync_packets: 20,
278            // Moderate retry interval - not too fast (flooding) nor too slow
279            sync_retry_interval: Duration::from_millis(250),
280            // Very generous timeout for sync (30 seconds)
281            sync_timeout: Some(Duration::from_secs(30)),
282            // Moderate retry interval during gameplay
283            running_retry_interval: Duration::from_millis(250),
284            // Frequent keepalives to detect issues
285            keepalive_interval: Duration::from_millis(200),
286        }
287    }
288
289    /// Configuration preset for stress testing under the most hostile conditions.
290    ///
291    /// This preset is specifically designed for automated testing scenarios where
292    /// reliability is paramount, even at the cost of very long sync times. It uses
293    /// aggressive parameters to survive the most hostile simulated network conditions.
294    ///
295    /// **ONLY USE FOR TESTING** - These settings would cause unacceptable delays
296    /// in production. The 60-second sync timeout means users would wait up to a
297    /// full minute before connection failure is reported.
298    ///
299    /// Characteristics addressed:
300    /// - Extreme burst loss (10%+ probability with 8+ packet bursts)
301    /// - Very high combined packet loss (>25%)
302    /// - Multiple consecutive burst events during handshake
303    /// - Slow CI environments with timing variability (macOS CI, coverage builds)
304    ///
305    /// # Probability Analysis
306    ///
307    /// With 10% burst probability and 8-packet bursts:
308    /// - Each burst can drop 8 consecutive packets
309    /// - With 150ms retry interval and 60s timeout: ~400 retry opportunities
310    /// - With 40 required sync roundtrips spread across this window, the
311    ///   probability of success is very high even under worst-case conditions
312    ///
313    /// # Example
314    ///
315    /// ```
316    /// use fortress_rollback::SyncConfig;
317    ///
318    /// // For stress testing with extremely hostile network simulation
319    /// let config = SyncConfig::stress_test();
320    /// assert_eq!(config.num_sync_packets, 40);
321    /// ```
322    pub fn stress_test() -> Self {
323        Self {
324            // Double the sync packets compared to extreme - we have the timeout
325            // budget to spare and this dramatically increases success probability
326            num_sync_packets: 40,
327            // Faster retry interval to get more attempts within the timeout window
328            // 150ms gives ~400 attempts in 60 seconds
329            sync_retry_interval: Duration::from_millis(150),
330            // Very generous timeout for sync (60 seconds)
331            // This is acceptable for automated testing but NOT for production
332            sync_timeout: Some(Duration::from_secs(60)),
333            // Match the faster retry interval for gameplay
334            running_retry_interval: Duration::from_millis(150),
335            // Frequent keepalives to detect issues quickly once connected
336            keepalive_interval: Duration::from_millis(150),
337        }
338    }
339}
340
341/// Configuration for network protocol behavior.
342///
343/// These settings control network timing, buffering, and telemetry thresholds.
344/// The defaults work well for most scenarios; adjust for specific requirements.
345///
346/// # Forward Compatibility
347///
348/// New fields may be added to this struct in future versions. To ensure your
349/// code continues to compile, always use the `..Default::default()` or
350/// `..ProtocolConfig::default()` pattern when constructing instances.
351///
352/// # Example
353///
354/// ```
355/// use fortress_rollback::ProtocolConfig;
356/// use web_time::Duration;
357///
358/// // For competitive/LAN play, use faster quality reports
359/// let competitive_config = ProtocolConfig {
360///     quality_report_interval: Duration::from_millis(100),
361///     shutdown_delay: Duration::from_millis(3000),
362///     ..ProtocolConfig::default()
363/// };
364///
365/// // For debugging, use longer timeouts and lower thresholds
366/// let debug_config = ProtocolConfig {
367///     shutdown_delay: Duration::from_millis(10000),
368///     sync_retry_warning_threshold: 5,
369///     sync_duration_warning_ms: 1000,
370///     ..ProtocolConfig::default()
371/// };
372/// ```
373#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
374#[must_use = "ProtocolConfig has no effect unless passed to SessionBuilder::with_protocol_config()"]
375pub struct ProtocolConfig {
376    /// Interval between network quality reports.
377    ///
378    /// Lower values provide more responsive network stats but increase bandwidth
379    /// usage slightly. The quality report is a small packet that measures RTT.
380    ///
381    /// Default: 200ms
382    pub quality_report_interval: Duration,
383
384    /// Time to wait in Disconnected state before transitioning to Shutdown.
385    ///
386    /// This delay allows for graceful cleanup and final message delivery.
387    /// After this timeout, the protocol will no longer process messages.
388    ///
389    /// Default: 5000ms
390    pub shutdown_delay: Duration,
391
392    /// Number of checksums to retain for desync detection history.
393    ///
394    /// Higher values can detect older desyncs but use more memory.
395    /// Only relevant when desync detection is enabled.
396    ///
397    /// Default: 32
398    pub max_checksum_history: usize,
399
400    /// Maximum pending output messages before warning.
401    ///
402    /// When pending outputs exceed this limit, it indicates the peer
403    /// isn't acknowledging inputs quickly enough. This may suggest
404    /// network congestion or peer disconnection.
405    ///
406    /// Default: 128
407    pub pending_output_limit: usize,
408
409    /// Threshold for emitting sync retry warnings.
410    ///
411    /// Emits a telemetry warning when sync requests exceed this number.
412    /// With 5 required roundtrips and 200ms retry interval, this threshold
413    /// represents roughly 50% sustained packet loss over multiple retries.
414    ///
415    /// Default: 10
416    pub sync_retry_warning_threshold: u32,
417
418    /// Threshold for emitting sync duration warnings in milliseconds.
419    ///
420    /// Emits a telemetry warning when synchronization takes longer than this.
421    /// Typical sync should complete in ~1 second for good connections.
422    ///
423    /// Default: 3000ms
424    pub sync_duration_warning_ms: u128,
425
426    /// Multiplier for input history retention.
427    ///
428    /// Determines how many frames of received input history to retain.
429    /// The protocol keeps inputs for `input_history_multiplier * max_prediction` frames
430    /// behind the most recent received frame. This allows for packet reordering
431    /// and delayed decoding without losing the ability to decode old packets.
432    ///
433    /// Higher values use more memory but are more tolerant of extreme packet reordering.
434    ///
435    /// Default: 2
436    pub input_history_multiplier: usize,
437
438    /// Optional seed for protocol RNG, enabling deterministic behavior.
439    ///
440    /// When set to `Some(seed)`, the protocol will use a deterministic RNG seeded
441    /// with this value for generating:
442    /// - Session magic numbers (protocol identifiers)
443    /// - Sync request validation tokens
444    ///
445    /// This enables fully reproducible network sessions, which is useful for:
446    /// - Replay systems
447    /// - Deterministic testing
448    /// - Debugging network issues
449    ///
450    /// When `None` (the default), the protocol uses non-deterministic random values
451    /// for security (harder to predict session IDs) and uniqueness (different magic
452    /// numbers for each session).
453    ///
454    /// # Example
455    ///
456    /// ```
457    /// use fortress_rollback::ProtocolConfig;
458    ///
459    /// // For deterministic testing
460    /// let config = ProtocolConfig {
461    ///     protocol_rng_seed: Some(12345),
462    ///     ..ProtocolConfig::default()
463    /// };
464    ///
465    /// // Or use the deterministic preset
466    /// let config = ProtocolConfig::deterministic(42);
467    /// ```
468    ///
469    /// Default: `None` (non-deterministic)
470    pub protocol_rng_seed: Option<u64>,
471}
472
473impl Default for ProtocolConfig {
474    fn default() -> Self {
475        Self {
476            quality_report_interval: Duration::from_millis(200),
477            shutdown_delay: Duration::from_millis(5000),
478            max_checksum_history: 32,
479            pending_output_limit: 128,
480            sync_retry_warning_threshold: 10,
481            sync_duration_warning_ms: 3000,
482            input_history_multiplier: 2,
483            protocol_rng_seed: None,
484        }
485    }
486}
487
488impl std::fmt::Display for ProtocolConfig {
489    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
490        // Destructure to ensure all fields are included when new fields are added.
491        let Self {
492            quality_report_interval,
493            shutdown_delay,
494            max_checksum_history,
495            pending_output_limit,
496            sync_retry_warning_threshold,
497            sync_duration_warning_ms,
498            input_history_multiplier,
499            protocol_rng_seed,
500        } = self;
501
502        write!(
503            f,
504            "ProtocolConfig {{ quality_report: {:?}, shutdown: {:?}, checksum_history: {}, pending_limit: {}, retry_warn: {}, duration_warn_ms: {}, history_mult: {}, seed: {} }}",
505            quality_report_interval,
506            shutdown_delay,
507            max_checksum_history,
508            pending_output_limit,
509            sync_retry_warning_threshold,
510            sync_duration_warning_ms,
511            input_history_multiplier,
512            protocol_rng_seed.map_or_else(|| "None".to_string(), |s| s.to_string()),
513        )
514    }
515}
516
517impl ProtocolConfig {
518    /// Creates a new `ProtocolConfig` with default values.
519    pub fn new() -> Self {
520        Self::default()
521    }
522
523    /// Configuration preset for competitive/LAN play.
524    ///
525    /// Uses faster quality reports and shorter shutdown delay for
526    /// more responsive network stats and quicker cleanup.
527    pub fn competitive() -> Self {
528        Self {
529            quality_report_interval: Duration::from_millis(100),
530            shutdown_delay: Duration::from_millis(3000),
531            max_checksum_history: 32,
532            pending_output_limit: 128,
533            sync_retry_warning_threshold: 10,
534            sync_duration_warning_ms: 2000,
535            input_history_multiplier: 2,
536            protocol_rng_seed: None,
537        }
538    }
539
540    /// Configuration preset for high-latency WAN connections.
541    ///
542    /// Uses longer intervals and more tolerant thresholds to reduce
543    /// unnecessary warnings on slower connections.
544    pub fn high_latency() -> Self {
545        Self {
546            quality_report_interval: Duration::from_millis(400),
547            shutdown_delay: Duration::from_millis(10000),
548            max_checksum_history: 64,
549            pending_output_limit: 256,
550            sync_retry_warning_threshold: 20,
551            sync_duration_warning_ms: 10000,
552            input_history_multiplier: 3,
553            protocol_rng_seed: None,
554        }
555    }
556
557    /// Configuration preset for debugging.
558    ///
559    /// Uses longer timeouts and lower warning thresholds to make
560    /// it easier to observe telemetry events during development.
561    pub fn debug() -> Self {
562        Self {
563            quality_report_interval: Duration::from_millis(500),
564            shutdown_delay: Duration::from_millis(30000),
565            max_checksum_history: 128,
566            pending_output_limit: 64,
567            sync_retry_warning_threshold: 5,
568            sync_duration_warning_ms: 1000,
569            input_history_multiplier: 4,
570            protocol_rng_seed: None,
571        }
572    }
573
574    /// Configuration preset for mobile/cellular networks.
575    ///
576    /// Mobile networks have high variability and frequent temporary
577    /// disconnections during handoffs. This preset is more tolerant
578    /// of sync delays and allows for larger output buffers.
579    ///
580    /// Characteristics addressed:
581    /// - High jitter requiring more buffering
582    /// - Connection handoffs during WiFi/cellular switches
583    /// - Higher than normal retry expectations
584    pub fn mobile() -> Self {
585        Self {
586            // Slower quality reports to reduce bandwidth on metered connections
587            quality_report_interval: Duration::from_millis(350),
588            // Very long shutdown delay to handle reconnection attempts
589            shutdown_delay: Duration::from_millis(15000),
590            // Larger checksum history for delayed desync detection
591            max_checksum_history: 64,
592            // Higher pending output limit for buffering during jitter
593            pending_output_limit: 256,
594            // Much higher threshold before warning - mobile is expected to retry often
595            sync_retry_warning_threshold: 25,
596            // Longer sync expected on mobile
597            sync_duration_warning_ms: 12000,
598            // More history for packet reordering on mobile
599            input_history_multiplier: 3,
600            protocol_rng_seed: None,
601        }
602    }
603
604    /// Configuration preset for deterministic/reproducible sessions.
605    ///
606    /// Uses a fixed RNG seed to ensure protocol behavior is reproducible
607    /// across runs. This is essential for:
608    /// - Replay systems
609    /// - Deterministic testing
610    /// - Debugging network issues
611    /// - Cross-platform consistency
612    ///
613    /// # Arguments
614    ///
615    /// * `seed` - The RNG seed for protocol randomness
616    ///
617    /// # Example
618    ///
619    /// ```
620    /// use fortress_rollback::ProtocolConfig;
621    ///
622    /// // Create a deterministic config with seed 42
623    /// let config = ProtocolConfig::deterministic(42);
624    /// assert_eq!(config.protocol_rng_seed, Some(42));
625    /// ```
626    pub fn deterministic(seed: u64) -> Self {
627        Self {
628            protocol_rng_seed: Some(seed),
629            ..Self::default()
630        }
631    }
632
633    /// Validates the protocol configuration.
634    ///
635    /// # Errors
636    ///
637    /// Returns a [`FortressError`] if any configuration value is out of range.
638    pub fn validate(&self) -> Result<(), FortressError> {
639        // Validate quality_report_interval: 1ms to 10000ms
640        if self.quality_report_interval < Duration::from_millis(1)
641            || self.quality_report_interval > Duration::from_millis(10000)
642        {
643            return Err(InvalidRequestKind::DurationConfigOutOfRange {
644                field: "quality_report_interval",
645                min_ms: 1,
646                max_ms: 10000,
647                actual_ms: self.quality_report_interval.as_millis() as u64,
648            }
649            .into());
650        }
651
652        // Validate shutdown_delay: 1ms to 300000ms (5 minutes)
653        if self.shutdown_delay < Duration::from_millis(1)
654            || self.shutdown_delay > Duration::from_millis(300000)
655        {
656            return Err(InvalidRequestKind::DurationConfigOutOfRange {
657                field: "shutdown_delay",
658                min_ms: 1,
659                max_ms: 300000,
660                actual_ms: self.shutdown_delay.as_millis() as u64,
661            }
662            .into());
663        }
664
665        // Validate max_checksum_history: 1 to 1024
666        if self.max_checksum_history < 1 || self.max_checksum_history > 1024 {
667            return Err(InvalidRequestKind::ConfigValueOutOfRange {
668                field: "max_checksum_history",
669                min: 1,
670                max: 1024,
671                actual: self.max_checksum_history as u64,
672            }
673            .into());
674        }
675
676        // Validate pending_output_limit: 1 to 4096
677        if self.pending_output_limit < 1 || self.pending_output_limit > 4096 {
678            return Err(InvalidRequestKind::ConfigValueOutOfRange {
679                field: "pending_output_limit",
680                min: 1,
681                max: 4096,
682                actual: self.pending_output_limit as u64,
683            }
684            .into());
685        }
686
687        // Validate sync_retry_warning_threshold: 1 to 1000
688        if self.sync_retry_warning_threshold < 1 || self.sync_retry_warning_threshold > 1000 {
689            return Err(InvalidRequestKind::ConfigValueOutOfRange {
690                field: "sync_retry_warning_threshold",
691                min: 1,
692                max: 1000,
693                actual: self.sync_retry_warning_threshold as u64,
694            }
695            .into());
696        }
697
698        // Validate sync_duration_warning_ms: 1 to 300000 (5 minutes)
699        if self.sync_duration_warning_ms < 1 || self.sync_duration_warning_ms > 300000 {
700            return Err(InvalidRequestKind::ConfigValueOutOfRange {
701                field: "sync_duration_warning_ms",
702                min: 1,
703                max: 300000,
704                actual: self.sync_duration_warning_ms as u64,
705            }
706            .into());
707        }
708
709        // Validate input_history_multiplier: 1 to 16
710        if self.input_history_multiplier < 1 || self.input_history_multiplier > 16 {
711            return Err(InvalidRequestKind::ConfigValueOutOfRange {
712                field: "input_history_multiplier",
713                min: 1,
714                max: 16,
715                actual: self.input_history_multiplier as u64,
716            }
717            .into());
718        }
719
720        Ok(())
721    }
722}
723
724/// Configuration for spectator sessions.
725///
726/// These settings control spectator behavior including buffer sizes,
727/// catch-up speed, and frame lag tolerance.
728///
729/// # Example
730///
731/// ```
732/// use fortress_rollback::SpectatorConfig;
733///
734/// // For watching a fast-paced game, use larger buffer and faster catchup
735/// let fast_game_config = SpectatorConfig {
736///     buffer_size: 90,
737///     catchup_speed: 2,
738///     max_frames_behind: 15,
739///     ..SpectatorConfig::default()
740/// };
741///
742/// // For spectators on slower connections
743/// let slow_connection_config = SpectatorConfig {
744///     buffer_size: 120,
745///     max_frames_behind: 20,
746///     ..SpectatorConfig::default()
747/// };
748/// ```
749#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
750#[must_use = "SpectatorConfig has no effect unless passed to SessionBuilder::with_spectator_config()"]
751pub struct SpectatorConfig {
752    /// The number of frames of input that the spectator can buffer.
753    /// This defines how many frames of inputs from the host the spectator
754    /// can store before older inputs are overwritten.
755    ///
756    /// A larger buffer allows the spectator to tolerate more latency
757    /// or jitter, but uses more memory.
758    ///
759    /// Default: 60 (1 second at 60 FPS)
760    pub buffer_size: usize,
761
762    /// How many frames to advance per step when the spectator is behind.
763    /// When the spectator falls more than `max_frames_behind` frames behind
764    /// the host, it will advance this many frames per step to catch up.
765    ///
766    /// Higher values catch up faster but may cause visual stuttering.
767    ///
768    /// Default: 1
769    pub catchup_speed: usize,
770
771    /// The maximum number of frames the spectator can fall behind before
772    /// triggering catch-up mode. When the spectator is more than this many
773    /// frames behind the host's current frame, it will use `catchup_speed`
774    /// to advance faster.
775    ///
776    /// Default: 10
777    pub max_frames_behind: usize,
778}
779
780impl Default for SpectatorConfig {
781    fn default() -> Self {
782        Self {
783            buffer_size: 60,
784            catchup_speed: 1,
785            max_frames_behind: 10,
786        }
787    }
788}
789
790impl std::fmt::Display for SpectatorConfig {
791    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
792        // Destructure to ensure all fields are included when new fields are added.
793        let Self {
794            buffer_size,
795            catchup_speed,
796            max_frames_behind,
797        } = self;
798
799        write!(
800            f,
801            "SpectatorConfig {{ buffer: {}, catchup_speed: {}, max_behind: {} }}",
802            buffer_size, catchup_speed, max_frames_behind,
803        )
804    }
805}
806
807impl SpectatorConfig {
808    /// Creates a new `SpectatorConfig` with default values.
809    pub fn new() -> Self {
810        Self::default()
811    }
812
813    /// Configuration preset for fast-paced games.
814    ///
815    /// Uses a larger buffer and faster catch-up for games where
816    /// falling behind is more noticeable.
817    pub fn fast_paced() -> Self {
818        Self {
819            buffer_size: 90,
820            catchup_speed: 2,
821            max_frames_behind: 15,
822        }
823    }
824
825    /// Configuration preset for spectators on slower connections.
826    ///
827    /// Uses a larger buffer and more tolerance for falling behind.
828    pub fn slow_connection() -> Self {
829        Self {
830            buffer_size: 120,
831            catchup_speed: 1,
832            max_frames_behind: 20,
833        }
834    }
835
836    /// Configuration preset for local viewing with minimal latency.
837    ///
838    /// Uses smaller buffer and stricter catch-up for responsive viewing.
839    pub fn local() -> Self {
840        Self {
841            buffer_size: 30,
842            catchup_speed: 2,
843            max_frames_behind: 5,
844        }
845    }
846
847    /// Configuration preset for streaming/broadcast scenarios.
848    ///
849    /// Optimized for live event streaming, tournament broadcasts, and
850    /// replay viewers. Uses a very large buffer and conservative catch-up
851    /// to avoid visual stuttering on stream.
852    ///
853    /// Characteristics:
854    /// - Large buffer (3 seconds at 60 FPS)
855    /// - Slow, smooth catch-up to avoid jarring speed changes
856    /// - High tolerance for falling behind
857    pub fn broadcast() -> Self {
858        Self {
859            // 3 seconds of buffer at 60 FPS for smooth streaming
860            buffer_size: 180,
861            // Very slow catch-up to avoid visual stuttering on stream
862            catchup_speed: 1,
863            // Can fall far behind before catching up - prioritize smooth playback
864            max_frames_behind: 30,
865        }
866    }
867
868    /// Configuration preset for mobile/cellular spectators.
869    ///
870    /// Uses larger buffers and tolerant catch-up for variable
871    /// mobile network conditions.
872    pub fn mobile() -> Self {
873        Self {
874            // 2 seconds of buffer at 60 FPS
875            buffer_size: 120,
876            // Moderate catch-up speed
877            catchup_speed: 1,
878            // High tolerance for network variability
879            max_frames_behind: 25,
880        }
881    }
882}
883
884/// Configuration for input queue sizing.
885///
886/// These settings control the size of the input queue (circular buffer) that stores
887/// player inputs. A larger queue allows for longer input history and higher frame delays,
888/// but uses more memory.
889///
890/// # Forward Compatibility
891///
892/// New fields may be added to this struct in future versions. To ensure your
893/// code continues to compile, always use the `..Default::default()` or
894/// `..InputQueueConfig::default()` pattern when constructing instances.
895///
896/// # Memory Usage
897///
898/// Each input queue stores `queue_length` inputs per player. With 2 players and
899/// 128-frame queue (default), this is 256 input slots total.
900///
901/// # Constraints
902///
903/// - `queue_length` must be at least 2 (minimum for circular buffer operation)
904/// - `queue_length` should be a power of 2 for optimal modulo performance (not enforced)
905/// - `frame_delay` must be less than `queue_length` (enforced at session creation)
906///
907/// # Example
908///
909/// ```
910/// use fortress_rollback::InputQueueConfig;
911///
912/// // Default configuration (128 frames = ~2.1 seconds at 60 FPS)
913/// let default = InputQueueConfig::default();
914/// assert_eq!(default.queue_length, 128);
915///
916/// // For games needing longer input history
917/// let high_latency = InputQueueConfig::high_latency();
918/// assert_eq!(high_latency.queue_length, 256);
919///
920/// // For memory-constrained environments
921/// let minimal = InputQueueConfig::minimal();
922/// assert_eq!(minimal.queue_length, 32);
923/// ```
924#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
925#[must_use = "InputQueueConfig has no effect unless passed to SessionBuilder::with_input_queue_config()"]
926pub struct InputQueueConfig {
927    /// The length of the input queue (circular buffer) per player.
928    ///
929    /// This determines:
930    /// - How many frames of input history are stored
931    /// - The maximum allowed frame delay (`queue_length - 1`)
932    /// - Memory usage per player
933    ///
934    /// At 60 FPS:
935    /// - 32 frames = ~0.5 seconds
936    /// - 64 frames = ~1.1 seconds
937    /// - 128 frames (default) = ~2.1 seconds
938    /// - 256 frames = ~4.3 seconds
939    ///
940    /// # Formal Specification Alignment
941    /// - **TLA+**: `QUEUE_LENGTH` in `specs/tla/InputQueue.tla` (uses 3 for model checking)
942    /// - **Kani**: `INPUT_QUEUE_LENGTH` in `src/input_queue.rs` (uses 8 for tractable verification)
943    /// - **Z3**: `INPUT_QUEUE_LENGTH` in `tests/test_z3_verification.rs` (uses 128)
944    /// - **formal-spec.md**: INV-4 (queue length bounds), INV-5 (index validity)
945    /// - **spec-divergences.md**: Documents why different values are used
946    ///
947    /// Default: 128
948    pub queue_length: usize,
949}
950
951impl Default for InputQueueConfig {
952    fn default() -> Self {
953        Self {
954            queue_length: INPUT_QUEUE_LENGTH,
955        }
956    }
957}
958
959impl std::fmt::Display for InputQueueConfig {
960    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
961        // Destructure to ensure all fields are included when new fields are added.
962        let Self { queue_length } = self;
963        write!(f, "InputQueueConfig {{ queue_length: {} }}", queue_length)
964    }
965}
966
967impl InputQueueConfig {
968    /// Creates a new `InputQueueConfig` with default values.
969    pub fn new() -> Self {
970        Self::default()
971    }
972
973    /// Configuration for high-latency networks.
974    ///
975    /// Uses a larger queue (256 frames = ~4.3 seconds at 60 FPS) to allow
976    /// for higher frame delays and longer rollback windows.
977    pub fn high_latency() -> Self {
978        Self { queue_length: 256 }
979    }
980
981    /// Configuration for minimal memory usage.
982    ///
983    /// Uses a smaller queue (32 frames = ~0.5 seconds at 60 FPS).
984    /// Suitable for games with low latency requirements.
985    pub fn minimal() -> Self {
986        Self { queue_length: 32 }
987    }
988
989    /// Configuration for standard networks.
990    ///
991    /// Uses the default queue size (128 frames = ~2.1 seconds at 60 FPS).
992    pub fn standard() -> Self {
993        Self::default()
994    }
995
996    /// Returns the maximum allowed frame delay for this configuration.
997    ///
998    /// This is always `queue_length - 1` to ensure the circular buffer
999    /// doesn't overflow when advancing the queue head.
1000    #[must_use]
1001    pub fn max_frame_delay(&self) -> usize {
1002        self.queue_length.saturating_sub(1)
1003    }
1004
1005    /// Validates that the given frame delay is valid for this configuration.
1006    ///
1007    /// # Errors
1008    ///
1009    /// Returns a [`FortressError`] if `frame_delay >= queue_length`.
1010    pub fn validate_frame_delay(&self, frame_delay: usize) -> Result<(), FortressError> {
1011        if frame_delay >= self.queue_length {
1012            return Err(InvalidRequestKind::FrameDelayTooLarge {
1013                delay: frame_delay,
1014                max_delay: self.max_frame_delay(),
1015            }
1016            .into());
1017        }
1018        Ok(())
1019    }
1020
1021    /// Validates the configuration itself.
1022    ///
1023    /// # Errors
1024    ///
1025    /// Returns a [`FortressError`] if `queue_length < 2`.
1026    pub fn validate(&self) -> Result<(), FortressError> {
1027        if self.queue_length < 2 {
1028            return Err(InvalidRequestKind::QueueLengthTooSmall {
1029                length: self.queue_length,
1030            }
1031            .into());
1032        }
1033        Ok(())
1034    }
1035}
1036
1037/// Controls how game states are saved for rollback.
1038///
1039/// This enum replaces the boolean `sparse_saving` parameter for improved API clarity.
1040/// Using an enum makes the code self-documenting and prevents accidentally passing
1041/// the wrong boolean value.
1042///
1043/// # Choosing a Save Mode
1044///
1045/// - **`SaveMode::EveryFrame`** (default): Saves state every frame. Best when:
1046///   - State serialization is fast
1047///   - You want minimal rollback distance
1048///   - You have sufficient memory for frame history
1049///
1050/// - **`SaveMode::Sparse`**: Only saves the minimum confirmed frame. Best when:
1051///   - State serialization is expensive (complex game state)
1052///   - You want to minimize save overhead
1053///   - You can tolerate potentially longer rollbacks
1054///
1055/// # Example
1056///
1057/// ```
1058/// use fortress_rollback::{SessionBuilder, SaveMode, Config};
1059///
1060/// # struct MyConfig;
1061/// # impl Config for MyConfig {
1062/// #     type Input = u32;
1063/// #     type State = ();
1064/// #     type Address = std::net::SocketAddr;
1065/// # }
1066/// // For games with expensive state serialization
1067/// let builder = SessionBuilder::<MyConfig>::new()
1068///     .with_save_mode(SaveMode::Sparse);
1069///
1070/// // For games with fast state serialization (default)
1071/// let builder = SessionBuilder::<MyConfig>::new()
1072///     .with_save_mode(SaveMode::EveryFrame);
1073/// ```
1074#[derive(Debug, Default, Clone, Copy, PartialEq, Eq, Hash)]
1075pub enum SaveMode {
1076    /// Save game state every frame.
1077    ///
1078    /// This is the default mode. It provides the shortest possible rollback distance
1079    /// since the most recent confirmed state is always available. However, it requires
1080    /// a save operation every frame, which may be expensive for complex game states.
1081    ///
1082    /// Use this mode when:
1083    /// - Your game state is small or fast to serialize
1084    /// - You want minimal rollback distance
1085    /// - You have sufficient memory for the frame history
1086    #[default]
1087    EveryFrame,
1088
1089    /// Only save the minimum confirmed frame.
1090    ///
1091    /// In this mode, only the frame for which all inputs from all players are confirmed
1092    /// correct will be saved. This dramatically reduces the number of save operations
1093    /// but may result in longer rollbacks when predictions are incorrect.
1094    ///
1095    /// Use this mode when:
1096    /// - Saving your game state is expensive (large or complex state)
1097    /// - Advancing the game state is relatively cheap
1098    /// - You can tolerate longer rollbacks in exchange for fewer saves
1099    Sparse,
1100}
1101
1102impl std::fmt::Display for SaveMode {
1103    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
1104        match self {
1105            Self::EveryFrame => write!(f, "EveryFrame"),
1106            Self::Sparse => write!(f, "Sparse"),
1107        }
1108    }
1109}
1110
1111// =============================================================================
1112// Unit Tests
1113// =============================================================================
1114
1115#[cfg(test)]
1116#[allow(
1117    clippy::panic,
1118    clippy::unwrap_used,
1119    clippy::expect_used,
1120    clippy::indexing_slicing
1121)]
1122mod tests {
1123    use super::*;
1124
1125    // ========================================================================
1126    // SaveMode Tests
1127    // ========================================================================
1128
1129    #[test]
1130    fn test_save_mode_default_is_every_frame() {
1131        let mode = SaveMode::default();
1132        assert_eq!(mode, SaveMode::EveryFrame);
1133    }
1134
1135    #[test]
1136    fn test_save_mode_equality() {
1137        assert_eq!(SaveMode::EveryFrame, SaveMode::EveryFrame);
1138        assert_eq!(SaveMode::Sparse, SaveMode::Sparse);
1139        assert_ne!(SaveMode::EveryFrame, SaveMode::Sparse);
1140    }
1141
1142    #[test]
1143    fn test_save_mode_debug_format() {
1144        let every_frame = SaveMode::EveryFrame;
1145        let sparse = SaveMode::Sparse;
1146        assert_eq!(format!("{:?}", every_frame), "EveryFrame");
1147        assert_eq!(format!("{:?}", sparse), "Sparse");
1148    }
1149
1150    #[test]
1151    fn test_save_mode_display() {
1152        assert_eq!(SaveMode::EveryFrame.to_string(), "EveryFrame");
1153        assert_eq!(SaveMode::Sparse.to_string(), "Sparse");
1154    }
1155
1156    #[test]
1157    fn test_save_mode_clone() {
1158        let mode = SaveMode::Sparse;
1159        // Intentionally using clone to verify Clone trait works
1160        let cloned = Clone::clone(&mode);
1161        assert_eq!(mode, cloned);
1162    }
1163
1164    #[test]
1165    fn test_save_mode_copy() {
1166        let mode = SaveMode::EveryFrame;
1167        let copied: SaveMode = mode; // Copy
1168        assert_eq!(mode, copied);
1169    }
1170
1171    // ========================================================================
1172    // InputQueueConfig Tests
1173    // ========================================================================
1174
1175    #[test]
1176    fn test_input_queue_config_presets() {
1177        let high_latency = InputQueueConfig::high_latency();
1178        assert_eq!(high_latency.queue_length, 256);
1179
1180        let minimal = InputQueueConfig::minimal();
1181        assert_eq!(minimal.queue_length, 32);
1182
1183        let standard = InputQueueConfig::standard();
1184        assert_eq!(standard, InputQueueConfig::default());
1185    }
1186
1187    /// Test that standard() explicitly equals INPUT_QUEUE_LENGTH.
1188    ///
1189    /// This test catches any accidental hardcoding of the queue length value.
1190    /// Under Kani, INPUT_QUEUE_LENGTH is 8; in production it's 128.
1191    /// The test should pass in both environments.
1192    #[test]
1193    fn test_standard_preset_uses_input_queue_length_constant() {
1194        let standard = InputQueueConfig::standard();
1195        assert_eq!(
1196            standard.queue_length, INPUT_QUEUE_LENGTH,
1197            "standard() should return INPUT_QUEUE_LENGTH ({}), but got {}. \
1198             This may indicate a hardcoded value that doesn't account for \
1199             different build configurations (e.g., Kani vs production).",
1200            INPUT_QUEUE_LENGTH, standard.queue_length
1201        );
1202    }
1203
1204    /// Data-driven test: all presets should pass validation.
1205    ///
1206    /// Uses a table-driven approach to test all presets consistently.
1207    #[test]
1208    fn test_all_presets_are_valid_configurations() {
1209        let presets: &[(&str, InputQueueConfig)] = &[
1210            ("standard", InputQueueConfig::standard()),
1211            ("high_latency", InputQueueConfig::high_latency()),
1212            ("minimal", InputQueueConfig::minimal()),
1213        ];
1214
1215        for (name, config) in presets {
1216            assert!(
1217                config.validate().is_ok(),
1218                "Preset '{}' with queue_length={} should be valid, but validation failed: {:?}",
1219                name,
1220                config.queue_length,
1221                config.validate()
1222            );
1223        }
1224    }
1225
1226    /// Data-driven test: all presets should have valid max_frame_delay.
1227    #[test]
1228    fn test_all_presets_max_frame_delay_is_valid() {
1229        let presets: &[(&str, InputQueueConfig)] = &[
1230            ("standard", InputQueueConfig::standard()),
1231            ("high_latency", InputQueueConfig::high_latency()),
1232            ("minimal", InputQueueConfig::minimal()),
1233        ];
1234
1235        for (name, config) in presets {
1236            let max_delay = config.max_frame_delay();
1237            assert!(
1238                config.validate_frame_delay(max_delay).is_ok(),
1239                "Preset '{}': max_frame_delay() returned {}, but this is not a valid frame delay \
1240                 for queue_length={}",
1241                name,
1242                max_delay,
1243                config.queue_length
1244            );
1245        }
1246    }
1247
1248    /// Test that hardcoded preset values match their documented values.
1249    ///
1250    /// Note: This test uses hardcoded values intentionally for high_latency and minimal
1251    /// because those presets ARE hardcoded in the implementation. This test documents
1252    /// and verifies that contract.
1253    #[test]
1254    fn test_hardcoded_preset_values() {
1255        // high_latency() is hardcoded to 256 (documented: ~4.3 seconds at 60 FPS)
1256        assert_eq!(
1257            InputQueueConfig::high_latency().queue_length,
1258            256,
1259            "high_latency() is documented to return queue_length=256"
1260        );
1261
1262        // minimal() is hardcoded to 32 (documented: ~0.5 seconds at 60 FPS)
1263        assert_eq!(
1264            InputQueueConfig::minimal().queue_length,
1265            32,
1266            "minimal() is documented to return queue_length=32"
1267        );
1268    }
1269
1270    #[test]
1271    fn test_input_queue_config_max_frame_delay() {
1272        let config = InputQueueConfig { queue_length: 64 };
1273        assert_eq!(config.max_frame_delay(), 63);
1274
1275        let config = InputQueueConfig { queue_length: 128 };
1276        assert_eq!(config.max_frame_delay(), 127);
1277    }
1278
1279    #[test]
1280    fn test_input_queue_config_validate() {
1281        // Valid configs
1282        assert!(InputQueueConfig { queue_length: 2 }.validate().is_ok());
1283        assert!(InputQueueConfig { queue_length: 128 }.validate().is_ok());
1284
1285        // Invalid configs
1286        assert!(InputQueueConfig { queue_length: 0 }.validate().is_err());
1287        assert!(InputQueueConfig { queue_length: 1 }.validate().is_err());
1288    }
1289
1290    #[test]
1291    fn test_input_queue_config_validate_frame_delay() {
1292        let config = InputQueueConfig { queue_length: 32 };
1293
1294        // Valid delays
1295        assert!(config.validate_frame_delay(0).is_ok());
1296        assert!(config.validate_frame_delay(31).is_ok());
1297
1298        // Invalid delays
1299        assert!(config.validate_frame_delay(32).is_err());
1300        assert!(config.validate_frame_delay(100).is_err());
1301    }
1302
1303    #[test]
1304    fn test_input_queue_config_display() {
1305        let config = InputQueueConfig { queue_length: 128 };
1306        assert_eq!(config.to_string(), "InputQueueConfig { queue_length: 128 }");
1307
1308        let config = InputQueueConfig { queue_length: 256 };
1309        assert_eq!(config.to_string(), "InputQueueConfig { queue_length: 256 }");
1310    }
1311
1312    // ========================================================================
1313    // SyncConfig Tests
1314    // ========================================================================
1315
1316    #[test]
1317    fn sync_config_default_values() {
1318        let config = SyncConfig::default();
1319        assert_eq!(config.num_sync_packets, 5);
1320        assert_eq!(config.sync_retry_interval, Duration::from_millis(200));
1321        assert!(config.sync_timeout.is_none());
1322        assert_eq!(config.running_retry_interval, Duration::from_millis(200));
1323        assert_eq!(config.keepalive_interval, Duration::from_millis(200));
1324    }
1325
1326    #[test]
1327    fn sync_config_new_equals_default() {
1328        let new_config = SyncConfig::new();
1329        let default_config = SyncConfig::default();
1330        assert_eq!(new_config, default_config);
1331    }
1332
1333    #[test]
1334    fn sync_config_high_latency_preset() {
1335        let config = SyncConfig::high_latency();
1336        assert_eq!(config.num_sync_packets, 5);
1337        assert_eq!(config.sync_retry_interval, Duration::from_millis(400));
1338        assert_eq!(config.sync_timeout, Some(Duration::from_secs(10)));
1339        assert_eq!(config.running_retry_interval, Duration::from_millis(400));
1340        assert_eq!(config.keepalive_interval, Duration::from_millis(400));
1341    }
1342
1343    #[test]
1344    fn sync_config_lossy_preset() {
1345        let config = SyncConfig::lossy();
1346        assert_eq!(config.num_sync_packets, 8);
1347        assert_eq!(config.sync_retry_interval, Duration::from_millis(200));
1348        assert_eq!(config.sync_timeout, Some(Duration::from_secs(10)));
1349        assert_eq!(config.running_retry_interval, Duration::from_millis(200));
1350        assert_eq!(config.keepalive_interval, Duration::from_millis(200));
1351    }
1352
1353    #[test]
1354    fn sync_config_lan_preset() {
1355        let config = SyncConfig::lan();
1356        assert_eq!(config.num_sync_packets, 3);
1357        assert_eq!(config.sync_retry_interval, Duration::from_millis(100));
1358        assert_eq!(config.sync_timeout, Some(Duration::from_secs(5)));
1359        assert_eq!(config.running_retry_interval, Duration::from_millis(100));
1360        assert_eq!(config.keepalive_interval, Duration::from_millis(100));
1361    }
1362
1363    #[test]
1364    fn sync_config_mobile_preset() {
1365        let config = SyncConfig::mobile();
1366        assert_eq!(config.num_sync_packets, 10);
1367        assert_eq!(config.sync_retry_interval, Duration::from_millis(350));
1368        assert_eq!(config.sync_timeout, Some(Duration::from_secs(15)));
1369        assert_eq!(config.running_retry_interval, Duration::from_millis(350));
1370        assert_eq!(config.keepalive_interval, Duration::from_millis(300));
1371    }
1372
1373    #[test]
1374    fn sync_config_competitive_preset() {
1375        let config = SyncConfig::competitive();
1376        assert_eq!(config.num_sync_packets, 4);
1377        assert_eq!(config.sync_retry_interval, Duration::from_millis(100));
1378        assert_eq!(config.sync_timeout, Some(Duration::from_secs(3)));
1379        assert_eq!(config.running_retry_interval, Duration::from_millis(100));
1380        assert_eq!(config.keepalive_interval, Duration::from_millis(100));
1381    }
1382
1383    #[test]
1384    fn sync_config_extreme_preset() {
1385        let config = SyncConfig::extreme();
1386        assert_eq!(config.num_sync_packets, 20);
1387        assert_eq!(config.sync_retry_interval, Duration::from_millis(250));
1388        assert_eq!(config.sync_timeout, Some(Duration::from_secs(30)));
1389        assert_eq!(config.running_retry_interval, Duration::from_millis(250));
1390        assert_eq!(config.keepalive_interval, Duration::from_millis(200));
1391    }
1392
1393    #[test]
1394    fn sync_config_stress_test_preset() {
1395        let config = SyncConfig::stress_test();
1396        assert_eq!(config.num_sync_packets, 40);
1397        assert_eq!(config.sync_retry_interval, Duration::from_millis(150));
1398        assert_eq!(config.sync_timeout, Some(Duration::from_secs(60)));
1399        assert_eq!(config.running_retry_interval, Duration::from_millis(150));
1400        assert_eq!(config.keepalive_interval, Duration::from_millis(150));
1401    }
1402
1403    #[test]
1404    fn sync_config_equality() {
1405        let config1 = SyncConfig::default();
1406        let config2 = SyncConfig::default();
1407        let config3 = SyncConfig::lan();
1408        assert_eq!(config1, config2);
1409        assert_ne!(config1, config3);
1410    }
1411
1412    #[test]
1413    #[allow(clippy::clone_on_copy)] // Testing Clone trait implementation explicitly
1414    fn sync_config_clone() {
1415        let config = SyncConfig::high_latency();
1416        let cloned = config.clone();
1417        assert_eq!(config, cloned);
1418    }
1419
1420    #[test]
1421    fn sync_config_copy() {
1422        let config = SyncConfig::lossy();
1423        let copied: SyncConfig = config; // Copy trait
1424        assert_eq!(config, copied);
1425    }
1426
1427    #[test]
1428    fn sync_config_debug_format() {
1429        let config = SyncConfig::default();
1430        let debug_str = format!("{:?}", config);
1431        assert!(debug_str.contains("SyncConfig"));
1432        assert!(debug_str.contains("num_sync_packets"));
1433        assert!(debug_str.contains("sync_retry_interval"));
1434    }
1435
1436    #[test]
1437    fn sync_config_display() {
1438        // Test default (no timeout)
1439        let config = SyncConfig::default();
1440        let display_str = config.to_string();
1441        assert!(display_str.contains("SyncConfig"));
1442        assert!(display_str.contains("num_sync_packets: 5"));
1443        assert!(display_str.contains("timeout: None"));
1444
1445        // Test with timeout
1446        let config = SyncConfig::lan();
1447        let display_str = config.to_string();
1448        assert!(display_str.contains("SyncConfig"));
1449        assert!(display_str.contains("num_sync_packets: 3"));
1450        assert!(display_str.contains("5s")); // timeout: 5s
1451    }
1452
1453    #[test]
1454    fn sync_config_presets_differ() {
1455        // Ensure all presets are distinct configurations
1456        let presets = [
1457            SyncConfig::default(),
1458            SyncConfig::high_latency(),
1459            SyncConfig::lossy(),
1460            SyncConfig::lan(),
1461            SyncConfig::mobile(),
1462            SyncConfig::competitive(),
1463        ];
1464
1465        // Check that no two presets are equal (except default and new)
1466        for (i, preset_a) in presets.iter().enumerate() {
1467            for (j, preset_b) in presets.iter().enumerate() {
1468                if i != j {
1469                    assert_ne!(
1470                        preset_a, preset_b,
1471                        "Presets at index {} and {} should differ",
1472                        i, j
1473                    );
1474                }
1475            }
1476        }
1477    }
1478
1479    // ========================================================================
1480    // ProtocolConfig Tests
1481    // ========================================================================
1482
1483    #[test]
1484    fn protocol_config_default_values() {
1485        let config = ProtocolConfig::default();
1486        assert_eq!(config.quality_report_interval, Duration::from_millis(200));
1487        assert_eq!(config.shutdown_delay, Duration::from_millis(5000));
1488        assert_eq!(config.max_checksum_history, 32);
1489        assert_eq!(config.pending_output_limit, 128);
1490        assert_eq!(config.sync_retry_warning_threshold, 10);
1491        assert_eq!(config.sync_duration_warning_ms, 3000);
1492    }
1493
1494    #[test]
1495    fn protocol_config_new_equals_default() {
1496        let new_config = ProtocolConfig::new();
1497        let default_config = ProtocolConfig::default();
1498        assert_eq!(new_config, default_config);
1499    }
1500
1501    #[test]
1502    fn protocol_config_competitive_preset() {
1503        let config = ProtocolConfig::competitive();
1504        assert_eq!(config.quality_report_interval, Duration::from_millis(100));
1505        assert_eq!(config.shutdown_delay, Duration::from_millis(3000));
1506        assert_eq!(config.max_checksum_history, 32);
1507        assert_eq!(config.pending_output_limit, 128);
1508        assert_eq!(config.sync_retry_warning_threshold, 10);
1509        assert_eq!(config.sync_duration_warning_ms, 2000);
1510    }
1511
1512    #[test]
1513    fn protocol_config_high_latency_preset() {
1514        let config = ProtocolConfig::high_latency();
1515        assert_eq!(config.quality_report_interval, Duration::from_millis(400));
1516        assert_eq!(config.shutdown_delay, Duration::from_millis(10000));
1517        assert_eq!(config.max_checksum_history, 64);
1518        assert_eq!(config.pending_output_limit, 256);
1519        assert_eq!(config.sync_retry_warning_threshold, 20);
1520        assert_eq!(config.sync_duration_warning_ms, 10000);
1521    }
1522
1523    #[test]
1524    fn protocol_config_debug_preset() {
1525        let config = ProtocolConfig::debug();
1526        assert_eq!(config.quality_report_interval, Duration::from_millis(500));
1527        assert_eq!(config.shutdown_delay, Duration::from_millis(30000));
1528        assert_eq!(config.max_checksum_history, 128);
1529        assert_eq!(config.pending_output_limit, 64);
1530        assert_eq!(config.sync_retry_warning_threshold, 5);
1531        assert_eq!(config.sync_duration_warning_ms, 1000);
1532    }
1533
1534    #[test]
1535    fn protocol_config_mobile_preset() {
1536        let config = ProtocolConfig::mobile();
1537        assert_eq!(config.quality_report_interval, Duration::from_millis(350));
1538        assert_eq!(config.shutdown_delay, Duration::from_millis(15000));
1539        assert_eq!(config.max_checksum_history, 64);
1540        assert_eq!(config.pending_output_limit, 256);
1541        assert_eq!(config.sync_retry_warning_threshold, 25);
1542        assert_eq!(config.sync_duration_warning_ms, 12000);
1543    }
1544
1545    #[test]
1546    fn protocol_config_equality() {
1547        let config1 = ProtocolConfig::default();
1548        let config2 = ProtocolConfig::default();
1549        let config3 = ProtocolConfig::competitive();
1550        assert_eq!(config1, config2);
1551        assert_ne!(config1, config3);
1552    }
1553
1554    #[test]
1555    #[allow(clippy::clone_on_copy)] // Testing Clone trait implementation explicitly
1556    fn protocol_config_clone() {
1557        let config = ProtocolConfig::high_latency();
1558        let cloned = config.clone();
1559        assert_eq!(config, cloned);
1560    }
1561
1562    #[test]
1563    fn protocol_config_copy() {
1564        let config = ProtocolConfig::mobile();
1565        let copied: ProtocolConfig = config; // Copy trait
1566        assert_eq!(config, copied);
1567    }
1568
1569    #[test]
1570    fn protocol_config_debug_format() {
1571        let config = ProtocolConfig::default();
1572        let debug_str = format!("{:?}", config);
1573        assert!(debug_str.contains("ProtocolConfig"));
1574        assert!(debug_str.contains("quality_report_interval"));
1575        assert!(debug_str.contains("shutdown_delay"));
1576    }
1577
1578    #[test]
1579    fn protocol_config_display() {
1580        // Test default (no seed)
1581        let config = ProtocolConfig::default();
1582        let display_str = config.to_string();
1583        assert!(display_str.contains("ProtocolConfig"));
1584        assert!(display_str.contains("checksum_history: 32"));
1585        assert!(display_str.contains("pending_limit: 128"));
1586        assert!(display_str.contains("seed: None"));
1587
1588        // Test with seed
1589        let config = ProtocolConfig::deterministic(42);
1590        let display_str = config.to_string();
1591        assert!(display_str.contains("ProtocolConfig"));
1592        assert!(display_str.contains("seed: 42"));
1593    }
1594
1595    #[test]
1596    fn protocol_config_presets_differ() {
1597        // Ensure all presets are distinct configurations
1598        let presets = [
1599            ProtocolConfig::default(),
1600            ProtocolConfig::competitive(),
1601            ProtocolConfig::high_latency(),
1602            ProtocolConfig::debug(),
1603            ProtocolConfig::mobile(),
1604        ];
1605
1606        for (i, preset_a) in presets.iter().enumerate() {
1607            for (j, preset_b) in presets.iter().enumerate() {
1608                if i != j {
1609                    assert_ne!(
1610                        preset_a, preset_b,
1611                        "ProtocolConfig presets at index {} and {} should differ",
1612                        i, j
1613                    );
1614                }
1615            }
1616        }
1617    }
1618
1619    // ========================================================================
1620    // ProtocolConfig Validation Tests
1621    // ========================================================================
1622
1623    #[test]
1624    fn test_protocol_config_validate_default_is_valid() {
1625        let config = ProtocolConfig::default();
1626        config.validate().unwrap();
1627    }
1628
1629    #[test]
1630    fn test_protocol_config_validate_all_presets_are_valid() {
1631        let presets: &[(&str, ProtocolConfig)] = &[
1632            ("default", ProtocolConfig::default()),
1633            ("competitive", ProtocolConfig::competitive()),
1634            ("high_latency", ProtocolConfig::high_latency()),
1635            ("debug", ProtocolConfig::debug()),
1636            ("mobile", ProtocolConfig::mobile()),
1637        ];
1638
1639        for (name, config) in presets {
1640            assert!(
1641                config.validate().is_ok(),
1642                "Preset '{}' should be valid, but validation failed: {:?}",
1643                name,
1644                config.validate()
1645            );
1646        }
1647    }
1648
1649    #[test]
1650    fn test_protocol_config_validate_quality_report_interval_valid() {
1651        // Valid: minimum boundary (1ms)
1652        let config = ProtocolConfig {
1653            quality_report_interval: Duration::from_millis(1),
1654            ..ProtocolConfig::default()
1655        };
1656        config.validate().unwrap();
1657
1658        // Valid: maximum boundary (10000ms)
1659        let config = ProtocolConfig {
1660            quality_report_interval: Duration::from_millis(10000),
1661            ..ProtocolConfig::default()
1662        };
1663        config.validate().unwrap();
1664
1665        // Valid: middle value
1666        let config = ProtocolConfig {
1667            quality_report_interval: Duration::from_millis(500),
1668            ..ProtocolConfig::default()
1669        };
1670        config.validate().unwrap();
1671    }
1672
1673    #[test]
1674    fn test_protocol_config_validate_quality_report_interval_too_low() {
1675        // Invalid: 0ms (below minimum)
1676        let config = ProtocolConfig {
1677            quality_report_interval: Duration::from_millis(0),
1678            ..ProtocolConfig::default()
1679        };
1680        let result = config.validate();
1681        assert!(result.is_err());
1682        let err = result.unwrap_err();
1683        assert!(matches!(
1684            err,
1685            FortressError::InvalidRequestStructured {
1686                kind: InvalidRequestKind::DurationConfigOutOfRange {
1687                    field: "quality_report_interval",
1688                    min_ms: 1,
1689                    max_ms: 10000,
1690                    ..
1691                }
1692            }
1693        ));
1694    }
1695
1696    #[test]
1697    fn test_protocol_config_validate_quality_report_interval_too_high() {
1698        // Invalid: 10001ms (above maximum)
1699        let config = ProtocolConfig {
1700            quality_report_interval: Duration::from_millis(10001),
1701            ..ProtocolConfig::default()
1702        };
1703        let result = config.validate();
1704        assert!(result.is_err());
1705        let err = result.unwrap_err();
1706        assert!(matches!(
1707            err,
1708            FortressError::InvalidRequestStructured {
1709                kind: InvalidRequestKind::DurationConfigOutOfRange {
1710                    field: "quality_report_interval",
1711                    min_ms: 1,
1712                    max_ms: 10000,
1713                    ..
1714                }
1715            }
1716        ));
1717    }
1718
1719    #[test]
1720    fn test_protocol_config_validate_shutdown_delay_valid() {
1721        // Valid: minimum boundary (1ms)
1722        let config = ProtocolConfig {
1723            shutdown_delay: Duration::from_millis(1),
1724            ..ProtocolConfig::default()
1725        };
1726        config.validate().unwrap();
1727
1728        // Valid: maximum boundary (300000ms)
1729        let config = ProtocolConfig {
1730            shutdown_delay: Duration::from_millis(300000),
1731            ..ProtocolConfig::default()
1732        };
1733        config.validate().unwrap();
1734
1735        // Valid: middle value
1736        let config = ProtocolConfig {
1737            shutdown_delay: Duration::from_millis(10000),
1738            ..ProtocolConfig::default()
1739        };
1740        config.validate().unwrap();
1741    }
1742
1743    #[test]
1744    fn test_protocol_config_validate_shutdown_delay_too_low() {
1745        // Invalid: 0ms (below minimum)
1746        let config = ProtocolConfig {
1747            shutdown_delay: Duration::from_millis(0),
1748            ..ProtocolConfig::default()
1749        };
1750        let result = config.validate();
1751        assert!(result.is_err());
1752        let err = result.unwrap_err();
1753        assert!(matches!(
1754            err,
1755            FortressError::InvalidRequestStructured {
1756                kind: InvalidRequestKind::DurationConfigOutOfRange {
1757                    field: "shutdown_delay",
1758                    min_ms: 1,
1759                    max_ms: 300000,
1760                    ..
1761                }
1762            }
1763        ));
1764    }
1765
1766    #[test]
1767    fn test_protocol_config_validate_shutdown_delay_too_high() {
1768        // Invalid: 300001ms (above maximum)
1769        let config = ProtocolConfig {
1770            shutdown_delay: Duration::from_millis(300001),
1771            ..ProtocolConfig::default()
1772        };
1773        let result = config.validate();
1774        assert!(result.is_err());
1775        let err = result.unwrap_err();
1776        assert!(matches!(
1777            err,
1778            FortressError::InvalidRequestStructured {
1779                kind: InvalidRequestKind::DurationConfigOutOfRange {
1780                    field: "shutdown_delay",
1781                    min_ms: 1,
1782                    max_ms: 300000,
1783                    ..
1784                }
1785            }
1786        ));
1787    }
1788
1789    #[test]
1790    fn test_protocol_config_validate_max_checksum_history_valid() {
1791        // Valid: minimum boundary (1)
1792        let config = ProtocolConfig {
1793            max_checksum_history: 1,
1794            ..ProtocolConfig::default()
1795        };
1796        config.validate().unwrap();
1797
1798        // Valid: maximum boundary (1024)
1799        let config = ProtocolConfig {
1800            max_checksum_history: 1024,
1801            ..ProtocolConfig::default()
1802        };
1803        config.validate().unwrap();
1804
1805        // Valid: middle value
1806        let config = ProtocolConfig {
1807            max_checksum_history: 64,
1808            ..ProtocolConfig::default()
1809        };
1810        config.validate().unwrap();
1811    }
1812
1813    #[test]
1814    fn test_protocol_config_validate_max_checksum_history_too_low() {
1815        // Invalid: 0 (below minimum)
1816        let config = ProtocolConfig {
1817            max_checksum_history: 0,
1818            ..ProtocolConfig::default()
1819        };
1820        let result = config.validate();
1821        assert!(result.is_err());
1822        let err = result.unwrap_err();
1823        assert!(matches!(
1824            err,
1825            FortressError::InvalidRequestStructured {
1826                kind: InvalidRequestKind::ConfigValueOutOfRange {
1827                    field: "max_checksum_history",
1828                    min: 1,
1829                    max: 1024,
1830                    ..
1831                }
1832            }
1833        ));
1834    }
1835
1836    #[test]
1837    fn test_protocol_config_validate_max_checksum_history_too_high() {
1838        // Invalid: 1025 (above maximum)
1839        let config = ProtocolConfig {
1840            max_checksum_history: 1025,
1841            ..ProtocolConfig::default()
1842        };
1843        let result = config.validate();
1844        assert!(result.is_err());
1845        let err = result.unwrap_err();
1846        assert!(matches!(
1847            err,
1848            FortressError::InvalidRequestStructured {
1849                kind: InvalidRequestKind::ConfigValueOutOfRange {
1850                    field: "max_checksum_history",
1851                    min: 1,
1852                    max: 1024,
1853                    ..
1854                }
1855            }
1856        ));
1857    }
1858
1859    #[test]
1860    fn test_protocol_config_validate_pending_output_limit_valid() {
1861        // Valid: minimum boundary (1)
1862        let config = ProtocolConfig {
1863            pending_output_limit: 1,
1864            ..ProtocolConfig::default()
1865        };
1866        config.validate().unwrap();
1867
1868        // Valid: maximum boundary (4096)
1869        let config = ProtocolConfig {
1870            pending_output_limit: 4096,
1871            ..ProtocolConfig::default()
1872        };
1873        config.validate().unwrap();
1874
1875        // Valid: middle value
1876        let config = ProtocolConfig {
1877            pending_output_limit: 256,
1878            ..ProtocolConfig::default()
1879        };
1880        config.validate().unwrap();
1881    }
1882
1883    #[test]
1884    fn test_protocol_config_validate_pending_output_limit_too_low() {
1885        // Invalid: 0 (below minimum)
1886        let config = ProtocolConfig {
1887            pending_output_limit: 0,
1888            ..ProtocolConfig::default()
1889        };
1890        let result = config.validate();
1891        assert!(result.is_err());
1892        let err = result.unwrap_err();
1893        assert!(matches!(
1894            err,
1895            FortressError::InvalidRequestStructured {
1896                kind: InvalidRequestKind::ConfigValueOutOfRange {
1897                    field: "pending_output_limit",
1898                    min: 1,
1899                    max: 4096,
1900                    ..
1901                }
1902            }
1903        ));
1904    }
1905
1906    #[test]
1907    fn test_protocol_config_validate_pending_output_limit_too_high() {
1908        // Invalid: 4097 (above maximum)
1909        let config = ProtocolConfig {
1910            pending_output_limit: 4097,
1911            ..ProtocolConfig::default()
1912        };
1913        let result = config.validate();
1914        assert!(result.is_err());
1915        let err = result.unwrap_err();
1916        assert!(matches!(
1917            err,
1918            FortressError::InvalidRequestStructured {
1919                kind: InvalidRequestKind::ConfigValueOutOfRange {
1920                    field: "pending_output_limit",
1921                    min: 1,
1922                    max: 4096,
1923                    ..
1924                }
1925            }
1926        ));
1927    }
1928
1929    #[test]
1930    fn test_protocol_config_validate_sync_retry_warning_threshold_valid() {
1931        // Valid: minimum boundary (1)
1932        let config = ProtocolConfig {
1933            sync_retry_warning_threshold: 1,
1934            ..ProtocolConfig::default()
1935        };
1936        config.validate().unwrap();
1937
1938        // Valid: maximum boundary (1000)
1939        let config = ProtocolConfig {
1940            sync_retry_warning_threshold: 1000,
1941            ..ProtocolConfig::default()
1942        };
1943        config.validate().unwrap();
1944
1945        // Valid: middle value
1946        let config = ProtocolConfig {
1947            sync_retry_warning_threshold: 25,
1948            ..ProtocolConfig::default()
1949        };
1950        config.validate().unwrap();
1951    }
1952
1953    #[test]
1954    fn test_protocol_config_validate_sync_retry_warning_threshold_too_low() {
1955        // Invalid: 0 (below minimum)
1956        let config = ProtocolConfig {
1957            sync_retry_warning_threshold: 0,
1958            ..ProtocolConfig::default()
1959        };
1960        let result = config.validate();
1961        assert!(result.is_err());
1962        let err = result.unwrap_err();
1963        assert!(matches!(
1964            err,
1965            FortressError::InvalidRequestStructured {
1966                kind: InvalidRequestKind::ConfigValueOutOfRange {
1967                    field: "sync_retry_warning_threshold",
1968                    min: 1,
1969                    max: 1000,
1970                    ..
1971                }
1972            }
1973        ));
1974    }
1975
1976    #[test]
1977    fn test_protocol_config_validate_sync_retry_warning_threshold_too_high() {
1978        // Invalid: 1001 (above maximum)
1979        let config = ProtocolConfig {
1980            sync_retry_warning_threshold: 1001,
1981            ..ProtocolConfig::default()
1982        };
1983        let result = config.validate();
1984        assert!(result.is_err());
1985        let err = result.unwrap_err();
1986        assert!(matches!(
1987            err,
1988            FortressError::InvalidRequestStructured {
1989                kind: InvalidRequestKind::ConfigValueOutOfRange {
1990                    field: "sync_retry_warning_threshold",
1991                    min: 1,
1992                    max: 1000,
1993                    ..
1994                }
1995            }
1996        ));
1997    }
1998
1999    #[test]
2000    fn test_protocol_config_validate_sync_duration_warning_ms_valid() {
2001        // Valid: minimum boundary (1)
2002        let config = ProtocolConfig {
2003            sync_duration_warning_ms: 1,
2004            ..ProtocolConfig::default()
2005        };
2006        config.validate().unwrap();
2007
2008        // Valid: maximum boundary (300000)
2009        let config = ProtocolConfig {
2010            sync_duration_warning_ms: 300000,
2011            ..ProtocolConfig::default()
2012        };
2013        config.validate().unwrap();
2014
2015        // Valid: middle value
2016        let config = ProtocolConfig {
2017            sync_duration_warning_ms: 5000,
2018            ..ProtocolConfig::default()
2019        };
2020        config.validate().unwrap();
2021    }
2022
2023    #[test]
2024    fn test_protocol_config_validate_sync_duration_warning_ms_too_low() {
2025        // Invalid: 0 (below minimum)
2026        let config = ProtocolConfig {
2027            sync_duration_warning_ms: 0,
2028            ..ProtocolConfig::default()
2029        };
2030        let result = config.validate();
2031        assert!(result.is_err());
2032        let err = result.unwrap_err();
2033        assert!(matches!(
2034            err,
2035            FortressError::InvalidRequestStructured {
2036                kind: InvalidRequestKind::ConfigValueOutOfRange {
2037                    field: "sync_duration_warning_ms",
2038                    min: 1,
2039                    max: 300000,
2040                    ..
2041                }
2042            }
2043        ));
2044    }
2045
2046    #[test]
2047    fn test_protocol_config_validate_sync_duration_warning_ms_too_high() {
2048        // Invalid: 300001 (above maximum)
2049        let config = ProtocolConfig {
2050            sync_duration_warning_ms: 300001,
2051            ..ProtocolConfig::default()
2052        };
2053        let result = config.validate();
2054        assert!(result.is_err());
2055        let err = result.unwrap_err();
2056        assert!(matches!(
2057            err,
2058            FortressError::InvalidRequestStructured {
2059                kind: InvalidRequestKind::ConfigValueOutOfRange {
2060                    field: "sync_duration_warning_ms",
2061                    min: 1,
2062                    max: 300000,
2063                    ..
2064                }
2065            }
2066        ));
2067    }
2068
2069    #[test]
2070    fn test_protocol_config_validate_input_history_multiplier_valid() {
2071        // Valid: minimum boundary (1)
2072        let config = ProtocolConfig {
2073            input_history_multiplier: 1,
2074            ..ProtocolConfig::default()
2075        };
2076        config.validate().unwrap();
2077
2078        // Valid: maximum boundary (16)
2079        let config = ProtocolConfig {
2080            input_history_multiplier: 16,
2081            ..ProtocolConfig::default()
2082        };
2083        config.validate().unwrap();
2084
2085        // Valid: middle value
2086        let config = ProtocolConfig {
2087            input_history_multiplier: 4,
2088            ..ProtocolConfig::default()
2089        };
2090        config.validate().unwrap();
2091    }
2092
2093    #[test]
2094    fn test_protocol_config_validate_input_history_multiplier_too_low() {
2095        // Invalid: 0 (below minimum)
2096        let config = ProtocolConfig {
2097            input_history_multiplier: 0,
2098            ..ProtocolConfig::default()
2099        };
2100        let result = config.validate();
2101        assert!(result.is_err());
2102        let err = result.unwrap_err();
2103        assert!(matches!(
2104            err,
2105            FortressError::InvalidRequestStructured {
2106                kind: InvalidRequestKind::ConfigValueOutOfRange {
2107                    field: "input_history_multiplier",
2108                    min: 1,
2109                    max: 16,
2110                    ..
2111                }
2112            }
2113        ));
2114    }
2115
2116    #[test]
2117    fn test_protocol_config_validate_input_history_multiplier_too_high() {
2118        // Invalid: 17 (above maximum)
2119        let config = ProtocolConfig {
2120            input_history_multiplier: 17,
2121            ..ProtocolConfig::default()
2122        };
2123        let result = config.validate();
2124        assert!(result.is_err());
2125        let err = result.unwrap_err();
2126        assert!(matches!(
2127            err,
2128            FortressError::InvalidRequestStructured {
2129                kind: InvalidRequestKind::ConfigValueOutOfRange {
2130                    field: "input_history_multiplier",
2131                    min: 1,
2132                    max: 16,
2133                    ..
2134                }
2135            }
2136        ));
2137    }
2138
2139    #[test]
2140    fn test_protocol_config_validate_multiple_invalid_fields() {
2141        // Test that validation stops at the first invalid field
2142        // (quality_report_interval is checked first)
2143        let config = ProtocolConfig {
2144            quality_report_interval: Duration::from_millis(0), // Invalid
2145            shutdown_delay: Duration::from_millis(0),          // Also invalid
2146            max_checksum_history: 0,                           // Also invalid
2147            ..ProtocolConfig::default()
2148        };
2149        let result = config.validate();
2150        assert!(result.is_err());
2151        let err = result.unwrap_err();
2152        // Should report the first field that failed (quality_report_interval)
2153        assert!(matches!(
2154            err,
2155            FortressError::InvalidRequestStructured {
2156                kind: InvalidRequestKind::DurationConfigOutOfRange {
2157                    field: "quality_report_interval",
2158                    ..
2159                }
2160            }
2161        ));
2162    }
2163
2164    #[test]
2165    fn test_protocol_config_validate_all_fields_at_boundaries() {
2166        // Test a config with all fields at their minimum valid values
2167        let config = ProtocolConfig {
2168            quality_report_interval: Duration::from_millis(1),
2169            shutdown_delay: Duration::from_millis(1),
2170            max_checksum_history: 1,
2171            pending_output_limit: 1,
2172            sync_retry_warning_threshold: 1,
2173            sync_duration_warning_ms: 1,
2174            input_history_multiplier: 1,
2175            protocol_rng_seed: None,
2176        };
2177        config.validate().unwrap();
2178
2179        // Test a config with all fields at their maximum valid values
2180        let config = ProtocolConfig {
2181            quality_report_interval: Duration::from_millis(10000),
2182            shutdown_delay: Duration::from_millis(300000),
2183            max_checksum_history: 1024,
2184            pending_output_limit: 4096,
2185            sync_retry_warning_threshold: 1000,
2186            sync_duration_warning_ms: 300000,
2187            input_history_multiplier: 16,
2188            protocol_rng_seed: None,
2189        };
2190        config.validate().unwrap();
2191    }
2192
2193    // ========================================================================
2194    // ProtocolConfig Deterministic RNG Seed Tests
2195    // ========================================================================
2196
2197    #[test]
2198    fn test_protocol_config_deterministic_preset() {
2199        let config = ProtocolConfig::deterministic(12345);
2200        assert_eq!(config.protocol_rng_seed, Some(12345));
2201        // Other fields should be default
2202        assert_eq!(
2203            config.quality_report_interval,
2204            ProtocolConfig::default().quality_report_interval
2205        );
2206    }
2207
2208    #[test]
2209    fn test_protocol_config_deterministic_different_seeds() {
2210        let config1 = ProtocolConfig::deterministic(1);
2211        let config2 = ProtocolConfig::deterministic(2);
2212        assert_ne!(config1.protocol_rng_seed, config2.protocol_rng_seed);
2213    }
2214
2215    #[test]
2216    fn test_protocol_config_default_has_no_seed() {
2217        let config = ProtocolConfig::default();
2218        assert_eq!(config.protocol_rng_seed, None);
2219    }
2220
2221    #[test]
2222    fn test_protocol_config_all_presets_have_no_seed() {
2223        // All presets except deterministic() should have no seed
2224        assert_eq!(ProtocolConfig::competitive().protocol_rng_seed, None);
2225        assert_eq!(ProtocolConfig::high_latency().protocol_rng_seed, None);
2226        assert_eq!(ProtocolConfig::debug().protocol_rng_seed, None);
2227        assert_eq!(ProtocolConfig::mobile().protocol_rng_seed, None);
2228    }
2229
2230    #[test]
2231    fn test_protocol_config_seed_validates_ok() {
2232        // Config with seed should validate successfully
2233        let config = ProtocolConfig::deterministic(42);
2234        config.validate().unwrap();
2235    }
2236
2237    // ========================================================================
2238    // SpectatorConfig Tests
2239    // ========================================================================
2240
2241    #[test]
2242    fn spectator_config_default_values() {
2243        let config = SpectatorConfig::default();
2244        assert_eq!(config.buffer_size, 60);
2245        assert_eq!(config.catchup_speed, 1);
2246        assert_eq!(config.max_frames_behind, 10);
2247    }
2248
2249    #[test]
2250    fn spectator_config_new_equals_default() {
2251        let new_config = SpectatorConfig::new();
2252        let default_config = SpectatorConfig::default();
2253        assert_eq!(new_config, default_config);
2254    }
2255
2256    #[test]
2257    fn spectator_config_fast_paced_preset() {
2258        let config = SpectatorConfig::fast_paced();
2259        assert_eq!(config.buffer_size, 90);
2260        assert_eq!(config.catchup_speed, 2);
2261        assert_eq!(config.max_frames_behind, 15);
2262    }
2263
2264    #[test]
2265    fn spectator_config_slow_connection_preset() {
2266        let config = SpectatorConfig::slow_connection();
2267        assert_eq!(config.buffer_size, 120);
2268        assert_eq!(config.catchup_speed, 1);
2269        assert_eq!(config.max_frames_behind, 20);
2270    }
2271
2272    #[test]
2273    fn spectator_config_local_preset() {
2274        let config = SpectatorConfig::local();
2275        assert_eq!(config.buffer_size, 30);
2276        assert_eq!(config.catchup_speed, 2);
2277        assert_eq!(config.max_frames_behind, 5);
2278    }
2279
2280    #[test]
2281    fn spectator_config_broadcast_preset() {
2282        let config = SpectatorConfig::broadcast();
2283        assert_eq!(config.buffer_size, 180);
2284        assert_eq!(config.catchup_speed, 1);
2285        assert_eq!(config.max_frames_behind, 30);
2286    }
2287
2288    #[test]
2289    fn spectator_config_mobile_preset() {
2290        let config = SpectatorConfig::mobile();
2291        assert_eq!(config.buffer_size, 120);
2292        assert_eq!(config.catchup_speed, 1);
2293        assert_eq!(config.max_frames_behind, 25);
2294    }
2295
2296    #[test]
2297    fn spectator_config_display() {
2298        let config = SpectatorConfig::default();
2299        let display_str = config.to_string();
2300        assert!(display_str.contains("SpectatorConfig"));
2301        assert!(display_str.contains("buffer: 60"));
2302        assert!(display_str.contains("catchup_speed: 1"));
2303        assert!(display_str.contains("max_behind: 10"));
2304
2305        let config = SpectatorConfig::broadcast();
2306        let display_str = config.to_string();
2307        assert!(display_str.contains("buffer: 180"));
2308        assert!(display_str.contains("max_behind: 30"));
2309    }
2310
2311    #[test]
2312    fn spectator_config_equality() {
2313        let config1 = SpectatorConfig::default();
2314        let config2 = SpectatorConfig::default();
2315        let config3 = SpectatorConfig::broadcast();
2316        assert_eq!(config1, config2);
2317        assert_ne!(config1, config3);
2318    }
2319
2320    #[test]
2321    #[allow(clippy::clone_on_copy)]
2322    fn spectator_config_clone() {
2323        let config = SpectatorConfig::fast_paced();
2324        let cloned = config.clone();
2325        assert_eq!(config, cloned);
2326    }
2327
2328    #[test]
2329    fn spectator_config_copy() {
2330        let config = SpectatorConfig::local();
2331        let copied: SpectatorConfig = config;
2332        assert_eq!(config, copied);
2333    }
2334
2335    #[test]
2336    fn spectator_config_debug_format() {
2337        let config = SpectatorConfig::default();
2338        let debug_str = format!("{:?}", config);
2339        assert!(debug_str.contains("SpectatorConfig"));
2340        assert!(debug_str.contains("buffer_size"));
2341        assert!(debug_str.contains("catchup_speed"));
2342    }
2343}
2344
2345// =============================================================================
2346// Kani Formal Verification Proofs for InputQueueConfig
2347// =============================================================================
2348//
2349// These proofs formally verify the validation constraints for configurable constants.
2350// This completes the Phase 11 gap analysis by adding formal verification for:
2351// - InputQueueConfig.validate() - queue_length >= 2
2352// - InputQueueConfig.validate_frame_delay() - frame_delay < queue_length
2353// - InputQueueConfig.max_frame_delay() - derivation is correct
2354//
2355// The proofs verify these constraints hold for ANY valid configuration within
2356// Kani's symbolic execution bounds.
2357#[cfg(kani)]
2358mod kani_config_proofs {
2359    use super::*;
2360
2361    /// Proof: validate() accepts all queue_length >= 2.
2362    ///
2363    /// Verifies that InputQueueConfig.validate() returns Ok for any queue_length >= 2
2364    /// and Err for queue_length < 2.
2365    ///
2366    /// - Tier: 1 (Fast, <30s)
2367    /// - Verifies: Queue length validation correctness
2368    /// - Related: proof_validate_boundary_at_two, proof_all_presets_valid
2369    #[kani::proof]
2370    #[kani::unwind(2)]
2371    fn proof_validate_accepts_valid_queue_lengths() {
2372        let queue_length: usize = kani::any();
2373        // Focus on boundary region for tractability
2374        kani::assume(queue_length <= 512);
2375
2376        let config = InputQueueConfig { queue_length };
2377        let result = config.validate();
2378
2379        if queue_length >= 2 {
2380            kani::assert(result.is_ok(), "validate() should accept queue_length >= 2");
2381        } else {
2382            kani::assert(result.is_err(), "validate() should reject queue_length < 2");
2383        }
2384    }
2385
2386    /// Proof: validate() boundary condition at queue_length = 2.
2387    ///
2388    /// Specifically verifies the minimum valid queue_length.
2389    ///
2390    /// - Tier: 1 (Fast, <30s)
2391    /// - Verifies: Boundary condition at minimum queue length
2392    /// - Related: proof_validate_accepts_valid_queue_lengths
2393    #[kani::proof]
2394    #[kani::unwind(2)]
2395    fn proof_validate_boundary_at_two() {
2396        // queue_length = 1 should fail
2397        let config_one = InputQueueConfig { queue_length: 1 };
2398        kani::assert(
2399            config_one.validate().is_err(),
2400            "queue_length=1 should be invalid",
2401        );
2402
2403        // queue_length = 2 should succeed
2404        let config_two = InputQueueConfig { queue_length: 2 };
2405        kani::assert(
2406            config_two.validate().is_ok(),
2407            "queue_length=2 should be valid",
2408        );
2409    }
2410
2411    /// Proof: validate_frame_delay() enforces frame_delay < queue_length.
2412    ///
2413    /// Verifies that validate_frame_delay returns Ok when frame_delay < queue_length
2414    /// and Err when frame_delay >= queue_length.
2415    ///
2416    /// - Tier: 2 (Medium, 30s-2min)
2417    /// - Verifies: Frame delay validation constraint
2418    /// - Related: proof_max_frame_delay_is_valid_delay, proof_max_frame_delay_derivation
2419    #[kani::proof]
2420    #[kani::unwind(10)]
2421    fn proof_validate_frame_delay_constraint() {
2422        let queue_length: usize = kani::any();
2423        let frame_delay: usize = kani::any();
2424
2425        // Keep bounds tractable
2426        kani::assume(queue_length >= 2 && queue_length <= 256);
2427        kani::assume(frame_delay <= 256);
2428
2429        let config = InputQueueConfig { queue_length };
2430        let result = config.validate_frame_delay(frame_delay);
2431
2432        if frame_delay < queue_length {
2433            kani::assert(
2434                result.is_ok(),
2435                "validate_frame_delay should accept delay < queue_length",
2436            );
2437        } else {
2438            kani::assert(
2439                result.is_err(),
2440                "validate_frame_delay should reject delay >= queue_length",
2441            );
2442        }
2443    }
2444
2445    /// Proof: max_frame_delay() returns queue_length - 1 (with saturation).
2446    ///
2447    /// Verifies that max_frame_delay() correctly computes queue_length - 1,
2448    /// using saturating_sub to handle the edge case of queue_length = 0.
2449    ///
2450    /// - Tier: 1 (Fast, <30s)
2451    /// - Verifies: Max frame delay derivation correctness
2452    /// - Related: proof_max_frame_delay_is_valid_delay, proof_validate_frame_delay_constraint
2453    #[kani::proof]
2454    #[kani::unwind(2)]
2455    fn proof_max_frame_delay_derivation() {
2456        let queue_length: usize = kani::any();
2457        kani::assume(queue_length <= 512);
2458
2459        let config = InputQueueConfig { queue_length };
2460        let max_delay = config.max_frame_delay();
2461
2462        // Should be queue_length - 1, or 0 if queue_length is 0
2463        let expected = queue_length.saturating_sub(1);
2464        kani::assert(
2465            max_delay == expected,
2466            "max_frame_delay should equal queue_length.saturating_sub(1)",
2467        );
2468    }
2469
2470    /// Proof: max_frame_delay() is always a valid frame_delay.
2471    ///
2472    /// Verifies that validate_frame_delay(max_frame_delay()) always succeeds
2473    /// for valid configurations (queue_length >= 2).
2474    ///
2475    /// - Tier: 2 (Medium, 30s-2min)
2476    /// - Verifies: Max delay is always valid for valid configs
2477    /// - Related: proof_max_frame_delay_derivation, proof_validate_frame_delay_constraint
2478    #[kani::proof]
2479    #[kani::unwind(2)]
2480    fn proof_max_frame_delay_is_valid_delay() {
2481        let queue_length: usize = kani::any();
2482        kani::assume(queue_length >= 2 && queue_length <= 256);
2483
2484        let config = InputQueueConfig { queue_length };
2485        let max_delay = config.max_frame_delay();
2486        let result = config.validate_frame_delay(max_delay);
2487
2488        kani::assert(
2489            result.is_ok(),
2490            "max_frame_delay() should always be a valid frame_delay for valid configs",
2491        );
2492    }
2493
2494    /// Proof: All presets are valid configurations.
2495    ///
2496    /// Verifies that standard(), high_latency(), and minimal() presets
2497    /// all pass validate().
2498    ///
2499    /// - Tier: 1 (Fast, <30s)
2500    /// - Verifies: All factory presets pass validation
2501    /// - Related: proof_preset_values, proof_validate_accepts_valid_queue_lengths
2502    #[kani::proof]
2503    #[kani::unwind(2)]
2504    fn proof_all_presets_valid() {
2505        let standard = InputQueueConfig::standard();
2506        let high_latency = InputQueueConfig::high_latency();
2507        let minimal = InputQueueConfig::minimal();
2508
2509        kani::assert(
2510            standard.validate().is_ok(),
2511            "standard() preset should be valid",
2512        );
2513        kani::assert(
2514            high_latency.validate().is_ok(),
2515            "high_latency() preset should be valid",
2516        );
2517        kani::assert(
2518            minimal.validate().is_ok(),
2519            "minimal() preset should be valid",
2520        );
2521    }
2522
2523    /// Proof: Presets have correct queue_length values.
2524    ///
2525    /// Verifies that preset implementations return their expected values.
2526    /// Note: `standard()` uses `INPUT_QUEUE_LENGTH` which varies between
2527    /// Kani (8) and production (128) builds. The other presets use
2528    /// hardcoded values that don't change.
2529    ///
2530    /// - Tier: 1 (Fast, <30s)
2531    /// - Verifies: Preset queue length values match specifications
2532    /// - Related: proof_all_presets_valid
2533    #[kani::proof]
2534    #[kani::unwind(2)]
2535    fn proof_preset_values() {
2536        let standard = InputQueueConfig::standard();
2537        let high_latency = InputQueueConfig::high_latency();
2538        let minimal = InputQueueConfig::minimal();
2539
2540        // standard() uses Self::default() which returns INPUT_QUEUE_LENGTH
2541        kani::assert(
2542            standard.queue_length == INPUT_QUEUE_LENGTH,
2543            "standard() should have queue_length=INPUT_QUEUE_LENGTH",
2544        );
2545        // high_latency() returns a hardcoded value
2546        kani::assert(
2547            high_latency.queue_length == 256,
2548            "high_latency() should have queue_length=256",
2549        );
2550        // minimal() returns a hardcoded value
2551        kani::assert(
2552            minimal.queue_length == 32,
2553            "minimal() should have queue_length=32",
2554        );
2555    }
2556}