//! Comprehensive roundtrip tests for CSS and AACS cryptographic implementations.
//!
//! These tests prove the cryptographic algorithms work end-to-end.
//! Tests that require access to private internals are placed as unit tests
//! inside the respective source files (css/lfsr.rs, css/crack.rs, aacs/handshake.rs).
//!
//! This file tests public API items accessible from integration tests.

use libfreemkv::aacs;
use libfreemkv::css;

// ── CSS Public API Tests ────────────────────────────────────────────────────

/// Test: css_descramble_sector modifies encrypted region and preserves header.
#[test]
fn css_descramble_sector_roundtrip_via_public_api() {
    let state = css::CssState {
        title_key: [0x42, 0x13, 0x37, 0xBE, 0xEF],
        crack_span: None,
    };

    let mut sector = vec![0xAAu8; 2048];
    sector[0x14] = 0x30;
    sector[0x54..0x59].copy_from_slice(&[0xDE, 0xAD, 0xBE, 0xEF, 0x42]);
    let original = sector.clone();

    css::descramble_sector(&state, &mut sector);
    assert_eq!(sector[0x14] & 0x30, 0x00, "flag not cleared");
    // Header preserved (except flag byte)
    for i in 0..128 {
        if i == 0x14 {
            continue;
        }
        assert_eq!(sector[i], original[i], "header byte {} changed", i);
    }
    // Encrypted region modified
    assert_ne!(&sector[128..256], &original[128..256]);
}

/// Test: css_is_scrambled detects scramble flags correctly.
#[test]
fn css_is_scrambled_detection() {
    let mut sector = vec![0u8; 2048];
    assert!(
        !css::is_scrambled(&sector),
        "empty sector should not be scrambled"
    );

    sector[0x14] = 0x10; // bit 4 set
    assert!(css::is_scrambled(&sector), "bit 4 set should be detected");

    sector[0x14] = 0x20; // bit 5 set
    assert!(css::is_scrambled(&sector), "bit 5 set should be detected");

    sector[0x14] = 0x30; // both bits set
    assert!(
        css::is_scrambled(&sector),
        "both bits set should be detected"
    );

    sector[0x14] = 0xCF; // bits 4-5 clear, other bits set
    assert!(
        !css::is_scrambled(&sector),
        "bits 4-5 clear should not be scrambled"
    );
}

// ── AACS Public API Tests ───────────────────────────────────────────────────

