// Copyright 2026 Exochain Foundation
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at:
//
//     https://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
//
// SPDX-License-Identifier: Apache-2.0

//! Foundational 0dentity types.
//!
//! All fractional values use **basis points** (0–10_000 = 0%–100.00%) so that
//! the workspace's `float_arithmetic = "deny"` lint is satisfied and all
//! computations remain fully deterministic across platforms.
//!
//! Spec reference: §2 (Foundational Types).

use std::{collections::BTreeMap, fmt};

pub use exo_core::types::Signature;
use exo_core::types::{Did, Hash256, PublicKey};
use serde::{
    Deserialize, Deserializer, Serialize, Serializer, de::Error as SerdeDeError,
    ser::Error as SerdeSerError,
};
use zeroize::{Zeroize, Zeroizing};

// ---------------------------------------------------------------------------
// ClaimType
// ---------------------------------------------------------------------------

/// The universe of claim types recognised by 0dentity.
///
/// Each variant maps to one or more axes on the polar graph.
/// Spec §2.2.
#[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord, Hash, Serialize, Deserialize)]
pub enum ClaimType {
    // --- Explicit claims (user-provided) ---
    DisplayName,
    Email,
    Phone,
    GovernmentId,
    BiometricLiveness,
    ProfessionalCredential { provider: String },

    // --- Implicit claims (system-observed) ---
    DeviceFingerprint,
    BehavioralSignature,
    GeographicConsistency,
    SessionContinuity,

    // --- Network claims (peer-generated) ---
    PeerAttestation { attester_did: Did },
    DelegationGrant { delegator_did: Did },
    SybilChallengeResolution { challenge_id: String },

    // --- Governance claims (protocol-generated) ---
    GovernanceVote { proposal_hash: Hash256 },
    ProposalAuthored { proposal_hash: Hash256 },
    ValidatorService { round_range: (u64, u64) },

    // --- Cryptographic claims (key-management) ---
    KeyRotation { old_key_hash: Hash256 },
    EntropyAttestation,
}

// ---------------------------------------------------------------------------
// ClaimStatus
// ---------------------------------------------------------------------------

/// Verification status of an identity claim.
#[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord, Hash, Serialize, Deserialize)]
pub enum ClaimStatus {
    /// Claim made but not yet verified.
    Pending,
    /// Claim independently verified (e.g., OTP confirmed).
    Verified,
    /// Claim expired and needs renewal.
    Expired,
    /// Claim revoked by subject or authority.
    Revoked,
    /// Claim challenged and under review.
    Challenged,
}

// ---------------------------------------------------------------------------
// IdentityClaim
// ---------------------------------------------------------------------------

/// A single atomic claim made by an identity.
///
/// Claims are content-addressed via BLAKE3. The `claim_hash` field is the
/// authoritative identifier; `dag_node_hash` links each claim into the
/// append-only DAG.
/// Spec §2.2.
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
pub struct IdentityClaim {
    /// Content-addressed hash of the claim payload (BLAKE3).
    pub claim_hash: Hash256,
    /// The DID of the identity making this claim.
    pub subject_did: Did,
    /// What kind of claim this is.
    pub claim_type: ClaimType,
    /// Verification status of this claim.
    pub status: ClaimStatus,
    /// When the claim was first made (epoch ms).
    pub created_ms: u64,
    /// When the claim was last verified (epoch ms), if ever.
    pub verified_ms: Option<u64>,
    /// When this claim expires and must be renewed (epoch ms).
    /// `None` = does not expire.
    pub expires_ms: Option<u64>,
    /// Signature of the subject over the claim payload.
    pub signature: Signature,
    /// Hash of the DAG node where this claim is recorded.
    pub dag_node_hash: Hash256,
}

// ---------------------------------------------------------------------------
// PolarAxes
// ---------------------------------------------------------------------------

