//! Operator-managed trust-keyset verifying-keys file (SEC-25 Phase 2).
//!
//! W2 SEC-25 Phase 1 shipped the dataplane verifier
//! [`crate::verify_signed_trust_keyset_envelope`] which accepts a
//! `HashMap<String, ed25519_dalek::VerifyingKey>` keyring and an envelope. This
//! module is the **operator-side keyring loader** that turns the JSON file
//! described in `docs/trust-plane-runtime.md` § Signed keyset envelopes into
//! that map.
//!
//! Phase 2 wires this into the supervisor (see `cellos-supervisor::trust_keyset_load`)
//! behind `CELLOS_TRUST_VERIFY_KEYS_PATH`. Sibling consumers
//! (`cellos-trustd`, taudit, etc.) can also call into [`parse_trust_verify_keys`]
//! / [`load_trust_verify_keys_file`] directly to avoid re-implementing the
//! file format.
//!
//! ## File format
//!
//! Top-level JSON object whose keys are signer kids and whose values are the
//! base64url encoding of the raw 32-byte Ed25519 public key (no padding,
//! though padding is tolerated):
//!
//! ```json
//! {
//!   "ops-envelope-2026-q2": "kE3...base64url-32-bytes...",
//!   "ops-envelope-2026-q3": "vQp...base64url-32-bytes..."
//! }
//! ```
//!
//! Duplicate kids are rejected (JSON parsers vary in their dedup behavior;
//! `serde_json` collapses by default — we do not silently accept that).
//!
//! ## Symlink hardening
//!
//! [`load_trust_verify_keys_file`] opens the file with `O_NOFOLLOW` on Unix
//! (matching the SEC-15b protection applied to `CELLOS_POLICY_PACK_PATH` and
//! `CELLOS_AUTHORITY_KEYS_PATH`) so a swapped-in symlink at the final path
//! component cannot redirect verifying-key loading to an attacker-controlled
//! file.

use std::collections::HashMap;
use std::path::Path;

use base64::engine::general_purpose::URL_SAFE_NO_PAD;
use base64::Engine as _;
use ed25519_dalek::{Signature, SigningKey, VerifyingKey};
use serde::{Deserialize, Serialize};
use serde_json::Value;
use sha2::{Digest, Sha256};

use crate::error::CellosError;
use crate::types::CloudEventV1;

/// Parse the verifying-keys JSON document into a `kid → VerifyingKey` map.
///
/// The expected shape is a top-level JSON object (`{ "<kid>": "<base64url-pubkey>", ... }`).
/// Each value MUST decode under base64url to exactly 32 bytes (Ed25519 raw
/// public key length). Padding is tolerated to be friendly to publishers that
/// emit padded base64url.
///
/// # Errors
///
/// Returns [`CellosError::InvalidSpec`] when:
/// - the input is not valid JSON;
/// - the top-level value is not a JSON object;
/// - any value is not a string;
/// - any value fails base64url decode;
/// - any decoded value is not 32 bytes;
/// - the JSON parser surfaces a duplicate kid (defense in depth — `serde_json`
///   normally collapses duplicates).
///
/// An empty object is accepted (returns an empty map). The supervisor uses
/// that as the "no operator keyring configured" path: envelope verification
/// will then fail with `no signature verified` for any envelope whose signer
/// kid is not in the empty keyring, which is the intended behaviour.
pub fn parse_trust_verify_keys(raw: &str) -> Result<HashMap<String, VerifyingKey>, CellosError> {
    let value: Value = serde_json::from_str(raw).map_err(|e| {
        CellosError::InvalidSpec(format!("trust verify keys: JSON parse error: {e}"))
    })?;

    let object = value.as_object().ok_or_else(|| {
        CellosError::InvalidSpec(
            "trust verify keys: top-level value must be a JSON object mapping kid -> base64url-pubkey".into(),
        )
    })?;

    // Defence in depth against parser-side duplicate-kid collapse: the JSON
    // text is re-scanned to count each kid. `serde_json` collapses duplicate
    // keys silently in `to_value`, so we walk the raw text via a streaming
    // pass below before deferring to the parsed object for value extraction.
    detect_duplicate_keys(raw)?;

    let mut keys: HashMap<String, VerifyingKey> = HashMap::with_capacity(object.len());
    for (kid, value) in object {
        let pubkey_b64 = value.as_str().ok_or_else(|| {
            CellosError::InvalidSpec(format!(
                "trust verify keys: value for kid {kid:?} must be a base64url string, got {value}"
            ))
        })?;

        // Tolerate padded or unpadded base64url.
        let trimmed = pubkey_b64.trim_end_matches('=');
        let bytes = URL_SAFE_NO_PAD.decode(trimmed).map_err(|e| {
            CellosError::InvalidSpec(format!(
                "trust verify keys: kid {kid:?} value is not valid base64url: {e}"
            ))
        })?;

        let array: [u8; 32] = bytes.as_slice().try_into().map_err(|_| {
            CellosError::InvalidSpec(format!(
                "trust verify keys: kid {kid:?} decoded to {} bytes, expected 32",
                bytes.len()
            ))
        })?;

        let verifying_key = VerifyingKey::from_bytes(&array).map_err(|e| {
            CellosError::InvalidSpec(format!(
                "trust verify keys: kid {kid:?} is not a valid Ed25519 verifying key: {e}"
            ))
        })?;

        keys.insert(kid.clone(), verifying_key);
    }

    Ok(keys)
}

