//! HashiCorp Vault configuration for secure secret management.
//!
//! This module provides configuration structures for integrating with HashiCorp Vault
//! to securely manage secrets, API keys, and sensitive configuration data in the
//! Foxhunt trading system. Supports token-based authentication and namespace isolation.

use secrecy::{ExposeSecret, SecretString};
use serde::{Deserialize, Serialize};
use std::fmt;

/// HashiCorp Vault configuration for secure secret storage.
///
/// Configures connection to HashiCorp Vault for retrieving sensitive
/// configuration data such as API keys, database passwords, and other
/// secrets. Supports Vault Enterprise features like namespaces.
///
/// # Security
///
/// The Vault token is wrapped in `SecretString` to prevent accidental
/// exposure in logs, debug output, or memory dumps. The token is automatically
/// zeroized when the config is dropped.
#[derive(Clone, Serialize, Deserialize)]
#[allow(clippy::module_name_repetitions)]
pub struct VaultConfig {
    /// Vault server URL (e.g., "<https://vault.example.com:8200>")
    pub url: String,
    /// Vault authentication token for API access (securely stored)
    #[serde(
        serialize_with = "serialize_secret",
        deserialize_with = "deserialize_secret"
    )]
    pub token: SecretString,
    /// Mount path for the secrets engine (e.g., "secret/")
    pub mount_path: String,
    /// Vault namespace for multi-tenant deployments (Enterprise feature)
    pub namespace: Option<String>,
}

/// Custom serializer for SecretString that prevents token exposure
fn serialize_secret<S>(_secret: &SecretString, serializer: S) -> Result<S::Ok, S::Error>
where
    S: serde::Serializer,
{
    // Serialize as redacted placeholder to prevent token exposure
    serializer.serialize_str("***REDACTED***")
}

/// Custom deserializer for SecretString
fn deserialize_secret<'de, D>(deserializer: D) -> Result<SecretString, D::Error>
where
    D: serde::Deserializer<'de>,
{
    let s = String::deserialize(deserializer)?;
    Ok(SecretString::from(s))
}

impl fmt::Debug for VaultConfig {
    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
        f.debug_struct("VaultConfig")
            .field("url", &self.url)
            .field("token", &"***REDACTED***")
            .field("mount_path", &self.mount_path)
            .field("namespace", &self.namespace)
            .finish()
    }
}

impl Drop for VaultConfig {
    fn drop(&mut self) {
        // Explicitly zeroize the token when VaultConfig is dropped
        // This ensures the secret is cleared from memory
        // Note: SecretString already implements ZeroizeOnDrop, but we make it explicit
        // for documentation purposes
    }
}

impl VaultConfig {
    /// Creates a new VaultConfig with the specified parameters.
    ///
    /// # Security
    ///
    /// The token is immediately wrapped in a `SecretString` to prevent exposure.
    ///
    /// Consider using `from_env()` or loading from secure configuration
    /// sources instead of passing plain strings.
    pub fn new(url: String, token: String, mount_path: String) -> Self {
        Self {
            url,
            token: SecretString::from(token),
            mount_path,
            namespace: None,
        }
    }

    /// Sets the namespace for multi-tenant Vault deployments.
    pub fn with_namespace(mut self, namespace: String) -> Self {
        self.namespace = Some(namespace);
        self
    }

    /// Gets a reference to the secret token (requires explicit exposure)
    ///
    /// # Security
    ///
    /// This method requires the caller to explicitly acknowledge they are
    /// exposing the secret. Use only when necessary (e.g., when making
    ///
    /// API calls to Vault) and ensure the exposed value is not logged
    /// or stored in insecure locations.
    pub const fn token(&self) -> &SecretString {
        &self.token
    }

