//! Actor attestation primitives — `Attestor` trait, `Attestation` value
//! type, `verify`, and identity-rotation envelope (T-3.D.0, ADR 0010 +
//! ADR 0014).
//!
//! This crate owns **only** the trait and the canonical-bytes verifier.
//! OS-keychain backends (macOS Keychain / Linux Secret Service / Windows
//! DPAPI) are scaffolded behind `#[cfg(target_os = "...")]` modules with
//! `unimplemented!()` bodies that compile on every platform but panic at
//! runtime; v0 of cortex uses [`InMemoryAttestor`] in tests and the CLI
//! init flow (lane T-3.D.5/6 will wire up the real backends).
//!
//! ## Verify-side fail-closed contract (ADR 0010 §1b)
//!
//! [`verify`] **MUST** reject any preimage whose `schema_version` differs
//! from [`crate::canonical::SCHEMA_VERSION_ATTESTATION`]. There is no
//! "best effort" decode and no partial verification: an unknown version is
//! a hard error.
//!
//! ## Future flag: `--require-presence`
//!
//! ADR 0010 §5 specifies a `--require-presence` flag that asks the OS for
//! user-presence (Touch ID, etc.) before signing. That flag is plumbed in
//! the CLI lane (T-3.D.5 / T-3.D.6) — this trait deliberately does **not**
//! take a `require_presence: bool` parameter in v0 because the in-memory
//! attestor cannot honor it. When the keychain backends land, add a method
//! `sign_with_presence(&self, signing_input: &[u8]) -> Result<Signature, _>`
//! on the trait (default-impl returning the same bytes) so existing
//! callers keep compiling.
//
// TODO(T-3.D.5/6): plumb `--require-presence` through this trait once the
// OS keychain backends are functional.

use chrono::{DateTime, Utc};
use ed25519_dalek::{Signature, Signer, SigningKey, Verifier, VerifyingKey};
use serde::{Deserialize, Serialize};

use crate::canonical::{
    canonical_rotation_input, canonical_signing_input, AttestationPreimage,
    SCHEMA_VERSION_ATTESTATION,
};

/// Errors raised by [`verify`] and the rotation-envelope verifier.
///
/// Variants are deliberately distinct so audit code can log *why* a
/// signature failed (unknown schema vs key-id mismatch vs bad signature)
/// without having to parse a free-form string.
#[derive(Debug, thiserror::Error, Clone, PartialEq, Eq)]
pub enum VerifyError {
    /// Preimage carried a `schema_version` this build does not understand.
    /// Per ADR 0010 §1b this is a hard, fail-closed error.
    #[error("unknown attestation schema_version: found {found}, expected {expected}")]
    UnknownSchemaVersion {
        /// Version observed in the preimage.
        found: u16,
        /// Version this build understands.
        expected: u16,
    },
    /// The `key_id` in the preimage did not match the verifying public
    /// key's expected fingerprint. Defends against signature substitution
    /// across keys.
    #[error("key_id mismatch: preimage says {preimage}, verifier expected {expected}")]
    KeyIdMismatch {
        /// `key_id` declared in the preimage.
        preimage: String,
        /// `key_id` the verifier was configured with.
        expected: String,
    },
    /// Ed25519 signature verification failed (wrong key, wrong bytes,
    /// truncated signature, …). Catch-all for cryptographic failure.
    #[error("ed25519 signature verification failed")]
    BadSignature,
    /// Signature bytes could not be parsed as Ed25519 (e.g. wrong length).
    #[error("malformed signature bytes")]
    MalformedSignature,
}

/// Generates Ed25519 attestations over a canonical signing input.
///
/// Implementors MUST be `Send + Sync` so a single attestor instance can be
/// shared between request handlers and background workers without locking.
///
/// The trait is intentionally narrow: it does not own the canonical
/// encoder. Callers build an [`AttestationPreimage`], pipe it through
/// [`canonical_signing_input`], and pass the resulting bytes to [`Self::sign`].
/// This separation lets hardware-token backends sign opaque bytes without
/// having to understand the cortex schema.
pub trait Attestor: Send + Sync {
    /// Sign the canonical signing input bytes.
    ///
    /// Implementations MAY surface a presence prompt, hardware-token
    /// confirmation, etc. **Errors are intentionally not modeled here in
    /// v0** — the in-memory attestor cannot fail, and OS-keychain failures
    /// are surfaced as panics until the CLI lanes wire in a richer
    /// `Result` type. (TODO: T-3.D.5/6 — change return type to `Result<…>`.)
    fn sign(&self, signing_input: &[u8]) -> Signature;

