#![cfg(feature = "mfa")]
#![allow(
    clippy::useless_vec,
    clippy::useless_conversion,
    clippy::map_identity,
    unused_variables
)]
//! Comprehensive MFA (Multi-Factor Authentication) Tests
//!
//! Coverage: TOTP (RFC 6238), Backup Codes, Enrollment, Verification, Security
//! Test Count: 55 tests targeting 95%+ coverage
//!
//! Test Categories:
//! 1. TOTP Advanced (15 tests) - Replay attacks, drift tolerance, time boundaries
//! 2. Backup Code Security (12 tests) - One-time use, exhaustion, format validation
//! 3. Enrollment Flow (10 tests) - State machine, session expiration, max attempts
//! 4. Verification Flow (10 tests) - Account lockout, metadata, mixed methods
//! 5. Integration & Security (8 tests) - End-to-end, lifecycle, attack scenarios

use chrono::{Duration, Utc};
use uuid::Uuid;

// Import MFA modules from api
use api::auth::mfa::{
    backup_codes::{hash_backup_code, BackupCodeGenerator},
    enrollment::{EnrollmentSession, EnrollmentStatus, MfaEnrollment},
    totp::{TotpGenerator, TotpVerifier},
    verification::{MfaVerification, VerificationMetadata, VerificationMethod, VerificationResult},
};
use secrecy::{ExposeSecret, SecretString};

// ============================================================================
// SECTION 1: TOTP ADVANCED TESTS (15 tests)
// ============================================================================

#[test]
fn test_totp_replay_attack_prevention() {
    let generator = TotpGenerator::new();
    let verifier = TotpVerifier::new();
    let secret = "JBSWY3DPEHPK3PXP";
    let time = 1234567890u64;

    // Generate code
    let code = generator.generate_code_at_time(secret, time).unwrap();

    // First verification succeeds
    assert!(verifier.verify_at_time(secret, &code, time, 1).unwrap());

    // In a real system, we'd track used codes to prevent replay
    // This test documents that the current implementation allows replay
    // within the same time window (expected behavior per RFC 6238)
    assert!(verifier.verify_at_time(secret, &code, time, 1).unwrap());

    // However, code should fail outside drift tolerance
    let future_time = time + 90; // 3 periods later
    assert!(!verifier
        .verify_at_time(secret, &code, future_time, 1)
        .unwrap());
}

#[test]
fn test_totp_time_boundary_conditions() {
    let generator = TotpGenerator::new();
    let verifier = TotpVerifier::new();
    let secret = "JBSWY3DPEHPK3PXP";

    // Test at exact period boundary (time % 30 == 0)
    let boundary_time = 1234567890u64; // Divisible by 30
    assert_eq!(boundary_time % 30, 0);

    let code = generator
        .generate_code_at_time(secret, boundary_time)
        .unwrap();
    assert!(verifier
        .verify_at_time(secret, &code, boundary_time, 1)
        .unwrap());

    // Code should work 1 second before period ends
    let near_end = boundary_time + 29;
    assert!(verifier.verify_at_time(secret, &code, near_end, 1).unwrap());

    // Code should work at next period boundary with drift=1
    let next_boundary = boundary_time + 30;
    assert!(verifier
        .verify_at_time(secret, &code, next_boundary, 1)
        .unwrap());
}

#[test]
fn test_totp_invalid_base32_secret() {
    let generator = TotpGenerator::new();

    // Invalid Base32 characters (0, 1, 8, 9 are valid in some alphabets but not RFC 4648)
    let invalid_secret = "INVALID!@#$%";
    let result = generator.generate_code(invalid_secret);
    assert!(result.is_err(), "Should reject invalid Base32 secret");

    // Empty secret
    let empty_secret = "";
    let result = generator.generate_code(empty_secret);
    assert!(result.is_err(), "Should reject empty secret");
}

#[test]
fn test_totp_excessive_drift_tolerance() {
    let generator = TotpGenerator::new();
    let verifier = TotpVerifier::new();
    let secret = "JBSWY3DPEHPK3PXP";
    let base_time = 1234567890u64;

    let code = generator.generate_code_at_time(secret, base_time).unwrap();

    // With drift=2, should accept ±60 seconds
    assert!(verifier
        .verify_at_time(secret, &code, base_time + 60, 2)
        .unwrap());
    assert!(verifier
        .verify_at_time(secret, &code, base_time - 60, 2)
        .unwrap());

    // Should reject beyond drift=2 (±90 seconds)
    assert!(!verifier
        .verify_at_time(secret, &code, base_time + 90, 2)
        .unwrap());
    assert!(!verifier
        .verify_at_time(secret, &code, base_time - 90, 2)
        .unwrap());
}

#[test]
fn test_totp_zero_drift_strict_validation() {
    let generator = TotpGenerator::new();
    let verifier = TotpVerifier::new();
    let secret = "JBSWY3DPEHPK3PXP";
    let time = 1234567890u64;

    let code = generator.generate_code_at_time(secret, time).unwrap();

    // With drift=0, only exact time window works
    assert!(verifier.verify_at_time(secret, &code, time, 0).unwrap());
    assert!(verifier
        .verify_at_time(secret, &code, time + 15, 0)
        .unwrap()); // Same period

    // Even 1 period off fails with drift=0
    assert!(!verifier
        .verify_at_time(secret, &code, time + 30, 0)
        .unwrap());
    assert!(!verifier
        .verify_at_time(secret, &code, time - 30, 0)
        .unwrap());
}

