//! JWT token management with automatic refresh
//!
//! Manages access tokens and refresh tokens via OS keyring for secure persistence.
//! Since FXT is a CLI tool (not a long-running service), tokens are read from
//! keyring on each access rather than cached in memory.

use anyhow::{Context, Result};
use jsonwebtoken::{decode, Algorithm, DecodingKey, Validation};
use serde::{Deserialize, Serialize};
use std::sync::Arc;
use std::time::{Duration, SystemTime, UNIX_EPOCH};
use tokio::sync::RwLock;

/// Recursively copy a directory tree (used for token storage migration fallback).
fn copy_dir_recursive(src: &std::path::Path, dst: &std::path::Path) -> Result<()> {
    std::fs::create_dir_all(dst)
        .with_context(|| format!("Failed to create directory {}", dst.display()))?;
    for dir_entry in std::fs::read_dir(src)
        .with_context(|| format!("Failed to read directory {}", src.display()))?
    {
        let entry = dir_entry.context("Failed to read directory entry")?;
        let src_path = entry.path();
        let dst_path = dst.join(entry.file_name());
        if src_path.is_dir() {
            copy_dir_recursive(&src_path, &dst_path)?;
        } else {
            std::fs::copy(&src_path, &dst_path).with_context(|| {
                format!(
                    "Failed to copy {} to {}",
                    src_path.display(),
                    dst_path.display()
                )
            })?;
        }
    }
    Ok(())
}

/// Buffer (in seconds) before actual JWT expiry at which we consider the token expired.
/// This ensures we refresh proactively rather than racing against clock skew.
const TOKEN_REFRESH_BUFFER_SECS: u64 = 60;

/// JWT token information
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct TokenInfo {
    /// Access token (JWT)
    pub access_token: String,
    /// Refresh token for obtaining new access tokens
    pub refresh_token: String,
    /// Token expiration time (Unix timestamp in seconds)
    pub expires_at: u64,
}

impl TokenInfo {
    /// Check if the access token is expired or nearing expiration
    ///
    /// Returns true if the token expires within [`TOKEN_REFRESH_BUFFER_SECS`] seconds
    pub fn is_expired(&self) -> bool {
        let now = SystemTime::now()
            .duration_since(UNIX_EPOCH)
            .unwrap_or(Duration::from_secs(0))
            .as_secs();

        self.expires_at <= now + TOKEN_REFRESH_BUFFER_SECS
    }

    /// Get remaining time until token expiration
    pub fn time_until_expiry(&self) -> Duration {
        let now = SystemTime::now()
            .duration_since(UNIX_EPOCH)
            .unwrap_or(Duration::from_secs(0))
            .as_secs();

        if self.expires_at > now {
            Duration::from_secs(self.expires_at - now)
        } else {
            Duration::from_secs(0)
        }
    }
}

/// JWT claims structure for token parsing
#[derive(Debug, Deserialize)]
struct JwtClaims {
    exp: u64,
}

/// Extract expiration timestamp from JWT token without signature verification
fn extract_token_expiry(token: &str) -> Result<u64> {
    let mut validation = Validation::new(Algorithm::HS256);
    validation.insecure_disable_signature_validation();
    validation.validate_exp = false;
    validation.validate_aud = false; // Disable audience validation for flexibility

    // Key value is irrelevant — signature validation is disabled above
    let token_data = decode::<JwtClaims>(token, &DecodingKey::from_secret(b"dummy"), &validation)
        .context("Failed to decode JWT token")?;

    Ok(token_data.claims.exp)
}

/// Token storage interface for secure token persistence
#[async_trait::async_trait]
pub trait TokenStorage: Send + Sync {
    /// Store access token securely
    async fn store_access_token(&self, token: &str) -> Result<()>;

    /// Retrieve stored access token
    async fn get_access_token(&self) -> Result<Option<String>>;

    /// Store refresh token securely
    async fn store_refresh_token(&self, token: &str) -> Result<()>;