    /// Stable fingerprint of the public verifying key. Embedded into every
    /// attestation preimage and matched on verify (see
    /// [`VerifyError::KeyIdMismatch`]).
    fn key_id(&self) -> &str;

    /// The verifying key for this attestor — exposed so test harnesses and
    /// `cortex audit verify` can reconstruct the public side without going
    /// through OS-specific lookup. OS-keychain backends MUST publish the
    /// public key alongside the private key (see ADR 0010 §3).
    fn verifying_key(&self) -> VerifyingKey;
}

/// Cryptographic proof that an event originated from the named principal.
///
/// Wire shape (ADR 0014 §"Signed preimage"). Note: the **`signature`**
/// field is the only part of an event payload **not** included in the
/// canonical preimage (a 64-byte signature cannot sign itself), but
/// `key_id` and `signed_at` **are** included so a captured signature
/// cannot be moved to a different timestamp or rebound to a different key.
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
pub struct Attestation {
    /// Public-key fingerprint (matches [`Attestor::key_id`]).
    pub key_id: String,
    /// Ed25519 signature over [`canonical_signing_input`].
    #[serde(with = "signature_serde")]
    pub signature: [u8; 64],
    /// Wall-clock timestamp the signature was produced at; MUST equal the
    /// `signed_at` field in the preimage that was signed.
    pub signed_at: DateTime<Utc>,
}

/// Identity-rotation envelope — proves the holder of `old_pubkey` (or the
/// recovery key) authorizes `new_pubkey` to take over (ADR 0010 §6).
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
pub struct RotationEnvelope {
    /// Schema version for the rotation envelope canonical bytes.
    /// Bumped independently of [`SCHEMA_VERSION_ATTESTATION`] when the
    /// rotation framing changes.
    pub schema_version: u16,
    /// Old public key bytes (32 bytes, raw — not multibase).
    pub old_pubkey: [u8; 32],
    /// New public key bytes (32 bytes, raw — not multibase).
    pub new_pubkey: [u8; 32],
    /// When the rotation was signed.
    pub signed_at: DateTime<Utc>,
    /// Ed25519 signature over [`canonical_rotation_input`] using the
    /// **old** signing key (or recovery key, where applicable).
    #[serde(with = "signature_serde")]
    pub signature: [u8; 64],
}

// -- impl ---------------------------------------------------------------

/// Sign an [`AttestationPreimage`] and produce a verifiable
/// [`Attestation`].
///
/// Convenience wrapper around [`canonical_signing_input`] +
/// [`Attestor::sign`]; takes the `signed_at` from the preimage so the two
/// sides cannot drift.
#[must_use]
pub fn attest(preimage: &AttestationPreimage, attestor: &dyn Attestor) -> Attestation {
    let bytes = canonical_signing_input(preimage);
    let sig = attestor.sign(&bytes);
    Attestation {
        key_id: preimage.key_id.clone(),
        signature: sig.to_bytes(),
        signed_at: preimage.signed_at,
    }
}

