ipfrs_network/
multipath_quic.rs

1//! QUIC Multipath Support
2//!
3//! This module implements multipath QUIC connections, allowing a single connection
4//! to use multiple network paths simultaneously for improved reliability, throughput,
5//! and seamless network transitions.
6//!
7//! ## Features
8//!
9//! - **Multiple Paths**: Manage multiple network paths for a single connection
10//! - **Path Quality Monitoring**: Track latency, bandwidth, and packet loss per path
11//! - **Traffic Distribution**: Distribute traffic across paths based on quality and strategy
12//! - **Path Migration**: Seamlessly migrate between paths when quality changes
13//! - **Load Balancing**: Balance load across available paths for optimal throughput
14//! - **Redundancy**: Send critical data on multiple paths for reliability
15//!
16//! ## Path Selection Strategies
17//!
18//! - **Round Robin**: Distribute traffic evenly across all paths
19//! - **Quality Based**: Prefer paths with better quality metrics
20//! - **Lowest Latency**: Always use the path with lowest latency
21//! - **Highest Bandwidth**: Always use the path with highest bandwidth
22//! - **Redundant**: Send data on all paths for maximum reliability
23//!
24//! ## Example
25//!
26//! ```rust,no_run
27//! use ipfrs_network::multipath_quic::{MultipathQuicManager, MultipathConfig, PathSelectionStrategy};
28//!
29//! # async fn example() -> Result<(), Box<dyn std::error::Error>> {
30//! // Create multipath manager with quality-based strategy
31//! let config = MultipathConfig {
32//!     max_paths: 4,
33//!     strategy: PathSelectionStrategy::QualityBased,
34//!     enable_redundancy: false,
35//!     ..Default::default()
36//! };
37//!
38//! let mut manager = MultipathQuicManager::new(config);
39//!
40//! // Paths will be automatically detected and managed
41//! // Traffic will be distributed based on the configured strategy
42//! # Ok(())
43//! # }
44//! ```
45
46use dashmap::DashMap;
47use parking_lot::RwLock;
48use std::collections::VecDeque;
49use std::net::SocketAddr;
50use std::sync::Arc;
51use std::time::{Duration, Instant};
52use thiserror::Error;
53use tracing::{debug, info, warn};
54
55/// Errors that can occur in multipath QUIC operations
56#[derive(Debug, Error)]
57pub enum MultipathError {
58    #[error("No paths available")]
59    NoPathsAvailable,
60
61    #[error("Path not found: {0}")]
62    PathNotFound(u64),
63
64    #[error("Path quality too low: {0}")]
65    PathQualityTooLow(f64),
66
67    #[error("Maximum paths reached: {0}")]
68    MaxPathsReached(usize),
69
70    #[error("Invalid configuration: {0}")]
71    InvalidConfig(String),
72}
73
74/// Result type for multipath operations
75pub type Result<T> = std::result::Result<T, MultipathError>;
76
77/// Unique identifier for a network path
78pub type PathId = u64;
79
80/// Network path representing a single route to a peer
81#[derive(Debug, Clone)]
82pub struct NetworkPath {
83    /// Unique identifier for this path
84    pub id: PathId,
85
86    /// Local socket address for this path
87    pub local_addr: SocketAddr,
88
89    /// Remote socket address for this path
90    pub remote_addr: SocketAddr,
91
92    /// Path state
93    pub state: PathState,
94
95    /// Quality metrics for this path
96    pub quality: PathQuality,
97
98    /// When this path was created
99    pub created_at: Instant,
100
101    /// When this path was last used
102    pub last_used: Instant,
103
104    /// Total bytes sent on this path
105    pub bytes_sent: u64,
106
107    /// Total bytes received on this path
108    pub bytes_received: u64,
109}
110
111/// State of a network path
112#[derive(Debug, Clone, Copy, PartialEq, Eq)]
113pub enum PathState {
114    /// Path is being validated
115    Validating,
116
117    /// Path is active and ready to use
118    Active,
119
120    /// Path is standby (available but not preferred)
121    Standby,
122
123    /// Path is degraded (poor quality)
124    Degraded,
125
126    /// Path is failed and should not be used
127    Failed,
128
129    /// Path is being closed
130    Closing,
131}
132
133/// Quality metrics for a network path
134#[derive(Debug, Clone)]
135pub struct PathQuality {
136    /// Round-trip time (RTT) in milliseconds
137    pub rtt_ms: f64,
138
139    /// Bandwidth estimate in bytes/second
140    pub bandwidth_bps: u64,
141
142    /// Packet loss rate (0.0 - 1.0)
143    pub loss_rate: f64,
144
145    /// Jitter in milliseconds
146    pub jitter_ms: f64,
147
148    /// Overall quality score (0.0 - 1.0, higher is better)
149    pub score: f64,
150
151    /// Number of samples used for metrics
152    pub sample_count: usize,
153
154    /// Last time quality was updated
155    pub last_updated: Instant,
156}
157
158impl Default for PathQuality {
159    fn default() -> Self {
160        Self {
161            rtt_ms: 0.0,
162            bandwidth_bps: 0,
163            loss_rate: 0.0,
164            jitter_ms: 0.0,
165            score: 0.5, // Start with neutral score
166            sample_count: 0,
167            last_updated: Instant::now(),
168        }
169    }
170}
171
172impl PathQuality {
173    /// Calculate overall quality score based on metrics
174    pub fn calculate_score(&mut self) {
175        // Weights for different metrics
176        const RTT_WEIGHT: f64 = 0.3;
177        const BANDWIDTH_WEIGHT: f64 = 0.3;
178        const LOSS_WEIGHT: f64 = 0.3;
179        const JITTER_WEIGHT: f64 = 0.1;
180
181        // Normalize RTT (assume 0-500ms range, lower is better)
182        let rtt_score = (1.0 - (self.rtt_ms / 500.0).clamp(0.0, 1.0)).max(0.0);
183
184        // Normalize bandwidth (assume 0-100Mbps range, higher is better)
185        let bandwidth_mbps = self.bandwidth_bps as f64 / 125_000.0;
186        let bandwidth_score = (bandwidth_mbps / 100.0).clamp(0.0, 1.0);
187
188        // Loss rate score (lower is better)
189        let loss_score = (1.0 - self.loss_rate).max(0.0);
190
191        // Jitter score (assume 0-50ms range, lower is better)
192        let jitter_score = (1.0 - (self.jitter_ms / 50.0).clamp(0.0, 1.0)).max(0.0);
193
194        // Calculate weighted score
195        self.score = rtt_score * RTT_WEIGHT
196            + bandwidth_score * BANDWIDTH_WEIGHT
197            + loss_score * LOSS_WEIGHT
198            + jitter_score * JITTER_WEIGHT;
199    }
200
201    /// Update quality metrics with exponential moving average
202    pub fn update(&mut self, rtt_ms: f64, bandwidth_bps: u64, loss_rate: f64, jitter_ms: f64) {
203        const ALPHA: f64 = 0.8; // Weight for new samples
204
205        if self.sample_count == 0 {
206            // First sample, use it directly
207            self.rtt_ms = rtt_ms;
208            self.bandwidth_bps = bandwidth_bps;
209            self.loss_rate = loss_rate;
210            self.jitter_ms = jitter_ms;
211        } else {
212            // Exponential moving average
213            self.rtt_ms = self.rtt_ms * (1.0 - ALPHA) + rtt_ms * ALPHA;
214            self.bandwidth_bps = ((self.bandwidth_bps as f64) * (1.0 - ALPHA)
215                + (bandwidth_bps as f64) * ALPHA) as u64;
216            self.loss_rate = self.loss_rate * (1.0 - ALPHA) + loss_rate * ALPHA;
217            self.jitter_ms = self.jitter_ms * (1.0 - ALPHA) + jitter_ms * ALPHA;
218        }
219
220        self.sample_count += 1;
221        self.last_updated = Instant::now();
222        self.calculate_score();
223    }
224}
225
226/// Strategy for selecting which path to use for traffic
227#[derive(Debug, Clone, Copy, PartialEq, Eq)]
228pub enum PathSelectionStrategy {
229    /// Distribute traffic evenly across all active paths
230    RoundRobin,
231
232    /// Prefer paths with better quality scores
233    QualityBased,
234
235    /// Always use the path with lowest latency
236    LowestLatency,
237
238    /// Always use the path with highest bandwidth
239    HighestBandwidth,
240
241    /// Send data on all paths for maximum reliability (redundant mode)
242    Redundant,
243
244    /// Distribute based on weighted round-robin using quality scores
245    WeightedRoundRobin,
246}
247
248/// Configuration for multipath QUIC manager
249#[derive(Debug, Clone)]
250pub struct MultipathConfig {
251    /// Maximum number of concurrent paths
252    pub max_paths: usize,
253
254    /// Path selection strategy
255    pub strategy: PathSelectionStrategy,
256
257    /// Enable redundant transmission for critical data
258    pub enable_redundancy: bool,
259
260    /// Minimum quality score to keep a path active (0.0 - 1.0)
261    pub min_quality_threshold: f64,
262
263    /// Interval for path quality monitoring
264    pub quality_check_interval: Duration,
265
266    /// Maximum age for a path without traffic before considering it stale
267    pub path_idle_timeout: Duration,
268
269    /// Enable automatic path migration based on quality
270    pub enable_auto_migration: bool,
271
272    /// Quality difference threshold to trigger migration (0.0 - 1.0)
273    pub migration_quality_threshold: f64,
274}
275
276impl Default for MultipathConfig {
277    fn default() -> Self {
278        Self {
279            max_paths: 4,
280            strategy: PathSelectionStrategy::QualityBased,
281            enable_redundancy: false,
282            min_quality_threshold: 0.3,
283            quality_check_interval: Duration::from_secs(5),
284            path_idle_timeout: Duration::from_secs(60),
285            enable_auto_migration: true,
286            migration_quality_threshold: 0.2, // Migrate if 20% quality difference
287        }
288    }
289}
290
291impl MultipathConfig {
292    /// Configuration optimized for low-latency applications
293    pub fn low_latency() -> Self {
294        Self {
295            max_paths: 2,
296            strategy: PathSelectionStrategy::LowestLatency,
297            enable_redundancy: false,
298            min_quality_threshold: 0.4,
299            quality_check_interval: Duration::from_secs(1),
300            path_idle_timeout: Duration::from_secs(30),
301            enable_auto_migration: true,
302            migration_quality_threshold: 0.15,
303        }
304    }
305
306    /// Configuration optimized for high-bandwidth applications
307    pub fn high_bandwidth() -> Self {
308        Self {
309            max_paths: 4,
310            strategy: PathSelectionStrategy::HighestBandwidth,
311            enable_redundancy: false,
312            min_quality_threshold: 0.3,
313            quality_check_interval: Duration::from_secs(5),
314            path_idle_timeout: Duration::from_secs(60),
315            enable_auto_migration: true,
316            migration_quality_threshold: 0.25,
317        }
318    }
319
320    /// Configuration optimized for high reliability
321    pub fn high_reliability() -> Self {
322        Self {
323            max_paths: 4,
324            strategy: PathSelectionStrategy::Redundant,
325            enable_redundancy: true,
326            min_quality_threshold: 0.2,
327            quality_check_interval: Duration::from_secs(3),
328            path_idle_timeout: Duration::from_secs(90),
329            enable_auto_migration: false, // Don't migrate in redundant mode
330            migration_quality_threshold: 0.3,
331        }
332    }
333
334    /// Configuration optimized for mobile devices
335    pub fn mobile() -> Self {
336        Self {
337            max_paths: 2, // WiFi + Cellular
338            strategy: PathSelectionStrategy::QualityBased,
339            enable_redundancy: false,
340            min_quality_threshold: 0.25,
341            quality_check_interval: Duration::from_secs(2),
342            path_idle_timeout: Duration::from_secs(45),
343            enable_auto_migration: true,
344            migration_quality_threshold: 0.2,
345        }
346    }
347}
348
349/// Statistics for multipath operations
350#[derive(Debug, Clone, Default)]
351pub struct MultipathStats {
352    /// Total number of paths created
353    pub paths_created: usize,
354
355    /// Number of currently active paths
356    pub active_paths: usize,
357
358    /// Total bytes sent across all paths
359    pub total_bytes_sent: u64,
360
361    /// Total bytes received across all paths
362    pub total_bytes_received: u64,
363
364    /// Number of path migrations performed
365    pub migrations_count: usize,
366
367    /// Number of path failures detected
368    pub path_failures: usize,
369
370    /// Average quality score across all active paths
371    pub avg_quality_score: f64,
372
373    /// Best path quality score
374    pub best_path_quality: f64,
375
376    /// Number of packets sent redundantly
377    pub redundant_packets: usize,
378}
379
380/// Multipath QUIC connection manager
381pub struct MultipathQuicManager {
382    /// Configuration
383    config: MultipathConfig,
384
385    /// Active network paths
386    paths: Arc<DashMap<PathId, NetworkPath>>,
387
388    /// Next path ID
389    next_path_id: Arc<RwLock<PathId>>,
390
391    /// Round-robin index for path selection
392    round_robin_index: Arc<RwLock<usize>>,
393
394    /// Statistics
395    stats: Arc<RwLock<MultipathStats>>,
396
397    /// Quality monitoring history
398    quality_history: Arc<RwLock<VecDeque<(Instant, f64)>>>,
399}
400
401impl MultipathQuicManager {
402    /// Create a new multipath QUIC manager
403    pub fn new(config: MultipathConfig) -> Self {
404        info!(
405            "Creating multipath QUIC manager with strategy: {:?}",
406            config.strategy
407        );
408
409        Self {
410            config,
411            paths: Arc::new(DashMap::new()),
412            next_path_id: Arc::new(RwLock::new(0)),
413            round_robin_index: Arc::new(RwLock::new(0)),
414            stats: Arc::new(RwLock::new(MultipathStats::default())),
415            quality_history: Arc::new(RwLock::new(VecDeque::with_capacity(100))),
416        }
417    }
418
419    /// Add a new network path
420    pub fn add_path(&self, local_addr: SocketAddr, remote_addr: SocketAddr) -> Result<PathId> {
421        if self.paths.len() >= self.config.max_paths {
422            warn!(
423                "Maximum paths reached: {}, cannot add new path",
424                self.config.max_paths
425            );
426            return Err(MultipathError::MaxPathsReached(self.config.max_paths));
427        }
428
429        let path_id = {
430            let mut id = self.next_path_id.write();
431            let current_id = *id;
432            *id += 1;
433            current_id
434        };
435
436        let path = NetworkPath {
437            id: path_id,
438            local_addr,
439            remote_addr,
440            state: PathState::Validating,
441            quality: PathQuality::default(),
442            created_at: Instant::now(),
443            last_used: Instant::now(),
444            bytes_sent: 0,
445            bytes_received: 0,
446        };
447
448        self.paths.insert(path_id, path);
449
450        let mut stats = self.stats.write();
451        stats.paths_created += 1;
452        stats.active_paths = self.paths.len();
453
454        info!(
455            "Added new path {}: {} -> {}",
456            path_id, local_addr, remote_addr
457        );
458
459        Ok(path_id)
460    }
461
462    /// Remove a network path
463    pub fn remove_path(&self, path_id: PathId) -> Result<()> {
464        if self.paths.remove(&path_id).is_some() {
465            let mut stats = self.stats.write();
466            stats.active_paths = self.paths.len();
467
468            info!("Removed path {}", path_id);
469            Ok(())
470        } else {
471            Err(MultipathError::PathNotFound(path_id))
472        }
473    }
474
475    /// Update path state
476    pub fn update_path_state(&self, path_id: PathId, state: PathState) -> Result<()> {
477        if let Some(mut path) = self.paths.get_mut(&path_id) {
478            let old_state = path.state;
479            path.state = state;
480
481            debug!(
482                "Path {} state changed: {:?} -> {:?}",
483                path_id, old_state, state
484            );
485
486            if state == PathState::Failed {
487                let mut stats = self.stats.write();
488                stats.path_failures += 1;
489            }
490
491            Ok(())
492        } else {
493            Err(MultipathError::PathNotFound(path_id))
494        }
495    }
496
497    /// Update path quality metrics
498    pub fn update_path_quality(
499        &self,
500        path_id: PathId,
501        rtt_ms: f64,
502        bandwidth_bps: u64,
503        loss_rate: f64,
504        jitter_ms: f64,
505    ) -> Result<()> {
506        if let Some(mut path) = self.paths.get_mut(&path_id) {
507            path.quality
508                .update(rtt_ms, bandwidth_bps, loss_rate, jitter_ms);
509
510            debug!(
511                "Path {} quality updated: score={:.2}, rtt={:.1}ms, bandwidth={}Mbps, loss={:.2}%",
512                path_id,
513                path.quality.score,
514                path.quality.rtt_ms,
515                path.quality.bandwidth_bps / 125_000,
516                path.quality.loss_rate * 100.0
517            );
518
519            // Check if quality is too low
520            if path.quality.score < self.config.min_quality_threshold
521                && path.state == PathState::Active
522            {
523                warn!(
524                    "Path {} quality too low: {:.2}, marking as degraded",
525                    path_id, path.quality.score
526                );
527                path.state = PathState::Degraded;
528            }
529
530            // Record quality in history
531            let mut history = self.quality_history.write();
532            history.push_back((Instant::now(), path.quality.score));
533            if history.len() > 100 {
534                history.pop_front();
535            }
536
537            Ok(())
538        } else {
539            Err(MultipathError::PathNotFound(path_id))
540        }
541    }
542
543    /// Select the best path for sending data
544    pub fn select_path(&self) -> Result<PathId> {
545        let active_paths: Vec<_> = self
546            .paths
547            .iter()
548            .filter(|entry| entry.value().state == PathState::Active)
549            .collect();
550
551        if active_paths.is_empty() {
552            return Err(MultipathError::NoPathsAvailable);
553        }
554
555        match self.config.strategy {
556            PathSelectionStrategy::RoundRobin => {
557                let mut index = self.round_robin_index.write();
558                let selected = &active_paths[*index % active_paths.len()];
559                *index = (*index + 1) % active_paths.len();
560                Ok(selected.value().id)
561            }
562
563            PathSelectionStrategy::QualityBased | PathSelectionStrategy::WeightedRoundRobin => {
564                // Select path with highest quality score
565                let best = active_paths
566                    .iter()
567                    .max_by(|a, b| {
568                        a.value()
569                            .quality
570                            .score
571                            .partial_cmp(&b.value().quality.score)
572                            .unwrap_or(std::cmp::Ordering::Equal)
573                    })
574                    .ok_or(MultipathError::NoPathsAvailable)?;
575
576                Ok(best.value().id)
577            }
578
579            PathSelectionStrategy::LowestLatency => {
580                // Select path with lowest RTT
581                let best = active_paths
582                    .iter()
583                    .min_by(|a, b| {
584                        a.value()
585                            .quality
586                            .rtt_ms
587                            .partial_cmp(&b.value().quality.rtt_ms)
588                            .unwrap_or(std::cmp::Ordering::Equal)
589                    })
590                    .ok_or(MultipathError::NoPathsAvailable)?;
591
592                Ok(best.value().id)
593            }
594
595            PathSelectionStrategy::HighestBandwidth => {
596                // Select path with highest bandwidth
597                let best = active_paths
598                    .iter()
599                    .max_by(|a, b| {
600                        a.value()
601                            .quality
602                            .bandwidth_bps
603                            .cmp(&b.value().quality.bandwidth_bps)
604                    })
605                    .ok_or(MultipathError::NoPathsAvailable)?;
606
607                Ok(best.value().id)
608            }
609
610            PathSelectionStrategy::Redundant => {
611                // In redundant mode, return the first active path
612                // (caller should send on all paths)
613                Ok(active_paths[0].value().id)
614            }
615        }
616    }
617
618    /// Select all paths for redundant transmission
619    pub fn select_all_paths(&self) -> Vec<PathId> {
620        self.paths
621            .iter()
622            .filter(|entry| entry.value().state == PathState::Active)
623            .map(|entry| entry.value().id)
624            .collect()
625    }
626
627    /// Record data sent on a path
628    pub fn record_sent(&self, path_id: PathId, bytes: u64) {
629        if let Some(mut path) = self.paths.get_mut(&path_id) {
630            path.bytes_sent += bytes;
631            path.last_used = Instant::now();
632
633            let mut stats = self.stats.write();
634            stats.total_bytes_sent += bytes;
635        }
636    }
637
638    /// Record data received on a path
639    pub fn record_received(&self, path_id: PathId, bytes: u64) {
640        if let Some(mut path) = self.paths.get_mut(&path_id) {
641            path.bytes_received += bytes;
642            path.last_used = Instant::now();
643
644            let mut stats = self.stats.write();
645            stats.total_bytes_received += bytes;
646        }
647    }
648
649    /// Get path information
650    pub fn get_path(&self, path_id: PathId) -> Option<NetworkPath> {
651        self.paths.get(&path_id).map(|entry| entry.value().clone())
652    }
653
654    /// Get all active paths
655    pub fn get_active_paths(&self) -> Vec<NetworkPath> {
656        self.paths
657            .iter()
658            .filter(|entry| entry.value().state == PathState::Active)
659            .map(|entry| entry.value().clone())
660            .collect()
661    }
662
663    /// Get all paths
664    pub fn get_all_paths(&self) -> Vec<NetworkPath> {
665        self.paths
666            .iter()
667            .map(|entry| entry.value().clone())
668            .collect()
669    }
670
671    /// Check if automatic migration should be triggered
672    pub fn should_migrate(&self, current_path_id: PathId) -> Option<PathId> {
673        if !self.config.enable_auto_migration {
674            return None;
675        }
676
677        let current_path = self.paths.get(&current_path_id)?;
678        let current_quality = current_path.quality.score;
679
680        // Find best alternative path
681        let best_path = self
682            .paths
683            .iter()
684            .filter(|entry| {
685                entry.value().id != current_path_id && entry.value().state == PathState::Active
686            })
687            .max_by(|a, b| {
688                a.value()
689                    .quality
690                    .score
691                    .partial_cmp(&b.value().quality.score)
692                    .unwrap_or(std::cmp::Ordering::Equal)
693            })?;
694
695        let best_quality = best_path.quality.score;
696
697        // Check if quality difference exceeds threshold
698        if best_quality - current_quality >= self.config.migration_quality_threshold {
699            info!(
700                "Migration recommended: path {} (quality={:.2}) -> path {} (quality={:.2})",
701                current_path_id,
702                current_quality,
703                best_path.value().id,
704                best_quality
705            );
706
707            let mut stats = self.stats.write();
708            stats.migrations_count += 1;
709
710            return Some(best_path.value().id);
711        }
712
713        None
714    }
715
716    /// Cleanup stale paths
717    pub fn cleanup_stale_paths(&self) {
718        let now = Instant::now();
719        let timeout = self.config.path_idle_timeout;
720
721        let stale_paths: Vec<PathId> = self
722            .paths
723            .iter()
724            .filter(|entry| {
725                let path = entry.value();
726                now.duration_since(path.last_used) > timeout && path.state != PathState::Active
727            })
728            .map(|entry| entry.value().id)
729            .collect();
730
731        for path_id in stale_paths {
732            info!("Removing stale path {}", path_id);
733            let _ = self.remove_path(path_id);
734        }
735    }
736
737    /// Get statistics
738    pub fn stats(&self) -> MultipathStats {
739        let mut stats = self.stats.read().clone();
740
741        // Calculate average quality score
742        let active_paths: Vec<_> = self
743            .paths
744            .iter()
745            .filter(|entry| entry.value().state == PathState::Active)
746            .collect();
747
748        if !active_paths.is_empty() {
749            let total_quality: f64 = active_paths
750                .iter()
751                .map(|entry| entry.value().quality.score)
752                .sum();
753            stats.avg_quality_score = total_quality / active_paths.len() as f64;
754
755            stats.best_path_quality = active_paths
756                .iter()
757                .map(|entry| entry.value().quality.score)
758                .fold(0.0, f64::max);
759        }
760
761        stats.active_paths = active_paths.len();
762
763        stats
764    }
765
766    /// Get configuration
767    pub fn config(&self) -> &MultipathConfig {
768        &self.config
769    }
770}
771
772#[cfg(test)]
773mod tests {
774    use super::*;
775
776    #[test]
777    fn test_path_quality_calculation() {
778        let mut quality = PathQuality::default();
779
780        // Update with good metrics
781        quality.update(10.0, 10_000_000, 0.01, 2.0);
782
783        assert!(quality.score > 0.7, "Good quality should have high score");
784        assert_eq!(quality.sample_count, 1);
785    }
786
787    #[test]
788    fn test_path_quality_ema() {
789        let mut quality = PathQuality::default();
790
791        // First update
792        quality.update(100.0, 1_000_000, 0.1, 10.0);
793        let first_rtt = quality.rtt_ms;
794
795        // Second update with better RTT
796        quality.update(50.0, 1_000_000, 0.1, 10.0);
797
798        assert!(
799            quality.rtt_ms < first_rtt,
800            "RTT should decrease with better sample"
801        );
802        assert!(
803            quality.rtt_ms > 50.0,
804            "RTT should be smoothed with EMA, not exact"
805        );
806        assert_eq!(quality.sample_count, 2);
807    }
808
809    #[test]
810    fn test_manager_creation() {
811        let config = MultipathConfig::default();
812        let manager = MultipathQuicManager::new(config);
813
814        let stats = manager.stats();
815        assert_eq!(stats.active_paths, 0);
816        assert_eq!(stats.paths_created, 0);
817    }
818
819    #[test]
820    fn test_add_path() {
821        let config = MultipathConfig::default();
822        let manager = MultipathQuicManager::new(config);
823
824        let local = "127.0.0.1:8080".parse().unwrap();
825        let remote = "192.168.1.1:9090".parse().unwrap();
826
827        let path_id = manager.add_path(local, remote).unwrap();
828
829        assert_eq!(path_id, 0);
830
831        // Path starts in Validating state, so active_paths is 0
832        let stats = manager.stats();
833        assert_eq!(stats.active_paths, 0);
834        assert_eq!(stats.paths_created, 1);
835
836        // Activate the path
837        manager
838            .update_path_state(path_id, PathState::Active)
839            .unwrap();
840
841        // Now it should be counted as active
842        let stats = manager.stats();
843        assert_eq!(stats.active_paths, 1);
844    }
845
846    #[test]
847    fn test_max_paths_limit() {
848        let config = MultipathConfig {
849            max_paths: 2,
850            ..Default::default()
851        };
852        let manager = MultipathQuicManager::new(config);
853
854        let local = "127.0.0.1:8080".parse().unwrap();
855        let remote = "192.168.1.1:9090".parse().unwrap();
856
857        // Add 2 paths (should succeed)
858        manager.add_path(local, remote).unwrap();
859        manager.add_path(local, remote).unwrap();
860
861        // Try to add 3rd path (should fail)
862        let result = manager.add_path(local, remote);
863        assert!(result.is_err());
864        assert!(matches!(result, Err(MultipathError::MaxPathsReached(2))));
865    }
866
867    #[test]
868    fn test_remove_path() {
869        let config = MultipathConfig::default();
870        let manager = MultipathQuicManager::new(config);
871
872        let local = "127.0.0.1:8080".parse().unwrap();
873        let remote = "192.168.1.1:9090".parse().unwrap();
874
875        let path_id = manager.add_path(local, remote).unwrap();
876        manager.remove_path(path_id).unwrap();
877
878        let stats = manager.stats();
879        assert_eq!(stats.active_paths, 0);
880    }
881
882    #[test]
883    fn test_update_path_state() {
884        let config = MultipathConfig::default();
885        let manager = MultipathQuicManager::new(config);
886
887        let local = "127.0.0.1:8080".parse().unwrap();
888        let remote = "192.168.1.1:9090".parse().unwrap();
889
890        let path_id = manager.add_path(local, remote).unwrap();
891
892        manager
893            .update_path_state(path_id, PathState::Active)
894            .unwrap();
895
896        let path = manager.get_path(path_id).unwrap();
897        assert_eq!(path.state, PathState::Active);
898    }
899
900    #[test]
901    fn test_path_selection_round_robin() {
902        let config = MultipathConfig {
903            strategy: PathSelectionStrategy::RoundRobin,
904            ..Default::default()
905        };
906        let manager = MultipathQuicManager::new(config);
907
908        let local = "127.0.0.1:8080".parse().unwrap();
909        let remote = "192.168.1.1:9090".parse().unwrap();
910
911        let path1 = manager.add_path(local, remote).unwrap();
912        let path2 = manager.add_path(local, remote).unwrap();
913
914        manager.update_path_state(path1, PathState::Active).unwrap();
915        manager.update_path_state(path2, PathState::Active).unwrap();
916
917        let selected1 = manager.select_path().unwrap();
918        let selected2 = manager.select_path().unwrap();
919
920        assert_ne!(selected1, selected2, "Round robin should alternate paths");
921    }
922
923    #[test]
924    fn test_path_selection_quality_based() {
925        let config = MultipathConfig {
926            strategy: PathSelectionStrategy::QualityBased,
927            ..Default::default()
928        };
929        let manager = MultipathQuicManager::new(config);
930
931        let local = "127.0.0.1:8080".parse().unwrap();
932        let remote = "192.168.1.1:9090".parse().unwrap();
933
934        let path1 = manager.add_path(local, remote).unwrap();
935        let path2 = manager.add_path(local, remote).unwrap();
936
937        manager.update_path_state(path1, PathState::Active).unwrap();
938        manager.update_path_state(path2, PathState::Active).unwrap();
939
940        // Give path2 better quality
941        manager
942            .update_path_quality(path1, 100.0, 1_000_000, 0.1, 10.0)
943            .unwrap();
944        manager
945            .update_path_quality(path2, 10.0, 10_000_000, 0.01, 2.0)
946            .unwrap();
947
948        let selected = manager.select_path().unwrap();
949        assert_eq!(selected, path2, "Should select higher quality path");
950    }
951
952    #[test]
953    fn test_path_selection_lowest_latency() {
954        let config = MultipathConfig {
955            strategy: PathSelectionStrategy::LowestLatency,
956            ..Default::default()
957        };
958        let manager = MultipathQuicManager::new(config);
959
960        let local = "127.0.0.1:8080".parse().unwrap();
961        let remote = "192.168.1.1:9090".parse().unwrap();
962
963        let path1 = manager.add_path(local, remote).unwrap();
964        let path2 = manager.add_path(local, remote).unwrap();
965
966        manager.update_path_state(path1, PathState::Active).unwrap();
967        manager.update_path_state(path2, PathState::Active).unwrap();
968
969        // path1 has lower latency
970        manager
971            .update_path_quality(path1, 10.0, 1_000_000, 0.1, 5.0)
972            .unwrap();
973        manager
974            .update_path_quality(path2, 100.0, 10_000_000, 0.01, 2.0)
975            .unwrap();
976
977        let selected = manager.select_path().unwrap();
978        assert_eq!(selected, path1, "Should select lowest latency path");
979    }
980
981    #[test]
982    fn test_record_sent_received() {
983        let config = MultipathConfig::default();
984        let manager = MultipathQuicManager::new(config);
985
986        let local = "127.0.0.1:8080".parse().unwrap();
987        let remote = "192.168.1.1:9090".parse().unwrap();
988
989        let path_id = manager.add_path(local, remote).unwrap();
990
991        manager.record_sent(path_id, 1000);
992        manager.record_received(path_id, 500);
993
994        let path = manager.get_path(path_id).unwrap();
995        assert_eq!(path.bytes_sent, 1000);
996        assert_eq!(path.bytes_received, 500);
997
998        let stats = manager.stats();
999        assert_eq!(stats.total_bytes_sent, 1000);
1000        assert_eq!(stats.total_bytes_received, 500);
1001    }
1002
1003    #[test]
1004    fn test_auto_migration() {
1005        let config = MultipathConfig {
1006            enable_auto_migration: true,
1007            migration_quality_threshold: 0.2,
1008            ..Default::default()
1009        };
1010        let manager = MultipathQuicManager::new(config);
1011
1012        let local = "127.0.0.1:8080".parse().unwrap();
1013        let remote = "192.168.1.1:9090".parse().unwrap();
1014
1015        let path1 = manager.add_path(local, remote).unwrap();
1016        let path2 = manager.add_path(local, remote).unwrap();
1017
1018        manager.update_path_state(path1, PathState::Active).unwrap();
1019        manager.update_path_state(path2, PathState::Active).unwrap();
1020
1021        // path1 has poor quality, path2 has good quality
1022        manager
1023            .update_path_quality(path1, 200.0, 500_000, 0.2, 20.0)
1024            .unwrap();
1025        manager
1026            .update_path_quality(path2, 10.0, 10_000_000, 0.01, 2.0)
1027            .unwrap();
1028
1029        let migration = manager.should_migrate(path1);
1030        assert_eq!(migration, Some(path2), "Should recommend migration");
1031
1032        let stats = manager.stats();
1033        assert_eq!(stats.migrations_count, 1);
1034    }
1035
1036    #[test]
1037    fn test_config_presets() {
1038        let low_latency = MultipathConfig::low_latency();
1039        assert_eq!(low_latency.strategy, PathSelectionStrategy::LowestLatency);
1040        assert_eq!(low_latency.max_paths, 2);
1041
1042        let high_bandwidth = MultipathConfig::high_bandwidth();
1043        assert_eq!(
1044            high_bandwidth.strategy,
1045            PathSelectionStrategy::HighestBandwidth
1046        );
1047
1048        let high_reliability = MultipathConfig::high_reliability();
1049        assert_eq!(high_reliability.strategy, PathSelectionStrategy::Redundant);
1050        assert!(high_reliability.enable_redundancy);
1051
1052        let mobile = MultipathConfig::mobile();
1053        assert_eq!(mobile.max_paths, 2);
1054        assert!(mobile.enable_auto_migration);
1055    }
1056
1057    #[test]
1058    fn test_select_all_paths() {
1059        let config = MultipathConfig::default();
1060        let manager = MultipathQuicManager::new(config);
1061
1062        let local = "127.0.0.1:8080".parse().unwrap();
1063        let remote = "192.168.1.1:9090".parse().unwrap();
1064
1065        let path1 = manager.add_path(local, remote).unwrap();
1066        let path2 = manager.add_path(local, remote).unwrap();
1067
1068        manager.update_path_state(path1, PathState::Active).unwrap();
1069        manager.update_path_state(path2, PathState::Active).unwrap();
1070
1071        let all_paths = manager.select_all_paths();
1072        assert_eq!(all_paths.len(), 2);
1073        assert!(all_paths.contains(&path1));
1074        assert!(all_paths.contains(&path2));
1075    }
1076
1077    #[test]
1078    fn test_quality_threshold_degradation() {
1079        let config = MultipathConfig {
1080            min_quality_threshold: 0.5,
1081            ..Default::default()
1082        };
1083        let manager = MultipathQuicManager::new(config);
1084
1085        let local = "127.0.0.1:8080".parse().unwrap();
1086        let remote = "192.168.1.1:9090".parse().unwrap();
1087
1088        let path_id = manager.add_path(local, remote).unwrap();
1089        manager
1090            .update_path_state(path_id, PathState::Active)
1091            .unwrap();
1092
1093        // Update with poor quality
1094        manager
1095            .update_path_quality(path_id, 300.0, 100_000, 0.5, 50.0)
1096            .unwrap();
1097
1098        let path = manager.get_path(path_id).unwrap();
1099        assert_eq!(
1100            path.state,
1101            PathState::Degraded,
1102            "Path should be marked degraded due to low quality"
1103        );
1104    }
1105}