//! NIST ACVP test vectors for arcana.
//!
//! Vectors sourced from https://github.com/usnistgov/ACVP-Server
//! (gen-val/json-files/). Format: prompt.json (inputs) +
//! expectedResults.json (expected outputs), merged by tgId/tcId.
//!
//! Run with:
//!   cargo test --release -p arcana --test acvp

use arcana::BlockCipher;

// Reuse the zero-dep JSON parser from the wycheproof test.
mod serde {
    use std::collections::HashMap;

    #[derive(Debug, Clone)]
    pub enum Value {
        Null,
        Number(f64),
        Str(String),
        Array(Vec<Value>),
        Object(HashMap<String, Value>),
    }

    impl Value {
        pub fn get_str(&self, key: &str) -> &str {
            match self {
                Value::Object(map) => match map.get(key) {
                    Some(Value::Str(s)) => s,
                    _ => "",
                },
                _ => "",
            }
        }
        pub fn get_array(&self, key: &str) -> &[Value] {
            match self {
                Value::Object(map) => match map.get(key) {
                    Some(Value::Array(a)) => a,
                    _ => &[],
                },
                _ => &[],
            }
        }
        pub fn get_u64(&self, key: &str) -> u64 {
            match self {
                Value::Object(map) => match map.get(key) {
                    Some(Value::Number(n)) => *n as u64,
                    _ => 0,
                },
                _ => 0,
            }
        }
        pub fn get(&self, key: &str) -> Option<&Value> {
            match self {
                Value::Object(map) => map.get(key),
                _ => None,
            }
        }
    }

    pub fn read_json(path: &str) -> Value {
        let content = std::fs::read_to_string(path).unwrap_or_else(|_| panic!("Cannot read {}", path));
        let mut chars = content.chars().peekable();
        parse_value(&mut chars)
    }

    fn skip_ws(c: &mut std::iter::Peekable<std::str::Chars>) {
        while let Some(&ch) = c.peek() {
            if ch.is_whitespace() {
                c.next();
            } else {
                break;
            }
        }
    }

    fn parse_value(c: &mut std::iter::Peekable<std::str::Chars>) -> Value {
        skip_ws(c);
        match c.peek() {
            Some('"') => Value::Str(parse_string(c)),
            Some('{') => parse_object(c),
            Some('[') => parse_array(c),
            Some('t') => {
                for _ in 0..4 {
                    c.next();
                }
                Value::Str("true".into())
            }
            Some('f') => {
                for _ in 0..5 {
                    c.next();
                }
                Value::Str("false".into())
            }
            Some('n') => {
                for _ in 0..4 {
                    c.next();
                }
                Value::Null
            }
            Some(ch) if *ch == '-' || ch.is_ascii_digit() => parse_number(c),
            other => panic!("Unexpected: {:?}", other),
        }
    }

    fn parse_string(c: &mut std::iter::Peekable<std::str::Chars>) -> String {
        c.next();
        let mut s = String::new();
        loop {
            match c.next() {
                Some('"') => return s,
                Some('\\') => match c.next() {
                    Some('n') => s.push('\n'),
                    Some('t') => s.push('\t'),
                    Some('"') => s.push('"'),
                    Some('\\') => s.push('\\'),
                    Some('/') => s.push('/'),
                    Some('u') => {
                        let h: String = c.take(4).collect();
                        let cp = u32::from_str_radix(&h, 16).unwrap();
                        if let Some(ch) = char::from_u32(cp) {
                            s.push(ch);
                        }
                    }
                    other => panic!("Esc: {:?}", other),
                },
                Some(ch) => s.push(ch),
                None => panic!("Unterminated string"),
            }
        }
    }

    fn parse_number(c: &mut std::iter::Peekable<std::str::Chars>) -> Value {
        let mut s = String::new();
        while let Some(&ch) = c.peek() {
            if "-+.eE".contains(ch) || ch.is_ascii_digit() {
                s.push(ch);
                c.next();
            } else {
                break;
            }
        }
        Value::Number(s.parse().unwrap())
    }