/// Test 6: aacs_decrypt_unit_roundtrip
///
/// Build a synthetic 6144-byte aligned unit with TS sync bytes, encrypt it
/// using the AACS algorithm (AES-ECB header derivation + AES-CBC body),
/// then decrypt with decrypt_unit() and verify the plaintext matches.
#[test]
fn aacs_decrypt_unit_roundtrip() {
    use aes::Aes128;
    use aes::cipher::{BlockEncrypt, KeyInit, generic_array::GenericArray};

    let unit_key = [0xAAu8; 16];
    let aacs_iv: [u8; 16] = [
        0x0B, 0xA0, 0xF8, 0xDD, 0xFE, 0xA6, 0x1F, 0xB3, 0xD8, 0xDF, 0x9F, 0x56, 0x6A, 0x05, 0x0F,
        0x78,
    ];

    // Build plaintext unit with TS sync bytes every 192 bytes starting at offset 4
    let mut plain = vec![0u8; aacs::content::ALIGNED_UNIT_LEN];
    let mut offset = 4;
    while offset < aacs::content::ALIGNED_UNIT_LEN {
        plain[offset] = 0x47; // TS sync byte
        offset += 192;
    }
    // Flag the unit encrypted via the CPI bits (byte 0) — the authoritative gate
    // `decrypt_unit` consults. Set BEFORE snapshotting `expected`: decrypt
    // preserves the plaintext header, so the round-tripped unit carries these
    // bits too.
    plain[0] |= 0xC0;

    // Save original plaintext for comparison
    let expected = plain.clone();

    // Encrypt: replicate the AACS encryption algorithm (reverse of decrypt_unit)
    let header: [u8; 16] = plain[..16].try_into().unwrap();

    // Step 1: AES-ECB encrypt header with unit key
    let cipher_header = Aes128::new(GenericArray::from_slice(&unit_key));
    let mut block = GenericArray::clone_from_slice(&header);
    cipher_header.encrypt_block(&mut block);
    let mut derived = [0u8; 16];
    derived.copy_from_slice(&block);

    // Step 2: XOR to get per-unit decryption key
    let mut encrypt_key = [0u8; 16];
    for i in 0..16 {
        encrypt_key[i] = derived[i] ^ header[i];
    }

    // Step 3: AES-CBC encrypt bytes 16..6144
    let cipher = Aes128::new(GenericArray::from_slice(&encrypt_key));
    let mut prev = aacs_iv;
    let num_blocks = (aacs::content::ALIGNED_UNIT_LEN - 16) / 16;
    for i in 0..num_blocks {
        let off = 16 + i * 16;
        for j in 0..16 {
            plain[off + j] ^= prev[j];
        }
        let mut blk = GenericArray::clone_from_slice(&plain[off..off + 16]);
        cipher.encrypt_block(&mut blk);
        plain[off..off + 16].copy_from_slice(&blk);
        prev.copy_from_slice(&plain[off..off + 16]);
    }

    // Verify it looks encrypted (body TS syncs scrambled)
    assert!(!aacs::content::is_clean(
        &plain,
        libfreemkv::disc::ContentFormat::BdTs
    ));

    // Now decrypt
    aacs::content::decrypt_unit(&mut plain, &unit_key);
    assert!(
        aacs::content::is_clean(&plain, libfreemkv::disc::ContentFormat::BdTs),
        "decrypted unit should read as clear (TS syncs restored)"
    );

    // Verify TS sync bytes at expected positions (flag byte is cleared by decrypt)
    let mut sync_count = 0;
    let mut off = 4;
    while off < aacs::content::ALIGNED_UNIT_LEN {
        if plain[off] == 0x47 {
            sync_count += 1;
        }
        off += 192;
    }
    let expected_syncs = (aacs::content::ALIGNED_UNIT_LEN - 4) / 192 + 1;
    assert_eq!(
        sync_count, expected_syncs,
        "TS sync bytes not recovered: got {}, expected {}",
        sync_count, expected_syncs
    );

    // Decryption clears no flag, so the unit round-trips byte-for-byte.
    assert_eq!(
        &plain[..],
        &expected[..],
        "decrypted unit does not match original plaintext"
    );
}

/// Test 7: aacs_disc_hash_deterministic
///
/// compute disc_hash on the same data twice, verify identical results.
#[test]
fn aacs_disc_hash_deterministic() {
    let data1 = b"Unit_Key_RO.inf test data for deterministic hashing";
    let data2 = b"Different data should produce different hash";

    let hash1a = aacs::inf::disc_hash(data1);
    let hash1b = aacs::inf::disc_hash(data1);
    assert_eq!(hash1a, hash1b, "disc_hash not deterministic on same input");

    let hash2 = aacs::inf::disc_hash(data2);
    assert_ne!(
        hash1a, hash2,
        "different inputs should produce different hashes"
    );

    // Verify it is a 20-byte SHA-1 hash
    assert_eq!(hash1a.len(), 20);

    // Verify disc_hash_hex formatting
    let hex = aacs::inf::disc_hash_hex(&hash1a);
    assert!(hex.starts_with("0x"), "hex should start with 0x prefix");
    assert_eq!(
        hex.len(),
        42,
        "hex string should be 42 chars (0x + 40 hex digits)"
    );
}