#[test]
fn test_totp_future_code_rejection() {
    let generator = TotpGenerator::new();
    let verifier = TotpVerifier::new();
    let secret = "JBSWY3DPEHPK3PXP";
    let current_time = 1234567890u64;

    // Generate code for future time
    let future_time = current_time + 120; // 4 periods ahead
    let future_code = generator
        .generate_code_at_time(secret, future_time)
        .unwrap();

    // Should reject future code even with drift=1
    assert!(!verifier
        .verify_at_time(secret, &future_code, current_time, 1)
        .unwrap());

    // Should reject even with drift=2
    assert!(!verifier
        .verify_at_time(secret, &future_code, current_time, 2)
        .unwrap());
}

#[test]
fn test_totp_past_code_expiration() {
    let generator = TotpGenerator::new();
    let verifier = TotpVerifier::new();
    let secret = "JBSWY3DPEHPK3PXP";
    let current_time = 1234567890u64;

    // Generate code for past time
    let past_time = current_time - 120; // 4 periods ago
    let past_code = generator.generate_code_at_time(secret, past_time).unwrap();

    // Should reject past code outside drift tolerance
    assert!(!verifier
        .verify_at_time(secret, &past_code, current_time, 1)
        .unwrap());

    // Should still reject with drift=2 (only covers ±60 seconds)
    assert!(!verifier
        .verify_at_time(secret, &past_code, current_time, 2)
        .unwrap());
}

#[test]
fn test_totp_qr_uri_special_characters() {
    let generator = TotpGenerator::new();
    let secret = SecretString::new("JBSWY3DPEHPK3PXP".to_string().into());

    // Test with special characters in issuer and account
    let uri = generator
        .generate_qr_uri(&secret, "Foxhunt HFT (Test)", "user+test@example.com")
        .unwrap();

    // Should URL-encode special characters
    assert!(uri.contains("otpauth://totp/"));
    assert!(uri.contains("secret=JBSWY3DPEHPK3PXP"));
    assert!(uri.contains("%28")); // '(' encoded
    assert!(uri.contains("%29")); // ')' encoded
    assert!(uri.contains("%2B")); // '+' encoded
}

#[test]
fn test_totp_6_vs_8_digit_codes() {
    let generator = TotpGenerator::new();
    let secret = "JBSWY3DPEHPK3PXP";
    let time = 1234567890u64;

    // Default is 6 digits
    let code_6 = generator.generate_code_at_time(secret, time).unwrap();
    assert_eq!(code_6.len(), 6);
    assert!(code_6.chars().all(|c| c.is_ascii_digit()));

    // 8 digit configuration (would require modifying TotpConfig)
    // This test documents current behavior - only 6 digits supported
    // In future, could test with TotpConfig { digits: 8, .. }
}

#[test]
fn test_totp_hmac_determinism() {
    let generator = TotpGenerator::new();
    let secret = "JBSWY3DPEHPK3PXP";
    let time = 1234567890u64;

    // Same secret and time should always produce same code
    let code1 = generator.generate_code_at_time(secret, time).unwrap();
    let code2 = generator.generate_code_at_time(secret, time).unwrap();
    let code3 = generator.generate_code_at_time(secret, time).unwrap();

    assert_eq!(code1, code2);
    assert_eq!(code2, code3);
}

#[test]
fn test_totp_different_secrets_different_codes() {
    let generator = TotpGenerator::new();
    let time = 1234567890u64;

    let secret1 = "JBSWY3DPEHPK3PXP";
    let secret2 = "HXDMVJECJJWSRB3H";

    let code1 = generator.generate_code_at_time(secret1, time).unwrap();
    let code2 = generator.generate_code_at_time(secret2, time).unwrap();

    assert_ne!(
        code1, code2,
        "Different secrets should produce different codes"
    );
}

#[test]
fn test_totp_secret_generation_uniqueness() {
    let generator = TotpGenerator::new();

    let secret1 = generator.generate_secret().unwrap();
    let secret2 = generator.generate_secret().unwrap();
    let secret3 = generator.generate_secret().unwrap();

    // Should generate unique secrets
    assert_ne!(secret1.expose_secret(), secret2.expose_secret());
    assert_ne!(secret2.expose_secret(), secret3.expose_secret());
    assert_ne!(secret1.expose_secret(), secret3.expose_secret());

    // All should be valid Base32
    assert!(base32::decode(
        base32::Alphabet::Rfc4648 { padding: false },
        secret1.expose_secret()
    )
    .is_some());
}

#[test]
fn test_totp_code_at_period_transition() {
    let generator = TotpGenerator::new();
    let verifier = TotpVerifier::new();
    let secret = "JBSWY3DPEHPK3PXP";

    // Test exactly at period transition
    let period_start = 1234567890u64;
    let period_end = period_start + 29; // Last second of period
    let next_period = period_start + 30;

    let code = generator
        .generate_code_at_time(secret, period_start)
        .unwrap();

    // Should work at start and end of same period
    assert!(verifier
        .verify_at_time(secret, &code, period_start, 0)
        .unwrap());
    assert!(verifier
        .verify_at_time(secret, &code, period_end, 0)
        .unwrap());

    // Should fail at next period with drift=0
    assert!(!verifier
        .verify_at_time(secret, &code, next_period, 0)
        .unwrap());

    // Should work at next period with drift=1
    assert!(verifier
        .verify_at_time(secret, &code, next_period, 1)
        .unwrap());
}

