//! Agent capability tokens and scope controls (M10p2).
//!
//! A [`CapabilityToken`] grants an agent permission to read and/or write within
//! specific [scope classes](ScopeClass) for a bounded lifetime (TTL). Every
//! capability-gated read/write consults the [`CapabilityRegistry`] with a single
//! atomic `revoked_at_ns` load, so **revocation takes effect in under a second**
//! by construction — it never routes through the 60s sweeper.
//!
//! Capabilities and revocations are durable (`Tag::Capability` /
//! `Tag::CapabilityRevocation`) so an agent's rights — and the audit trail of
//! when they were revoked — survive restart.

use std::sync::atomic::{AtomicU64, Ordering};

use dashmap::DashMap;
use serde::{Deserialize, Serialize};

use super::scope::{CommunityId, SignalScope};
use crate::schema::TidalError;

/// A scope *class* a capability can grant rights over.
///
/// Class-level (not a concrete community id): "community write" grants writes to
/// any community the agent is otherwise eligible for. Mirrors the
/// [`SignalScope`] variants.
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, PartialOrd, Ord, Serialize, Deserialize)]
#[repr(u8)]
pub enum ScopeClass {
    /// The local profile.
    Local = 0,
    /// Community overlays.
    Community = 1,
    /// Agent sessions.
    Session = 2,
    /// Agent-attributed writes.
    Agent = 3,
}

impl ScopeClass {
    /// The scope class of a [`SignalScope`] value.
    #[must_use]
    pub const fn of(scope: SignalScope) -> Self {
        match scope {
            SignalScope::Local => Self::Local,
            SignalScope::Community(_) => Self::Community,
            SignalScope::Session => Self::Session,
            SignalScope::Agent => Self::Agent,
        }
    }
}

// `ScopeClass` hand-mirrors `SignalScope`'s variant taxonomy and discriminant
// ordering (the durable wire byte). These const assertions pin each
// `ScopeClass as u8` to the matching [`SignalScope::discriminant`] at *compile
// time*, so adding or reordering a scope on either side that breaks the
// alignment is a build error — not a silent drift caught (or missed) at runtime.
// `Community` uses the `NONE` sentinel because `discriminant` ignores the id.
const _: () = {
    assert!(ScopeClass::Local as u8 == SignalScope::Local.discriminant());
    assert!(
        ScopeClass::Community as u8 == SignalScope::Community(CommunityId::NONE).discriminant()
    );
    assert!(ScopeClass::Session as u8 == SignalScope::Session.discriminant());
    assert!(ScopeClass::Agent as u8 == SignalScope::Agent.discriminant());
};

/// A single scope permission within a capability token.
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
pub struct ScopePermission {
    /// The scope class this permission applies to.
    pub scope: ScopeClass,
    /// Whether reads are permitted.
    pub read: bool,
    /// Whether writes are permitted.
    pub write: bool,
}

impl ScopePermission {
    /// A read+write permission for a scope class.
    #[must_use]
    pub const fn read_write(scope: ScopeClass) -> Self {
        Self {
            scope,
            read: true,
            write: true,
        }
    }

    /// A read-only permission for a scope class.
    #[must_use]
    pub const fn read_only(scope: ScopeClass) -> Self {
        Self {
            scope,
            read: true,
            write: false,
        }
    }
}

/// Why a capability check was denied.
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum CapabilityDenial {
    /// No (non-revoked, non-expired) token grants the requested scope/action.
    InsufficientCapability,
    /// A matching token exists but has been revoked.
    Revoked,
    /// A matching token exists but has expired (past its TTL).
    Expired,
}

impl std::fmt::Display for CapabilityDenial {
    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
        let s = match self {
            Self::InsufficientCapability => "agent lacks a capability for the requested scope",
            Self::Revoked => "agent capability has been revoked",
            Self::Expired => "agent capability has expired",
        };
        f.write_str(s)
    }
}

/// A capability token granting an agent scoped rights for a bounded lifetime.
#[derive(Debug, Serialize, Deserialize)]
pub struct CapabilityToken {
    /// Stable token id (`"{agent}#{seq}"`).
    pub token_id: String,
    /// The agent this token was granted to.
    pub agent_id: String,
    /// The scope permissions granted.
    pub permissions: Vec<ScopePermission>,
    /// Wall-clock ns when the token was granted.
    pub created_at_ns: u64,
    /// Wall-clock ns when the token expires (`u64::MAX` = never).
    pub expires_at_ns: u64,
    /// Wall-clock ns when the token was revoked (`0` = not revoked). Read on the
    /// synchronous check path with `Ordering::Acquire`.
    #[serde(with = "crate::governance::serde_util::atomic_u64")]
    pub revoked_at_ns: AtomicU64,
}

