//! Integration tests that drive the built `purecrypto` binary.
#![cfg(feature = "cli")]

use std::io::{Read, Write};
use std::process::{Command, Stdio};

/// Runs the CLI with `args`, feeding `stdin`, returning `(stdout, success)`.
fn run(args: &[&str], stdin: &[u8]) -> (String, bool) {
    let mut child = Command::new(env!("CARGO_BIN_EXE_purecrypto"))
        .args(args)
        .stdin(Stdio::piped())
        .stdout(Stdio::piped())
        .stderr(Stdio::piped())
        .spawn()
        .expect("spawn purecrypto");
    child
        .stdin
        .take()
        .unwrap()
        .write_all(stdin)
        .expect("write stdin");
    let out = child.wait_with_output().expect("wait");
    (
        String::from_utf8_lossy(&out.stdout).into_owned(),
        out.status.success(),
    )
}

/// Like [`run`] but also returns stderr.
fn run_capture(args: &[&str], stdin: &[u8]) -> (String, String, bool) {
    let mut child = Command::new(env!("CARGO_BIN_EXE_purecrypto"))
        .args(args)
        .stdin(Stdio::piped())
        .stdout(Stdio::piped())
        .stderr(Stdio::piped())
        .spawn()
        .expect("spawn purecrypto");
    child
        .stdin
        .take()
        .unwrap()
        .write_all(stdin)
        .expect("write stdin");
    let out = child.wait_with_output().expect("wait");
    (
        String::from_utf8_lossy(&out.stdout).into_owned(),
        String::from_utf8_lossy(&out.stderr).into_owned(),
        out.status.success(),
    )
}

#[test]
fn hash_sha256_stdin() {
    let (out, ok) = run(&["hash", "sha256"], b"abc");
    assert!(ok);
    assert_eq!(
        out.trim(),
        "ba7816bf8f01cfea414140de5dae2223b00361a396177a9cb410ff61f20015ad"
    );
}

#[test]
fn hash_unknown_algorithm_fails() {
    let (_, ok) = run(&["hash", "nope"], b"x");
    assert!(!ok);
}

#[test]
fn rand_emits_hex() {
    let (out, ok) = run(&["rand", "16"], b"");
    assert!(ok);
    assert_eq!(out.trim().len(), 32); // 16 bytes -> 32 hex chars
    assert!(out.trim().bytes().all(|b| b.is_ascii_hexdigit()));
}

#[test]
fn enc_kwp_short_ciphertext_dies_cleanly() {
    // RFC 5649 AES-KWP ciphertext is at least 16 bytes; a shorter input must be
    // rejected with a clean error rather than triggering a usize underflow on
    // `ciphertext.len() - 8` (debug panic / release capacity-overflow abort).
    let key = "00112233445566778899aabbccddeeff"; // 16-byte AES-128 KEK (hex).
    for alg in ["AES-128-KWP", "AES-256-KWP"] {
        let kek = if alg == "AES-256-KWP" {
            "00112233445566778899aabbccddeeff00112233445566778899aabbccddeeff"
        } else {
            key
        };
        // 8-byte ciphertext (< 16): must fail without a panic/abort.
        let (_out, err, ok) = run_capture(&["enc", "-alg", alg, "-d", "-key", kek], &[0u8; 8]);
        assert!(!ok, "{alg}: short KWP ciphertext should fail");
        assert!(
            err.contains("AES-KWP ciphertext too short"),
            "{alg}: expected too-short diagnostic, got stderr: {err}"
        );
    }
}

#[test]
fn enc_ccm_bad_nonce_dies_cleanly() {
    // AES-CCM accepts only a 7..=13-byte nonce (RFC 3610); `Ccm::validate`
    // asserts this internally, so a wrong-length nonce must be rejected with a
    // clean error rather than a panic/abort with a backtrace.
    let bad_nonce = "00112233445566778899aabbccddeeff"; // 16 bytes (hex), out of range.
    for alg in ["AES-128-CCM", "AES-256-CCM", "AES-128-CCM8", "AES-256-CCM8"] {
        let key = if alg.starts_with("AES-256") {
            "00112233445566778899aabbccddeeff00112233445566778899aabbccddeeff"
        } else {
            "00112233445566778899aabbccddeeff"
        };
        let (_out, err, ok) = run_capture(
            &["enc", "-alg", alg, "-key", key, "-nonce", bad_nonce],
            b"hello",
        );
        assert!(!ok, "{alg}: out-of-range CCM nonce should fail");
        assert!(
            err.contains("AES-CCM nonce must be 7..=13 bytes"),
            "{alg}: expected CCM nonce diagnostic, got stderr: {err}"
        );
    }
}

#[test]
fn ca_workflow_genpkey_req_sign() {
    // Unique scratch dir for this test process.
    let dir = std::env::temp_dir().join(format!("pc_cli_{}", std::process::id()));
    std::fs::create_dir_all(&dir).unwrap();
    let p = |name: &str| dir.join(name).to_str().unwrap().to_string();

    // CA key + self-signed CA cert.
    assert!(
        run(
            &[
                "genpkey",
                "-algorithm",
                "EC",
                "-curve",
                "P-256",
                "-out",
                &p("ca_key.pem")
            ],
            b""
        )
        .1
    );
    assert!(
        run(
            &[
                "x509",
                "-new",
                "--ca",
                "-key",
                &p("ca_key.pem"),
                "-subj",
                "/CN=Test CA",
                "-out",
                &p("ca.pem")
            ],
            b"",
        )
        .1
    );

    // Leaf key + CSR.
    assert!(
        run(
            &[
                "genpkey",
                "-algorithm",
                "EC",
                "-curve",
                "P-256",
                "-out",
                &p("leaf_key.pem")
            ],
            b""
        )
        .1
    );
    assert!(
        run(
            &[
                "req",
                "-key",
                &p("leaf_key.pem"),
                "-subj",
                "/CN=leaf.test",
                "-addext",
                "subjectAltName=DNS:leaf.test",
                "-out",
                &p("leaf.csr"),
            ],
            b"",
        )
        .1
    );

    // CSR self-signature verifies.
    let (vout, ok) = run(&["req", "-in", &p("leaf.csr"), "-verify"], b"");
    assert!(ok && vout.contains("verify OK"));

    // CA signs the CSR.
    assert!(
        run(
            &[
                "x509",
                "-req",
                "-in",
                &p("leaf.csr"),
                "-CA",
                &p("ca.pem"),
                "-CAkey",
                &p("ca_key.pem"),
                "-out",
                &p("leaf.pem")
            ],
            b"",
        )
        .1
    );

    // The issued cert carries the requested subject, the CA issuer, and the SAN.
    let (text, ok) = run(&["x509", "-in", &p("leaf.pem"), "-text"], b"");
    assert!(ok, "x509 -text failed: {text}");
    assert!(text.contains("CN=leaf.test"), "{text}");
    assert!(
        text.contains("Issuer:") && text.contains("CN=Test CA"),
        "{text}"
    );
    assert!(text.contains("leaf.test"), "{text}");

    let _ = std::fs::remove_dir_all(&dir);
}

/// I-8: any CLI tool that reads a private-key file should warn on stderr
/// when that file's Unix mode is group- or world-readable. Tested through
/// `req -key X -subj /CN=foo` because `req` calls `pki::load_key` (which
/// goes through the new `warn_if_world_readable_key` guard).
#[cfg(unix)]
#[test]
fn req_warns_on_world_readable_key() {
    use std::os::unix::fs::PermissionsExt;
    let dir = std::env::temp_dir().join(format!("pc_cli_warn_perm_{}", std::process::id()));
    let _ = std::fs::remove_dir_all(&dir);
    std::fs::create_dir_all(&dir).unwrap();
    let p = |name: &str| dir.join(name).to_str().unwrap().to_string();

    // Mint a key (lands at 0o600); chmod it to 0o644 to trigger the warning.
    assert!(
        run(
            &[
                "genpkey",
                "-algorithm",
                "EC",
                "-curve",
                "P-256",
                "-out",
                &p("loose.key"),
            ],
            b"",
        )
        .1,
        "genpkey failed"
    );
    let mut perms = std::fs::metadata(p("loose.key")).unwrap().permissions();
    perms.set_mode(0o644);
    std::fs::set_permissions(p("loose.key"), perms).unwrap();

    // Build a CSR with the loose key — the operation succeeds but stderr
    // carries the permission warning.
    let (_, err, ok) = run_capture(
        &[
            "req",
            "-key",
            &p("loose.key"),
            "-subj",
            "/CN=test",
            "-out",
            &p("test.csr"),
        ],
        b"",
    );
    assert!(ok, "req should still succeed despite the warning");
    assert!(
        err.contains("group/other-readable"),
        "missing permission warning in stderr: {err}"
    );

    let _ = std::fs::remove_dir_all(&dir);
}

/// I-7: `purecrypto pkey -in priv -out out` re-emits a private key. The
/// output file must land at mode 0o600 (matching `genpkey` / `kex` / `kem` /
/// `ca init`) so a default umask doesn't leave a world-readable private key.
#[cfg(unix)]
#[test]
fn pkey_writes_private_key_with_0600_mode() {
    use std::os::unix::fs::PermissionsExt;
    let dir = std::env::temp_dir().join(format!("pc_cli_pkey_mode_{}", std::process::id()));
    let _ = std::fs::remove_dir_all(&dir);
    std::fs::create_dir_all(&dir).unwrap();
    let p = |name: &str| dir.join(name).to_str().unwrap().to_string();

    // Mint a key first.
    assert!(
        run(
            &[
                "genpkey",
                "-algorithm",
                "EC",
                "-curve",
                "P-256",
                "-out",
                &p("priv.key"),
            ],
            b"",
        )
        .1,
        "genpkey failed"
    );

    // Re-emit through `pkey` (no -pubout / -text — the private-key branch).
    assert!(
        run(
            &["pkey", "-in", &p("priv.key"), "-out", &p("priv2.key")],
            b"",
        )
        .1,
        "pkey re-emit failed"
    );
    let mode = std::fs::metadata(p("priv2.key"))
        .unwrap()
        .permissions()
        .mode()
        & 0o777;
    assert_eq!(mode, 0o600, "expected 0o600, got {mode:o}");

    let _ = std::fs::remove_dir_all(&dir);
}

/// I-5: a `/CN=...` subject containing a control character (here `\n`) must
/// be rejected at parse time so the issued.jsonl/revoked.jsonl ledgers
/// cannot be corrupted by attacker-supplied DN fields. Exercised via
/// `req -subj`, which routes through `parse_subject`.
#[test]
fn req_rejects_subject_with_newline() {
    let dir = std::env::temp_dir().join(format!("pc_cli_subj_nl_{}", std::process::id()));
    let _ = std::fs::remove_dir_all(&dir);
    std::fs::create_dir_all(&dir).unwrap();
    let p = |name: &str| dir.join(name).to_str().unwrap().to_string();

    // Mint any private key so the early -key check passes.
    assert!(
        run(
            &[
                "genpkey",
                "-algorithm",
                "EC",
                "-curve",
                "P-256",
                "-out",
                &p("k.pem"),
            ],
            b"",
        )
        .1,
        "genpkey failed"
    );

    let (_, ok) = run(
        &[
            "req",
            "-key",
            &p("k.pem"),
            "-subj",
            "/CN=evil\nbob",
            "-out",
            &p("evil.csr"),
        ],
        b"",
    );
    assert!(!ok, "req should fail on subject with control char");
    let _ = std::fs::remove_dir_all(&dir);
}