/// Read [`parse_trust_verify_keys`]'s input from a path and decode it.
///
/// On Unix this opens with `O_NOFOLLOW` (matching `CELLOS_POLICY_PACK_PATH` /
/// `CELLOS_AUTHORITY_KEYS_PATH` policy in `composition.rs` — SEC-15b) so the
/// final path component cannot be a symlink redirected at an
/// attacker-controlled file. On non-Unix [`std::fs::read_to_string`] is used
/// (Windows lacks an `O_NOFOLLOW` analogue in the std API).
///
/// # Errors
///
/// Returns [`CellosError::InvalidSpec`] when the file cannot be opened, read,
/// or decoded as UTF-8, plus every error class from [`parse_trust_verify_keys`].
pub fn load_trust_verify_keys_file(
    path: &Path,
) -> Result<HashMap<String, VerifyingKey>, CellosError> {
    #[cfg(unix)]
    let raw = {
        use std::io::Read;
        use std::os::unix::fs::OpenOptionsExt;
        let mut opts = std::fs::OpenOptions::new();
        opts.read(true);
        // O_NOFOLLOW value is platform-specific. cellos-core deliberately
        // avoids a `libc` dependency, so we hard-code the kernel ABI values
        // for the runtime targets we care about. Adding a new Unix variant
        // here is a one-line change, not a libc-crate refactor.
        //   - Linux: octal 0o400000 == 0x20000  (asm-generic/fcntl.h)
        //   - macOS / *BSD:           == 0x100  (sys/fcntl.h)
        // Using the wrong constant silently maps to a different flag (on
        // Linux 0x100 is `O_NOCTTY`, which would *not* refuse a symlink),
        // so this MUST stay accurate per platform.
        #[cfg(target_os = "linux")]
        const O_NOFOLLOW: i32 = 0x20000;
        #[cfg(any(
            target_os = "macos",
            target_os = "ios",
            target_os = "freebsd",
            target_os = "netbsd",
            target_os = "openbsd",
            target_os = "dragonfly",
        ))]
        const O_NOFOLLOW: i32 = 0x100;
        // Build break here on a new Unix is intentional: pick the right
        // constant from the platform's <fcntl.h> rather than guessing.
        #[cfg(not(any(
            target_os = "linux",
            target_os = "macos",
            target_os = "ios",
            target_os = "freebsd",
            target_os = "netbsd",
            target_os = "openbsd",
            target_os = "dragonfly",
        )))]
        compile_error!(
            "cellos-core::trust_keys: O_NOFOLLOW value not yet defined for this Unix target — \
             add the platform-specific value (see <fcntl.h>) before building."
        );
        opts.custom_flags(O_NOFOLLOW);
        let mut file = opts.open(path).map_err(|e| {
            CellosError::InvalidSpec(format!(
                "trust verify keys: cannot open {}: {e}",
                path.display()
            ))
        })?;
        let mut buf = String::new();
        file.read_to_string(&mut buf).map_err(|e| {
            CellosError::InvalidSpec(format!(
                "trust verify keys: cannot read {}: {e}",
                path.display()
            ))
        })?;
        buf
    };
    #[cfg(not(unix))]
    let raw = std::fs::read_to_string(path).map_err(|e| {
        CellosError::InvalidSpec(format!(
            "trust verify keys: cannot read {}: {e}",
            path.display()
        ))
    })?;

    parse_trust_verify_keys(&raw)
}