/// The 8 axes of the 0dentity polar graph.
///
/// All values are in **basis points** (0–10_000 = 0%–100.00%) to satisfy the
/// workspace's no-floating-point determinism requirement.
/// Spec §2.2, §5.
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
pub struct PolarAxes {
    /// Verified reachability: email + phone channels (bp).
    pub communication: u32,
    /// KYC depth: government ID, biometric liveness (bp).
    pub credential_depth: u32,
    /// Fingerprint consistency: device binding stability (bp).
    pub device_trust: u32,
    /// Typing cadence, interaction patterns, session rhythm (bp).
    pub behavioral_signature: u32,
    /// Peer attestations, vouches, delegation history (bp).
    pub network_reputation: u32,
    /// Account age, verification freshness, claim renewal (bp).
    pub temporal_stability: u32,
    /// Key algorithm, entropy, rotation hygiene (bp).
    pub cryptographic_strength: u32,
    /// Governance participation, challenge record (bp).
    pub constitutional_standing: u32,
}

impl PolarAxes {
    /// Return all 8 axis values as an array (in basis points).
    #[must_use]
    pub fn as_array(&self) -> [u32; 8] {
        [
            self.communication,
            self.credential_depth,
            self.device_trust,
            self.behavioral_signature,
            self.network_reputation,
            self.temporal_stability,
            self.cryptographic_strength,
            self.constitutional_standing,
        ]
    }
}

// ---------------------------------------------------------------------------
// ZerodentityScore
// ---------------------------------------------------------------------------

/// Full 0dentity polar-decomposition score for a DID.
///
/// `composite` and `symmetry` are in basis points (0–10_000).
/// Spec §2.2, §5.
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
pub struct ZerodentityScore {
    /// The DID this score belongs to.
    pub subject_did: Did,
    /// Per-axis scores (each in basis points, 0–10_000).
    pub axes: PolarAxes,
    /// Composite score: unweighted mean of all axes (basis points).
    pub composite: u32,
    /// When this score was last computed (epoch ms).
    pub computed_ms: u64,
    /// Hash of the claim DAG state at computation time.
    pub dag_state_hash: Hash256,
    /// Number of verified claims contributing to this score.
    pub claim_count: u32,
    /// Shape symmetry index (0–10_000 bp; 10_000 = perfect octagon).
    pub symmetry: u32,
}

// ---------------------------------------------------------------------------
// DeviceFingerprint
// ---------------------------------------------------------------------------

/// A single device fingerprint composite.
///
/// `consistency_score_bp` is in basis points (0–10_000), or `None` on first
/// capture. Only composite hashes are ever persisted — raw signals are
/// hashed client-side and never transmitted.
/// Spec §2.2.
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
pub struct DeviceFingerprint {
    /// BLAKE3 of all signal hashes concatenated in sorted-key order.
    pub composite_hash: Hash256,
    /// Individual signal hashes. Key = signal type, Value = BLAKE3 of signal.
    ///
    /// Note: `FingerprintSignal` is defined later in this file; the field
    /// uses `BTreeMap` so iteration order is deterministic.
    pub signal_hashes: BTreeMap<FingerprintSignal, Hash256>,
    /// When this fingerprint was captured (epoch ms).
    pub captured_ms: u64,
    /// Similarity vs. previous fingerprint (bp, 0–10_000). `None` on first capture.
    pub consistency_score_bp: Option<u32>,
}

// ---------------------------------------------------------------------------
// BehavioralSignalType / BehavioralSample
// ---------------------------------------------------------------------------

/// Type of behavioural biometric signal.
/// Spec §2.2, §3.5.
#[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord, Hash, Serialize, Deserialize)]
pub enum BehavioralSignalType {
    KeystrokeDynamics,
    MouseDynamics,
    TouchDynamics,
    ScrollBehavior,
    FormNavigationCadence,
}