impl Clone for CapabilityToken {
    fn clone(&self) -> Self {
        Self {
            token_id: self.token_id.clone(),
            agent_id: self.agent_id.clone(),
            permissions: self.permissions.clone(),
            created_at_ns: self.created_at_ns,
            expires_at_ns: self.expires_at_ns,
            revoked_at_ns: AtomicU64::new(self.revoked_at_ns.load(Ordering::Acquire)),
        }
    }
}

impl CapabilityToken {
    /// Whether this token has been revoked (`O(1)` atomic load).
    #[must_use]
    pub fn is_revoked(&self) -> bool {
        self.revoked_at_ns.load(Ordering::Acquire) != 0
    }

    /// Whether this token has expired by `now_ns`.
    #[must_use]
    pub const fn is_expired(&self, now_ns: u64) -> bool {
        now_ns >= self.expires_at_ns
    }

    /// Whether this token grants `action` (`write` when true, else `read`) on
    /// `scope`, ignoring revocation/expiry (checked separately).
    #[must_use]
    pub fn grants(&self, scope: ScopeClass, write: bool) -> bool {
        self.permissions
            .iter()
            .any(|p| p.scope == scope && (if write { p.write } else { p.read }))
    }

    /// Engage revocation at `now_ns` (idempotent: first non-zero store wins).
    pub fn revoke(&self, now_ns: u64) {
        let _ = self.revoked_at_ns.compare_exchange(
            0,
            now_ns.max(1),
            Ordering::AcqRel,
            Ordering::Acquire,
        );
    }

    /// Serialize to durable bytes.
    ///
    /// A serialize failure is propagated as [`TidalError::Internal`] rather than
    /// swallowed into empty bytes: an empty value would deserialize back as a
    /// *missing* token (silent loss of an agent's rights / its revocation audit
    /// trail), which is exactly the failure mode the durable capability log
    /// exists to prevent. In practice this struct always serializes cleanly, so
    /// the error path is an invariant guard, not an expected outcome.
    ///
    /// # Errors
    ///
    /// Returns [`TidalError::Internal`] if serialization fails.
    pub fn to_bytes(&self) -> crate::Result<Vec<u8>> {
        serde_json::to_vec(self)
            .map_err(|e| TidalError::internal("capability_token_to_bytes", e.to_string()))
    }

    /// Deserialize from durable bytes; malformed bytes return `None`.
    #[must_use]
    pub fn from_bytes(bytes: &[u8]) -> Option<Self> {
        serde_json::from_slice(bytes).ok()
    }
}

/// Default upper bound on live tokens held in a [`CapabilityRegistry`].
///
/// Capabilities are short-lived, agent-scoped grants; an unbounded in-memory map
/// would grow forever as agents are granted and revoked over a long-running
/// process. The cap turns that unbounded growth into a fixed ceiling. Sized
/// generously so a normal workload never evicts: 100k tokens is far more than
/// any realistic concurrent agent population, yet bounds memory to a few MB.
pub const DEFAULT_TOKEN_CAP: usize = 100_000;

/// In-memory registry of agent capability tokens.
///
/// Keyed by token id; an agent→tokens index lets the check path enumerate an
/// agent's tokens without a scan. The check path is `O(tokens-per-agent)` atomic
/// loads — no sweeper, no lock beyond `DashMap` sharding.
///
/// The registry is **bounded** at [`cap`](Self::cap) tokens. When a fresh
/// [`insert`](Self::insert) would exceed the cap, the registry first evicts an
/// already-dead token (revoked or expired) if one exists, else the
/// oldest-granted token — so live, in-force capabilities are never silently
/// dropped while any reclaimable slot remains. The durable
/// `Tag::Capability` / `Tag::CapabilityRevocation` log remains the source of
/// truth; eviction only trims the in-memory cache.
#[derive(Debug)]
pub struct CapabilityRegistry {
    tokens: DashMap<String, std::sync::Arc<CapabilityToken>>,
    by_agent: DashMap<String, Vec<String>>,
    grant_seq: AtomicU64,
    /// Maximum number of tokens retained in memory before eviction kicks in.
    cap: usize,
}