#[test]
fn test_totp_verifier_time_remaining() {
    let verifier = TotpVerifier::new();

    let remaining = verifier.time_remaining();

    // Should be between 1 and 30 seconds (never 0 due to rounding)
    assert!(remaining > 0, "Time remaining should be > 0");
    assert!(remaining <= 30, "Time remaining should be <= 30");
}

#[test]
fn test_totp_constant_time_comparison() {
    let verifier = TotpVerifier::new();
    let secret = "JBSWY3DPEHPK3PXP";
    let time = 1234567890u64;

    // This test verifies constant-time comparison indirectly
    // by ensuring timing doesn't leak information

    // Valid code
    let generator = TotpGenerator::new();
    let valid_code = generator.generate_code_at_time(secret, time).unwrap();

    // Nearly-valid code (differs by 1 digit at different positions)
    let almost_code1 = format!("{}23456", &valid_code[0..0]); // Wrong from start
    let almost_code2 = format!("{}3456", &valid_code[0..1]); // Wrong from position 1

    // All should take similar time (constant-time comparison)
    assert!(verifier
        .verify_at_time(secret, &valid_code, time, 1)
        .unwrap());
    assert!(!verifier
        .verify_at_time(secret, &almost_code1, time, 1)
        .unwrap());
    assert!(!verifier
        .verify_at_time(secret, &almost_code2, time, 1)
        .unwrap());
}

// ============================================================================
// SECTION 2: BACKUP CODE SECURITY TESTS (12 tests)
// ============================================================================

#[test]
fn test_backup_code_generation_count() {
    let generator = BackupCodeGenerator::new();

    // Valid counts (1-20)
    assert!(generator.generate_codes(1).is_ok());
    assert!(generator.generate_codes(10).is_ok());
    assert!(generator.generate_codes(20).is_ok());

    // Invalid counts
    assert!(generator.generate_codes(0).is_err());
    assert!(generator.generate_codes(21).is_err());
    assert!(generator.generate_codes(100).is_err());
}

#[test]
fn test_backup_code_format_normalization() {
    // Test the normalization function directly
    use api::auth::mfa::backup_codes::BackupCodeGenerator;

    let generator = BackupCodeGenerator::new();
    let codes = generator.generate_codes(1).unwrap();
    let code = &codes[0];

    // Display format should have hyphens
    assert!(code.display.contains('-'));
    assert_eq!(code.display.split('-').count(), 4); // 4 groups

    // Hint should be first 4 characters
    assert_eq!(code.hint.len(), 4);
    assert_eq!(&code.hint, &code.expose()[0..4]);

    // Code itself should be 16 characters
    assert_eq!(code.expose().len(), 16);
}

#[test]
fn test_backup_code_charset_validation() {
    let generator = BackupCodeGenerator::new();
    let codes = generator.generate_codes(10).unwrap();

    for code in &codes {
        let code_str = code.expose();

        // Should only contain allowed characters (no 0, O, 1, I, l)
        for c in code_str.chars() {
            assert!(
                c.is_ascii_uppercase() || c.is_ascii_digit(),
                "Invalid character: {}",
                c
            );
            assert_ne!(c, '0', "Should not contain 0");
            assert_ne!(c, 'O', "Should not contain O");
            assert_ne!(c, '1', "Should not contain 1");
            assert_ne!(c, 'I', "Should not contain I");
        }
    }
}

#[test]
fn test_backup_code_uniqueness() {
    let generator = BackupCodeGenerator::new();
    let codes = generator.generate_codes(20).unwrap();

    // All codes should be unique
    let mut seen = std::collections::HashSet::new();
    for code in &codes {
        assert!(
            seen.insert(code.expose().to_string()),
            "Duplicate code generated: {}",
            code.expose()
        );
    }
}

#[test]
fn test_backup_code_hash_uniqueness() {
    let codes = vec!["ABCD2345EFGH6789", "ABCD2345EFGH678A", "DIFFERENT23456789"];

    let hash1 = hash_backup_code(codes[0]);
    let hash2 = hash_backup_code(codes[1]);
    let hash3 = hash_backup_code(codes[2]);

    // Different codes should have different hashes
    assert_ne!(hash1, hash2);
    assert_ne!(hash2, hash3);
    assert_ne!(hash1, hash3);

    // Same code should have same hash (determinism)
    assert_eq!(hash_backup_code(codes[0]), hash1);
}

#[test]
fn test_backup_code_hash_length() {
    let code = "ABCD2345EFGH6789";
    let hash = hash_backup_code(code);

    // SHA-256 produces 64 hex characters
    assert_eq!(hash.len(), 64);
    assert!(hash.chars().all(|c| c.is_ascii_hexdigit()));
}

#[test]
fn test_backup_code_single_generation() {
    let generator = BackupCodeGenerator::new();
    let code = generator.generate_single_code().unwrap();

    assert_eq!(code.expose().len(), 16);
    assert_eq!(code.hint.len(), 4);
    assert!(code.display.contains('-'));
}

#[test]
fn test_backup_code_display_formatting() {
    let generator = BackupCodeGenerator::new();
    let codes = generator.generate_codes(5).unwrap();

    for code in &codes {
        // Display should be in format XXXX-XXXX-XXXX-XXXX
        let parts: Vec<&str> = code.display.split('-').collect();
        assert_eq!(parts.len(), 4);

        for part in parts {
            assert_eq!(part.len(), 4);
        }
    }
}