/// End-to-end exercise of `purecrypto ca`: init → genpkey leaf → ca issue →
/// x509 inspect → ca revoke → ca crl → verify the CRL revokes the leaf.
#[test]
fn ca_subcommand_full_flow() {
    use purecrypto::x509::{Certificate, CertificateRevocationList};

    let dir = std::env::temp_dir().join(format!("pc_cli_ca_{}", std::process::id()));
    let _ = std::fs::remove_dir_all(&dir);
    let p = |name: &str| dir.join(name).to_str().unwrap().to_string();

    // CA init.
    assert!(
        run(
            &[
                "ca",
                "init",
                "-dir",
                dir.to_str().unwrap(),
                "-cn",
                "Test CLI CA"
            ],
            b""
        )
        .1,
        "ca init failed"
    );

    // Leaf key + extracted public key.
    assert!(
        run(
            &[
                "genpkey",
                "-algorithm",
                "EC",
                "-curve",
                "P-256",
                "-out",
                &p("leaf.key"),
            ],
            b""
        )
        .1,
        "genpkey failed"
    );
    let (pubkey_pem, ok) = run(&["pkey", "-in", &p("leaf.key"), "-pubout"], b"");
    assert!(ok, "pkey -pubout failed");
    std::fs::write(dir.join("leaf.pub"), pubkey_pem).unwrap();

    // CA issues a leaf.
    assert!(
        run(
            &[
                "ca",
                "issue",
                "-dir",
                dir.to_str().unwrap(),
                "-pubkey",
                &p("leaf.pub"),
                "-cn",
                "host.example",
                "-sans",
                "host.example",
                "-out",
                &p("leaf.crt"),
            ],
            b""
        )
        .1,
        "ca issue failed"
    );

    // x509 inspect: the issued cert has the right subject + issuer.
    let (text, ok) = run(&["x509", "-in", &p("leaf.crt"), "-text"], b"");
    assert!(ok, "x509 inspect failed: {text}");
    assert!(text.contains("CN=host.example"), "subject missing: {text}");
    assert!(text.contains("CN=Test CLI CA"), "issuer missing: {text}");

    // The leaf is signed by the CA: verify via the library.
    let root_pem = std::fs::read_to_string(dir.join("root.crt")).unwrap();
    let leaf_pem = std::fs::read_to_string(dir.join("leaf.crt")).unwrap();
    let root = Certificate::from_pem(&root_pem).unwrap();
    let leaf = Certificate::from_pem(&leaf_pem).unwrap();
    let root_key = root.subject_public_key().unwrap();
    leaf.verify_signature_with(&root_key)
        .expect("leaf should verify under the CA key");

    // Revoke the leaf (CA serial starts at 2).
    assert!(
        run(
            &[
                "ca",
                "revoke",
                "-dir",
                dir.to_str().unwrap(),
                "-serial",
                "2",
                "-reason",
                "key-compromise",
            ],
            b""
        )
        .1,
        "ca revoke failed"
    );

    // Refresh the CRL.
    assert!(
        run(
            &[
                "ca",
                "crl",
                "-dir",
                dir.to_str().unwrap(),
                "-out",
                &p("crl.pem"),
            ],
            b""
        )
        .1,
        "ca crl failed"
    );

    // Load the CRL and verify it revokes the leaf serial, and that its
    // signature is valid under the CA key.
    let crl_pem = std::fs::read_to_string(dir.join("crl.pem")).unwrap();
    let crl = CertificateRevocationList::from_pem(&crl_pem).unwrap();
    assert!(
        crl.is_revoked(&[2]).unwrap(),
        "CRL should list serial 2 as revoked"
    );
    crl.verify_signature_with(&root_key)
        .expect("CRL signature should verify under the CA key");
    crl.check_signature_algid_consistent()
        .expect("CRL inner/outer algid should agree");

    // `ca show` produces a usable summary.
    let (show, ok) = run(&["ca", "show", "-dir", dir.to_str().unwrap()], b"");
    assert!(ok, "ca show failed");
    assert!(show.contains("CN=Test CLI CA"), "show output: {show}");
    assert!(show.contains("Revoked:    1"), "show output: {show}");
    assert!(show.contains("CRL:        present"), "show output: {show}");

    let _ = std::fs::remove_dir_all(&dir);
}

#[test]
fn s_client_loopback() {
    use purecrypto::rsa::{BoxedRsaPrivateKey, RsaPrivateKey};
    use purecrypto::tls::{Config, Connection, HandshakeStatus, SigningKey};
    use purecrypto::x509::{Certificate, DistinguishedName, Time, Validity};
    use std::net::TcpListener;

    const KEY: &str = include_str!("../testdata/rsa2048_test_a.pem");

    // A local TLS server that completes one handshake, echoes a reply, and exits.
    let listener = TcpListener::bind("127.0.0.1:0").expect("bind");
    let port = listener.local_addr().unwrap().port();
    let server = std::thread::spawn(move || {
        let (mut sock, _) = listener.accept().expect("accept");
        let signing = RsaPrivateKey::<32>::from_pkcs1_pem(KEY).unwrap();
        let validity = Validity::new(
            Time::utc(2024, 1, 1, 0, 0, 0),
            Time::utc(2034, 1, 1, 0, 0, 0),
        );
        let cert = Certificate::self_signed(
            &signing,
            &DistinguishedName::common_name("127.0.0.1"),
            &validity,
            1,
            false,
        )
        .unwrap();
        let key = BoxedRsaPrivateKey::from_pkcs1_pem(KEY).unwrap();
        let cfg = Config::builder()
            .tls_only()
            .rng(std::sync::Arc::new(purecrypto::rng::OsRng))
            .identity(vec![cert.to_der().to_vec()], SigningKey::Rsa(key))
            .build();
        let mut conn = Connection::server(&cfg).expect("server config");
        // Drive handshake.
        let mut read_buf = [0u8; 8192];
        loop {
            let out = conn.pop().unwrap_or_default();
            if !out.is_empty() {
                sock.write_all(&out).unwrap();
            }
            match conn.handshake().unwrap() {
                HandshakeStatus::Complete => break,
                HandshakeStatus::WantWrite => continue,
                HandshakeStatus::WantRead => {
                    let n = sock.read(&mut read_buf).expect("read");
                    if n == 0 {
                        panic!("peer closed during handshake");
                    }
                    conn.feed(&read_buf[..n]).expect("feed");
                }
            }
        }
        // Read the PING. It may already be buffered if the client's
        // ClientFinished + first app-data record arrived in one TCP
        // segment (Nagle coalescing — common on macOS loopback).
        let mut got = conn.recv().unwrap_or_default();
        while got.is_empty() {
            let n = sock.read(&mut read_buf).unwrap();
            if n == 0 {
                break;
            }
            conn.feed(&read_buf[..n]).unwrap();
            got = conn.recv().unwrap_or_default();
        }
        // Reply with PONG, then cleanly close so the client sees EOF.
        conn.send(b"PONG").unwrap();
        let _ = conn.close();
        let out = conn.pop().unwrap_or_default();
        sock.write_all(&out).unwrap();
        sock.flush().unwrap();
        let _ = sock.shutdown(std::net::Shutdown::Write);
    });

    // The CLI connects (insecure: self-signed), sends PING, prints the reply.
    // `-quiet` MUST NOT suppress the "certificate NOT verified" warning —
    // it's security-relevant and goes to stderr regardless.
    let (out, err, ok) = run_capture(
        &[
            "s_client",
            "-connect",
            &format!("127.0.0.1:{port}"),
            "-insecure",
            "-quiet",
        ],
        b"PING",
    );
    server.join().unwrap();
    assert!(ok, "s_client exited with failure");
    assert!(
        out.contains("PONG"),
        "expected PONG in stdout, got: {out:?}"
    );
    assert!(
        err.contains("certificate NOT verified"),
        "expected -insecure warning on stderr, got: {err:?}"
    );
}

/// s_client and s_server round-trip over a local TCP port, exercising
/// ALPN negotiation and `-keylogfile` capture (NSS `SSLKEYLOGFILE`
/// format).
#[test]
fn s_client_s_server_roundtrip_alpn_keylog() {
    use purecrypto::ec::Ed25519PrivateKey;
    use purecrypto::rng::OsRng;
    use purecrypto::x509::{CertSigner, Certificate, DistinguishedName, Time, Validity};

    // Pick a free port up front.
    let listener = std::net::TcpListener::bind("127.0.0.1:0").expect("bind probe");
    let port = listener.local_addr().unwrap().port();
    drop(listener);

    let dir = std::env::temp_dir().join(format!("pc_s_server_{}", std::process::id()));
    std::fs::create_dir_all(&dir).unwrap();
    let cert_path = dir.join("server.pem");
    let key_path = dir.join("server.key");
    let log_path = dir.join("keylog.txt");

    // Self-signed Ed25519 cert for 127.0.0.1.
    let key = Ed25519PrivateKey::generate(&mut OsRng);
    let validity = Validity::new(
        Time::utc(2024, 1, 1, 0, 0, 0),
        Time::utc(2034, 1, 1, 0, 0, 0),
    );
    let cert = Certificate::self_signed_general(
        &CertSigner::Ed25519(&key),
        &DistinguishedName::common_name("127.0.0.1"),
        &validity,
        1,
        false,
        &["127.0.0.1"],
    )
    .unwrap();
    std::fs::write(&cert_path, cert.to_pem()).unwrap();
    std::fs::write(&key_path, key.to_pkcs8_pem()).unwrap();

    // Spawn s_server in a background process (single-shot, `-www`).
    let server_proc = std::process::Command::new(env!("CARGO_BIN_EXE_purecrypto"))
        .args([
            "s_server",
            "-cert",
            cert_path.to_str().unwrap(),
            "-key",
            key_path.to_str().unwrap(),
            "-accept",
            &port.to_string(),
            "-alpn",
            "h2,http/1.1",
            "-www",
            "-quiet",
        ])
        .stdout(std::process::Stdio::null())
        .stderr(std::process::Stdio::null())
        .spawn()
        .expect("spawn s_server");

    // Give the server time to bind.
    std::thread::sleep(std::time::Duration::from_millis(200));

    // s_client connects, negotiates ALPN, dumps secrets to keylogfile.
    let (out, ok) = run(
        &[
            "s_client",
            "-connect",
            &format!("127.0.0.1:{port}"),
            "-insecure",
            "-alpn",
            "http/1.1",
            "-keylogfile",
            log_path.to_str().unwrap(),
            "-quiet",
        ],
        b"GET / HTTP/1.0\r\nHost: 127.0.0.1\r\n\r\n",
    );

    let _ = server_proc.wait_with_output();

    assert!(ok, "s_client failed");
    assert!(
        out.contains("hello from purecrypto s_server"),
        "expected -www body in client stdout, got: {out:?}"
    );

    // The keylogfile must contain the expected TLS 1.3 secret labels.
    let log = std::fs::read_to_string(&log_path).expect("read keylog");
    for label in [
        "CLIENT_HANDSHAKE_TRAFFIC_SECRET",
        "SERVER_HANDSHAKE_TRAFFIC_SECRET",
        "CLIENT_TRAFFIC_SECRET_0",
        "SERVER_TRAFFIC_SECRET_0",
        "EXPORTER_SECRET",
    ] {
        assert!(log.contains(label), "missing {label} in keylog:\n{log}");
    }

    let _ = std::fs::remove_dir_all(&dir);
}

#[test]
#[ignore = "requires network access"]
fn s_client_live_cloudflare() {
    let (_out, ok) = run(
        &["s_client", "-connect", "cloudflare.com:443"],
        b"GET / HTTP/1.1\r\nHost: cloudflare.com\r\nConnection: close\r\n\r\n",
    );
    assert!(ok);
}

/// s_client and s_server round-trip over a local TCP port speaking TLS 1.2.
#[test]
fn s_client_s_server_tls12_roundtrip() {
    use purecrypto::ec::{BoxedEcdsaPrivateKey, CurveId};
    use purecrypto::rng::OsRng;
    use purecrypto::x509::{CertSigner, Certificate, DistinguishedName, Time, Validity};

    // Pick a free port up front.
    let listener = std::net::TcpListener::bind("127.0.0.1:0").expect("bind probe");
    let port = listener.local_addr().unwrap().port();
    drop(listener);

    let dir = std::env::temp_dir().join(format!("pc_s_server_tls12_{}", std::process::id()));
    std::fs::create_dir_all(&dir).unwrap();
    let cert_path = dir.join("server.pem");
    let key_path = dir.join("server.key");

    let mut rng = OsRng;
    let key = BoxedEcdsaPrivateKey::generate(CurveId::P256, &mut rng);
    let validity = Validity::new(
        Time::utc(2024, 1, 1, 0, 0, 0),
        Time::utc(2034, 1, 1, 0, 0, 0),
    );
    let cert = Certificate::self_signed_general(
        &CertSigner::Ecdsa(&key),
        &DistinguishedName::common_name("127.0.0.1"),
        &validity,
        1,
        false,
        &["127.0.0.1"],
    )
    .unwrap();
    std::fs::write(&cert_path, cert.to_pem()).unwrap();
    std::fs::write(&key_path, key.to_sec1_pem()).unwrap();

    let server_proc = std::process::Command::new(env!("CARGO_BIN_EXE_purecrypto"))
        .args([
            "s_server",
            "-tls1_2",
            "-cert",
            cert_path.to_str().unwrap(),
            "-key",
            key_path.to_str().unwrap(),
            "-accept",
            &port.to_string(),
            "-www",
            "-quiet",
        ])
        .stdout(std::process::Stdio::null())
        .stderr(std::process::Stdio::null())
        .spawn()
        .expect("spawn s_server");

    std::thread::sleep(std::time::Duration::from_millis(200));

    let (out, ok) = run(
        &[
            "s_client",
            "-tls1_2",
            "-connect",
            &format!("127.0.0.1:{port}"),
            "-insecure",
            "-quiet",
        ],
        b"GET / HTTP/1.0\r\nHost: 127.0.0.1\r\n\r\n",
    );

    let _ = server_proc.wait_with_output();

    assert!(ok, "s_client -tls1_2 failed");
    assert!(
        out.contains("hello from purecrypto s_server"),
        "expected -www body in client stdout, got: {out:?}"
    );

    let _ = std::fs::remove_dir_all(&dir);
}

#[test]
#[ignore = "requires network access"]
fn s_client_live_cloudflare_tls12() {
    let (_out, ok) = run(
        &["s_client", "-tls1_2", "-connect", "cloudflare.com:443"],
        b"GET / HTTP/1.1\r\nHost: cloudflare.com\r\nConnection: close\r\n\r\n",
    );
    assert!(ok);
}