/// Test: aacs_decrypt_unit_key_roundtrip
///
/// Verify that encrypting a unit key with AES-ECB and decrypting it with
/// decrypt_unit_key recovers the original.
#[test]
fn aacs_decrypt_unit_key_roundtrip() {
    use aes::Aes128;
    use aes::cipher::{BlockEncrypt, KeyInit, generic_array::GenericArray};

    let vuk = [
        0x11u8, 0x14, 0x36, 0x0B, 0x10, 0xEE, 0x6E, 0xAC, 0x78, 0xAA, 0x4A, 0xC0, 0xB7, 0x52, 0xEA,
        0xEB,
    ];
    let original_unit_key = [
        0x9E, 0x5D, 0x13, 0x10, 0x33, 0x74, 0x43, 0xE8, 0x11, 0xA5, 0x2E, 0xBB, 0xEA, 0xE0, 0x47,
        0x0F,
    ];

    // Encrypt: AES-ECB encrypt the unit key with VUK
    let cipher = Aes128::new(GenericArray::from_slice(&vuk));
    let mut block = GenericArray::clone_from_slice(&original_unit_key);
    cipher.encrypt_block(&mut block);
    let mut encrypted_uk = [0u8; 16];
    encrypted_uk.copy_from_slice(&block);

    // Decrypt with the public API
    let decrypted = aacs::derive::decrypt_unit_key(&vuk, &encrypted_uk);
    assert_eq!(
        decrypted, original_unit_key,
        "decrypt_unit_key did not recover original unit key"
    );
}

/// Test: aacs_vuk_derivation
///
/// Verify derive_vuk: VUK = AES-ECB-DECRYPT(media_key, volume_id) XOR volume_id
#[test]
fn aacs_vuk_derivation_roundtrip() {
    let media_key = [
        0x25u8, 0x2F, 0xB6, 0x36, 0xE8, 0x83, 0x52, 0x9E, 0x11, 0x9A, 0xB7, 0x15, 0xF4, 0xEB, 0x16,
        0x40,
    ];
    let volume_id = [
        0xA1u8, 0x3C, 0xBE, 0x2C, 0xE4, 0x05, 0x65, 0xD1, 0x04, 0xB5, 0x3E, 0x76, 0x8C, 0x70, 0x0E,
        0x30,
    ];

    let vuk = aacs::derive::derive_vuk(&media_key, &volume_id);

    // VUK should be non-zero and different from both inputs
    assert_ne!(vuk, [0u8; 16], "VUK should not be all zeros");
    assert_ne!(vuk, media_key, "VUK should differ from media_key");
    assert_ne!(vuk, volume_id, "VUK should differ from volume_id");

    // Verify determinism
    let vuk2 = aacs::derive::derive_vuk(&media_key, &volume_id);
    assert_eq!(vuk, vuk2, "derive_vuk not deterministic");
}

/// Test: `is_clean` distinguishes clean vs scrambled units via the TS proof floor.
#[test]
fn aacs_is_clean_detection() {
    // A clear unit: TS sync (0x47) intact at every 192-byte packet → not
    // scrambled. (Flag bits play no role.)
    let mut clear = vec![0u8; aacs::content::ALIGNED_UNIT_LEN];
    let mut off = 4;
    while off < aacs::content::ALIGNED_UNIT_LEN {
        clear[off] = 0x47;
        off += 192;
    }
    assert!(
        aacs::content::is_clean(&clear, libfreemkv::disc::ContentFormat::BdTs),
        "clear unit (syncs intact) must not be scrambled"
    );

    // Flag bits do NOT make a synced unit look scrambled.
    let mut flagged = clear.clone();
    flagged[0] = 0xC0; // copy-control bits
    flagged[7] = 0xC0; // TSC bits
    assert!(
        aacs::content::is_clean(&flagged, libfreemkv::disc::ContentFormat::BdTs),
        "flag bits must not be read as encryption"
    );

    // A scrambled body (syncs destroyed) → scrambled.
    let scrambled = vec![0x99u8; aacs::content::ALIGNED_UNIT_LEN];
    assert!(
        !aacs::content::is_clean(&scrambled, libfreemkv::disc::ContentFormat::BdTs),
        "unit with no intact TS syncs must read as scrambled"
    );

    // Too short
    let short = vec![0xFFu8; 100];
    assert!(
        aacs::content::is_clean(&short, libfreemkv::disc::ContentFormat::BdTs),
        "short buffer should not be detected"
    );
}