    /// Retrieve stored refresh token
    async fn get_refresh_token(&self) -> Result<Option<String>>;

    /// Remove stored refresh token
    async fn remove_refresh_token(&self) -> Result<()>;

    /// Remove stored access token
    async fn clear_access_token(&self) -> Result<()>;
}

/// OS keyring-based token storage (production)
#[derive(Clone)]
pub struct KeyringTokenStorage {
    service_name: String,
    username: String,
}

impl KeyringTokenStorage {
    /// Create a new keyring token storage
    ///
    /// # Arguments
    /// * `service_name` - Application service name (e.g., "foxhunt-fxt")
    ///
    /// * `username` - Username for keyring entry
    pub const fn new(service_name: String, username: String) -> Self {
        Self {
            service_name,
            username,
        }
    }

    /// Clear all tokens from OS keyring (access + refresh)
    ///
    /// Clears both access token and refresh token from the keyring.
    /// Silently succeeds if tokens were not present.
    pub async fn clear_tokens(&self) -> Result<()> {
        // Clear access token
        self.clear_access_token().await?;

        // Clear refresh token
        let service_name = self.service_name.clone();
        let username = self.username.clone();

        tokio::task::spawn_blocking(move || {
            let entry = keyring::Entry::new(&service_name, &username)
                .context("Failed to create keyring entry")?;

            match entry.delete_credential() {
                Ok(()) => {
                    tracing::info!("Refresh token removed from OS keyring");
                    Ok(())
                },
                Err(keyring::Error::NoEntry) => Ok(()), // Already deleted
                Err(e) => Err(anyhow::anyhow!("Failed to remove refresh token: {}", e)),
            }
        })
        .await
        .context("Keyring task panicked")??;

        tracing::info!("All tokens cleared from keyring");
        Ok(())
    }
}

#[async_trait::async_trait]
impl TokenStorage for KeyringTokenStorage {
    async fn store_access_token(&self, token: &str) -> Result<()> {
        let owned_token = token.to_owned();

        tokio::task::spawn_blocking(move || {
            let entry = keyring::Entry::new("foxhunt-fxt-access", "default")
                .context("Failed to create keyring entry for access token")?;

            entry
                .set_password(&owned_token)
                .context("Failed to store access token in keyring")?;

            tracing::debug!("Access token stored securely in OS keyring");
            Ok(())
        })
        .await
        .context("Keyring task panicked")?
    }

    async fn get_access_token(&self) -> Result<Option<String>> {
        tokio::task::spawn_blocking(move || {
            let entry = keyring::Entry::new("foxhunt-fxt-access", "default")
                .context("Failed to create keyring entry for access token")?;

            match entry.get_password() {
                Ok(token) => Ok(Some(token)),
                Err(keyring::Error::NoEntry) => Ok(None),
                Err(e) => Err(anyhow::anyhow!("Failed to retrieve access token: {}", e)),
            }
        })
        .await
        .context("Keyring task panicked")?
    }

    async fn store_refresh_token(&self, token: &str) -> Result<()> {
        let service_name = self.service_name.clone();
        let username = self.username.clone();
        let owned_token = token.to_owned();

        tokio::task::spawn_blocking(move || {
            let entry = keyring::Entry::new(&service_name, &username)
                .context("Failed to create keyring entry")?;

            entry
                .set_password(&owned_token)
                .context("Failed to store refresh token in OS keyring")?;

            tracing::info!("Refresh token stored securely in OS keyring");
            Ok(())
        })
        .await
        .context("Keyring task panicked")?
    }

    async fn get_refresh_token(&self) -> Result<Option<String>> {
        let service_name = self.service_name.clone();
        let username = self.username.clone();

        tokio::task::spawn_blocking(move || {
            let entry = keyring::Entry::new(&service_name, &username)
                .context("Failed to create keyring entry")?;

            match entry.get_password() {
                Ok(token) => Ok(Some(token)),
                Err(keyring::Error::NoEntry) => Ok(None),
                Err(e) => Err(anyhow::anyhow!("Failed to retrieve refresh token: {}", e)),
            }
        })
        .await
        .context("Keyring task panicked")?
    }

