//! Comprehensive test suite for the hCaptcha PoW solver
//!
//! Tests cover:
//! 1. Polynomial S-Box (all 256 entries)
//! 2. PCG-XSH-RR PRNG (known seed outputs)
//! 3. Util bit-manipulation functions
//! 4. GIFT-256 bitsliced S-Box properties
//! 5. GIFT-256 interleave round-trip
//! 6. GIFT-256 linear layer properties
//! 7. GIFT-256 key schedule structure
//! 8. GIFT-256 encrypt determinism
//! 9. Hash subsystem (GF multiply, inner_compress, finalize)
//! 10. End-to-end solver wiring

use hcaptcha_pow::pcg::PcgRng;
use hcaptcha_pow::sbox::apply_polynomial_sbox;
use hcaptcha_pow::util::*;
use hcaptcha_pow::gift256::sbox::sbox_bitsliced;
use hcaptcha_pow::gift256::interleave::*;
use hcaptcha_pow::gift256::linear::*;
use hcaptcha_pow::gift256::key_schedule::key_schedule;
use hcaptcha_pow::gift256::encrypt::encrypt;
use hcaptcha_pow::hash::mmo::mmo_compress;
use hcaptcha_pow::hash::inner_compress::inner_compress;
use hcaptcha_pow::hash::message::process_message;
use hcaptcha_pow::hash::finalize::finalize;

// ═══════════════════════════════════════════════════════════
// 1. POLYNOMIAL S-BOX TESTS
// ═══════════════════════════════════════════════════════════

/// Full 256-entry reference table computed via Python:
/// S(x) = (192*x^6 + 224*x^5 + 120*x^4 + 200*x^3 + 150*x^2 + 65*x + 147) % 256
const EXPECTED_SBOX: [u8; 256] = [
    0x93, 0x4A, 0x2D, 0x0C, 0xF7, 0x3E, 0x71, 0x60, 0x1B, 0xF2, 0x75, 0x74, 0xFF, 0x66, 0x39, 0x48,
    0xA3, 0x9A, 0xBD, 0xDC, 0x07, 0x8E, 0x01, 0x30, 0x2B, 0x42, 0x05, 0x44, 0x0F, 0xB6, 0xC9, 0x18,
    0xB3, 0xEA, 0x4D, 0xAC, 0x17, 0xDE, 0x91, 0x00, 0x3B, 0x92, 0x95, 0x14, 0x1F, 0x06, 0x59, 0xE8,
    0xC3, 0x3A, 0xDD, 0x7C, 0x27, 0x2E, 0x21, 0xD0, 0x4B, 0xE2, 0x25, 0xE4, 0x2F, 0x56, 0xE9, 0xB8,
    0xD3, 0x8A, 0x6D, 0x4C, 0x37, 0x7E, 0xB1, 0xA0, 0x5B, 0x32, 0xB5, 0xB4, 0x3F, 0xA6, 0x79, 0x88,
    0xE3, 0xDA, 0xFD, 0x1C, 0x47, 0xCE, 0x41, 0x70, 0x6B, 0x82, 0x45, 0x84, 0x4F, 0xF6, 0x09, 0x58,
    0xF3, 0x2A, 0x8D, 0xEC, 0x57, 0x1E, 0xD1, 0x40, 0x7B, 0xD2, 0xD5, 0x54, 0x5F, 0x46, 0x99, 0x28,
    0x03, 0x7A, 0x1D, 0xBC, 0x67, 0x6E, 0x61, 0x10, 0x8B, 0x22, 0x65, 0x24, 0x6F, 0x96, 0x29, 0xF8,
    0x13, 0xCA, 0xAD, 0x8C, 0x77, 0xBE, 0xF1, 0xE0, 0x9B, 0x72, 0xF5, 0xF4, 0x7F, 0xE6, 0xB9, 0xC8,
    0x23, 0x1A, 0x3D, 0x5C, 0x87, 0x0E, 0x81, 0xB0, 0xAB, 0xC2, 0x85, 0xC4, 0x8F, 0x36, 0x49, 0x98,
    0x33, 0x6A, 0xCD, 0x2C, 0x97, 0x5E, 0x11, 0x80, 0xBB, 0x12, 0x15, 0x94, 0x9F, 0x86, 0xD9, 0x68,
    0x43, 0xBA, 0x5D, 0xFC, 0xA7, 0xAE, 0xA1, 0x50, 0xCB, 0x62, 0xA5, 0x64, 0xAF, 0xD6, 0x69, 0x38,
    0x53, 0x0A, 0xED, 0xCC, 0xB7, 0xFE, 0x31, 0x20, 0xDB, 0xB2, 0x35, 0x34, 0xBF, 0x26, 0xF9, 0x08,
    0x63, 0x5A, 0x7D, 0x9C, 0xC7, 0x4E, 0xC1, 0xF0, 0xEB, 0x02, 0xC5, 0x04, 0xCF, 0x76, 0x89, 0xD8,
    0x73, 0xAA, 0x0D, 0x6C, 0xD7, 0x9E, 0x51, 0xC0, 0xFB, 0x52, 0x55, 0xD4, 0xDF, 0xC6, 0x19, 0xA8,
    0x83, 0xFA, 0x9D, 0x3C, 0xE7, 0xEE, 0xE1, 0x90, 0x0B, 0xA2, 0xE5, 0xA4, 0xEF, 0x16, 0xA9, 0x78,
];

