//! Test Utilities for Foxhunt HFT Trading System
//!
//! Provides reusable test helpers for E2E integration tests across all services.
//! This module is only available when running tests.
//!
//! # Features
//!
//! - **JWT Token Generation**: Generate valid JWT tokens for authenticated gRPC requests
//! - **Test User Credentials**: Create realistic test user profiles
//! - **Token Metadata**: Track JTI for revocation tests
//!
//! # Usage
//!
//! ```rust,ignore
//! use common::test_utils::{create_test_jwt_token, create_test_user_credentials};
//!
//! #[tokio::test]
//! async fn test_authenticated_request() {
//!     // Generate JWT token
//!     let (token, jti) = create_test_jwt_token().expect("Failed to create token");
//!
//!     // Add to gRPC metadata
//!     let mut request = tonic::Request::new(MyRequest { ... });
//!     request.metadata_mut().insert(
//!         "authorization",
//!         format!("Bearer {}", token).parse().unwrap()
//!     );
//!
//!     // Make authenticated request
//!     let response = client.my_method(request).await?;
//! }
//! ```

use anyhow::{Context, Result};
use serde::{Deserialize, Serialize};
use std::time::{SystemTime, UNIX_EPOCH};

#[cfg(test)]
use jsonwebtoken::{encode, EncodingKey, Header};
#[cfg(test)]
use uuid::Uuid;

/// JWT claims structure matching API Gateway expectations
///
/// This structure mirrors the production JWT claims format to ensure
/// test tokens are compatible with the authentication interceptor.
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
pub struct TestJwtClaims {
    /// JWT ID (unique identifier for revocation tracking)
    pub jti: String,
    /// Subject (user ID)
    pub sub: String,
    /// Issued at timestamp (Unix epoch seconds)
    pub iat: u64,
    /// Expiration timestamp (Unix epoch seconds)
    pub exp: u64,
    /// Not before timestamp (optional)
    #[serde(skip_serializing_if = "Option::is_none")]
    pub nbf: Option<u64>,
    /// Issuer (should be "foxhunt-api")
    pub iss: String,
    /// Audience (should be "foxhunt-services")
    pub aud: String,
    /// User roles (e.g., ["trader", "admin"])
    pub roles: Vec<String>,
    /// User permissions (e.g., ["api.access", "trade.execute"])
    pub permissions: Vec<String>,
    /// Token type: "access" or "refresh"
    pub token_type: String,
    /// Session ID for tracking related tokens (optional)
    #[serde(skip_serializing_if = "Option::is_none")]
    pub session_id: Option<String>,
}

/// JWT configuration for test token generation
///
/// Uses the same secret as API Gateway for compatibility.
#[derive(Debug, Clone)]
pub struct TestJwtConfig {
    /// JWT secret (must match API Gateway configuration)
    pub secret: String,
    /// JWT issuer (must match API Gateway configuration)
    pub issuer: String,
    /// JWT audience (must match API Gateway configuration)
    pub audience: String,
}

impl Default for TestJwtConfig {
    fn default() -> Self {
        Self {
            // Use same test secret as API Gateway (64+ chars for validation)
            secret: std::env::var("JWT_SECRET")
                .unwrap_or_else(|_| {
                    "test-secret-must-be-at-least-64-characters-long-for-security-validation-ok-1234567890".to_string()
                }),
            issuer: "foxhunt-api".to_string(),
            audience: "foxhunt-services".to_string(),
        }
    }
}

/// Test user credentials for authenticated requests
///
/// Contains all necessary information for creating test users and tokens.
#[derive(Debug, Clone)]
pub struct TestUserCredentials {
    /// User identifier
    pub user_id: String,
    /// User roles
    pub roles: Vec<String>,
    /// User permissions
    pub permissions: Vec<String>,
}

impl Default for TestUserCredentials {
    /// Default test user with standard trader permissions
    fn default() -> Self {
        Self {
            user_id: "test_user_default".to_string(),
            roles: vec!["trader".to_string()],
            permissions: vec![
                "api.access".to_string(),
                "trade.execute".to_string(),
                "trade.view".to_string(),
            ],
        }
    }
}

impl TestUserCredentials {
    /// Create a new test user with custom credentials
    pub fn new(user_id: impl Into<String>, roles: Vec<String>, permissions: Vec<String>) -> Self {
        Self {
            user_id: user_id.into(),
            roles,
            permissions,
        }
    }