    async fn remove_refresh_token(&self) -> Result<()> {
        let service_name = self.service_name.clone();
        let username = self.username.clone();

        tokio::task::spawn_blocking(move || {
            let entry = keyring::Entry::new(&service_name, &username)
                .context("Failed to create keyring entry")?;

            match entry.delete_credential() {
                Ok(()) => {
                    tracing::info!("Refresh token removed from OS keyring");
                    Ok(())
                },
                Err(keyring::Error::NoEntry) => Ok(()), // Already deleted
                Err(e) => Err(anyhow::anyhow!("Failed to remove refresh token: {}", e)),
            }
        })
        .await
        .context("Keyring task panicked")?
    }

    async fn clear_access_token(&self) -> Result<()> {
        tokio::task::spawn_blocking(move || {
            let entry = keyring::Entry::new("foxhunt-fxt-access", "default")
                .context("Failed to create keyring entry for access token")?;

            match entry.delete_credential() {
                Ok(()) => {
                    tracing::debug!("Access token removed from OS keyring");
                    Ok(())
                },
                Err(keyring::Error::NoEntry) => Ok(()), // Already deleted
                Err(e) => Err(anyhow::anyhow!("Failed to clear access token: {}", e)),
            }
        })
        .await
        .context("Keyring task panicked")?
    }
}

/// File-based token storage (reliable alternative to buggy keyring on Linux)
///
/// Stores tokens in encrypted files with proper permissions (600 on Unix).
/// This is a workaround for the keyring bug where credentials stored via one Entry
/// object cannot be retrieved by a different Entry object.
///
/// Security notes:
/// - Files stored in `~/.config/foxhunt-fxt/tokens/`
/// - Directory permissions: 700 (owner read/write/execute only)
/// - File permissions: 600 (owner read/write only)
/// - AES-256-GCM encryption provides production-grade security
/// - This provides equivalent security to OS keyring with better reliability
pub struct FileTokenStorage {
    token_dir: std::path::PathBuf,
    key_manager: std::sync::Mutex<crate::auth::key_manager::KeyManager>,
}

impl FileTokenStorage {
    /// Create a new file-based token storage
    ///
    /// Creates token directory with 700 permissions if it doesn't exist.
    pub fn new() -> Result<Self> {
        let config_base = dirs::config_dir().context("Cannot determine config directory")?;
        let new_dir = config_base.join("foxhunt-fxt");
        let old_dir = config_base.join("foxhunt-tli");

        // Migrate from legacy foxhunt-tli directory if it exists and foxhunt-fxt does not
        if !new_dir.exists() && old_dir.exists() {
            tracing::info!(
                "Migrating token storage from {} to {}",
                old_dir.display(),
                new_dir.display()
            );
            if let Err(e) = std::fs::rename(&old_dir, &new_dir) {
                // rename() fails across mount points; fall back to copy + remove
                tracing::warn!("rename() failed ({}), falling back to copy", e);
                copy_dir_recursive(&old_dir, &new_dir)?;
                // Best-effort cleanup of old dir; failure is non-fatal
                drop(std::fs::remove_dir_all(&old_dir));
            }
        }

        let token_dir = new_dir.join("tokens");

        // Create directory with 700 permissions (owner only)
        std::fs::create_dir_all(&token_dir).context("Failed to create token directory")?;

        #[cfg(unix)]
        {
            use std::os::unix::fs::PermissionsExt;
            let perms = std::fs::Permissions::from_mode(0o700);
            std::fs::set_permissions(&token_dir, perms)
                .context("Failed to set token directory permissions")?;
        }

        Ok(Self {
            token_dir,
            key_manager: std::sync::Mutex::new(crate::auth::key_manager::KeyManager::new()),
        })
    }