/// Test: aacs_clear_unit_reports_not_encrypted
///
/// `decrypt_unit` is now PURE (applies the key unconditionally). The "leave a
/// clear unit untouched" policy lives at the caller's gate `aacs_unit_encrypted`:
/// a CPI-clear unit reports not-encrypted, so the caller never hands it to
/// decrypt_unit.
#[test]
fn aacs_clear_unit_reports_not_encrypted() {
    let mut unit = vec![0x42u8; aacs::content::ALIGNED_UNIT_LEN];
    // Intact TS syncs every 192 bytes → not scrambled.
    let mut off = 4;
    while off < aacs::content::ALIGNED_UNIT_LEN {
        unit[off] = 0x47;
        off += 192;
    }
    // CPI bits (byte 0) CLEAR → the authoritative gate reads this as plaintext.
    unit[0] &= 0x3F;

    assert!(aacs::content::is_clean(
        &unit,
        libfreemkv::disc::ContentFormat::BdTs
    ));
    assert!(
        !aacs::content::aacs_unit_encrypted(&unit, libfreemkv::disc::ContentFormat::BdTs),
        "CPI-clear unit reports not-encrypted; the caller never decrypts it"
    );
}

// ── AACS cross-validation with independent AES implementation ──────────────

/// Independent AES-128-ECB encrypt (uses `aes` crate directly, NOT our library).
fn ref_aes_ecb_encrypt(key: &[u8; 16], data: &[u8; 16]) -> [u8; 16] {
    use aes::Aes128;
    use aes::cipher::{BlockEncrypt, KeyInit, generic_array::GenericArray};
    let cipher = Aes128::new(GenericArray::from_slice(key));
    let mut block = GenericArray::clone_from_slice(data);
    cipher.encrypt_block(&mut block);
    let mut out = [0u8; 16];
    out.copy_from_slice(&block);
    out
}

/// Independent AES-128-CBC encrypt (uses `aes` crate directly, NOT our library).
fn ref_aes_cbc_encrypt(key: &[u8; 16], iv: &[u8; 16], data: &mut [u8]) {
    use aes::Aes128;
    use aes::cipher::{BlockEncrypt, KeyInit, generic_array::GenericArray};
    let cipher = Aes128::new(GenericArray::from_slice(key));
    let mut prev = *iv;
    let num_blocks = data.len() / 16;
    for i in 0..num_blocks {
        let off = i * 16;
        for j in 0..16 {
            data[off + j] ^= prev[j];
        }
        let mut block = GenericArray::clone_from_slice(&data[off..off + 16]);
        cipher.encrypt_block(&mut block);
        data[off..off + 16].copy_from_slice(&block);
        prev.copy_from_slice(&data[off..off + 16]);
    }
}

/// The standard AACS IV, copied here independently so we are NOT importing
/// the library's constant — this IS the cross-validation reference value.
const CROSS_AACS_IV: [u8; 16] = [
    0x0B, 0xA0, 0xF8, 0xDD, 0xFE, 0xA6, 0x1F, 0xB3, 0xD8, 0xDF, 0x9F, 0x56, 0x6A, 0x05, 0x0F, 0x78,
];