/// Verify an [`Attestation`] against the canonical preimage and a
/// verifying public key.
///
/// Fail-closed contract:
///
/// - `preimage.schema_version != SCHEMA_VERSION_ATTESTATION` →
///   [`VerifyError::UnknownSchemaVersion`].
/// - `preimage.key_id != expected_key_id` →
///   [`VerifyError::KeyIdMismatch`].
/// - signature does not verify under `public_key` over the canonical
///   bytes of `preimage` → [`VerifyError::BadSignature`].
///
/// **Note on `expected_key_id`**: callers pass the fingerprint they
/// believe `public_key` represents. This binds the public key to its
/// declared identity at the verify boundary; constructions that derive
/// `key_id` from `public_key` directly (e.g. `multibase(public_key)`)
/// can pass `&derived_key_id`.
pub fn verify(
    preimage: &AttestationPreimage,
    attestation: &Attestation,
    public_key: &VerifyingKey,
    expected_key_id: &str,
) -> Result<(), VerifyError> {
    // Fail-closed schema check FIRST (ADR 0010 §1b).
    if preimage.schema_version != SCHEMA_VERSION_ATTESTATION {
        return Err(VerifyError::UnknownSchemaVersion {
            found: preimage.schema_version,
            expected: SCHEMA_VERSION_ATTESTATION,
        });
    }

    // Match the preimage's declared key_id against the caller's expected
    // key_id. This stops a captured signature from being verified under a
    // different key whose owner happened to publish the same bytes.
    if preimage.key_id != expected_key_id || attestation.key_id != expected_key_id {
        return Err(VerifyError::KeyIdMismatch {
            preimage: preimage.key_id.clone(),
            expected: expected_key_id.to_string(),
        });
    }

    // signed_at must round-trip: the preimage encoder takes signed_at from
    // the preimage struct, so if they disagree the signed bytes will not
    // match. Belt-and-braces: also reject mismatched signed_at up front.
    if preimage.signed_at != attestation.signed_at {
        return Err(VerifyError::BadSignature);
    }

    let signing_input = canonical_signing_input(preimage);
    let sig = Signature::from_bytes(&attestation.signature);
    public_key
        .verify(&signing_input, &sig)
        .map_err(|_| VerifyError::BadSignature)
}

/// Sign an identity-rotation envelope using the **old** key (or recovery
/// key) — ADR 0010 §6.
#[must_use]
pub fn sign_rotation(
    old_pubkey: &VerifyingKey,
    new_pubkey: &VerifyingKey,
    signed_at: DateTime<Utc>,
    attestor: &dyn Attestor,
) -> RotationEnvelope {
    let old_bytes = old_pubkey.to_bytes();
    let new_bytes = new_pubkey.to_bytes();
    let bytes = canonical_rotation_input(
        SCHEMA_VERSION_ATTESTATION,
        &old_bytes,
        &new_bytes,
        signed_at,
    );
    let sig = attestor.sign(&bytes);
    RotationEnvelope {
        schema_version: SCHEMA_VERSION_ATTESTATION,
        old_pubkey: old_bytes,
        new_pubkey: new_bytes,
        signed_at,
        signature: sig.to_bytes(),
    }
}

/// Verify an identity-rotation envelope against the **old** verifying key.
/// Fails closed on unknown schema version.
pub fn verify_rotation(env: &RotationEnvelope) -> Result<(), VerifyError> {
    if env.schema_version != SCHEMA_VERSION_ATTESTATION {
        return Err(VerifyError::UnknownSchemaVersion {
            found: env.schema_version,
            expected: SCHEMA_VERSION_ATTESTATION,
        });
    }
    let old_pk =
        VerifyingKey::from_bytes(&env.old_pubkey).map_err(|_| VerifyError::MalformedSignature)?;
    let bytes = canonical_rotation_input(
        env.schema_version,
        &env.old_pubkey,
        &env.new_pubkey,
        env.signed_at,
    );
    let sig = Signature::from_bytes(&env.signature);
    old_pk
        .verify(&bytes, &sig)
        .map_err(|_| VerifyError::BadSignature)
}

// -- in-memory attestor (test + bootstrap use) ---------------------------

/// In-memory Ed25519 attestor. Used in tests, fixtures, and the bootstrap
/// path before the OS keychain is wired up.
///
/// **Not** suitable for production: the signing key is held in plain
/// process memory. Production paths use one of the OS-keychain backends
/// scaffolded below.
pub struct InMemoryAttestor {
    signing_key: SigningKey,
    key_id: String,
}

impl std::fmt::Debug for InMemoryAttestor {
    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
        f.debug_struct("InMemoryAttestor")
            .field("key_id", &self.key_id)
            // Deliberately omit the signing key.
            .finish_non_exhaustive()
    }
}

impl InMemoryAttestor {
    /// Wrap a pre-existing signing key with an explicit `key_id`.
    pub fn from_signing_key(signing_key: SigningKey, key_id: impl Into<String>) -> Self {
        Self {
            signing_key,
            key_id: key_id.into(),
        }
    }