    /// Create a new file-based token storage with a custom directory
    ///
    /// Creates token directory with 700 permissions if it doesn't exist.
    /// Primarily intended for testing but can be used to specify custom token directories.
    pub fn with_directory(token_dir: std::path::PathBuf) -> Result<Self> {
        // Create directory with 700 permissions (owner only)
        std::fs::create_dir_all(&token_dir).context("Failed to create token directory")?;

        #[cfg(unix)]
        {
            use std::os::unix::fs::PermissionsExt;
            let perms = std::fs::Permissions::from_mode(0o700);
            std::fs::set_permissions(&token_dir, perms)
                .context("Failed to set token directory permissions")?;
        }

        Ok(Self {
            token_dir,
            key_manager: std::sync::Mutex::new(crate::auth::key_manager::KeyManager::new()),
        })
    }

    /// Get path to access token file
    fn access_token_path(&self) -> std::path::PathBuf {
        self.token_dir.join("access_token")
    }

    /// Get path to refresh token file
    fn refresh_token_path(&self) -> std::path::PathBuf {
        self.token_dir.join("refresh_token")
    }

    /// Write token to file with 600 permissions
    ///
    /// Token is encrypted using AES-256-GCM.
    fn write_token(&self, path: &std::path::Path, token: &str) -> Result<()> {
        // Derive encryption key
        let mut key_manager = self
            .key_manager
            .lock()
            .map_err(|e| anyhow::anyhow!("KeyManager lock poisoned: {}", e))?;
        let key = key_manager
            .derive_key()
            .context("Failed to derive encryption key")?;

        // Encrypt token using AES-256-GCM (Wave 155)
        let encrypted = crate::auth::encryption::write_token_encrypted(token, &key)
            .context("Failed to encrypt token")?;

        // Write to file
        std::fs::write(path, encrypted)
            .with_context(|| format!("Failed to write token to {}", path.display()))?;

        // Set permissions to 600 (owner read/write only) on Unix
        #[cfg(unix)]
        {
            use std::os::unix::fs::PermissionsExt;
            let perms = std::fs::Permissions::from_mode(0o600);
            std::fs::set_permissions(path, perms)
                .with_context(|| format!("Failed to set permissions on {}", path.display()))?;
        }

        Ok(())
    }

    /// Read token from file
    ///
    /// Returns None if file doesn't exist, otherwise decrypts token.
    /// Backward compatible with Wave 154 hex-encoded tokens.
    fn read_token(&self, path: &std::path::Path) -> Result<Option<String>> {
        // Check if file exists
        if !path.exists() {
            return Ok(None);
        }

        // Read encrypted data (could be hex or "ENC:" format)
        let file_data = std::fs::read_to_string(path)
            .with_context(|| format!("Failed to read token from {}", path.display()))?;

        // Derive decryption key
        let mut key_manager = self
            .key_manager
            .lock()
            .map_err(|e| anyhow::anyhow!("KeyManager lock poisoned: {}", e))?;
        let key = key_manager
            .derive_key()
            .context("Failed to derive encryption key")?;

        // Auto-detect format and decrypt (backward compatible with Wave 154 hex)
        let token = crate::auth::encryption::read_token_auto(file_data.trim(), &key)
            .context("Failed to decrypt token")?;

        Ok(Some(token))
    }

    /// Delete token file (idempotent)
    fn delete_token(&self, path: &std::path::Path) -> Result<()> {
        if path.exists() {
            std::fs::remove_file(path)
                .with_context(|| format!("Failed to delete token file {}", path.display()))?;
        }
        Ok(())
    }
}

