//! A `TokenProvider` that renews its token using a backing `TokenProvider` when the current
//! token is expired or about to expire.

use std::sync::Arc;

use chrono::{Duration, Utc};
use tokio::sync::Mutex;

use super::{Token, TokenProvider};

/// A `TokenProvider` that renews its token whenever the current token expires.
#[derive(Clone)]
pub struct RenewingTokenProvider<T> {
    provider: T,

    // This is a mutex instead of anything fancier like an ArcSwap because no other authenticated
    // requests can reasonably continue (without becoming a thundering herd) while the thread that
    // discovered the expired token is fetching a replacement.
    // This is an async mutex instead of a regular mutex because fetching the new token is async.
    token: Arc<Mutex<Arc<Token>>>,
}

impl<T: TokenProvider> RenewingTokenProvider<T> {
    /// Returns new `RenewingTokenProvider` that fetches a new token using the provided `provider`
    /// when the current one expires.
    pub fn new(provider: T) -> Self {
        Self {
            provider,
            token: Default::default(),
        }
    }
}

#[async_trait::async_trait]
impl<T: TokenProvider> TokenProvider for RenewingTokenProvider<T> {
    async fn get_token(&self) -> anyhow::Result<Arc<Token>> {
        let mut token = self.token.lock().await;
        if token.expiry <= Utc::now() - Duration::minutes(5) {
            let new_token = self.provider.get_token().await?;
            *token = new_token;
            Ok(Arc::clone(&token))
        } else {
            Ok(token.clone())
        }
    }
}

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

    use crate::token_provider::MockTokenProvider;

    #[tokio::test]
    async fn renewing_provider_renews_expired() {
        let new_token = Arc::new(Token {
            token: "dummy token".into(),
            expiry: Utc::now(),
        });
        let new_token_return = new_token.clone();
        let mut mock_provider = MockTokenProvider::new();
        mock_provider
            .expect_get_token()
            .times(1)
            .returning(move || Box::pin(futures::future::ok(new_token_return.clone())));
        let renewing = RenewingTokenProvider::new(mock_provider);
        assert_eq!(renewing.get_token().await.unwrap(), new_token);
        assert_eq!(renewing.get_token().await.unwrap(), new_token);
    }
}