    fn parse_object(c: &mut std::iter::Peekable<std::str::Chars>) -> Value {
        c.next();
        let mut map = std::collections::HashMap::new();
        skip_ws(c);
        if c.peek() == Some(&'}') {
            c.next();
            return Value::Object(map);
        }
        loop {
            skip_ws(c);
            let key = parse_string(c);
            skip_ws(c);
            c.next();
            let val = parse_value(c);
            map.insert(key, val);
            skip_ws(c);
            match c.peek() {
                Some(',') => {
                    c.next();
                }
                Some('}') => {
                    c.next();
                    return Value::Object(map);
                }
                o => panic!("Exp , or }} got {:?}", o),
            }
        }
    }

    fn parse_array(c: &mut std::iter::Peekable<std::str::Chars>) -> Value {
        c.next();
        let mut arr = Vec::new();
        skip_ws(c);
        if c.peek() == Some(&']') {
            c.next();
            return Value::Array(arr);
        }
        loop {
            arr.push(parse_value(c));
            skip_ws(c);
            match c.peek() {
                Some(',') => {
                    c.next();
                }
                Some(']') => {
                    c.next();
                    return Value::Array(arr);
                }
                o => panic!("Exp , or ] got {:?}", o),
            }
        }
    }
}

fn hex(s: &str) -> Vec<u8> {
    let s: String = s.chars().filter(|c| !c.is_whitespace()).collect();
    (0..s.len())
        .step_by(2)
        .map(|i| u8::from_str_radix(&s[i..i + 2], 16).unwrap())
        .collect()
}

/// Load prompt + expectedResults, return the root Value for each.
/// ACVP files are either a bare object or a 2-element array [header, data].
fn load_acvp(dir: &str) -> (serde::Value, serde::Value) {
    let prompt_raw = serde::read_json(&format!("{}/prompt.json", dir));
    let result_raw = serde::read_json(&format!("{}/expectedResults.json", dir));

    let prompt = match &prompt_raw {
        serde::Value::Array(a) if a.len() == 2 => a[1].clone(),
        _ => prompt_raw,
    };
    let result = match &result_raw {
        serde::Value::Array(a) if a.len() == 2 => a[1].clone(),
        _ => result_raw,
    };
    (prompt, result)
}

/// Build a map from (tgId, tcId) → expected result Value.
fn build_result_map(result: &serde::Value) -> std::collections::HashMap<(u64, u64), serde::Value> {
    let mut map = std::collections::HashMap::new();
    for tg in result.get_array("testGroups") {
        let tg_id = tg.get_u64("tgId");
        for tc in tg.get_array("tests") {
            let tc_id = tc.get_u64("tcId");
            map.insert((tg_id, tc_id), tc.clone());
        }
    }
    map
}

// =====================================================================
// SHA3-256, SHA3-512
// =====================================================================

fn acvp_sha3_impl<H: arcana::Hasher>(dir: &str, label: &str) {
    let (prompt, result) = load_acvp(dir);
    let rmap = build_result_map(&result);
    let mut count = 0u32;

    for tg in prompt.get_array("testGroups") {
        let tg_id = tg.get_u64("tgId");
        let test_type = tg.get_str("testType");
        if test_type != "AFT" {
            continue; // only Algorithmic Functional Tests; skip MCT/LDT
        }
        for tc in tg.get_array("tests") {
            let tc_id = tc.get_u64("tcId");
            let msg_hex = tc.get_str("msg");
            if msg_hex.is_empty() && tc.get_u64("len") == 0 {
                // len=0 → empty message
            }
            let len_bits = tc.get_u64("len") as usize;

            let expected = rmap.get(&(tg_id, tc_id)).unwrap();
            let md = hex(expected.get_str("md"));

            // ACVP SHA3-2.0 vectors are bit-oriented; skip non-byte-aligned.
            if len_bits % 8 != 0 {
                continue;
            }
            let msg = hex(msg_hex);
            let len_bytes = len_bits / 8;
            if msg.len() < len_bytes {
                continue;
            }

            let digest = H::hash(&msg[..len_bytes]);
            assert_eq!(digest, md, "{} tcId={}: mismatch", label, tc_id);
            count += 1;
        }
    }

    println!("ACVP {}: {} vectors passed", label, count);
    assert!(count > 0, "expected > 0 {} vectors", label);
}

#[test]
fn acvp_sha3_256() {
    acvp_sha3_impl::<arcana::hash::sha3::Sha3_256>("tests/acvp/SHA3-256-2.0", "SHA3-256");
}