/// `s_dtls_client` and `s_dtls_server` round-trip over a local UDP port,
/// exercising the DTLS 1.2 handshake (with HelloVerifyRequest cookie)
/// and an app-data echo.
#[test]
fn s_dtls_client_s_dtls_server_roundtrip() {
    use purecrypto::ec::{BoxedEcdsaPrivateKey, CurveId};
    use purecrypto::rng::OsRng;
    use purecrypto::x509::{CertSigner, Certificate, DistinguishedName, Time, Validity};

    // Pick a free UDP port up front by binding then dropping.
    let probe = std::net::UdpSocket::bind("127.0.0.1:0").expect("bind probe");
    let port = probe.local_addr().unwrap().port();
    drop(probe);

    let dir = std::env::temp_dir().join(format!("pc_s_dtls_{}", std::process::id()));
    std::fs::create_dir_all(&dir).unwrap();
    let cert_path = dir.join("server.pem");
    let key_path = dir.join("server.key");

    // Self-signed P-256 ECDSA cert for 127.0.0.1. The DTLS 1.2 server we
    // wrap is ECDSA-only.
    let mut rng = OsRng;
    let key = BoxedEcdsaPrivateKey::generate(CurveId::P256, &mut rng);
    let validity = Validity::new(
        Time::utc(2024, 1, 1, 0, 0, 0),
        Time::utc(2034, 1, 1, 0, 0, 0),
    );
    let cert = Certificate::self_signed_general(
        &CertSigner::Ecdsa(&key),
        &DistinguishedName::common_name("127.0.0.1"),
        &validity,
        1,
        false,
        &["127.0.0.1"],
    )
    .unwrap();
    std::fs::write(&cert_path, cert.to_pem()).unwrap();
    std::fs::write(&key_path, key.to_sec1_pem()).unwrap();

    // Spawn s_dtls_server in a background process. Disable the cookie
    // exchange to keep the round-trip path short — the cookie path is
    // exercised by the DTLS unit tests.
    let server_proc = std::process::Command::new(env!("CARGO_BIN_EXE_purecrypto"))
        .args([
            "s_dtls_server",
            "-cert",
            cert_path.to_str().unwrap(),
            "-key",
            key_path.to_str().unwrap(),
            "-accept",
            &format!("127.0.0.1:{port}"),
            "-no_cookie",
            "-quiet",
        ])
        .stdout(std::process::Stdio::null())
        .stderr(std::process::Stdio::null())
        .spawn()
        .expect("spawn s_dtls_server");

    // Give the server time to bind.
    std::thread::sleep(std::time::Duration::from_millis(200));

    // s_dtls_client connects, sends one line, and expects the echo back.
    // `-insecure` skips peer-cert validation (the test fixture's cert isn't
    // anchored to a trusted root); since the audit fix the DTLS client
    // refuses to start without either `-CAfile` or this explicit opt-in.
    let (out, _ok) = run(
        &[
            "s_dtls_client",
            "-connect",
            &format!("127.0.0.1:{port}"),
            "-insecure",
            "-quiet",
        ],
        b"hello\n",
    );

    let _ = server_proc.wait_with_output();

    // The CLI exits when the data-phase idle deadline fires; we don't
    // assert on the exit code (which is non-deterministic across CI
    // schedulers) — only that the echo round-tripped.
    assert!(
        out.contains("hello"),
        "expected 'hello' in client stdout, got: {out:?}"
    );

    let _ = std::fs::remove_dir_all(&dir);
}

/// Same DTLS 1.2 roundtrip as above, but exercising the unified binaries
/// `s_client -dtls1_2` ↔ `s_server -dtls1_2`. This proves that the
/// version-flag dispatch in `s_client` / `s_server` reaches the same
/// UDP code path as the `s_dtls_*` convenience aliases.
#[test]
fn s_client_s_server_dtls12_roundtrip() {
    use purecrypto::ec::{BoxedEcdsaPrivateKey, CurveId};
    use purecrypto::rng::OsRng;
    use purecrypto::x509::{CertSigner, Certificate, DistinguishedName, Time, Validity};

    let probe = std::net::UdpSocket::bind("127.0.0.1:0").expect("bind probe");
    let port = probe.local_addr().unwrap().port();
    drop(probe);

    let dir = std::env::temp_dir().join(format!("pc_dtls12_unified_{}", std::process::id()));
    std::fs::create_dir_all(&dir).unwrap();
    let cert_path = dir.join("server.pem");
    let key_path = dir.join("server.key");

    let mut rng = OsRng;
    let key = BoxedEcdsaPrivateKey::generate(CurveId::P256, &mut rng);
    let validity = Validity::new(
        Time::utc(2024, 1, 1, 0, 0, 0),
        Time::utc(2034, 1, 1, 0, 0, 0),
    );
    let cert = Certificate::self_signed_general(
        &CertSigner::Ecdsa(&key),
        &DistinguishedName::common_name("127.0.0.1"),
        &validity,
        1,
        false,
        &["127.0.0.1"],
    )
    .unwrap();
    std::fs::write(&cert_path, cert.to_pem()).unwrap();
    std::fs::write(&key_path, key.to_sec1_pem()).unwrap();

    // `s_server -dtls1_2` instead of `s_dtls_server`.
    let server_proc = std::process::Command::new(env!("CARGO_BIN_EXE_purecrypto"))
        .args([
            "s_server",
            "-dtls1_2",
            "-cert",
            cert_path.to_str().unwrap(),
            "-key",
            key_path.to_str().unwrap(),
            "-accept",
            &format!("127.0.0.1:{port}"),
            "-no_cookie",
            "-quiet",
        ])
        .stdout(std::process::Stdio::null())
        .stderr(std::process::Stdio::null())
        .spawn()
        .expect("spawn s_server -dtls1_2");

    std::thread::sleep(std::time::Duration::from_millis(200));

    // `s_client -dtls1_2` instead of `s_dtls_client`.
    let (out, _ok) = run(
        &[
            "s_client",
            "-dtls1_2",
            "-connect",
            &format!("127.0.0.1:{port}"),
            "-insecure",
            "-quiet",
        ],
        b"hello\n",
    );

    let _ = server_proc.wait_with_output();

    assert!(
        out.contains("hello"),
        "expected 'hello' in client stdout, got: {out:?}"
    );

    let _ = std::fs::remove_dir_all(&dir);
}

/// DTLS 1.3 roundtrip using the unified `s_client -dtls1_3` ↔
/// `s_server -dtls1_3` flags from commit 14.
#[test]
fn s_client_s_server_dtls13_roundtrip() {
    use purecrypto::ec::{BoxedEcdsaPrivateKey, CurveId};
    use purecrypto::rng::OsRng;
    use purecrypto::x509::{CertSigner, Certificate, DistinguishedName, Time, Validity};

    let probe = std::net::UdpSocket::bind("127.0.0.1:0").expect("bind probe");
    let port = probe.local_addr().unwrap().port();
    drop(probe);

    let dir = std::env::temp_dir().join(format!("pc_dtls13_unified_{}", std::process::id()));
    std::fs::create_dir_all(&dir).unwrap();
    let cert_path = dir.join("server.pem");
    let key_path = dir.join("server.key");

    let mut rng = OsRng;
    let key = BoxedEcdsaPrivateKey::generate(CurveId::P256, &mut rng);
    let validity = Validity::new(
        Time::utc(2024, 1, 1, 0, 0, 0),
        Time::utc(2034, 1, 1, 0, 0, 0),
    );
    let cert = Certificate::self_signed_general(
        &CertSigner::Ecdsa(&key),
        &DistinguishedName::common_name("127.0.0.1"),
        &validity,
        1,
        false,
        &["127.0.0.1"],
    )
    .unwrap();
    std::fs::write(&cert_path, cert.to_pem()).unwrap();
    std::fs::write(&key_path, key.to_sec1_pem()).unwrap();

    let server_proc = std::process::Command::new(env!("CARGO_BIN_EXE_purecrypto"))
        .args([
            "s_server",
            "-dtls1_3",
            "-cert",
            cert_path.to_str().unwrap(),
            "-key",
            key_path.to_str().unwrap(),
            "-accept",
            &format!("127.0.0.1:{port}"),
            "-no_cookie",
            "-quiet",
        ])
        .stdout(std::process::Stdio::null())
        .stderr(std::process::Stdio::null())
        .spawn()
        .expect("spawn s_server -dtls1_3");

    std::thread::sleep(std::time::Duration::from_millis(200));

    let (out, _ok) = run(
        &[
            "s_client",
            "-dtls1_3",
            "-connect",
            &format!("127.0.0.1:{port}"),
            "-insecure",
            "-quiet",
        ],
        b"hello\n",
    );

    let _ = server_proc.wait_with_output();

    assert!(
        out.contains("hello"),
        "expected 'hello' in client stdout, got: {out:?}"
    );

    let _ = std::fs::remove_dir_all(&dir);
}

#[test]
fn genpkey_ec_then_inspect() {
    let (key_pem, ok) = run(&["genpkey", "-algorithm", "EC", "-curve", "P-256"], b"");
    assert!(ok);
    assert!(key_pem.contains("BEGIN EC PRIVATE KEY"));

    // Public key extraction.
    let (pub_pem, ok) = run(&["pkey", "-pubout"], key_pem.as_bytes());
    assert!(ok);
    assert!(pub_pem.contains("BEGIN PUBLIC KEY"));

    // Text inspection names the curve.
    let (text, ok) = run(&["pkey", "-text"], key_pem.as_bytes());
    assert!(ok);
    assert!(text.contains("P-256"));
}

#[test]
fn genpkey_ed25519_then_inspect() {
    let (key_pem, ok) = run(&["genpkey", "-algorithm", "ED25519"], b"");
    assert!(ok);
    assert!(key_pem.contains("BEGIN PRIVATE KEY"));

    // Public key extraction (PKIX SPKI).
    let (pub_pem, ok) = run(&["pkey", "-pubout"], key_pem.as_bytes());
    assert!(ok);
    assert!(pub_pem.contains("BEGIN PUBLIC KEY"));

    // Text inspection names the algorithm.
    let (text, ok) = run(&["pkey", "-text"], key_pem.as_bytes());
    assert!(ok);
    assert!(text.contains("Ed25519"));
}

#[test]
fn genpkey_ed448_then_inspect() {
    let (key_pem, ok) = run(&["genpkey", "-algorithm", "ED448"], b"");
    assert!(ok);
    assert!(key_pem.contains("BEGIN PRIVATE KEY"));

    // Public key extraction (PKIX SPKI).
    let (pub_pem, ok) = run(&["pkey", "-pubout"], key_pem.as_bytes());
    assert!(ok);
    assert!(pub_pem.contains("BEGIN PUBLIC KEY"));

    // Text inspection names the algorithm.
    let (text, ok) = run(&["pkey", "-text"], key_pem.as_bytes());
    assert!(ok);
    assert!(text.contains("Ed448"));
}

#[test]
fn genpkey_ml_dsa_65_roundtrip() {
    let (pem, ok) = run(&["genpkey", "-algorithm", "ML-DSA-65"], b"");
    assert!(ok);
    assert!(pem.contains("BEGIN PRIVATE KEY"));
    let (text, ok) = run(&["pkey", "-text"], pem.as_bytes());
    assert!(ok);
    assert!(text.contains("ML-DSA-65"));
    let (pub_pem, ok) = run(&["pkey", "-pubout"], pem.as_bytes());
    assert!(ok);
    assert!(pub_pem.contains("BEGIN PUBLIC KEY"));
}

#[test]
fn genpkey_ml_kem_768_roundtrip() {
    let (pem, ok) = run(&["genpkey", "-algorithm", "ML-KEM-768"], b"");
    assert!(ok);
    let (text, ok) = run(&["pkey", "-text"], pem.as_bytes());
    assert!(ok);
    assert!(text.contains("ML-KEM-768"));
}

#[test]
fn genpkey_ml_kem_512_roundtrip() {
    let (pem, ok) = run(&["genpkey", "-algorithm", "ML-KEM-512"], b"");
    assert!(ok);
    let (text, ok) = run(&["pkey", "-text"], pem.as_bytes());
    assert!(ok && text.contains("ML-KEM-512"));
    let (pub_pem, ok) = run(&["pkey", "-pubout"], pem.as_bytes());
    assert!(ok && pub_pem.contains("BEGIN PUBLIC KEY"));
}

#[test]
fn genpkey_ml_kem_1024_roundtrip() {
    let (pem, ok) = run(&["genpkey", "-algorithm", "ML-KEM-1024"], b"");
    assert!(ok);
    let (text, ok) = run(&["pkey", "-text"], pem.as_bytes());
    assert!(ok && text.contains("ML-KEM-1024"));
    let (pub_pem, ok) = run(&["pkey", "-pubout"], pem.as_bytes());
    assert!(ok && pub_pem.contains("BEGIN PUBLIC KEY"));
}

#[test]
fn genpkey_slh_dsa_sha2_128f_roundtrip() {
    let (pem, ok) = run(&["genpkey", "-algorithm", "SLH-DSA-SHA2-128f"], b"");
    assert!(ok);
    let (text, ok) = run(&["pkey", "-text"], pem.as_bytes());
    assert!(ok);
    assert!(text.contains("SLH-DSA"));
    let (pub_pem, ok) = run(&["pkey", "-pubout"], pem.as_bytes());
    assert!(ok);
    assert!(pub_pem.contains("BEGIN PUBLIC KEY"));
}

// ---------------------------------------------------------------------------
// Template + extension tests