/// A single behavioural biometric sample.
///
/// `baseline_similarity_bp` is in basis points (0–10_000), or `None` if no
/// baseline exists yet.
/// Spec §2.2.
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
pub struct BehavioralSample {
    /// BLAKE3 hash of the raw sample data (never stored in raw form).
    pub sample_hash: Hash256,
    /// What kind of behavioural signal this is.
    pub signal_type: BehavioralSignalType,
    /// When captured (epoch ms).
    pub captured_ms: u64,
    /// Similarity to established baseline (bp, 0–10_000). `None` if no baseline.
    pub baseline_similarity_bp: Option<u32>,
}

// ---------------------------------------------------------------------------
// OtpChannel / OtpState / OtpChallenge
// ---------------------------------------------------------------------------

/// Communication channel used to deliver an OTP code.
/// Spec §2.2.
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
pub enum OtpChannel {
    Email,
    Sms,
}

/// Lifecycle state of an OTP challenge.
/// Spec §2.2.
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
pub enum OtpState {
    /// Dispatched, awaiting user input.
    Pending,
    /// User entered correct code.
    Verified,
    /// TTL expired without verification.
    Expired,
    /// Too many failed attempts.
    LockedOut,
}

/// Zeroizing wrapper for the OTP HMAC secret.
///
/// The secret must remain available while a challenge is pending so the server
/// can verify the submitted OTP code. It must not be exposed through debug
/// output or left in memory after the challenge value is dropped.
#[derive(Clone, PartialEq, Eq)]
pub struct OtpHmacSecret {
    bytes: Zeroizing<[u8; 32]>,
}

impl OtpHmacSecret {
    /// Wrap non-zero HMAC secret material and wipe the caller-owned stack copy.
    pub(crate) fn new(mut bytes: [u8; 32]) -> Option<Self> {
        let secret = Zeroizing::new(bytes);
        bytes.zeroize();
        Self::from_zeroizing(secret)
    }

    /// Wrap already-zeroizing HMAC secret material without creating another
    /// plain byte-array copy.
    pub(crate) fn from_zeroizing(mut bytes: Zeroizing<[u8; 32]>) -> Option<Self> {
        if bytes.iter().all(|byte| *byte == 0) {
            bytes.zeroize();
            return None;
        }

        Some(Self { bytes })
    }

    pub(crate) fn expose_secret(&self) -> &[u8; 32] {
        &self.bytes
    }
}

impl fmt::Debug for OtpHmacSecret {
    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
        f.write_str("OtpHmacSecret(<redacted>)")
    }
}

impl Serialize for OtpHmacSecret {
    fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
    where
        S: Serializer,
    {
        let _ = serializer;
        Err(S::Error::custom("OTP HMAC secret must not be serialized"))
    }
}

impl<'de> Deserialize<'de> for OtpHmacSecret {
    fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
    where
        D: Deserializer<'de>,
    {
        let bytes = <[u8; 32]>::deserialize(deserializer)?;
        Self::new(bytes).ok_or_else(|| {
            D::Error::custom("OTP HMAC secret must not be all zero when deserializing challenge")
        })
    }
}

/// OTP challenge — state machine for one verification attempt.
///
/// The 32-byte HMAC secret is stored so the server can verify the user's code
/// without storing the code itself.
/// Spec §2.2, §4.3–4.6.
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
pub struct OtpChallenge {
    /// Unique challenge identifier (hex of BLAKE3 of creation inputs).
    pub challenge_id: String,
    /// The DID being verified.
    pub subject_did: Did,
    /// Delivery channel.
    pub channel: OtpChannel,
    /// HMAC-SHA256 secret (32 bytes). Used to verify the presented code.
    pub hmac_secret: OtpHmacSecret,
    /// When the OTP was dispatched (epoch ms).
    pub dispatched_ms: u64,
    /// TTL in ms. Email: 300_000 (5 min). SMS: 180_000 (3 min).
    pub ttl_ms: u64,
    /// Number of verification attempts made.
    pub attempts: u32,
    /// Maximum allowed attempts before lockout.
    pub max_attempts: u32,
    /// Lifecycle state.
    pub state: OtpState,
}

