//! Identity resolution primitives for ATProto handles, DIDs, services, and multikey parsing.
//!
//! This module provides a narrow interface over HTTP and DNS resolution,
//! allowing callers to swap real network I/O with recorded fixtures in tests.

use async_trait::async_trait;
use k256::ecdsa::signature::hazmat::PrehashVerifier;
use serde::{Deserialize, Serialize};
use std::fmt;
use std::sync::Arc;
use thiserror::Error;
use url::Url;

use crate::common::APP_USER_AGENT;

/// A DID method identifier.
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum DidMethod {
    /// `did:plc:` — decentralized identifiers on the PLC directory.
    Plc,
    /// `did:web:` — decentralized identifiers over HTTPS.
    Web,
    /// An unrecognized DID method.
    Other,
}

/// A decentralized identifier (DID), internally stored as a string.
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct Did(pub String);

impl Did {
    /// Returns the method component of this DID.
    pub fn method(&self) -> DidMethod {
        if self.0.starts_with("did:plc:") {
            DidMethod::Plc
        } else if self.0.starts_with("did:web:") {
            DidMethod::Web
        } else {
            DidMethod::Other
        }
    }
}

impl fmt::Display for Did {
    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
        write!(f, "{}", self.0)
    }
}

/// A verification method (public key) from a DID document.
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
pub struct VerificationMethod {
    /// The identifier for this method, e.g. `"#atproto_labeler"`.
    pub id: String,
    /// The cryptographic type, e.g. `"EcdsaSecp256k1VerificationKey2019"`.
    #[serde(rename = "type")]
    pub type_: String,
    /// The controller DID for this method.
    pub controller: String,
    /// The public key in multibase format.
    #[serde(rename = "publicKeyMultibase", skip_serializing_if = "Option::is_none")]
    pub public_key_multibase: Option<String>,
}

/// A service endpoint from a DID document.
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
pub struct Service {
    /// The identifier for this service, e.g. `"#atproto_labeler"` or `"did:plc:xyz#atproto_labeler"`.
    pub id: String,
    /// The service type, e.g. `"AtprotoLabeler"`.
    #[serde(rename = "type")]
    pub type_: String,
    /// The service endpoint URL or value.
    #[serde(rename = "serviceEndpoint")]
    pub service_endpoint: String,
}

/// A minimal DID document with only the fields we need.
///
/// Per user-global rules, we use explicit field-level `#[serde(rename)]`
/// attributes instead of `#[serde(flatten)]`.
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
pub struct DidDocument {
    /// The DID identifier.
    pub id: String,
    /// Aliases for this DID (e.g., handles).
    #[serde(rename = "alsoKnownAs", skip_serializing_if = "Option::is_none")]
    pub also_known_as: Option<Vec<String>>,
    /// Public keys and other verification methods.
    #[serde(rename = "verificationMethod", skip_serializing_if = "Option::is_none")]
    pub verification_method: Option<Vec<VerificationMethod>>,
    /// Service endpoints provided by this DID.
    #[serde(skip_serializing_if = "Option::is_none")]
    pub service: Option<Vec<Service>>,
}

/// The parsed DID document along with the raw bytes and source name.
///
/// Downstream stages attach the raw bytes to miette diagnostics.
#[derive(Debug, Clone)]
pub struct RawDidDocument {
    /// The parsed DID document.
    pub parsed: DidDocument,
    /// The raw bytes returned by the server.
    pub source_bytes: Arc<[u8]>,
    /// The URL or source name for diagnostics.
    pub source_name: String,
}

/// A public key that may be one of several supported curves.
#[derive(Debug, Clone)]
pub enum AnyVerifyingKey {
    /// secp256k1 verifying key.
    K256(k256::ecdsa::VerifyingKey),
    /// P-256 verifying key.
    P256(p256::ecdsa::VerifyingKey),
}

impl AnyVerifyingKey {
    /// Returns the short curve name for this key ("secp256k1" or "P-256").
    pub fn curve_name(&self) -> &'static str {
        match self {
            AnyVerifyingKey::K256(_) => "secp256k1",
            AnyVerifyingKey::P256(_) => "P-256",
        }
    }

    /// Verifies a prehashed signature against this key.
    ///
    /// The prehash must be a 32-byte SHA-256 digest.
    pub fn verify_prehash(
        &self,
        prehash: &[u8; 32],
        sig: &AnySignature,
    ) -> Result<(), AnySignatureError> {
        match (self, sig) {
            (AnyVerifyingKey::K256(key), AnySignature::K256(sig)) => key
                .verify_prehash(prehash, sig)
                .map_err(AnySignatureError::K256),
            (AnyVerifyingKey::P256(key), AnySignature::P256(sig)) => key
                .verify_prehash(prehash, sig)
                .map_err(AnySignatureError::P256),
            _ => Err(AnySignatureError::CurveMismatch),
        }
    }
}

/// A signature that may be one of several supported curves.
#[derive(Debug, Clone)]
pub enum AnySignature {
    /// secp256k1 signature.
    K256(k256::ecdsa::Signature),
    /// P-256 signature.
    P256(p256::ecdsa::Signature),
}

