//! JWT Key Utility Functions
//!
//! This module provides utility functions for JWT key management, including:
//!
//! - Cache expiry calculations
//! - Backoff period enforcement
//! - Error handling and reporting
//!
//! These utilities support the core JWT key operations with helper functions
//! that implement common patterns and checks used throughout the JWT key system.
//!
//! See the [module-level documentation](super) for a more detailed overview and usage.

use std::time::Instant;

use crate::oauth2::jwk::cache::JwtKeyCache;

/// Checks if refresh is still in cooldown due to recent failure.
///
/// This method determines whether enough time has passed since the last
/// JWT key refresh failure to attempt another refresh. It implements a
/// simple backoff mechanism to prevent excessive API calls when the
/// authentication service is experiencing issues.
///
/// # Thread Safety
/// This method acquires a read lock on the failure timestamp, allowing
/// multiple threads to check the backoff status concurrently.
/// - Reads from the shared [`Client::jwt_keys_last_refresh_failure`](crate::Client::jwt_keys_last_refresh_failure)
///   timestamp
///
/// # Arguments
/// - `jwt_key_cache` (&[`JwtKeyCache`]): cache which contains the config for JWT key caching &
///   refreshing policies.
///
/// # Returns
/// - Some([`u64`]): Indicating the JWT key refresh cooldown remaining
/// - None: If there is no remaining JWT key refresh cooldown.
pub(super) async fn check_refresh_cooldown(jwt_key_cache: &JwtKeyCache) -> Option<u64> {
    let config = &jwt_key_cache.config;

    // Check for last background refresh failure
    let last_refresh_failure = &jwt_key_cache.last_refresh_failure;
    if let Some(last_failure) = *last_refresh_failure.read().await {
        // Check if last refresh failure is within backoff period
        let elapsed_secs = last_failure.elapsed().as_secs();
        let is_cooldown = elapsed_secs < config.refresh_cooldown.as_secs();

        if is_cooldown {
            debug!(
                "Respecting background refresh cooldown: {}s elapsed of {}s required",
                elapsed_secs,
                config.refresh_cooldown.as_secs()
            );

            // Return Some with the remaining cooldown in seconds
            let remaining_cooldown = config.refresh_cooldown.as_secs() - elapsed_secs;

            return Some(remaining_cooldown);
        } else {
            trace!(
                "Background cooldown period elapsed: {}s passed (required {}s)",
                elapsed_secs,
                config.refresh_cooldown.as_secs()
            );

            // Return None indicating there is no active cooldown
            return None;
        }
    }

    // No previous JWT key refresh failure
    trace!("No previous JWT key refresh failures recorded, no backoff needed");

    // Return None indicating there is no active cooldown
    return None;
}

/// Determines if the cache is approaching expiry based on elapsed time
///
/// Checks whether the elapsed time since the last cache update has crossed
/// the nearing expiration threshold percentage of the cache lifetime, indicating that a proactive
/// refresh should be triggered.
///
/// # Parameters
/// - `jwt_key_cache` (&[`JwtKeyCache`]): cache which contains the config for JWT key caching &
///   refreshing policies.
/// - `timestamp` ([`Instant`]): Timestamp of when the keys stored in cache were last updated.
///
/// # Returns
/// - `true` if the elapsed time exceeds the threshold percentage of the TTL
/// - `false` if the cache is still well within its valid period
pub(super) fn is_cache_approaching_expiry(jwt_key_cache: &JwtKeyCache, timestamp: Instant) -> bool {
    let config = &jwt_key_cache.config;

    // Calculate elasped milliseconds
    let elapsed_millis = timestamp.elapsed().as_millis();

    // Determine how many seconds need to pass for the keys to be considered nearing expiration
    // By default, 80% of 3600 second TTL must have elapsed, 2880 seconds.
    let threshold_percentage = config.background_refresh_threshold as f64 / 100.0;
    let threshold_millis = (config.cache_ttl.as_millis() as f64 * threshold_percentage) as u128;

    // By default, if more than 2880 seconds have elapsed then the keys are nearing expiration.
    let is_approaching_expiry = elapsed_millis > threshold_millis;

    // Return result
    let elapsed_seconds = timestamp.elapsed().as_secs();
    let threshold_seconds = (config.cache_ttl.as_secs() as f64 * threshold_percentage) as u64;

    if is_approaching_expiry {
        debug!(
            "JWT keys cache approaching expiry: elapsed={}s, threshold={}s ({}% of ttl={}s)",
            elapsed_seconds,
            threshold_seconds,
            config.background_refresh_threshold,
            config.cache_ttl.as_secs()
        );

        // Return true if cache is approaching expiry
        true
    } else {
        trace!(            "JWT keys cache not yet approaching expiry: elapsed={}s, threshold={}s ({}% of ttl={}s)",
        elapsed_seconds,
        threshold_seconds,
        config.background_refresh_threshold,
        config.cache_ttl.as_secs());

        // Return false if cache is not yet approaching expiry
        false
    }
}