// ---------------------------------------------------------------------------
// FingerprintSignal
// ---------------------------------------------------------------------------

/// The 15 client-side device signals used in fingerprinting.
///
/// Only their BLAKE3 hashes are transmitted — raw values are never sent.
/// Spec §3.1.
#[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord, Hash, Serialize, Deserialize)]
pub enum FingerprintSignal {
    AudioContext,
    BatteryStatus,
    CanvasRendering,
    ColorDepthDPR,
    DeviceMemory,
    DoNotTrack,
    FontEnumeration,
    HardwareConcurrency,
    Platform,
    ScreenGeometry,
    TimezoneLocale,
    TouchSupport,
    UserAgent,
    WebGLParameters,
    WebRTCLocalIPs,
}

impl fmt::Display for FingerprintSignal {
    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
        let s = match self {
            Self::AudioContext => "AudioContext",
            Self::BatteryStatus => "BatteryStatus",
            Self::CanvasRendering => "CanvasRendering",
            Self::ColorDepthDPR => "ColorDepthDPR",
            Self::DeviceMemory => "DeviceMemory",
            Self::DoNotTrack => "DoNotTrack",
            Self::FontEnumeration => "FontEnumeration",
            Self::HardwareConcurrency => "HardwareConcurrency",
            Self::Platform => "Platform",
            Self::ScreenGeometry => "ScreenGeometry",
            Self::TimezoneLocale => "TimezoneLocale",
            Self::TouchSupport => "TouchSupport",
            Self::UserAgent => "UserAgent",
            Self::WebGLParameters => "WebGLParameters",
            Self::WebRTCLocalIPs => "WebRTCLocalIPs",
        };
        f.write_str(s)
    }
}

// ---------------------------------------------------------------------------
// AttestationType / PeerAttestation
// ---------------------------------------------------------------------------

/// The kind of peer attestation being made.
/// Spec §7.2.
#[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord, Hash, Serialize, Deserialize)]
pub enum AttestationType {
    /// Attesting that the target DID is a real, unique person.
    Identity,
    /// Attesting that the target is a trustworthy participant.
    Trustworthy,
    /// Attesting the target has specific professional credentials.
    Professional,
    /// General character vouching.
    Character,
}

impl fmt::Display for AttestationType {
    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
        let s = match self {
            Self::Identity => "Identity",
            Self::Trustworthy => "Trustworthy",
            Self::Professional => "Professional",
            Self::Character => "Character",
        };
        f.write_str(s)
    }
}

impl std::str::FromStr for AttestationType {
    type Err = ();
    fn from_str(s: &str) -> Result<Self, Self::Err> {
        match s {
            "Identity" => Ok(Self::Identity),
            "Trustworthy" => Ok(Self::Trustworthy),
            "Professional" => Ok(Self::Professional),
            "Character" => Ok(Self::Character),
            _ => Err(()),
        }
    }
}

/// A peer attestation record — created when one DID vouches for another.
/// Spec §7.2.
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
pub struct PeerAttestation {
    /// Deterministic BLAKE3 identifier for this signed attestation.
    pub attestation_id: String,
    /// The DID making the attestation.
    pub attester_did: Did,
    /// The DID being attested.
    pub target_did: Did,
    /// Kind of attestation.
    pub attestation_type: AttestationType,
    /// Optional message hash (e.g., signed statement).
    pub message_hash: Option<Hash256>,
    /// When this attestation was created (epoch ms).
    pub created_ms: u64,
    /// Ed25519 public key used to verify the attester's signature.
    pub attester_public_key: PublicKey,
    /// Attester signature over the canonical attestation payload.
    pub signature: Signature,
    /// DAG node hash for this attestation.
    pub dag_node_hash: Hash256,
}

// ---------------------------------------------------------------------------
// IdentitySession
// ---------------------------------------------------------------------------