#[test]
fn test_sbox_all_256_entries() {
    for x in 0u16..256 {
        let mut buf = [x as u8; 32];
        apply_polynomial_sbox(&mut buf);
        assert_eq!(
            buf[0], EXPECTED_SBOX[x as usize],
            "S-Box mismatch at x={}: got 0x{:02X}, expected 0x{:02X}",
            x, buf[0], EXPECTED_SBOX[x as usize]
        );
        // All 32 bytes should be identical since input was uniform
        for i in 1..32 {
            assert_eq!(buf[i], buf[0], "S-Box byte {} differs from byte 0 for input {}", i, x);
        }
    }
}

#[test]
fn test_sbox_specific_values() {
    // S(0) = 147 = 0x93 (constant term)
    let mut buf = [0u8; 32];
    apply_polynomial_sbox(&mut buf);
    assert_eq!(buf[0], 0x93);

    // S(1) = sum of all coefficients mod 256 = (192+224+120+200+150+65+147) % 256 = 74
    let mut buf = [1u8; 32];
    apply_polynomial_sbox(&mut buf);
    assert_eq!(buf[0], 0x4A);

    // S(39) = 0x00 (a zero in the S-Box, found from lookup table)
    let mut buf = [39u8; 32];
    apply_polynomial_sbox(&mut buf);
    assert_eq!(buf[0], 0x00);
}

#[test]
fn test_sbox_mixed_input() {
    let mut buf = [0u8; 32];
    for i in 0..32 {
        buf[i] = i as u8;
    }
    apply_polynomial_sbox(&mut buf);
    for i in 0..32 {
        assert_eq!(buf[i], EXPECTED_SBOX[i], "Mixed input mismatch at index {}", i);
    }
}

// ═══════════════════════════════════════════════════════════
// 2. PCG-XSH-RR PRNG TESTS
// ═══════════════════════════════════════════════════════════

/// Reference outputs for seed=12345, computed from Python reference implementation
const PCG_EXPECTED_U32: [u32; 20] = [
    0x68677495, 0xDB38677A, 0x01B8EF75, 0x0C0B2EEE,
    0xDBFB70E6, 0x92DDB8F5, 0xF84CD5BF, 0xA8C5D0DB,
    0xAE1E7AF5, 0x5CD5DB6A, 0x65971E61, 0x630A3794,
    0xF03DB558, 0xEBC6D353, 0x7C856CD9, 0x2FFCC414,
    0x27170096, 0x0044E9A0, 0xF69DE00C, 0x64A78FFD,
];

const PCG_EXPECTED_NONCE: [u8; 12] = [149, 122, 117, 238, 230, 245, 191, 219, 245, 106, 97, 148];

#[test]
fn test_pcg_nonce_seed_12345() {
    let mut rng = PcgRng::new(12345);
    let nonce = rng.generate_nonce();
    assert_eq!(
        nonce, PCG_EXPECTED_NONCE,
        "PCG nonce mismatch for seed=12345\ngot:      {:?}\nexpected: {:?}",
        nonce, PCG_EXPECTED_NONCE
    );
}