/// Single-pass duplicate-key detector for the top-level JSON object.
///
/// `serde_json`'s `Value` collapses duplicate object keys with last-write-wins
/// semantics. For a verifying-keys file that's a silent footgun: an attacker
/// who can inject a second copy of an existing kid with a different pubkey
/// would silently substitute the verifier's key. This walker scans the raw
/// JSON text for top-level object string keys and rejects the file if any
/// kid appears twice.
///
/// The walker is deliberately simple — it tracks string state and a single
/// nesting depth so it only counts keys at the outermost object — and does
/// not attempt to fully reparse JSON. It is robust against escaped quotes,
/// nested objects, arrays, and whitespace; if the structure is malformed in
/// a way the walker can't reason about, it falls through and lets
/// `serde_json::from_str` (called by the caller) surface the parse error.
fn detect_duplicate_keys(raw: &str) -> Result<(), CellosError> {
    use std::collections::HashSet;

    let bytes = raw.as_bytes();
    let mut seen: HashSet<String> = HashSet::new();
    let mut idx = 0;
    let mut depth: i32 = 0;
    let mut in_string = false;
    let mut after_colon_in_outer = false;
    let mut current_key: Option<String> = None;
    let mut escape = false;
    let mut started = false;

    while idx < bytes.len() {
        let b = bytes[idx];
        if in_string {
            if escape {
                escape = false;
                if let Some(k) = current_key.as_mut() {
                    k.push(b as char);
                }
                idx += 1;
                continue;
            }
            match b {
                b'\\' => {
                    escape = true;
                    if let Some(k) = current_key.as_mut() {
                        k.push(b as char);
                    }
                }
                b'"' => {
                    in_string = false;
                    if depth == 1 && !after_colon_in_outer {
                        if let Some(key) = current_key.take() {
                            if !seen.insert(key.clone()) {
                                return Err(CellosError::InvalidSpec(format!(
                                    "trust verify keys: duplicate kid {key:?} in keys file"
                                )));
                            }
                        }
                    } else {
                        // string was a value, not a key — discard.
                        let _ = current_key.take();
                    }
                }
                _ => {
                    if let Some(k) = current_key.as_mut() {
                        k.push(b as char);
                    }
                }
            }
            idx += 1;
            continue;
        }

        match b {
            b'"' => {
                in_string = true;
                // Only collect strings that could be top-level keys: depth==1
                // AND we are NOT after a colon (i.e. we expect a key here).
                if depth == 1 && !after_colon_in_outer {
                    current_key = Some(String::new());
                } else {
                    current_key = Some(String::new()); // placeholder so the
                                                       // closing quote branch
                                                       // discards uniformly.
                }
            }
            b'{' => {
                depth += 1;
                started = true;
            }
            b'}' => {
                depth -= 1;
                after_colon_in_outer = false;
                if depth == 0 {
                    return Ok(());
                }
            }
            b'[' => {
                depth += 1;
            }
            b']' => {
                depth -= 1;
            }
            b':' => {
                if depth == 1 {
                    after_colon_in_outer = true;
                }
            }
            b',' => {
                if depth == 1 {
                    after_colon_in_outer = false;
                }
            }
            _ => {}
        }
        idx += 1;
    }

    // Reached end of input without closing the outermost object: let
    // serde_json surface the structural error. Treat as no-duplicate-detected
    // here (the parse will fail later regardless).
    let _ = started;
    Ok(())
}

