//! Error types for the authentication plugin.
//!
//! This module provides structured error handling with specific error kinds
//! for different failure scenarios in authentication operations.

use std::fmt;

/// Enum representing different kinds of authentication errors.
#[derive(Debug, Clone, PartialEq, Eq)]
pub enum AuthErrorKind {
    /// Failed to authenticate with invalid credentials
    InvalidCredentials,
    /// Token has expired
    TokenExpired,
    /// Token format is invalid
    InvalidToken,
    /// Failed to verify token signature
    SignatureVerificationFailed,
    /// Keyring operation failed
    KeyringError,
    /// Network or HTTP request failed
    NetworkError,
    /// Server returned an error response
    ServerError,
    /// MFA verification failed
    MfaFailed,
    /// User not found
    UserNotFound,
    /// Session not found or expired
    SessionNotFound,
    /// Configuration error
    ConfigurationError,
    /// Internal error
    InternalError,
}

impl fmt::Display for AuthErrorKind {
    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
        match self {
            Self::InvalidCredentials => write!(f, "invalid credentials"),
            Self::TokenExpired => write!(f, "token expired"),
            Self::InvalidToken => write!(f, "invalid token format"),
            Self::SignatureVerificationFailed => write!(f, "signature verification failed"),
            Self::KeyringError => write!(f, "keyring operation failed"),
            Self::NetworkError => write!(f, "network error"),
            Self::ServerError => write!(f, "server error"),
            Self::MfaFailed => write!(f, "MFA verification failed"),
            Self::UserNotFound => write!(f, "user not found"),
            Self::SessionNotFound => write!(f, "session not found"),
            Self::ConfigurationError => write!(f, "configuration error"),
            Self::InternalError => write!(f, "internal error"),
        }
    }
}

/// Structured error type for authentication operations.
///
/// Provides detailed error information including:
/// - Error kind for programmatic handling
/// - Context message for additional details
/// - Optional source error for error chaining
#[derive(Debug)]
pub struct AuthError {
    /// The kind of error that occurred
    pub kind: AuthErrorKind,
    /// Additional context about the error
    pub context: String,
    /// Optional underlying error
    pub source: Option<Box<dyn std::error::Error + Send + Sync>>,
}

impl AuthError {
    /// Creates a new AuthError with the specified kind and context.
    ///
    /// # Arguments
    ///
    /// * `kind` - The type of authentication error
    /// * `context` - Additional context describing the error
    ///
    /// # Example
    ///
    /// ```
    /// use nu_plugin_auth::error::{AuthError, AuthErrorKind};
    ///
    /// let error = AuthError::new(
    ///     AuthErrorKind::InvalidCredentials,
    ///     "Username or password is incorrect"
    /// );
    /// ```
    pub fn new(kind: AuthErrorKind, context: impl Into<String>) -> Self {
        Self {
            kind,
            context: context.into(),
            source: None,
        }
    }

    /// Creates an AuthError with an underlying source error.
    ///
    /// # Arguments
    ///
    /// * `kind` - The type of authentication error
    /// * `context` - Additional context describing the error
    /// * `source` - The underlying error that caused this error
    pub fn with_source(
        kind: AuthErrorKind,
        context: impl Into<String>,
        source: impl std::error::Error + Send + Sync + 'static,
    ) -> Self {
        Self {
            kind,
            context: context.into(),
            source: Some(Box::new(source)),
        }
    }

    /// Creates an invalid credentials error.
    pub fn invalid_credentials(context: impl Into<String>) -> Self {
        Self::new(AuthErrorKind::InvalidCredentials, context)
    }

    /// Creates a token expired error.
    pub fn token_expired(context: impl Into<String>) -> Self {
        Self::new(AuthErrorKind::TokenExpired, context)
    }

    /// Creates an invalid token error.
    pub fn invalid_token(context: impl Into<String>) -> Self {
        Self::new(AuthErrorKind::InvalidToken, context)
    }

    /// Creates a keyring error.
    pub fn keyring_error(context: impl Into<String>) -> Self {
        Self::new(AuthErrorKind::KeyringError, context)
    }

    /// Creates a network error.
    pub fn network_error(context: impl Into<String>) -> Self {
        Self::new(AuthErrorKind::NetworkError, context)
    }

    /// Creates a server error.
    pub fn server_error(context: impl Into<String>) -> Self {
        Self::new(AuthErrorKind::ServerError, context)
    }

    /// Creates an MFA failed error.
    pub fn mfa_failed(context: impl Into<String>) -> Self {
        Self::new(AuthErrorKind::MfaFailed, context)
    }

    /// Creates a configuration error.
    pub fn configuration_error(context: impl Into<String>) -> Self {
        Self::new(AuthErrorKind::ConfigurationError, context)
    }
}

impl fmt::Display for AuthError {
    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
        write!(f, "{}: {}", self.kind, self.context)?;
        if let Some(ref source) = self.source {
            write!(f, " (caused by: {})", source)?;
        }
        Ok(())
    }
}

impl std::error::Error for AuthError {
    fn source(&self) -> Option<&(dyn std::error::Error + 'static)> {
        self.source
            .as_ref()
            .map(|e| e.as_ref() as &(dyn std::error::Error + 'static))
    }
}

impl From<AuthError> for nu_protocol::LabeledError {
    fn from(err: AuthError) -> Self {
        nu_protocol::LabeledError::new(err.to_string())
    }
}

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

    #[test]
    fn test_error_display() {
        let error = AuthError::new(
            AuthErrorKind::InvalidCredentials,
            "username admin not found",
        );
        assert!(error.to_string().contains("invalid credentials"));
        assert!(error.to_string().contains("username admin not found"));
    }

    #[test]
    fn test_error_with_source() {
        let io_error = std::io::Error::new(std::io::ErrorKind::NotFound, "file not found");
        let error = AuthError::with_source(
            AuthErrorKind::KeyringError,
            "failed to read keyring",
            io_error,
        );
        assert!(error.to_string().contains("caused by"));
    }

    #[test]
    fn test_error_kind_display() {
        assert_eq!(AuthErrorKind::InvalidCredentials.to_string(), "invalid credentials");
        assert_eq!(AuthErrorKind::TokenExpired.to_string(), "token expired");
        assert_eq!(AuthErrorKind::KeyringError.to_string(), "keyring operation failed");
    }

    #[test]
    fn test_convenience_constructors() {
        let error = AuthError::invalid_credentials("bad password");
        assert_eq!(error.kind, AuthErrorKind::InvalidCredentials);

        let error = AuthError::network_error("connection refused");
        assert_eq!(error.kind, AuthErrorKind::NetworkError);
    }
}