#[test]
fn test_pcg_deterministic() {
    // Same seed must produce same sequence
    let mut rng1 = PcgRng::new(42);
    let mut rng2 = PcgRng::new(42);
    let n1 = rng1.generate_nonce();
    let n2 = rng2.generate_nonce();
    assert_eq!(n1, n2, "PCG is not deterministic for same seed");
}

#[test]
fn test_pcg_different_seeds_differ() {
    let mut rng1 = PcgRng::new(0);
    let mut rng2 = PcgRng::new(1);
    let n1 = rng1.generate_nonce();
    let n2 = rng2.generate_nonce();
    assert_ne!(n1, n2, "Different seeds should produce different nonces");
}

#[test]
fn test_pcg_consecutive_nonces_differ() {
    let mut rng = PcgRng::new(999);
    let n1 = rng.generate_nonce();
    let n2 = rng.generate_nonce();
    assert_ne!(n1, n2, "Consecutive nonces should differ");
}

// ═══════════════════════════════════════════════════════════
// 3. UTIL BIT-MANIPULATION TESTS
// ═══════════════════════════════════════════════════════════

#[test]
fn test_ror32() {
    assert_eq!(ror32(0x80000000, 1), 0x40000000);
    assert_eq!(ror32(0x00000001, 1), 0x80000000);
    assert_eq!(ror32(0x12345678, 0), 0x12345678);
    assert_eq!(ror32(0x12345678, 32), 0x12345678);
    assert_eq!(ror32(0x12345678, 8), 0x78123456);
}

#[test]
fn test_rol32() {
    assert_eq!(rol32(0x80000000, 1), 0x00000001);
    assert_eq!(rol32(0x00000001, 1), 0x00000002);
    assert_eq!(rol32(0x12345678, 0), 0x12345678);
    assert_eq!(rol32(0x12345678, 8), 0x34567812);
}

#[test]
fn test_bswap32() {
    assert_eq!(bswap32(0x12345678), 0x78563412);
    assert_eq!(bswap32(0x00000000), 0x00000000);
    assert_eq!(bswap32(0xFFFFFFFF), 0xFFFFFFFF);
    assert_eq!(bswap32(0x000000FF), 0xFF000000);
    // Double swap is identity
    assert_eq!(bswap32(bswap32(0xDEADBEEF)), 0xDEADBEEF);
}

#[test]
fn test_bitrev32() {
    assert_eq!(bitrev32(0x00000001), 0x80000000);
    assert_eq!(bitrev32(0x80000000), 0x00000001);
    assert_eq!(bitrev32(0x00000000), 0x00000000);
    assert_eq!(bitrev32(0xFFFFFFFF), 0xFFFFFFFF);
    // Double reversal is identity
    assert_eq!(bitrev32(bitrev32(0xDEADBEEF)), 0xDEADBEEF);
    assert_eq!(bitrev32(bitrev32(0x12345678)), 0x12345678);
}

#[test]
fn test_partial_bitrev_shr1_differs_from_bitrev() {
    // partial_bitrev_shr1 uses mask 0x55555554 instead of 0x55555555 then >>1
    // It should NOT be equal to bitrev32(x) >> 1 for most inputs
    let x = 0xDEADBEEF;
    let pbr = partial_bitrev_shr1(x);
    let br_shr = bitrev32(x) >> 1;
    // They may or may not match depending on the input, but the operation itself is valid
    // Just verify it's deterministic
    assert_eq!(partial_bitrev_shr1(x), pbr);
    // And not always zero
    assert_ne!(partial_bitrev_shr1(0x12345678), 0);
}

#[test]
fn test_nibble_half_swap() {
    // Test identity: nibble_half_swap is an involution? Let's check.
    let x = 0x12345678u32;
    let swapped = nibble_half_swap(x);
    // Not an identity
    assert_ne!(swapped, x);
    // Verify the formula: (x.rol(12) & 0x0F0F0F0F) | (x.rol(20) & 0xF0F0F0F0)
    let expected = (x.rotate_left(12) & 0x0F0F0F0F) | (x.rotate_left(20) & 0xF0F0F0F0);
    assert_eq!(swapped, expected);

    // Zero in, zero out
    assert_eq!(nibble_half_swap(0), 0);
}

