use std::{future::Future, time::Duration};

use thiserror::Error;

/// Error returned when a future exceeds the configured timeout.
#[derive(Debug, Error, PartialEq, Eq)]
#[error("operation timed out after {duration:?}")]
pub struct TimeoutError {
    /// Configured timeout duration.
    pub duration: Duration,
}

/// Runs a future with a timeout.
pub async fn run_with_timeout<F, T>(duration: Duration, future: F) -> Result<T, TimeoutError>
where
    F: Future<Output = T>,
{
    tokio::time::timeout(duration, future)
        .await
        .map_err(|_| TimeoutError { duration })
}

#[cfg(test)]
mod tests {
    use super::run_with_timeout;
    use std::time::Duration;

    #[tokio::test]
    async fn returns_value_before_timeout() {
        let value = run_with_timeout(Duration::from_millis(50), async { 7 })
            .await
            .expect("value");

        assert_eq!(value, 7);
    }

    #[tokio::test]
    async fn returns_timeout_error() {
        let result = run_with_timeout(Duration::from_millis(5), async {
            tokio::time::sleep(Duration::from_millis(50)).await;
        })
        .await;

        assert!(result.is_err());
    }
}