#[test]
fn acvp_sha3_512() {
    acvp_sha3_impl::<arcana::hash::sha3::Sha3_512>("tests/acvp/SHA3-512-2.0", "SHA3-512");
}

// =====================================================================
// AES-CTR
// =====================================================================

#[test]
fn acvp_aes_ctr() {
    let (prompt, result) = load_acvp("tests/acvp/ACVP-AES-CTR-1.0");
    let rmap = build_result_map(&result);
    let mut count = 0u32;

    for tg in prompt.get_array("testGroups") {
        let tg_id = tg.get_u64("tgId");
        let direction = tg.get_str("direction");
        let key_len = tg.get_u64("keyLen") as usize / 8;

        for tc in tg.get_array("tests") {
            let tc_id = tc.get_u64("tcId");
            let key = hex(tc.get_str("key"));
            let pt_hex = tc.get_str("pt");

            if key.len() != key_len {
                continue;
            }

            let exp = rmap.get(&(tg_id, tc_id)).unwrap();
            let iv = hex(exp.get_str("iv"));

            if iv.len() != 16 {
                continue;
            }

            // payloadLen is in bits; skip non-byte-aligned.
            let payload_bits = tc.get_u64("payloadLen") as usize;
            if payload_bits % 8 != 0 {
                continue;
            }

            use arcana::cipher::ctx::{Algorithm, Cipher, Direction, Mode, Padding};
            let algo = match key.len() {
                16 => Algorithm::Aes128,
                24 => Algorithm::Aes192,
                32 => Algorithm::Aes256,
                _ => continue,
            };

            if direction == "encrypt" {
                let pt = hex(pt_hex);
                let ct_expected = hex(exp.get_str("ct"));
                let mut c = Cipher::new(algo, Mode::Ctr, Padding::None).unwrap();
                c.init(Direction::Encrypt, &key, &iv).unwrap();
                let mut ct = vec![0u8; pt.len() + 16];
                let mut n = c.update(&pt, &mut ct).unwrap();
                n += c.finalize(&mut ct[n..]).unwrap();
                ct.truncate(n);
                assert_eq!(ct, ct_expected, "AES-CTR enc tcId={}", tc_id);
                count += 1;
            } else {
                let ct_hex = tc.get_str("ct");
                if ct_hex.is_empty() {
                    continue;
                }
                let ct = hex(ct_hex);
                let pt_expected = hex(exp.get_str("pt"));
                let mut c = Cipher::new(algo, Mode::Ctr, Padding::None).unwrap();
                c.init(Direction::Decrypt, &key, &iv).unwrap();
                let mut pt = vec![0u8; ct.len() + 16];
                let mut n = c.update(&ct, &mut pt).unwrap();
                n += c.finalize(&mut pt[n..]).unwrap();
                pt.truncate(n);
                assert_eq!(pt, pt_expected, "AES-CTR dec tcId={}", tc_id);
                count += 1;
            }
        }
    }

    println!("ACVP AES-CTR: {} vectors passed", count);
    assert!(count > 20, "expected > 20 AES-CTR vectors");
}

// =====================================================================
// HMAC-SHA3-256, HMAC-SHA3-512
// =====================================================================

fn acvp_hmac_sha3_impl(dir: &str, algo: arcana::mac::ctx::Algorithm, label: &str) {
    use arcana::mac::ctx::Mac;

    let (prompt, result) = load_acvp(dir);
    let rmap = build_result_map(&result);
    let mut count = 0u32;

    for tg in prompt.get_array("testGroups") {
        let tg_id = tg.get_u64("tgId");
        for tc in tg.get_array("tests") {
            let tc_id = tc.get_u64("tcId");
            let key = hex(tc.get_str("key"));
            let msg = hex(tc.get_str("msg"));

            let exp = rmap.get(&(tg_id, tc_id)).unwrap();
            let expected_mac = hex(exp.get_str("mac"));

            let mut m = Mac::new(algo);
            m.init(&key).unwrap();
            m.update(&msg).unwrap();
            let full = m.sign_to_vec().unwrap();

            // ACVP may truncate; compare only expected_mac.len() bytes.
            let truncated = &full[..expected_mac.len()];
            assert_eq!(truncated, expected_mac.as_slice(), "{} tcId={}: mismatch", label, tc_id,);
            count += 1;
        }
    }

    println!("ACVP {}: {} vectors passed", label, count);
    assert!(count > 0, "expected > 0 {} vectors", label);
}