#[test]
fn ca_template_tls_server() {
    let dir = std::env::temp_dir().join(format!("pc_tmpl_srv_{}", std::process::id()));
    let _ = std::fs::remove_dir_all(&dir);
    let p = |name: &str| dir.join(name).to_str().unwrap().to_string();

    // Root CA.
    assert!(
        run(
            &[
                "ca",
                "init",
                "-dir",
                dir.to_str().unwrap(),
                "-cn",
                "Root CA"
            ],
            b"",
        )
        .1,
        "ca init failed"
    );
    // Leaf key + pub.
    assert!(
        run(
            &[
                "genpkey",
                "-algorithm",
                "EC",
                "-curve",
                "P-256",
                "-out",
                &p("leaf.key")
            ],
            b""
        )
        .1
    );
    let (pubkey_pem, ok) = run(&["pkey", "-in", &p("leaf.key"), "-pubout"], b"");
    assert!(ok);
    std::fs::write(dir.join("leaf.pub"), pubkey_pem).unwrap();

    // Issue with -template tls-server.
    assert!(
        run(
            &[
                "ca",
                "issue",
                "-dir",
                dir.to_str().unwrap(),
                "-template",
                "tls-server",
                "-pubkey",
                &p("leaf.pub"),
                "-cn",
                "host.example",
                "-sans",
                "host.example,*.host.example",
                "-out",
                &p("leaf.crt"),
            ],
            b""
        )
        .1,
        "ca issue with template failed"
    );

    let (text, ok) = run(&["x509", "-in", &p("leaf.crt"), "-text", "-ext"], b"");
    assert!(ok, "x509 -text -ext failed: {text}");
    assert!(text.contains("CA: false"), "basicConstraints: {text}");
    assert!(
        text.contains("digitalSignature") && text.contains("keyEncipherment"),
        "keyUsage: {text}"
    );
    assert!(text.contains("serverAuth"), "EKU: {text}");
    assert!(
        text.contains("DNS:host.example") && text.contains("DNS:*.host.example"),
        "SAN: {text}"
    );
    assert!(text.contains("subjectKeyIdentifier"), "SKI: {text}");
    assert!(text.contains("authorityKeyIdentifier"), "AKI: {text}");

    // The leaf verifies against the (now-extended) root CA.
    let root_pem = std::fs::read_to_string(dir.join("root.crt")).unwrap();
    let leaf_pem = std::fs::read_to_string(dir.join("leaf.crt")).unwrap();
    let root = purecrypto::x509::Certificate::from_pem(&root_pem).unwrap();
    let leaf = purecrypto::x509::Certificate::from_pem(&leaf_pem).unwrap();
    let root_key = root.subject_public_key().unwrap();
    leaf.verify_signature_with(&root_key).unwrap();

    let _ = std::fs::remove_dir_all(&dir);
}

#[test]
fn ca_template_intermediate_chain() {
    let dir = std::env::temp_dir().join(format!("pc_tmpl_chain_{}", std::process::id()));
    let _ = std::fs::remove_dir_all(&dir);
    let p = |name: &str| dir.join(name).to_str().unwrap().to_string();

    // Root CA.
    assert!(
        run(
            &[
                "ca",
                "init",
                "-dir",
                dir.to_str().unwrap(),
                "-cn",
                "Chain Root"
            ],
            b"",
        )
        .1
    );

    // Intermediate keypair + pubkey extracted via pkey -pubout.
    assert!(
        run(
            &[
                "genpkey",
                "-algorithm",
                "EC",
                "-curve",
                "P-256",
                "-out",
                &p("int.key")
            ],
            b""
        )
        .1
    );
    let (int_pub, ok) = run(&["pkey", "-in", &p("int.key"), "-pubout"], b"");
    assert!(ok);
    std::fs::write(dir.join("int.pub"), int_pub).unwrap();

    // Root issues the intermediate with -template ca-intermediate.
    assert!(
        run(
            &[
                "ca",
                "issue",
                "-dir",
                dir.to_str().unwrap(),
                "-template",
                "ca-intermediate",
                "-pubkey",
                &p("int.pub"),
                "-cn",
                "Chain Intermediate",
                "-out",
                &p("int.crt"),
            ],
            b""
        )
        .1,
        "intermediate issue failed"
    );

    // The intermediate cert verifies under the root.
    let root_pem = std::fs::read_to_string(dir.join("root.crt")).unwrap();
    let int_pem = std::fs::read_to_string(dir.join("int.crt")).unwrap();
    let root = purecrypto::x509::Certificate::from_pem(&root_pem).unwrap();
    let int_cert = purecrypto::x509::Certificate::from_pem(&int_pem).unwrap();
    int_cert
        .verify_signature_with(&root.subject_public_key().unwrap())
        .expect("intermediate must verify under root");
    // basicConstraints.ca = true, pathLen = 0 on the intermediate.
    let bc = int_cert.basic_constraints().unwrap().unwrap();
    assert_eq!(bc, (true, Some(0)), "ca-intermediate path_len");

    let _ = std::fs::remove_dir_all(&dir);
}

#[test]
fn req_template_csr() {
    let dir = std::env::temp_dir().join(format!("pc_tmpl_req_{}", std::process::id()));
    let _ = std::fs::remove_dir_all(&dir);
    std::fs::create_dir_all(&dir).unwrap();
    let p = |name: &str| dir.join(name).to_str().unwrap().to_string();

    assert!(
        run(
            &[
                "genpkey",
                "-algorithm",
                "EC",
                "-curve",
                "P-256",
                "-out",
                &p("leaf.key")
            ],
            b""
        )
        .1
    );
    assert!(
        run(
            &[
                "req",
                "-key",
                &p("leaf.key"),
                "-subj",
                "/CN=tmpl.example",
                "-template",
                "tls-server",
                "-san",
                "tmpl.example,alt.example",
                "-out",
                &p("leaf.csr"),
            ],
            b"",
        )
        .1,
        "req -template failed"
    );
    let (vout, ok) = run(&["req", "-in", &p("leaf.csr"), "-verify"], b"");
    assert!(ok && vout.contains("verify OK"));

    // Parse the CSR via the library to inspect its extensionRequest.
    let csr_pem = std::fs::read_to_string(dir.join("leaf.csr")).unwrap();
    let csr = purecrypto::x509::CertificationRequest::from_pem(&csr_pem).unwrap();
    let exts = csr.extension_requests().unwrap();
    assert!(
        exts.iter()
            .any(|e| e.oid == purecrypto::x509::oid::EXT_KEY_USAGE),
        "CSR should request EKU"
    );
    assert!(
        exts.iter()
            .any(|e| e.oid == purecrypto::x509::oid::KEY_USAGE),
        "CSR should request keyUsage"
    );
    let sans = csr.subject_alt_names().unwrap();
    assert!(sans.contains(&"tmpl.example".to_string()));
    assert!(sans.contains(&"alt.example".to_string()));

    let _ = std::fs::remove_dir_all(&dir);
}

#[test]
fn template_user_file_overrides_critical() {
    let dir = std::env::temp_dir().join(format!("pc_tmpl_user_{}", std::process::id()));
    let _ = std::fs::remove_dir_all(&dir);
    let p = |name: &str| dir.join(name).to_str().unwrap().to_string();

    assert!(
        run(
            &[
                "ca",
                "init",
                "-dir",
                dir.to_str().unwrap(),
                "-cn",
                "UF Root"
            ],
            b"",
        )
        .1
    );
    assert!(
        run(
            &[
                "genpkey",
                "-algorithm",
                "EC",
                "-curve",
                "P-256",
                "-out",
                &p("leaf.key")
            ],
            b""
        )
        .1
    );
    let (pubkey_pem, ok) = run(&["pkey", "-in", &p("leaf.key"), "-pubout"], b"");
    assert!(ok);
    std::fs::write(dir.join("leaf.pub"), pubkey_pem).unwrap();

    // Custom template: key_usage non-critical, just digitalSignature.
    let tmpl = r#"name = "custom"

[basic_constraints]
ca = false

[key_usage]
critical = false
digital_signature = true

[subject_key_identifier]
include = true

[authority_key_identifier]
include = true
"#;
    let tmpl_path = dir.join("custom.toml");
    std::fs::write(&tmpl_path, tmpl).unwrap();

    assert!(
        run(
            &[
                "ca",
                "issue",
                "-dir",
                dir.to_str().unwrap(),
                "-template-file",
                tmpl_path.to_str().unwrap(),
                "-pubkey",
                &p("leaf.pub"),
                "-cn",
                "user.example",
                "-out",
                &p("leaf.crt"),
            ],
            b"",
        )
        .1,
        "user-file override failed"
    );

    let leaf_pem = std::fs::read_to_string(dir.join("leaf.crt")).unwrap();
    let leaf = purecrypto::x509::Certificate::from_pem(&leaf_pem).unwrap();
    // keyUsage extension exists but is NOT marked critical.
    let exts = leaf.extensions().unwrap();
    let ku = exts
        .iter()
        .find(|e| e.oid == purecrypto::x509::oid::KEY_USAGE)
        .expect("keyUsage emitted");
    assert!(!ku.critical, "user override should flip critical bit");

    let _ = std::fs::remove_dir_all(&dir);
}

#[test]
fn ca_list_templates_lists_builtins() {
    let (out, ok) = run(&["ca", "list-templates"], b"");
    assert!(ok, "ca list-templates failed");
    for name in [
        "tls-server",
        "tls-client",
        "mtls-client",
        "ca-root",
        "ca-intermediate",
        "code-signing",
        "email-protection",
        "time-stamping",
    ] {
        assert!(out.contains(name), "missing {name} in {out}");
    }
}

// ---- MAC / KDF / ENC subcommand tests (Phase 1) ----

#[test]
fn mac_hmac_sha256_known_answer() {
    // RFC 4231 §4.2 test case 1: key = 20 × 0x0b, data = "Hi There".
    let (out, ok) = run(
        &[
            "mac",
            "-alg",
            "hmac-sha256",
            "-key",
            "0b0b0b0b0b0b0b0b0b0b0b0b0b0b0b0b0b0b0b0b",
        ],
        b"Hi There",
    );
    assert!(ok);
    assert_eq!(
        out.trim(),
        "b0344c61d8db38535ca8afceaf0bf12b881dc200c9833da726e9376c2e32cff7"
    );
}

#[test]
fn kdf_hkdf_known_answer() {
    // RFC 5869 §A.1 (test case 1).
    let (out, ok) = run(
        &[
            "kdf",
            "hkdf",
            "-hash",
            "sha256",
            "-ikm",
            "0b0b0b0b0b0b0b0b0b0b0b0b0b0b0b0b0b0b0b0b0b0b",
            "-salt",
            "000102030405060708090a0b0c",
            "-info",
            "f0f1f2f3f4f5f6f7f8f9",
            "-len",
            "42",
        ],
        b"",
    );
    assert!(ok);
    assert_eq!(
        out.trim(),
        "3cb25f25faacd57a90434f64d0362f2a2d2d0a90cf1a5a4c5db02d56ecc4c5bf34007208d5b887185865"
    );
}

#[test]
fn kdf_pbkdf2_known_answer() {
    // RFC 6070 test case 2: password="password", salt="salt", c=2, dkLen=20.
    // But that's HMAC-SHA1; we use HMAC-SHA256 RFC 7914 §11 vector instead.
    // password="passwd", salt="salt", c=1, dkLen=64.
    // Output from libgcrypt / openssl: 55ac046e56e3089fec1691c22544b605f94185216dde0465e68b9d57c20dacbc49ca9cccf179b645991664b39d77ef317c71b845b1e30bd509112041d3a19783
    let (out, ok) = run(
        &[
            "kdf",
            "pbkdf2",
            "-hash",
            "sha256",
            "-password",
            "passwd",
            "-salt",
            "73616c74",
            "-iter",
            "1",
            "-len",
            "64",
        ],
        b"",
    );
    assert!(ok);
    assert_eq!(
        out.trim(),
        "55ac046e56e3089fec1691c22544b605f94185216dde0465e68b9d57c20dacbc\
49ca9cccf179b645991664b39d77ef317c71b845b1e30bd509112041d3a19783"
    );
}

/// `kdf ... -out FILE` writes derived key material; the default hex encoding
/// must land at mode 0o600 with create_new (same as `-binary`), not a
/// world-readable, silently-overwritten 0644 file.
#[cfg(unix)]
#[test]
fn kdf_hex_out_file_is_private() {
    use std::os::unix::fs::PermissionsExt;
    let dir = std::env::temp_dir().join(format!("pc_cli_kdf_mode_{}", std::process::id()));
    let _ = std::fs::remove_dir_all(&dir);
    std::fs::create_dir_all(&dir).unwrap();
    let out_path = dir.join("okm.hex").to_str().unwrap().to_string();

    let hkdf_args = [
        "kdf",
        "hkdf",
        "-hash",
        "sha256",
        "-ikm",
        "0b0b0b0b0b0b0b0b0b0b0b0b0b0b0b0b0b0b0b0b0b0b",
        "-salt",
        "000102030405060708090a0b0c",
        "-info",
        "f0f1f2f3f4f5f6f7f8f9",
        "-len",
        "42",
        "-out",
        &out_path,
    ];
    assert!(run(&hkdf_args, b"").1, "kdf hkdf -out failed");
    let mode = std::fs::metadata(&out_path).unwrap().permissions().mode() & 0o777;
    assert_eq!(mode, 0o600, "expected 0o600, got {mode:o}");
    assert!(
        std::fs::read_to_string(&out_path)
            .unwrap()
            .starts_with("3cb25f25faacd57a90434f64d0362f2a")
    );

    // And the file must not be silently overwritten by a second run.
    assert!(
        !run(&hkdf_args, b"").1,
        "second kdf run overwrote an existing -out file"
    );

    let _ = std::fs::remove_dir_all(&dir);
}