impl Default for CapabilityRegistry {
    fn default() -> Self {
        Self::new()
    }
}

impl CapabilityRegistry {
    /// Construct an empty registry with the [`DEFAULT_TOKEN_CAP`].
    #[must_use]
    pub fn new() -> Self {
        Self::with_capacity(DEFAULT_TOKEN_CAP)
    }

    /// Construct an empty registry with an explicit token cap.
    ///
    /// `cap` is clamped to at least 1 so the registry can always hold the token
    /// just inserted.
    #[must_use]
    pub fn with_capacity(cap: usize) -> Self {
        Self {
            tokens: DashMap::new(),
            by_agent: DashMap::new(),
            grant_seq: AtomicU64::new(0),
            cap: cap.max(1),
        }
    }

    /// The maximum number of tokens retained in memory.
    #[must_use]
    pub const fn cap(&self) -> usize {
        self.cap
    }

    /// Whether any capability has been granted.
    #[must_use]
    pub fn is_empty(&self) -> bool {
        self.tokens.is_empty()
    }

    /// Number of tokens currently retained in memory.
    #[must_use]
    pub fn len(&self) -> usize {
        self.tokens.len()
    }

    /// Mint a stable token id for `agent` (`"{agent}#{seq}"`); deterministic and
    /// collision-free without a random source.
    #[must_use]
    pub fn next_token_id(&self, agent: &str) -> String {
        let seq = self.grant_seq.fetch_add(1, Ordering::Relaxed);
        format!("{agent}#{seq}")
    }

    /// Register a granted token (in-memory).
    ///
    /// If the registry is at its [`cap`](Self::cap), one existing token is
    /// evicted first (see the type-level docs): a dead (revoked/expired) token
    /// is preferred, falling back to the oldest-granted, so a live capability is
    /// never dropped while a reclaimable one exists.
    pub fn insert(&self, token: CapabilityToken) {
        // Evict before insert so the post-insert size never exceeds `cap`.
        // `now_ns` is only used to classify expiry; a coarse wall clock is fine
        // because eviction is a cache-trim, not a security gate (revocation is
        // enforced on the check path via the atomic load, independent of this).
        if self.tokens.len() >= self.cap {
            let now_ns = crate::schema::Timestamp::now().as_nanos();
            self.evict_one(now_ns);
        }
        let token_id = token.token_id.clone();
        let agent = token.agent_id.clone();
        self.tokens
            .insert(token_id.clone(), std::sync::Arc::new(token));
        self.by_agent.entry(agent).or_default().push(token_id);
    }