#[test]
fn test_backup_code_entropy() {
    let generator = BackupCodeGenerator::new();
    let codes = generator.generate_codes(20).unwrap();

    // Test character distribution (should be relatively uniform)
    let mut char_counts = std::collections::HashMap::new();

    for code in &codes {
        for c in code.expose().chars() {
            *char_counts.entry(c).or_insert(0) += 1;
        }
    }

    // Should have good distribution (at least 10 different characters used)
    assert!(
        char_counts.len() >= 10,
        "Should use at least 10 different characters, got {}",
        char_counts.len()
    );

    // No character should dominate (> 30% of all characters)
    let total_chars: usize = char_counts.values().sum();
    for count in char_counts.values() {
        let percentage = (*count as f64) / (total_chars as f64);
        assert!(
            percentage < 0.3,
            "Character distribution too skewed: {:.2}%",
            percentage * 100.0
        );
    }
}

#[test]
fn test_backup_code_case_insensitivity() {
    // This test documents expected behavior for case handling
    let code_upper = "ABCD2345EFGH6789";
    let code_lower = "abcd2345efgh6789";

    // Hashes should be different (case-sensitive hashing)
    let hash_upper = hash_backup_code(code_upper);
    let hash_lower = hash_backup_code(code_lower);
    assert_ne!(hash_upper, hash_lower);

    // Note: In real validation, codes are normalized to uppercase
    // before hashing, so this test documents the raw hash behavior
}

#[test]
fn test_backup_code_length_validation() {
    // Test various invalid lengths
    let too_short = "ABCD2345";
    let too_long = "ABCD2345EFGH67890123";
    let valid = "ABCD2345EFGH6789";

    // All generate 64-char hashes, but validation checks length separately
    assert_eq!(hash_backup_code(too_short).len(), 64);
    assert_eq!(hash_backup_code(too_long).len(), 64);
    assert_eq!(hash_backup_code(valid).len(), 64);
}

#[test]
fn test_backup_code_hint_accuracy() {
    let generator = BackupCodeGenerator::new();
    let codes = generator.generate_codes(10).unwrap();

    for code in &codes {
        // Hint should match first 4 characters of actual code
        let actual_first_4: String = code.expose().chars().take(4).collect();
        assert_eq!(code.hint, actual_first_4);
    }
}

// ============================================================================
// SECTION 3: ENROLLMENT FLOW TESTS (10 tests)
// ============================================================================

#[test]
fn test_enrollment_initial_state() {
    let user_id = Uuid::new_v4();
    let enrollment = MfaEnrollment::new(user_id);

    assert_eq!(enrollment.user_id, user_id);
    assert_eq!(enrollment.status, EnrollmentStatus::NotStarted);
    assert!(enrollment.session.is_none());
    assert_eq!(enrollment.verification_attempts, 0);
    assert_eq!(enrollment.max_attempts, 3);
}

#[test]
fn test_enrollment_start_session() {
    let user_id = Uuid::new_v4();
    let mut enrollment = MfaEnrollment::new(user_id);

    let session = EnrollmentSession {
        session_id: Uuid::new_v4(),
        user_id,
        qr_code_uri: "otpauth://totp/test".to_string(),
        qr_code_png: vec![1, 2, 3],
        manual_entry_key: "TEST123".to_string(),
        expires_at: Utc::now() + Duration::minutes(15),
    };

    enrollment.start(session);

    assert_eq!(enrollment.status, EnrollmentStatus::InProgress);
    assert!(enrollment.session.is_some());
    assert_eq!(enrollment.verification_attempts, 0);
}

#[test]
fn test_enrollment_complete() {
    let user_id = Uuid::new_v4();
    let mut enrollment = MfaEnrollment::new(user_id);

    // Start enrollment
    let session = EnrollmentSession {
        session_id: Uuid::new_v4(),
        user_id,
        qr_code_uri: "otpauth://totp/test".to_string(),
        qr_code_png: vec![],
        manual_entry_key: "TEST123".to_string(),
        expires_at: Utc::now() + Duration::minutes(15),
    };
    enrollment.start(session);

    // Complete enrollment
    enrollment.complete();

    assert_eq!(enrollment.status, EnrollmentStatus::Completed);
    assert!(enrollment.session.is_none());
}

#[test]
fn test_enrollment_fail() {
    let user_id = Uuid::new_v4();
    let mut enrollment = MfaEnrollment::new(user_id);

    enrollment.fail("Invalid verification code".to_string());

    match &enrollment.status {
        EnrollmentStatus::Failed(reason) => {
            assert_eq!(reason, "Invalid verification code");
        },
        _ => panic!("Expected Failed status"),
    }
    assert!(enrollment.session.is_none());
}

#[test]
fn test_enrollment_verification_attempts() {
    let user_id = Uuid::new_v4();
    let mut enrollment = MfaEnrollment::new(user_id);

    assert_eq!(enrollment.verification_attempts, 0);
    assert!(!enrollment.is_max_attempts_reached());

    enrollment.increment_attempts();
    assert_eq!(enrollment.verification_attempts, 1);
    assert!(!enrollment.is_max_attempts_reached());

    enrollment.increment_attempts();
    enrollment.increment_attempts();
    assert_eq!(enrollment.verification_attempts, 3);
    assert!(enrollment.is_max_attempts_reached());

    // Can still increment beyond max
    enrollment.increment_attempts();
    assert_eq!(enrollment.verification_attempts, 4);
    assert!(enrollment.is_max_attempts_reached());
}

