//! Generator integration tests — unbiased sampling (smoke), BIP39 sanity,
//! zxcvbn strength gate.
//!
//! # Note on length cap
//!
//! `generate_password` enforces `length <= 128`. The task originally specified
//! `length: 10_000` in a single call, but that would error at runtime.
//!
//! We use **Option 1 (aggregation)**: call `generate_password` 80 times with
//! `length: 128` to gather 10,240 characters total, then aggregate per-class
//! counts before asserting proportions. The ±5pp tolerance is unchanged because
//! sample size is the same (~10k chars).

use relicario_core::{
    Capitalization, CharClasses, GeneratorRequest, SymbolCharset,
    generate_passphrase, generate_password, validate_passphrase_strength,
};

#[test]
fn random_password_class_balance_is_reasonable() {
    // Aggregate 80 × 128 = 10,240 chars so we have enough for tight statistics.
    // (generate_password caps at length 128, so we cannot do a single 10,000-char call.)
    let req = GeneratorRequest::Random {
        length: 128,
        classes: CharClasses { lower: true, upper: true, digits: true, symbols: true },
        symbol_charset: SymbolCharset::SafeOnly,
    };

    let mut lower = 0usize;
    let mut upper = 0usize;
    let mut digits = 0usize;
    let mut total = 0usize;

    for _ in 0..80 {
        let pw = generate_password(&req).unwrap();
        lower  += pw.chars().filter(|c| c.is_ascii_lowercase()).count();
        upper  += pw.chars().filter(|c| c.is_ascii_uppercase()).count();
        digits += pw.chars().filter(|c| c.is_ascii_digit()).count();
        total  += pw.len();
    }
    let symbols = total - lower - upper - digits;

    // Charset sizes: lower 26 + upper 26 + digits 10 + safe_symbols 12 = 74
    // Expected proportions: 26/74 ≈ 35.1%, 10/74 ≈ 13.5%, 12/74 ≈ 16.2%
    // Allow ±5pp slop.
    let t = total as f64;
    let assert_pct = |label: &str, actual: usize, expected_pct: f64| {
        let pct = (actual as f64) / t * 100.0;
        assert!(
            (pct - expected_pct).abs() < 5.0,
            "{label}: actual {pct:.1}% vs expected {expected_pct:.1}%"
        );
    };
    assert_pct("lower",   lower,   26.0 / 74.0 * 100.0);
    assert_pct("upper",   upper,   26.0 / 74.0 * 100.0);
    assert_pct("digits",  digits,  10.0 / 74.0 * 100.0);
    assert_pct("symbols", symbols, 12.0 / 74.0 * 100.0);
}

#[test]
fn bip39_5_word_passphrase_passes_zxcvbn_gate() {
    let req = GeneratorRequest::Bip39 {
        word_count: 5,
        separator: " ".into(),
        capitalization: Capitalization::Lower,
    };
    let pw = generate_passphrase(&req).unwrap();
    validate_passphrase_strength(&pw).expect("5-word bip39 should pass score >= 3");
}

#[test]
fn common_weak_passphrases_fail_gate() {
    for weak in &["password", "12345678", "letmein", "qwertyui", "hunter2"] {
        assert!(
            validate_passphrase_strength(weak).is_err(),
            "expected '{weak}' to fail gate"
        );
    }
}

#[test]
fn random_passwords_are_unique_across_calls() {
    let req = GeneratorRequest::default();
    let mut seen = std::collections::HashSet::new();
    for _ in 0..1000 {
        let pw = generate_password(&req).unwrap();
        assert!(seen.insert(pw.as_str().to_owned()));
    }
}