/// Error from signature verification across multiple curves.
#[derive(Debug, thiserror::Error)]
pub enum AnySignatureError {
    /// secp256k1 signature error.
    #[error("secp256k1 signature verification failed")]
    K256(#[source] k256::ecdsa::Error),
    /// P-256 signature error.
    #[error("P-256 signature verification failed")]
    P256(#[source] p256::ecdsa::Error),
    /// Signature and key use mismatched curves.
    #[error("Signature and key use mismatched curves")]
    CurveMismatch,
}

/// A multikey public key, parsed and ready for signature verification.
#[derive(Debug, Clone)]
pub struct ParsedMultikey {
    /// The verifying key.
    pub verifying_key: AnyVerifyingKey,
}

/// Errors during identity resolution.
#[derive(Debug, Error)]
pub enum IdentityError {
    /// Handle was syntactically invalid.
    #[error("Invalid handle format")]
    InvalidHandle,

    /// Handle could not be resolved via DNS or HTTPS.
    #[error("Handle could not be resolved")]
    HandleUnresolvable {
        /// DNS lookup error (if any).
        dns_error: Option<Box<IdentityError>>,
        /// HTTPS fallback error (if any).
        http_error: Option<Box<IdentityError>>,
    },

    /// DNS lookup failed.
    #[error("DNS lookup failed")]
    DnsLookupFailed {
        /// The underlying DNS error.
        #[source]
        source: Box<IdentityError>,
    },

    /// DNS backend resolver error.
    #[error("DNS backend error")]
    DnsBackend(#[from] hickory_resolver::ResolveError),

    /// HTTPS fallback for handle resolution failed.
    #[error("HTTP fallback for handle resolution failed")]
    HandleHttpFallbackFailed {
        /// The underlying HTTP error.
        #[source]
        source: Box<IdentityError>,
    },

    /// DID method is not supported.
    #[error("Unsupported DID method: {method}")]
    UnsupportedDidMethod { method: String },

    /// DID resolution failed with a non-200 status.
    #[error("DID resolution failed with status {status}")]
    DidResolutionFailed {
        /// The HTTP status code.
        status: u16,
        /// The response body.
        body: String,
    },

    /// DNS record exists but contains no DID entry.
    #[error("DNS record for {handle} has no did= entry")]
    DnsNoDidRecord {
        /// The handle that was queried.
        handle: String,
    },

    /// DID body is invalid or malformed.
    #[error("Invalid DID body: {body}")]
    InvalidDidBody {
        /// The invalid body content.
        body: String,
    },

    /// DID document could not be decoded.
    #[error("DID document decode failed")]
    DidDocumentDecodeFailed {
        /// The source name for diagnostics.
        source_name: String,
        /// The raw bytes for diagnostics.
        source_bytes: Arc<[u8]>,
        /// The JSON parse error.
        #[source]
        cause: serde_json::Error,
    },

    /// Multikey decoding failed.
    #[error("Multikey decoding failed")]
    MultikeyDecodeFailed {
        /// The underlying error.
        #[source]
        source: Box<IdentityError>,
    },

    /// Multibase encoding was not supported.
    #[error("Unsupported multibase encoding")]
    UnsupportedMultibase(String),

    /// Curve is not supported.
    #[error("Unsupported curve")]
    UnsupportedCurve { codec_prefix: Vec<u8> },

    /// Multikey length was invalid.
    #[error("Invalid multikey length")]
    MultikeyLengthInvalid,

    /// HTTP transport error from reqwest.
    #[error("HTTP transport error")]
    HttpTransport(#[from] reqwest::Error),
}

/// Trait for HTTP clients used by identity resolution.
///
/// Narrow interface allowing test fixtures to be injected.
#[async_trait]
pub trait HttpClient: Send + Sync {
    /// Fetches the bytes at the given URL, returning status code and body.
    async fn get_bytes(&self, url: &Url) -> Result<(u16, Vec<u8>), IdentityError>;
}

/// Trait for DNS resolvers used by identity resolution.
///
/// Narrow interface allowing test fixtures to be injected.
#[async_trait]
pub trait DnsResolver: Send + Sync {
    /// Performs a TXT lookup on the given name.
    async fn txt_lookup(&self, name: &str) -> Result<Vec<String>, IdentityError>;
}

/// Real HTTP client using reqwest.
pub struct RealHttpClient {
    inner: reqwest::Client,
}

impl RealHttpClient {
    /// Creates a new HTTP client with a conservative 10-second timeout.
    ///
    /// Uses rustls for TLS (per Cargo.toml `rustls-tls` feature) rather than native-tls
    /// to avoid linking against OpenSSL. Sets a User-Agent header for HTTPS requests.
    pub fn new() -> Result<Self, IdentityError> {
        let client = reqwest::Client::builder()
            .use_rustls_tls()
            .user_agent(APP_USER_AGENT)
            .timeout(std::time::Duration::from_secs(10))
            .build()?;
        Ok(Self { inner: client })
    }

    /// Creates a new HTTP client from an existing reqwest::Client.
    ///
    /// Allows sharing a single client instance across multiple stages.
    pub fn from_client(client: reqwest::Client) -> Self {
        Self { inner: client }
    }
}

#[async_trait]
impl HttpClient for RealHttpClient {
    async fn get_bytes(&self, url: &Url) -> Result<(u16, Vec<u8>), IdentityError> {
        let response = self.inner.get(url.clone()).send().await?;
        let status = response.status().as_u16();
        let bytes = response.bytes().await?;
        Ok((status, bytes.to_vec()))
    }
}

/// Real DNS resolver using hickory-resolver.
pub struct RealDnsResolver {
    inner: hickory_resolver::TokioResolver,
}

impl RealDnsResolver {
    /// Creates a new DNS resolver with system configuration.
    pub fn new() -> Self {
        let resolver = hickory_resolver::Resolver::builder_tokio()
            .expect("failed to build DNS resolver")
            .build();
        Self { inner: resolver }
    }
}

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

#[async_trait]
impl DnsResolver for RealDnsResolver {
    async fn txt_lookup(&self, name: &str) -> Result<Vec<String>, IdentityError> {
        let lookup = self.inner.txt_lookup(name).await?;
        lookup
            .iter()
            .map(|record| {
                let text = record
                    .iter()
                    .map(|data| {
                        String::from_utf8(data.to_vec()).unwrap_or_else(|_| {
                            tracing::debug!(
                                target = "atproto_devtool::identity",
                                "dropping non-UTF-8 TXT record data"
                            );
                            String::new()
                        })
                    })
                    .collect::<Vec<_>>()
                    .join("");
                Ok(text)
            })
            .collect()
    }
}

/// Resolves an ATProto handle to a DID via DNS or HTTPS fallback.
///
/// Attempts DNS lookup on `_atproto.<handle>` for a `did=...` record,
/// then falls back to HTTPS GET on `https://<handle>/.well-known/atproto-did`.
pub async fn resolve_handle(
    handle: &str,
    http: &dyn HttpClient,
    dns: &dyn DnsResolver,
) -> Result<Did, IdentityError> {
    // Validate handle format: at least one dot, all ASCII, no leading/trailing dot.
    if handle.is_empty()
        || handle.starts_with('.')
        || handle.ends_with('.')
        || !handle.is_ascii()
        || !handle.contains('.')
    {
        return Err(IdentityError::InvalidHandle);
    }

    tracing::debug!(
        target = "atproto_devtool::identity",
        handle = %handle,
        "resolving handle"
    );

    // DNS path: look up _atproto.<handle> for did= records.
    let dns_name = format!("_atproto.{handle}");

    let dns_error_opt = match dns.txt_lookup(&dns_name).await {
        Ok(records) => {
            // Check if any record contains a did= entry.
            for record in records {
                let trimmed = record.trim();
                if let Some(did_str) = trimmed.strip_prefix("did=") {
                    let did = Did(did_str.to_string());
                    tracing::debug!(
                        target = "atproto_devtool::identity",
                        did = %did,
                        "resolved handle via DNS"
                    );
                    return Ok(did);
                }
            }
            // Records exist but no did= entry found; populate error for fallback.
            Some(Box::new(IdentityError::DnsNoDidRecord {
                handle: handle.to_string(),
            }))
        }
        Err(e) => Some(Box::new(e)),
    };

    // HTTPS fallback: GET https://<handle>/.well-known/atproto-did.
    let url = format!("https://{handle}/.well-known/atproto-did");
    let url = url
        .parse::<Url>()
        .map_err(|_| IdentityError::InvalidHandle)?;

    let http_error_opt = match http.get_bytes(&url).await {
        Ok((200, bytes)) => {
            let did_str = String::from_utf8_lossy(&bytes).trim().to_string();
            if !did_str.is_empty() && did_str.starts_with("did:") {
                let did = Did(did_str);
                tracing::debug!(
                    target = "atproto_devtool::identity",
                    did = %did,
                    "resolved handle via HTTPS"
                );
                return Ok(did);
            } else {
                Some(Box::new(IdentityError::HandleHttpFallbackFailed {
                    source: Box::new(IdentityError::InvalidDidBody { body: did_str }),
                }))
            }
        }
        Ok((status, bytes)) => Some(Box::new(IdentityError::HandleHttpFallbackFailed {
            source: Box::new(IdentityError::DidResolutionFailed {
                status,
                body: String::from_utf8_lossy(&bytes).to_string(),
            }),
        })),
        Err(e) => Some(Box::new(IdentityError::HandleHttpFallbackFailed {
            source: Box::new(e),
        })),
    };

    // Both paths failed.
    Err(IdentityError::HandleUnresolvable {
        dns_error: dns_error_opt,
        http_error: http_error_opt,
    })
}

/// Resolves a DID to a parsed DID document with raw bytes.
///
/// Returns both the parsed document and the raw bytes for use in diagnostics.
pub async fn resolve_did(
    did: &Did,
    http: &dyn HttpClient,
) -> Result<RawDidDocument, IdentityError> {
    tracing::debug!(
        target = "atproto_devtool::identity",
        did = %did,
        "resolving DID"
    );

    let (url, source_name) = match did.method() {
        DidMethod::Plc => {
            // did:plc identifiers are base32-like and contain only URL-safe characters.
            // No additional percent-encoding is needed.
            let did_str = &did.0;
            let url_str = format!("https://plc.directory/{did_str}");
            let url = url_str
                .parse::<Url>()
                .map_err(|_| IdentityError::DidResolutionFailed {
                    status: 400,
                    body: "Invalid DID format".to_string(),
                })?;
            (url.clone(), url.to_string())
        }
        DidMethod::Web => {
            // Strip did:web: prefix and URL-decode path segments.
            let rest = did.0.strip_prefix("did:web:").unwrap_or("");
            let parts: Vec<&str> = rest.split(':').collect();

            if parts.is_empty() {
                return Err(IdentityError::DidResolutionFailed {
                    status: 400,
                    body: "Invalid did:web format".to_string(),
                });
            }

            let host = parts[0];
            let path_parts = &parts[1..];

            let url_str = if path_parts.is_empty() {
                format!("https://{host}/.well-known/did.json")
            } else {
                let path = path_parts
                    .iter()
                    .map(|p| percent_decode_str(p).unwrap_or_default())
                    .collect::<Vec<_>>()
                    .join("/");
                format!("https://{host}/{path}/did.json")
            };

            let url = url_str
                .parse::<Url>()
                .map_err(|_| IdentityError::DidResolutionFailed {
                    status: 400,
                    body: "Invalid URL".to_string(),
                })?;
            (url.clone(), url.to_string())
        }
        DidMethod::Other => {
            return Err(IdentityError::UnsupportedDidMethod {
                method: did.0.clone(),
            });
        }
    };

    let (status, bytes) = http.get_bytes(&url).await?;

    if status != 200 {
        return Err(IdentityError::DidResolutionFailed {
            status,
            body: String::from_utf8_lossy(&bytes).to_string(),
        });
    }

    tracing::debug!(
        target = "atproto_devtool::identity",
        bytes_len = bytes.len(),
        "fetched DID document"
    );

    let parsed = serde_json::from_slice::<DidDocument>(&bytes).map_err(|e| {
        IdentityError::DidDocumentDecodeFailed {
            source_name: source_name.clone(),
            source_bytes: Arc::from(bytes.clone()),
            cause: e,
        }
    })?;

    Ok(RawDidDocument {
        parsed,
        source_bytes: Arc::from(bytes),
        source_name,
    })
}

/// Finds a service in a DID document by fragment and type.
///
/// Matches both `"#fragment"` and `"did:..#fragment"` ID forms.
pub fn find_service<'a>(
    doc: &'a DidDocument,
    id_fragment: &str,
    expected_type: &str,
) -> Option<&'a Service> {
    let services = doc.service.as_ref()?;

    for service in services {
        // Extract the fragment after '#', matching both forms:
        // - "#fragment" (short form)
        // - "did:...#fragment" (full form)
        let frag = service.id.rsplit_once('#').map(|(_, f)| f);
        if frag == Some(id_fragment) && service.type_ == expected_type {
            return Some(service);
        }
    }

    None
}

/// Parses a multikey string into a verifying key.
///
/// Accepts either a bare base58btc multibase string (`z…`) or a
/// `did:key:z…` form. The latter is how PLC audit logs and some DID
/// documents surface verification methods, so stripping the prefix here
/// means every caller can stay agnostic to the wire shape.
pub fn parse_multikey(raw: &str) -> Result<ParsedMultikey, IdentityError> {
    tracing::debug!(target = "atproto_devtool::identity", "parsing multikey");

    let multibase_str = raw.strip_prefix("did:key:").unwrap_or(raw);

    let (base, bytes) =
        multibase::decode(multibase_str).map_err(|_| IdentityError::MultikeyDecodeFailed {
            source: Box::new(IdentityError::UnsupportedMultibase(
                "failed to decode multibase".to_string(),
            )),
        })?;

    // Require base58btc encoding.
    if base != multibase::Base::Base58Btc {
        return Err(IdentityError::UnsupportedMultibase(
            "multikey must use base58btc encoding".to_string(),
        ));
    }

    if bytes.len() < 2 {
        return Err(IdentityError::MultikeyLengthInvalid);
    }

    // Parse the varint curve identifier (two bytes for both supported curves).
    let curve_bytes = [bytes[0], bytes[1]];
    let rest = &bytes[2..];

    match curve_bytes {
        // secp256k1-pub (0xe7 0x01)
        [0xe7, 0x01] => {
            if rest.len() != 33 {
                return Err(IdentityError::MultikeyLengthInvalid);
            }
            let key = k256::ecdsa::VerifyingKey::from_sec1_bytes(rest).map_err(|_| {
                IdentityError::MultikeyDecodeFailed {
                    source: Box::new(IdentityError::MultikeyLengthInvalid),
                }
            })?;
            tracing::debug!(
                target = "atproto_devtool::identity",
                curve = "secp256k1",
                "parsed multikey"
            );
            Ok(ParsedMultikey {
                verifying_key: AnyVerifyingKey::K256(key),
            })
        }
        // p256-pub (0x80 0x24)
        [0x80, 0x24] => {
            if rest.len() != 33 {
                return Err(IdentityError::MultikeyLengthInvalid);
            }
            let key = p256::ecdsa::VerifyingKey::from_sec1_bytes(rest).map_err(|_| {
                IdentityError::MultikeyDecodeFailed {
                    source: Box::new(IdentityError::MultikeyLengthInvalid),
                }
            })?;
            tracing::debug!(
                target = "atproto_devtool::identity",
                curve = "p256",
                "parsed multikey"
            );
            Ok(ParsedMultikey {
                verifying_key: AnyVerifyingKey::P256(key),
            })
        }
        _ => Err(IdentityError::UnsupportedCurve {
            codec_prefix: curve_bytes.to_vec(),
        }),
    }
}

/// A historic key entry from a PLC audit log for a given verification method fragment.
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct PlcHistoricKey {
    /// The multikey string (the raw key material, not a DID fragment).
    pub key_id: String,
    /// The CID of the operation that introduced this key.
    pub operation_cid: String,
    /// ISO8601 timestamp from the operation.
    pub introduced_at: String,
    /// Whether this key was nullified.
    pub nullified: bool,
}

/// Fetches the PLC audit log for a DID and extracts historic keys for a fragment.
///
/// Only valid for `did:plc:` DIDs. Panics in debug for other methods; returns
/// `Err(UnsupportedDidMethod)` in release. Returns keys in chronological order
/// (oldest-first), matching the PLC API wire order. The caller treats the
/// result as a set — iteration order does not affect verification correctness.
///
/// Transport errors are propagated as `HttpTransport`; decode errors as `DidDocumentDecodeFailed`.
pub async fn plc_history_for_fragment(
    did: &Did,
    fragment: &str,
    http: &dyn HttpClient,
) -> Result<Vec<PlcHistoricKey>, IdentityError> {
    debug_assert!(
        did.method() == DidMethod::Plc,
        "plc_history_for_fragment called with non-plc DID: {did}"
    );

    if did.method() != DidMethod::Plc {
        return Err(IdentityError::UnsupportedDidMethod {
            method: format!("{:?}", did.method()),
        });
    }

    // Construct the audit log URL: https://plc.directory/{did}/log/audit
    let audit_url = format!("https://plc.directory/{did}/log/audit");
    let url = Url::parse(&audit_url).map_err(|_| IdentityError::DidResolutionFailed {
        status: 400,
        body: "Invalid PLC audit URL".to_string(),
    })?;

    let (status, bytes) = http.get_bytes(&url).await?;

    if status != 200 {
        return Err(IdentityError::DidResolutionFailed {
            status,
            body: format!("PLC audit log fetch returned status {status}"),
        });
    }

    // Parse the JSON array of operations.
    let operations: Vec<serde_json::Value> =
        serde_json::from_slice(&bytes).map_err(|cause| IdentityError::DidDocumentDecodeFailed {
            source_name: "plc audit log".to_string(),
            source_bytes: Arc::from(bytes.into_boxed_slice()),
            cause,
        })?;

    let mut historic_keys: Vec<PlcHistoricKey> = Vec::new();

    // Walk operations in wire order (oldest-first). Deduplicate by multikey
    // string: PLC audit logs carry one entry per operation, so a single
    // persistent key is repeated on every unrelated rotation. Callers want
    // the set of distinct keys; the dedup keeps the earliest introduction.
    for op in operations {
        // Extract the verificationMethods object from operation.
        let vm = match op
            .get("operation")
            .and_then(|o| o.get("verificationMethods"))
        {
            Some(vm) => vm,
            None => continue,
        };

        // Look for the requested fragment.
        if let Some(multikey_value) = vm.get(fragment) {
            let multikey_str = match multikey_value.as_str() {
                Some(s) => s.to_string(),
                None => continue,
            };

            let operation_cid = op
                .get("cid")
                .and_then(|c| c.as_str())
                .unwrap_or("unknown")
                .to_string();

            // Extract the timestamp from the operation if available.
            let introduced_at = op
                .get("operation")
                .and_then(|o| o.get("createdAt"))
                .and_then(|c| c.as_str())
                .unwrap_or("unknown")
                .to_string();

            let nullified = op
                .get("nullified")
                .and_then(|n| n.as_bool())
                .unwrap_or(false);

            if historic_keys.iter().any(|k| k.key_id == multikey_str) {
                continue;
            }

            historic_keys.push(PlcHistoricKey {
                key_id: multikey_str,
                operation_cid,
                introduced_at,
                nullified,
            });
        }
    }

    Ok(historic_keys)
}

/// Helper to percent-decode a string.
fn percent_decode_str(s: &str) -> Result<String, IdentityError> {
    let decoded = percent_encoding::percent_decode_str(s)
        .decode_utf8()
        .map_err(|_| IdentityError::DidResolutionFailed {
            status: 400,
            body: "Invalid UTF-8 in percent-encoded path".to_string(),
        })?;
    Ok(decoded.to_string())
}

#[cfg(test)]
mod tests {
    use super::*;
    use k256::ecdsa::SigningKey as K256SigningKey;
    use k256::ecdsa::signature::hazmat::PrehashSigner;
    use p256::ecdsa::SigningKey as P256SigningKey;
    use std::collections::HashMap;

    /// Response variant for FakeHttpClient.
    #[derive(Clone)]
    enum Response {
        /// HTTP response with status code and body.
        Http(u16, Vec<u8>),
        /// Transport error (network-level failure).
        Transport(String),
    }

    /// Fake HTTP client for testing, built from a map of URL → response.
    struct FakeHttpClient {
        responses: HashMap<String, Response>,
    }

    #[async_trait]
    impl HttpClient for FakeHttpClient {
        async fn get_bytes(&self, url: &Url) -> Result<(u16, Vec<u8>), IdentityError> {
            match self.responses.get(url.as_str()).cloned() {
                Some(Response::Http(status, body)) => Ok((status, body)),
                Some(Response::Transport(message)) => {
                    // Simulate a transport error by returning a synthesized status 0.
                    // We cannot construct a real reqwest::Error in unit tests, so the
                    // message is threaded into the body so assertions can inspect it.
                    Err(IdentityError::DidResolutionFailed {
                        status: 0,
                        body: format!("Transport error: {message}"),
                    })
                }
                None => Err(IdentityError::DidResolutionFailed {
                    status: 404,
                    body: "Not found".to_string(),
                }),
            }
        }
    }

    /// Fake DNS resolver for testing, built from a map of name → records.
    struct FakeDnsResolver {
        records: HashMap<String, Vec<String>>,
    }

    #[async_trait]
    impl DnsResolver for FakeDnsResolver {
        async fn txt_lookup(&self, name: &str) -> Result<Vec<String>, IdentityError> {
            self.records
                .get(name)
                .cloned()
                .ok_or_else(|| IdentityError::DnsLookupFailed {
                    source: Box::new(IdentityError::InvalidHandle),
                })
        }
    }

    #[tokio::test]
    async fn resolve_handle_via_dns() {
        let mut records = HashMap::new();
        records.insert(
            "_atproto.alice.example".to_string(),
            vec!["did=did:plc:abc123".to_string()],
        );
        let dns = FakeDnsResolver { records };
        let http = FakeHttpClient {
            responses: HashMap::new(),
        };

        let result = resolve_handle("alice.example", &http, &dns).await;

        assert!(result.is_ok());
        let did = result.unwrap();
        assert_eq!(did.0, "did:plc:abc123");
    }

    #[tokio::test]
    async fn resolve_handle_via_https_fallback() {
        let dns = FakeDnsResolver {
            records: HashMap::new(),
        };
        let mut responses = HashMap::new();
        responses.insert(
            "https://alice.example/.well-known/atproto-did".to_string(),
            Response::Http(200, b"did:plc:abc123\n".to_vec()),
        );
        let http = FakeHttpClient { responses };

        let result = resolve_handle("alice.example", &http, &dns).await;

        assert!(result.is_ok());
        let did = result.unwrap();
        assert_eq!(did.0, "did:plc:abc123");
    }

    #[tokio::test]
    async fn resolve_handle_both_paths_fail() {
        let dns = FakeDnsResolver {
            records: HashMap::new(),
        };
        let http = FakeHttpClient {
            responses: HashMap::new(),
        };

        let result = resolve_handle("alice.example", &http, &dns).await;

        assert!(result.is_err());
        match result.unwrap_err() {
            IdentityError::HandleUnresolvable {
                dns_error,
                http_error,
            } => {
                assert!(dns_error.is_some());
                assert!(http_error.is_some());
            }
            _ => panic!("Expected HandleUnresolvable error"),
        }
    }

    #[tokio::test]
    async fn resolve_did_plc_success() {
        let plc_doc = include_bytes!("../../tests/fixtures/identity/plc_bsky_labeler.json");
        let mut responses = HashMap::new();
        responses.insert(
            "https://plc.directory/did:plc:test-labeler".to_string(),
            Response::Http(200, plc_doc.to_vec()),
        );
        let http = FakeHttpClient { responses };

        let did = Did("did:plc:test-labeler".to_string());
        let raw_doc = resolve_did(&did, &http).await.expect("resolve_did");
        assert_eq!(raw_doc.parsed.id, "did:plc:test-labeler");
        assert!(raw_doc.source_bytes.as_ref() == plc_doc);
        assert_eq!(
            raw_doc.source_name,
            "https://plc.directory/did:plc:test-labeler"
        );

        // Verify both services are present.
        let services = raw_doc.parsed.service.as_ref().expect("services");
        assert!(
            services.iter().any(|s| s.type_ == "AtprotoLabeler"),
            "fixture must contain a labeler service"
        );
        assert!(
            services
                .iter()
                .any(|s| s.type_ == "AtprotoPersonalDataServer"),
            "fixture must contain a PDS service"
        );

        // Verify both verification methods are present.
        let vms = raw_doc
            .parsed
            .verification_method
            .as_ref()
            .expect("verificationMethod");
        assert!(
            vms.iter().any(|vm| vm.id == "#atproto"),
            "fixture must contain a repo signing key"
        );
        assert!(
            vms.iter().any(|vm| vm.id == "#atproto_label"),
            "fixture must contain a label signing key"
        );
    }

    #[tokio::test]
    async fn resolve_did_web_success() {
        let web_doc = include_bytes!("../../tests/fixtures/identity/web_example.json");
        let mut responses = HashMap::new();
        responses.insert(
            "https://example.com/.well-known/did.json".to_string(),
            Response::Http(200, web_doc.to_vec()),
        );
        let http = FakeHttpClient { responses };

        let did = Did("did:web:example.com".to_string());
        let result = resolve_did(&did, &http).await;

        assert!(result.is_ok());
        let raw_doc = result.unwrap();
        assert_eq!(raw_doc.parsed.id, "did:web:example.com");
        assert_eq!(
            raw_doc.source_name,
            "https://example.com/.well-known/did.json"
        );
    }

    #[tokio::test]
    async fn resolve_did_decode_failure_preserves_bytes() {
        let bad_json = b"not valid json";
        let mut responses = HashMap::new();
        responses.insert(
            "https://plc.directory/did:plc:bad".to_string(),
            Response::Http(200, bad_json.to_vec()),
        );
        let http = FakeHttpClient { responses };

        let did = Did("did:plc:bad".to_string());
        let result = resolve_did(&did, &http).await;

        assert!(result.is_err());
        match result.unwrap_err() {
            IdentityError::DidDocumentDecodeFailed {
                source_name: _,
                source_bytes,
                cause: _,
            } => {
                assert_eq!(source_bytes.as_ref(), bad_json);
            }
            _ => panic!("Expected DidDocumentDecodeFailed error"),
        }
    }

    #[test]
    fn find_service_matches_both_id_forms() {
        let doc = DidDocument {
            id: "did:plc:abc".to_string(),
            also_known_as: None,
            verification_method: None,
            service: Some(vec![
                Service {
                    id: "did:plc:abc#atproto_labeler".to_string(),
                    type_: "AtprotoLabeler".to_string(),
                    service_endpoint: "https://example.com/labeler".to_string(),
                },
                Service {
                    id: "#atproto_pds".to_string(),
                    type_: "AtprotoPersonalDataServer".to_string(),
                    service_endpoint: "https://example.com/pds".to_string(),
                },
                // Service with a name that contains the search fragment as a substring.
                Service {
                    id: "#xatproto_labeler".to_string(),
                    type_: "OtherType".to_string(),
                    service_endpoint: "https://example.com/other".to_string(),
                },
            ]),
        };

        let labeler = find_service(&doc, "atproto_labeler", "AtprotoLabeler");
        assert!(labeler.is_some());
        let labeler = labeler.unwrap();
        assert_eq!(labeler.id, "did:plc:abc#atproto_labeler");

        let pds = find_service(&doc, "atproto_pds", "AtprotoPersonalDataServer");
        assert!(pds.is_some());
        let pds = pds.unwrap();
        assert_eq!(pds.id, "#atproto_pds");

        // Ensure false-positive on substring is not matched.
        let wrong = find_service(&doc, "atproto_labeler", "OtherType");
        assert!(wrong.is_none());
    }

    #[test]
    fn find_service_type_mismatch_returns_none() {
        let doc = DidDocument {
            id: "did:plc:abc".to_string(),
            also_known_as: None,
            verification_method: None,
            service: Some(vec![Service {
                id: "#atproto_labeler".to_string(),
                type_: "AtprotoLabeler".to_string(),
                service_endpoint: "https://example.com/labeler".to_string(),
            }]),
        };

        let result = find_service(&doc, "atproto_labeler", "WrongType");
        assert!(result.is_none());
    }

    #[test]
    fn parse_multikey_k256() {
        // Generated from an ephemeral k256 keypair.
        // These keys are not production keys; they are synthetic fixtures for testing.
        let multikey = include_str!("../../tests/fixtures/identity/multikey_k256.txt").trim();

        let result = parse_multikey(multikey);
        assert!(result.is_ok());

        let parsed = result.unwrap();

        // Verify the key can be extracted and its bytes match the expected value.
        match &parsed.verifying_key {
            AnyVerifyingKey::K256(key) => {
                let sec1_bytes = key.to_sec1_bytes();
                assert_eq!(sec1_bytes.len(), 33); // Compressed form is 33 bytes.
                // Expected bytes derived from the multikey fixture.
                // zQ3shVc2UkAfJCdc1TR8E66J85h48P43r93q8jGPkPpjF9Ef9 decodes to:
                // [0xe7, 0x01] (k256 prefix) + 33-byte SEC1 point.
                let expected_hex =
                    "0279be667ef9dcbbac55a06295ce870b07029bfcdb2dce28d959f2815b16f81798";
                let actual_hex: String = sec1_bytes.iter().map(|b| format!("{b:02x}")).collect();
                assert_eq!(actual_hex, expected_hex);
            }
            _ => panic!("Expected K256 verifying key"),
        }
    }

    #[test]
    fn parse_multikey_p256() {
        // Generated from an ephemeral p256 keypair.
        // These keys are not production keys; they are synthetic fixtures for testing.
        let multikey = include_str!("../../tests/fixtures/identity/multikey_p256.txt").trim();

        let result = parse_multikey(multikey);
        assert!(result.is_ok());

        let parsed = result.unwrap();

        // Verify the key can be extracted and is the correct type.
        match &parsed.verifying_key {
            AnyVerifyingKey::P256(key) => {
                // Force compressed form for consistent testing.
                let sec1_bytes = key.to_encoded_point(true).as_bytes().to_vec();
                assert_eq!(sec1_bytes.len(), 33);
                // Expected bytes derived from the multikey fixture.
                let expected_hex =
                    "026b17d1f2e12c4247f8bce6e563a440f277037d812deb33a0f4a13945d898c296";
                let actual_hex: String = sec1_bytes.iter().map(|b| format!("{b:02x}")).collect();
                assert_eq!(actual_hex, expected_hex);
            }
            _ => panic!("Expected P256 verifying key"),
        }
    }

    #[test]
    fn parse_multikey_unsupported_curve() {
        // Build a multikey with an unsupported curve prefix (0x01 0x00).
        let mut unsupported_bytes = vec![0x01, 0x00];
        unsupported_bytes.extend_from_slice(&[0; 33]); // Fake 33-byte key.
        // multibase::encode already returns the z-prefixed string for Base58Btc.
        let multikey = multibase::encode(multibase::Base::Base58Btc, unsupported_bytes);

        let result = parse_multikey(&multikey);
        assert!(result.is_err());

        match result.unwrap_err() {
            IdentityError::UnsupportedCurve { codec_prefix: _ } => {}
            _ => panic!("Expected UnsupportedCurve error"),
        }
    }

    #[test]
    fn parse_multikey_not_base58btc() {
        // Use base16 encoding instead of base58btc.
        let mut key_bytes = vec![0xe7, 0x01];
        key_bytes.extend_from_slice(&[0; 33]);
        // Manually create base16 multibase string (f prefix).
        let hex_str: String = key_bytes.iter().map(|b| format!("{b:02x}")).collect();
        let multikey = format!("f{hex_str}");

        let result = parse_multikey(&multikey);
        assert!(result.is_err());

        match result.unwrap_err() {
            IdentityError::UnsupportedMultibase(_) => {}
            _ => panic!("Expected UnsupportedMultibase error"),
        }
    }

    #[test]
    fn parse_multikey_accepts_did_key_prefix() {
        // The bare multikey parses, and prepending "did:key:" must be equivalent.
        // PLC audit logs store verificationMethods values in the did:key form,
        // so this path is load-bearing for the crypto stage's history fallback.
        let bare = include_str!("../../tests/fixtures/identity/multikey_k256.txt").trim();
        let did_key = format!("did:key:{bare}");

        let from_bare = parse_multikey(bare).expect("bare multikey should parse");
        let from_did_key = parse_multikey(&did_key).expect("did:key multikey should parse");

        assert!(matches!(from_bare.verifying_key, AnyVerifyingKey::K256(_)));
        assert!(matches!(
            from_did_key.verifying_key,
            AnyVerifyingKey::K256(_)
        ));
    }

    #[test]
    fn parse_multikey_wrong_length() {
        // Build a multikey with a 10-byte body instead of 33.
        let mut wrong_len_bytes = vec![0x80, 0x24]; // p256 prefix
        wrong_len_bytes.extend_from_slice(&[0; 10]); // Only 10 bytes instead of 33

        // multibase::encode already returns the z-prefixed string for Base58Btc.
        let multikey = multibase::encode(multibase::Base::Base58Btc, &wrong_len_bytes);

        let result = parse_multikey(&multikey);
        assert!(result.is_err());

        // Must strictly assert MultikeyLengthInvalid.
        match result.unwrap_err() {
            IdentityError::MultikeyLengthInvalid => {
                // Correct error variant.
            }
            e => panic!("Expected MultikeyLengthInvalid, got {e:?}"),
        }
    }

    #[test]
    fn verify_prehash_k256_valid() {
        // Create an ephemeral k256 keypair for testing.
        let signing_key = K256SigningKey::random(&mut k256::elliptic_curve::rand_core::OsRng);
        let verifying_key = signing_key.verifying_key();

        // Create a test prehash (32 bytes).
        let prehash = *b"01234567890123456789012345678901";

        // Sign the prehash.
        let signature = signing_key.sign_prehash(&prehash).expect("signing failed");

        // Wrap in our generic types.
        let any_key = AnyVerifyingKey::K256(*verifying_key);
        let any_sig = AnySignature::K256(signature);

        // Verify should succeed.
        assert!(any_key.verify_prehash(&prehash, &any_sig).is_ok());
    }

    #[test]
    fn verify_prehash_p256_valid() {
        // Create an ephemeral p256 keypair for testing.
        let signing_key = P256SigningKey::random(&mut p256::elliptic_curve::rand_core::OsRng);
        let verifying_key = signing_key.verifying_key();

        // Create a test prehash (32 bytes).
        let prehash = *b"01234567890123456789012345678901";

        // Sign the prehash.
        let signature = signing_key.sign_prehash(&prehash).expect("signing failed");

        // Wrap in our generic types.
        let any_key = AnyVerifyingKey::P256(*verifying_key);
        let any_sig = AnySignature::P256(signature);

        // Verify should succeed.
        assert!(any_key.verify_prehash(&prehash, &any_sig).is_ok());
    }

    #[test]
    fn verify_prehash_curve_mismatch() {
        // Test k256 key with p256 signature (should fail).
        let k256_signing_key = K256SigningKey::random(&mut k256::elliptic_curve::rand_core::OsRng);
        let k256_key = AnyVerifyingKey::K256(*k256_signing_key.verifying_key());

        let p256_signing_key = P256SigningKey::random(&mut p256::elliptic_curve::rand_core::OsRng);
        let prehash = *b"01234567890123456789012345678901";
        let p256_sig = p256_signing_key
            .sign_prehash(&prehash)
            .expect("signing failed");
        let p256_any_sig = AnySignature::P256(p256_sig);

        // Verify should fail due to curve mismatch.
        assert!(k256_key.verify_prehash(&prehash, &p256_any_sig).is_err());

        // Test p256 key with k256 signature (symmetric case).
        let p256_signing_key_2 =
            P256SigningKey::random(&mut p256::elliptic_curve::rand_core::OsRng);
        let p256_key = AnyVerifyingKey::P256(*p256_signing_key_2.verifying_key());

        let k256_signing_key_2 =
            K256SigningKey::random(&mut k256::elliptic_curve::rand_core::OsRng);
        let k256_sig = k256_signing_key_2
            .sign_prehash(&prehash)
            .expect("signing failed");
        let k256_any_sig = AnySignature::K256(k256_sig);

        // Verify should fail due to curve mismatch.
        assert!(p256_key.verify_prehash(&prehash, &k256_any_sig).is_err());
    }

    #[tokio::test]
    async fn plc_history_parses_rotation_fixture() {
        // Load the fixture with one key rotation.
        let fixture_bytes =
            include_bytes!("../../tests/fixtures/identity/plc_audit_log_with_rotation.json");
        let mut responses = HashMap::new();
        responses.insert(
            "https://plc.directory/did:plc:test/log/audit".to_string(),
            Response::Http(200, fixture_bytes.to_vec()),
        );
        let http = FakeHttpClient { responses };

        let did = Did("did:plc:test".to_string());
        let result = plc_history_for_fragment(&did, "atproto_label", &http)
            .await
            .expect("plc_history should succeed");

        // Expect two distinct keys from the fixture, in chronological order.
        assert_eq!(result.len(), 2);
        assert_eq!(
            result[0].key_id,
            "z3u1HhU9Dn1R1TDe8kSmEMCrJ5B8t9K7c7N2L3xX7y"
        );
        assert!(!result[0].nullified);
        assert_eq!(
            result[1].key_id,
            "z3u1HhU9Dn1R1TDe8kSmEMCrJ5B8t9K7c7N2L3xX7z"
        );
        assert!(!result[1].nullified);
    }

    #[tokio::test]
    async fn plc_history_dedupes_repeated_key() {
        // Craft an audit log where the same atproto_label multikey appears
        // across several operations (e.g. because only unrelated signing
        // keys rotated). The caller wants the set of distinct historic keys.
        let key = "did:key:zQ3shw6eSipD1cnrmmokVWvKCuE6Yc9j2jAjWJ9nWpuF4yQKV";
        let log = serde_json::json!([
            {"cid": "op3", "operation": {"verificationMethods": {"atproto_label": key}}},
            {"cid": "op2", "operation": {"verificationMethods": {"atproto_label": key}}},
            {"cid": "op1", "operation": {"verificationMethods": {"atproto_label": key}}},
        ]);
        let mut responses = HashMap::new();
        responses.insert(
            "https://plc.directory/did:plc:dedupe/log/audit".to_string(),
            Response::Http(200, serde_json::to_vec(&log).unwrap()),
        );
        let http = FakeHttpClient { responses };

        let did = Did("did:plc:dedupe".to_string());
        let result = plc_history_for_fragment(&did, "atproto_label", &http)
            .await
            .expect("plc_history should succeed");

        assert_eq!(result.len(), 1);
        assert_eq!(result[0].key_id, key);
        // Newest entry wins on dedupe.
        assert_eq!(result[0].operation_cid, "op3");
    }

    #[tokio::test]
    #[should_panic(expected = "plc_history_for_fragment called with non-plc DID")]
    async fn plc_history_unsupported_method_errors() {
        // did:web should panic in debug (due to debug_assert).
        // In release, it would return Err(UnsupportedDidMethod).
        let mut responses = HashMap::new();
        responses.insert(
            "https://plc.directory/did:web:example.com/log/audit".to_string(),
            Response::Http(200, b"[]".to_vec()),
        );
        let http = FakeHttpClient { responses };

        let did = Did("did:web:example.com".to_string());
        let _result = plc_history_for_fragment(&did, "atproto_label", &http).await;
    }

    #[tokio::test]
    async fn plc_history_transport_error_propagates() {
        // FakeHttpClient returning a transport error (represented as status 0).
        let mut responses = HashMap::new();
        responses.insert(
            "https://plc.directory/did:plc:test/log/audit".to_string(),
            Response::Transport("connection refused".to_string()),
        );
        let http = FakeHttpClient { responses };

        let did = Did("did:plc:test".to_string());
        let result = plc_history_for_fragment(&did, "atproto_label", &http).await;

        assert!(result.is_err());
        // The error should be DidResolutionFailed with status 0 (transport error).
        match result.unwrap_err() {
            IdentityError::DidResolutionFailed { status, body } => {
                assert_eq!(status, 0);
                assert_eq!(body, "Transport error: connection refused");
            }
            e => panic!("Expected DidResolutionFailed with status 0, got {e:?}"),
        }
    }
}