    /// Evict a single token to make room: a dead (revoked or expired) token if
    /// any exists, otherwise the oldest-granted. Keeps the `by_agent` index
    /// consistent. A no-op if the registry is empty.
    ///
    /// One linear pass that only runs when the registry is at capacity, so its
    /// cost is amortized away over the cap. The victim id/agent are cloned at
    /// most once (only when a new best candidate is found), not per iteration.
    fn evict_one(&self, now_ns: u64) {
        // Victim selection state: a `(created_at_ns, token_id, agent_id, live)`
        // tuple for the best candidate seen so far. `live` records whether the
        // chosen victim is still in force, so the observability warn below needs
        // no second map lookup. A dead token short-circuits the scan because
        // dropping it costs no live capability.
        let mut victim: Option<(u64, String, String, bool)> = None;
        for entry in &self.tokens {
            let t = entry.value();
            if t.is_revoked() || t.is_expired(now_ns) {
                victim = Some((
                    t.created_at_ns,
                    t.token_id.clone(),
                    t.agent_id.clone(),
                    false,
                ));
                break;
            }
            // Keep the smallest (created_at_ns, token_id) — token_id breaks ties
            // deterministically. Only clone when this entry becomes the new best.
            let is_new_best = match &victim {
                Some((c, id, _, _)) => (t.created_at_ns, t.token_id.as_str()) < (*c, id.as_str()),
                None => true,
            };
            if is_new_best {
                victim = Some((
                    t.created_at_ns,
                    t.token_id.clone(),
                    t.agent_id.clone(),
                    true,
                ));
            }
        }
        let Some((victim_created_ns, victim_id, victim_agent, victim_live)) = victim else {
            return; // registry empty
        };
        // Observability: if the victim is still live (non-revoked, non-expired)
        // we are trimming a *valid* grant out of the in-memory cache. The check
        // path consults only the in-memory maps, so until the durable
        // `Tag::Capability` row is rehydrated (today only on the next `open()`)
        // a `check()` for this agent/scope will return a false denial. Surface
        // the condition so it is operable rather than silent.
        //
        // Lazy durable rehydration — having `check()` reload an evicted-but-durable
        // token from `Tag::Capability` before returning a denial — is deferred to
        // the M10 "Governance & Agent Rights" follow-up (see
        // `docs/planning/ROADMAP.md` M10, per `CODING_GUIDELINES.md` §12 "no TODO
        // without a tracking note"). It requires a storage-engine handle inside the
        // registry, which neither `check()` nor `evict_one()` hold today — both are
        // pure in-memory paths — so the wiring belongs where the registry is
        // constructed (`db/mod.rs`), not here. Until then this is the contract: a
        // live eviction is visible via the warn below, and the durable log + the
        // next `open()` remain the recovery mechanism (default cap is sized so a
        // realistic agent population never evicts a live grant at all).
        if victim_live {
            tracing::warn!(
                token_id = %victim_id,
                agent_id = %victim_agent,
                created_at_ns = victim_created_ns,
                cap = self.cap,
                "evicting a live capability token under cap pressure; \
                 check() will deny this grant in-memory until it is \
                 rehydrated from durable Tag::Capability on next open()"
            );
        }
        self.tokens.remove(&victim_id);
        if let Some(mut ids) = self.by_agent.get_mut(&victim_agent) {
            ids.retain(|id| id != &victim_id);
        }
        // Drop the now-empty agent index entry so it cannot leak either.
        self.by_agent
            .remove_if(&victim_agent, |_, ids| ids.is_empty());
    }

    /// Revoke a token by id at `now_ns`. Returns `true` if a token was found.
    pub fn revoke(&self, token_id: &str, now_ns: u64) -> bool {
        if let Some(t) = self.tokens.get(token_id) {
            t.revoke(now_ns);
            true
        } else {
            false
        }
    }

    /// Check whether `agent` may perform `action` (`write` true / read false) on
    /// `scope` at `now_ns`.
    ///
    /// Returns `Ok(())` if some live token grants it. On denial, returns the
    /// most-specific reason: if a matching token exists but is revoked/expired,
    /// that reason wins over `InsufficientCapability`.
    ///
    /// # Errors
    ///
    /// Returns a [`CapabilityDenial`] describing why the check failed.
    pub fn check(
        &self,
        agent: &str,
        scope: ScopeClass,
        write: bool,
        now_ns: u64,
    ) -> Result<(), CapabilityDenial> {
        let Some(token_ids) = self.by_agent.get(agent) else {
            return Err(CapabilityDenial::InsufficientCapability);
        };
        let mut saw_revoked = false;
        let mut saw_expired = false;
        for tid in token_ids.iter() {
            if let Some(t) = self.tokens.get(tid) {
                if !t.grants(scope, write) {
                    continue;
                }
                if t.is_revoked() {
                    saw_revoked = true;
                    continue;
                }
                if t.is_expired(now_ns) {
                    saw_expired = true;
                    continue;
                }
                return Ok(());
            }
        }
        if saw_revoked {
            Err(CapabilityDenial::Revoked)
        } else if saw_expired {
            Err(CapabilityDenial::Expired)
        } else {
            Err(CapabilityDenial::InsufficientCapability)
        }
    }

    /// A single token by id (clone), if it exists.
    #[must_use]
    pub fn token(&self, token_id: &str) -> Option<CapabilityToken> {
        self.tokens.get(token_id).map(|t| (**t).clone())
    }

    /// All tokens granted to an agent (clones; for inspection/tests).
    #[must_use]
    pub fn tokens_for(&self, agent: &str) -> Vec<CapabilityToken> {
        self.by_agent
            .get(agent)
            .map(|ids| {
                ids.iter()
                    .filter_map(|id| self.tokens.get(id).map(|t| (**t).clone()))
                    .collect()
            })
            .unwrap_or_default()
    }
}

#[cfg(test)]
#[allow(clippy::unwrap_used)]
mod tests {
    use super::*;