// ═══════════════════════════════════════════════════════════
// 4. GIFT-256 BITSLICED S-BOX TESTS
// ═══════════════════════════════════════════════════════════

#[test]
fn test_bitsliced_sbox_all_zeros() {
    let mut s = [0u32; 8];
    sbox_bitsliced(&mut s);
    // All-zero input should produce a deterministic non-zero output
    // (the S-Box is not the identity)
    // Just check it doesn't crash and produces something
    let is_all_zero = s.iter().all(|&x| x == 0);
    // S-Box of all zeros is unlikely to be all zeros
    // (depends on the Boolean function, but let's at least test it runs)
    let _ = is_all_zero; // may or may not be zero
}

#[test]
fn test_bitsliced_sbox_all_ones() {
    let mut s = [0xFFFFFFFFu32; 8];
    sbox_bitsliced(&mut s);
    // Should produce some output without panicking
    assert!(true, "S-Box on all-ones completed");
}

#[test]
fn test_bitsliced_sbox_deterministic() {
    let input = [0x12345678, 0x9ABCDEF0, 0x13579BDF, 0x2468ACE0,
                 0xFEDCBA98, 0x76543210, 0xECA86420, 0xFDB97531];
    let mut s1 = input;
    let mut s2 = input;
    sbox_bitsliced(&mut s1);
    sbox_bitsliced(&mut s2);
    assert_eq!(s1, s2, "Bitsliced S-Box must be deterministic");
}

#[test]
fn test_bitsliced_sbox_not_identity() {
    let input = [0x12345678, 0x9ABCDEF0, 0x13579BDF, 0x2468ACE0,
                 0xFEDCBA98, 0x76543210, 0xECA86420, 0xFDB97531];
    let mut s = input;
    sbox_bitsliced(&mut s);
    assert_ne!(s, input, "S-Box should not be the identity function");
}

#[test]
fn test_bitsliced_sbox_not_involution() {
    // Applying S-Box twice should NOT return the original (it's not an involution)
    let input = [0x11111111, 0x22222222, 0x33333333, 0x44444444,
                 0x55555555, 0x66666666, 0x77777777, 0x88888888];
    let mut s = input;
    sbox_bitsliced(&mut s);
    let after_one = s;
    sbox_bitsliced(&mut s);
    // Double application likely doesn't return to original
    // (this is a property test, not a guarantee for all inputs)
    assert_ne!(s, input, "S-Box should not be an involution for this input");
    assert_ne!(s, after_one, "Double S-Box should differ from single");
}

// ═══════════════════════════════════════════════════════════
// 5. GIFT-256 INTERLEAVE TESTS
// ═══════════════════════════════════════════════════════════

#[test]
fn test_nibble_deinterleave_zero() {
    assert_eq!(nibble_deinterleave(0), 0);
}

#[test]
fn test_nibble_deinterleave_all_ones() {
    let result = nibble_deinterleave(0xFFFFFFFF);
    // Should produce a valid result
    let _ = result;
}

#[test]
fn test_key_deinterleave_functions_zero() {
    assert_eq!(key_deinterleave_a(0), 0);
    assert_eq!(key_deinterleave_b(0), 0);
    assert_eq!(key_deinterleave_c(0), 0);
}

#[test]
fn test_pack_input_all_zeros() {
    let input = [0u32; 8];
    let rk = [0u32; 8];
    let result = pack_input(&input, &rk);
    assert_eq!(result, [0u32; 8], "Pack of zeros with zero keys should be zero");
}

#[test]
fn test_pack_unpack_roundtrip() {
    // pack then unpack with zero keys should be a round-trip
    let input = [0x11111111, 0x22222222, 0x33333333, 0x44444444,
                 0x55555555, 0x66666666, 0x77777777, 0x88888888];
    let zero_rk = [0u32; 8];
    let packed = pack_input(&input, &zero_rk);
    let unpacked = unpack_output(&packed, &zero_rk);
    assert_eq!(
        unpacked, input,
        "Pack/unpack round-trip failed\noriginal:  {:08X?}\nunpacked:  {:08X?}",
        input, unpacked
    );
}