impl Default for FileTokenStorage {
    fn default() -> Self {
        // Try the normal path first; fall back to /tmp/foxhunt-fxt/tokens/ if it fails.
        // This avoids a panic when the user's config directory is unavailable.
        Self::new().unwrap_or_else(|e| {
            tracing::warn!(
                "FileTokenStorage::new() failed ({}), falling back to /tmp/foxhunt-fxt/tokens/",
                e
            );
            let fallback = std::path::PathBuf::from("/tmp/foxhunt-fxt/tokens");
            Self::with_directory(fallback).unwrap_or_else(|e2| {
                // Last resort: construct without creating dirs -- will fail at read/write
                // time rather than panicking here.
                tracing::error!("Fallback token storage also failed: {}", e2);
                Self {
                    token_dir: std::path::PathBuf::from("/tmp/foxhunt-fxt/tokens"),
                    key_manager: std::sync::Mutex::new(crate::auth::key_manager::KeyManager::new()),
                }
            })
        })
    }
}

impl Clone for FileTokenStorage {
    fn clone(&self) -> Self {
        Self {
            token_dir: self.token_dir.clone(),
            key_manager: std::sync::Mutex::new(crate::auth::key_manager::KeyManager::new()),
        }
    }
}

#[async_trait::async_trait]
impl TokenStorage for FileTokenStorage {
    async fn store_access_token(&self, token: &str) -> Result<()> {
        let path = self.access_token_path();
        let owned_token = token.to_owned();
        let storage = self.clone();

        tokio::task::spawn_blocking(move || {
            storage.write_token(&path, &owned_token)?;
            tracing::debug!("Access token stored in file: {}", path.display());
            Ok(())
        })
        .await
        .context("File task panicked")?
    }

    async fn get_access_token(&self) -> Result<Option<String>> {
        let path = self.access_token_path();
        let storage = self.clone();

        tokio::task::spawn_blocking(move || storage.read_token(&path))
            .await
            .context("File task panicked")?
    }

    async fn store_refresh_token(&self, token: &str) -> Result<()> {
        let path = self.refresh_token_path();
        let owned_token = token.to_owned();
        let storage = self.clone();

        tokio::task::spawn_blocking(move || {
            storage.write_token(&path, &owned_token)?;
            tracing::info!("Refresh token stored in file: {}", path.display());
            Ok(())
        })
        .await
        .context("File task panicked")?
    }

    async fn get_refresh_token(&self) -> Result<Option<String>> {
        let path = self.refresh_token_path();
        let storage = self.clone();

        tokio::task::spawn_blocking(move || storage.read_token(&path))
            .await
            .context("File task panicked")?
    }

    async fn remove_refresh_token(&self) -> Result<()> {
        let path = self.refresh_token_path();
        let storage = self.clone();

        tokio::task::spawn_blocking(move || {
            storage.delete_token(&path)?;
            tracing::info!("Refresh token removed from file");
            Ok(())
        })
        .await
        .context("File task panicked")?
    }

    async fn clear_access_token(&self) -> Result<()> {
        let path = self.access_token_path();
        let storage = self.clone();

        tokio::task::spawn_blocking(move || {
            storage.delete_token(&path)?;
            tracing::debug!("Access token removed from file");
            Ok(())
        })
        .await
        .context("File task panicked")?
    }
}

/// In-memory token storage (development/testing only)
pub struct InMemoryTokenStorage {
    access_token: Arc<RwLock<Option<String>>>,
    refresh_token: Arc<RwLock<Option<String>>>,
}

impl InMemoryTokenStorage {
    /// Create a new in-memory token storage
    pub fn new() -> Self {
        Self {
            access_token: Arc::new(RwLock::new(None)),
            refresh_token: Arc::new(RwLock::new(None)),
        }
    }
}

impl Default for InMemoryTokenStorage {
    fn default() -> Self {
        Self::new()
    }
}

#[async_trait::async_trait]
impl TokenStorage for InMemoryTokenStorage {
    async fn store_access_token(&self, token: &str) -> Result<()> {
        let mut t = self.access_token.write().await;
        *t = Some(token.to_owned());
        tracing::warn!("Access token stored in-memory (development mode - not secure)");
        Ok(())
    }