#[test]
fn acvp_hmac_sha3_256() {
    acvp_hmac_sha3_impl(
        "tests/acvp/HMAC-SHA3-256-2.0",
        arcana::mac::ctx::Algorithm::HmacSha3_256,
        "HMAC-SHA3-256",
    );
}

#[test]
fn acvp_hmac_sha3_512() {
    acvp_hmac_sha3_impl(
        "tests/acvp/HMAC-SHA3-512-2.0",
        arcana::mac::ctx::Algorithm::HmacSha3_512,
        "HMAC-SHA3-512",
    );
}

// =====================================================================
// ECDSA SigVer FIPS 186-5 (P-256 + P-384 + P-521 groups)
// =====================================================================

#[test]
fn acvp_ecdsa_sigver() {
    use arcana::Hasher;
    use arcana::ecc::curves::{Curve, P256, P384, P521, PublicKey};
    use arcana::ecc::ecdsa::Signature;

    let (prompt, result) = load_acvp("tests/acvp/ECDSA-SigVer-FIPS186-5");
    let rmap = build_result_map(&result);
    let mut pass = 0u32;
    let mut skipped = 0u32;

    for tg in prompt.get_array("testGroups") {
        let tg_id = tg.get_u64("tgId");
        let curve = tg.get_str("curve");
        let hash_alg = tg.get_str("hashAlg");

        for tc in tg.get_array("tests") {
            let tc_id = tc.get_u64("tcId");
            let msg = hex(tc.get_str("message"));
            let qx = hex(tc.get_str("qx"));
            let qy = hex(tc.get_str("qy"));
            let r = hex(tc.get_str("r"));
            let s = hex(tc.get_str("s"));

            let exp = rmap.get(&(tg_id, tc_id)).unwrap();
            let test_passed = exp.get_str("testPassed").to_lowercase() == "true";

            // Build uncompressed public key: 0x04 || qx || qy.
            let coord_len = match curve {
                "P-256" => 32,
                "P-384" => 48,
                "P-521" => 66,
                _ => {
                    skipped += 1;
                    continue;
                }
            };

            // Pad qx/qy to coord_len if needed.
            let mut pk_bytes = vec![0x04u8];
            let mut qx_padded = vec![0u8; coord_len];
            let start = coord_len.saturating_sub(qx.len());
            qx_padded[start..].copy_from_slice(&qx);
            pk_bytes.extend_from_slice(&qx_padded);
            let mut qy_padded = vec![0u8; coord_len];
            let start = coord_len.saturating_sub(qy.len());
            qy_padded[start..].copy_from_slice(&qy);
            pk_bytes.extend_from_slice(&qy_padded);

            let pk = PublicKey { bytes: pk_bytes };
            let sig = Signature { r, s };

            // Hash the message.
            let digest = match hash_alg {
                "SHA2-256" => arcana::hash::sha256::Sha256::hash(&msg),
                "SHA2-384" => arcana::hash::sha384::Sha384::hash(&msg),
                "SHA2-512" => arcana::hash::sha512::Sha512::hash(&msg),
                _ => {
                    skipped += 1;
                    continue;
                }
            };

            let ok = match curve {
                "P-256" => P256::verify(&pk, &digest, &sig),
                "P-384" => P384::verify(&pk, &digest, &sig),
                "P-521" => P521::verify(&pk, &digest, &sig),
                _ => {
                    skipped += 1;
                    continue;
                }
            };

            assert_eq!(
                ok, test_passed,
                "ECDSA SigVer tcId={}: curve={} hash={} expected={} got={}",
                tc_id, curve, hash_alg, test_passed, ok,
            );
            pass += 1;
        }
    }

    println!("ACVP ECDSA SigVer: {} passed, {} skipped", pass, skipped);
    assert!(pass > 20, "expected > 20 ECDSA SigVer vectors");
}

// =====================================================================
// AES-CBC (ACVP vectors — independent from CAVP MMT)
// =====================================================================