/// Absolute 0dentity bearer-session lifetime: 24 hours.
pub const IDENTITY_SESSION_TTL_MS: u64 = 24 * 60 * 60 * 1_000;

/// An authenticated session for a DID.
///
/// Created after successful OTP verification; carries the session token and
/// the public key used to verify subsequent requests.
/// Spec §8.
#[derive(Clone, PartialEq, Eq, Serialize, Deserialize)]
pub struct IdentitySession {
    /// Opaque session token issued after verified onboarding bootstrap.
    pub session_token: String,
    /// The DID authenticated by this session.
    pub subject_did: Did,
    /// The session public key (raw bytes).
    pub public_key: Vec<u8>,
    /// When the session was created (epoch ms).
    pub created_ms: u64,
    /// When the session was last used (epoch ms).
    pub last_active_ms: u64,
    /// Whether the session has been revoked.
    pub revoked: bool,
}

impl fmt::Debug for IdentitySession {
    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
        f.debug_struct("IdentitySession")
            .field("session_token", &"<redacted>")
            .field("subject_did", &self.subject_did)
            .field("public_key", &self.public_key)
            .field("created_ms", &self.created_ms)
            .field("last_active_ms", &self.last_active_ms)
            .field("revoked", &self.revoked)
            .finish()
    }
}

impl IdentitySession {
    /// Return the exclusive expiry deadline for this session, or `None` when
    /// the timestamp arithmetic overflows and the session must fail closed.
    #[must_use]
    pub fn expires_at_ms(&self) -> Option<u64> {
        self.created_ms.checked_add(IDENTITY_SESSION_TTL_MS)
    }

    /// Returns true when the session is expired at `now_ms`.
    #[must_use]
    pub fn is_expired_at(&self, now_ms: u64) -> bool {
        self.expires_at_ms()
            .is_none_or(|expires_at| now_ms >= expires_at)
    }
}

// ---------------------------------------------------------------------------
// Display / FromStr implementations for persistence
// ---------------------------------------------------------------------------

impl fmt::Display for ClaimType {
    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
        match self {
            Self::DisplayName => f.write_str("DisplayName"),
            Self::Email => f.write_str("Email"),
            Self::Phone => f.write_str("Phone"),
            Self::GovernmentId => f.write_str("GovernmentId"),
            Self::BiometricLiveness => f.write_str("BiometricLiveness"),
            Self::ProfessionalCredential { provider } => {
                write!(f, "ProfessionalCredential:{provider}")
            }
            Self::DeviceFingerprint => f.write_str("DeviceFingerprint"),
            Self::BehavioralSignature => f.write_str("BehavioralSignature"),
            Self::GeographicConsistency => f.write_str("GeographicConsistency"),
            Self::SessionContinuity => f.write_str("SessionContinuity"),
            Self::PeerAttestation { attester_did } => {
                write!(f, "PeerAttestation:{}", attester_did.as_str())
            }
            Self::DelegationGrant { delegator_did } => {
                write!(f, "DelegationGrant:{}", delegator_did.as_str())
            }
            Self::SybilChallengeResolution { challenge_id } => {
                write!(f, "SybilChallengeResolution:{challenge_id}")
            }
            Self::GovernanceVote { proposal_hash } => write!(
                f,
                "GovernanceVote:{}",
                hex::encode(proposal_hash.as_bytes())
            ),
            Self::ProposalAuthored { proposal_hash } => write!(
                f,
                "ProposalAuthored:{}",
                hex::encode(proposal_hash.as_bytes())
            ),
            Self::ValidatorService {
                round_range: (start, end),
            } => write!(f, "ValidatorService:{start}:{end}"),
            Self::KeyRotation { old_key_hash } => {
                write!(f, "KeyRotation:{}", hex::encode(old_key_hash.as_bytes()))
            }
            Self::EntropyAttestation => f.write_str("EntropyAttestation"),
        }
    }
}