    /// Validates the vault configuration.
    ///
    /// # Security
    ///
    /// Validation checks length without exposing the token value.
    ///
    /// # Errors
    /// Returns error if the operation fails
    pub fn validate(&self) -> Result<(), String> {
        if self.url.is_empty() {
            return Err("Vault URL cannot be empty".to_owned());
        }
        if self.token.expose_secret().is_empty() {
            return Err("Vault token cannot be empty".to_owned());
        }
        if self.mount_path.is_empty() {
            return Err("Vault mount path cannot be empty".to_owned());
        }
        Ok(())
    }
}

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

    fn create_test_config() -> VaultConfig {
        VaultConfig::new(
            "https://vault.example.com:8200".to_owned(),
            "test-token-12345".to_owned(),
            "secret/".to_owned(),
        )
    }

    #[test]
    fn test_vault_config_creation() {
        let config = create_test_config();
        assert_eq!(config.url, "https://vault.example.com:8200");
        assert_eq!(config.mount_path, "secret/");
        assert!(config.namespace.is_none());
    }

    #[test]
    fn test_vault_config_with_namespace() {
        let config = create_test_config().with_namespace("production".to_owned());
        assert_eq!(config.namespace.as_deref(), Some("production"));
    }

    #[test]
    fn test_vault_config_validation_success() {
        let config = create_test_config();
        assert!(config.validate().is_ok());
    }

    #[test]
    fn test_vault_config_validation_empty_url() {
        let mut config = create_test_config();
        config.url = String::new();
        assert!(config.validate().is_err());
        assert_eq!(config.validate().unwrap_err(), "Vault URL cannot be empty");
    }

    #[test]
    fn test_vault_config_validation_empty_token() {
        let mut config = create_test_config();
        config.token = SecretString::from(String::new());
        assert!(config.validate().is_err());
        assert_eq!(
            config.validate().unwrap_err(),
            "Vault token cannot be empty"
        );
    }

    #[test]
    fn test_vault_config_validation_empty_mount_path() {
        let mut config = create_test_config();
        config.mount_path = String::new();
        assert!(config.validate().is_err());
        assert_eq!(
            config.validate().unwrap_err(),
            "Vault mount path cannot be empty"
        );
    }

    #[test]
    fn test_vault_config_serialization() {
        let config = create_test_config();
        let serialized = serde_json::to_string(&config).unwrap();
        // Token should be redacted in serialization
        assert!(serialized.contains("***REDACTED***"));
        assert!(!serialized.contains("test-token-12345"));
    }

    #[test]
    fn test_vault_config_deserialization() {
        let config = create_test_config();
        let serialized = serde_json::to_string(&config).unwrap();
        let deserialized: VaultConfig = serde_json::from_str(&serialized).unwrap();
        assert_eq!(config.url, deserialized.url);
        assert_eq!(config.mount_path, deserialized.mount_path);
    }

    #[test]
    fn test_vault_config_clone() {
        let config1 = create_test_config();
        let config2 = config1.clone();
        assert_eq!(config1.url, config2.url);
    }

    #[test]
    fn test_vault_config_debug() {
        let config = create_test_config();
        let debug_output = format!("{:?}", config);
        assert!(debug_output.contains("VaultConfig"));
        assert!(debug_output.contains("***REDACTED***"));
        assert!(!debug_output.contains("test-token-12345"));
    }

    #[test]
    fn test_vault_config_namespace_none() {
        let config = create_test_config();
        assert!(config.namespace.is_none());
    }

    #[test]
    fn test_vault_config_namespace_some() {
        let config = create_test_config().with_namespace("dev".to_owned());
        assert!(config.namespace.is_some());
        assert_eq!(config.namespace.as_deref(), Some("dev"));
    }

    #[test]
    fn test_vault_config_token_not_exposed() {
        let config = create_test_config();
        // Verify token accessor works
        assert_eq!(config.token().expose_secret(), "test-token-12345");
    }

    #[test]
    fn test_vault_config_token_redacted_in_display() {
        let config = create_test_config();
        let debug_str = format!("{:?}", config);
        assert!(!debug_str.contains("test-token-12345"));
    }
}