#[test]
fn test_enrollment_session_not_expired() {
    let user_id = Uuid::new_v4();
    let mut enrollment = MfaEnrollment::new(user_id);

    // Session expires in future
    let session = EnrollmentSession {
        session_id: Uuid::new_v4(),
        user_id,
        qr_code_uri: "otpauth://totp/test".to_string(),
        qr_code_png: vec![],
        manual_entry_key: "TEST123".to_string(),
        expires_at: Utc::now() + Duration::hours(1),
    };
    enrollment.start(session);

    assert!(!enrollment.is_expired());
}

#[test]
fn test_enrollment_session_expired() {
    let user_id = Uuid::new_v4();
    let mut enrollment = MfaEnrollment::new(user_id);

    // Session already expired
    let session = EnrollmentSession {
        session_id: Uuid::new_v4(),
        user_id,
        qr_code_uri: "otpauth://totp/test".to_string(),
        qr_code_png: vec![],
        manual_entry_key: "TEST123".to_string(),
        expires_at: Utc::now() - Duration::hours(1),
    };
    enrollment.start(session);

    assert!(enrollment.is_expired());
}

#[test]
fn test_enrollment_no_session_not_expired() {
    let user_id = Uuid::new_v4();
    let enrollment = MfaEnrollment::new(user_id);

    // No session means not expired
    assert!(!enrollment.is_expired());
}

#[test]
fn test_enrollment_state_transitions() {
    let user_id = Uuid::new_v4();
    let mut enrollment = MfaEnrollment::new(user_id);

    // NotStarted → InProgress
    assert_eq!(enrollment.status, EnrollmentStatus::NotStarted);

    let session = EnrollmentSession {
        session_id: Uuid::new_v4(),
        user_id,
        qr_code_uri: "otpauth://totp/test".to_string(),
        qr_code_png: vec![],
        manual_entry_key: "TEST123".to_string(),
        expires_at: Utc::now() + Duration::minutes(15),
    };
    enrollment.start(session);
    assert_eq!(enrollment.status, EnrollmentStatus::InProgress);

    // InProgress → Completed
    enrollment.complete();
    assert_eq!(enrollment.status, EnrollmentStatus::Completed);

    // Can restart after completion
    let new_session = EnrollmentSession {
        session_id: Uuid::new_v4(),
        user_id,
        qr_code_uri: "otpauth://totp/test2".to_string(),
        qr_code_png: vec![],
        manual_entry_key: "TEST456".to_string(),
        expires_at: Utc::now() + Duration::minutes(15),
    };
    enrollment.start(new_session);
    assert_eq!(enrollment.status, EnrollmentStatus::InProgress);

    // InProgress → Failed
    enrollment.fail("Test failure".to_string());
    match &enrollment.status {
        EnrollmentStatus::Failed(reason) => assert_eq!(reason, "Test failure"),
        _ => panic!("Expected Failed status"),
    }
}

#[test]
fn test_enrollment_session_data_integrity() {
    let user_id = Uuid::new_v4();
    let mut enrollment = MfaEnrollment::new(user_id);

    let session_id = Uuid::new_v4();
    let qr_uri = "otpauth://totp/FoxhuntHFT:user@example.com".to_string();
    let qr_png = vec![1, 2, 3, 4, 5];
    let manual_key = "JBSWY3DPEHPK3PXP".to_string();
    let expires = Utc::now() + Duration::minutes(10);

    let session = EnrollmentSession {
        session_id,
        user_id,
        qr_code_uri: qr_uri.clone(),
        qr_code_png: qr_png.clone(),
        manual_entry_key: manual_key.clone(),
        expires_at: expires,
    };

    enrollment.start(session);

    // Verify all session data preserved
    let stored_session = enrollment.session.as_ref().expect("INVARIANT: Option should be Some");
    assert_eq!(stored_session.session_id, session_id);
    assert_eq!(stored_session.user_id, user_id);
    assert_eq!(stored_session.qr_code_uri, qr_uri);
    assert_eq!(stored_session.qr_code_png, qr_png);
    assert_eq!(stored_session.manual_entry_key, manual_key);
    assert_eq!(stored_session.expires_at, expires);
}

// ============================================================================
// SECTION 4: VERIFICATION FLOW TESTS (10 tests)
// ============================================================================

#[test]
fn test_verification_result_success_creation() {
    let user_id = Uuid::new_v4();
    let metadata = VerificationMetadata {
        failed_attempts: 0,
        is_locked: false,
        locked_until: None,
        backup_codes_remaining: Some(10),
        totp_drift: None,
    };

    let result = VerificationResult::success(user_id, VerificationMethod::Totp, metadata);

    assert!(result.success);
    assert_eq!(result.user_id, user_id);
    assert_eq!(result.method, VerificationMethod::Totp);
    assert_eq!(result.metadata.failed_attempts, 0);
    assert!(!result.metadata.is_locked);
}

#[test]
fn test_verification_result_failure_creation() {
    let user_id = Uuid::new_v4();
    let metadata = VerificationMetadata {
        failed_attempts: 3,
        is_locked: false,
        locked_until: None,
        backup_codes_remaining: Some(8),
        totp_drift: Some(1),
    };

    let result = VerificationResult::failure(user_id, VerificationMethod::Totp, metadata);

    assert!(!result.success);
    assert_eq!(result.metadata.failed_attempts, 3);
    assert_eq!(result.metadata.totp_drift, Some(1));
}