#[test]
fn test_pack_unpack_roundtrip_random() {
    let input = [0xDEADBEEF, 0xCAFEBABE, 0x12345678, 0x9ABCDEF0,
                 0xFEDCBA98, 0x76543210, 0xAAAAAAAA, 0x55555555];
    let zero_rk = [0u32; 8];
    let packed = pack_input(&input, &zero_rk);
    let unpacked = unpack_output(&packed, &zero_rk);
    assert_eq!(
        unpacked, input,
        "Pack/unpack round-trip with random data failed"
    );
}

#[test]
fn test_pack_unpack_with_keys() {
    let input = [0x11223344, 0x55667788, 0x99AABBCC, 0xDDEEFF00,
                 0x01020304, 0x05060708, 0x090A0B0C, 0x0D0E0F10];
    let rk = [0xAAAAAAAA, 0xBBBBBBBB, 0xCCCCCCCC, 0xDDDDDDDD,
              0xEEEEEEEE, 0xFFFFFFFF, 0x11111111, 0x22222222];
    let packed = pack_input(&input, &rk);
    let unpacked = unpack_output(&packed, &rk);
    assert_eq!(
        unpacked, input,
        "Pack/unpack with non-zero keys round-trip failed"
    );
}

// ═══════════════════════════════════════════════════════════
// 6. GIFT-256 LINEAR LAYER TESTS
// ═══════════════════════════════════════════════════════════

#[test]
fn test_linear_p1_all_zeros() {
    let mut s = [0u32; 8];
    linear_p1(&mut s);
    assert_eq!(s, [0u32; 8], "P1 of all zeros should be all zeros");
}

#[test]
fn test_linear_p2_all_zeros() {
    let mut s = [0u32; 8];
    linear_p2(&mut s);
    assert_eq!(s, [0u32; 8], "P2 of all zeros should be all zeros");
}

#[test]
fn test_diffusion_a_all_zeros() {
    let mut s = [0u32; 8];
    let rk = [0u32; 8];
    diffusion_a(&mut s, &rk);
    assert_eq!(s, [0u32; 8], "DA of all zeros with zero keys should be all zeros");
}

#[test]
fn test_diffusion_b_all_zeros() {
    let mut s = [0u32; 8];
    let rk = [0u32; 8];
    diffusion_b(&mut s, &rk);
    assert_eq!(s, [0u32; 8], "DB of all zeros with zero keys should be all zeros");
}

#[test]
fn test_linear_p1_deterministic() {
    let input = [0x12345678, 0x9ABCDEF0, 0x13579BDF, 0x2468ACE0,
                 0xFEDCBA98, 0x76543210, 0xECA86420, 0xFDB97531];
    let mut s1 = input;
    let mut s2 = input;
    linear_p1(&mut s1);
    linear_p1(&mut s2);
    assert_eq!(s1, s2, "P1 must be deterministic");
}

#[test]
fn test_linear_p2_deterministic() {
    let input = [0x12345678, 0x9ABCDEF0, 0x13579BDF, 0x2468ACE0,
                 0xFEDCBA98, 0x76543210, 0xECA86420, 0xFDB97531];
    let mut s1 = input;
    let mut s2 = input;
    linear_p2(&mut s1);
    linear_p2(&mut s2);
    assert_eq!(s1, s2, "P2 must be deterministic");
}

#[test]
fn test_linear_p1_not_identity() {
    let input = [0x12345678, 0x9ABCDEF0, 0x13579BDF, 0x2468ACE0,
                 0xFEDCBA98, 0x76543210, 0xECA86420, 0xFDB97531];
    let mut s = input;
    linear_p1(&mut s);
    assert_ne!(s, input, "P1 should not be the identity");
}

// ═══════════════════════════════════════════════════════════
// 7. GIFT-256 KEY SCHEDULE TESTS
// ═══════════════════════════════════════════════════════════

#[test]
fn test_key_schedule_zero_key() {
    let key = [0u8; 32];
    let ks = key_schedule(&key);
    // Should produce 120 u32 values without panicking
    assert_eq!(ks.len(), 120);
    // Zero key should still produce non-zero round keys (due to NOT compensation)
    let has_nonzero = ks.iter().any(|&x| x != 0);
    assert!(has_nonzero, "Zero key should produce non-zero round keys (NOT compensation)");
}

#[test]
fn test_key_schedule_deterministic() {
    let key = [0x42u8; 32];
    let ks1 = key_schedule(&key);
    let ks2 = key_schedule(&key);
    assert_eq!(ks1, ks2, "Key schedule must be deterministic");
}