/// Build a plaintext aligned unit with TS sync markers and recognisable
/// content, encrypt it using only the `aes` crate (independent of the
/// library), then decrypt with `decrypt_unit()` and verify the match.
#[test]
fn aacs_cross_validation_encrypt_then_decrypt() {
    let unit_key: [u8; 16] = [
        0x01, 0x23, 0x45, 0x67, 0x89, 0xAB, 0xCD, 0xEF, 0xFE, 0xDC, 0xBA, 0x98, 0x76, 0x54, 0x32,
        0x10,
    ];

    let mut plaintext = vec![0u8; aacs::content::ALIGNED_UNIT_LEN];
    // TS sync bytes every 192 bytes starting at offset 4
    let mut off = 4;
    while off < aacs::content::ALIGNED_UNIT_LEN {
        plaintext[off] = 0x47;
        off += 192;
    }
    // Fill the rest with a recognisable pattern (prime modulus avoids artefacts).
    // Index-arithmetic — clearer with a counted loop than an enumerate chain.
    #[allow(clippy::needless_range_loop)]
    for i in 16..aacs::content::ALIGNED_UNIT_LEN {
        if plaintext[i] == 0 {
            plaintext[i] = (i % 251) as u8;
        }
    }
    // Flag the unit encrypted via the CPI bits (byte 0) — the gate `decrypt_unit`
    // consults. Set before snapshotting `expected`; decrypt preserves the header.
    plaintext[0] |= 0xC0;

    let expected = plaintext.clone();

    // -- Encrypt with independent implementation --
    let mut header = [0u8; 16];
    header.copy_from_slice(&plaintext[..16]);
    let derived = ref_aes_ecb_encrypt(&unit_key, &header);
    let mut dk = [0u8; 16];
    for i in 0..16 {
        dk[i] = derived[i] ^ header[i];
    }
    ref_aes_cbc_encrypt(
        &dk,
        &CROSS_AACS_IV,
        &mut plaintext[16..aacs::content::ALIGNED_UNIT_LEN],
    );

    // Sanity: ciphertext should differ
    assert_ne!(
        &plaintext[16..32],
        &expected[16..32],
        "encryption did not change ciphertext region"
    );

    // -- Decrypt with the library --
    aacs::content::decrypt_unit(&mut plaintext, &unit_key);

    // Decryption clears no flag, so the unit round-trips byte-for-byte.
    assert_eq!(
        &plaintext[..],
        &expected[..],
        "decrypted unit does not match original plaintext"
    );
}

/// Same cross-validation with a different key and all-0xFF payload to
/// exercise different AES round-key schedules.
#[test]
fn aacs_cross_validation_alternate_key() {
    let unit_key: [u8; 16] = [
        0xDE, 0xAD, 0xBE, 0xEF, 0xCA, 0xFE, 0xBA, 0xBE, 0x01, 0x02, 0x03, 0x04, 0x05, 0x06, 0x07,
        0x08,
    ];

    let mut plaintext = vec![0xFFu8; aacs::content::ALIGNED_UNIT_LEN];
    let mut off = 4;
    while off < aacs::content::ALIGNED_UNIT_LEN {
        plaintext[off] = 0x47;
        off += 192;
    }
    // No flag set: the CBC-encrypted body scrambles the packet syncs.
    let expected = plaintext.clone();

    let mut header = [0u8; 16];
    header.copy_from_slice(&plaintext[..16]);
    let derived = ref_aes_ecb_encrypt(&unit_key, &header);
    let mut dk = [0u8; 16];
    for i in 0..16 {
        dk[i] = derived[i] ^ header[i];
    }
    ref_aes_cbc_encrypt(
        &dk,
        &CROSS_AACS_IV,
        &mut plaintext[16..aacs::content::ALIGNED_UNIT_LEN],
    );

    aacs::content::decrypt_unit(&mut plaintext, &unit_key);

    // Decryption clears no flag, so the unit round-trips byte-for-byte.
    assert_eq!(&plaintext[..], &expected[..]);
}

// (`decrypt_bus` is a crate-internal layer — its cross-validation lives in-crate
// in `aacs::content`'s unit tests, not here.)