#[test]
fn test_verification_metadata_lockout_info() {
    let user_id = Uuid::new_v4();
    let locked_until = Utc::now() + Duration::minutes(15);

    let metadata = VerificationMetadata {
        failed_attempts: 5,
        is_locked: true,
        locked_until: Some(locked_until),
        backup_codes_remaining: Some(10),
        totp_drift: None,
    };

    let result = VerificationResult::failure(user_id, VerificationMethod::Totp, metadata);

    assert!(result.metadata.is_locked);
    assert_eq!(result.metadata.locked_until, Some(locked_until));
    assert_eq!(result.metadata.failed_attempts, 5);
}

#[test]
fn test_verification_backup_codes_remaining() {
    let user_id = Uuid::new_v4();

    // With backup codes
    let metadata = VerificationMetadata {
        failed_attempts: 0,
        is_locked: false,
        locked_until: None,
        backup_codes_remaining: Some(5),
        totp_drift: None,
    };
    let result = VerificationResult::success(user_id, VerificationMethod::BackupCode, metadata);
    assert_eq!(result.metadata.backup_codes_remaining, Some(5));

    // Without backup codes (TOTP verification)
    let metadata2 = VerificationMetadata {
        failed_attempts: 0,
        is_locked: false,
        locked_until: None,
        backup_codes_remaining: None,
        totp_drift: Some(0),
    };
    let result2 = VerificationResult::success(user_id, VerificationMethod::Totp, metadata2);
    assert_eq!(result2.metadata.backup_codes_remaining, None);
}

#[test]
fn test_verification_totp_drift_tracking() {
    let user_id = Uuid::new_v4();

    // No drift (exact match)
    let metadata = VerificationMetadata {
        failed_attempts: 0,
        is_locked: false,
        locked_until: None,
        backup_codes_remaining: None,
        totp_drift: Some(0),
    };
    let result = VerificationResult::success(user_id, VerificationMethod::Totp, metadata);
    assert_eq!(result.metadata.totp_drift, Some(0));

    // Drift of 1 period
    let metadata2 = VerificationMetadata {
        failed_attempts: 0,
        is_locked: false,
        locked_until: None,
        backup_codes_remaining: None,
        totp_drift: Some(1),
    };
    let result2 = VerificationResult::success(user_id, VerificationMethod::Totp, metadata2);
    assert_eq!(result2.metadata.totp_drift, Some(1));
}

#[test]
fn test_verification_method_serialization() {
    // Test JSON serialization
    let method_totp = VerificationMethod::Totp;
    let json = serde_json::to_string(&method_totp).expect("INVARIANT: Serialization should succeed for valid types");
    assert_eq!(json, "\"totp\"");

    let method_backup = VerificationMethod::BackupCode;
    let json = serde_json::to_string(&method_backup).expect("INVARIANT: Serialization should succeed for valid types");
    assert_eq!(json, "\"backup_code\"");

    let method_device = VerificationMethod::TrustedDevice;
    let json = serde_json::to_string(&method_device).expect("INVARIANT: Serialization should succeed for valid types");
    assert_eq!(json, "\"trusted_device\"");
}

#[test]
fn test_verification_timestamp_recent() {
    let user_id = Uuid::new_v4();
    let metadata = VerificationMetadata {
        failed_attempts: 0,
        is_locked: false,
        locked_until: None,
        backup_codes_remaining: Some(10),
        totp_drift: None,
    };

    let result = VerificationResult::success(user_id, VerificationMethod::Totp, metadata);

    // Timestamp should be very recent (within last second)
    let now = Utc::now();
    let diff = now.signed_duration_since(result.timestamp);
    assert!(diff.num_seconds() < 2, "Timestamp should be recent");
}

#[test]
fn test_verification_request_structure() {
    let user_id = Uuid::new_v4();

    let verification = MfaVerification {
        user_id,
        method: VerificationMethod::Totp,
        code: "123456".to_string(),
        ip_address: Some("192.168.1.100".to_string()),
        user_agent: Some("Mozilla/5.0".to_string()),
    };

    assert_eq!(verification.user_id, user_id);
    assert_eq!(verification.method, VerificationMethod::Totp);
    assert_eq!(verification.code, "123456");
    assert_eq!(verification.ip_address, Some("192.168.1.100".to_string()));
    assert_eq!(verification.user_agent, Some("Mozilla/5.0".to_string()));
}

#[test]
fn test_verification_multiple_methods() {
    let user_id = Uuid::new_v4();
    let metadata = VerificationMetadata {
        failed_attempts: 0,
        is_locked: false,
        locked_until: None,
        backup_codes_remaining: Some(10),
        totp_drift: Some(0),
    };

    // TOTP verification
    let result_totp =
        VerificationResult::success(user_id, VerificationMethod::Totp, metadata.clone());
    assert_eq!(result_totp.method, VerificationMethod::Totp);

    // Backup code verification
    let result_backup =
        VerificationResult::success(user_id, VerificationMethod::BackupCode, metadata.clone());
    assert_eq!(result_backup.method, VerificationMethod::BackupCode);

    // Trusted device verification
    let result_device =
        VerificationResult::success(user_id, VerificationMethod::TrustedDevice, metadata);
    assert_eq!(result_device.method, VerificationMethod::TrustedDevice);
}

