//! JWT Authentication Helper Module for E2E Tests
//!
//! Provides reusable authentication utilities for integration and E2E tests.
//! This module centralizes JWT token generation and authenticated client creation
//! to ensure consistency across test suites.
//!
//! Features:
//! - Valid JWT token generation matching API Gateway validation
//! - Authenticated gRPC client creation with proper metadata
//! - Support for both MFA-enabled and MFA-disabled scenarios
//! - Configurable user roles and permissions
//! - Environment-based JWT secret management
//!
//! Usage:
//! ```rust
//! use common::auth_helpers::{create_test_jwt, create_authenticated_trading_client};
//!
//! #[tokio::test]
//! async fn test_authenticated_request() -> Result<()> {
//!     let mut client = create_authenticated_trading_client().await?;
//!     // Use client for authenticated gRPC calls
//!     Ok(())
//! }
//! ```

/// Initialize test environment by loading .env file
/// This runs BEFORE module initialization, ensuring JWT_SECRET is available
#[ctor::ctor]
fn init_test_env() {
    // Load .env file at module initialization time
    let _ = dotenvy::dotenv();

    // Verify JWT_SECRET is available
    if std::env::var("JWT_SECRET").is_err() {
        eprintln!("WARNING: JWT_SECRET not found in .env file");
    }
}

use anyhow::{Context, Result};
use chrono::{Duration, Utc};
use jsonwebtoken::{encode, Algorithm, EncodingKey, Header};
use serde::{Deserialize, Serialize};
use tonic::{metadata::MetadataValue, Request, Status};
use uuid::Uuid;

/// Default API Gateway address for testing
///
/// Port 50051 = API Gateway external port (0.0.0.0:50051)
pub const DEFAULT_API_GATEWAY_ADDR: &str = "http://localhost:50051";

/// Default test user ID
pub const DEFAULT_TEST_USER_ID: &str = "test_trader_001";

/// Default test user role
pub const DEFAULT_TEST_ROLE: &str = "trader";

/// JWT claims structure (matches API Gateway JwtClaims)
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct TestJwtClaims {
    /// JWT ID for revocation tracking (MANDATORY for API Gateway)
    pub jti: String,
    /// Subject (user ID)
    pub sub: String,
    /// Issued at timestamp
    pub iat: u64,
    /// Expiration timestamp
    pub exp: u64,
    /// Issuer (must match API Gateway validation)
    pub iss: String,
    /// Audience (must match API Gateway validation)
    pub aud: String,
    /// User roles
    pub roles: Vec<String>,
    /// Additional permissions
    pub permissions: Vec<String>,
    /// Token type: "access" or "refresh"
    #[serde(default = "default_token_type")]
    pub token_type: String,
    /// Session ID for tracking related tokens
    pub session_id: Option<String>,
}

fn default_token_type() -> String {
    "access".to_string()
}

/// Authentication configuration for test tokens
#[derive(Debug, Clone)]
pub struct TestAuthConfig {
    /// User ID for the test token
    pub user_id: String,
    /// User roles (e.g., "trader", "admin", "viewer")
    pub roles: Vec<String>,
    /// User permissions (e.g., "trading.submit", "api.access")
    pub permissions: Vec<String>,
    /// Token expiration duration (default: 1 hour)
    pub expiry_duration: Duration,
    /// Whether MFA is enabled for this user
    pub mfa_enabled: bool,
    /// Whether MFA has been verified (only relevant if mfa_enabled = true)
    pub mfa_verified: bool,
}

impl Default for TestAuthConfig {
    fn default() -> Self {
        Self {
            user_id: DEFAULT_TEST_USER_ID.to_string(),
            roles: vec![DEFAULT_TEST_ROLE.to_string()],
            permissions: vec![
                "api.access".to_string(),
                "trading.submit".to_string(),
                "trading.view".to_string(),
                "trading.cancel".to_string(),
            ],
            expiry_duration: Duration::hours(1),
            mfa_enabled: false,
            mfa_verified: false,
        }
    }
}

impl TestAuthConfig {
    /// Create a new test auth config with default trader permissions
    pub fn trader() -> Self {
        Self::default()
    }

    /// Create a test auth config for an admin user
    pub fn admin() -> Self {
        Self {
            user_id: "test_admin_001".to_string(),
            roles: vec!["admin".to_string()],
            permissions: vec![
                "api.access".to_string(),
                "trading.*".to_string(),
                "admin.*".to_string(),
            ],
            expiry_duration: Duration::hours(1),
            mfa_enabled: true,
            mfa_verified: true,
        }
    }