    /// Build from a 32-byte secret seed. Useful for fixtures and tests
    /// where the keypair must be reproducible. `key_id` is derived as the
    /// lowercase hex of the public key (32 bytes → 64 hex chars).
    #[must_use]
    pub fn from_seed(seed: &[u8; 32]) -> Self {
        let signing_key = SigningKey::from_bytes(seed);
        let pk = signing_key.verifying_key();
        let key_id = hex_lower(&pk.to_bytes());
        Self {
            signing_key,
            key_id,
        }
    }
}

impl Attestor for InMemoryAttestor {
    fn sign(&self, signing_input: &[u8]) -> Signature {
        self.signing_key.sign(signing_input)
    }

    fn key_id(&self) -> &str {
        &self.key_id
    }

    fn verifying_key(&self) -> VerifyingKey {
        self.signing_key.verifying_key()
    }
}

/// Hex (lowercase, no separator) for fingerprint derivation. Local helper
/// — `cortex-core` does not pull in `hex` to keep the dep surface tight.
fn hex_lower(bytes: &[u8]) -> String {
    let mut s = String::with_capacity(bytes.len() * 2);
    for b in bytes {
        s.push_str(&format!("{b:02x}"));
    }
    s
}

// -- OS keychain skeletons (cfg-gated, unimplemented in v0) --------------

/// Marker trait for "this attestor records a future identity-rotation
/// commitment in addition to signing payloads." Implementors emit a
/// rotation envelope when a new key is provisioned. Implementations land
/// alongside the keychain backends.
pub trait IdentityRotation: Attestor {
    /// Sign a rotation envelope `(old → new)` using the **old** key
    /// material. See [`sign_rotation`] for the free-function form.
    fn sign_rotation(&self, new_pubkey: &VerifyingKey, signed_at: DateTime<Utc>) -> RotationEnvelope
    where
        Self: Sized,
    {
        sign_rotation(&self.verifying_key(), new_pubkey, signed_at, self)
    }
}

impl IdentityRotation for InMemoryAttestor {}

/// macOS Keychain backend skeleton. **Unimplemented in v0** — see ADR
/// 0010 §3 and the T-3.D.5 / T-3.D.6 follow-up lanes.
#[cfg(target_os = "macos")]
#[derive(Debug)]
pub struct KeychainAttestor {
    key_id: String,
}

#[cfg(target_os = "macos")]
impl KeychainAttestor {
    /// Look up the cortex-identity key by `key_id` in the macOS Keychain.
    /// **Unimplemented in v0** — panics on construction. TODO(T-3.D.5/6).
    pub fn open(_key_id: impl Into<String>) -> Self {
        unimplemented!(
            "KeychainAttestor (macOS) not implemented in v0; use InMemoryAttestor (T-3.D.5/6)"
        );
    }

    /// `key_id` getter (works without OS calls so the type can be
    /// constructed in tests behind a fixture).
    #[must_use]
    pub fn key_id(&self) -> &str {
        &self.key_id
    }
}

#[cfg(target_os = "macos")]
impl Attestor for KeychainAttestor {
    fn sign(&self, _signing_input: &[u8]) -> Signature {
        unimplemented!("KeychainAttestor::sign (macOS) — see T-3.D.5/6")
    }
    fn key_id(&self) -> &str {
        &self.key_id
    }
    fn verifying_key(&self) -> VerifyingKey {
        unimplemented!("KeychainAttestor::verifying_key (macOS) — see T-3.D.5/6")
    }
}

/// Linux Secret Service backend skeleton. **Unimplemented in v0**.
#[cfg(target_os = "linux")]
#[derive(Debug)]
pub struct KeychainAttestor {
    key_id: String,
}

#[cfg(target_os = "linux")]
impl KeychainAttestor {
    /// Look up the cortex-identity key via D-Bus Secret Service.
    /// **Unimplemented in v0**. TODO(T-3.D.5/6).
    pub fn open(_key_id: impl Into<String>) -> Self {
        unimplemented!(
            "KeychainAttestor (Linux) not implemented in v0; use InMemoryAttestor (T-3.D.5/6)"
        );
    }

    /// `key_id` getter.
    #[must_use]
    pub fn key_id(&self) -> &str {
        &self.key_id
    }
}