// ── I5: Per-event signing (HMAC-SHA256 / Ed25519) ───────────────────────────
//
// Extends the SEC-25 envelope-verification model down to individual
// CloudEvents so JetStream consumers / projectors can independently verify
// authorship of a single event without re-walking the keyset envelope.
//
// Doctrine: D1 — this is an OPT-IN signing path. Producers that don't sign
// emit raw `CloudEventV1` envelopes exactly as before; consumers that don't
// verify see no change.
//
// Algorithms:
//   - "ed25519": producer signs the canonical-JSON serialization with an
//     Ed25519 signing key; consumer verifies with the matching public key.
//   - "hmac-sha256": shared symmetric key; FIPS 198 HMAC over the canonical
//     JSON serialization. Implemented inline over `sha2::Sha256` so we
//     don't pull a new crate dependency.
//
// `notBefore` / `notAfter` mirror the trust-keyset envelope schema and are
// advisory — this primitive does not enforce them.

/// Per-event signed envelope wrapping a single CloudEvent.
#[derive(Debug, Clone, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct SignedEventEnvelopeV1 {
    pub event: CloudEventV1,
    pub signer_kid: String,
    pub algorithm: String,
    pub signature: String,
    #[serde(skip_serializing_if = "Option::is_none")]
    pub not_before: Option<String>,
    #[serde(skip_serializing_if = "Option::is_none")]
    pub not_after: Option<String>,
}

/// Canonical JSON form of a CloudEvent for signing/verification.
pub fn canonical_event_signing_payload(event: &CloudEventV1) -> Result<Vec<u8>, CellosError> {
    serde_json::to_vec(event).map_err(|e| {
        CellosError::InvalidSpec(format!("canonical_event_signing_payload: serialize: {e}"))
    })
}

/// Sign a CloudEvent with an Ed25519 signing key.
pub fn sign_event_ed25519(
    event: &CloudEventV1,
    signer_kid: &str,
    signing_key: &SigningKey,
) -> Result<SignedEventEnvelopeV1, CellosError> {
    use ed25519_dalek::Signer;
    let payload = canonical_event_signing_payload(event)?;
    let signature = signing_key.sign(&payload);
    Ok(SignedEventEnvelopeV1 {
        event: event.clone(),
        signer_kid: signer_kid.to_string(),
        algorithm: "ed25519".to_string(),
        signature: URL_SAFE_NO_PAD.encode(signature.to_bytes()),
        not_before: None,
        not_after: None,
    })
}

/// Sign a CloudEvent with HMAC-SHA256 (FIPS 198).
pub fn sign_event_hmac_sha256(
    event: &CloudEventV1,
    signer_kid: &str,
    key_bytes: &[u8],
) -> Result<SignedEventEnvelopeV1, CellosError> {
    let payload = canonical_event_signing_payload(event)?;
    let mac = hmac_sha256(key_bytes, &payload);
    Ok(SignedEventEnvelopeV1 {
        event: event.clone(),
        signer_kid: signer_kid.to_string(),
        algorithm: "hmac-sha256".to_string(),
        signature: URL_SAFE_NO_PAD.encode(mac),
        not_before: None,
        not_after: None,
    })
}