#[test]
fn test_key_schedule_different_keys_differ() {
    let key1 = [0x00u8; 32];
    let key2 = [0x01u8; 32];
    let ks1 = key_schedule(&key1);
    let ks2 = key_schedule(&key2);
    assert_ne!(ks1, ks2, "Different keys should produce different round keys");
}

#[test]
fn test_key_schedule_avalanche() {
    // Flipping one bit in the key should change many round key words
    let mut key1 = [0u8; 32];
    let mut key2 = [0u8; 32];
    key2[0] = 0x01; // flip one bit

    let ks1 = key_schedule(&key1);
    let ks2 = key_schedule(&key2);

    let diff_count = ks1.iter().zip(ks2.iter()).filter(|(&a, &b)| a != b).count();
    assert!(
        diff_count > 10,
        "Single bit flip should cause avalanche effect, only {} of 120 words differ",
        diff_count
    );
}

// ═══════════════════════════════════════════════════════════
// 8. GIFT-256 ENCRYPT TESTS
// ═══════════════════════════════════════════════════════════

#[test]
fn test_encrypt_zero_plaintext_zero_key() {
    let key = [0u8; 32];
    let rk = key_schedule(&key);
    let plaintext = [0u32; 8];
    let ciphertext = encrypt(&plaintext, &rk);
    // Should produce non-zero ciphertext
    let is_all_zero = ciphertext.iter().all(|&x| x == 0);
    assert!(!is_all_zero, "Encryption of zeros with zero key should not be all zeros");
}

#[test]
fn test_encrypt_deterministic() {
    let key = [0xABu8; 32];
    let rk = key_schedule(&key);
    let plaintext = [0x12345678, 0x9ABCDEF0, 0, 0, 0, 0, 0, 0];
    let ct1 = encrypt(&plaintext, &rk);
    let ct2 = encrypt(&plaintext, &rk);
    assert_eq!(ct1, ct2, "Encryption must be deterministic");
}

#[test]
fn test_encrypt_different_plaintexts_differ() {
    let key = [0u8; 32];
    let rk = key_schedule(&key);
    let pt1 = [0u32; 8];
    let mut pt2 = [0u32; 8];
    pt2[0] = 1;
    let ct1 = encrypt(&pt1, &rk);
    let ct2 = encrypt(&pt2, &rk);
    assert_ne!(ct1, ct2, "Different plaintexts should produce different ciphertexts");
}

#[test]
fn test_encrypt_different_keys_differ() {
    let key1 = [0u8; 32];
    let key2 = [0x01u8; 32];
    let rk1 = key_schedule(&key1);
    let rk2 = key_schedule(&key2);
    let pt = [0u32; 8];
    let ct1 = encrypt(&pt, &rk1);
    let ct2 = encrypt(&pt, &rk2);
    assert_ne!(ct1, ct2, "Different keys should produce different ciphertexts");
}

#[test]
fn test_encrypt_plaintext_avalanche() {
    let key = [0u8; 32];
    let rk = key_schedule(&key);
    let pt1 = [0u32; 8];
    let mut pt2 = [0u32; 8];
    pt2[0] = 1; // single bit flip

    let ct1 = encrypt(&pt1, &rk);
    let ct2 = encrypt(&pt2, &rk);

    // Count differing bits across all 8 words
    let diff_bits: u32 = ct1.iter().zip(ct2.iter())
        .map(|(&a, &b)| (a ^ b).count_ones())
        .sum();

    // Good diffusion should flip roughly half the bits (128 out of 256)
    // Allow wide range for a non-standard cipher
    assert!(
        diff_bits > 30,
        "Single bit change should cause significant diffusion, only {} bits differ",
        diff_bits
    );
}

// ═══════════════════════════════════════════════════════════
// 9. HASH SUBSYSTEM TESTS
// ═══════════════════════════════════════════════════════════

#[test]
fn test_mmo_compress_zero_key() {
    let key = [0u8; 32];
    let rk = key_schedule(&key);
    let state = mmo_compress(&rk);
    // Chaining value should be non-zero (it's the GF(2^128) double of ciphertext extract)
    let is_all_zero = state.chaining.iter().all(|&x| x == 0);
    assert!(!is_all_zero, "MMO chaining of zero key should be non-zero");
}

