#![allow(unused_imports)]
use super::*;
use argon2::password_hash::{PasswordHash, SaltString, rand_core::OsRng};
use argon2::{Argon2, PasswordHasher as _};
use athena_auth_core::AdminAuditLog;
use athena_auth_core::AuthContext;
use athena_auth_core::UpdateUser;
use athena_auth_core::VerificationOps;
use athena_auth_core::adapters::{MemoryDatabaseAdapter, UserOps};
use athena_auth_core::config::{Argon2Config, AuthConfig};
use std::collections::HashMap;
use std::sync::Arc;

fn create_test_context() -> AuthContext<MemoryDatabaseAdapter> {
    let config = AuthConfig::new("test-secret-key-at-least-32-chars-long");
    create_test_context_with_config(config)
}

fn create_test_context_with_config(config: AuthConfig) -> AuthContext<MemoryDatabaseAdapter> {
    let config = Arc::new(config);
    let database = Arc::new(MemoryDatabaseAdapter::new());
    AuthContext::new(config, database)
}

fn create_signup_request(email: &str, password: &str) -> AuthRequest {
    let body = serde_json::json!({
        "name": "Test User",
        "email": email,
        "password": password,
    });
    AuthRequest::from_parts(
        HttpMethod::Post,
        "/sign-up/email".to_string(),
        HashMap::new(),
        Some(body.to_string().into_bytes()),
        HashMap::new(),
    )
}

fn create_signup_request_without_name(email: &str, password: &str) -> AuthRequest {
    let body = serde_json::json!({
        "email": email,
        "password": password,
    });
    AuthRequest::from_parts(
        HttpMethod::Post,
        "/sign-up/email".to_string(),
        HashMap::new(),
        Some(body.to_string().into_bytes()),
        HashMap::new(),
    )
}

fn create_signin_request(email: &str, password: &str) -> AuthRequest {
    let body = serde_json::json!({
        "email": email,
        "password": password,
    });
    AuthRequest::from_parts(
        HttpMethod::Post,
        "/sign-in/email".to_string(),
        HashMap::new(),
        Some(body.to_string().into_bytes()),
        HashMap::new(),
    )
}

async fn wait_for_audit_logs(ctx: &AuthContext<MemoryDatabaseAdapter>) -> Vec<AdminAuditLog> {
    for _ in 0..50 {
        let logs = ctx.database.list_admin_audit_logs(5, 0).await.unwrap();
        if !logs.is_empty() {
            return logs;
        }
        tokio::time::sleep(std::time::Duration::from_millis(10)).await;
    }

    ctx.database.list_admin_audit_logs(5, 0).await.unwrap()
}

async fn wait_for_password_hash_change(
    ctx: &AuthContext<MemoryDatabaseAdapter>,
    email: &str,
    original_hash: &str,
) -> String {
    for _ in 0..50 {
        let user = ctx
            .database
            .get_user_by_email(email)
            .await
            .unwrap()
            .unwrap();
        let current_hash = user.password_hash().unwrap().to_string();
        if current_hash != original_hash {
            return current_hash;
        }
        tokio::time::sleep(std::time::Duration::from_millis(10)).await;
    }

    ctx.database
        .get_user_by_email(email)
        .await
        .unwrap()
        .unwrap()
        .password_hash()
        .unwrap()
        .to_string()
}

#[tokio::test]
async fn test_auto_sign_in_false_returns_no_session() {
    let plugin = EmailPasswordPlugin::new().auto_sign_in(false);
    let ctx = create_test_context();

    let req = create_signup_request("auto@example.com", "Password123!");
    let response = plugin.handle_sign_up(&req, &ctx).await.unwrap();
    assert_eq!(response.status, 200);

    // Response should NOT have a Set-Cookie header
    let has_cookie = response
        .headers
        .iter()
        .any(|(k, _)| k.eq_ignore_ascii_case("Set-Cookie"));
    assert!(!has_cookie, "auto_sign_in=false should not set a cookie");

    // Response body token should be null
    let body: serde_json::Value = serde_json::from_slice(&response.body).unwrap();
    assert!(
        body["token"].is_null(),
        "auto_sign_in=false should return null token"
    );
    // But the user should still be created
    assert!(body["user"]["id"].is_string());
}

#[tokio::test]
async fn test_auto_sign_in_true_returns_session() {
    let plugin = EmailPasswordPlugin::new(); // default auto_sign_in=true
    let ctx = create_test_context();

    let req = create_signup_request("autotrue@example.com", "Password123!");
    let response = plugin.handle_sign_up(&req, &ctx).await.unwrap();
    assert_eq!(response.status, 200);

    // Response SHOULD have a Set-Cookie header
    let has_cookie = response
        .headers
        .iter()
        .any(|(k, _)| k.eq_ignore_ascii_case("Set-Cookie"));
    assert!(has_cookie, "auto_sign_in=true should set a cookie");

    // Response body token should be a string
    let body: serde_json::Value = serde_json::from_slice(&response.body).unwrap();
    assert!(
        body["token"].is_string(),
        "auto_sign_in=true should return a session token"
    );
}