#[cfg(target_os = "linux")]
impl Attestor for KeychainAttestor {
    fn sign(&self, _signing_input: &[u8]) -> Signature {
        unimplemented!("KeychainAttestor::sign (Linux) — see T-3.D.5/6")
    }
    fn key_id(&self) -> &str {
        &self.key_id
    }
    fn verifying_key(&self) -> VerifyingKey {
        unimplemented!("KeychainAttestor::verifying_key (Linux) — see T-3.D.5/6")
    }
}

/// Windows DPAPI backend skeleton. **Unimplemented in v0**.
#[cfg(target_os = "windows")]
#[derive(Debug)]
pub struct KeychainAttestor {
    key_id: String,
}

#[cfg(target_os = "windows")]
impl KeychainAttestor {
    /// Look up the cortex-identity key via Windows DPAPI.
    /// **Unimplemented in v0**. TODO(T-3.D.5/6).
    pub fn open(_key_id: impl Into<String>) -> Self {
        unimplemented!(
            "KeychainAttestor (Windows) not implemented in v0; use InMemoryAttestor (T-3.D.5/6)"
        );
    }

    /// `key_id` getter.
    #[must_use]
    pub fn key_id(&self) -> &str {
        &self.key_id
    }
}

#[cfg(target_os = "windows")]
impl Attestor for KeychainAttestor {
    fn sign(&self, _signing_input: &[u8]) -> Signature {
        unimplemented!("KeychainAttestor::sign (Windows) — see T-3.D.5/6")
    }
    fn key_id(&self) -> &str {
        &self.key_id
    }
    fn verifying_key(&self) -> VerifyingKey {
        unimplemented!("KeychainAttestor::verifying_key (Windows) — see T-3.D.5/6")
    }
}

// -- serde adapter for [u8; 64] ------------------------------------------

mod signature_serde {
    use serde::{Deserialize, Deserializer, Serializer};

    pub fn serialize<S: Serializer>(bytes: &[u8; 64], s: S) -> Result<S::Ok, S::Error> {
        s.serialize_bytes(bytes)
    }

    pub fn deserialize<'de, D: Deserializer<'de>>(d: D) -> Result<[u8; 64], D::Error> {
        let v: Vec<u8> = Vec::deserialize(d)?;
        if v.len() != 64 {
            return Err(serde::de::Error::custom(format!(
                "expected 64 signature bytes, got {}",
                v.len()
            )));
        }
        let mut out = [0u8; 64];
        out.copy_from_slice(&v);
        Ok(out)
    }
}

#[cfg(test)]
mod tests {
    use super::*;
    use crate::canonical::{LineageBinding, SourceIdentity};
    use chrono::TimeZone;
    use ed25519_dalek::SigningKey;
    use std::sync::atomic::{AtomicU8, Ordering};

    /// Deterministic seed source so test failures are reproducible without
    /// pulling `rand` into the runtime dep graph. Each call advances a
    /// process-local counter so siblings get distinct keys within a run.
    static SEED_COUNTER: AtomicU8 = AtomicU8::new(1);
    fn fresh_attestor() -> InMemoryAttestor {
        let n = SEED_COUNTER.fetch_add(1, Ordering::Relaxed);
        let seed = [n; 32];
        InMemoryAttestor::from_seed(&seed)
    }

    fn fixture_preimage(attestor: &InMemoryAttestor) -> AttestationPreimage {
        AttestationPreimage {
            schema_version: SCHEMA_VERSION_ATTESTATION,
            source: SourceIdentity::User,
            event_id: "evt_01ARZ3NDEKTSV4RRFFQ69G5FAV".into(),
            payload_hash: "deadbeef".into(),
            session_id: "session-001".into(),
            ledger_id: "ledger-main".into(),
            lineage: LineageBinding::PreviousHash("aaaaaaaaaaaaaaaa".into()),
            signed_at: Utc.with_ymd_and_hms(2026, 5, 2, 12, 0, 0).unwrap(),
            key_id: attestor.key_id().to_string(),
        }
    }