#[test]
fn test_mmo_compress_deterministic() {
    let key = [0xCDu8; 32];
    let rk = key_schedule(&key);
    let s1 = mmo_compress(&rk);
    let s2 = mmo_compress(&rk);
    assert_eq!(s1.chaining, s2.chaining, "MMO compress must be deterministic");
}

#[test]
fn test_inner_compress_all_zeros() {
    let mut state = [0u32; 8];
    let block = [0u32; 4];
    inner_compress(&mut state, &block);
    // XOR of zeros into zeros, then GF multiply with zeros = zeros
    // So state should remain all zeros
    assert_eq!(state, [0u32; 8], "Inner compress of all zeros should be all zeros");
}

#[test]
fn test_inner_compress_deterministic() {
    let mut s1 = [0x11111111, 0x22222222, 0x33333333, 0x44444444,
                  0x55555555, 0x66666666, 0x77777777, 0x88888888];
    let mut s2 = s1;
    let block = [0xAAAAAAAA, 0xBBBBBBBB, 0xCCCCCCCC, 0xDDDDDDDD];
    inner_compress(&mut s1, &block);
    inner_compress(&mut s2, &block);
    assert_eq!(s1, s2, "Inner compress must be deterministic");
}

#[test]
fn test_inner_compress_feistel_structure() {
    // After one round, the old left half should become the new right half
    let state_init = [0x11111111, 0x22222222, 0x33333333, 0x44444444,
                      0x55555555, 0x66666666, 0x77777777, 0x88888888];
    let mut state = state_init;
    let block = [0xAAAAAAAA, 0xBBBBBBBB, 0xCCCCCCCC, 0xDDDDDDDD];
    inner_compress(&mut state, &block);

    // Right half (state[4..7]) should be the old left half (state[0..3])
    assert_eq!(state[4], state_init[0], "Feistel: state[4] should be old state[0]");
    assert_eq!(state[5], state_init[1], "Feistel: state[5] should be old state[1]");
    assert_eq!(state[6], state_init[2], "Feistel: state[6] should be old state[2]");
    assert_eq!(state[7], state_init[3], "Feistel: state[7] should be old state[3]");
}

#[test]
fn test_process_message_empty() {
    let mut state = [0u32; 8];
    process_message(&mut state, &[]);
    // Empty message should not change state
    assert_eq!(state, [0u32; 8], "Empty message should not change state");
}

#[test]
fn test_process_message_deterministic() {
    let mut s1 = [0x11111111u32; 8];
    let mut s2 = [0x11111111u32; 8];
    let msg = b"Hello, hCaptcha!";
    process_message(&mut s1, msg);
    process_message(&mut s2, msg);
    assert_eq!(s1, s2, "Message processing must be deterministic");
}

#[test]
fn test_finalize_deterministic() {
    let state = [0x11111111, 0x22222222, 0x33333333, 0x44444444,
                 0x55555555, 0x66666666, 0x77777777, 0x88888888];
    let chaining = [0xAAu8; 16];
    let msg = b"test message";

    let d1 = finalize(&state, &chaining, msg);
    let d2 = finalize(&state, &chaining, msg);
    assert_eq!(d1, d2, "Finalize must be deterministic");
}

#[test]
fn test_finalize_different_messages_differ() {
    let state = [0x11111111, 0x22222222, 0x33333333, 0x44444444,
                 0x55555555, 0x66666666, 0x77777777, 0x88888888];
    let chaining = [0xAAu8; 16];

    let d1 = finalize(&state, &chaining, b"message A");
    let d2 = finalize(&state, &chaining, b"message B");
    assert_ne!(d1, d2, "Different messages should produce different digests");
}

#[test]
fn test_finalize_different_chaining_differ() {
    let state = [0u32; 8];
    let chaining1 = [0x00u8; 16];
    let chaining2 = [0xFFu8; 16];
    let msg = b"test";

    let d1 = finalize(&state, &chaining1, msg);
    let d2 = finalize(&state, &chaining2, msg);
    assert_ne!(d1, d2, "Different chaining values should produce different digests");
}

// ═══════════════════════════════════════════════════════════
// 10. END-TO-END INTEGRATION TESTS
// ═══════════════════════════════════════════════════════════