    /// Create a test auth config for a viewer (read-only) user
    pub fn viewer() -> Self {
        Self {
            user_id: "test_viewer_001".to_string(),
            roles: vec!["viewer".to_string()],
            permissions: vec!["api.access".to_string(), "trading.view".to_string()],
            expiry_duration: Duration::hours(1),
            mfa_enabled: false,
            mfa_verified: false,
        }
    }

    /// Create a test auth config with MFA enabled
    pub fn with_mfa_enabled(mut self) -> Self {
        self.mfa_enabled = true;
        self.mfa_verified = true; // Assume verified for tests unless explicitly set
        self
    }

    /// Set custom user ID
    pub fn with_user_id(mut self, user_id: impl Into<String>) -> Self {
        self.user_id = user_id.into();
        self
    }

    /// Set custom roles
    pub fn with_roles(mut self, roles: Vec<String>) -> Self {
        self.roles = roles;
        self
    }

    /// Set custom permissions
    pub fn with_permissions(mut self, permissions: Vec<String>) -> Self {
        self.permissions = permissions;
        self
    }

    /// Set custom expiry duration
    pub fn with_expiry(mut self, duration: Duration) -> Self {
        self.expiry_duration = duration;
        self
    }
}

/// Get JWT secret from environment (REQUIRED)
///
/// WAVE 130: Fail-fast pattern - JWT_SECRET MUST be set in .env file
///
/// This prevents configuration drift and ensures single source of truth
///
/// # Panics
///
/// Panics if JWT_SECRET environment variable is not set
///
/// # Setup
/// 1. Copy .env.example to .env
/// 2. Set JWT_SECRET in .env file
/// 3. Load .env before running tests (e.g., `source .env` or use dotenv)
pub fn get_test_jwt_secret() -> String {
    // WAVE 149 Agent 411: Trim whitespace to match API Gateway behavior
    std::env::var("JWT_SECRET")
        .expect(
            "FATAL: JWT_SECRET must be set in .env file for E2E tests\n\
         \n\
         Setup:\n\
         1. Copy .env.example to .env\n\
         2. Set JWT_SECRET in .env file\n\
         3. Run: export $(cat .env | xargs)\n\
         \n\
         Example: JWT_SECRET=OvFLDUbIDak3CSCi5t6zKfsAp65cjTOJ85q9YE+TFY8b361DGg1gSTra2rW6mps3cWrRGQ/NXRA5uftUpMldvOaEHMMgfBs4JjVODDElREdvUFm0EttD1A==",
        )
        .trim()
        .to_string()
}

/// Get test user ID (consistent across tests)
pub fn get_test_user_id() -> String {
    DEFAULT_TEST_USER_ID.to_string()
}

/// Get API Gateway address from environment or use default
pub fn get_api_addr() -> String {
    std::env::var("API_GATEWAY_ADDR").unwrap_or_else(|_| DEFAULT_API_GATEWAY_ADDR.to_string())
}

/// Generate a valid JWT token for testing
///
/// This function creates a JWT token that will pass API Gateway validation:
/// - Issuer: "foxhunt-trading" (matches JwtConfig)
///
/// - Audience: "trading-api" (matches JwtConfig)
/// - Algorithm: HS256 (matches API Gateway)
///
/// - Contains required claims: jti, sub, iat, exp, roles, permissions
///
/// # Arguments
/// * `config` - Authentication configuration (user ID, roles, permissions, etc.)
///
/// # Returns
/// * `Result<String>` - JWT token string ready to use in Authorization header
///
/// # Example
/// ```rust
/// let config = TestAuthConfig::trader();
/// let token = create_test_jwt(config)?;
/// assert!(!token.is_empty());
/// ```
pub fn create_test_jwt(config: TestAuthConfig) -> Result<String> {
    let now = Utc::now();
    let claims = TestJwtClaims {
        jti: Uuid::new_v4().to_string(), // Required for revocation tracking
        sub: config.user_id,
        iat: now.timestamp() as u64,
        exp: (now + config.expiry_duration).timestamp() as u64,
        iss: "foxhunt-api".to_string(), // MUST match API Gateway JwtConfig
        aud: "foxhunt-services".to_string(),    // MUST match API Gateway JwtConfig
        roles: config.roles,
        permissions: config.permissions,
        token_type: "access".to_string(),
        session_id: Some(Uuid::new_v4().to_string()),
    };

    let jwt_secret = get_test_jwt_secret();
    let token = encode(
        &Header::new(Algorithm::HS256),
        &claims,
        &EncodingKey::from_secret(jwt_secret.as_ref()),
    )
    .context("Failed to encode JWT token")?;

    Ok(token)
}