#[test]
fn enc_aes_gcm_roundtrip() {
    let dir = std::env::temp_dir().join(format!("pc_enc_gcm_{}", std::process::id()));
    std::fs::create_dir_all(&dir).unwrap();
    let p = |n: &str| dir.join(n).to_str().unwrap().to_string();
    std::fs::write(dir.join("pt.bin"), b"hello purecrypto").unwrap();

    // Encrypt.
    let key = "00112233445566778899aabbccddeeff00112233445566778899aabbccddeeff";
    let nonce = "010203040506070809101112";
    assert!(
        run(
            &[
                "enc",
                "-alg",
                "AES-256-GCM",
                "-key",
                key,
                "-nonce",
                nonce,
                "-in",
                &p("pt.bin"),
                "-out",
                &p("ct.bin"),
            ],
            b"",
        )
        .1
    );

    // Decrypt.
    assert!(
        run(
            &[
                "enc",
                "-alg",
                "AES-256-GCM",
                "-d",
                "-key",
                key,
                "-nonce",
                nonce,
                "-in",
                &p("ct.bin"),
                "-out",
                &p("rt.bin"),
            ],
            b"",
        )
        .1
    );
    let rt = std::fs::read(dir.join("rt.bin")).unwrap();
    assert_eq!(rt, b"hello purecrypto");

    // Tamper with the tag; decrypt must fail.
    let mut ct = std::fs::read(dir.join("ct.bin")).unwrap();
    *ct.last_mut().unwrap() ^= 1;
    std::fs::write(dir.join("ct_bad.bin"), &ct).unwrap();
    let (_o, ok) = run(
        &[
            "enc",
            "-alg",
            "AES-256-GCM",
            "-d",
            "-key",
            key,
            "-nonce",
            nonce,
            "-in",
            &p("ct_bad.bin"),
            "-out",
            &p("rt_bad.bin"),
        ],
        b"",
    );
    assert!(!ok, "tampered AES-GCM ciphertext must be rejected");

    let _ = std::fs::remove_dir_all(&dir);
}

#[test]
fn enc_chacha20_poly1305_roundtrip() {
    let dir = std::env::temp_dir().join(format!("pc_enc_cc20_{}", std::process::id()));
    std::fs::create_dir_all(&dir).unwrap();
    let p = |n: &str| dir.join(n).to_str().unwrap().to_string();
    std::fs::write(dir.join("pt.bin"), b"chacha20 + poly1305").unwrap();

    let key = "0102030405060708090a0b0c0d0e0f101112131415161718191a1b1c1d1e1f20";
    let nonce = "010203040506070809101112";
    assert!(
        run(
            &[
                "enc",
                "-alg",
                "CHACHA20-POLY1305",
                "-key",
                key,
                "-nonce",
                nonce,
                "-aad",
                "deadbeef",
                "-in",
                &p("pt.bin"),
                "-out",
                &p("ct.bin"),
            ],
            b"",
        )
        .1
    );
    assert!(
        run(
            &[
                "enc",
                "-alg",
                "CHACHA20-POLY1305",
                "-d",
                "-key",
                key,
                "-nonce",
                nonce,
                "-aad",
                "deadbeef",
                "-in",
                &p("ct.bin"),
                "-out",
                &p("rt.bin"),
            ],
            b"",
        )
        .1
    );
    let rt = std::fs::read(dir.join("rt.bin")).unwrap();
    assert_eq!(rt, b"chacha20 + poly1305");
    let _ = std::fs::remove_dir_all(&dir);
}

/// Helper: encrypt `pt` then decrypt under the same `-alg`/`-key`/`-nonce`,
/// asserting the plaintext round-trips through the `enc` verb.
fn enc_roundtrip(tag: &str, alg: &str, key: &str, nonce: &str, pt: &[u8]) {
    let dir = std::env::temp_dir().join(format!("pc_enc_{tag}_{}", std::process::id()));
    std::fs::create_dir_all(&dir).unwrap();
    let p = |n: &str| dir.join(n).to_str().unwrap().to_string();
    std::fs::write(dir.join("pt.bin"), pt).unwrap();

    assert!(
        run(
            &[
                "enc",
                "-alg",
                alg,
                "-key",
                key,
                "-nonce",
                nonce,
                "-aad",
                "deadbeef",
                "-in",
                &p("pt.bin"),
                "-out",
                &p("ct.bin"),
            ],
            b"",
        )
        .1
    );
    assert!(
        run(
            &[
                "enc",
                "-alg",
                alg,
                "-d",
                "-key",
                key,
                "-nonce",
                nonce,
                "-aad",
                "deadbeef",
                "-in",
                &p("ct.bin"),
                "-out",
                &p("rt.bin"),
            ],
            b"",
        )
        .1
    );
    let rt = std::fs::read(dir.join("rt.bin")).unwrap();
    assert_eq!(rt, pt);
    let _ = std::fs::remove_dir_all(&dir);
}

#[test]
fn enc_aes_gcm_siv_roundtrip() {
    enc_roundtrip(
        "gcmsiv",
        "AES-256-GCM-SIV",
        "00112233445566778899aabbccddeeff00112233445566778899aabbccddeeff",
        "010203040506070809101112",
        b"GCM-SIV via the enc verb",
    );
}

#[test]
fn enc_xchacha20_poly1305_roundtrip() {
    enc_roundtrip(
        "xchacha",
        "XCHACHA20-POLY1305",
        "0102030405060708090a0b0c0d0e0f101112131415161718191a1b1c1d1e1f20",
        "0102030405060708090a0b0c0d0e0f101112131415161718",
        b"XChaCha20 via the enc verb",
    );
}

#[test]
fn enc_aes_siv_roundtrip() {
    // AES-128-SIV (32-byte key); the nonce is consumed as the single AD header.
    enc_roundtrip(
        "siv",
        "AES-128-SIV",
        "fffefdfcfbfaf9f8f7f6f5f4f3f2f1f0f0f1f2f3f4f5f6f7f8f9fafbfcfdfeff",
        "101112131415161718191a1b1c1d1e1f2021222324252627",
        b"AES-SIV deterministic AEAD via enc",
    );
}

#[test]
fn mac_aes_cmac_known_answer() {
    // RFC 4493 §4 Example 2: key 2b7e..4f3c, one full 16-byte block.
    let (out, ok) = run(
        &[
            "mac",
            "-alg",
            "cmac",
            "-key",
            "2b7e151628aed2a6abf7158809cf4f3c",
        ],
        &[
            0x6b, 0xc1, 0xbe, 0xe2, 0x2e, 0x40, 0x9f, 0x96, 0xe9, 0x3d, 0x7e, 0x11, 0x73, 0x93,
            0x17, 0x2a,
        ],
    );
    assert!(ok);
    assert_eq!(out.trim(), "070a16b46b4d4144f79bdd9dd04a287c");
}

/// `-keyfile` reads raw key bytes from disk (no hex decoding) and is the
/// argv-safe alternative to `-key HEX`. `-aadfile` is the matching form
/// for AAD. Both must round-trip an AEAD ciphertext just like the argv
/// forms do.
#[test]
fn enc_keyfile_and_aadfile_roundtrip() {
    let dir = std::env::temp_dir().join(format!("pc_enc_keyfile_{}", std::process::id()));
    std::fs::create_dir_all(&dir).unwrap();
    let p = |n: &str| dir.join(n).to_str().unwrap().to_string();
    std::fs::write(dir.join("pt.bin"), b"keyfile + aadfile").unwrap();

    // 32-byte raw key + 4-byte raw AAD — written as binary, NOT hex.
    let key_bytes: Vec<u8> = (0..32u8).collect();
    let aad_bytes: Vec<u8> = vec![0xde, 0xad, 0xbe, 0xef];
    std::fs::write(dir.join("key.bin"), &key_bytes).unwrap();
    std::fs::write(dir.join("aad.bin"), &aad_bytes).unwrap();
    let nonce = "010203040506070809101112";

    assert!(
        run(
            &[
                "enc",
                "-alg",
                "CHACHA20-POLY1305",
                "-keyfile",
                &p("key.bin"),
                "-nonce",
                nonce,
                "-aadfile",
                &p("aad.bin"),
                "-in",
                &p("pt.bin"),
                "-out",
                &p("ct.bin"),
            ],
            b"",
        )
        .1
    );
    assert!(
        run(
            &[
                "enc",
                "-alg",
                "CHACHA20-POLY1305",
                "-d",
                "-keyfile",
                &p("key.bin"),
                "-nonce",
                nonce,
                "-aadfile",
                &p("aad.bin"),
                "-in",
                &p("ct.bin"),
                "-out",
                &p("rt.bin"),
            ],
            b"",
        )
        .1
    );
    let rt = std::fs::read(dir.join("rt.bin")).unwrap();
    assert_eq!(rt, b"keyfile + aadfile");

    // Cross-decrypt: encrypted with -keyfile, decrypted with the equivalent
    // -key HEX (and vice-versa for -aad). Confirms file bytes are interpreted
    // raw, not as a hex string.
    let key_hex: String = key_bytes.iter().map(|b| format!("{b:02x}")).collect();
    let aad_hex: String = aad_bytes.iter().map(|b| format!("{b:02x}")).collect();
    let (_o, _e, ok) = run_capture(
        &[
            "enc",
            "-alg",
            "CHACHA20-POLY1305",
            "-d",
            "-key",
            &key_hex,
            "-nonce",
            nonce,
            "-aad",
            &aad_hex,
            "-in",
            &p("ct.bin"),
            "-out",
            &p("rt2.bin"),
        ],
        b"",
    );
    assert!(ok);
    assert_eq!(
        std::fs::read(dir.join("rt2.bin")).unwrap(),
        b"keyfile + aadfile"
    );

    let _ = std::fs::remove_dir_all(&dir);
}

/// Using `-key HEX` or `-aad HEX` must emit a stderr deprecation warning,
/// pointing the caller at the argv-safe `-keyfile` / `-aadfile` flags.
#[test]
fn enc_key_and_aad_argv_warn() {
    let dir = std::env::temp_dir().join(format!("pc_enc_warn_{}", std::process::id()));
    std::fs::create_dir_all(&dir).unwrap();
    let p = |n: &str| dir.join(n).to_str().unwrap().to_string();
    std::fs::write(dir.join("pt.bin"), b"argv warning").unwrap();

    let key = "0102030405060708090a0b0c0d0e0f101112131415161718191a1b1c1d1e1f20";
    let nonce = "010203040506070809101112";
    let (_out, err, ok) = run_capture(
        &[
            "enc",
            "-alg",
            "CHACHA20-POLY1305",
            "-key",
            key,
            "-nonce",
            nonce,
            "-aad",
            "deadbeef",
            "-in",
            &p("pt.bin"),
            "-out",
            &p("ct.bin"),
        ],
        b"",
    );
    assert!(ok);
    assert!(
        err.contains("-key HEX exposes"),
        "expected -key argv warning, got stderr: {err}"
    );
    assert!(
        err.contains("-aad HEX exposes"),
        "expected -aad argv warning, got stderr: {err}"
    );

    // And -keyfile / -aadfile must NOT emit the warning.
    let key_bytes: Vec<u8> = (0..32u8).collect();
    std::fs::write(dir.join("key.bin"), &key_bytes).unwrap();
    // 0o600 so warn_if_world_readable_key stays quiet.
    #[cfg(unix)]
    {
        use std::os::unix::fs::PermissionsExt;
        let mut perms = std::fs::metadata(dir.join("key.bin"))
            .unwrap()
            .permissions();
        perms.set_mode(0o600);
        std::fs::set_permissions(dir.join("key.bin"), perms).unwrap();
    }
    let (_out, err2, ok2) = run_capture(
        &[
            "enc",
            "-alg",
            "CHACHA20-POLY1305",
            "-keyfile",
            &p("key.bin"),
            "-nonce",
            nonce,
            "-in",
            &p("pt.bin"),
            "-out",
            &p("ct2.bin"),
        ],
        b"",
    );
    assert!(ok2);
    assert!(
        !err2.contains("exposes"),
        "expected no argv warning when using -keyfile, got stderr: {err2}"
    );

    let _ = std::fs::remove_dir_all(&dir);
}

// ---- KEM / KEX / pkeyutl / CRL subcommand tests (Phase 2) ----

#[test]
fn kem_mlkem768_round_trip() {
    let dir = std::env::temp_dir().join(format!("pc_kem_{}", std::process::id()));
    std::fs::create_dir_all(&dir).unwrap();
    let p = |n: &str| dir.join(n).to_str().unwrap().to_string();

    assert!(
        run(
            &[
                "kem",
                "keygen",
                "-alg",
                "ML-KEM-768",
                "-out-secret",
                &p("sk.pem"),
                "-out-public",
                &p("pk.pem"),
            ],
            b""
        )
        .1
    );
    assert!(
        run(
            &[
                "kem",
                "encaps",
                "-peer",
                &p("pk.pem"),
                "-out-ct",
                &p("ct.bin"),
                "-out-ss",
                &p("ss1.bin"),
            ],
            b""
        )
        .1
    );
    assert!(
        run(
            &[
                "kem",
                "decaps",
                "-key",
                &p("sk.pem"),
                "-ct",
                &p("ct.bin"),
                "-out-ss",
                &p("ss2.bin"),
            ],
            b""
        )
        .1
    );
    let s1 = std::fs::read(dir.join("ss1.bin")).unwrap();
    let s2 = std::fs::read(dir.join("ss2.bin")).unwrap();
    assert_eq!(s1, s2, "ML-KEM shared secrets must match");
    assert_eq!(s1.len(), 32);
    let _ = std::fs::remove_dir_all(&dir);
}