    /// Create an admin user with elevated permissions
    pub fn admin() -> Self {
        Self {
            user_id: "test_admin".to_string(),
            roles: vec!["admin".to_string(), "trader".to_string()],
            permissions: vec![
                "api.access".to_string(),
                "admin.access".to_string(),
                "trade.execute".to_string(),
                "trade.view".to_string(),
                "trade.cancel".to_string(),
                "system.manage".to_string(),
            ],
        }
    }

    /// Create a read-only user
    pub fn read_only() -> Self {
        Self {
            user_id: "test_readonly".to_string(),
            roles: vec!["viewer".to_string()],
            permissions: vec!["api.access".to_string(), "trade.view".to_string()],
        }
    }

    /// Create a trader user (default permissions)
    pub fn trader() -> Self {
        Self::default()
    }
}

/// Generate a valid JWT access token for integration tests
///
/// Returns `(token, jti)` where:
/// - `token`: The JWT token string (use in Authorization header)
/// - `jti`: The JWT ID for tracking/revocation
///
/// # Default Configuration
///
/// - **User ID**: "test_user_default"
/// - **Roles**: ["trader"]
/// - **Permissions**: ["api.access", "trade.execute", "trade.view"]
/// - **TTL**: 3600 seconds (1 hour)
///
/// # Example
///
/// ```rust,ignore
/// use common::test_utils::create_test_jwt_token;
/// use tonic::metadata::MetadataValue;
///
/// #[tokio::test]
/// async fn test_authenticated_grpc_call() {
///     let (token, _jti) = create_test_jwt_token()
///         .expect("Failed to generate test token");
///
///     let mut request = tonic::Request::new(GetRegimeStateRequest {
///         symbol: "ES.FUT".to_string(),
///     });
///
///     // Add Authorization header
///     request.metadata_mut().insert(
///         "authorization",
///         MetadataValue::from_str(&format!("Bearer {}", token))
///             .expect("Failed to create metadata value")
///     );
///
///     let response = client.get_regime_state(request).await?;
/// }
/// ```
#[cfg(test)]
pub fn create_test_jwt_token() -> Result<(String, String)> {
    let credentials = TestUserCredentials::default();
    create_test_jwt_token_with_credentials(&credentials, 3600)
}

/// Generate a JWT token with custom user credentials
///
/// Returns `(token, jti)` for token tracking.
///
/// # Arguments
///
/// - `credentials`: User credentials (user_id, roles, permissions)
/// - `ttl_seconds`: Time-to-live in seconds (e.g., 3600 for 1 hour)
///
/// # Example
///
/// ```rust,ignore
/// use common::test_utils::{create_test_jwt_token_with_credentials, TestUserCredentials};
///
/// #[tokio::test]
/// async fn test_admin_endpoint() {
///     let admin_creds = TestUserCredentials::admin();
///     let (token, _) = create_test_jwt_token_with_credentials(&admin_creds, 3600)?;
///
///     // Use admin token for privileged operations
///     // ...
/// }
/// ```
#[cfg(test)]
pub fn create_test_jwt_token_with_credentials(
    credentials: &TestUserCredentials,
    ttl_seconds: u64,
) -> Result<(String, String)> {
    let config = TestJwtConfig::default();
    let jti = Uuid::new_v4().to_string();

    let now = SystemTime::now()
        .duration_since(UNIX_EPOCH)
        .context("Failed to get current timestamp")?
        .as_secs();

    let claims = TestJwtClaims {
        jti: jti.clone(),
        sub: credentials.user_id.clone(),
        iat: now,
        exp: now + ttl_seconds,
        nbf: Some(now),
        iss: config.issuer,
        aud: config.audience,
        roles: credentials.roles.clone(),
        permissions: credentials.permissions.clone(),
        token_type: "access".to_string(),
        session_id: Some(Uuid::new_v4().to_string()),
    };

    let token = encode(
        &Header::default(),
        &claims,
        &EncodingKey::from_secret(config.secret.as_bytes()),
    )
    .context("Failed to encode JWT token")?;

    Ok((token, jti))
}