    async fn get_access_token(&self) -> Result<Option<String>> {
        Ok(self.access_token.read().await.clone())
    }

    async fn store_refresh_token(&self, token: &str) -> Result<()> {
        let mut t = self.refresh_token.write().await;
        *t = Some(token.to_owned());
        tracing::warn!("Refresh token stored in-memory (development mode - not secure)");
        Ok(())
    }

    async fn get_refresh_token(&self) -> Result<Option<String>> {
        Ok(self.refresh_token.read().await.clone())
    }

    async fn remove_refresh_token(&self) -> Result<()> {
        let mut t = self.refresh_token.write().await;
        *t = None;
        Ok(())
    }

    async fn clear_access_token(&self) -> Result<()> {
        let mut t = self.access_token.write().await;
        *t = None;
        Ok(())
    }
}

/// Authentication token manager
///
/// Manages JWT tokens via keyring storage. Since FXT is a CLI tool,
/// tokens are read from keyring on each access rather than cached in memory.
pub struct AuthTokenManager<S: TokenStorage> {
    /// Token storage backend
    storage: Arc<S>,
}

impl<S: TokenStorage> AuthTokenManager<S> {
    /// Create a new authentication token manager
    pub fn new(storage: S) -> Self {
        Self {
            storage: Arc::new(storage),
        }
    }

    /// Set tokens after successful authentication
    pub async fn set_tokens(&self, token_info: TokenInfo) -> Result<()> {
        // Store access token in keyring
        self.storage
            .store_access_token(&token_info.access_token)
            .await
            .context("Failed to store access token")?;

        // Store refresh token in keyring
        self.storage
            .store_refresh_token(&token_info.refresh_token)
            .await
            .context("Failed to store refresh token")?;

        tracing::info!("Authentication tokens stored successfully in keyring");
        Ok(())
    }

    /// Get current access token (returns None if expired or not set)
    pub async fn get_access_token(&self) -> Option<String> {
        // Read access token from keyring
        match self.storage.get_access_token().await {
            Ok(Some(token)) => {
                // Check if token is expired by parsing expiry
                if let Ok(expires_at) = extract_token_expiry(&token) {
                    let token_info = TokenInfo {
                        access_token: token.clone(),
                        refresh_token: String::new(), // Not needed for expiry check
                        expires_at,
                    };

                    if !token_info.is_expired() {
                        return Some(token);
                    } else {
                        tracing::warn!("Access token is expired or near expiration");
                    }
                } else {
                    // If we can't parse expiry, return the token anyway
                    return Some(token);
                }
            },
            Ok(None) => {},
            Err(e) => {
                tracing::error!("Failed to read access token from keyring: {}", e);
            },
        }

        None
    }

    /// Get cached access token synchronously (for gRPC interceptor)
    ///
    /// This method provides synchronous access to the token from keyring for use in
    /// tonic's Interceptor trait. Note: This performs a blocking keyring read.
    pub fn get_cached_access_token(&self) -> Option<String> {
        // Synchronously read from storage (blocking operation)
        let storage = Arc::clone(&self.storage);

        // Use tokio's block_in_place to allow blocking within async context
        tokio::task::block_in_place(move || {
            // Create a new runtime handle for this blocking context
            let handle = tokio::runtime::Handle::current();
            handle.block_on(async move {
                match storage.get_access_token().await {
                    Ok(token) => token,
                    Err(e) => {
                        tracing::error!("Failed to read access token from keyring: {}", e);
                        None
                    },
                }
            })
        })
    }

    /// Get stored refresh token
    pub async fn get_refresh_token(&self) -> Result<Option<String>> {
        self.storage.get_refresh_token().await
    }