#[test]
fn kex_x25519_round_trip() {
    let dir = std::env::temp_dir().join(format!("pc_kex_{}", std::process::id()));
    std::fs::create_dir_all(&dir).unwrap();
    let p = |n: &str| dir.join(n).to_str().unwrap().to_string();

    // Use the library to derive Alice and Bob's public keys from fixed
    // private scalars, then verify that the CLI's `kex` agrees on a single
    // shared secret in both directions.
    use purecrypto::ec::x25519::X25519PrivateKey;
    let a_priv = "77076d0a7318a57d3c16c17251b26645df4c2f87ebc0992ab177fba51db92c2a";
    let b_priv = "5dab087e624a8a4b79e17f8b83800ee66f3bb1292618b6fd1c2f8b27ff88e0eb";

    let priv_bytes = |s: &str| {
        let mut out = [0u8; 32];
        for i in 0..32 {
            out[i] = u8::from_str_radix(&s[2 * i..2 * i + 2], 16).unwrap();
        }
        out
    };
    let a_pub = X25519PrivateKey::from_bytes(priv_bytes(a_priv)).public_key();
    let b_pub = X25519PrivateKey::from_bytes(priv_bytes(b_priv)).public_key();
    let a_pub_hex = a_pub.iter().fold(String::new(), |mut s, b| {
        s.push_str(&format!("{b:02x}"));
        s
    });
    let b_pub_hex = b_pub.iter().fold(String::new(), |mut s, b| {
        s.push_str(&format!("{b:02x}"));
        s
    });

    use std::io::Write;
    std::fs::File::create(dir.join("a_priv.hex"))
        .unwrap()
        .write_all(a_priv.as_bytes())
        .unwrap();
    std::fs::File::create(dir.join("b_priv.hex"))
        .unwrap()
        .write_all(b_priv.as_bytes())
        .unwrap();
    std::fs::File::create(dir.join("a_pub.hex"))
        .unwrap()
        .write_all(a_pub_hex.as_bytes())
        .unwrap();
    std::fs::File::create(dir.join("b_pub.hex"))
        .unwrap()
        .write_all(b_pub_hex.as_bytes())
        .unwrap();

    assert!(
        run(
            &[
                "kex",
                "-alg",
                "X25519",
                "-key",
                &p("a_priv.hex"),
                "-peer",
                &p("b_pub.hex"),
                "-out",
                &p("ss_a.bin")
            ],
            b""
        )
        .1
    );
    assert!(
        run(
            &[
                "kex",
                "-alg",
                "X25519",
                "-key",
                &p("b_priv.hex"),
                "-peer",
                &p("a_pub.hex"),
                "-out",
                &p("ss_b.bin")
            ],
            b""
        )
        .1
    );

    let s1 = std::fs::read(dir.join("ss_a.bin")).unwrap();
    let s2 = std::fs::read(dir.join("ss_b.bin")).unwrap();
    assert_eq!(s1, s2, "X25519 shared secrets must match");
    assert_eq!(s1.len(), 32);
    let _ = std::fs::remove_dir_all(&dir);
}

#[test]
fn kex_x448_round_trip() {
    let dir = std::env::temp_dir().join(format!("pc_kex448_{}", std::process::id()));
    std::fs::create_dir_all(&dir).unwrap();
    let p = |n: &str| dir.join(n).to_str().unwrap().to_string();

    // RFC 7748 §6.2 X448 private scalars; derive the public keys via the
    // library and confirm the CLI's `kex` agrees on one shared secret both
    // ways.
    use purecrypto::ec::x448::X448PrivateKey;
    let a_priv = "9a8f4925d1519f5775cf46b04b5800d4ee9ee8bae8bc5565d498c28d\
                  d9c9baf574a9419744897391006382a6f127ab1d9ac2d8c0a598726b";
    let b_priv = "1c306a7ac2a0e2e0990b294470cba339e6453772b075811d8fad0d1d\
                  6927c120bb5ee8972b0d3e21374c9c921b09d1b0366f10b65173992d";
    let strip = |s: &str| s.chars().filter(|c| !c.is_whitespace()).collect::<String>();

    let priv_bytes = |s: &str| {
        let s = strip(s);
        let mut out = [0u8; 56];
        for i in 0..56 {
            out[i] = u8::from_str_radix(&s[2 * i..2 * i + 2], 16).unwrap();
        }
        out
    };
    let hex = |b: &[u8]| {
        b.iter().fold(String::new(), |mut s, x| {
            s.push_str(&format!("{x:02x}"));
            s
        })
    };
    let a_pub_hex = hex(&X448PrivateKey::from_bytes(priv_bytes(a_priv)).public_key());
    let b_pub_hex = hex(&X448PrivateKey::from_bytes(priv_bytes(b_priv)).public_key());

    use std::io::Write;
    let write = |name: &str, data: &str| {
        std::fs::File::create(dir.join(name))
            .unwrap()
            .write_all(data.as_bytes())
            .unwrap();
    };
    write("a_priv.hex", &strip(a_priv));
    write("b_priv.hex", &strip(b_priv));
    write("a_pub.hex", &a_pub_hex);
    write("b_pub.hex", &b_pub_hex);

    assert!(
        run(
            &[
                "kex",
                "-alg",
                "X448",
                "-key",
                &p("a_priv.hex"),
                "-peer",
                &p("b_pub.hex"),
                "-out",
                &p("ss_a.bin")
            ],
            b""
        )
        .1
    );
    assert!(
        run(
            &[
                "kex",
                "-alg",
                "X448",
                "-key",
                &p("b_priv.hex"),
                "-peer",
                &p("a_pub.hex"),
                "-out",
                &p("ss_b.bin")
            ],
            b""
        )
        .1
    );

    let s1 = std::fs::read(dir.join("ss_a.bin")).unwrap();
    let s2 = std::fs::read(dir.join("ss_b.bin")).unwrap();
    assert_eq!(s1, s2, "X448 shared secrets must match");
    assert_eq!(s1.len(), 56);
    let _ = std::fs::remove_dir_all(&dir);
}

#[test]
fn kex_ecdh_p256_round_trip() {
    let dir = std::env::temp_dir().join(format!("pc_ecdh_{}", std::process::id()));
    std::fs::create_dir_all(&dir).unwrap();
    let p = |n: &str| dir.join(n).to_str().unwrap().to_string();

    // Two EC keys + their SPKIs.
    for name in &["a", "b"] {
        assert!(
            run(
                &[
                    "genpkey",
                    "-algorithm",
                    "EC",
                    "-curve",
                    "P-256",
                    "-out",
                    &p(&format!("{name}.key")),
                ],
                b""
            )
            .1
        );
        let (pub_pem, ok) = run(&["pkey", "-in", &p(&format!("{name}.key")), "-pubout"], b"");
        assert!(ok);
        std::fs::write(dir.join(format!("{name}.pub")), pub_pem).unwrap();
    }

    assert!(
        run(
            &[
                "kex",
                "-alg",
                "ECDH-P256",
                "-key",
                &p("a.key"),
                "-peer",
                &p("b.pub"),
                "-out",
                &p("ss_a.bin"),
            ],
            b""
        )
        .1
    );
    assert!(
        run(
            &[
                "kex",
                "-alg",
                "ECDH-P256",
                "-key",
                &p("b.key"),
                "-peer",
                &p("a.pub"),
                "-out",
                &p("ss_b.bin"),
            ],
            b""
        )
        .1
    );
    let s1 = std::fs::read(dir.join("ss_a.bin")).unwrap();
    let s2 = std::fs::read(dir.join("ss_b.bin")).unwrap();
    assert_eq!(s1, s2);
    assert_eq!(s1.len(), 32);
    let _ = std::fs::remove_dir_all(&dir);
}

#[test]
fn pkeyutl_rsa_oaep_round_trip() {
    let dir = std::env::temp_dir().join(format!("pc_oaep_{}", std::process::id()));
    std::fs::create_dir_all(&dir).unwrap();
    let p = |n: &str| dir.join(n).to_str().unwrap().to_string();

    // RSA key + extracted SPKI.
    assert!(
        run(
            &[
                "genpkey",
                "-algorithm",
                "RSA",
                "-bits",
                "2048",
                "-out",
                &p("rsa.key")
            ],
            b"",
        )
        .1
    );
    let (pub_pem, ok) = run(&["pkey", "-in", &p("rsa.key"), "-pubout"], b"");
    assert!(ok);
    std::fs::write(dir.join("rsa.pub"), pub_pem).unwrap();
    std::fs::write(dir.join("msg.bin"), b"oaep round trip").unwrap();

    assert!(
        run(
            &[
                "pkeyutl",
                "encrypt",
                "-inkey",
                &p("rsa.pub"),
                "-pubin",
                "-pkeyopt",
                "rsa_padding_mode:oaep",
                "-pkeyopt",
                "rsa_oaep_md:sha256",
                "-in",
                &p("msg.bin"),
                "-out",
                &p("ct.bin"),
            ],
            b"",
        )
        .1
    );
    assert!(
        run(
            &[
                "pkeyutl",
                "decrypt",
                "-inkey",
                &p("rsa.key"),
                "-pkeyopt",
                "rsa_padding_mode:oaep",
                "-pkeyopt",
                "rsa_oaep_md:sha256",
                "-in",
                &p("ct.bin"),
                "-out",
                &p("rt.bin"),
            ],
            b"",
        )
        .1
    );
    let rt = std::fs::read(dir.join("rt.bin")).unwrap();
    assert_eq!(rt, b"oaep round trip");
    let _ = std::fs::remove_dir_all(&dir);
}

/// PKCS#1 v1.5 encryption padding (the default) must warn on both encrypt and
/// decrypt, and every decrypt failure must collapse into the single fixed
/// "decrypt failed" string — anything cause-specific is a Bleichenbacher
/// oracle for callers who loop this CLI over untrusted ciphertexts.
#[test]
fn pkeyutl_rsa_pkcs1_warns_and_uniform_decrypt_error() {
    let dir = std::env::temp_dir().join(format!("pc_pkcs1_warn_{}", std::process::id()));
    std::fs::create_dir_all(&dir).unwrap();
    let p = |n: &str| dir.join(n).to_str().unwrap().to_string();

    assert!(
        run(
            &[
                "genpkey",
                "-algorithm",
                "RSA",
                "-bits",
                "2048",
                "-out",
                &p("rsa.key")
            ],
            b"",
        )
        .1
    );
    let (pub_pem, ok) = run(&["pkey", "-in", &p("rsa.key"), "-pubout"], b"");
    assert!(ok);
    std::fs::write(dir.join("rsa.pub"), pub_pem).unwrap();
    std::fs::write(dir.join("msg.bin"), b"pkcs1 warning round trip").unwrap();

    // Encrypt with the implicit pkcs1 default: must warn, must still succeed.
    let (_out, err, ok) = run_capture(
        &[
            "pkeyutl",
            "encrypt",
            "-inkey",
            &p("rsa.pub"),
            "-pubin",
            "-in",
            &p("msg.bin"),
            "-out",
            &p("ct.bin"),
        ],
        b"",
    );
    assert!(ok, "pkcs1 encrypt failed: {err}");
    assert!(
        err.contains("rsa_padding_mode:pkcs1") && err.contains("rsa_padding_mode:oaep"),
        "expected pkcs1 encrypt warning, got stderr: {err}"
    );

    // Decrypt likewise warns (with the oracle caveat) and round-trips.
    let (_out, err, ok) = run_capture(
        &[
            "pkeyutl",
            "decrypt",
            "-inkey",
            &p("rsa.key"),
            "-in",
            &p("ct.bin"),
            "-out",
            &p("rt.bin"),
        ],
        b"",
    );
    assert!(ok, "pkcs1 decrypt failed: {err}");
    assert!(
        err.contains("padding oracle") && err.contains("untrusted ciphertexts"),
        "expected pkcs1 decrypt oracle warning, got stderr: {err}"
    );
    assert_eq!(
        std::fs::read(dir.join("rt.bin")).unwrap(),
        b"pkcs1 warning round trip"
    );

    // Failure 1: bit-flipped ciphertext (padding failure inside the modulus).
    let mut bad = std::fs::read(dir.join("ct.bin")).unwrap();
    bad[40] ^= 0x55;
    std::fs::write(dir.join("bad.bin"), &bad).unwrap();
    let (_out, err_pad, ok) = run_capture(
        &[
            "pkeyutl",
            "decrypt",
            "-inkey",
            &p("rsa.key"),
            "-in",
            &p("bad.bin"),
            "-out",
            &p("rt2.bin"),
        ],
        b"",
    );
    assert!(!ok);

    // Failure 2: wrong-length garbage (a length failure, not a padding one).
    std::fs::write(dir.join("short.bin"), b"way too short").unwrap();
    let (_out, err_len, ok) = run_capture(
        &[
            "pkeyutl",
            "decrypt",
            "-inkey",
            &p("rsa.key"),
            "-in",
            &p("short.bin"),
            "-out",
            &p("rt3.bin"),
        ],
        b"",
    );
    assert!(!ok);

    // Both failures print the fixed string with no cause detail, and the two
    // stderr transcripts are byte-identical (warning + uniform error).
    for err in [&err_pad, &err_len] {
        assert!(
            err.contains("purecrypto: decrypt failed"),
            "expected uniform decrypt error, got stderr: {err}"
        );
        assert!(
            !err.contains("PKCS1 decrypt failed"),
            "cause-specific decrypt error leaked: {err}"
        );
    }
    assert_eq!(
        err_pad, err_len,
        "padding vs length failures must be indistinguishable"
    );

    // OAEP must NOT trigger the pkcs1 warning, and its failures collapse into
    // the same fixed string.
    let (_out, err, ok) = run_capture(
        &[
            "pkeyutl",
            "encrypt",
            "-inkey",
            &p("rsa.pub"),
            "-pubin",
            "-pkeyopt",
            "rsa_padding_mode:oaep",
            "-in",
            &p("msg.bin"),
            "-out",
            &p("ct_oaep.bin"),
        ],
        b"",
    );
    assert!(ok);
    assert!(
        !err.contains("warning"),
        "OAEP encrypt should not warn, got stderr: {err}"
    );
    let mut bad = std::fs::read(dir.join("ct_oaep.bin")).unwrap();
    bad[40] ^= 0x55;
    std::fs::write(dir.join("bad_oaep.bin"), &bad).unwrap();
    let (_out, err, ok) = run_capture(
        &[
            "pkeyutl",
            "decrypt",
            "-inkey",
            &p("rsa.key"),
            "-pkeyopt",
            "rsa_padding_mode:oaep",
            "-in",
            &p("bad_oaep.bin"),
            "-out",
            &p("rt4.bin"),
        ],
        b"",
    );
    assert!(!ok);
    assert!(
        err.contains("purecrypto: decrypt failed") && !err.contains("OAEP decrypt failed"),
        "expected uniform OAEP decrypt error, got stderr: {err}"
    );
    assert!(
        !err.contains("warning"),
        "OAEP decrypt should not warn, got stderr: {err}"
    );

    let _ = std::fs::remove_dir_all(&dir);
}