#[test]
fn acvp_aes_cbc() {
    use arcana::cipher::aes::Aes;
    use arcana::cipher::modes::{cbc_decrypt, cbc_encrypt};

    let (prompt, result) = load_acvp("tests/acvp/ACVP-AES-CBC-1.0");
    let rmap = build_result_map(&result);
    let mut count = 0u32;

    for tg in prompt.get_array("testGroups") {
        let tg_id = tg.get_u64("tgId");
        let direction = tg.get_str("direction");
        let test_type = tg.get_str("testType");
        if test_type == "MCT" {
            continue;
        }

        for tc in tg.get_array("tests") {
            let tc_id = tc.get_u64("tcId");
            let key = hex(tc.get_str("key"));
            let iv = hex(tc.get_str("iv"));
            let exp = rmap.get(&(tg_id, tc_id)).unwrap();
            let cipher = Aes::new(&key);

            if direction == "encrypt" {
                let mut data = hex(tc.get_str("pt"));
                let ct_expected = hex(exp.get_str("ct"));
                cbc_encrypt(&cipher, &iv, &mut data);
                assert_eq!(data, ct_expected, "ACVP AES-CBC enc tcId={}", tc_id);
            } else {
                let mut data = hex(tc.get_str("ct"));
                let pt_expected = hex(exp.get_str("pt"));
                cbc_decrypt(&cipher, &iv, &mut data);
                assert_eq!(data, pt_expected, "ACVP AES-CBC dec tcId={}", tc_id);
            }
            count += 1;
        }
    }

    println!("ACVP AES-CBC: {} vectors passed", count);
    assert!(count > 100, "expected > 100 AES-CBC vectors");
}

// =====================================================================
// AES-ECB (ACVP vectors)
// =====================================================================

#[test]
fn acvp_aes_ecb() {
    use arcana::cipher::aes::Aes;
    use arcana::cipher::modes::{ecb_decrypt, ecb_encrypt};

    let (prompt, result) = load_acvp("tests/acvp/ACVP-AES-ECB-1.0");
    let rmap = build_result_map(&result);
    let mut count = 0u32;

    for tg in prompt.get_array("testGroups") {
        let tg_id = tg.get_u64("tgId");
        let direction = tg.get_str("direction");
        let test_type = tg.get_str("testType");
        if test_type == "MCT" {
            continue;
        }

        for tc in tg.get_array("tests") {
            let tc_id = tc.get_u64("tcId");
            let key = hex(tc.get_str("key"));
            let exp = rmap.get(&(tg_id, tc_id)).unwrap();
            let cipher = Aes::new(&key);

            if direction == "encrypt" {
                let mut data = hex(tc.get_str("pt"));
                let ct_expected = hex(exp.get_str("ct"));
                ecb_encrypt(&cipher, &mut data);
                assert_eq!(data, ct_expected, "ACVP AES-ECB enc tcId={}", tc_id);
            } else {
                let mut data = hex(tc.get_str("ct"));
                let pt_expected = hex(exp.get_str("pt"));
                ecb_decrypt(&cipher, &mut data);
                assert_eq!(data, pt_expected, "ACVP AES-ECB dec tcId={}", tc_id);
            }
            count += 1;
        }
    }

    println!("ACVP AES-ECB: {} vectors passed", count);
    assert!(count > 100, "expected > 100 AES-ECB vectors");
}

// =====================================================================
// AES-GCM (ACVP vectors — independent from Wycheproof/CAVP GCM)
// =====================================================================