#[tokio::test]
async fn test_sign_up_without_name_defaults_to_email_local_part() {
    let plugin = EmailPasswordPlugin::new();
    let ctx = create_test_context();

    let req = create_signup_request_without_name("local-part@example.com", "Password123!");
    let response = plugin.handle_sign_up(&req, &ctx).await.unwrap();
    assert_eq!(response.status, 200);

    let body: serde_json::Value = serde_json::from_slice(&response.body).unwrap();
    assert_eq!(body["user"]["name"], "local-part");
}

#[tokio::test]
async fn test_sign_in_response_contains_last_sign_in_after_first_session() {
    let plugin = EmailPasswordPlugin::new().auto_sign_in(false);
    let ctx = create_test_context();

    let signup_req = create_signup_request("firstsignin@example.com", "Password123!");
    let signup_response = plugin.handle_sign_up(&signup_req, &ctx).await.unwrap();
    assert_eq!(signup_response.status, 200);

    let signin_req = create_signin_request("firstsignin@example.com", "Password123!");
    let signin_response = plugin.handle_sign_in(&signin_req, &ctx).await.unwrap();
    assert_eq!(signin_response.status, 200);

    let body: serde_json::Value = serde_json::from_slice(&signin_response.body).unwrap();
    assert!(
        body["user"]["lastSignInAt"].as_str().is_some(),
        "sign-in response should include a populated user.lastSignInAt after first interactive session"
    );
}

#[tokio::test]
async fn test_password_max_length_rejection() {
    let plugin = EmailPasswordPlugin::new().password_max_length(128);
    let ctx = create_test_context();

    // Password of exactly 129 chars should be rejected
    let long_password = format!("A1!{}", "a".repeat(126)); // 129 chars total
    let req = create_signup_request("long@example.com", &long_password);
    let err = plugin.handle_sign_up(&req, &ctx).await.unwrap_err();
    assert_eq!(err.status_code(), 400);

    // Password of exactly 128 chars should be accepted
    let ok_password = format!("A1!{}", "a".repeat(125)); // 128 chars total
    let req = create_signup_request("ok@example.com", &ok_password);
    let response = plugin.handle_sign_up(&req, &ctx).await.unwrap();
    assert_eq!(response.status, 200);
}

#[tokio::test]
async fn test_custom_password_hasher() {
    /// A simple test hasher that prefixes the password with "hashed:"
    struct TestHasher;

    #[async_trait]
    impl PasswordHasher for TestHasher {
        async fn hash(&self, password: &str) -> AuthResult<String> {
            Ok(format!("hashed:{}", password))
        }
        async fn verify(&self, hash: &str, password: &str) -> AuthResult<bool> {
            Ok(hash == format!("hashed:{}", password))
        }
    }

    let hasher: Arc<dyn PasswordHasher> = Arc::new(TestHasher);
    let plugin = EmailPasswordPlugin::new().password_hasher(hasher);
    let ctx = create_test_context();

    // Sign up with custom hasher
    let req = create_signup_request("hasher@example.com", "Password123!");
    let response = plugin.handle_sign_up(&req, &ctx).await.unwrap();
    assert_eq!(response.status, 200);

    // Verify the stored hash uses our custom hasher
    let user = ctx
        .database
        .get_user_by_email("hasher@example.com")
        .await
        .unwrap()
        .unwrap();
    let stored_hash = user
        .metadata
        .get(PASSWORD_HASH_KEY)
        .unwrap()
        .as_str()
        .unwrap();
    assert_eq!(stored_hash, "hashed:Password123!");

    // Sign in should work with the custom hasher
    let signin_body = serde_json::json!({
        "email": "hasher@example.com",
        "password": "Password123!",
    });
    let signin_req = AuthRequest::from_parts(
        HttpMethod::Post,
        "/sign-in/email".to_string(),
        HashMap::new(),
        Some(signin_body.to_string().into_bytes()),
        HashMap::new(),
    );
    let response = plugin.handle_sign_in(&signin_req, &ctx).await.unwrap();
    assert_eq!(response.status, 200);

    // Sign in with wrong password should fail
    let bad_body = serde_json::json!({
        "email": "hasher@example.com",
        "password": "WrongPassword!",
    });
    let bad_req = AuthRequest::from_parts(
        HttpMethod::Post,
        "/sign-in/email".to_string(),
        HashMap::new(),
        Some(bad_body.to_string().into_bytes()),
        HashMap::new(),
    );
    let err = plugin.handle_sign_in(&bad_req, &ctx).await.unwrap_err();
    assert_eq!(err.to_string(), AuthError::InvalidCredentials.to_string());
}

