Skip to main content

aria2_core/engine/
bt_seed_manager.rs

1use std::collections::HashMap;
2use std::net::SocketAddr;
3use std::sync::Arc;
4use std::sync::atomic::{AtomicU64, Ordering};
5use std::time::{Duration, Instant};
6use tracing::{debug, info, warn};
7
8use crate::error::{Aria2Error, Result};
9
10use super::bt_upload_session::{BtSeedingConfig, BtUploadSession, PieceDataProvider};
11use super::choking_algorithm::{ChokeAction, ChokingAlgorithm};
12
13/// Threshold for banning peers that send too many invalid pieces.
14///
15/// When a peer sends `BAD_DATA_THRESHOLD` or more pieces with invalid hashes,
16/// they are permanently banned for the remainder of the session.
17pub const BAD_DATA_THRESHOLD: u32 = 3;
18
19/// Represents an active upload session with a peer.
20///
21/// Tracks upload statistics and session state for a single peer connection
22/// during the seeding phase.
23#[derive(Debug, Clone)]
24pub struct UploadSession {
25    /// Peer's socket address
26    pub peer_addr: SocketAddr,
27    /// Bytes uploaded to this peer in the current session
28    pub uploaded_bytes: u64,
29    /// Upload speed in bytes/sec (rolling average)
30    pub upload_speed: u64,
31    /// Last time data was uploaded to this peer
32    pub last_upload_time: Instant,
33    /// Whether this session is active
34    pub is_active: bool,
35}
36
37impl UploadSession {
38    /// Create a new upload session for a peer.
39    pub fn new(peer_addr: SocketAddr) -> Self {
40        Self {
41            peer_addr,
42            uploaded_bytes: 0,
43            upload_speed: 0,
44            last_upload_time: Instant::now(),
45            is_active: true,
46        }
47    }
48
49    /// Record bytes uploaded to this peer.
50    pub fn record_upload(&mut self, bytes: u64) {
51        self.uploaded_bytes += bytes;
52        self.last_upload_time = Instant::now();
53    }
54
55    /// Update the upload speed (rolling average).
56    pub fn update_speed(&mut self, speed: u64) {
57        self.upload_speed = speed;
58    }
59
60    /// Mark this session as inactive.
61    pub fn deactivate(&mut self) {
62        self.is_active = false;
63    }
64}
65
66#[derive(Debug, Clone, Default)]
67pub struct SeedExitCondition {
68    pub seed_time: Option<Duration>,
69    pub seed_ratio: Option<f64>,
70}
71
72impl SeedExitCondition {
73    pub fn infinite() -> Self {
74        Self {
75            seed_time: None,
76            seed_ratio: None,
77        }
78    }
79
80    pub fn with_time(secs: u64) -> Self {
81        if secs == 0 {
82            Self::infinite()
83        } else {
84            Self {
85                seed_time: Some(Duration::from_secs(secs)),
86                seed_ratio: None,
87            }
88        }
89    }
90
91    pub fn with_ratio(ratio: f64) -> Self {
92        if ratio <= 0.0 {
93            Self::infinite()
94        } else {
95            Self {
96                seed_time: None,
97                seed_ratio: Some(ratio),
98            }
99        }
100    }
101
102    pub fn with_time_and_ratio(secs: u64, ratio: f64) -> Self {
103        let time = if secs == 0 {
104            None
105        } else {
106            Some(Duration::from_secs(secs))
107        };
108        let r = if ratio <= 0.0 { None } else { Some(ratio) };
109        Self {
110            seed_time: time,
111            seed_ratio: r,
112        }
113    }
114
115    /// Check if the seed ratio condition has been met.
116    ///
117    /// Returns `true` when seeding should stop based on upload/download ratio:
118    /// - If `seed_ratio <= 0.0`, returns `false` (infinite seeding)
119    /// - If `downloaded == 0`, returns `false` (nothing downloaded yet)
120    /// - Otherwise, checks if `uploaded / downloaded >= seed_ratio`
121    ///
122    /// # Arguments
123    ///
124    /// * `uploaded` - Total bytes uploaded during this session
125    /// * `downloaded` - Total bytes downloaded during this session
126    /// * `seed_ratio` - Target ratio (e.g., 1.0 means 1:1 upload:download)
127    pub fn check_seed_condition(uploaded: u64, downloaded: u64, seed_ratio: f64) -> bool {
128        if seed_ratio <= 0.0 {
129            return false; // Infinite seeding (ratio 0 or negative)
130        }
131        if downloaded == 0 {
132            return false; // Nothing downloaded yet, can't compute ratio
133        }
134        (uploaded as f64 / downloaded as f64) >= seed_ratio
135    }
136
137    /// Check if the seed time condition has been met.
138    ///
139    /// Returns `true` when pure seeding duration exceeds the limit:
140    /// - If `seed_time_secs == 0`, returns `false` (infinite seeding)
141    /// - If not in pure seeding phase (`is_pure_seeding == false`), returns `false`
142    /// - Otherwise, checks if elapsed seconds since `seeding_started_at >= seed_time_secs`
143    ///
144    /// # Arguments
145    ///
146    /// * `seeding_started_at` - Instant when pure seeding phase began
147    /// * `seed_time_secs` - Maximum allowed seeding duration in seconds (0 = infinite)
148    /// * `is_pure_seeding` - Whether all pieces are complete (true = in seeding phase)
149    pub fn check_seed_time(
150        seeding_started_at: Instant,
151        seed_time_secs: u64,
152        is_pure_seeding: bool,
153    ) -> bool {
154        if seed_time_secs == 0 {
155            return false; // Infinite seeding
156        }
157        if !is_pure_seeding {
158            return false; // Still downloading, haven't entered pure seeding yet
159        }
160        seeding_started_at.elapsed().as_secs() >= seed_time_secs
161    }
162}
163
164pub struct BtSeedManager {
165    /// Info hash of the torrent being seeded
166    info_hash: [u8; 20],
167    sessions: Vec<BtUploadSession>,
168    piece_data: Arc<dyn PieceDataProvider>,
169    config: BtSeedingConfig,
170    exit_condition: SeedExitCondition,
171    pub total_uploaded: u64,
172    total_downloaded: u64,
173    pub seeding_start_time: Instant,
174    last_optimistic_unchoke: Instant,
175    optimistic_round: usize,
176    /// Choking algorithm for tit-for-tat peer selection during seeding.
177    /// When present, drives intelligent choke/unchoke decisions every rotation interval.
178    pub choking_algo: Option<ChokingAlgorithm>,
179
180    // Upload statistics (atomic for thread-safe access)
181    /// Total uploaded bytes (atomic for concurrent access)
182    uploaded_bytes_atomic: AtomicU64,
183    /// Current upload speed in bytes/sec
184    upload_speed_atomic: AtomicU64,
185    /// Last upload timestamp.
186    /// Uses std::sync::Mutex because the lock is only held for short synchronous
187    /// reads/writes of an Instant and never across .await points.
188    last_upload_time: std::sync::Mutex<Instant>,
189
190    // Seeding control
191    /// Target seed ratio (upload/download)
192    seed_ratio: f64,
193    /// Target seed duration
194    seed_time: Duration,
195
196    // Active upload tracking
197    /// Map of peer address to upload session
198    active_uploads: HashMap<SocketAddr, UploadSession>,
199    /// Maximum number of concurrent uploads
200    max_uploads: usize,
201
202    // Upload speed limiting
203    /// Maximum upload speed in bytes/sec (None = unlimited)
204    max_upload_speed: Option<u64>,
205    /// Timestamp of last speed throttle check.
206    /// Uses std::sync::Mutex because the lock is only held for short synchronous
207    /// reads/writes of an Instant and never across .await points.
208    last_throttle_check: std::sync::Mutex<Instant>,
209    /// Bytes uploaded in current throttle window
210    throttle_window_bytes: AtomicU64,
211}
212
213impl BtSeedManager {
214    /// Create a new BtSeedManager with default settings.
215    ///
216    /// # Arguments
217    ///
218    /// * `connections` - Peer connections to use for seeding
219    /// * `piece_data` - Provider for piece data
220    /// * `config` - Seeding configuration
221    /// * `exit_condition` - Conditions for exiting seeding
222    /// * `total_downloaded` - Total bytes downloaded (for ratio calculation)
223    pub fn new(
224        connections: Vec<aria2_protocol::bittorrent::peer::connection::PeerConnection>,
225        piece_data: Arc<dyn PieceDataProvider>,
226        config: BtSeedingConfig,
227        exit_condition: SeedExitCondition,
228        total_downloaded: u64,
229    ) -> Self {
230        Self::new_with_choking_algo(
231            connections,
232            piece_data,
233            config,
234            exit_condition,
235            total_downloaded,
236            None,
237        )
238    }
239
240    /// Create a new BtSeedManager with an optional ChokingAlgorithm.
241    ///
242    /// When `choking_algo` is `Some`, the seeding loop will call
243    /// [`ChokingAlgorithm::rotate_choke`] every `config.choke_rotation_interval_secs`
244    /// and apply the resulting choke/unchoke actions to sessions.
245    pub fn new_with_choking_algo(
246        connections: Vec<aria2_protocol::bittorrent::peer::connection::PeerConnection>,
247        piece_data: Arc<dyn PieceDataProvider>,
248        config: BtSeedingConfig,
249        exit_condition: SeedExitCondition,
250        total_downloaded: u64,
251        choking_algo: Option<ChokingAlgorithm>,
252    ) -> Self {
253        let sessions = connections
254            .into_iter()
255            .map(|conn| BtUploadSession::new(conn, &config))
256            .collect();
257
258        let seed_ratio = exit_condition.seed_ratio.unwrap_or(0.0);
259        let seed_time = exit_condition.seed_time.unwrap_or(Duration::ZERO);
260        let max_uploads = config.max_peers_to_unchoke;
261        let max_upload_speed = config.max_upload_bytes_per_sec;
262
263        Self {
264            info_hash: [0u8; 20], // Default, can be set via builder
265            sessions,
266            piece_data,
267            config,
268            exit_condition,
269            total_uploaded: 0,
270            total_downloaded,
271            seeding_start_time: Instant::now(),
272            last_optimistic_unchoke: Instant::now(),
273            optimistic_round: 0,
274            choking_algo,
275            uploaded_bytes_atomic: AtomicU64::new(0),
276            upload_speed_atomic: AtomicU64::new(0),
277            last_upload_time: std::sync::Mutex::new(Instant::now()),
278            seed_ratio,
279            seed_time,
280            active_uploads: HashMap::new(),
281            max_uploads,
282            max_upload_speed,
283            last_throttle_check: std::sync::Mutex::new(Instant::now()),
284            throttle_window_bytes: AtomicU64::new(0),
285        }
286    }
287
288    /// Create a new BtSeedManager with explicit info_hash.
289    ///
290    /// This is the preferred constructor when the info_hash is known.
291    pub fn new_with_info_hash(
292        info_hash: [u8; 20],
293        connections: Vec<aria2_protocol::bittorrent::peer::connection::PeerConnection>,
294        piece_data: Arc<dyn PieceDataProvider>,
295        config: BtSeedingConfig,
296        exit_condition: SeedExitCondition,
297        total_downloaded: u64,
298    ) -> Self {
299        let mut manager = Self::new(
300            connections,
301            piece_data,
302            config,
303            exit_condition,
304            total_downloaded,
305        );
306        manager.info_hash = info_hash;
307        manager
308    }
309
310    /// Handle a piece request from a peer.
311    ///
312    /// This method reads the requested piece data from the piece provider
313    /// and returns it for sending to the peer.
314    ///
315    /// # Arguments
316    ///
317    /// * `peer` - The peer's socket address
318    /// * `index` - The piece index
319    /// * `begin` - The offset within the piece
320    /// * `length` - The length of data to read
321    ///
322    /// # Returns
323    ///
324    /// The requested piece data, or an error if the piece is not available.
325    pub async fn handle_piece_request(
326        &mut self,
327        peer: SocketAddr,
328        index: u32,
329        begin: u32,
330        length: u32,
331    ) -> Result<Vec<u8>> {
332        // Apply upload speed throttling if configured
333        if let Some(max_speed) = self.max_upload_speed {
334            self.throttle_upload(max_speed, length as u64).await?;
335        }
336
337        // Get piece data from provider
338        let piece_data = self
339            .piece_data
340            .get_piece_data(index, begin, length)
341            .ok_or_else(|| {
342                Aria2Error::Recoverable(crate::error::RecoverableError::InvalidPieceIndex {
343                    index,
344                    max_index: self.piece_data.num_pieces(),
345                })
346            })?;
347
348        // Update upload statistics
349        self.uploaded_bytes_atomic
350            .fetch_add(piece_data.len() as u64, Ordering::Relaxed);
351        self.total_uploaded += piece_data.len() as u64;
352
353        // Update session tracking
354        if let Some(session) = self.active_uploads.get_mut(&peer) {
355            session.record_upload(piece_data.len() as u64);
356        } else {
357            let mut session = UploadSession::new(peer);
358            session.record_upload(piece_data.len() as u64);
359            self.active_uploads.insert(peer, session);
360        }
361
362        // Update last upload time
363        if let Ok(mut last) = self.last_upload_time.lock() {
364            *last = Instant::now();
365        }
366
367        debug!(
368            "Handled piece request: peer={}, index={}, offset={}, len={}, total_uploaded={}",
369            peer, index, begin, length, self.total_uploaded
370        );
371
372        Ok(piece_data)
373    }
374
375    /// Throttle upload speed to respect the configured maximum.
376    ///
377    /// Uses a token-bucket style algorithm with a 1-second window.
378    /// If the current window has exceeded the speed limit, sleeps until
379    /// the window resets.
380    ///
381    /// # Arguments
382    ///
383    /// * `max_speed` - Maximum upload speed in bytes/sec
384    /// * `bytes_to_upload` - Number of bytes about to be uploaded
385    async fn throttle_upload(&self, max_speed: u64, bytes_to_upload: u64) -> Result<()> {
386        if max_speed == 0 {
387            return Ok(()); // No limit
388        }
389
390        let now = Instant::now();
391        let window_bytes = self.throttle_window_bytes.load(Ordering::Relaxed);
392
393        // Check if we need to reset the window (every 1 second)
394        let should_reset = if let Ok(last_check) = self.last_throttle_check.lock() {
395            now.duration_since(*last_check) >= Duration::from_secs(1)
396        } else {
397            false
398        };
399
400        if should_reset {
401            // Reset the window
402            self.throttle_window_bytes.store(0, Ordering::Relaxed);
403            if let Ok(mut last_check) = self.last_throttle_check.lock() {
404                *last_check = now;
405            }
406        }
407
408        // Check if we would exceed the limit
409        if window_bytes + bytes_to_upload > max_speed {
410            // Calculate how long to wait
411            let bytes_over = window_bytes + bytes_to_upload - max_speed;
412            let wait_secs = bytes_over as f64 / max_speed as f64;
413            let wait_duration = Duration::from_secs_f64(wait_secs.min(1.0));
414
415            debug!(
416                "Throttling upload: {} bytes over limit, waiting {:?}",
417                bytes_over, wait_duration
418            );
419
420            tokio::time::sleep(wait_duration).await;
421
422            // Reset window after waiting
423            self.throttle_window_bytes.store(0, Ordering::Relaxed);
424            if let Ok(mut last_check) = self.last_throttle_check.lock() {
425                *last_check = Instant::now();
426            }
427        }
428
429        // Track the bytes we're about to upload
430        self.throttle_window_bytes
431            .fetch_add(bytes_to_upload, Ordering::Relaxed);
432
433        Ok(())
434    }
435
436    /// Check if seeding should stop based on configured conditions.
437    ///
438    /// Returns `true` when either:
439    /// - The seed ratio has been reached (uploaded >= ratio * downloaded)
440    /// - The seed time has elapsed
441    ///
442    /// # Arguments
443    ///
444    /// * `downloaded_bytes` - Total bytes downloaded (for ratio calculation)
445    pub fn should_stop_seeding(&self, downloaded_bytes: u64) -> bool {
446        // Check seed ratio
447        if self.seed_ratio > 0.0 && downloaded_bytes > 0 {
448            let uploaded = self.uploaded_bytes_atomic.load(Ordering::Relaxed);
449            let ratio = uploaded as f64 / downloaded_bytes as f64;
450            if ratio >= self.seed_ratio {
451                info!(
452                    "Seed ratio reached: {:.2} >= {:.2} (uploaded={}, downloaded={})",
453                    ratio, self.seed_ratio, uploaded, downloaded_bytes
454                );
455                return true;
456            }
457        }
458
459        // Check seed time
460        if self.seed_time > Duration::ZERO {
461            let elapsed = self.seeding_start_time.elapsed();
462            if elapsed >= self.seed_time {
463                info!("Seed time reached: {:?} >= {:?}", elapsed, self.seed_time);
464                return true;
465            }
466        }
467
468        false
469    }
470
471    /// Get upload statistics.
472    ///
473    /// Returns a tuple of (total_uploaded_bytes, current_upload_speed).
474    pub fn get_upload_stats(&self) -> (u64, u64) {
475        let uploaded = self.uploaded_bytes_atomic.load(Ordering::Relaxed);
476        let speed = self.upload_speed_atomic.load(Ordering::Relaxed);
477        (uploaded, speed)
478    }
479
480    /// Update the upload speed statistic.
481    ///
482    /// This should be called periodically to track the current upload rate.
483    pub fn update_upload_speed(&self, speed: u64) {
484        self.upload_speed_atomic.store(speed, Ordering::Relaxed);
485    }
486
487    /// Get the info hash of the torrent being seeded.
488    pub fn info_hash(&self) -> &[u8; 20] {
489        &self.info_hash
490    }
491
492    /// Set the info hash.
493    pub fn set_info_hash(&mut self, info_hash: [u8; 20]) {
494        self.info_hash = info_hash;
495    }
496
497    /// Get the number of active upload sessions.
498    pub fn num_active_uploads(&self) -> usize {
499        self.active_uploads.values().filter(|s| s.is_active).count()
500    }
501
502    /// Get the maximum number of concurrent uploads.
503    pub fn max_uploads(&self) -> usize {
504        self.max_uploads
505    }
506
507    /// Set the maximum number of concurrent uploads.
508    pub fn set_max_uploads(&mut self, max: usize) {
509        self.max_uploads = max;
510    }
511
512    /// Get the seed ratio target.
513    pub fn seed_ratio(&self) -> f64 {
514        self.seed_ratio
515    }
516
517    /// Get the seed time target.
518    pub fn seed_time(&self) -> Duration {
519        self.seed_time
520    }
521
522    /// Get a reference to active upload sessions.
523    pub fn active_uploads(&self) -> &HashMap<SocketAddr, UploadSession> {
524        &self.active_uploads
525    }
526
527    /// Get total downloaded bytes.
528    pub fn total_downloaded(&self) -> u64 {
529        self.total_downloaded
530    }
531
532    pub async fn run_seeding_loop(&mut self) -> Result<()> {
533        info!(
534            "Seeding started: {} peers, condition={:?}, choking_algo={}",
535            self.sessions.len(),
536            self.exit_condition,
537            self.choking_algo.is_some()
538        );
539
540        for session in &mut self.sessions {
541            session.unchoke_peer().await.ok();
542        }
543
544        // Determine choke rotation interval from choking_algo config or fallback
545        let choke_rotation_secs = self
546            .choking_algo
547            .as_ref()
548            .map(|c| c.config().choke_rotation_interval_secs)
549            .unwrap_or(10);
550        let mut choke_interval = tokio::time::interval(Duration::from_secs(choke_rotation_secs));
551        // Don't let the interval accumulate missed ticks
552        choke_interval.set_missed_tick_behavior(tokio::time::MissedTickBehavior::Delay);
553
554        loop {
555            // --- Choking algorithm rotation (every N seconds) ---
556            if let Some(ref mut algo) = self.choking_algo {
557                choke_interval.tick().await;
558                let actions = algo.rotate_choke();
559                for action in actions {
560                    match action {
561                        ChokeAction::Unchoke(idx) => {
562                            if let Some(session) = self.sessions.get_mut(idx)
563                                && !session.is_dead()
564                                && session.is_peer_choked()
565                            {
566                                debug!("ChokingAlgo: Unchoke peer #{}", idx);
567                                session.unchoke_peer().await.ok();
568                            }
569                        }
570                        ChokeAction::Choke(idx) => {
571                            if let Some(session) = self.sessions.get_mut(idx)
572                                && !session.is_dead()
573                                && !session.is_peer_choked()
574                            {
575                                debug!("ChokingAlgo: Choke peer #{}", idx);
576                                session.choke_peer().await.ok();
577                            }
578                        }
579                        ChokeAction::NoChange(_) => {}
580                    }
581                }
582            }
583
584            // --- Process incoming messages from all sessions ---
585            let mut alive_sessions = Vec::new();
586            for session in &mut self.sessions {
587                if !session.is_dead() {
588                    match session
589                        .handle_incoming_messages(self.piece_data.as_ref())
590                        .await
591                    {
592                        Ok(uploaded) => {
593                            self.total_uploaded += uploaded;
594                        }
595                        Err(e) => {
596                            warn!("Upload session error: {}", e);
597                            session.is_dead = true;
598                        }
599                    }
600                }
601                if !session.is_dead() {
602                    alive_sessions.push(session.uploaded_bytes());
603                }
604            }
605
606            // Fallback: optimistic unchoke when no choking algorithm is configured
607            if self.choking_algo.is_none() {
608                self.maybe_optimistic_unchoke().await;
609            }
610
611            if self.should_exit() {
612                info!(
613                    "Seeding exit condition met after {:?}",
614                    self.seeding_start_time.elapsed()
615                );
616                break;
617            }
618
619            if alive_sessions.is_empty() && !self.sessions.is_empty() {
620                debug!("All upload peers disconnected");
621                break;
622            }
623
624            tokio::time::sleep(Duration::from_millis(50)).await;
625        }
626
627        for session in &mut self.sessions {
628            if !session.is_dead() {
629                session.choke_peer().await.ok();
630            }
631        }
632
633        Ok(())
634    }
635
636    pub fn should_exit(&self) -> bool {
637        let elapsed = self.seeding_start_time.elapsed();
638
639        if let Some(max_time) = self.exit_condition.seed_time
640            && elapsed >= max_time
641        {
642            return true;
643        }
644
645        if let Some(ratio) = self.exit_condition.seed_ratio
646            && self.total_downloaded > 0
647        {
648            let actual_ratio = self.total_uploaded as f64 / self.total_downloaded as f64;
649            if actual_ratio >= ratio {
650                return true;
651            }
652        }
653
654        false
655    }
656
657    async fn maybe_optimistic_unchoke(&mut self) {
658        let interval = Duration::from_secs(self.config.optimistic_unchoke_interval_secs);
659        if self.last_optimistic_unchoke.elapsed() < interval {
660            return;
661        }
662        self.last_optimistic_unchoke = Instant::now();
663        self.optimistic_round += 1;
664
665        let choked_indices: Vec<usize> = self
666            .sessions
667            .iter()
668            .enumerate()
669            .filter(|(_, s)| !s.is_dead() && s.is_peer_choked())
670            .map(|(i, _)| i)
671            .collect();
672
673        if choked_indices.is_empty() {
674            return;
675        }
676
677        let idx = self.optimistic_round % choked_indices.len();
678        let target = choked_indices[idx];
679        if let Some(session) = self.sessions.get_mut(target) {
680            debug!("Optimistic unchoke peer #{}", target);
681            session.unchoke_peer().await.ok();
682        }
683    }
684
685    pub fn total_uploaded(&self) -> u64 {
686        self.total_uploaded
687    }
688
689    pub fn seeding_duration(&self) -> Duration {
690        self.seeding_start_time.elapsed()
691    }
692
693    pub fn num_alive_peers(&self) -> usize {
694        self.sessions.iter().filter(|s| !s.is_dead()).count()
695    }
696
697    pub fn num_total_peers(&self) -> usize {
698        self.sessions.len()
699    }
700
701    // ------------------------------------------------------------------
702    // Choking algorithm integration helpers
703    // ------------------------------------------------------------------
704
705    /// Sync upload statistics from sessions into the choking algorithm.
706    ///
707    /// Call this periodically (e.g., after each message handling round) so
708    /// the algorithm has up-to-date speed data for scoring.
709    pub fn sync_choking_algo_stats(&mut self) {
710        if let Some(ref mut algo) = self.choking_algo {
711            for (i, session) in self.sessions.iter().enumerate() {
712                if let Some(peer) = algo.get_peer_mut(i) {
713                    // Update uploaded bytes from the session
714                    let session_uploaded = session.uploaded_bytes();
715                    if session_uploaded > peer.uploaded_bytes {
716                        peer.on_data_sent(session_uploaded - peer.uploaded_bytes);
717                    }
718                }
719            }
720        }
721    }
722
723    /// Get a reference to the choking algorithm, if configured.
724    pub fn choking_algo(&self) -> Option<&ChokingAlgorithm> {
725        self.choking_algo.as_ref()
726    }
727
728    /// Get a mutable reference to the choking algorithm, if configured.
729    pub fn choking_algo_mut(&mut self) -> Option<&mut ChokingAlgorithm> {
730        self.choking_algo.as_mut()
731    }
732
733    // ------------------------------------------------------------------
734    // Upload speed limit API
735    // ------------------------------------------------------------------
736
737    /// Get the maximum upload speed limit (bytes/sec).
738    ///
739    /// Returns `None` if unlimited.
740    pub fn max_upload_speed(&self) -> Option<u64> {
741        self.max_upload_speed
742    }
743
744    /// Set the maximum upload speed limit (bytes/sec).
745    ///
746    /// Set to `None` or `Some(0)` for unlimited.
747    pub fn set_max_upload_speed(&mut self, speed: Option<u64>) {
748        self.max_upload_speed = speed.filter(|&s| s > 0);
749    }
750
751    /// Get the current upload speed (bytes/sec).
752    pub fn current_upload_speed(&self) -> u64 {
753        self.upload_speed_atomic.load(Ordering::Relaxed)
754    }
755
756    /// Calculate and update the current upload speed.
757    ///
758    /// This should be called periodically (e.g., every second) to track
759    /// the actual upload rate.
760    pub fn calculate_upload_speed(&self) -> u64 {
761        let now = Instant::now();
762        if let Ok(mut last_time) = self.last_upload_time.lock() {
763            let elapsed = now.duration_since(*last_time);
764            if elapsed >= Duration::from_secs(1) {
765                let uploaded = self.uploaded_bytes_atomic.load(Ordering::Relaxed);
766                let speed = uploaded / elapsed.as_secs().max(1);
767                self.upload_speed_atomic.store(speed, Ordering::Relaxed);
768                *last_time = now;
769                return speed;
770            }
771        }
772        self.upload_speed_atomic.load(Ordering::Relaxed)
773    }
774}
775
776#[cfg(test)]
777mod tests {
778    use super::*;
779    use crate::engine::bt_upload_session::InMemoryPieceProvider;
780    use crate::engine::choking_algorithm::{ChokeAction, ChokingConfig};
781    use crate::engine::peer_stats::PeerStats;
782
783    #[test]
784    fn test_exit_condition_default_infinite() {
785        let cond = SeedExitCondition::default();
786        assert!(cond.seed_time.is_none());
787        assert!(cond.seed_ratio.is_none());
788    }
789
790    #[test]
791    fn test_exit_condition_with_time_zero_is_infinite() {
792        let cond = SeedExitCondition::with_time(0);
793        assert!(cond.seed_time.is_none());
794    }
795
796    #[test]
797    fn test_exit_condition_with_time_positive() {
798        let cond = SeedExitCondition::with_time(60);
799        assert_eq!(cond.seed_time, Some(Duration::from_secs(60)));
800    }
801
802    #[test]
803    fn test_exit_condition_with_ratio_zero_is_infinite() {
804        let cond = SeedExitCondition::with_ratio(0.0);
805        assert!(cond.seed_ratio.is_none());
806    }
807
808    #[test]
809    fn test_exit_condition_with_ratio_positive() {
810        let cond = SeedExitCondition::with_ratio(1.5);
811        assert_eq!(cond.seed_ratio, Some(1.5));
812    }
813
814    #[test]
815    fn test_exit_condition_combined() {
816        let cond = SeedExitCondition::with_time_and_ratio(120, 2.0);
817        assert_eq!(cond.seed_time, Some(Duration::from_secs(120)));
818        assert_eq!(cond.seed_ratio, Some(2.0));
819    }
820
821    #[test]
822    fn test_should_exit_by_time() {
823        let manager = make_test_manager(SeedExitCondition::with_time(1), 1000, 500);
824        assert!(!manager.should_exit());
825
826        let mut manager = manager;
827        manager.seeding_start_time = Instant::now() - Duration::from_secs(2);
828        assert!(manager.should_exit());
829    }
830
831    #[test]
832    fn test_should_exit_by_ratio() {
833        let manager = make_test_manager(SeedExitCondition::with_ratio(1.0), 1000, 499);
834        assert!(!manager.should_exit());
835
836        let mut manager = manager;
837        manager.total_uploaded = 1500;
838        assert!(manager.should_exit());
839    }
840
841    #[test]
842    fn test_should_not_exit_early() {
843        let manager = make_test_manager(SeedExitCondition::with_time_and_ratio(10, 3.0), 1000, 100);
844        assert!(!manager.should_exit());
845
846        let mut manager = manager;
847        manager.total_uploaded = 2000;
848        manager.seeding_start_time = Instant::now() - Duration::from_secs(5);
849        assert!(!manager.should_exit(), "Neither time nor ratio reached yet");
850    }
851
852    #[test]
853    fn test_seed_manager_stats() {
854        let manager = make_test_manager(SeedExitCondition::infinite(), 1024 * 100, 51200);
855        assert_eq!(manager.num_total_peers(), 0);
856        assert_eq!(manager.num_alive_peers(), 0);
857        assert_eq!(manager.total_uploaded(), 51200);
858    }
859
860    fn make_test_manager(
861        exit_cond: SeedExitCondition,
862        downloaded: u64,
863        uploaded: u64,
864    ) -> BtSeedManager {
865        let provider = Arc::new(InMemoryPieceProvider::new(16384, 10));
866        let config = BtSeedingConfig::default();
867        let conns: Vec<aria2_protocol::bittorrent::peer::connection::PeerConnection> = vec![];
868        let mut mgr = BtSeedManager::new(conns, provider, config, exit_cond, downloaded);
869        mgr.total_uploaded = uploaded;
870        mgr
871    }
872
873    fn make_test_manager_with_choking_algo(
874        exit_cond: SeedExitCondition,
875        downloaded: u64,
876        uploaded: u64,
877    ) -> BtSeedManager {
878        use std::net::SocketAddr;
879        let provider = Arc::new(InMemoryPieceProvider::new(16384, 10));
880        let config = BtSeedingConfig::default();
881        let conns: Vec<aria2_protocol::bittorrent::peer::connection::PeerConnection> = vec![];
882
883        // Create a choking algorithm with fast rotation for testing
884        let choking_config = ChokingConfig {
885            max_upload_slots: 2,
886            optimistic_unchoke_interval_secs: 1,
887            snubbed_timeout_secs: 1,
888            choke_rotation_interval_secs: 1,
889        };
890        let mut algo = ChokingAlgorithm::new(choking_config);
891        // Add dummy peer stats for testing
892        let addr: SocketAddr = "127.0.0.1:6881".parse().unwrap();
893        algo.add_peer(PeerStats::new([0u8; 20], addr));
894
895        let mut mgr = BtSeedManager::new_with_choking_algo(
896            conns,
897            provider,
898            config,
899            exit_cond,
900            downloaded,
901            Some(algo),
902        );
903        mgr.total_uploaded = uploaded;
904        mgr
905    }
906
907    // ==================================================================
908    // Choking algorithm integration tests
909    // ==================================================================
910
911    #[test]
912    fn test_bt_seed_manager_without_choking_algo() {
913        // Verify that BtSeedManager works without choking_algo
914        let mut manager = make_test_manager(SeedExitCondition::infinite(), 1000, 500);
915        assert!(manager.choking_algo.is_none());
916        assert!(manager.choking_algo().is_none());
917        assert!(manager.choking_algo_mut().is_none());
918
919        // Stats should still work
920        assert_eq!(manager.num_total_peers(), 0);
921        assert_eq!(manager.num_alive_peers(), 0);
922        assert_eq!(manager.total_uploaded(), 500);
923
924        // sync_choking_algo_stats should be a no-op when algo is None
925        manager.sync_choking_algo_stats(); // Should not panic
926    }
927
928    #[test]
929    fn test_bt_seed_manager_with_choking_algo() {
930        // Verify BtSeedManager with choking_algo initialized correctly
931        let manager = make_test_manager_with_choking_algo(SeedExitCondition::infinite(), 2000, 800);
932        assert!(manager.choking_algo.is_some());
933
934        // Check algo has peers
935        let algo = manager.choking_algo().unwrap();
936        assert_eq!(algo.len(), 1);
937        assert!(!algo.is_empty());
938
939        // Stats should work
940        assert_eq!(manager.num_total_peers(), 0); // sessions are empty (no real connections)
941        assert_eq!(manager.total_uploaded(), 800);
942    }
943
944    #[test]
945    fn test_bt_seed_manager_choking_algo_rotate_choke() {
946        // Verify rotate_choke produces actions through the seed manager
947        let mut manager = make_test_manager_with_choking_algo(SeedExitCondition::infinite(), 0, 0);
948
949        // Get mutable access and call rotate_choke
950        if let Some(algo) = manager.choking_algo_mut() {
951            let actions = algo.rotate_choke();
952
953            // With max_upload_slots=2 and 1 peer, we expect:
954            // - The peer should be unchoked (it's in top K)
955            let unchoke_count = actions
956                .iter()
957                .filter(|a| matches!(a, ChokeAction::Unchoke(_)))
958                .count();
959
960            assert_eq!(actions.len(), 1, "Should have one action for one peer");
961            assert_eq!(unchoke_count, 1, "Single peer should be unchoked (top-K)");
962        } else {
963            panic!("Expected choking_algo to be present");
964        }
965    }
966
967    #[test]
968    fn test_bt_seed_manager_new_with_none_algo() {
969        // new() should produce None for choking_algo by default
970        let provider = Arc::new(InMemoryPieceProvider::new(16384, 10));
971        let config = BtSeedingConfig::default();
972        let conns: Vec<aria2_protocol::bittorrent::peer::connection::PeerConnection> = vec![];
973
974        let mgr = BtSeedManager::new(conns, provider, config, SeedExitCondition::infinite(), 0);
975        assert!(mgr.choking_algo.is_none());
976    }
977
978    #[test]
979    fn test_bt_seed_manager_new_with_some_algo() {
980        // new_with_choking_algo(Some(...)) should preserve it
981        let provider = Arc::new(InMemoryPieceProvider::new(16384, 10));
982        let config = BtSeedingConfig::default();
983        let conns: Vec<aria2_protocol::bittorrent::peer::connection::PeerConnection> = vec![];
984
985        let choking_config = ChokingConfig::default();
986        let algo = ChokingAlgorithm::new(choking_config);
987
988        let mgr = BtSeedManager::new_with_choking_algo(
989            conns,
990            provider,
991            config,
992            SeedExitCondition::infinite(),
993            0,
994            Some(algo),
995        );
996        assert!(mgr.choking_algo.is_some());
997        assert_eq!(mgr.choking_algo.unwrap().len(), 0); // No peers added yet
998    }
999
1000    // ==================================================================
1001    // H2: Seed condition utility function tests
1002    // ==================================================================
1003
1004    #[test]
1005    fn test_seed_ratio_met_1_0() {
1006        // Ratio 1.0: should stop when uploaded >= downloaded
1007        assert!(!SeedExitCondition::check_seed_condition(500, 1000, 1.0));
1008        assert!(SeedExitCondition::check_seed_condition(1000, 1000, 1.0));
1009        assert!(SeedExitCondition::check_seed_condition(1500, 1000, 1.0));
1010    }
1011
1012    #[test]
1013    fn test_seed_ratio_infinite_never_stops() {
1014        // Ratio 0.0 means infinite seeding - never stops
1015        assert!(!SeedExitCondition::check_seed_condition(999999, 1, 0.0));
1016        assert!(!SeedExitCondition::check_seed_condition(u64::MAX, 1, -1.0));
1017    }
1018
1019    #[test]
1020    fn test_seed_ratio_zero_downloaded() {
1021        // Nothing downloaded yet - can't compute ratio
1022        assert!(!SeedExitCondition::check_seed_condition(1000, 0, 2.0));
1023    }
1024
1025    #[test]
1026    fn test_seed_time_met_stops() {
1027        let start = Instant::now();
1028        // Time not met immediately after start
1029        assert!(!SeedExitCondition::check_seed_time(start, 5, true));
1030        // Time met when elapsed >= limit (simulate with negative time)
1031        let past_start = Instant::now() - Duration::from_secs(10);
1032        assert!(SeedExitCondition::check_seed_time(past_start, 5, true));
1033    }
1034
1035    #[test]
1036    fn test_seed_time_infinite_zero() {
1037        // seed_time=0 means infinite seeding
1038        let start = Instant::now();
1039        assert!(!SeedExitCondition::check_seed_time(start, 0, true));
1040    }
1041
1042    #[test]
1043    fn test_seed_time_not_pure_seeding() {
1044        // If still downloading (not pure seeding), time check returns false
1045        let past_start = Instant::now() - Duration::from_secs(100);
1046        assert!(
1047            !SeedExitCondition::check_seed_time(past_start, 5, false),
1048            "Should not stop if not in pure seeding phase"
1049        );
1050    }
1051
1052    #[test]
1053    fn test_both_conditions_either_triggers_stop() {
1054        // Test that either condition being met triggers exit
1055        let past_start = Instant::now() - Duration::from_secs(100);
1056
1057        // Time condition met, ratio not met -> should stop (time triggers)
1058        assert!(
1059            SeedExitCondition::check_seed_time(past_start, 5, true)
1060                || SeedExitCondition::check_seed_condition(500, 1000, 2.0),
1061            "Time condition should trigger stop"
1062        );
1063
1064        // Ratio condition met, time not met -> should stop (ratio triggers)
1065        assert!(
1066            !SeedExitCondition::check_seed_time(Instant::now(), 100, true)
1067                && SeedExitCondition::check_seed_condition(2000, 1000, 1.5),
1068            "Ratio condition should trigger stop"
1069        );
1070
1071        // Neither met -> should NOT stop
1072        assert!(
1073            !SeedExitCondition::check_seed_time(Instant::now(), 100, true)
1074                && !SeedExitCondition::check_seed_condition(500, 1000, 2.0),
1075            "Neither condition met, should continue seeding"
1076        );
1077    }
1078
1079    // ==================================================================
1080    // New tests for enhanced BtSeedManager
1081    // ==================================================================
1082
1083    #[test]
1084    fn test_upload_session_creation() {
1085        let addr: SocketAddr = "192.168.1.1:6881".parse().unwrap();
1086        let session = UploadSession::new(addr);
1087        assert_eq!(session.peer_addr, addr);
1088        assert_eq!(session.uploaded_bytes, 0);
1089        assert_eq!(session.upload_speed, 0);
1090        assert!(session.is_active);
1091    }
1092
1093    #[test]
1094    fn test_upload_session_record_upload() {
1095        let addr: SocketAddr = "192.168.1.1:6881".parse().unwrap();
1096        let mut session = UploadSession::new(addr);
1097        session.record_upload(1024);
1098        assert_eq!(session.uploaded_bytes, 1024);
1099        session.record_upload(2048);
1100        assert_eq!(session.uploaded_bytes, 3072);
1101    }
1102
1103    #[test]
1104    fn test_upload_session_update_speed() {
1105        let addr: SocketAddr = "192.168.1.1:6881".parse().unwrap();
1106        let mut session = UploadSession::new(addr);
1107        session.update_speed(50000);
1108        assert_eq!(session.upload_speed, 50000);
1109    }
1110
1111    #[test]
1112    fn test_upload_session_deactivate() {
1113        let addr: SocketAddr = "192.168.1.1:6881".parse().unwrap();
1114        let mut session = UploadSession::new(addr);
1115        assert!(session.is_active);
1116        session.deactivate();
1117        assert!(!session.is_active);
1118    }
1119
1120    #[test]
1121    fn test_bt_seed_manager_get_upload_stats() {
1122        let manager = make_test_manager(SeedExitCondition::infinite(), 1000, 500);
1123        let (uploaded, speed) = manager.get_upload_stats();
1124        // Initial stats should be 0 (atomic starts at 0)
1125        assert_eq!(uploaded, 0);
1126        assert_eq!(speed, 0);
1127    }
1128
1129    #[test]
1130    fn test_bt_seed_manager_update_upload_speed() {
1131        let manager = make_test_manager(SeedExitCondition::infinite(), 1000, 500);
1132        manager.update_upload_speed(100000);
1133        let (_, speed) = manager.get_upload_stats();
1134        assert_eq!(speed, 100000);
1135    }
1136
1137    #[test]
1138    fn test_bt_seed_manager_should_stop_seeding_ratio() {
1139        let manager = make_test_manager(SeedExitCondition::with_ratio(1.0), 1000, 0);
1140
1141        // Should not stop initially (uploaded = 0)
1142        assert!(!manager.should_stop_seeding(1000));
1143
1144        // Simulate uploading 500 bytes (ratio = 0.5)
1145        manager.uploaded_bytes_atomic.store(500, Ordering::Relaxed);
1146        assert!(!manager.should_stop_seeding(1000));
1147
1148        // Simulate uploading 1000 bytes (ratio = 1.0)
1149        manager.uploaded_bytes_atomic.store(1000, Ordering::Relaxed);
1150        assert!(manager.should_stop_seeding(1000));
1151
1152        // Simulate uploading 1500 bytes (ratio = 1.5)
1153        manager.uploaded_bytes_atomic.store(1500, Ordering::Relaxed);
1154        assert!(manager.should_stop_seeding(1000));
1155    }
1156
1157    #[test]
1158    fn test_bt_seed_manager_should_stop_seeding_time() {
1159        let mut manager = make_test_manager(SeedExitCondition::with_time(1), 1000, 0);
1160
1161        // Should not stop immediately
1162        assert!(!manager.should_stop_seeding(1000));
1163
1164        // Simulate time passing by modifying seeding_start_time
1165        manager.seeding_start_time = Instant::now() - Duration::from_secs(2);
1166        assert!(manager.should_stop_seeding(1000));
1167    }
1168
1169    #[test]
1170    fn test_bt_seed_manager_should_stop_seeding_infinite() {
1171        let manager = make_test_manager(SeedExitCondition::infinite(), 1000, 0);
1172
1173        // Should never stop with infinite seeding
1174        assert!(!manager.should_stop_seeding(1000));
1175        assert!(!manager.should_stop_seeding(0));
1176    }
1177
1178    #[test]
1179    fn test_bt_seed_manager_info_hash() {
1180        let mut manager = make_test_manager(SeedExitCondition::infinite(), 1000, 0);
1181
1182        // Default info_hash should be all zeros
1183        assert_eq!(manager.info_hash(), &[0u8; 20]);
1184
1185        // Set a custom info_hash
1186        let custom_hash = [0x12u8; 20];
1187        manager.set_info_hash(custom_hash);
1188        assert_eq!(manager.info_hash(), &custom_hash);
1189    }
1190
1191    #[test]
1192    fn test_bt_seed_manager_new_with_info_hash() {
1193        let custom_hash = [0xABu8; 20];
1194        let provider = Arc::new(InMemoryPieceProvider::new(16384, 10));
1195        let config = BtSeedingConfig::default();
1196        let conns: Vec<aria2_protocol::bittorrent::peer::connection::PeerConnection> = vec![];
1197
1198        let manager = BtSeedManager::new_with_info_hash(
1199            custom_hash,
1200            conns,
1201            provider,
1202            config,
1203            SeedExitCondition::infinite(),
1204            1000,
1205        );
1206
1207        assert_eq!(manager.info_hash(), &custom_hash);
1208    }
1209
1210    #[test]
1211    fn test_bt_seed_manager_max_uploads() {
1212        let mut manager = make_test_manager(SeedExitCondition::infinite(), 1000, 0);
1213
1214        // Default max_uploads should match config
1215        assert_eq!(manager.max_uploads(), 4); // BtSeedingConfig::default().max_peers_to_unchoke
1216
1217        // Set a new max
1218        manager.set_max_uploads(8);
1219        assert_eq!(manager.max_uploads(), 8);
1220    }
1221
1222    #[test]
1223    fn test_bt_seed_manager_seed_ratio_and_time() {
1224        let manager = make_test_manager(SeedExitCondition::with_time_and_ratio(60, 2.0), 1000, 0);
1225
1226        assert_eq!(manager.seed_ratio(), 2.0);
1227        assert_eq!(manager.seed_time(), Duration::from_secs(60));
1228    }
1229
1230    #[test]
1231    fn test_bt_seed_manager_total_downloaded() {
1232        let manager = make_test_manager(SeedExitCondition::infinite(), 5000, 0);
1233        assert_eq!(manager.total_downloaded(), 5000);
1234    }
1235
1236    #[test]
1237    fn test_bt_seed_manager_num_active_uploads() {
1238        let manager = make_test_manager(SeedExitCondition::infinite(), 1000, 0);
1239
1240        // Initially no active uploads
1241        assert_eq!(manager.num_active_uploads(), 0);
1242    }
1243
1244    #[tokio::test]
1245    async fn test_bt_seed_manager_handle_piece_request() {
1246        let mut provider = InMemoryPieceProvider::new(16384, 10);
1247        // Set up some test data
1248        provider.set_piece_data(0, vec![0xABu8; 16384]);
1249
1250        let provider_arc = Arc::new(provider);
1251        let config = BtSeedingConfig::default();
1252        let conns: Vec<aria2_protocol::bittorrent::peer::connection::PeerConnection> = vec![];
1253
1254        let mut manager = BtSeedManager::new(
1255            conns,
1256            provider_arc,
1257            config,
1258            SeedExitCondition::infinite(),
1259            1000,
1260        );
1261
1262        let peer_addr: SocketAddr = "192.168.1.1:6881".parse().unwrap();
1263
1264        // Request a piece
1265        let result = manager.handle_piece_request(peer_addr, 0, 0, 1024).await;
1266        assert!(result.is_ok());
1267
1268        let data = result.unwrap();
1269        assert_eq!(data.len(), 1024);
1270        assert!(data.iter().all(|&b| b == 0xAB));
1271
1272        // Check that upload stats were updated
1273        let (uploaded, _) = manager.get_upload_stats();
1274        assert_eq!(uploaded, 1024);
1275
1276        // Check that active uploads were tracked
1277        assert_eq!(manager.num_active_uploads(), 1);
1278    }
1279
1280    #[tokio::test]
1281    async fn test_bt_seed_manager_handle_piece_request_invalid_piece() {
1282        let provider = Arc::new(InMemoryPieceProvider::new(16384, 10));
1283        let config = BtSeedingConfig::default();
1284        let conns: Vec<aria2_protocol::bittorrent::peer::connection::PeerConnection> = vec![];
1285
1286        let mut manager =
1287            BtSeedManager::new(conns, provider, config, SeedExitCondition::infinite(), 1000);
1288
1289        let peer_addr: SocketAddr = "192.168.1.1:6881".parse().unwrap();
1290
1291        // Request a piece that doesn't exist (piece 0 has no data)
1292        let result = manager.handle_piece_request(peer_addr, 0, 0, 1024).await;
1293        assert!(result.is_err());
1294    }
1295
1296    // ==================================================================
1297    // Upload speed throttling tests
1298    // ==================================================================
1299
1300    #[test]
1301    fn test_bt_seed_manager_max_upload_speed() {
1302        let mut manager = make_test_manager(SeedExitCondition::infinite(), 1000, 0);
1303
1304        // Initially no limit
1305        assert!(manager.max_upload_speed().is_none());
1306
1307        // Set a limit
1308        manager.set_max_upload_speed(Some(100000));
1309        assert_eq!(manager.max_upload_speed(), Some(100000));
1310
1311        // Set to 0 should be treated as unlimited
1312        manager.set_max_upload_speed(Some(0));
1313        assert!(manager.max_upload_speed().is_none());
1314
1315        // Set to None should be unlimited
1316        manager.set_max_upload_speed(None);
1317        assert!(manager.max_upload_speed().is_none());
1318    }
1319
1320    #[test]
1321    fn test_bt_seed_manager_current_upload_speed() {
1322        let manager = make_test_manager(SeedExitCondition::infinite(), 1000, 0);
1323
1324        // Initially 0
1325        assert_eq!(manager.current_upload_speed(), 0);
1326
1327        // Update speed
1328        manager.update_upload_speed(50000);
1329        assert_eq!(manager.current_upload_speed(), 50000);
1330    }
1331
1332    #[tokio::test]
1333    async fn test_bt_seed_manager_throttle_upload_no_limit() {
1334        let manager = make_test_manager(SeedExitCondition::infinite(), 1000, 0);
1335
1336        // No limit set, should return immediately
1337        let result = manager.throttle_upload(0, 10000).await;
1338        assert!(result.is_ok());
1339    }
1340
1341    #[tokio::test]
1342    async fn test_bt_seed_manager_throttle_upload_within_limit() {
1343        let manager = make_test_manager(SeedExitCondition::infinite(), 1000, 0);
1344
1345        // Within limit, should not throttle
1346        let result = manager.throttle_upload(100000, 1000).await;
1347        assert!(result.is_ok());
1348
1349        // Check that bytes were tracked
1350        assert_eq!(manager.throttle_window_bytes.load(Ordering::Relaxed), 1000);
1351    }
1352
1353    // ==================================================================
1354    // Seeding statistics tests
1355    // ==================================================================
1356
1357    #[test]
1358    fn test_seed_stats_calculation() {
1359        let mut manager = make_test_manager(SeedExitCondition::infinite(), 1000, 0);
1360
1361        // Simulate some uploads
1362        manager.uploaded_bytes_atomic.store(500, Ordering::Relaxed);
1363        manager.total_uploaded = 500;
1364
1365        let (uploaded, _) = manager.get_upload_stats();
1366        assert_eq!(uploaded, 500);
1367
1368        // Calculate ratio
1369        let ratio = uploaded as f64 / manager.total_downloaded as f64;
1370        assert!((ratio - 0.5).abs() < 0.001);
1371    }
1372
1373    #[test]
1374    fn test_seed_stats_with_elapsed_time() {
1375        let manager = make_test_manager(SeedExitCondition::infinite(), 1000, 0);
1376
1377        let duration = manager.seeding_duration();
1378        // Should be very small (just created)
1379        assert!(duration.as_secs() < 1);
1380    }
1381
1382    // ==================================================================
1383    // Edge cases and stress tests
1384    // ==================================================================
1385
1386    #[test]
1387    fn test_seed_exit_condition_zero_ratio_is_infinite() {
1388        // Ratio 0.0 should never trigger stop
1389        let cond = SeedExitCondition::with_ratio(0.0);
1390        assert!(cond.seed_ratio.is_none());
1391    }
1392
1393    #[test]
1394    fn test_seed_exit_condition_zero_time_is_infinite() {
1395        // Time 0 should never trigger stop
1396        let cond = SeedExitCondition::with_time(0);
1397        assert!(cond.seed_time.is_none());
1398    }
1399
1400    #[test]
1401    fn test_seed_exit_condition_negative_ratio() {
1402        // Negative ratio should be treated as infinite
1403        let cond = SeedExitCondition::with_ratio(-1.0);
1404        assert!(cond.seed_ratio.is_none());
1405    }
1406
1407    #[test]
1408    fn test_should_stop_seeding_with_both_conditions() {
1409        // Test with both time and ratio set
1410        let mut manager =
1411            make_test_manager(SeedExitCondition::with_time_and_ratio(60, 1.0), 1000, 0);
1412
1413        // Neither condition met yet
1414        assert!(!manager.should_stop_seeding(1000));
1415
1416        // Meet ratio condition
1417        manager.uploaded_bytes_atomic.store(1000, Ordering::Relaxed);
1418        assert!(manager.should_stop_seeding(1000));
1419
1420        // Reset and meet time condition
1421        manager.uploaded_bytes_atomic.store(0, Ordering::Relaxed);
1422        manager.seeding_start_time = Instant::now() - Duration::from_secs(120);
1423        assert!(manager.should_stop_seeding(1000));
1424    }
1425
1426    #[test]
1427    fn test_max_uploads_enforcement() {
1428        let mut manager = make_test_manager(SeedExitCondition::infinite(), 1000, 0);
1429
1430        // Default max uploads
1431        assert_eq!(manager.max_uploads(), 4);
1432
1433        // Increase max uploads
1434        manager.set_max_uploads(10);
1435        assert_eq!(manager.max_uploads(), 10);
1436
1437        // Decrease max uploads
1438        manager.set_max_uploads(2);
1439        assert_eq!(manager.max_uploads(), 2);
1440    }
1441
1442    #[test]
1443    fn test_active_uploads_tracking() {
1444        let manager = make_test_manager(SeedExitCondition::infinite(), 1000, 0);
1445
1446        // Initially no active uploads
1447        assert_eq!(manager.num_active_uploads(), 0);
1448        assert!(manager.active_uploads().is_empty());
1449    }
1450}