#[test]
fn acvp_aes_gcm() {
    use arcana::cipher::aes::{Aes128, Aes192, Aes256};
    use arcana::cipher::modes::Gcm;

    let (prompt, result) = load_acvp("tests/acvp/ACVP-AES-GCM-1.0");
    let rmap = build_result_map(&result);
    let mut count = 0u32;

    for tg in prompt.get_array("testGroups") {
        let tg_id = tg.get_u64("tgId");
        let direction = tg.get_str("direction");
        let iv_len = tg.get_u64("ivLen") as usize;
        let tag_len = tg.get_u64("tagLen") as usize;

        if iv_len != 96 || tag_len != 128 {
            continue;
        }

        for tc in tg.get_array("tests") {
            let tc_id = tc.get_u64("tcId");
            let key = hex(tc.get_str("key"));
            let iv = hex(tc.get_str("iv"));
            let aad = hex(tc.get_str("aad"));
            let exp = rmap.get(&(tg_id, tc_id)).unwrap();
            let nonce: [u8; 12] = iv.try_into().unwrap();

            if direction == "encrypt" {
                let pt = hex(tc.get_str("pt"));
                let ct_expected = hex(exp.get_str("ct"));
                let tag_expected = hex(exp.get_str("tag"));

                let (ct, tag) = match key.len() {
                    16 => {
                        let c = Aes128::new(&key);
                        Gcm::encrypt(&c, &nonce, &aad, &pt)
                    }
                    24 => {
                        let c = Aes192::new(&key);
                        Gcm::encrypt(&c, &nonce, &aad, &pt)
                    }
                    32 => {
                        let c = Aes256::new(&key);
                        Gcm::encrypt(&c, &nonce, &aad, &pt)
                    }
                    _ => continue,
                };
                assert_eq!(ct, ct_expected, "ACVP GCM enc ct tcId={}", tc_id);
                assert_eq!(tag.to_vec(), tag_expected, "ACVP GCM enc tag tcId={}", tc_id);
            } else {
                let ct = hex(tc.get_str("ct"));
                let tag_hex = hex(tc.get_str("tag"));
                let tag: [u8; 16] = tag_hex.try_into().unwrap();

                // AFT decrypt: if expectedResults has "pt", decrypt
                // should succeed. If it has "testPassed: false",
                // decrypt should fail.
                let tp_str = exp.get_str("testPassed");
                let expect_success = if !tp_str.is_empty() {
                    tp_str.to_lowercase() == "true"
                } else {
                    exp.get("pt").is_some() // pt present → success expected
                };

                let pt_opt = match key.len() {
                    16 => {
                        let c = Aes128::new(&key);
                        Gcm::decrypt(&c, &nonce, &aad, &ct, &tag)
                    }
                    24 => {
                        let c = Aes192::new(&key);
                        Gcm::decrypt(&c, &nonce, &aad, &ct, &tag)
                    }
                    32 => {
                        let c = Aes256::new(&key);
                        Gcm::decrypt(&c, &nonce, &aad, &ct, &tag)
                    }
                    _ => continue,
                };
                assert_eq!(pt_opt.is_some(), expect_success, "ACVP GCM dec tcId={}", tc_id);
                if let (Some(pt), true) = (&pt_opt, expect_success) {
                    let pt_expected = hex(exp.get_str("pt"));
                    assert_eq!(*pt, pt_expected, "ACVP GCM dec pt tcId={}", tc_id);
                }
            }
            count += 1;
        }
    }

    println!("ACVP AES-GCM: {} vectors passed", count);
    assert!(count > 0, "expected > 0 AES-GCM vectors");
}

// =====================================================================
// CMAC-AES (ACVP vectors)
// =====================================================================

#[test]
fn acvp_cmac_aes() {
    use arcana::mac::ctx::{Algorithm, Mac};

    let (prompt, result) = load_acvp("tests/acvp/CMAC-AES-1.0");
    let rmap = build_result_map(&result);
    let mut count = 0u32;

    for tg in prompt.get_array("testGroups") {
        let tg_id = tg.get_u64("tgId");
        let direction = tg.get_str("direction");
        let key_len = tg.get_u64("keyLen") as usize / 8;
        let mac_len = tg.get_u64("macLen") as usize / 8;
        let msg_len = tg.get_u64("msgLen") as usize / 8;

        let algo = match key_len {
            16 => Algorithm::CmacAes128,
            24 => Algorithm::CmacAes192,
            32 => Algorithm::CmacAes256,
            _ => continue,
        };

        if direction == "gen" {
            for tc in tg.get_array("tests") {
                let tc_id = tc.get_u64("tcId");
                let key = hex(tc.get_str("key"));
                let message = hex(tc.get_str("message"));
                let exp = rmap.get(&(tg_id, tc_id)).unwrap();
                let expected_mac = hex(exp.get_str("mac"));

                let mut m = Mac::new(algo);
                m.init(&key).unwrap();
                if msg_len > 0 {
                    m.update(&message[..msg_len]).unwrap();
                } else {
                    m.update(&[]).unwrap();
                }
                let full = m.sign_to_vec().unwrap();
                let truncated = &full[..mac_len];
                assert_eq!(truncated, expected_mac.as_slice(), "ACVP CMAC gen tcId={}", tc_id,);
                count += 1;
            }
        } else if direction == "ver" {
            for tc in tg.get_array("tests") {
                let tc_id = tc.get_u64("tcId");
                let key = hex(tc.get_str("key"));
                let message = hex(tc.get_str("message"));
                let mac_val = hex(tc.get_str("mac"));
                let exp = rmap.get(&(tg_id, tc_id)).unwrap();
                let test_passed = exp.get_str("testPassed").to_lowercase() == "true";

                let mut m = Mac::new(algo);
                m.init(&key).unwrap();
                if msg_len > 0 {
                    m.update(&message[..msg_len]).unwrap();
                } else {
                    m.update(&[]).unwrap();
                }
                let result = m.verify(&mac_val);
                assert_eq!(
                    result.is_ok(),
                    test_passed,
                    "ACVP CMAC ver tcId={}: expected={} got={}",
                    tc_id,
                    test_passed,
                    result.is_ok(),
                );
                count += 1;
            }
        }
    }

    println!("ACVP CMAC-AES: {} vectors passed", count);
    assert!(count > 100, "expected > 100 CMAC-AES vectors");
}