#[test]
fn test_verification_failed_attempts_progression() {
    let user_id = Uuid::new_v4();

    // Attempt 1
    let metadata1 = VerificationMetadata {
        failed_attempts: 1,
        is_locked: false,
        locked_until: None,
        backup_codes_remaining: Some(10),
        totp_drift: None,
    };
    let result1 = VerificationResult::failure(user_id, VerificationMethod::Totp, metadata1);
    assert_eq!(result1.metadata.failed_attempts, 1);
    assert!(!result1.metadata.is_locked);

    // Attempt 3 (still not locked)
    let metadata3 = VerificationMetadata {
        failed_attempts: 3,
        is_locked: false,
        locked_until: None,
        backup_codes_remaining: Some(10),
        totp_drift: None,
    };
    let result3 = VerificationResult::failure(user_id, VerificationMethod::Totp, metadata3);
    assert_eq!(result3.metadata.failed_attempts, 3);
    assert!(!result3.metadata.is_locked);

    // Attempt 5 (now locked)
    let locked_until = Utc::now() + Duration::minutes(15);
    let metadata5 = VerificationMetadata {
        failed_attempts: 5,
        is_locked: true,
        locked_until: Some(locked_until),
        backup_codes_remaining: Some(10),
        totp_drift: None,
    };
    let result5 = VerificationResult::failure(user_id, VerificationMethod::Totp, metadata5);
    assert_eq!(result5.metadata.failed_attempts, 5);
    assert!(result5.metadata.is_locked);
    assert!(result5.metadata.locked_until.is_some());
}

// ============================================================================
// SECTION 5: INTEGRATION & SECURITY TESTS (8 tests)
// ============================================================================

#[test]
fn test_integration_full_enrollment_flow() {
    let user_id = Uuid::new_v4();
    let generator = TotpGenerator::new();

    // Step 1: Generate TOTP secret
    let secret = generator.generate_secret().unwrap();

    // Step 2: Generate QR code URI
    let qr_uri = generator
        .generate_qr_uri(&secret, "FoxhuntHFT", "trader@example.com")
        .unwrap();
    assert!(qr_uri.starts_with("otpauth://totp/"));

    // Step 3: Create enrollment session
    let mut enrollment = MfaEnrollment::new(user_id);
    let session = EnrollmentSession {
        session_id: Uuid::new_v4(),
        user_id,
        qr_code_uri: qr_uri,
        qr_code_png: vec![],
        manual_entry_key: secret.expose_secret().to_string(),
        expires_at: Utc::now() + Duration::minutes(15),
    };
    enrollment.start(session);
    assert_eq!(enrollment.status, EnrollmentStatus::InProgress);

    // Step 4: User scans QR code and enters code
    let time = 1234567890u64;
    let code = generator
        .generate_code_at_time(secret.expose_secret(), time)
        .unwrap();

    // Step 5: Verify code
    let verifier = TotpVerifier::new();
    let is_valid = verifier
        .verify_at_time(secret.expose_secret(), &code, time, 1)
        .unwrap();
    assert!(is_valid);

    // Step 6: Complete enrollment
    enrollment.complete();
    assert_eq!(enrollment.status, EnrollmentStatus::Completed);
}

#[test]
fn test_integration_backup_codes_generation() {
    let user_id = Uuid::new_v4();
    let generator = BackupCodeGenerator::new();

    // Generate 10 backup codes during enrollment
    let codes = generator.generate_codes(10).unwrap();
    assert_eq!(codes.len(), 10);

    // All codes should be valid format
    for code in &codes {
        assert_eq!(code.expose().len(), 16);
        assert_eq!(code.hint.len(), 4);
        assert!(code.display.contains('-'));
    }

    // Hash all codes for storage
    let hashed_codes: Vec<String> = codes.iter().map(|c| hash_backup_code(c.expose())).collect();

    // All hashes should be unique
    let unique_hashes: std::collections::HashSet<_> = hashed_codes.iter().collect();
    assert_eq!(unique_hashes.len(), 10);
}

#[test]
fn test_integration_totp_and_backup_codes() {
    let user_id = Uuid::new_v4();

    // TOTP setup
    let totp_generator = TotpGenerator::new();
    let totp_secret = totp_generator.generate_secret().unwrap();

    // Backup codes setup
    let backup_generator = BackupCodeGenerator::new();
    let backup_codes = backup_generator.generate_codes(10).unwrap();

    // Both should be independent
    assert_ne!(
        totp_secret.expose_secret().len(),
        backup_codes[0].expose().len()
    );

    // TOTP verification
    let time = 1234567890u64;
    let totp_code = totp_generator
        .generate_code_at_time(totp_secret.expose_secret(), time)
        .unwrap();
    let verifier = TotpVerifier::new();
    assert!(verifier
        .verify_at_time(totp_secret.expose_secret(), &totp_code, time, 1)
        .unwrap());

    // Backup codes should be available as fallback
    assert_eq!(backup_codes.len(), 10);
}

