//! Bounded exponential backoff helper used by the transport reconnect loop.
//!
//! Caller pattern:
//! ```text
//! let mut backoff = ExponentialBackoff::new(base, cap);
//! loop {
//!     match try_open().await {
//!         Ok(t) => break t,
//!         Err(e) => {
//!             tracing::warn!(error = %e, "open failed");
//!             tokio::time::sleep(backoff.next_delay()).await;
//!         }
//!     }
//! }
//! ```

use std::time::Duration;

#[derive(Debug, Clone)]
pub struct ExponentialBackoff {
    base: Duration,
    cap: Duration,
    attempt: u32,
}

impl ExponentialBackoff {
    pub fn new(base: Duration, cap: Duration) -> Self {
        assert!(base > Duration::ZERO, "backoff base must be positive");
        assert!(cap >= base, "backoff cap must be >= base");
        Self {
            base,
            cap,
            attempt: 0,
        }
    }

    /// The next delay to sleep for. Doubles each call, capped at `cap`.
    pub fn next_delay(&mut self) -> Duration {
        let exp = self.attempt.min(31);
        let delay = self
            .base
            .checked_mul(1u32 << exp)
            .unwrap_or(self.cap)
            .min(self.cap);
        self.attempt = self.attempt.saturating_add(1);
        delay
    }

    pub fn reset(&mut self) {
        self.attempt = 0;
    }

    pub fn attempts(&self) -> u32 {
        self.attempt
    }
}

#[cfg(test)]
mod tests {
    use super::*;

    #[test]
    fn doubles_until_cap() {
        // Arrange
        let mut b = ExponentialBackoff::new(Duration::from_millis(100), Duration::from_secs(2));

        // Act / Assert
        assert_eq!(b.next_delay(), Duration::from_millis(100));
        assert_eq!(b.next_delay(), Duration::from_millis(200));
        assert_eq!(b.next_delay(), Duration::from_millis(400));
        assert_eq!(b.next_delay(), Duration::from_millis(800));
        assert_eq!(b.next_delay(), Duration::from_millis(1600));
        assert_eq!(b.next_delay(), Duration::from_secs(2)); // capped
        assert_eq!(b.next_delay(), Duration::from_secs(2)); // still capped
    }

    #[test]
    fn reset_returns_to_base() {
        // Arrange
        let mut b = ExponentialBackoff::new(Duration::from_millis(50), Duration::from_secs(1));
        let _ = b.next_delay();
        let _ = b.next_delay();

        // Act
        b.reset();

        // Assert
        assert_eq!(b.next_delay(), Duration::from_millis(50));
    }
}