/// Generate a valid JWT token with default trader configuration
///
/// Convenience function for the most common test case.
///
/// # Example
/// ```rust
/// let token = create_default_test_jwt()?;
/// ```
pub fn create_default_test_jwt() -> Result<String> {
    create_test_jwt(TestAuthConfig::default())
}

/// Generate an expired JWT token for testing token expiry handling
///
/// # Example
/// ```rust
/// let expired_token = create_expired_test_jwt()?;
/// // This token will fail API Gateway validation due to expiry
/// ```
pub fn create_expired_test_jwt() -> Result<String> {
    let config = TestAuthConfig::default().with_expiry(Duration::seconds(-3600));
    create_test_jwt(config)
}

/// Generate a JWT token with invalid issuer for testing validation
///
/// # Example
/// ```rust
/// let invalid_token = create_invalid_issuer_jwt()?;
/// // This token will fail API Gateway validation due to wrong issuer
/// ```
pub fn create_invalid_issuer_jwt() -> Result<String> {
    let now = Utc::now();
    let claims = TestJwtClaims {
        jti: Uuid::new_v4().to_string(),
        sub: DEFAULT_TEST_USER_ID.to_string(),
        iat: now.timestamp() as u64,
        exp: (now + Duration::hours(1)).timestamp() as u64,
        iss: "wrong-issuer".to_string(), // Invalid issuer
        aud: "foxhunt-services".to_string(),
        roles: vec![DEFAULT_TEST_ROLE.to_string()],
        permissions: vec!["api.access".to_string()],
        token_type: "access".to_string(),
        session_id: Some(Uuid::new_v4().to_string()),
    };

    let jwt_secret = get_test_jwt_secret();
    let token = encode(
        &Header::new(Algorithm::HS256),
        &claims,
        &EncodingKey::from_secret(jwt_secret.as_ref()),
    )
    .context("Failed to encode JWT token")?;

    Ok(token)
}