    /// Get current token info (returns None if expired or not set)
    pub async fn get_current_token(&self) -> Option<TokenInfo> {
        // Read both tokens from keyring
        let access_token = self.storage.get_access_token().await.ok()??;
        let refresh_token = self.storage.get_refresh_token().await.ok()??;

        // Extract expiry from access token (SAFE fallback for non-JWT tokens)
        let expires_at = extract_token_expiry(&access_token).unwrap_or_else(|e| {
            tracing::warn!(
                "Failed to extract token expiry, treating as expired for security: {}",
                e
            );
            0 // Treat as expired if we can't parse (secure default - forces re-authentication)
        });

        let token_info = TokenInfo {
            access_token,
            refresh_token,
            expires_at,
        };

        (!token_info.is_expired()).then_some(token_info)
    }

    /// Check if the token needs refresh (expired or near expiration)
    pub async fn needs_refresh(&self) -> bool {
        // Read tokens directly from storage (don't use get_current_token which filters expired tokens)
        let Ok(Some(access_token)) = self.storage.get_access_token().await else {
            return false; // No token = no refresh needed
        };

        let Ok(Some(refresh_token)) = self.storage.get_refresh_token().await else {
            return false; // No refresh token = can't refresh
        };

        // Extract expiry from access token
        let Ok(expires_at) = extract_token_expiry(&access_token) else {
            return false; // Can't parse = assume no refresh needed
        };

        let token_info = TokenInfo {
            access_token,
            refresh_token,
            expires_at,
        };

        // Return true if token is expired or near expiration
        token_info.is_expired()
    }

    /// Check if tokens are set and valid
    pub async fn has_valid_token(&self) -> bool {
        self.get_access_token().await.is_some()
    }

    /// Clear all tokens (logout)
    pub async fn clear_tokens(&self) -> Result<()> {
        // Clear access token from keyring
        self.storage.clear_access_token().await?;

        // Clear refresh token from keyring
        self.storage.remove_refresh_token().await?;

        tracing::info!("All authentication tokens cleared from keyring");
        Ok(())
    }

    /// Update tokens after refresh
    pub async fn update_tokens(&self, access_token: String, _expires_at: u64) -> Result<()> {
        // Store updated access token in keyring
        self.storage
            .store_access_token(&access_token)
            .await
            .context("Failed to store refreshed access token")?;

        tracing::info!("Access token updated after refresh");
        Ok(())
    }

    /// Get time until token expiration
    pub async fn time_until_expiry(&self) -> Option<Duration> {
        self.get_current_token()
            .await
            .map(|t| t.time_until_expiry())
    }
}

impl<S: TokenStorage> Clone for AuthTokenManager<S> {
    fn clone(&self) -> Self {
        Self {
            storage: Arc::clone(&self.storage),
        }
    }
}

#[cfg(test)]
#[allow(clippy::shadow_unrelated, clippy::shadow_reuse, clippy::unwrap_used, clippy::expect_used, clippy::assertions_on_result_states, clippy::str_to_string, clippy::let_underscore_must_use)]
mod tests {
    use super::*;

    #[test]
    fn test_token_expiration() {
        let now = SystemTime::now()
            .duration_since(UNIX_EPOCH)
            .unwrap()
            .as_secs();

        // Token expires in 30 seconds - should be considered expired
        let token_expired = TokenInfo {
            access_token: "test".to_owned(),
            refresh_token: "refresh".to_owned(),
            expires_at: now + 30,
        };
        assert!(token_expired.is_expired());

        // Token expires in 120 seconds - should be valid
        let token_valid = TokenInfo {
            access_token: "test".to_owned(),
            refresh_token: "refresh".to_owned(),
            expires_at: now + 120,
        };
        assert!(!token_valid.is_expired());
    }

    #[tokio::test]
    async fn test_in_memory_storage() {
        let storage = InMemoryTokenStorage::new();
        let manager = AuthTokenManager::new(storage);

        let now = SystemTime::now()
            .duration_since(UNIX_EPOCH)
            .unwrap()
            .as_secs();

        let token_info = TokenInfo {
            access_token: "access_token_123".to_owned(),
            refresh_token: "refresh_token_456".to_owned(),
            expires_at: now + 3600,
        };

        manager.set_tokens(token_info).await.unwrap();

        assert!(manager.has_valid_token().await);
        assert_eq!(
            manager.get_access_token().await.unwrap(),
            "access_token_123"
        );

        manager.clear_tokens().await.unwrap();
        assert!(!manager.has_valid_token().await);
    }