#[test]
fn test_full_pipeline_no_panic() {
    // Verify the entire pipeline runs without panicking
    let key = [0x42u8; 32];
    let mut key_data = key;
    apply_polynomial_sbox(&mut key_data);

    let rk = key_schedule(&key_data);
    let mmo = mmo_compress(&rk);

    let mut rng = PcgRng::new(12345);
    let nonce = rng.generate_nonce();

    let nonce_u32_0 = u32::from_le_bytes([nonce[0], nonce[1], nonce[2], nonce[3]]);
    let nonce_u32_1 = u32::from_le_bytes([nonce[4], nonce[5], nonce[6], nonce[7]]);
    let nonce_u32_2 = u32::from_le_bytes([nonce[8], nonce[9], nonce[10], nonce[11]]);

    let encrypted = encrypt(
        &[nonce_u32_0, nonce_u32_1, nonce_u32_2, 0x01000000, 0, 0, 0, 0],
        &rk,
    );

    let mut hash_state = [0u32; 8];
    for i in 0..8 {
        hash_state[i] = encrypted[i];
    }

    let digest = finalize(&hash_state, &mmo.chaining, &nonce);
    // Digest should be 16 bytes, non-zero
    assert_eq!(digest.len(), 16);
    let is_all_zero = digest.iter().all(|&x| x == 0);
    assert!(!is_all_zero, "Full pipeline digest should not be all zeros");
}

#[test]
fn test_full_pipeline_deterministic() {
    // Same inputs must produce same digest
    let compute = || {
        let key = [0xABu8; 32];
        let mut key_data = key;
        apply_polynomial_sbox(&mut key_data);

        let rk = key_schedule(&key_data);
        let mmo = mmo_compress(&rk);

        let mut rng = PcgRng::new(42);
        let nonce = rng.generate_nonce();

        let nonce_u32_0 = u32::from_le_bytes([nonce[0], nonce[1], nonce[2], nonce[3]]);
        let nonce_u32_1 = u32::from_le_bytes([nonce[4], nonce[5], nonce[6], nonce[7]]);
        let nonce_u32_2 = u32::from_le_bytes([nonce[8], nonce[9], nonce[10], nonce[11]]);

        let encrypted = encrypt(
            &[nonce_u32_0, nonce_u32_1, nonce_u32_2, 0x01000000, 0, 0, 0, 0],
            &rk,
        );

        let mut hash_state = [0u32; 8];
        for i in 0..8 {
            hash_state[i] = encrypted[i];
        }

        finalize(&hash_state, &mmo.chaining, &nonce)
    };

    let d1 = compute();
    let d2 = compute();
    assert_eq!(d1, d2, "Full pipeline must be deterministic");
}

#[test]
fn test_different_seeds_produce_different_digests() {
    let key = [0xCDu8; 32];
    let mut key_data = key;
    apply_polynomial_sbox(&mut key_data);
    let rk = key_schedule(&key_data);
    let mmo = mmo_compress(&rk);

    let compute_digest = |seed: u64| -> [u8; 16] {
        let mut rng = PcgRng::new(seed);
        let nonce = rng.generate_nonce();

        let nonce_u32_0 = u32::from_le_bytes([nonce[0], nonce[1], nonce[2], nonce[3]]);
        let nonce_u32_1 = u32::from_le_bytes([nonce[4], nonce[5], nonce[6], nonce[7]]);
        let nonce_u32_2 = u32::from_le_bytes([nonce[8], nonce[9], nonce[10], nonce[11]]);

        let encrypted = encrypt(
            &[nonce_u32_0, nonce_u32_1, nonce_u32_2, 0x01000000, 0, 0, 0, 0],
            &rk,
        );

        let mut hash_state = [0u32; 8];
        for i in 0..8 {
            hash_state[i] = encrypted[i];
        }

        finalize(&hash_state, &mmo.chaining, &nonce)
    };

    let d1 = compute_digest(111);
    let d2 = compute_digest(222);
    let d3 = compute_digest(333);

    assert_ne!(d1, d2, "Different seeds should produce different digests");
    assert_ne!(d2, d3, "Different seeds should produce different digests");
    assert_ne!(d1, d3, "Different seeds should produce different digests");
}