impl fmt::Display for ClaimStatus {
    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
        let s = match self {
            Self::Pending => "Pending",
            Self::Verified => "Verified",
            Self::Expired => "Expired",
            Self::Revoked => "Revoked",
            Self::Challenged => "Challenged",
        };
        f.write_str(s)
    }
}

impl std::str::FromStr for ClaimStatus {
    type Err = ();
    fn from_str(s: &str) -> Result<Self, Self::Err> {
        match s {
            "Pending" => Ok(Self::Pending),
            "Verified" => Ok(Self::Verified),
            "Expired" => Ok(Self::Expired),
            "Revoked" => Ok(Self::Revoked),
            "Challenged" => Ok(Self::Challenged),
            _ => Err(()),
        }
    }
}

impl fmt::Display for BehavioralSignalType {
    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
        let s = match self {
            Self::KeystrokeDynamics => "KeystrokeDynamics",
            Self::MouseDynamics => "MouseDynamics",
            Self::TouchDynamics => "TouchDynamics",
            Self::ScrollBehavior => "ScrollBehavior",
            Self::FormNavigationCadence => "FormNavigationCadence",
        };
        f.write_str(s)
    }
}

impl std::str::FromStr for BehavioralSignalType {
    type Err = ();
    fn from_str(s: &str) -> Result<Self, Self::Err> {
        match s {
            "KeystrokeDynamics" => Ok(Self::KeystrokeDynamics),
            "MouseDynamics" => Ok(Self::MouseDynamics),
            "TouchDynamics" => Ok(Self::TouchDynamics),
            "ScrollBehavior" => Ok(Self::ScrollBehavior),
            "FormNavigationCadence" => Ok(Self::FormNavigationCadence),
            _ => Err(()),
        }
    }
}

impl fmt::Display for OtpChannel {
    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
        let s = match self {
            Self::Email => "Email",
            Self::Sms => "Sms",
        };
        f.write_str(s)
    }
}

impl std::str::FromStr for OtpChannel {
    type Err = ();
    fn from_str(s: &str) -> Result<Self, Self::Err> {
        match s {
            "Email" => Ok(Self::Email),
            "Sms" => Ok(Self::Sms),
            _ => Err(()),
        }
    }
}

impl OtpChannel {
    /// Return the default TTL for this channel in milliseconds.
    ///
    /// - Email: 5 minutes (300_000 ms)
    /// - SMS:   3 minutes (180_000 ms)
    #[must_use]
    pub fn ttl_ms(&self) -> u64 {
        match self {
            Self::Email => 300_000,
            Self::Sms => 180_000,
        }
    }
}

impl fmt::Display for OtpState {
    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
        let s = match self {
            Self::Pending => "Pending",
            Self::Verified => "Verified",
            Self::Expired => "Expired",
            Self::LockedOut => "LockedOut",
        };
        f.write_str(s)
    }
}

impl std::str::FromStr for OtpState {
    type Err = ();
    fn from_str(s: &str) -> Result<Self, Self::Err> {
        match s {
            "Pending" => Ok(Self::Pending),
            "Verified" => Ok(Self::Verified),
            "Expired" => Ok(Self::Expired),
            "LockedOut" => Ok(Self::LockedOut),
            _ => Err(()),
        }
    }
}

// ---------------------------------------------------------------------------
// Tests
// ---------------------------------------------------------------------------

#[cfg(test)]
#[allow(clippy::unwrap_used)]
mod tests {
    use std::str::FromStr;

    use super::*;
    use crate::zerodentity::OTP_MAX_ATTEMPTS;

    // ---- AttestationType ----

    #[test]
    fn attestation_type_from_str_roundtrips() {
        for s in ["Identity", "Trustworthy", "Professional", "Character"] {
            let t = AttestationType::from_str(s).unwrap();
            assert_eq!(t.to_string(), s);
        }
        assert!(AttestationType::from_str("Unknown").is_err());
    }

    // ---- ClaimStatus ----