/// Verify a [`SignedEventEnvelopeV1`] against a verifier-side keyring.
pub fn verify_signed_event_envelope<'a>(
    envelope: &'a SignedEventEnvelopeV1,
    verifying_keys: &HashMap<String, VerifyingKey>,
    hmac_keys: &HashMap<String, Vec<u8>>,
) -> Result<&'a CloudEventV1, CellosError> {
    let payload = canonical_event_signing_payload(&envelope.event)?;
    let sig_b64 = envelope.signature.trim_end_matches('=');
    let sig_bytes = URL_SAFE_NO_PAD.decode(sig_b64).map_err(|e| {
        CellosError::InvalidSpec(format!(
            "signed event envelope: signature is not valid base64url: {e}"
        ))
    })?;

    match envelope.algorithm.as_str() {
        "ed25519" => {
            let verifying_key = verifying_keys.get(&envelope.signer_kid).ok_or_else(|| {
                CellosError::InvalidSpec(format!(
                    "signed event envelope: unknown ed25519 signer kid {:?}",
                    envelope.signer_kid
                ))
            })?;
            let sig_array: [u8; 64] = sig_bytes.as_slice().try_into().map_err(|_| {
                CellosError::InvalidSpec(format!(
                    "signed event envelope: ed25519 signature must be 64 bytes, got {}",
                    sig_bytes.len()
                ))
            })?;
            let signature = Signature::from_bytes(&sig_array);
            verifying_key
                .verify_strict(&payload, &signature)
                .map_err(|e| {
                    CellosError::InvalidSpec(format!(
                        "signed event envelope: ed25519 verify failed: {e}"
                    ))
                })?;
            Ok(&envelope.event)
        }
        "hmac-sha256" => {
            let key = hmac_keys.get(&envelope.signer_kid).ok_or_else(|| {
                CellosError::InvalidSpec(format!(
                    "signed event envelope: unknown hmac-sha256 signer kid {:?}",
                    envelope.signer_kid
                ))
            })?;
            if sig_bytes.len() != 32 {
                return Err(CellosError::InvalidSpec(format!(
                    "signed event envelope: hmac-sha256 mac must be 32 bytes, got {}",
                    sig_bytes.len()
                )));
            }
            let expected = hmac_sha256(key, &payload);
            if !constant_time_eq(&expected, &sig_bytes) {
                return Err(CellosError::InvalidSpec(
                    "signed event envelope: hmac-sha256 verify failed".into(),
                ));
            }
            Ok(&envelope.event)
        }
        other => Err(CellosError::InvalidSpec(format!(
            "signed event envelope: unknown algorithm {other:?} (expected ed25519 or hmac-sha256)"
        ))),
    }
}

/// HMAC-SHA256 (RFC 2104 / FIPS 198). Returns the 32-byte MAC.
fn hmac_sha256(key: &[u8], message: &[u8]) -> [u8; 32] {
    const BLOCK: usize = 64;
    let mut block_key = [0u8; BLOCK];
    if key.len() > BLOCK {
        let mut hasher = Sha256::new();
        hasher.update(key);
        let digest = hasher.finalize();
        block_key[..32].copy_from_slice(&digest);
    } else {
        block_key[..key.len()].copy_from_slice(key);
    }

    let mut ipad = [0u8; BLOCK];
    let mut opad = [0u8; BLOCK];
    for i in 0..BLOCK {
        ipad[i] = block_key[i] ^ 0x36;
        opad[i] = block_key[i] ^ 0x5c;
    }

    let mut inner = Sha256::new();
    inner.update(ipad);
    inner.update(message);
    let inner_digest = inner.finalize();

    let mut outer = Sha256::new();
    outer.update(opad);
    outer.update(inner_digest);
    let mac = outer.finalize();

    let mut out = [0u8; 32];
    out.copy_from_slice(&mac);
    out
}

/// Constant-time equality over byte slices of equal length.
fn constant_time_eq(a: &[u8], b: &[u8]) -> bool {
    if a.len() != b.len() {
        return false;
    }
    let mut diff: u8 = 0;
    for (x, y) in a.iter().zip(b.iter()) {
        diff |= x ^ y;
    }
    diff == 0
}

#[cfg(test)]
mod tests {
    use super::{load_trust_verify_keys_file, parse_trust_verify_keys};
    use base64::engine::general_purpose::URL_SAFE_NO_PAD;
    use base64::Engine as _;
    use ed25519_dalek::SigningKey;
    use std::io::Write;