// ── CSS roundtrip test vectors ─────────────────────────────────────────────

/// CSS descramble modifies encrypted region deterministically.
#[test]
fn css_roundtrip_with_snapshot() {
    let title_key: [u8; 5] = [0x42, 0x13, 0x37, 0xBE, 0xEF];

    let mut sector = vec![0x00u8; 2048];
    sector[0x14] = 0x30;
    sector[0x54..0x59].copy_from_slice(&[0xDE, 0xAD, 0xBE, 0xEF, 0x42]);
    #[allow(clippy::needless_range_loop)]
    for i in 0x80..2048 {
        sector[i] = ((i * 7 + 3) & 0xFF) as u8;
    }
    let original = sector.clone();

    css::lfsr::descramble_sector(&title_key, &mut sector);
    assert_eq!(sector[0x14] & 0x30, 0x00, "flag not cleared");
    assert_ne!(&sector[0x80..0xA0], &original[0x80..0xA0]);

    // Determinism: same input → same output
    let mut sector2 = original.clone();
    css::lfsr::descramble_sector(&title_key, &mut sector2);
    assert_eq!(
        &sector[0x80..2048],
        &sector2[0x80..2048],
        "not deterministic"
    );
}

/// Multiple key/seed combinations produce different outputs.
#[test]
fn css_roundtrip_multiple_keys() {
    // The content cipher (dvdcss_unscramble) seeds its LFSRs from
    // `key XOR seed`, so the cases must have DISTINCT `key XOR seed` values to
    // produce distinct keystreams. (key=0/seed=0 and key=FF/seed=FF both give
    // key^seed=0 and are correctly identical — excluded here.)
    let cases: &[([u8; 5], [u8; 5])] = &[
        (
            [0x00, 0x00, 0x00, 0x00, 0x00],
            [0x00, 0x00, 0x00, 0x00, 0x00],
        ),
        (
            [0xFF, 0xFF, 0xFF, 0xFF, 0xFF],
            [0x00, 0x00, 0x00, 0x00, 0x00],
        ),
        (
            [0x01, 0x02, 0x03, 0x04, 0x05],
            [0xAA, 0xBB, 0xCC, 0xDD, 0xEE],
        ),
        (
            [0xAB, 0xCD, 0xEF, 0x01, 0x23],
            [0x12, 0x34, 0x56, 0x78, 0x9A],
        ),
    ];

    let mut results = Vec::new();
    for (idx, (key, seed)) in cases.iter().enumerate() {
        let mut sector = vec![0xAAu8; 2048];
        sector[0x14] = 0x30;
        sector[0x54..0x59].copy_from_slice(seed);

        css::lfsr::descramble_sector(key, &mut sector);
        assert_eq!(sector[0x14] & 0x30, 0x00, "case {}: flag not cleared", idx);
        results.push(sector[0x80..0xA0].to_vec());
    }
    // Different keys/seeds should produce different outputs
    for i in 0..results.len() {
        for j in (i + 1)..results.len() {
            assert_ne!(
                results[i], results[j],
                "cases {} and {} produced same output",
                i, j
            );
        }
    }
}

// ── CSS Stevenson attack tests ─────────────────────────────────────────────