#[test]
fn test_security_timing_attack_resistance() {
    let verifier = TotpVerifier::new();
    let secret = "JBSWY3DPEHPK3PXP";
    let time = 1234567890u64;

    let generator = TotpGenerator::new();
    let valid_code = generator.generate_code_at_time(secret, time).unwrap();

    // All verifications should take similar time (constant-time comparison)
    let wrong_codes = vec![
        "000000".to_string(),                  // All wrong
        format!("{}00000", &valid_code[0..1]), // First digit correct
        format!("{}0000", &valid_code[0..2]),  // First two correct
        format!("{}000", &valid_code[0..3]),   // First three correct
        format!("{}00", &valid_code[0..4]),    // First four correct
        format!("{}0", &valid_code[0..5]),     // First five correct
    ];

    for wrong_code in &wrong_codes {
        let result = verifier
            .verify_at_time(secret, wrong_code, time, 1)
            .unwrap();
        assert!(!result, "Wrong code should fail: {}", wrong_code);
    }

    // Valid code should succeed
    assert!(verifier
        .verify_at_time(secret, &valid_code, time, 1)
        .unwrap());
}

#[test]
fn test_security_backup_code_entropy_analysis() {
    let generator = BackupCodeGenerator::new();

    // Generate large sample for entropy analysis
    let codes = generator.generate_codes(20).unwrap();

    // Collect all characters
    let mut all_chars = String::new();
    for code in &codes {
        all_chars.push_str(code.expose());
    }

    // Calculate character frequency
    let mut freq_map = std::collections::HashMap::new();
    for c in all_chars.chars() {
        *freq_map.entry(c).or_insert(0) += 1;
    }

    // Shannon entropy calculation
    let total = all_chars.len() as f64;
    let mut entropy = 0.0;
    for count in freq_map.values() {
        let p = (*count as f64) / total;
        entropy -= p * p.log2();
    }

    // Entropy should be > 4.0 bits per character (good randomness)
    // Theoretical maximum for charset of 32 characters is 5.0 bits
    assert!(
        entropy > 4.0,
        "Entropy too low: {:.2} bits per character",
        entropy
    );
}

#[test]
fn test_security_session_expiration_enforcement() {
    let user_id = Uuid::new_v4();
    let mut enrollment = MfaEnrollment::new(user_id);

    // Create session that expires in 1 second
    let session = EnrollmentSession {
        session_id: Uuid::new_v4(),
        user_id,
        qr_code_uri: "otpauth://totp/test".to_string(),
        qr_code_png: vec![],
        manual_entry_key: "TEST123".to_string(),
        expires_at: Utc::now() + Duration::seconds(1),
    };
    enrollment.start(session);

    // Initially not expired
    assert!(!enrollment.is_expired());

    // Wait 2 seconds
    std::thread::sleep(std::time::Duration::from_secs(2));

    // Now should be expired
    assert!(enrollment.is_expired());
}

#[test]
fn test_security_qr_code_uri_injection() {
    let generator = TotpGenerator::new();
    let secret = SecretString::new("JBSWY3DPEHPK3PXP".to_string().into());

    // Try injection attacks in issuer/account
    let malicious_issuer = "FoxhuntHFT&secret=HACKED";
    let malicious_account = "user@example.com?secret=STOLEN";

    let uri = generator
        .generate_qr_uri(&secret, malicious_issuer, malicious_account)
        .unwrap();

    // Should URL-encode special characters
    assert!(uri.contains("%26")); // '&' encoded
    assert!(uri.contains("%3F")); // '?' encoded

    // Original secret should still be present
    assert!(uri.contains("secret=JBSWY3DPEHPK3PXP"));

    // Malicious parts should be encoded (not interpreted)
    assert!(!uri.contains("secret=HACKED"));
    assert!(!uri.contains("secret=STOLEN"));
}

#[test]
fn test_security_hash_collision_resistance() {
    // Test that similar backup codes produce different hashes
    let codes = vec![
        "ABCD2345EFGH6789",
        "ABCD2345EFGH678A", // Last char different
        "ABCD2345EFGH679A", // Second-to-last different
        "ABCD2345EFGH689A", // Third-to-last different
    ];

    let hashes: Vec<String> = codes.iter().map(|c| hash_backup_code(c)).collect();

    // All hashes should be unique
    let unique: std::collections::HashSet<_> = hashes.iter().collect();
    assert_eq!(unique.len(), codes.len());

    // Small changes should produce completely different hashes (avalanche effect)
    let hash_diff_1_2 = hamming_distance(&hashes[0], &hashes[1]);
    let hash_diff_2_3 = hamming_distance(&hashes[1], &hashes[2]);

    // At least 30% of bits should differ (good avalanche)
    assert!(
        hash_diff_1_2 > 19,
        "Hash avalanche too weak: {} bits different",
        hash_diff_1_2
    );
    assert!(
        hash_diff_2_3 > 19,
        "Hash avalanche too weak: {} bits different",
        hash_diff_2_3
    );
}

// ============================================================================
// HELPER FUNCTIONS
// ============================================================================

/// Calculate Hamming distance between two hex strings (number of different characters)
fn hamming_distance(s1: &str, s2: &str) -> usize {
    if s1.len() != s2.len() {
        return usize::MAX;
    }

    s1.chars()
        .zip(s2.chars())
        .filter(|(c1, c2)| c1 != c2)
        .count()
}

#[test]
fn test_helper_hamming_distance() {
    assert_eq!(hamming_distance("abc", "abc"), 0);
    assert_eq!(hamming_distance("abc", "abd"), 1);
    assert_eq!(hamming_distance("abc", "xyz"), 3);
    assert_eq!(hamming_distance("abc", "abcd"), usize::MAX);
}