    fn signing_key(seed: u8) -> SigningKey {
        SigningKey::from_bytes(&[seed; 32])
    }

    fn pubkey_b64(seed: u8) -> String {
        let signer = signing_key(seed);
        URL_SAFE_NO_PAD.encode(signer.verifying_key().to_bytes())
    }

    #[test]
    fn parses_well_formed_two_key_map() {
        let raw = format!(
            r#"{{ "ops-envelope-2026-q2": "{}", "ops-envelope-2026-q3": "{}" }}"#,
            pubkey_b64(7),
            pubkey_b64(11)
        );
        let keys = parse_trust_verify_keys(&raw).expect("well-formed map must parse");
        assert_eq!(keys.len(), 2);
        assert!(keys.contains_key("ops-envelope-2026-q2"));
        assert!(keys.contains_key("ops-envelope-2026-q3"));
        assert_eq!(
            keys["ops-envelope-2026-q2"],
            signing_key(7).verifying_key(),
            "kid q2 must round-trip to its source verifying key"
        );
    }

    #[test]
    fn rejects_duplicate_kid() {
        let raw = format!(
            r#"{{ "ops-envelope-2026-q2": "{}", "ops-envelope-2026-q2": "{}" }}"#,
            pubkey_b64(7),
            pubkey_b64(11)
        );
        let err = parse_trust_verify_keys(&raw).expect_err("duplicate kid must be rejected");
        let msg = format!("{err}");
        assert!(
            msg.contains("duplicate kid"),
            "expected duplicate-kid error, got: {msg}"
        );
    }

    #[test]
    fn rejects_malformed_base64() {
        let raw = r#"{ "ops-bad": "@@@not-base64@@@" }"#;
        let err = parse_trust_verify_keys(raw).expect_err("malformed base64 must be rejected");
        let msg = format!("{err}");
        assert!(
            msg.contains("not valid base64url"),
            "expected base64-decode error, got: {msg}"
        );
    }