/// Determines if the cache has completely expired based on elapsed time
///
/// Checks if the elapsed time since the last cache update has reached or
/// exceeded the configured JWT key cache lifetime which indicates expiration.
///
/// # Parameters
/// - `jwt_key_cache` (&[`JwtKeyCache`]): cache which contains the config for JWT key caching &
///   refreshing policies.
/// - `timestamp` ([`Instant`]): Timestamp of when the keys stored in cache were last updated.
///
/// # Returns
/// - `true` if the elapsed time has reached or exceeded the TTL
/// - `false` if the cache is still within its valid period
pub(super) fn is_cache_expired(jwt_key_cache: &JwtKeyCache, timestamp: Instant) -> bool {
    let cache_ttl = jwt_key_cache.config.cache_ttl;

    let is_expired = timestamp.elapsed().as_millis() >= cache_ttl.as_millis();

    if is_expired {
        debug!(
            "JWT keys cache expired: elapsed={}s, ttl={}s",
            timestamp.elapsed().as_secs(),
            cache_ttl.as_secs()
        );

        // Return true if cache is not yet expired
        true
    } else {
        trace!(
            "JWT keys cache valid: elapsed={}s, ttl={}s",
            timestamp.elapsed().as_secs(),
            cache_ttl.as_secs()
        );

        // Return false if cache is still valid
        false
    }
}

#[cfg(test)]
mod is_refresh_cooldown_tests {
    use crate::Client;

    use super::check_refresh_cooldown;

    /// Refresh cooldown should be active due to recent failure
    ///
    /// When there is a refresh failure within the default of the past (60 seconds),
    /// assert that the function returns true, indicating that we should not attempt
    /// a refresh due to cooldown period.
    ///
    /// # Test Setup
    /// - Create a basic Client
    /// - Set last refresh failure within default cooldown period of past 60 seconds
    ///
    /// # Assertions
    /// - Assert function returns Some indicating we are still in cooldown
    /// - Assert 30 seconds remain in cooldown period
    #[tokio::test]
    async fn test_check_refresh_cooldown_within_cooldown() {
        // Setup Client
        let esi_client = Client::builder()
            .user_agent("MyApp/1.0 (contact@email.com")
            .build()
            .expect("Failed to build Client");

        let jwt_key_cache = &esi_client.inner.jwt_key_cache;

        // Set the recent failure within cooldown period default of 60 seconds
        {
            let mut failure_time = jwt_key_cache.last_refresh_failure.write().await;
            *failure_time = Some(std::time::Instant::now() - std::time::Duration::from_secs(30));
        }

        // Run function
        let cooldown = check_refresh_cooldown(&jwt_key_cache).await;

        // Assert cooldown is some
        assert!(cooldown.is_some());
        let remaining_cooldown = cooldown.unwrap();

        // Assert cooldown returns expected 30 seconds remaining
        assert_eq!(remaining_cooldown, 30);
    }

    /// Refresh cooldown should be false due to not being in cooldown
    ///
    /// When the back off period is greater than the default of 60 seconds,
    /// assert that the function returns false, indicating that we can attempt a refresh.
    ///
    /// # Test Setup
    /// - Create a basic Client
    /// - Set last refresh failure beyond default cooldown period of past 60 seconds
    ///
    /// # Assertions
    /// - Assert cooldown is None indicating we are not in the cooldown period
    #[tokio::test]
    async fn test_check_refresh_cooldown_recent_failure() {
        // Setup Client
        let esi_client = Client::builder()
            .user_agent("MyApp/1.0 (contact@email.com")
            .build()
            .expect("Failed to build Client");

        let jwt_key_cache = &esi_client.inner.jwt_key_cache;

        // Set the last refresh failure greater than default of cooldown period of 60 seconds
        {
            let mut failure_time = jwt_key_cache.last_refresh_failure.write().await;
            *failure_time = Some(std::time::Instant::now() - std::time::Duration::from_secs(61));
        }

        // Run function
        let cooldown = check_refresh_cooldown(&jwt_key_cache).await;

        // Assert cooldown is None
        assert!(cooldown.is_none());
    }