// =====================================================================
// HMAC-SHA-1 / SHA2-256 / SHA2-384 / SHA2-512 (ACVP vectors)
// =====================================================================

fn acvp_hmac_sha2_impl(dir: &str, algo: arcana::mac::ctx::Algorithm, label: &str) {
    use arcana::mac::ctx::Mac;

    let (prompt, result) = load_acvp(dir);
    let rmap = build_result_map(&result);
    let mut count = 0u32;

    for tg in prompt.get_array("testGroups") {
        let tg_id = tg.get_u64("tgId");
        for tc in tg.get_array("tests") {
            let tc_id = tc.get_u64("tcId");
            let key = hex(tc.get_str("key"));
            let msg = hex(tc.get_str("msg"));
            let mac_len = tc.get_u64("macLen") as usize / 8;

            let exp = rmap.get(&(tg_id, tc_id)).unwrap();
            let expected_mac = hex(exp.get_str("mac"));

            let mut m = Mac::new(algo);
            m.init(&key).unwrap();
            m.update(&msg).unwrap();
            let full = m.sign_to_vec().unwrap();
            let truncated = &full[..mac_len];
            assert_eq!(truncated, expected_mac.as_slice(), "{} tcId={}: mismatch", label, tc_id,);
            count += 1;
        }
    }

    println!("ACVP {}: {} vectors passed", label, count);
    assert!(count > 0, "expected > 0 {} vectors", label);
}

#[test]
fn acvp_hmac_sha1() {
    acvp_hmac_sha2_impl(
        "tests/acvp/HMAC-SHA-1-2.0",
        arcana::mac::ctx::Algorithm::HmacSha1,
        "HMAC-SHA-1",
    );
}

#[test]
fn acvp_hmac_sha2_256() {
    acvp_hmac_sha2_impl(
        "tests/acvp/HMAC-SHA2-256-2.0",
        arcana::mac::ctx::Algorithm::HmacSha256,
        "HMAC-SHA2-256",
    );
}

#[test]
fn acvp_hmac_sha2_384() {
    acvp_hmac_sha2_impl(
        "tests/acvp/HMAC-SHA2-384-2.0",
        arcana::mac::ctx::Algorithm::HmacSha384,
        "HMAC-SHA2-384",
    );
}

#[test]
fn acvp_hmac_sha2_512() {
    acvp_hmac_sha2_impl(
        "tests/acvp/HMAC-SHA2-512-2.0",
        arcana::mac::ctx::Algorithm::HmacSha512,
        "HMAC-SHA2-512",
    );
}

// =====================================================================
// AES-CCM (limited to first 500 to keep test time reasonable)
// =====================================================================