/// Generate an expired JWT token for testing token expiration logic
///
/// The returned token expired 1 hour ago and should be rejected by
/// the authentication interceptor.
///
/// # Example
///
/// ```rust,ignore
/// use common::test_utils::create_expired_jwt_token;
///
/// #[tokio::test]
/// async fn test_expired_token_rejection() {
///     let expired_token = create_expired_jwt_token()
///         .expect("Failed to generate expired token");
///
///     let mut request = tonic::Request::new(MyRequest { ... });
///     request.metadata_mut().insert(
///         "authorization",
///         format!("Bearer {}", expired_token).parse().unwrap()
///     );
///
///     // Should return Unauthenticated error
///     let result = client.my_method(request).await;
///     assert!(result.is_err());
/// }
/// ```
#[cfg(test)]
pub fn create_expired_jwt_token() -> Result<String> {
    let config = TestJwtConfig::default();
    let credentials = TestUserCredentials::default();

    let now = SystemTime::now()
        .duration_since(UNIX_EPOCH)
        .context("Failed to get current timestamp")?
        .as_secs();

    let claims = TestJwtClaims {
        jti: Uuid::new_v4().to_string(),
        sub: credentials.user_id,
        iat: now - 7200,       // Issued 2 hours ago
        exp: now - 3600,       // Expired 1 hour ago
        nbf: Some(now - 7200), // Valid from 2 hours ago
        iss: config.issuer,
        aud: config.audience,
        roles: credentials.roles,
        permissions: credentials.permissions,
        token_type: "access".to_string(),
        session_id: Some(Uuid::new_v4().to_string()),
    };

    let token = encode(
        &Header::default(),
        &claims,
        &EncodingKey::from_secret(config.secret.as_bytes()),
    )
    .context("Failed to encode expired JWT token")?;

    Ok(token)
}

/// Generate a JWT refresh token
///
/// Refresh tokens have the same structure as access tokens but with
/// `token_type: "refresh"` and typically longer TTL.
///
/// # Example
///
/// ```rust,ignore
/// use common::test_utils::create_test_refresh_token;
///
/// #[tokio::test]
/// async fn test_token_refresh_flow() {
///     let (refresh_token, _) = create_test_refresh_token()
///         .expect("Failed to generate refresh token");
///
///     // Use refresh token to get new access token
///     // ...
/// }
/// ```
#[cfg(test)]
pub fn create_test_refresh_token() -> Result<(String, String)> {
    create_test_refresh_token_with_ttl(7200) // 2 hours default
}

/// Generate a JWT refresh token with custom TTL
#[cfg(test)]
pub fn create_test_refresh_token_with_ttl(ttl_seconds: u64) -> Result<(String, String)> {
    let config = TestJwtConfig::default();
    let credentials = TestUserCredentials::default();
    let jti = Uuid::new_v4().to_string();

    let now = SystemTime::now()
        .duration_since(UNIX_EPOCH)
        .context("Failed to get current timestamp")?
        .as_secs();

    let claims = TestJwtClaims {
        jti: jti.clone(),
        sub: credentials.user_id,
        iat: now,
        exp: now + ttl_seconds,
        nbf: Some(now),
        iss: config.issuer,
        aud: config.audience,
        roles: credentials.roles,
        permissions: credentials.permissions,
        token_type: "refresh".to_string(),
        session_id: Some(Uuid::new_v4().to_string()),
    };

    let token = encode(
        &Header::default(),
        &claims,
        &EncodingKey::from_secret(config.secret.as_bytes()),
    )
    .context("Failed to encode refresh token")?;

    Ok((token, jti))
}