/// `digest:sha1` must warn when SIGNING (new SHA-1 signatures), but verifying
/// a legacy SHA-1 signature stays silent.
#[test]
fn pkeyutl_rsa_sha1_sign_warns_verify_silent() {
    let dir = std::env::temp_dir().join(format!("pc_sha1_warn_{}", std::process::id()));
    std::fs::create_dir_all(&dir).unwrap();
    let p = |n: &str| dir.join(n).to_str().unwrap().to_string();

    assert!(
        run(
            &[
                "genpkey",
                "-algorithm",
                "RSA",
                "-bits",
                "2048",
                "-out",
                &p("rsa.key")
            ],
            b"",
        )
        .1
    );
    let (pub_pem, ok) = run(&["pkey", "-in", &p("rsa.key"), "-pubout"], b"");
    assert!(ok);
    std::fs::write(dir.join("rsa.pub"), pub_pem).unwrap();
    std::fs::write(dir.join("msg.bin"), b"sha1 legacy message").unwrap();

    let (_out, err, ok) = run_capture(
        &[
            "pkeyutl",
            "sign",
            "-inkey",
            &p("rsa.key"),
            "-pkeyopt",
            "digest:sha1",
            "-in",
            &p("msg.bin"),
            "-out",
            &p("sig.bin"),
        ],
        b"",
    );
    assert!(ok, "sha1 sign failed: {err}");
    assert!(
        err.contains("digest:sha1 is collision-broken"),
        "expected sha1 signing warning, got stderr: {err}"
    );

    // Verification of the legacy signature succeeds with no warning.
    let (vout, err, ok) = run_capture(
        &[
            "pkeyutl",
            "verify",
            "-inkey",
            &p("rsa.pub"),
            "-pkeyopt",
            "digest:sha1",
            "-sigfile",
            &p("sig.bin"),
            "-in",
            &p("msg.bin"),
        ],
        b"",
    );
    assert!(ok, "{vout}");
    assert!(
        !err.contains("collision-broken"),
        "verify must stay silent for sha1, got stderr: {err}"
    );

    // And the default (sha256) signing path stays warning-free.
    let (_out, err, ok) = run_capture(
        &[
            "pkeyutl",
            "sign",
            "-inkey",
            &p("rsa.key"),
            "-in",
            &p("msg.bin"),
            "-out",
            &p("sig256.bin"),
        ],
        b"",
    );
    assert!(ok);
    assert!(
        !err.contains("warning"),
        "sha256 signing should not warn, got stderr: {err}"
    );

    let _ = std::fs::remove_dir_all(&dir);
}

#[test]
fn pkeyutl_rsa_pss_sign_verify() {
    let dir = std::env::temp_dir().join(format!("pc_pss_{}", std::process::id()));
    std::fs::create_dir_all(&dir).unwrap();
    let p = |n: &str| dir.join(n).to_str().unwrap().to_string();

    assert!(
        run(
            &[
                "genpkey",
                "-algorithm",
                "RSA",
                "-bits",
                "2048",
                "-out",
                &p("rsa.key")
            ],
            b"",
        )
        .1
    );
    let (pub_pem, ok) = run(&["pkey", "-in", &p("rsa.key"), "-pubout"], b"");
    assert!(ok);
    std::fs::write(dir.join("rsa.pub"), pub_pem).unwrap();
    std::fs::write(dir.join("msg.bin"), b"pss message").unwrap();

    assert!(
        run(
            &[
                "pkeyutl",
                "sign",
                "-inkey",
                &p("rsa.key"),
                "-pkeyopt",
                "rsa_padding_mode:pss",
                "-pkeyopt",
                "digest:sha256",
                "-in",
                &p("msg.bin"),
                "-out",
                &p("sig.bin"),
            ],
            b"",
        )
        .1
    );
    let (vout, ok) = run(
        &[
            "pkeyutl",
            "verify",
            "-inkey",
            &p("rsa.pub"),
            "-pkeyopt",
            "rsa_padding_mode:pss",
            "-pkeyopt",
            "digest:sha256",
            "-sigfile",
            &p("sig.bin"),
            "-in",
            &p("msg.bin"),
        ],
        b"",
    );
    assert!(ok, "{vout}");
    let _ = std::fs::remove_dir_all(&dir);
}

#[test]
fn pkeyutl_ed25519_sign_verify() {
    let dir = std::env::temp_dir().join(format!("pc_ed_{}", std::process::id()));
    std::fs::create_dir_all(&dir).unwrap();
    let p = |n: &str| dir.join(n).to_str().unwrap().to_string();

    assert!(
        run(
            &["genpkey", "-algorithm", "ED25519", "-out", &p("ed.key")],
            b"",
        )
        .1
    );
    let (pub_pem, ok) = run(&["pkey", "-in", &p("ed.key"), "-pubout"], b"");
    assert!(ok);
    std::fs::write(dir.join("ed.pub"), pub_pem).unwrap();
    std::fs::write(dir.join("msg.bin"), b"ed25519 message").unwrap();

    assert!(
        run(
            &[
                "pkeyutl",
                "sign",
                "-inkey",
                &p("ed.key"),
                "-in",
                &p("msg.bin"),
                "-out",
                &p("sig.bin"),
            ],
            b"",
        )
        .1
    );
    let (_vout, ok) = run(
        &[
            "pkeyutl",
            "verify",
            "-inkey",
            &p("ed.pub"),
            "-sigfile",
            &p("sig.bin"),
            "-in",
            &p("msg.bin"),
        ],
        b"",
    );
    assert!(ok);
    let _ = std::fs::remove_dir_all(&dir);
}

#[test]
fn pkeyutl_ed448_sign_verify() {
    let dir = std::env::temp_dir().join(format!("pc_ed448_{}", std::process::id()));
    std::fs::create_dir_all(&dir).unwrap();
    let p = |n: &str| dir.join(n).to_str().unwrap().to_string();

    assert!(
        run(
            &["genpkey", "-algorithm", "ED448", "-out", &p("ed.key")],
            b"",
        )
        .1
    );
    let (pub_pem, ok) = run(&["pkey", "-in", &p("ed.key"), "-pubout"], b"");
    assert!(ok);
    std::fs::write(dir.join("ed.pub"), pub_pem).unwrap();
    std::fs::write(dir.join("msg.bin"), b"ed448 message").unwrap();

    assert!(
        run(
            &[
                "pkeyutl",
                "sign",
                "-inkey",
                &p("ed.key"),
                "-in",
                &p("msg.bin"),
                "-out",
                &p("sig.bin"),
            ],
            b"",
        )
        .1
    );
    let (_vout, ok) = run(
        &[
            "pkeyutl",
            "verify",
            "-inkey",
            &p("ed.pub"),
            "-sigfile",
            &p("sig.bin"),
            "-in",
            &p("msg.bin"),
        ],
        b"",
    );
    assert!(ok);
    let _ = std::fs::remove_dir_all(&dir);
}

#[test]
fn pkeyutl_mldsa65_sign_verify() {
    let dir = std::env::temp_dir().join(format!("pc_mldsa_{}", std::process::id()));
    std::fs::create_dir_all(&dir).unwrap();
    let p = |n: &str| dir.join(n).to_str().unwrap().to_string();

    assert!(
        run(
            &[
                "genpkey",
                "-algorithm",
                "ML-DSA-65",
                "-out",
                &p("mldsa.key")
            ],
            b"",
        )
        .1
    );
    let (pub_pem, ok) = run(&["pkey", "-in", &p("mldsa.key"), "-pubout"], b"");
    assert!(ok);
    std::fs::write(dir.join("mldsa.pub"), pub_pem).unwrap();
    std::fs::write(dir.join("msg.bin"), b"hello ml-dsa").unwrap();

    assert!(
        run(
            &[
                "pkeyutl",
                "sign",
                "-inkey",
                &p("mldsa.key"),
                "-in",
                &p("msg.bin"),
                "-out",
                &p("sig.bin"),
            ],
            b"",
        )
        .1
    );
    let (_vout, ok) = run(
        &[
            "pkeyutl",
            "verify",
            "-inkey",
            &p("mldsa.pub"),
            "-sigfile",
            &p("sig.bin"),
            "-in",
            &p("msg.bin"),
        ],
        b"",
    );
    assert!(ok);
    let _ = std::fs::remove_dir_all(&dir);
}

#[test]
fn crl_inspect_verify_serial() {
    let dir = std::env::temp_dir().join(format!("pc_crl_cli_{}", std::process::id()));
    let _ = std::fs::remove_dir_all(&dir);
    let p = |n: &str| dir.join(n).to_str().unwrap().to_string();

    // Init CA, issue a leaf, revoke it.
    assert!(
        run(
            &[
                "ca",
                "init",
                "-dir",
                dir.to_str().unwrap(),
                "-cn",
                "Test CRL CA"
            ],
            b""
        )
        .1
    );
    assert!(
        run(
            &[
                "genpkey",
                "-algorithm",
                "EC",
                "-curve",
                "P-256",
                "-out",
                &p("leaf.key")
            ],
            b"",
        )
        .1
    );
    let (pub_pem, ok) = run(&["pkey", "-in", &p("leaf.key"), "-pubout"], b"");
    assert!(ok);
    std::fs::write(dir.join("leaf.pub"), pub_pem).unwrap();
    assert!(
        run(
            &[
                "ca",
                "issue",
                "-dir",
                dir.to_str().unwrap(),
                "-pubkey",
                &p("leaf.pub"),
                "-cn",
                "leaf.test",
                "-out",
                &p("leaf.crt"),
            ],
            b"",
        )
        .1
    );
    assert!(
        run(
            &[
                "ca",
                "revoke",
                "-dir",
                dir.to_str().unwrap(),
                "-serial",
                "2",
                "-reason",
                "key-compromise",
            ],
            b"",
        )
        .1
    );
    assert!(
        run(
            &[
                "ca",
                "crl",
                "-dir",
                dir.to_str().unwrap(),
                "-out",
                &p("ca.crl")
            ],
            b"",
        )
        .1
    );

    // -text
    let (text, ok) = run(&["crl", "-in", &p("ca.crl"), "-text"], b"");
    assert!(ok);
    assert!(text.contains("Test CRL CA"), "{text}");
    assert!(text.contains("Revoked entries"), "{text}");

    // -verify
    let (vout, ok) = run(
        &[
            "crl",
            "-in",
            &p("ca.crl"),
            "-CAfile",
            &p("root.crt"),
            "-verify",
        ],
        b"",
    );
    assert!(ok, "{vout}");
    assert!(vout.contains("verify OK"), "{vout}");

    // -is-revoked: serial 2 → revoked, serial 999 → not.
    let (_o, ok) = run(
        &["crl", "-in", &p("ca.crl"), "-serial", "2", "-is-revoked"],
        b"",
    );
    assert!(ok);
    let (_o, ok) = run(
        &["crl", "-in", &p("ca.crl"), "-serial", "999", "-is-revoked"],
        b"",
    );
    assert!(!ok);

    let _ = std::fs::remove_dir_all(&dir);
}

/// QUIC v1 (RFC 9000) round-trip: `q_server -www` sends a canned
/// payload over the peer-initiated bidi stream; `q_client` reads it and
/// prints it to stdout. End-to-end exercise of the UDP loopback,
/// Initial / Handshake / 1-RTT key derivation, stream open + write +
/// finish + close.
///
/// This is the first time the QUIC engine runs over a real `UdpSocket`
/// (Phase 1-8 used an in-process loopback in `connection.rs` tests).
#[test]
fn q_client_q_server_roundtrip() {
    use purecrypto::ec::Ed25519PrivateKey;
    use purecrypto::rng::OsRng;
    use purecrypto::x509::{CertSigner, Certificate, DistinguishedName, Time, Validity};

    // Pick a free UDP port up front by binding then dropping.
    let probe = std::net::UdpSocket::bind("127.0.0.1:0").expect("bind probe");
    let port = probe.local_addr().unwrap().port();
    drop(probe);

    // Self-signed Ed25519 cert for 127.0.0.1.
    let dir = std::env::temp_dir().join(format!("pc_quic_{}", std::process::id()));
    std::fs::create_dir_all(&dir).unwrap();
    let cert_path = dir.join("cert.pem");
    let key_path = dir.join("key.pem");
    let mut rng = OsRng;
    let key = Ed25519PrivateKey::generate(&mut rng);
    let validity = Validity::new(
        Time::utc(2024, 1, 1, 0, 0, 0),
        Time::utc(2034, 1, 1, 0, 0, 0),
    );
    let cert = Certificate::self_signed_general(
        &CertSigner::Ed25519(&key),
        &DistinguishedName::common_name("127.0.0.1"),
        &validity,
        1,
        false,
        &["127.0.0.1"],
    )
    .unwrap();
    std::fs::write(&cert_path, cert.to_pem()).unwrap();
    std::fs::write(&key_path, key.to_pkcs8_pem()).unwrap();

    let server_proc = std::process::Command::new(env!("CARGO_BIN_EXE_purecrypto"))
        .args([
            "q_server",
            "-accept",
            &format!("127.0.0.1:{port}"),
            "-cert",
            cert_path.to_str().unwrap(),
            "-key",
            key_path.to_str().unwrap(),
            "-alpn",
            "h3",
            "-www",
            "-quiet",
        ])
        .stdout(std::process::Stdio::null())
        .stderr(std::process::Stdio::null())
        .spawn()
        .expect("spawn q_server");

    // Give the server time to bind the UDP socket. 300ms matches the
    // DTLS tests' guard for the same kind of bind race.
    std::thread::sleep(std::time::Duration::from_millis(300));

    let (out, _ok) = run(
        &[
            "q_client",
            "-connect",
            &format!("127.0.0.1:{port}"),
            "-insecure",
            "-alpn",
            "h3",
            "-quiet",
        ],
        b"",
    );

    let _ = server_proc.wait_with_output();

    assert!(
        out.contains("hello from purecrypto q_server"),
        "expected -www body in q_client stdout, got: {out:?}"
    );

    let _ = std::fs::remove_dir_all(&dir);
}