/// Attempt the Stevenson attack on synthetically scrambled sectors.
///
/// The CSS cipher on real DVDs stores ciphertext through a TAB1 output
/// layer that the Stevenson attack depends on. Synthetically scrambled
/// sectors (produced by calling descramble_sector on plaintext) may not
/// exhibit this relationship, so the attack is not guaranteed to converge
/// on synthetic data. This test verifies that when the attack DOES return
/// a key, that key correctly descrambles the sector.
#[test]
fn css_stevenson_attack_validates_cracked_key() {
    let candidates: &[([u8; 5], [u8; 5])] = &[
        (
            [0x42, 0x13, 0x37, 0xBE, 0xEF],
            [0x11, 0x22, 0x33, 0x44, 0x55],
        ),
        (
            [0x01, 0x02, 0x03, 0x04, 0x05],
            [0xAA, 0xBB, 0xCC, 0xDD, 0xEE],
        ),
        (
            [0x10, 0x20, 0x30, 0x40, 0x50],
            [0x05, 0x06, 0x07, 0x08, 0x09],
        ),
        (
            [0xAB, 0xCD, 0xEF, 0x01, 0x23],
            [0x12, 0x34, 0x56, 0x78, 0x9A],
        ),
        (
            [0x55, 0xAA, 0x55, 0xAA, 0x55],
            [0x00, 0x00, 0x00, 0x00, 0x00],
        ),
    ];

    let mut any_cracked = false;

    for (key, seed) in candidates {
        let mut sector = vec![0x00u8; 2048];
        sector[0x14] = 0x30;
        sector[0x54..0x59].copy_from_slice(seed);
        sector[0x80] = 0x00;
        sector[0x81] = 0x00;
        sector[0x82] = 0x01;
        sector[0x83] = 0xE0;
        sector[0x84] = 0x00;
        sector[0x85] = 0x00;
        sector[0x86] = 0x80;
        sector[0x87] = 0x80;
        sector[0x88] = 0x05;
        sector[0x89] = 0x21;

        let original = sector.clone();

        // "Encrypt" by descrambling plaintext (XOR keystream)
        css::lfsr::descramble_sector(key, &mut sector);
        sector[0x14] = 0x30;

        let cracked = css::stevenson::crack_title_key(&sector);

        if let Some(cracked_key) = cracked {
            let mut test = sector.clone();
            css::lfsr::descramble_sector(&cracked_key, &mut test);

            assert_eq!(test[0x80], 0x00, "PES byte 0 mismatch");
            assert_eq!(test[0x81], 0x00, "PES byte 1 mismatch");
            assert_eq!(test[0x82], 0x01, "PES byte 2 mismatch");
            assert_eq!(test[0x83], 0xE0, "PES byte 3 mismatch");
            assert_eq!(
                &test[0x80..2048],
                &original[0x80..2048],
                "cracked key did not recover original plaintext"
            );

            any_cracked = true;
            eprintln!(
                "Stevenson attack succeeded: key={:02X?} seed={:02X?} cracked={:02X?}",
                key, seed, cracked_key
            );
        }
    }

    if !any_cracked {
        eprintln!(
            "Stevenson attack did not converge on any synthetic key/seed pair. \
             This is expected: synthetic sectors lack the TAB1 output encoding \
             present in real CSS-encrypted DVD sectors."
        );
        // Never let this test pass vacuously: when the attack can't converge on
        // synthetic data, still assert always-true properties of the CSS keystream
        // so a real regression is caught on every run — descramble_sector is
        // DETERMINISTIC (same key/seed/data → same output) and NON-TRIVIAL (it
        // actually transforms the payload, not a silent no-op).
        for (key, seed) in candidates {
            let mut base = vec![0x00u8; 2048];
            base[0x14] = 0x30;
            base[0x54..0x59].copy_from_slice(seed);
            base[0x80..0x8A]
                .copy_from_slice(&[0x00, 0x00, 0x01, 0xE0, 0x00, 0x00, 0x80, 0x80, 0x05, 0x21]);
            let mut a = base.clone();
            let mut b = base.clone();
            css::lfsr::descramble_sector(key, &mut a);
            css::lfsr::descramble_sector(key, &mut b);
            assert_eq!(a, b, "descramble must be deterministic for key={key:02X?}");
            assert_ne!(
                &a[0x80..2048],
                &base[0x80..2048],
                "descramble must transform the payload for key={key:02X?}"
            );
        }
    }
}