    fn token(agent: &str, id: &str, write_community: bool, expires_at_ns: u64) -> CapabilityToken {
        CapabilityToken {
            token_id: id.to_string(),
            agent_id: agent.to_string(),
            permissions: vec![ScopePermission {
                scope: ScopeClass::Community,
                read: true,
                write: write_community,
            }],
            created_at_ns: 0,
            expires_at_ns,
            revoked_at_ns: AtomicU64::new(0),
        }
    }

    #[test]
    fn grants_only_listed_scope_and_action() {
        let t = token("a", "a#0", true, u64::MAX);
        assert!(t.grants(ScopeClass::Community, true));
        assert!(t.grants(ScopeClass::Community, false));
        assert!(!t.grants(ScopeClass::Local, true));
    }

    #[test]
    fn check_allows_then_revocation_denies_immediately() {
        let reg = CapabilityRegistry::new();
        reg.insert(token("trusted", "trusted#0", true, u64::MAX));
        assert!(
            reg.check("trusted", ScopeClass::Community, true, 100)
                .is_ok()
        );

        // Revoke -> subsequent checks fail with Revoked (atomic, immediate).
        assert!(reg.revoke("trusted#0", 200));
        assert_eq!(
            reg.check("trusted", ScopeClass::Community, true, 300),
            Err(CapabilityDenial::Revoked)
        );
    }

    #[test]
    fn experimental_agent_without_write_is_denied() {
        let reg = CapabilityRegistry::new();
        // Experimental agent has read-only community capability.
        reg.insert(token("experimental", "experimental#0", false, u64::MAX));
        assert_eq!(
            reg.check("experimental", ScopeClass::Community, true, 100),
            Err(CapabilityDenial::InsufficientCapability)
        );
        // Read is allowed.
        assert!(
            reg.check("experimental", ScopeClass::Community, false, 100)
                .is_ok()
        );
    }

    #[test]
    fn unknown_agent_is_insufficient() {
        let reg = CapabilityRegistry::new();
        assert_eq!(
            reg.check("ghost", ScopeClass::Community, true, 100),
            Err(CapabilityDenial::InsufficientCapability)
        );
    }

    #[test]
    fn expiry_denies() {
        let reg = CapabilityRegistry::new();
        reg.insert(token("a", "a#0", true, 1000));
        assert!(reg.check("a", ScopeClass::Community, true, 999).is_ok());
        assert_eq!(
            reg.check("a", ScopeClass::Community, true, 1000),
            Err(CapabilityDenial::Expired)
        );
    }

    #[test]
    fn token_serde_roundtrip_preserves_revocation() {
        let t = token("a", "a#0", true, u64::MAX);
        t.revoke(555);
        let back = CapabilityToken::from_bytes(&t.to_bytes().unwrap()).unwrap();
        assert!(back.is_revoked());
        assert_eq!(back.token_id, "a#0");
    }

    #[test]
    fn token_id_is_unique_and_stable() {
        let reg = CapabilityRegistry::new();
        let a = reg.next_token_id("agent");
        let b = reg.next_token_id("agent");
        assert_ne!(a, b);
        assert!(a.starts_with("agent#"));
    }

    #[test]
    fn scope_class_of_signalscope() {
        assert_eq!(ScopeClass::of(SignalScope::Local), ScopeClass::Local);
        assert_eq!(
            ScopeClass::of(SignalScope::Community(CommunityId(1))),
            ScopeClass::Community
        );
    }

    #[test]
    fn scope_class_discriminants_match_signal_scope() {
        // Runtime twin of the module-level `const _` assertions: pin every
        // `ScopeClass as u8` to its `SignalScope::discriminant`. The const block
        // is the load-bearing guard (it fails the build); this test documents
        // the invariant and keeps it visible in the suite. Iterating every
        // `SignalScope` variant means a new variant forces an update here too.
        for scope in [
            SignalScope::Local,
            SignalScope::Community(CommunityId::NONE),
            SignalScope::Session,
            SignalScope::Agent,
        ] {
            assert_eq!(
                ScopeClass::of(scope) as u8,
                scope.discriminant(),
                "ScopeClass discriminant drifted from SignalScope for {scope:?}"
            );
        }
    }

    /// Build a token with an explicit `created_at_ns` for eviction-order tests.
    fn token_created(agent: &str, id: &str, created_at_ns: u64) -> CapabilityToken {
        let mut t = token(agent, id, true, u64::MAX);
        t.created_at_ns = created_at_ns;
        t
    }