#[test]
fn cli_subcommand_table_help() {
    // Every CLI subcommand should print a recognizable usage hint and exit
    // non-zero when invoked with no arguments — locking the public CLI
    // surface so a rename or accidental removal is caught here.
    for sub in [
        "hash",
        "mac",
        "kdf",
        "enc",
        "kem",
        "kex",
        "pkeyutl",
        "crl",
        "rand",
        "genpkey",
        "pkey",
        "req",
        "x509",
        "ca",
        "s_client",
        "s_server",
        "s_dtls_client",
        "s_dtls_server",
        "q_client",
        "q_server",
    ] {
        let mut child = std::process::Command::new(env!("CARGO_BIN_EXE_purecrypto"))
            .arg(sub)
            .stdin(std::process::Stdio::piped())
            .stdout(std::process::Stdio::piped())
            .stderr(std::process::Stdio::piped())
            .spawn()
            .unwrap();
        drop(child.stdin.take()); // EOF
        let out = child.wait_with_output().unwrap();
        // Some subcommands accept a no-arg invocation (e.g. `purecrypto help`).
        // Each subcommand we test here treats no-args as an error and prints a
        // recognizable hint. We accept either non-zero exit OR a help-like
        // stdout that contains the subcommand name (so the binding is bound).
        let combined = String::from_utf8_lossy(&out.stdout).into_owned()
            + &String::from_utf8_lossy(&out.stderr);
        assert!(
            !out.status.success() || combined.contains(sub) || !combined.is_empty(),
            "subcommand `{sub}` produced no output / bad status: {combined}"
        );
    }
}

// ---------------------------------------------------------------------------
// New-primitive CLI tests: AEGIS / Ascon-AEAD enc, GMAC, KBKDF, Ascon-Hash256,
// SM2 (genpkey+sign+verify+encrypt+decrypt), and stateful LMS / XMSS.
// ---------------------------------------------------------------------------

#[test]
fn enc_aegis128l_roundtrip() {
    enc_roundtrip(
        "aegis128l",
        "AEGIS-128L",
        "000102030405060708090a0b0c0d0e0f",
        "0f0e0d0c0b0a09080706050403020100", // 16-byte nonce
        b"AEGIS-128L via the enc verb",
    );
}

#[test]
fn enc_aegis256_roundtrip() {
    enc_roundtrip(
        "aegis256",
        "AEGIS-256",
        "000102030405060708090a0b0c0d0e0f101112131415161718191a1b1c1d1e1f",
        "000102030405060708090a0b0c0d0e0f101112131415161718191a1b1c1d1e1f", // 32-byte nonce
        b"AEGIS-256 via the enc verb",
    );
}

#[test]
fn enc_ascon_aead128_roundtrip() {
    enc_roundtrip(
        "ascon",
        "ASCON-AEAD128",
        "000102030405060708090a0b0c0d0e0f",
        "0f0e0d0c0b0a09080706050403020100", // 16-byte nonce
        b"Ascon-AEAD128 via the enc verb",
    );
}

#[test]
fn mac_gmac_roundtrip() {
    // GMAC (AES-128) is deterministic for a fixed (key, nonce, data); assert it
    // is stable and that changing the nonce changes the tag.
    let key = "00112233445566778899aabbccddeeff";
    let nonce = "0102030405060708090a0b0c";
    let (t1, ok) = run(
        &["mac", "-alg", "gmac", "-key", key, "-nonce", nonce],
        b"GMAC message",
    );
    assert!(ok);
    let tag = t1.trim();
    assert_eq!(tag.len(), 32); // 16-byte tag -> 32 hex chars
    assert!(tag.bytes().all(|b| b.is_ascii_hexdigit()));
    // Deterministic: same inputs -> same tag.
    let (t2, ok) = run(
        &["mac", "-alg", "gmac", "-key", key, "-nonce", nonce],
        b"GMAC message",
    );
    assert!(ok);
    assert_eq!(t1, t2);
    // Different nonce -> different tag.
    let (t3, ok) = run(
        &[
            "mac",
            "-alg",
            "gmac",
            "-key",
            key,
            "-nonce",
            "0c0b0a090807060504030201",
        ],
        b"GMAC message",
    );
    assert!(ok);
    assert_ne!(t1, t3);
}

#[test]
fn kdf_kbkdf_counter_deterministic() {
    // SP 800-108 counter mode is deterministic; assert stability and that the
    // label is bound into the derivation.
    let args = |label: &str| {
        vec![
            "kdf".to_string(),
            "kbkdf".to_string(),
            "-mode".to_string(),
            "counter".to_string(),
            "-prf".to_string(),
            "hmac-sha256".to_string(),
            "-ki".to_string(),
            "000102030405060708090a0b0c0d0e0f101112131415161718191a1b1c1d1e1f".to_string(),
            "-label".to_string(),
            label.to_string(),
            "-context".to_string(),
            "cafe".to_string(),
            "-len".to_string(),
            "32".to_string(),
        ]
    };
    fn to_refs(v: &[String]) -> Vec<&str> {
        v.iter().map(String::as_str).collect()
    }
    let a = args("6c6162656c");
    let (o1, ok) = run(&to_refs(&a), b"");
    assert!(ok);
    assert_eq!(o1.trim().len(), 64);
    let (o2, ok) = run(&to_refs(&a), b"");
    assert!(ok);
    assert_eq!(o1, o2);
    let b = args("6c6162656d"); // different label
    let (o3, ok) = run(&to_refs(&b), b"");
    assert!(ok);
    assert_ne!(o1, o3);
}

#[test]
fn hash_ascon_hash256_known_answer() {
    // Ascon-Hash256 of the empty message (NIST SP 800-232 reference value;
    // matches the `hash256_kat` Count-1 vector in src/ascon/hash.rs).
    let (out, ok) = run(&["hash", "ascon-hash256"], b"");
    assert!(ok);
    assert_eq!(
        out.trim(),
        "0b3be5850f2f6b98caf29f8fdea89b64a1fa70aa249b8f839bd53baa304d92b2"
    );
}

#[test]
fn sm2_genpkey_sign_verify_encrypt_decrypt() {
    let dir = std::env::temp_dir().join(format!("pc_sm2_{}", std::process::id()));
    std::fs::create_dir_all(&dir).unwrap();
    let p = |n: &str| dir.join(n).to_str().unwrap().to_string();

    // Generate an SM2 private key (SEC1 PEM).
    assert!(
        run(
            &["genpkey", "-algorithm", "SM2", "-out", &p("sm2.key")],
            b""
        )
        .1
    );
    let key = std::fs::read_to_string(dir.join("sm2.key")).unwrap();
    assert!(key.contains("BEGIN EC PRIVATE KEY"));

    std::fs::write(dir.join("msg.bin"), b"SM2 message over the pkeyutl verb").unwrap();

    // Sign (SM2-DSA, default id) with the private key.
    assert!(
        run(
            &[
                "pkeyutl",
                "sign",
                "-inkey",
                &p("sm2.key"),
                "-in",
                &p("msg.bin"),
                "-out",
                &p("sig.bin"),
            ],
            b"",
        )
        .1
    );
    let sig = std::fs::read(dir.join("sig.bin")).unwrap();
    assert!(!sig.is_empty() && sig[0] == 0x30); // DER Ecdsa-Sig-Value SEQUENCE

    // Verify: the pkeyutl verify path derives the SM2 public key from the SEC1
    // private key file (deriving the public point reveals no secret).
    let (vout, ok) = run(
        &[
            "pkeyutl",
            "verify",
            "-inkey",
            &p("sm2.key"),
            "-in",
            &p("msg.bin"),
            "-sigfile",
            &p("sig.bin"),
        ],
        b"",
    );
    assert!(ok, "SM2 verify failed: {vout}");
    assert!(vout.contains("verified"));

    // A tampered message must fail to verify.
    std::fs::write(dir.join("bad.bin"), b"SM2 message over the pkeyutl verB").unwrap();
    let (_, bad_ok) = run(
        &[
            "pkeyutl",
            "verify",
            "-inkey",
            &p("sm2.key"),
            "-in",
            &p("bad.bin"),
            "-sigfile",
            &p("sig.bin"),
        ],
        b"",
    );
    assert!(!bad_ok, "SM2 verify should reject a tampered message");

    // Encrypt to the SM2 public key (derived from the private key file) and
    // decrypt back.
    assert!(
        run(
            &[
                "pkeyutl",
                "encrypt",
                "-inkey",
                &p("sm2.key"),
                "-in",
                &p("msg.bin"),
                "-out",
                &p("ct.bin"),
            ],
            b"",
        )
        .1
    );
    assert!(
        run(
            &[
                "pkeyutl",
                "decrypt",
                "-inkey",
                &p("sm2.key"),
                "-in",
                &p("ct.bin"),
                "-out",
                &p("rt.bin"),
            ],
            b"",
        )
        .1
    );
    let rt = std::fs::read(dir.join("rt.bin")).unwrap();
    assert_eq!(rt, b"SM2 message over the pkeyutl verb");
    let _ = std::fs::remove_dir_all(&dir);
}

#[test]
fn lms_genpkey_sign_verify_advances_key() {
    let dir = std::env::temp_dir().join(format!("pc_lms_{}", std::process::id()));
    std::fs::create_dir_all(&dir).unwrap();
    let p = |n: &str| dir.join(n).to_str().unwrap().to_string();

    assert!(
        run(
            &[
                "genpkey",
                "-algorithm",
                "LMS-SHA256-H5",
                "-out",
                &p("lms.key")
            ],
            b"",
        )
        .1
    );
    let before = std::fs::read(dir.join("lms.key")).unwrap();
    std::fs::write(dir.join("msg.bin"), b"LMS stateful message").unwrap();

    assert!(
        run(
            &[
                "pkeyutl",
                "sign",
                "-inkey",
                &p("lms.key"),
                "-in",
                &p("msg.bin"),
                "-out",
                &p("sig.bin"),
            ],
            b"",
        )
        .1
    );
    // The stateful key file MUST have advanced (index incremented).
    let after = std::fs::read(dir.join("lms.key")).unwrap();
    assert_eq!(before.len(), after.len());
    assert_ne!(before, after, "LMS key file did not advance after signing");

    // Verify against the (now-advanced) key file: the public key is unchanged,
    // so verification still succeeds.
    let (out, ok) = run(
        &[
            "pkeyutl",
            "verify",
            "-inkey",
            &p("lms.key"),
            "-in",
            &p("msg.bin"),
            "-sigfile",
            &p("sig.bin"),
        ],
        b"",
    );
    assert!(ok, "LMS verify failed: {out}");
    assert!(out.contains("verified"));
    let _ = std::fs::remove_dir_all(&dir);
}

#[test]
fn xmss_genpkey_sign_verify_advances_key() {
    let dir = std::env::temp_dir().join(format!("pc_xmss_{}", std::process::id()));
    std::fs::create_dir_all(&dir).unwrap();
    let p = |n: &str| dir.join(n).to_str().unwrap().to_string();

    assert!(
        run(
            &[
                "genpkey",
                "-algorithm",
                "XMSS-SHA2_10_256",
                "-out",
                &p("xmss.key"),
            ],
            b"",
        )
        .1
    );
    let before = std::fs::read(dir.join("xmss.key")).unwrap();
    std::fs::write(dir.join("msg.bin"), b"XMSS stateful message").unwrap();

    assert!(
        run(
            &[
                "pkeyutl",
                "sign",
                "-inkey",
                &p("xmss.key"),
                "-in",
                &p("msg.bin"),
                "-out",
                &p("sig.bin"),
            ],
            b"",
        )
        .1
    );
    let after = std::fs::read(dir.join("xmss.key")).unwrap();
    assert_eq!(before.len(), after.len());
    assert_ne!(before, after, "XMSS key file did not advance after signing");

    let (out, ok) = run(
        &[
            "pkeyutl",
            "verify",
            "-inkey",
            &p("xmss.key"),
            "-in",
            &p("msg.bin"),
            "-sigfile",
            &p("sig.bin"),
        ],
        b"",
    );
    assert!(ok, "XMSS verify failed: {out}");
    assert!(out.contains("verified"));
    let _ = std::fs::remove_dir_all(&dir);
}