/// Verify that `recover_title_key` works when given exact known plaintext,
/// even for combinations where `crack_title_key` (which guesses the pattern)
/// might not converge.
#[test]
fn css_recover_title_key_with_exact_plaintext() {
    let title_key: [u8; 5] = [0x42, 0x13, 0x37, 0xBE, 0xEF];
    let seed: [u8; 5] = [0x11, 0x22, 0x33, 0x44, 0x55];

    let mut sector = vec![0x00u8; 2048];
    sector[0x14] = 0x30;
    sector[0x54..0x59].copy_from_slice(&seed);
    let pes_header: [u8; 10] = [0x00, 0x00, 0x01, 0xE0, 0x00, 0x00, 0x80, 0x80, 0x05, 0x21];
    sector[0x80..0x8A].copy_from_slice(&pes_header);
    #[allow(clippy::needless_range_loop)]
    for i in 0x8A..2048 {
        sector[i] = ((i * 13 + 7) & 0xFF) as u8;
    }
    let original = sector.clone();

    // Scramble
    css::lfsr::descramble_sector(&title_key, &mut sector);
    sector[0x14] = 0x30;

    // Recover with exact known plaintext
    let recovered = css::stevenson::recover_title_key(&sector, &pes_header);

    if let Some(rkey) = recovered {
        let mut test = sector.clone();
        css::lfsr::descramble_sector(&rkey, &mut test);
        assert_eq!(
            &test[0x80..2048],
            &original[0x80..2048],
            "recovered key did not produce correct plaintext"
        );
        eprintln!("recover_title_key succeeded: {:02X?}", rkey);
    } else {
        eprintln!(
            "recover_title_key returned None for key={:02X?} seed={:02X?}. \
             The LFSR0 recovery phase may not converge for this combination.",
            title_key, seed
        );
        // Never pass vacuously: when LFSR0 recovery can't converge on this
        // synthetic sector, still assert always-true properties of the cipher so
        // a real regression is caught on every run — descramble_sector is
        // DETERMINISTIC and NON-TRIVIAL (actually transforms the payload).
        let mut a = original.clone();
        let mut b = original.clone();
        css::lfsr::descramble_sector(&title_key, &mut a);
        css::lfsr::descramble_sector(&title_key, &mut b);
        assert_eq!(a, b, "descramble must be deterministic");
        assert_ne!(
            &a[0x80..2048],
            &original[0x80..2048],
            "descramble must transform the payload"
        );
    }
}

/// Test: aacs_parse_unit_key_ro with minimal valid data
#[test]
fn aacs_parse_unit_key_ro_minimal() {
    // Build a minimal Unit_Key_RO.inf structure
    // Header: first 4 bytes = BE32 offset to key storage area
    let uk_pos: u32 = 100;
    let mut data = vec![0u8; 200];

    // Key storage offset
    data[0..4].copy_from_slice(&uk_pos.to_be_bytes());
    // app_type
    data[16] = 1; // BD-ROM
    // num_bdmv_dir
    data[17] = 1;
    // flags
    data[18] = 0;

    // At uk_pos: num_unit_keys = 1
    let pos = uk_pos as usize;
    data[pos] = 0;
    data[pos + 1] = 1; // 1 key

    // At uk_pos + 48: first encrypted key (16 bytes)
    let key_pos = pos + 48;
    for i in 0..16 {
        data[key_pos + i] = (0xA0 + i) as u8;
    }

    let result = aacs::inf::parse_unit_key_ro(&data, aacs::mkb::AacsVersion::V10);
    assert!(
        result.is_some(),
        "parse_unit_key_ro should succeed on valid data"
    );

    let ukf = result.unwrap();
    assert_eq!(ukf.app_type, 1);
    assert_eq!(ukf.num_bdmv_dir, 1);
    assert_eq!(ukf.encrypted_keys.len(), 1);
    assert_eq!(ukf.disc_hash.len(), 20);

    // disc_hash should be deterministic
    let hash = aacs::inf::disc_hash(&data);
    assert_eq!(ukf.disc_hash, hash);
}