    #[test]
    fn claim_status_from_str_roundtrips() {
        for s in ["Pending", "Verified", "Expired", "Revoked", "Challenged"] {
            let t = ClaimStatus::from_str(s).unwrap();
            assert_eq!(t.to_string(), s);
        }
        assert!(ClaimStatus::from_str("X").is_err());
    }

    // ---- BehavioralSignalType ----

    #[test]
    fn behavioral_signal_type_from_str_roundtrips() {
        for s in [
            "KeystrokeDynamics",
            "MouseDynamics",
            "TouchDynamics",
            "ScrollBehavior",
            "FormNavigationCadence",
        ] {
            let t = BehavioralSignalType::from_str(s).unwrap();
            assert_eq!(t.to_string(), s);
        }
        assert!(BehavioralSignalType::from_str("Unknown").is_err());
    }

    // ---- OtpChannel ----

    #[test]
    fn otp_channel_from_str_roundtrips() {
        assert_eq!(OtpChannel::from_str("Email").unwrap(), OtpChannel::Email);
        assert_eq!(OtpChannel::from_str("Sms").unwrap(), OtpChannel::Sms);
        assert!(OtpChannel::from_str("Unknown").is_err());
    }

    #[test]
    fn otp_channel_ttl_ms_email_5_min() {
        assert_eq!(OtpChannel::Email.ttl_ms(), 300_000);
    }

    #[test]
    fn otp_channel_ttl_ms_sms_3_min() {
        assert_eq!(OtpChannel::Sms.ttl_ms(), 180_000);
    }

    // ---- OtpState ----

    #[test]
    fn otp_state_from_str_roundtrips() {
        for s in ["Pending", "Verified", "Expired", "LockedOut"] {
            let t = OtpState::from_str(s).unwrap();
            assert_eq!(t.to_string(), s);
        }
        assert!(OtpState::from_str("X").is_err());
    }

    #[test]
    fn otp_challenge_serialization_fails_closed_before_secret_exposure() {
        let challenge = OtpChallenge {
            challenge_id: "challenge-secret-serialization".into(),
            subject_did: Did::new("did:exo:otp-subject").unwrap(),
            channel: OtpChannel::Email,
            hmac_secret: OtpHmacSecret::new([9u8; 32]).unwrap(),
            dispatched_ms: 1_000,
            ttl_ms: OtpChannel::Email.ttl_ms(),
            attempts: 0,
            max_attempts: OTP_MAX_ATTEMPTS,
            state: OtpState::Pending,
        };

        let error = serde_json::to_string(&challenge)
            .expect_err("OTP challenge serialization must fail before exposing HMAC secret bytes");
        assert!(error.to_string().contains("OTP HMAC secret"));
    }

    #[test]
    fn identity_session_debug_redacts_session_token() {
        let session = IdentitySession {
            session_token: "session-token-must-not-appear".into(),
            subject_did: Did::new("did:exo:session-subject").unwrap(),
            public_key: vec![1, 2, 3],
            created_ms: 1_000,
            last_active_ms: 2_000,
            revoked: false,
        };

        let debug = format!("{session:?}");
        assert!(debug.contains("session_token"));
        assert!(debug.contains("<redacted>"));
        assert!(
            !debug.contains("session-token-must-not-appear"),
            "IdentitySession Debug output must not expose bearer session tokens: {debug}"
        );
    }

    // ---- FingerprintSignal Display ----