    #[test]
    fn registry_is_bounded_and_never_exceeds_cap() {
        let reg = CapabilityRegistry::with_capacity(3);
        assert_eq!(reg.cap(), 3);
        for i in 0..10u64 {
            reg.insert(token_created("a", &format!("a#{i}"), i));
        }
        assert_eq!(reg.len(), 3, "registry must never exceed its cap");
    }

    #[test]
    fn eviction_drops_oldest_live_token_first() {
        let reg = CapabilityRegistry::with_capacity(2);
        reg.insert(token_created("a", "a#0", 10)); // oldest
        reg.insert(token_created("a", "a#1", 20));
        // Inserting a third (all live) evicts the oldest by created_at_ns.
        reg.insert(token_created("a", "a#2", 30));
        assert_eq!(reg.len(), 2);
        assert!(
            reg.token("a#0").is_none(),
            "oldest live token must be evicted"
        );
        assert!(reg.token("a#1").is_some());
        assert!(reg.token("a#2").is_some());
        // The agent index dropped the evicted id, not the survivors.
        let ids: Vec<String> = reg
            .tokens_for("a")
            .into_iter()
            .map(|t| t.token_id)
            .collect();
        assert!(!ids.contains(&"a#0".to_string()));
    }

    #[test]
    fn eviction_prefers_a_dead_token_over_a_live_one() {
        let reg = CapabilityRegistry::with_capacity(2);
        // a#0 is older but we revoke it; a#1 is younger and live.
        reg.insert(token_created("a", "a#0", 10));
        reg.insert(token_created("a", "a#1", 20));
        assert!(reg.revoke("a#0", 5)); // a#0 now dead
        // Inserting a third must reclaim the dead token, keeping both live ones.
        reg.insert(token_created("a", "a#2", 30));
        assert_eq!(reg.len(), 2);
        assert!(
            reg.token("a#0").is_none(),
            "dead token must be evicted first"
        );
        assert!(reg.token("a#1").is_some(), "live token must survive");
        assert!(reg.token("a#2").is_some());
    }

    #[test]
    fn live_eviction_keeps_registry_consistent_and_index_clean() {
        // When every resident token is live and the registry is at cap, the
        // oldest live token is the victim (the warned-about case). The registry
        // must stay internally consistent: the evicted id is gone from both the
        // token map and the agent index, and the survivors are intact.
        let reg = CapabilityRegistry::with_capacity(2);
        reg.insert(token_created("a", "a#0", 10)); // oldest live -> victim
        reg.insert(token_created("a", "a#1", 20));
        reg.insert(token_created("a", "a#2", 30)); // forces a live eviction

        assert_eq!(reg.len(), 2);
        assert!(reg.token("a#0").is_none(), "oldest live token is evicted");
        // The agent index no longer references the evicted id...
        let ids: Vec<String> = reg
            .tokens_for("a")
            .into_iter()
            .map(|t| t.token_id)
            .collect();
        assert!(!ids.contains(&"a#0".to_string()));
        assert!(ids.contains(&"a#1".to_string()));
        assert!(ids.contains(&"a#2".to_string()));
        // ...and check() now denies the evicted-but-still-valid grant purely
        // from the in-memory maps — the false-denial condition the warn flags.
        // (a#0 had no successor token granting Community, so this is a clean
        // InsufficientCapability, not Revoked/Expired.)
        let reg2 = CapabilityRegistry::with_capacity(1);
        reg2.insert(token_created("solo", "solo#0", 10));
        assert!(
            reg2.check("solo", ScopeClass::Community, true, 100).is_ok(),
            "live grant is honored while resident"
        );
        reg2.insert(token_created("other", "other#0", 20)); // evicts solo#0 (live)
        assert_eq!(
            reg2.check("solo", ScopeClass::Community, true, 100),
            Err(CapabilityDenial::InsufficientCapability),
            "evicted live grant is denied in-memory until durable rehydrate"
        );
    }

    #[test]
    fn token_to_bytes_is_fallible_and_succeeds_for_valid_token() {
        let t = token("a", "a#0", true, u64::MAX);
        let bytes = t.to_bytes().expect("a valid token always serializes");
        assert!(!bytes.is_empty());
    }
}