    /// Acceptance #2 (LANES): preimage with `schema_version: 999` →
    /// `verify` returns `Err(VerifyError::UnknownSchemaVersion)`.
    #[test]
    fn unknown_schema_version_fails_closed() {
        let attestor = fresh_attestor();
        let mut p = fixture_preimage(&attestor);
        let att = attest(&p, &attestor);

        // Tamper schema version AFTER signing so the bytes we'd verify are
        // a "future" preimage shape.
        p.schema_version = 999;

        let result = verify(&p, &att, &attestor.verifying_key(), attestor.key_id());
        match result {
            Err(VerifyError::UnknownSchemaVersion {
                found: 999,
                expected: SCHEMA_VERSION_ATTESTATION,
            }) => {}
            other => panic!("expected UnknownSchemaVersion, got {other:?}"),
        }
    }

    /// Acceptance #3 (LANES): rule documented + verifier exercises it.
    /// Two preimages with logically-equal fields produce identical
    /// canonical bytes regardless of struct literal order, and a captured
    /// signature verifies under either expression of the preimage.
    #[test]
    fn field_reorder_does_not_change_signed_semantics() {
        let attestor = fresh_attestor();
        let p1 = fixture_preimage(&attestor);
        let att = attest(&p1, &attestor);

        // Construct the "same" preimage with the field expressions in a
        // different source order; the in-memory representation and
        // canonical bytes must match.
        let p2 = AttestationPreimage {
            key_id: attestor.key_id().to_string(),
            signed_at: chrono::Utc.with_ymd_and_hms(2026, 5, 2, 12, 0, 0).unwrap(),
            lineage: LineageBinding::PreviousHash("aaaaaaaaaaaaaaaa".into()),
            ledger_id: "ledger-main".into(),
            session_id: "session-001".into(),
            payload_hash: "deadbeef".into(),
            event_id: "evt_01ARZ3NDEKTSV4RRFFQ69G5FAV".into(),
            source: SourceIdentity::User,
            schema_version: SCHEMA_VERSION_ATTESTATION,
        };

        verify(&p2, &att, &attestor.verifying_key(), attestor.key_id())
            .expect("captured sig must verify under reordered preimage struct literal");
    }

    /// Acceptance #4 (LANES): tamper `previous_hash` → verify FAILS.
    #[test]
    fn wrong_prev_signature_fails() {
        let attestor = fresh_attestor();
        let p = fixture_preimage(&attestor);
        let att = attest(&p, &attestor);

        let mut tampered = p.clone();
        tampered.lineage = LineageBinding::PreviousHash("bbbbbbbbbbbbbbbb".into());

        let result = verify(
            &tampered,
            &att,
            &attestor.verifying_key(),
            attestor.key_id(),
        );
        assert_eq!(result, Err(VerifyError::BadSignature));
    }

    /// Acceptance #5 (LANES): replay a stale row's signature after the
    /// chain has advanced → FAILS.
    #[test]
    fn replay_stale_row_after_chain_advance_fails() {
        let attestor = fresh_attestor();

        let mut p_old = fixture_preimage(&attestor);
        p_old.lineage = LineageBinding::ChainPosition(10);
        let att = attest(&p_old, &attestor);

        // Verifier sees the same signature claimed under chain_position=20
        // (i.e. the row was lifted forward in time after the chain moved).
        let mut p_new = p_old.clone();
        p_new.lineage = LineageBinding::ChainPosition(20);

        let result = verify(&p_new, &att, &attestor.verifying_key(), attestor.key_id());
        assert_eq!(result, Err(VerifyError::BadSignature));
    }

    /// Acceptance #6 positive (LANES): rotation envelope verifies cleanly
    /// when signed by the holder of the old key.
    #[test]
    fn identity_rotate_accepted_when_envelope_verifies() {
        let old = fresh_attestor();
        let new = fresh_attestor();
        let signed_at = Utc.with_ymd_and_hms(2026, 5, 2, 12, 0, 0).unwrap();

        let env = sign_rotation(&old.verifying_key(), &new.verifying_key(), signed_at, &old);

        verify_rotation(&env).expect("envelope signed by old key must verify");
    }

    /// Acceptance #6 negative (LANES): tampered envelope (e.g. attacker
    /// substitutes their own `new_pubkey`) → FAILS.
    #[test]
    fn identity_rotate_tampered_envelope_fails() {
        let old = fresh_attestor();
        let new = fresh_attestor();
        let attacker_new = fresh_attestor();
        let signed_at = Utc.with_ymd_and_hms(2026, 5, 2, 12, 0, 0).unwrap();

        let mut env = sign_rotation(&old.verifying_key(), &new.verifying_key(), signed_at, &old);

        // Replace new_pubkey with the attacker's; signature no longer
        // verifies because the attacker doesn't hold `old`'s signing key.
        env.new_pubkey = attacker_new.verifying_key().to_bytes();

        assert_eq!(verify_rotation(&env), Err(VerifyError::BadSignature));
    }