    #[test]
    fn rejects_wrong_length_pubkey() {
        // 16 bytes of zeros, base64url-encoded — too short for an Ed25519 pubkey.
        let too_short = URL_SAFE_NO_PAD.encode([0u8; 16]);
        let raw = format!(r#"{{ "ops-short": "{too_short}" }}"#);
        let err = parse_trust_verify_keys(&raw).expect_err("16-byte pubkey must be rejected");
        let msg = format!("{err}");
        assert!(
            msg.contains("expected 32"),
            "expected 32-byte length error, got: {msg}"
        );
    }

    #[test]
    fn empty_object_is_accepted() {
        let raw = "{}";
        let keys = parse_trust_verify_keys(raw).expect("empty object is the no-keys case");
        assert!(keys.is_empty());
    }

    #[test]
    fn missing_file_errors() {
        let path = std::path::Path::new("/nonexistent/path/that/should/not/exist.json");
        let err =
            load_trust_verify_keys_file(path).expect_err("missing file must surface an error");
        let msg = format!("{err}");
        assert!(
            msg.contains("cannot") && msg.contains("nonexistent"),
            "expected file-open error, got: {msg}"
        );
    }

    #[test]
    fn rejects_non_utf8_input() {
        let dir = tempfile::tempdir().expect("tmpdir");
        let path = dir.path().join("trust-keys-non-utf8.json");
        let mut f = std::fs::File::create(&path).expect("create");
        // Bytes that are NOT valid UTF-8.
        f.write_all(&[0xFF, 0xFE, 0xFD, 0xFC]).expect("write");
        drop(f);
        let err = load_trust_verify_keys_file(&path).expect_err("non-utf8 must error");
        let msg = format!("{err}");
        // On Unix this surfaces from `read_to_string`'s utf8 check.
        assert!(
            msg.contains("cannot read") || msg.contains("utf-8") || msg.contains("UTF-8"),
            "expected non-utf8 read error, got: {msg}"
        );
    }

    #[test]
    fn rejects_top_level_non_object() {
        let raw = r#"["not", "an", "object"]"#;
        let err = parse_trust_verify_keys(raw).expect_err("top-level non-object must be rejected");
        let msg = format!("{err}");
        assert!(
            msg.contains("must be a JSON object"),
            "expected top-level-object error, got: {msg}"
        );
    }

    #[test]
    fn loads_valid_file_via_load_helper() {
        // Round-trip the file path: write a well-formed two-key map and load
        // it back via the on-disk helper, exercising the O_NOFOLLOW path on
        // Unix.
        let dir = tempfile::tempdir().expect("tmpdir");
        let path = dir.path().join("trust-keys.json");
        let raw = format!(
            r#"{{ "kid-active-7": "{}", "kid-active-11": "{}" }}"#,
            pubkey_b64(7),
            pubkey_b64(11)
        );
        std::fs::write(&path, raw).expect("write keys");
        let keys = load_trust_verify_keys_file(&path).expect("load via helper");
        assert_eq!(keys.len(), 2);
        assert_eq!(keys["kid-active-7"], signing_key(7).verifying_key());
    }

    /// Symlink rejection — proves O_NOFOLLOW is the right kernel flag on this
    /// platform. Without this test we silently shipped `O_NOCTTY` on Linux
    /// (0x100 is O_NOCTTY there; O_NOFOLLOW is 0x20000) and the loader would
    /// accept attacker-swappable symlinks. Pin the property so a future rename
    /// or constant edit can't regress without a failing test.
    #[cfg(unix)]
    #[test]
    fn load_helper_rejects_symlink_at_final_component() {
        let dir = tempfile::tempdir().expect("tmpdir");
        let real_path = dir.path().join("trust-keys-real.json");
        let symlink_path = dir.path().join("trust-keys-symlink.json");
        let raw = format!(r#"{{ "kid-only-1": "{}" }}"#, pubkey_b64(7));
        std::fs::write(&real_path, raw).expect("write real keys file");
        std::os::unix::fs::symlink(&real_path, &symlink_path).expect("create symlink");

        // Sanity: reading the real file works.
        load_trust_verify_keys_file(&real_path).expect("real path loads");

        // The symlink at the final component MUST be rejected by O_NOFOLLOW.
        let err = load_trust_verify_keys_file(&symlink_path)
            .expect_err("symlink at final component must be rejected");
        let msg = format!("{err}");
        assert!(
            msg.contains("cannot open"),
            "expected open-side rejection, got: {msg}"
        );
    }

    // ── I5: per-event signing primitives ───────────────────────────────────

    use super::{
        canonical_event_signing_payload, sign_event_ed25519, sign_event_hmac_sha256,
        verify_signed_event_envelope,
    };
    use crate::types::CloudEventV1;
    use std::collections::HashMap;

    fn sample_event() -> CloudEventV1 {
        CloudEventV1 {
            specversion: "1.0".into(),
            id: "ev-001".into(),
            source: "/cellos-supervisor".into(),
            ty: "dev.cellos.events.cell.lifecycle.v1.started".into(),
            datacontenttype: Some("application/json".into()),
            data: Some(serde_json::json!({"cellId": "test-cell-1"})),
            time: Some("2026-05-06T12:00:00Z".into()),
            traceparent: None,
        }
    }

    #[test]
    fn ed25519_round_trip_verifies() {
        let signer = signing_key(31);
        let event = sample_event();
        let envelope = sign_event_ed25519(&event, "ops-event-2026-q2", &signer).expect("sign ok");

        assert_eq!(envelope.algorithm, "ed25519");
        assert_eq!(envelope.signer_kid, "ops-event-2026-q2");

        let mut keys = HashMap::new();
        keys.insert("ops-event-2026-q2".to_string(), signer.verifying_key());
        let hmac_keys: HashMap<String, Vec<u8>> = HashMap::new();
        let verified =
            verify_signed_event_envelope(&envelope, &keys, &hmac_keys).expect("verify ok");
        assert_eq!(verified.id, event.id);
    }

    #[test]
    fn ed25519_tampered_event_fails_verify() {
        let signer = signing_key(31);
        let event = sample_event();
        let mut envelope =
            sign_event_ed25519(&event, "ops-event-2026-q2", &signer).expect("sign ok");
        envelope.event.id = "ev-tampered".into();

        let mut keys = HashMap::new();
        keys.insert("ops-event-2026-q2".to_string(), signer.verifying_key());
        let hmac_keys: HashMap<String, Vec<u8>> = HashMap::new();
        let err = verify_signed_event_envelope(&envelope, &keys, &hmac_keys)
            .expect_err("tampered event must fail verify");
        assert!(format!("{err}").contains("ed25519 verify failed"));
    }

    #[test]
    fn ed25519_unknown_kid_fails_verify() {
        let signer = signing_key(31);
        let event = sample_event();
        let envelope = sign_event_ed25519(&event, "ops-event-2026-q2", &signer).expect("sign ok");
        let keys: HashMap<String, _> = HashMap::new();
        let hmac_keys: HashMap<String, Vec<u8>> = HashMap::new();
        let err = verify_signed_event_envelope(&envelope, &keys, &hmac_keys)
            .expect_err("unknown kid must fail");
        assert!(format!("{err}").contains("unknown ed25519 signer kid"));
    }

    #[test]
    fn hmac_sha256_round_trip_verifies() {
        let key = b"super-secret-shared-symmetric-key";
        let event = sample_event();
        let envelope = sign_event_hmac_sha256(&event, "ops-hmac-2026-q2", key).expect("sign ok");
        assert_eq!(envelope.algorithm, "hmac-sha256");

        let verifying_keys: HashMap<String, _> = HashMap::new();
        let mut hmac_keys: HashMap<String, Vec<u8>> = HashMap::new();
        hmac_keys.insert("ops-hmac-2026-q2".to_string(), key.to_vec());
        let verified = verify_signed_event_envelope(&envelope, &verifying_keys, &hmac_keys)
            .expect("verify ok");
        assert_eq!(verified.id, event.id);
    }

    #[test]
    fn hmac_sha256_tampered_event_fails_verify() {
        let key = b"super-secret-shared-symmetric-key";
        let event = sample_event();
        let mut envelope =
            sign_event_hmac_sha256(&event, "ops-hmac-2026-q2", key).expect("sign ok");
        envelope.event.ty = "dev.cellos.events.cell.lifecycle.v1.destroyed".into();

        let verifying_keys: HashMap<String, _> = HashMap::new();
        let mut hmac_keys: HashMap<String, Vec<u8>> = HashMap::new();
        hmac_keys.insert("ops-hmac-2026-q2".to_string(), key.to_vec());
        let err = verify_signed_event_envelope(&envelope, &verifying_keys, &hmac_keys)
            .expect_err("tampered event must fail");
        assert!(format!("{err}").contains("hmac-sha256 verify failed"));
    }

    #[test]
    fn unknown_algorithm_rejected() {
        let signer = signing_key(31);
        let event = sample_event();
        let mut envelope =
            sign_event_ed25519(&event, "ops-event-2026-q2", &signer).expect("sign ok");
        envelope.algorithm = "rsa-pss-sha512".into();

        let verifying_keys: HashMap<String, _> = HashMap::new();
        let hmac_keys: HashMap<String, Vec<u8>> = HashMap::new();
        let err = verify_signed_event_envelope(&envelope, &verifying_keys, &hmac_keys)
            .expect_err("unknown algorithm must be rejected");
        assert!(format!("{err}").contains("unknown algorithm"));
    }

    #[test]
    fn canonical_payload_is_deterministic() {
        let event = sample_event();
        let a = canonical_event_signing_payload(&event).expect("a");
        let b = canonical_event_signing_payload(&event).expect("b");
        assert_eq!(a, b, "canonical signing payload must be byte-identical");
    }
}