/// Create test user credentials (convenience wrapper for TestUserCredentials::new)
///
/// # Example
///
/// ```rust,ignore
/// use common::test_utils::create_test_user_credentials;
///
/// let trader = create_test_user_credentials(
///     "trader_001",
///     vec!["trader".to_string()],
///     vec!["api.access".to_string(), "trade.execute".to_string()],
/// );
/// ```
pub fn create_test_user_credentials(
    user_id: impl Into<String>,
    roles: Vec<String>,
    permissions: Vec<String>,
) -> TestUserCredentials {
    TestUserCredentials::new(user_id, roles, permissions)
}

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

    #[test]
    fn test_default_credentials() {
        let creds = TestUserCredentials::default();
        assert_eq!(creds.user_id, "test_user_default");
        assert_eq!(creds.roles, vec!["trader"]);
        assert!(creds.permissions.contains(&"api.access".to_string()));
        assert!(creds.permissions.contains(&"trade.execute".to_string()));
    }

    #[test]
    fn test_admin_credentials() {
        let creds = TestUserCredentials::admin();
        assert_eq!(creds.user_id, "test_admin");
        assert!(creds.roles.contains(&"admin".to_string()));
        assert!(creds.permissions.contains(&"admin.access".to_string()));
        assert!(creds.permissions.contains(&"system.manage".to_string()));
    }

    #[test]
    fn test_readonly_credentials() {
        let creds = TestUserCredentials::read_only();
        assert_eq!(creds.user_id, "test_readonly");
        assert_eq!(creds.roles, vec!["viewer"]);
        assert!(creds.permissions.contains(&"api.access".to_string()));
        assert!(creds.permissions.contains(&"trade.view".to_string()));
        assert!(!creds.permissions.contains(&"trade.execute".to_string()));
    }

    #[test]
    fn test_create_jwt_token() {
        let result = create_test_jwt_token();
        assert!(result.is_ok(), "Failed to create JWT token: {:?}", result);

        let (token, jti) = result.unwrap();

        // JWT should have 3 parts (header.payload.signature)
        let parts: Vec<&str> = token.split('.').collect();
        assert_eq!(
            parts.len(),
            3,
            "JWT should have header.payload.signature format"
        );

        // JTI should be a valid UUID
        assert!(Uuid::parse_str(&jti).is_ok(), "JTI should be a valid UUID");

        // Token should not be empty
        assert!(!token.is_empty(), "Token should not be empty");
        assert!(token.len() > 100, "Token should be reasonably long");
    }

    #[test]
    fn test_create_jwt_token_with_custom_credentials() {
        let creds = TestUserCredentials::admin();
        let result = create_test_jwt_token_with_credentials(&creds, 3600);
        assert!(
            result.is_ok(),
            "Failed to create JWT token with custom credentials"
        );

        let (token, _jti) = result.unwrap();
        let parts: Vec<&str> = token.split('.').collect();
        assert_eq!(parts.len(), 3, "JWT should have 3 parts");
    }

    #[test]
    fn test_create_expired_token() {
        let result = create_expired_jwt_token();
        assert!(result.is_ok(), "Failed to create expired token");

        let token = result.unwrap();
        let parts: Vec<&str> = token.split('.').collect();
        assert_eq!(parts.len(), 3, "JWT should have 3 parts");
    }

    #[test]
    fn test_create_refresh_token() {
        let result = create_test_refresh_token();
        assert!(result.is_ok(), "Failed to create refresh token");

        let (token, jti) = result.unwrap();
        let parts: Vec<&str> = token.split('.').collect();
        assert_eq!(parts.len(), 3, "JWT should have 3 parts");
        assert!(Uuid::parse_str(&jti).is_ok(), "JTI should be valid UUID");
    }

    #[test]
    fn test_create_user_credentials() {
        let creds = create_test_user_credentials(
            "custom_user",
            vec!["custom_role".to_string()],
            vec!["custom.permission".to_string()],
        );
        assert_eq!(creds.user_id, "custom_user");
        assert_eq!(creds.roles, vec!["custom_role"]);
        assert_eq!(creds.permissions, vec!["custom.permission"]);
    }

    #[test]
    fn test_jwt_config_default() {
        let config = TestJwtConfig::default();
        assert_eq!(config.issuer, "foxhunt-api");
        assert_eq!(config.audience, "foxhunt-services");
        assert!(
            config.secret.len() >= 64,
            "Secret should be at least 64 chars"
        );
    }

    #[test]
    fn test_multiple_tokens_unique_jti() {
        let (_, jti1) = create_test_jwt_token().unwrap();
        let (_, jti2) = create_test_jwt_token().unwrap();
        assert_ne!(jti1, jti2, "Each token should have unique JTI");
    }

    #[test]
    fn test_token_ttl_variations() {
        let short_ttl = create_test_jwt_token_with_credentials(
            &TestUserCredentials::default(),
            300, // 5 minutes
        );
        let long_ttl = create_test_jwt_token_with_credentials(
            &TestUserCredentials::default(),
            86400, // 24 hours
        );

        assert!(short_ttl.is_ok(), "Short TTL token should be created");
        assert!(long_ttl.is_ok(), "Long TTL token should be created");
    }
}