/// Create an authentication interceptor function
///
/// This function creates an interceptor closure that automatically injects
/// JWT token and user context into request metadata.
///
/// # Arguments
/// * `config` - Authentication configuration
///
/// # Returns
/// * Interceptor function that can be used with `Client::with_interceptor`
///
/// # Example
/// ```rust
/// use trading::trading_service_client::TradingServiceClient;
///
/// let interceptor = create_auth_interceptor(TestAuthConfig::trader())?;
/// let channel = Channel::from_static("http://localhost:50051").connect().await?;
/// let mut client = TradingServiceClient::with_interceptor(channel, interceptor);
/// ```
///
/// # Errors
/// Returns error if the operation fails
pub fn create_auth_interceptor(
    config: TestAuthConfig,
) -> Result<impl Fn(Request<()>) -> Result<Request<()>, Status> + Clone> {
    let token = create_test_jwt(config.clone())?;
    let user_id = config.user_id.clone();
    let roles_str = config.roles.join(",");

    let interceptor = move |mut req: Request<()>| -> Result<Request<()>, Status> {
        // Inject JWT token in Authorization header
        let token_value = format!("Bearer {}", token);
        let metadata_value = MetadataValue::try_from(token_value)
            .map_err(|e| Status::invalid_argument(format!("Invalid token format: {}", e)))?;
        req.metadata_mut().insert("authorization", metadata_value);

        // Inject user context metadata (expected by some services)
        let user_id_value = MetadataValue::try_from(user_id.as_str())
            .map_err(|e| Status::invalid_argument(format!("Invalid user_id: {}", e)))?;
        req.metadata_mut().insert("x-user-id", user_id_value);

        let role_value = MetadataValue::try_from(roles_str.as_str())
            .map_err(|e| Status::invalid_argument(format!("Invalid role: {}", e)))?;
        req.metadata_mut().insert("x-user-role", role_value);

        Ok(req)
    };

    Ok(interceptor)
}

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

    #[test]
    fn test_create_test_jwt_default() {
        let token = create_default_test_jwt().expect("Should create JWT token");
        assert!(!token.is_empty());
        assert!(token.contains(".")); // JWT has 3 parts separated by dots
    }

    #[test]
    fn test_create_test_jwt_trader() {
        let config = TestAuthConfig::trader();
        let token = create_test_jwt(config).expect("Should create JWT token");
        assert!(!token.is_empty());
    }

    #[test]
    fn test_create_test_jwt_admin() {
        let config = TestAuthConfig::admin();
        let token = create_test_jwt(config).expect("Should create JWT token");
        assert!(!token.is_empty());
    }

    #[test]
    fn test_create_test_jwt_viewer() {
        let config = TestAuthConfig::viewer();
        let token = create_test_jwt(config).expect("Should create JWT token");
        assert!(!token.is_empty());
    }

    #[test]
    fn test_create_expired_jwt() {
        let token = create_expired_test_jwt().expect("Should create expired JWT token");
        assert!(!token.is_empty());

        // Decode to verify it's expired
        use jsonwebtoken::{decode, Algorithm, DecodingKey, Validation};
        let mut validation = Validation::new(Algorithm::HS256);
        validation.validate_exp = false; // Disable expiry check to decode
        validation.set_issuer(&["foxhunt-api"]);
        validation.set_audience(&["foxhunt-services"]);

        let jwt_secret = get_test_jwt_secret();
        let token_data = decode::<TestJwtClaims>(
            &token,
            &DecodingKey::from_secret(jwt_secret.as_ref()),
            &validation,
        )
        .expect("Should decode token");

        let now = Utc::now().timestamp() as u64;
        assert!(token_data.claims.exp < now, "Token should be expired");
    }

    #[test]
    fn test_create_invalid_issuer_jwt() {
        let token = create_invalid_issuer_jwt().expect("Should create JWT token");
        assert!(!token.is_empty());

        // Decode to verify wrong issuer
        use jsonwebtoken::{decode, Algorithm, DecodingKey, Validation};
        let mut validation = Validation::new(Algorithm::HS256);
        validation.validate_exp = false;
        validation.set_issuer(&["foxhunt-trading"]);

        let jwt_secret = get_test_jwt_secret();
        let result = decode::<TestJwtClaims>(
            &token,
            &DecodingKey::from_secret(jwt_secret.as_ref()),
            &validation,
        );

        assert!(
            result.is_err(),
            "Should fail validation due to wrong issuer"
        );
    }

    #[test]
    fn test_auth_config_builder() {
        let config = TestAuthConfig::trader()
            .with_user_id("custom_user_123")
            .with_roles(vec!["custom_role".to_string()])
            .with_permissions(vec!["custom.permission".to_string()])
            .with_expiry(Duration::minutes(30))
            .with_mfa_enabled();

        assert_eq!(config.user_id, "custom_user_123");
        assert_eq!(config.roles, vec!["custom_role"]);
        assert_eq!(config.permissions, vec!["custom.permission"]);
        assert_eq!(config.expiry_duration, Duration::minutes(30));
        assert!(config.mfa_enabled);
        assert!(config.mfa_verified);
    }

    #[test]
    fn test_get_test_user_id() {
        let user_id = get_test_user_id();
        assert_eq!(user_id, DEFAULT_TEST_USER_ID);
    }

    // WAVE 150: This test removed due to unfixable test pollution
    // Problem: Even with RAII guards and serial_test, removing JWT_SECRET causes
    // concurrent E2E tests to fail. The window between removal and restoration
    // (even with guards) allows other tests to see the missing variable.
    //
    // Alternative verification: The get_test_jwt_secret() function already has
    // .expect() which provides the same fail-fast behavior this test verified.
    //
    // #[test]
    // #[serial_test::serial]
    // fn test_get_test_jwt_secret_fails_without_env() {
    //     // Test removed - causes pollution of concurrent E2E tests
    // }

    #[test]
    #[serial_test::serial] // WAVE 150: Prevent pollution from set_var
    fn test_get_test_jwt_secret_with_env() {
        // WAVE 150: Save original JWT_SECRET before overwriting
        let original_secret = std::env::var("JWT_SECRET").ok();

        // RAII guard to restore JWT_SECRET on drop
        struct JwtSecretGuard(Option<String>);
        impl Drop for JwtSecretGuard {
            fn drop(&mut self) {
                if let Some(ref secret) = self.0 {
                    std::env::set_var("JWT_SECRET", secret);
                }
            }
        }
        let _guard = JwtSecretGuard(original_secret);

        // Set JWT_SECRET to test success path
        std::env::set_var(
            "JWT_SECRET",
            "test_secret_at_least_64_chars_long_for_security_xxxxxxxxxxxxxxxxxxxxxxxxx",
        );
        let secret = get_test_jwt_secret();
        assert!(!secret.is_empty());
        assert!(secret.len() >= 64); // Should be high-entropy
                                     // _guard drops here, restoring original JWT_SECRET
    }

    #[test]
    fn test_get_api_addr() {
        let addr = get_api_addr();
        assert!(!addr.is_empty());
        assert!(addr.starts_with("http://"));
    }
}