#[tokio::test]
async fn test_sign_in_writes_success_audit_log() {
    let plugin = EmailPasswordPlugin::new().auto_sign_in(false);
    let ctx = create_test_context();

    let signup_req = create_signup_request("audit-success@example.com", "Password123!");
    let signup_response = plugin.handle_sign_up(&signup_req, &ctx).await.unwrap();
    assert_eq!(signup_response.status, 200);

    let signin_req = create_signin_request("audit-success@example.com", "Password123!");
    let signin_response = plugin.handle_sign_in(&signin_req, &ctx).await.unwrap();
    assert_eq!(signin_response.status, 200);

    let logs = wait_for_audit_logs(&ctx).await;
    assert!(
        !logs.is_empty(),
        "sign-in should write at least one audit row"
    );
    let log = &logs[0];
    assert_eq!(log.action, "auth.sign_in.email");
    assert!(log.success);
    assert_eq!(log.target_type.as_deref(), Some("user_id"));
    assert_eq!(log.metadata["source"], serde_json::json!("auth.sign_in"));
}

#[tokio::test]
async fn test_sign_in_unknown_email_writes_failure_audit_log() {
    let plugin = EmailPasswordPlugin::new();
    let ctx = create_test_context();

    let signin_req = create_signin_request("missing-audit@example.com", "Password123!");
    let err = plugin.handle_sign_in(&signin_req, &ctx).await.unwrap_err();
    assert_eq!(err.to_string(), AuthError::InvalidCredentials.to_string());

    let logs = wait_for_audit_logs(&ctx).await;
    assert!(
        !logs.is_empty(),
        "failed sign-in should write at least one audit row"
    );
    let log = &logs[0];
    assert_eq!(log.action, "auth.sign_in.email");
    assert!(!log.success);
    assert_eq!(log.target_type.as_deref(), Some("email"));
    assert_eq!(log.target_id.as_deref(), Some("missing-audit@example.com"));
    assert_eq!(log.error_message.as_deref(), Some("Invalid credentials"));
}

#[tokio::test]
async fn test_sign_in_two_factor_challenge_writes_unsuccessful_audit_log() {
    let plugin = EmailPasswordPlugin::new().auto_sign_in(false);
    let ctx = create_test_context();

    let signup_req = create_signup_request("audit-2fa@example.com", "Password123!");
    let signup_response = plugin.handle_sign_up(&signup_req, &ctx).await.unwrap();
    assert_eq!(signup_response.status, 200);

    let user = ctx
        .database
        .get_user_by_email("audit-2fa@example.com")
        .await
        .unwrap()
        .unwrap();
    ctx.database
        .update_user(
            user.id(),
            UpdateUser {
                two_factor_enabled: Some(true),
                ..Default::default()
            },
        )
        .await
        .unwrap();

    let signin_req = create_signin_request("audit-2fa@example.com", "Password123!");
    let signin_response = plugin.handle_sign_in(&signin_req, &ctx).await.unwrap();
    assert_eq!(signin_response.status, 200);

    let body: serde_json::Value = serde_json::from_slice(&signin_response.body).unwrap();
    assert_eq!(body["twoFactorRedirect"], true);
    assert!(body["token"].is_string());

    let logs = wait_for_audit_logs(&ctx).await;
    assert!(
        !logs.is_empty(),
        "2FA challenge sign-in should write at least one audit row"
    );
    let log = &logs[0];
    assert_eq!(log.action, "auth.sign_in.email");
    assert!(!log.success);
    assert_eq!(log.actor_session_token, None);
    assert_eq!(
        log.error_message.as_deref(),
        Some("Two-factor authentication required")
    );
    assert_eq!(log.metadata["details"]["twoFactorRequired"], true);
}

#[tokio::test]
async fn test_sign_in_rehashes_legacy_argon2_hash_in_background() {
    let plugin = EmailPasswordPlugin::new();
    let mut config = AuthConfig::new("test-secret-key-at-least-32-chars-long");
    config.password.argon2_config = Argon2Config {
        memory_cost: 1024,
        time_cost: 2,
        parallelism: 2,
    };
    let ctx = create_test_context_with_config(config);

    let legacy_hash = Argon2::default()
        .hash_password("Password123!".as_bytes(), &SaltString::generate(&mut OsRng))
        .unwrap()
        .to_string();
    let user = ctx
        .database
        .create_user(
            CreateUser::new()
                .with_email("rehash@example.com")
                .with_name("Rehash User")
                .with_metadata(serde_json::json!({
                    PASSWORD_HASH_KEY: legacy_hash,
                })),
        )
        .await
        .unwrap();

    let signin_req = create_signin_request("rehash@example.com", "Password123!");
    let signin_response = plugin.handle_sign_in(&signin_req, &ctx).await.unwrap();
    assert_eq!(signin_response.status, 200);

    let upgraded_hash =
        wait_for_password_hash_change(&ctx, "rehash@example.com", user.password_hash().unwrap())
            .await;
    let parsed_hash = PasswordHash::new(&upgraded_hash).unwrap();

    assert_eq!(parsed_hash.params.get_decimal("m").unwrap(), 1024);
    assert_eq!(parsed_hash.params.get_decimal("t").unwrap(), 2);
    assert_eq!(parsed_hash.params.get_decimal("p").unwrap(), 2);
}