#[test]
fn acvp_aes_ccm() {
    use arcana::cipher::ccm::AesCcm;

    let (prompt, result) = load_acvp("tests/acvp/ACVP-AES-CCM-1.0");
    let rmap = build_result_map(&result);
    let mut count = 0u32;
    let max_vectors = 500;

    for tg in prompt.get_array("testGroups") {
        if count >= max_vectors {
            break;
        }
        let tg_id = tg.get_u64("tgId");
        let direction = tg.get_str("direction");
        let key_len = tg.get_u64("keyLen") as usize / 8;
        let iv_len = tg.get_u64("ivLen") as usize / 8;
        let tag_len = tg.get_u64("tagLen") as usize / 8;

        if key_len != 16 && key_len != 32 {
            continue; // only AES-128 and AES-256
        }

        for tc in tg.get_array("tests") {
            if count >= max_vectors {
                break;
            }
            let tc_id = tc.get_u64("tcId");
            let key = hex(tc.get_str("key"));
            let aad = hex(tc.get_str("aad"));

            let exp = rmap.get(&(tg_id, tc_id)).unwrap();

            if direction == "encrypt" {
                let pt = hex(tc.get_str("pt"));
                let nonce_hex = tc.get_str("iv");
                let nonce = hex(nonce_hex);

                let ct_expected = hex(exp.get_str("ct"));

                if nonce.len() != 13 {
                    continue; // our API requires a 13-byte nonce
                }
                let ccm = AesCcm::new(&key, tag_len).expect("AesCcm::new");
                let nonce_arr: [u8; 13] = nonce.try_into().unwrap();
                let (ct, tag) = match ccm.encrypt(&nonce_arr, &aad, &pt) {
                    Some(v) => v,
                    None => continue,
                };

                // ACVP concatenates CT || Tag.
                let mut full = ct.clone();
                full.extend_from_slice(&tag);
                assert_eq!(full, ct_expected, "AES-CCM enc tcId={}: mismatch", tc_id,);
                count += 1;
            }
        }
    }

    println!("ACVP AES-CCM: {} vectors passed", count);
    assert!(count > 50, "expected > 50 AES-CCM vectors");
}

// =====================================================================
// AES-XTS
// =====================================================================

#[test]
fn acvp_aes_xts() {
    use arcana::cipher::xts::AesXts;

    let (prompt, result) = load_acvp("tests/acvp/ACVP-AES-XTS-1.0");
    let rmap = build_result_map(&result);
    let mut count = 0u32;

    for tg in prompt.get_array("testGroups") {
        let tg_id = tg.get_u64("tgId");
        let direction = tg.get_str("direction");
        let key_len = tg.get_u64("keyLen") as usize / 8;
        let payload_bits = tg.get_u64("payloadLen") as usize;
        if payload_bits % 8 != 0 {
            continue; // non-byte-aligned payloads not supported
        }

        for tc in tg.get_array("tests") {
            let tc_id = tc.get_u64("tcId");
            let key = hex(tc.get_str("key"));

            // XTS key = key1 || key2 (double length).
            if key.len() != key_len * 2 {
                continue;
            }

            let exp = rmap.get(&(tg_id, tc_id)).unwrap();

            // Tweak: some ACVP vectors use "tweak" field in prompt,
            // others in expected. Try both.
            let tweak_hex = tc.get_str("tweakValue");
            if tweak_hex.is_empty() || tweak_hex.len() != 32 {
                continue;
            }
            let tweak = hex(tweak_hex);
            let tweak_arr: [u8; 16] = tweak.try_into().unwrap();

            let xts = match AesXts::new(&key) {
                Some(x) => x,
                None => continue,
            };

            if direction == "encrypt" {
                let pt = hex(tc.get_str("pt"));
                let ct_expected = hex(exp.get_str("ct"));

                if pt.len() < 16 {
                    continue;
                }

                let mut data = pt.clone();
                xts.encrypt_sector(&tweak_arr, &mut data);
                assert_eq!(data, ct_expected, "AES-XTS enc tcId={}", tc_id);
                count += 1;
            } else {
                let ct = hex(tc.get_str("ct"));
                let pt_expected = hex(exp.get_str("pt"));

                if ct.len() < 16 {
                    continue;
                }

                let mut data = ct.clone();
                xts.decrypt_sector(&tweak_arr, &mut data);
                assert_eq!(data, pt_expected, "AES-XTS dec tcId={}", tc_id);
                count += 1;
            }
        }
    }

    println!("ACVP AES-XTS: {} vectors passed", count);
    assert!(count > 20, "expected > 20 AES-XTS vectors");
}