    #[test]
    fn fingerprint_signal_display_all_variants() {
        let variants = [
            (FingerprintSignal::AudioContext, "AudioContext"),
            (FingerprintSignal::BatteryStatus, "BatteryStatus"),
            (FingerprintSignal::CanvasRendering, "CanvasRendering"),
            (FingerprintSignal::ColorDepthDPR, "ColorDepthDPR"),
            (FingerprintSignal::DeviceMemory, "DeviceMemory"),
            (FingerprintSignal::DoNotTrack, "DoNotTrack"),
            (FingerprintSignal::FontEnumeration, "FontEnumeration"),
            (
                FingerprintSignal::HardwareConcurrency,
                "HardwareConcurrency",
            ),
            (FingerprintSignal::Platform, "Platform"),
            (FingerprintSignal::ScreenGeometry, "ScreenGeometry"),
            (FingerprintSignal::TimezoneLocale, "TimezoneLocale"),
            (FingerprintSignal::TouchSupport, "TouchSupport"),
            (FingerprintSignal::UserAgent, "UserAgent"),
            (FingerprintSignal::WebGLParameters, "WebGLParameters"),
            (FingerprintSignal::WebRTCLocalIPs, "WebRTCLocalIPs"),
        ];
        for (v, expected) in &variants {
            assert_eq!(v.to_string(), *expected);
        }
    }

    // ---- ClaimType Display ----

    #[test]
    fn claim_type_display_simple_variants() {
        assert_eq!(ClaimType::DisplayName.to_string(), "DisplayName");
        assert_eq!(ClaimType::Email.to_string(), "Email");
        assert_eq!(ClaimType::Phone.to_string(), "Phone");
        assert_eq!(ClaimType::GovernmentId.to_string(), "GovernmentId");
        assert_eq!(
            ClaimType::BiometricLiveness.to_string(),
            "BiometricLiveness"
        );
        assert_eq!(
            ClaimType::DeviceFingerprint.to_string(),
            "DeviceFingerprint"
        );
        assert_eq!(
            ClaimType::BehavioralSignature.to_string(),
            "BehavioralSignature"
        );
        assert_eq!(
            ClaimType::GeographicConsistency.to_string(),
            "GeographicConsistency"
        );
        assert_eq!(
            ClaimType::SessionContinuity.to_string(),
            "SessionContinuity"
        );
        assert_eq!(
            ClaimType::EntropyAttestation.to_string(),
            "EntropyAttestation"
        );
    }

    #[test]
    fn claim_type_display_parameterised_variants() {
        let did = Did::new("did:exo:x").unwrap();

        let pc = ClaimType::ProfessionalCredential {
            provider: "ACME".into(),
        };
        assert_eq!(pc.to_string(), "ProfessionalCredential:ACME");

        let pa = ClaimType::PeerAttestation {
            attester_did: did.clone(),
        };
        assert!(pa.to_string().starts_with("PeerAttestation:did:exo:x"));

        let dg = ClaimType::DelegationGrant { delegator_did: did };
        assert!(dg.to_string().starts_with("DelegationGrant:did:exo:x"));

        let scr = ClaimType::SybilChallengeResolution {
            challenge_id: "ch1".into(),
        };
        assert_eq!(scr.to_string(), "SybilChallengeResolution:ch1");

        let vs = ClaimType::ValidatorService {
            round_range: (10, 20),
        };
        assert_eq!(vs.to_string(), "ValidatorService:10:20");
    }

    #[test]
    fn claim_type_display_hash_variants() {
        let hash = Hash256::digest(b"test");

        let gv = ClaimType::GovernanceVote {
            proposal_hash: hash,
        };
        assert!(gv.to_string().starts_with("GovernanceVote:"));

        let prop = ClaimType::ProposalAuthored {
            proposal_hash: hash,
        };
        assert!(prop.to_string().starts_with("ProposalAuthored:"));

        let kr = ClaimType::KeyRotation { old_key_hash: hash };
        assert!(kr.to_string().starts_with("KeyRotation:"));
    }

    // ---- PolarAxes ----

    #[test]
    fn polar_axes_as_array_returns_correct_order() {
        let axes = PolarAxes {
            communication: 1,
            credential_depth: 2,
            device_trust: 3,
            behavioral_signature: 4,
            network_reputation: 5,
            temporal_stability: 6,
            cryptographic_strength: 7,
            constitutional_standing: 8,
        };
        assert_eq!(axes.as_array(), [1, 2, 3, 4, 5, 6, 7, 8]);
    }
}