    #[tokio::test]
    #[cfg(unix)]
    async fn test_file_storage_permissions() {
        use std::os::unix::fs::PermissionsExt;

        // Create storage in isolated temp directory for test isolation
        let temp_dir =
            std::env::temp_dir().join(format!("foxhunt_test_perms_{}", std::process::id()));
        let storage = FileTokenStorage::with_directory(temp_dir.clone()).unwrap();

        // Store a test token
        storage.store_access_token("test_token_123").await.unwrap();

        // Check file permissions (should be 600)
        let access_path = storage.access_token_path();
        let metadata = std::fs::metadata(&access_path).unwrap();
        let permissions = metadata.permissions();

        // 600 in octal = 0o600 = owner read/write only
        assert_eq!(
            permissions.mode() & 0o777,
            0o600,
            "Access token file should have 600 permissions"
        );

        // Verify token can be read back successfully (encryption roundtrip)
        let retrieved = storage.get_access_token().await.unwrap();
        assert_eq!(
            retrieved,
            Some("test_token_123".to_owned()),
            "Token should be retrievable after encryption"
        );

        // Cleanup
        storage.clear_access_token().await.unwrap();

        // Store refresh token
        storage
            .store_refresh_token("test_refresh_123")
            .await
            .unwrap();

        // Check file permissions (should be 600)
        let refresh_path = storage.refresh_token_path();
        let metadata = std::fs::metadata(&refresh_path).unwrap();
        let permissions = metadata.permissions();

        assert_eq!(
            permissions.mode() & 0o777,
            0o600,
            "Refresh token file should have 600 permissions"
        );

        // Verify refresh token can be read back successfully (encryption roundtrip)
        let retrieved_refresh = storage.get_refresh_token().await.unwrap();
        assert_eq!(
            retrieved_refresh,
            Some("test_refresh_123".to_owned()),
            "Refresh token should be retrievable after encryption"
        );

        // Cleanup
        storage.remove_refresh_token().await.unwrap();

        // Cleanup temp directory
        let _ = std::fs::remove_dir_all(&temp_dir);
    }

    #[tokio::test]
    async fn test_file_storage_encrypted_roundtrip() {
        // Create storage in isolated temp directory for test isolation
        let temp_dir =
            std::env::temp_dir().join(format!("foxhunt_test_roundtrip_{}", std::process::id()));
        let storage = FileTokenStorage::with_directory(temp_dir.clone()).unwrap();

        // Store and retrieve access token
        storage.store_access_token("access_123").await.unwrap();
        let retrieved = storage.get_access_token().await.unwrap();
        assert_eq!(
            retrieved,
            Some("access_123".to_owned()),
            "Access token roundtrip should work"
        );

        // Store and retrieve refresh token
        storage.store_refresh_token("refresh_456").await.unwrap();
        let retrieved = storage.get_refresh_token().await.unwrap();
        assert_eq!(
            retrieved,
            Some("refresh_456".to_owned()),
            "Refresh token roundtrip should work"
        );

        // Cleanup
        storage.clear_access_token().await.unwrap();
        storage.remove_refresh_token().await.unwrap();

        // Verify cleanup worked
        let after_clear = storage.get_access_token().await.unwrap();
        assert_eq!(after_clear, None, "Access token should be None after clear");
        let after_remove = storage.get_refresh_token().await.unwrap();
        assert_eq!(
            after_remove, None,
            "Refresh token should be None after remove"
        );

        // Cleanup temp directory
        let _ = std::fs::remove_dir_all(&temp_dir);
    }
}