    /// Refresh cooldown should be false due to no past failures recorded
    ///
    /// When there is no previous failure recorded, the function should return false,
    /// indicating that we can attempt a refresh.
    ///
    /// # Test Setup
    /// - Create a basic Client
    /// - Do not set the [`Client::jwt_key_last_refresh_failure`]
    ///
    /// # Assertions
    /// - Assert cooldown is None indicating we are not in the cooldown period
    #[tokio::test]
    async fn test_check_refresh_cooldown_no_failure() {
        // Setup Client
        // Don't set back off period
        let esi_client = Client::builder()
            .user_agent("MyApp/1.0 (contact@email.com")
            .build()
            .expect("Failed to build Client");

        let jwt_key_cache = &esi_client.inner.jwt_key_cache;

        // Run function
        let cooldown = check_refresh_cooldown(&jwt_key_cache).await;

        // Assert cooldown is None
        assert!(cooldown.is_none());
    }
}

#[cfg(test)]
mod is_cache_approaching_expiry_tests {

    use crate::Client;

    use super::is_cache_approaching_expiry;

    /// Validates function returns true if cache is past 80% expiration
    ///
    /// When the JWT key cache expiration is past 80% expired (2880 seconds of 3600 default expiration),
    /// the function should return true indicating that the cache is almost expired.
    ///
    /// # Test Setup
    /// - Create a basic Client
    /// - Set the JWT key cache to beyond 80% expired
    ///
    /// # Validations
    /// - Verifies the function returns true, cache is almost expired.
    #[test]
    fn test_is_cache_approaching_expiry_true() {
        // Setup Client
        let esi_client = Client::builder()
            .user_agent("MyApp/1.0 (contact@email.com")
            .build()
            .expect("Failed to build Client");

        // Set the expiration timestamp to psat default expiry of 2880 seconds
        // Default approaching expiry is 2880 seconds (80% of 3600 seconds default)
        let timestamp = std::time::Instant::now() - std::time::Duration::from_secs(2881);

        // Test function
        let result = is_cache_approaching_expiry(&esi_client.inner.jwt_key_cache, timestamp);

        // Assert true
        assert_eq!(result, true)
    }

    /// Validates function returns false if cache is not approaching expiration
    ///
    /// When the JWT key cache expiration is not yet at 80%, the function
    /// should return false indicating we are not yet nearing expiration.
    ///
    /// # Test Setup
    /// - Create a basic Client
    /// - Set the client JWT key cache to less than 80% expired
    ///
    /// # Validations
    /// - Verifies the function returns false, cache is not yet nearing expiration.
    #[test]
    fn test_is_cache_approaching_expiry_false() {
        // Setup Client
        let esi_client = Client::builder()
            .user_agent("MyApp/1.0 (contact@email.com")
            .build()
            .expect("Failed to build Client");

        // Set the expiration timestamp to represent fresh keys
        let timestamp = std::time::Instant::now();

        // Test function
        let result = is_cache_approaching_expiry(&esi_client.inner.jwt_key_cache, timestamp);

        // Assert false
        assert_eq!(result, false)
    }
}

#[cfg(test)]
mod is_cache_expired_tests {
    use crate::Client;

    use super::is_cache_expired;

    /// Validates function returns true if cache is expired
    ///
    /// When the JWT key cache has been set more than 3600 seconds ago
    /// by default, the cache should be considered fully expired.
    ///
    /// # Setup
    /// - Create a basic Client
    /// - Set the client JWT key cache to past 3600 seconds expiration
    ///
    /// # Assertions
    /// - Verifies the function returns true, the cache is fully expired
    #[test]
    fn test_is_cache_expired_true() {
        let esi_client = Client::builder()
            .user_agent("MyApp/1.0 (contact@example.com)")
            .build()
            .expect("Failed to build Client");

        // Set expiration timestamp to past default expiration of 3600 seconds
        let timestamp = std::time::Instant::now() - std::time::Duration::from_secs(3601);

        // Test function
        let result = is_cache_expired(&esi_client.inner.jwt_key_cache, timestamp);

        // Assert true
        assert_eq!(result, true)
    }

    /// Validates function returns false if cache is not expired
    ///
    /// When the JWT key cache has been set less than 3600 seconds ago
    /// by default, the cache should be not yet expired.
    ///
    /// # Setup
    /// - Create a basic Client
    /// - Set the client JWT key cache to fresh keys
    ///
    /// # Assertions
    /// - Verifies the function returns true, the cache is fully expired
    #[test]
    fn test_is_cache_expired_false() {
        // Setup basic Client
        let esi_client = Client::builder()
            .user_agent("MyApp/1.0 (contact@example.com)")
            .build()
            .expect("Failed to build Client");

        // Set expiration timestamp to represent fresh keys
        let timestamp = std::time::Instant::now();

        // Test function
        let result = is_cache_expired(&esi_client.inner.jwt_key_cache, timestamp);

        // Assert true
        assert_eq!(result, false)
    }
}