    /// Acceptance #6 schema-fail-closed: unknown rotation envelope schema
    /// version → fails closed (no partial verify).
    #[test]
    fn rotation_envelope_unknown_schema_version_fails_closed() {
        let old = fresh_attestor();
        let new = fresh_attestor();
        let signed_at = Utc.with_ymd_and_hms(2026, 5, 2, 12, 0, 0).unwrap();
        let mut env = sign_rotation(&old.verifying_key(), &new.verifying_key(), signed_at, &old);
        env.schema_version = 999;
        match verify_rotation(&env) {
            Err(VerifyError::UnknownSchemaVersion {
                found: 999,
                expected: SCHEMA_VERSION_ATTESTATION,
            }) => {}
            other => panic!("expected UnknownSchemaVersion, got {other:?}"),
        }
    }

    /// Acceptance #7 (LANES): truncated / malformed preimage —
    /// represented here as a verify call where the signature bytes have
    /// been corrupted — returns Err with no partial success.
    #[test]
    fn malformed_payload_fails_closed() {
        let attestor = fresh_attestor();
        let p = fixture_preimage(&attestor);
        let mut att = attest(&p, &attestor);
        // Truncate-then-zero (signature is fixed 64 bytes; flipping bytes
        // simulates a malformed payload reaching the verifier).
        for byte in att.signature.iter_mut().take(8) {
            *byte = 0;
        }
        let result = verify(&p, &att, &attestor.verifying_key(), attestor.key_id());
        assert_eq!(result, Err(VerifyError::BadSignature));
    }

    /// Negative on key_id: the preimage's declared key_id does not match
    /// what the verifier expects → fails closed.
    #[test]
    fn key_id_mismatch_fails_closed() {
        let attestor = fresh_attestor();
        let p = fixture_preimage(&attestor);
        let att = attest(&p, &attestor);
        let result = verify(&p, &att, &attestor.verifying_key(), "fp:wrong-key");
        match result {
            Err(VerifyError::KeyIdMismatch { .. }) => {}
            other => panic!("expected KeyIdMismatch, got {other:?}"),
        }
    }

    /// Positive baseline: a freshly-signed preimage verifies cleanly.
    #[test]
    fn fresh_attestation_verifies() {
        let attestor = fresh_attestor();
        let p = fixture_preimage(&attestor);
        let att = attest(&p, &attestor);
        verify(&p, &att, &attestor.verifying_key(), attestor.key_id())
            .expect("freshly-signed attestation must verify");
    }

    /// Cross-source cannot replay: a signature for `User` does not verify
    /// under a `Tool` preimage even when other fields are identical.
    #[test]
    fn cross_source_replay_fails() {
        let attestor = fresh_attestor();
        let p = fixture_preimage(&attestor);
        let att = attest(&p, &attestor);

        let mut p2 = p.clone();
        p2.source = SourceIdentity::Tool {
            name: "user".into(),
        };
        let result = verify(&p2, &att, &attestor.verifying_key(), attestor.key_id());
        assert_eq!(result, Err(VerifyError::BadSignature));
    }

    /// Different signing key + same logical preimage → fails. Defends
    /// against signature lift across attestor identities.
    #[test]
    fn different_key_does_not_verify() {
        let a1 = fresh_attestor();
        let p = fixture_preimage(&a1);
        let att = attest(&p, &a1);

        // Verify with a different key entirely (still expects a1's key_id
        // because the preimage declares it; we override expected_key_id
        // to a1's so we exercise the cryptographic check, not the key_id
        // mismatch path).
        let other = (2u8..=u8::MAX)
            .map(|n| SigningKey::from_bytes(&[n; 32]).verifying_key())
            .find(|candidate| candidate != &a1.verifying_key())
            .expect("finite seed range must contain a distinct test key");
        assert_eq!(
            verify(&p, &att, &other, a1.key_id()),
            Err(VerifyError::BadSignature)
        );
    }
}
