use crate::config::{NetworkCapability, ensure_network_access};
use crate::crypt::hash::{hash_bytes_raw, hash_public_key};
use crate::error::JacsError;
use base64::{Engine as _, engine::general_purpose::STANDARD as B64};
use serde::{Deserialize, Serialize};

#[derive(Clone, Debug, PartialEq, Eq)]
pub struct DnsRecord {
    pub owner: String,
    pub ttl: u32,
    pub txt: String,
}

#[derive(Clone, Debug, PartialEq, Eq)]
pub enum Provider {
    Plain,
    Aws,
    Azure,
    Cloudflare,
}

#[derive(Clone, Debug, PartialEq, Eq)]
pub enum DigestEncoding {
    Base64,
    Hex,
}

#[derive(Clone, Debug, PartialEq, Eq)]
pub struct AgentTxtFields {
    pub v: String,
    pub jacs_agent_id: String,
    pub alg: String,
    pub enc: DigestEncoding,
    pub digest: String,
}

pub fn pubkey_digest_sha256_bytes(pubkey: &[u8]) -> [u8; 32] {
    hash_bytes_raw(pubkey)
}

pub fn pubkey_digest_b64(pubkey: &[u8]) -> String {
    let bytes = pubkey_digest_sha256_bytes(pubkey);
    B64.encode(bytes)
}

pub fn pubkey_digest_hex(pubkey: &[u8]) -> String {
    let bytes = pubkey_digest_sha256_bytes(pubkey);
    hex::encode(bytes)
}

pub fn build_agent_dns_txt(agent_id: &str, digest: &str, enc: DigestEncoding) -> String {
    let enc_str = match enc {
        DigestEncoding::Base64 => "base64",
        DigestEncoding::Hex => "hex",
    };
    format!(
        "v=jacs; jacs_agent_id={}; alg=SHA-256; enc={}; jac_public_key_hash={}",
        agent_id, enc_str, digest
    )
}

pub fn parse_agent_txt(txt: &str) -> Result<AgentTxtFields, JacsError> {
    let parts: Vec<&str> = txt.split(';').map(|s| s.trim()).collect();
    let mut map = std::collections::HashMap::new();
    for p in parts {
        if p.is_empty() {
            continue;
        }
        if let Some((k, v)) = p.split_once('=') {
            map.insert(k.trim().to_string(), v.trim().to_string());
        }
    }
    let missing = |field: &str| JacsError::DnsRecordInvalid {
        domain: String::new(),
        reason: format!("Missing {} field in TXT record", field),
    };
    let v = map.get("v").cloned().ok_or_else(|| missing("v"))?;
    let jacs_agent_id = map
        .get("jacs_agent_id")
        .cloned()
        .ok_or_else(|| missing("jacs_agent_id"))?;
    let alg = map.get("alg").cloned().ok_or_else(|| missing("alg"))?;
    let enc_val = map.get("enc").cloned().ok_or_else(|| missing("enc"))?;
    let enc = match enc_val.as_str() {
        "base64" => DigestEncoding::Base64,
        "hex" => DigestEncoding::Hex,
        _ => {
            return Err(JacsError::DnsRecordInvalid {
                domain: String::new(),
                reason: format!("Unsupported encoding: {}", enc_val),
            });
        }
    };
    let digest = map
        .get("jac_public_key_hash")
        .cloned()
        .ok_or_else(|| missing("jac_public_key_hash"))?;
    Ok(AgentTxtFields {
        v,
        jacs_agent_id,
        alg,
        enc,
        digest,
    })
}

pub fn record_owner(domain: &str) -> String {
    format!("_v1.agent.jacs.{}.", domain.trim_end_matches('.'))
}

pub fn build_dns_record(
    domain: &str,
    ttl: u32,
    agent_id: &str,
    digest: &str,
    enc: DigestEncoding,
) -> DnsRecord {
    let owner = record_owner(domain);
    let txt = build_agent_dns_txt(agent_id, digest, enc);
    DnsRecord { owner, ttl, txt }
}

pub fn emit_plain_bind(rr: &DnsRecord) -> String {
    format!("{} {} IN TXT \"{}\"", rr.owner, rr.ttl, rr.txt)
}

pub fn emit_route53_change_batch(rr: &DnsRecord) -> String {
    // Inner value must be quoted and escaped for JSON
    let val = format!("\\\"{}\\\"", rr.txt);
    format!(
        r#"{{
  "Comment": "UPSERT JACS agent TXT",
  "Changes": [{{
    "Action": "UPSERT",
    "ResourceRecordSet": {{
      "Name": "{}",
      "Type": "TXT",
      "TTL": {},
      "ResourceRecords": [{{ "Value": "{}" }}]
    }}
  }}]
}}"#,
        rr.owner, rr.ttl, val
    )
}

pub fn emit_gcloud_dns(rr: &DnsRecord, zone: &str) -> String {
    format!(
        "gcloud dns record-sets transaction start --zone {zone}\n\
gcloud dns record-sets transaction add --zone {zone} \\\n+  --name {owner} --ttl {ttl} --type TXT \\\n+  --txt-data \"{txt}\"\n\
gcloud dns record-sets transaction execute --zone {zone}",
        zone = zone,
        owner = rr.owner,
        ttl = rr.ttl,
        txt = rr.txt
    )
}

pub fn emit_azure_cli(
    rr: &DnsRecord,
    resource_group: &str,
    dns_zone: &str,
    short_name: &str,
) -> String {
    format!(
        "az network dns record-set txt create -g {rg} -z {zone} -n {short} --ttl {ttl}\n\
az network dns record-set txt add-record -g {rg} -z {zone} -n {short} \\\n+  --value \"{txt}\"",
        rg = resource_group,
        zone = dns_zone,
        short = short_name,
        ttl = rr.ttl,
        txt = rr.txt
    )
}

pub fn emit_cloudflare_curl(rr: &DnsRecord, zone_id_hint: &str) -> String {
    let owner_no_dot = rr.owner.trim_end_matches('.');
    format!(
        "curl -X POST \"https://api.cloudflare.com/client/v4/zones/{zone_id}/dns_records\" \\n+  -H \"Authorization: Bearer ${{API_TOKEN}}\" -H \"Content-Type: application/json\" \\\n+  --data '{{\n    \"type\":\"TXT\",\n    \"name\":\"{name}\",\n    \"content\":\"{content}\",\n    \"ttl\":{ttl},\n    \"proxied\":false\n  }}'",
        zone_id = zone_id_hint,
        name = owner_no_dot,
        content = rr.txt,
        ttl = rr.ttl
    )
}

/// Find the first JACS-formatted TXT record from a list of individual TXT record strings.
///
/// DNS lookups may return multiple TXT records (SPF, DKIM, DMARC, etc.) at the same
/// domain name. This function filters for the record starting with `v=jacs` and ignores
/// all others, preventing non-JACS records from corrupting the parsed result.
///
/// Returns `Err(JacsError::DnsRecordInvalid)` if no JACS record is found.
pub fn find_jacs_txt_record(records: Vec<String>, domain: &str) -> Result<String, JacsError> {
    for record in records {
        if record.starts_with("v=jacs") {
            return Ok(record);
        }
    }
    Err(JacsError::DnsRecordInvalid {
        domain: domain.to_string(),
        reason: "No v=jacs TXT record found at this domain".to_string(),
    })
}

#[cfg(not(target_arch = "wasm32"))]
pub fn resolve_txt_dnssec(owner: &str) -> Result<String, JacsError> {
    use hickory_resolver::Resolver;
    use hickory_resolver::config::{ResolverConfig, ResolverOpts};
    ensure_network_access(NetworkCapability::DnsLookup)?;
    let mut opts = ResolverOpts::default();
    opts.validate = true;
    let resolver =
        Resolver::new(ResolverConfig::default(), opts).map_err(|e| JacsError::DnsLookupFailed {
            domain: owner.to_string(),
            reason: format!("Resolver init failed: {e}"),
        })?;
    let resp = resolver
        .txt_lookup(owner)
        .map_err(|e| JacsError::DnsLookupFailed {
            domain: owner.to_string(),
            reason: format!("DNS lookup failed: {e}"),
        })?;
    let mut records = Vec::new();
    for rr in resp.iter() {
        let mut record = String::new();
        for part in rr.txt_data() {
            record.push_str(&String::from_utf8(part.to_vec()).map_err(|e| {
                JacsError::DnsRecordInvalid {
                    domain: owner.to_string(),
                    reason: format!("UTF-8 decode failed: {e}"),
                }
            })?);
        }
        records.push(record);
    }
    find_jacs_txt_record(records, owner)
}

#[cfg(not(target_arch = "wasm32"))]
pub fn resolve_txt_insecure(owner: &str) -> Result<String, JacsError> {
    use hickory_resolver::Resolver;
    use hickory_resolver::config::{ResolverConfig, ResolverOpts};
    ensure_network_access(NetworkCapability::DnsLookup)?;
    let mut opts = ResolverOpts::default();
    opts.validate = false; // allow unsigned answers
    let resolver =
        Resolver::new(ResolverConfig::default(), opts).map_err(|e| JacsError::DnsLookupFailed {
            domain: owner.to_string(),
            reason: format!("Resolver init failed: {e}"),
        })?;
    let resp = resolver
        .txt_lookup(owner)
        .map_err(|e| JacsError::DnsLookupFailed {
            domain: owner.to_string(),
            reason: format!("DNS lookup failed: {e}"),
        })?;
    let mut records = Vec::new();
    for rr in resp.iter() {
        let mut record = String::new();
        for part in rr.txt_data() {
            record.push_str(&String::from_utf8(part.to_vec()).map_err(|e| {
                JacsError::DnsRecordInvalid {
                    domain: owner.to_string(),
                    reason: format!("UTF-8 decode failed: {e}"),
                }
            })?);
        }
        records.push(record);
    }
    find_jacs_txt_record(records, owner)
}

pub fn verify_pubkey_via_dns_or_embedded(
    agent_public_key: &[u8],
    agent_id: &str,
    jacs_agent_domain: Option<&str>,
    embedded_fingerprint: Option<&str>,
    strict_dns: bool,
) -> Result<(), JacsError> {
    let local_b64 = pubkey_digest_b64(agent_public_key);
    let local_hex = pubkey_digest_hex(agent_public_key);

    if let Some(domain) = jacs_agent_domain {
        let owner = record_owner(domain);
        let lookup = if strict_dns {
            resolve_txt_dnssec(&owner)
        } else {
            resolve_txt_insecure(&owner)
        };
        match lookup {
            Ok(txt) => {
                let f = parse_agent_txt(&txt)?;
                if f.v != "jacs" {
                    return Err(JacsError::DnsRecordInvalid {
                        domain: domain.to_string(),
                        reason: format!("Unexpected v field: {}", f.v),
                    });
                }
                if f.jacs_agent_id != agent_id {
                    return Err(JacsError::DnsRecordInvalid {
                        domain: domain.to_string(),
                        reason: "Agent ID mismatch".to_string(),
                    });
                }
                let ok = match f.enc {
                    DigestEncoding::Base64 => f.digest == local_b64,
                    DigestEncoding::Hex => f.digest.eq_ignore_ascii_case(&local_hex),
                };
                if ok {
                    return Ok(());
                } else {
                    return Err(JacsError::DnsRecordInvalid {
                        domain: domain.to_string(),
                        reason: "DNS fingerprint mismatch: digest does not match local public key"
                            .to_string(),
                    });
                }
            }
            Err(_e) => {
                // Fallback to embedded if provided
                if let Some(embed) = embedded_fingerprint {
                    // Accept either the new byte-based digest or the legacy normalized-string hex
                    let legacy_hex = hash_public_key(agent_public_key);
                    if embed == local_b64
                        || embed.eq_ignore_ascii_case(&local_hex)
                        || embed.eq_ignore_ascii_case(&legacy_hex)
                    {
                        return Ok(());
                    }
                    return Err(JacsError::DnsRecordInvalid {
                        domain: domain.to_string(),
                        reason: "Embedded fingerprint mismatch: does not match local public key"
                            .to_string(),
                    });
                }
                // Neither DNS nor embedded available
                if strict_dns {
                    return Err(JacsError::DnsLookupFailed {
                        domain: domain.to_string(),
                        reason: format!(
                            "Strict DNSSEC validation failed for {}: TXT not authenticated. Enable DNSSEC and publish DS at registrar",
                            owner
                        ),
                    });
                } else {
                    return Err(JacsError::DnsRecordMissing {
                        domain: domain.to_string(),
                    });
                }
            }
        }
    }

    if let Some(embed) = embedded_fingerprint {
        let legacy_hex = hash_public_key(agent_public_key);
        if embed == local_b64
            || embed.eq_ignore_ascii_case(&local_hex)
            || embed.eq_ignore_ascii_case(&legacy_hex)
        {
            return Ok(());
        }
        return Err(JacsError::SignatureVerificationFailed {
            reason:
                "embedded fingerprint mismatch (embedded present but does not match local public key)"
                    .to_string(),
        });
    }

    Err(JacsError::DnsRecordMissing {
        domain: "unknown".to_string(),
    })
}

// =============================================================================
// Registry Registration Verification
// =============================================================================

/// Information about an agent's registry registration.
#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
pub struct RegistryRegistration {
    /// Whether the agent is verified by the registry
    pub verified: bool,
    /// ISO 8601 timestamp of when the agent was verified
    pub verified_at: Option<String>,
    /// Type of registration (e.g., "agent", "organization")
    pub registration_type: String,
    /// The agent ID as registered with the registry
    pub agent_id: String,
    /// The public key hash registered with the registry
    pub public_key_hash: String,
}

/// Response from registry API for agent lookup
#[derive(Clone, Debug, Deserialize)]
struct RegistryApiResponse {
    #[serde(default)]
    verified: bool,
    #[serde(default)]
    verified_at: Option<String>,
    #[serde(default)]
    registration_type: Option<String>,
    #[serde(default)]
    agent_id: Option<String>,
    #[serde(default)]
    public_key_hash: Option<String>,
}

/// Check if an agent is registered with a registry.
///
/// This function queries the registry API to verify that an agent claiming
/// "verified-registry" status is actually registered.
///
/// # Arguments
///
/// * `agent_id` - The JACS agent ID (UUID format)
/// * `public_key_hash` - The SHA-256 hash of the agent's public key (hex encoded)
///
/// # Returns
///
/// * `Ok(RegistryRegistration)` - Agent is registered and public key matches
/// * `Err(JacsError)` - Verification failed with reason
///
/// # Environment Variables
///
/// * `JACS_REGISTRY_URL` - Registry API URL. No default -- returns an error if not configured.
///
/// # Errors
///
/// This function returns an error if:
/// - No registry URL is configured
/// - Registry API is unreachable (network error)
/// - Agent is not registered with the registry
/// - Public key hash doesn't match the registered key
///
/// # Example
///
/// ```rust,ignore
/// use jacs::dns::bootstrap::verify_registry_registration_sync;
///
/// let result = verify_registry_registration_sync(
///     "550e8400-e29b-41d4-a716-446655440000",
///     "sha256-hash-of-public-key"
/// );
///
/// match result {
///     Ok(reg) => println!("Agent verified at: {:?}", reg.verified_at),
///     Err(e) => println!("Verification failed: {}", e),
/// }
/// ```
#[cfg(not(target_arch = "wasm32"))]
pub fn verify_registry_registration_sync(
    agent_id: &str,
    public_key_hash: &str,
) -> Result<RegistryRegistration, JacsError> {
    // Validate agent_id is a valid UUID to prevent URL path traversal
    uuid::Uuid::parse_str(agent_id).map_err(|e| {
        JacsError::ValidationError(format!(
            "Invalid agent_id '{}' for registry registration: must be a valid UUID. {}",
            agent_id, e
        ))
    })?;

    // Registry API endpoint for agent verification
    let api_url = std::env::var("JACS_REGISTRY_URL").map_err(|_| {
        JacsError::ConfigError(
            "No registry URL configured. Set JACS_REGISTRY_URL to enable registry verification."
                .to_string(),
        )
    })?;
    let parsed = url::Url::parse(&api_url).map_err(|e| {
        JacsError::ConfigError(format!("Invalid JACS_REGISTRY_URL '{}': {}", api_url, e))
    })?;
    let host = parsed.host_str().unwrap_or_default();
    let http_localhost = parsed.scheme() == "http"
        && (host.eq_ignore_ascii_case("localhost") || host == "127.0.0.1");
    if parsed.scheme() != "https" && !http_localhost {
        return Err(JacsError::ConfigError(format!(
            "JACS_REGISTRY_URL must use HTTPS (got '{}'). Only localhost URLs are allowed over HTTP for testing.",
            api_url
        )));
    }
    ensure_network_access(NetworkCapability::RegistryLookup)?;
    let url = format!("{}/v1/agents/{}", api_url.trim_end_matches('/'), agent_id);

    // Build blocking HTTP client with TLS
    let client = reqwest::blocking::Client::builder()
        .timeout(std::time::Duration::from_secs(30))
        .build()
        .map_err(|e| JacsError::NetworkError(format!("Failed to build HTTP client: {}", e)))?;

    // Make request to registry API
    let response = client
        .get(&url)
        .header("Accept", "application/json")
        .send()
        .map_err(|e| {
            JacsError::NetworkError(format!(
                "Registry verification failed: unable to reach API at {}: {}",
                url, e
            ))
        })?;

    // Check response status
    if response.status() == reqwest::StatusCode::NOT_FOUND {
        return Err(JacsError::RegistrationFailed {
            reason: format!(
                "Agent '{}' is not registered with the registry. \
                Agents claiming 'verified-registry' must be registered with a JACS registry.",
                agent_id
            ),
        });
    }

    if !response.status().is_success() {
        return Err(JacsError::NetworkError(format!(
            "Registry API returned error status {}: agent verification failed",
            response.status()
        )));
    }

    // Parse response
    let api_response: RegistryApiResponse = response.json().map_err(|e| {
        JacsError::NetworkError(format!("Failed to parse registry API response: {}", e))
    })?;

    // Verify the agent is actually verified
    if !api_response.verified {
        return Err(JacsError::RegistrationFailed {
            reason: format!(
                "Agent '{}' is registered with the registry but not yet verified. \
                Complete the verification process with your registry provider.",
                agent_id
            ),
        });
    }

    // Verify public key hash matches
    let registered_hash = api_response.public_key_hash.as_deref().unwrap_or("");
    if !registered_hash.eq_ignore_ascii_case(public_key_hash) {
        return Err(JacsError::RegistrationFailed {
            reason: format!(
                "Public key mismatch: agent '{}' is registered with the registry \
                but with a different public key. Expected hash '{}', got '{}'",
                agent_id,
                &public_key_hash[..public_key_hash.len().min(16)],
                &registered_hash[..registered_hash.len().min(16)]
            ),
        });
    }

    Ok(RegistryRegistration {
        verified: true,
        verified_at: api_response.verified_at,
        registration_type: api_response
            .registration_type
            .unwrap_or_else(|| "agent".to_string()),
        agent_id: api_response
            .agent_id
            .unwrap_or_else(|| agent_id.to_string()),
        public_key_hash: registered_hash.to_string(),
    })
}

pub fn dnssec_guidance(provider: &str) -> &'static str {
    match provider {
        "aws" | "route53" => {
            "Enable DNSSEC signing in Route53 hosted zone settings, then publish the DS record at your registrar."
        }
        "cloudflare" => {
            "DNSSEC is one-click in the Cloudflare dashboard under DNS > Settings. Copy the DS record to your registrar."
        }
        "azure" => {
            "Enable DNSSEC signing on the Azure DNS zone, then publish the DS record at your registrar."
        }
        "gcloud" | "google" => {
            "Enable DNSSEC on the Cloud DNS zone (gcloud dns managed-zones update --dnssec-state on), then publish DS at registrar."
        }
        _ => {
            "Enable DNSSEC zone signing with your DNS provider, then publish the DS record at your domain registrar."
        }
    }
}

/// Result of verifying an agent's identity via DNS TXT record.
#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
pub struct DnsVerificationResult {
    /// Whether the DNS record matches the agent's public key hash.
    pub verified: bool,
    /// The agent ID extracted from the agent document.
    pub agent_id: String,
    /// The domain that was checked.
    pub domain: String,
    /// The public key hash from the agent document.
    pub document_hash: String,
    /// The public key hash from the DNS TXT record (empty if lookup failed).
    pub dns_hash: String,
    /// Human-readable status message.
    pub message: String,
}

/// Verify an agent's DNS TXT record matches its public key hash.
///
/// Parses the agent JSON to extract `jacsSignature.publicKeyHash` and `jacsSignature.agentID`,
/// then looks up the DNS TXT record at `_v1.agent.jacs.{domain}` and compares the hashes.
///
/// # Arguments
/// * `agent_json` - Full agent JSON document string
/// * `domain` - Domain to check (e.g., "example.com")
///
/// # Returns
/// `Ok(DnsVerificationResult)` with match status. Never returns `Err` for DNS failures;
/// those are reported via `verified: false` in the result.
#[cfg(not(target_arch = "wasm32"))]
pub fn verify_agent_dns(
    agent_json: &str,
    domain: &str,
) -> Result<DnsVerificationResult, JacsError> {
    let parsed: serde_json::Value =
        serde_json::from_str(agent_json).map_err(|e| JacsError::DocumentMalformed {
            field: "agent_json".to_string(),
            reason: format!("Invalid agent JSON: {}", e),
        })?;

    let sig = parsed
        .get("jacsSignature")
        .ok_or_else(|| JacsError::DocumentMalformed {
            field: "jacsSignature".to_string(),
            reason: "Missing jacsSignature in agent document".to_string(),
        })?;
    let agent_id = sig
        .get("agentID")
        .and_then(|v| v.as_str())
        .unwrap_or("")
        .to_string();
    let doc_hash = sig
        .get("publicKeyHash")
        .and_then(|v| v.as_str())
        .unwrap_or("")
        .to_string();

    if doc_hash.is_empty() {
        return Ok(DnsVerificationResult {
            verified: false,
            agent_id,
            domain: domain.to_string(),
            document_hash: doc_hash,
            dns_hash: String::new(),
            message: "Agent document has no publicKeyHash".to_string(),
        });
    }

    let owner = record_owner(domain);
    let txt = match resolve_txt_insecure(&owner) {
        Ok(t) => t,
        Err(e) => {
            return Ok(DnsVerificationResult {
                verified: false,
                agent_id,
                domain: domain.to_string(),
                document_hash: doc_hash,
                dns_hash: String::new(),
                message: format!("DNS lookup failed for {}: {}", owner, e),
            });
        }
    };

    let fields = match parse_agent_txt(&txt) {
        Ok(f) => f,
        Err(e) => {
            return Ok(DnsVerificationResult {
                verified: false,
                agent_id,
                domain: domain.to_string(),
                document_hash: doc_hash,
                dns_hash: String::new(),
                message: format!("Failed to parse DNS TXT record: {}", e),
            });
        }
    };

    // Compare agent IDs
    if !agent_id.is_empty() && fields.jacs_agent_id != agent_id {
        let msg = format!(
            "Agent ID mismatch: document={}, dns={}",
            agent_id, fields.jacs_agent_id
        );
        return Ok(DnsVerificationResult {
            verified: false,
            agent_id,
            domain: domain.to_string(),
            document_hash: doc_hash,
            dns_hash: fields.digest.clone(),
            message: msg,
        });
    }

    // Compare hashes (support both base64 and hex encodings)
    let matched = match fields.enc {
        DigestEncoding::Base64 => fields.digest == doc_hash,
        DigestEncoding::Hex => fields.digest.eq_ignore_ascii_case(&doc_hash),
    };

    Ok(DnsVerificationResult {
        verified: matched,
        agent_id,
        domain: domain.to_string(),
        document_hash: doc_hash,
        dns_hash: fields.digest,
        message: if matched {
            "DNS public key hash matches agent document".to_string()
        } else {
            "DNS public key hash does NOT match agent document".to_string()
        },
    })
}

pub fn tld_requirement_text() -> &'static str {
    "You must own a registered domain (TLD or subdomain of a TLD you control). Example: example.com or agents.example.com. The JACS TXT record is placed at _v1.agent.jacs.{your-domain}."
}

#[cfg(test)]
mod tests {
    use super::*;
    use serial_test::serial;

    #[test]
    fn test_verify_agent_dns_invalid_json() {
        let result = verify_agent_dns("not json", "example.com");
        assert!(result.is_err());
        let err_msg = result.unwrap_err().to_string();
        assert!(
            err_msg.contains("Invalid agent JSON"),
            "Expected 'Invalid agent JSON', got: {}",
            err_msg
        );
    }

    #[test]
    fn test_verify_agent_dns_missing_signature() {
        let result = verify_agent_dns(r#"{"hello":"world"}"#, "example.com");
        assert!(result.is_err());
        let err_msg = result.unwrap_err().to_string();
        assert!(
            err_msg.contains("jacsSignature"),
            "Expected 'jacsSignature' mention, got: {}",
            err_msg
        );
    }

    #[test]
    fn test_verify_agent_dns_empty_hash() {
        let agent = r#"{"jacsSignature":{"agentID":"test-id","publicKeyHash":""}}"#;
        let result = verify_agent_dns(agent, "example.com").unwrap();
        assert!(!result.verified);
        assert_eq!(result.agent_id, "test-id");
        assert!(result.message.contains("no publicKeyHash"));
    }

    #[test]
    fn test_verify_agent_dns_no_record() {
        // example.com won't have a JACS TXT record
        let agent = r#"{"jacsSignature":{"agentID":"test-id","publicKeyHash":"abc123"}}"#;
        let result = verify_agent_dns(agent, "example.com").unwrap();
        assert!(!result.verified);
        assert_eq!(result.domain, "example.com");
        assert!(
            result.message.contains("DNS lookup failed"),
            "Expected DNS lookup failure, got: {}",
            result.message
        );
    }

    #[test]
    fn test_dnssec_guidance_known_providers() {
        for provider in &["aws", "route53", "cloudflare", "azure", "gcloud", "google"] {
            let text = dnssec_guidance(provider);
            assert!(
                !text.is_empty(),
                "guidance for {} should be non-empty",
                provider
            );
            assert!(
                text.contains("DNSSEC"),
                "guidance for {} should contain 'DNSSEC', got: {}",
                provider,
                text
            );
        }
    }

    #[test]
    fn test_dnssec_guidance_unknown_provider() {
        let text = dnssec_guidance("unknown-provider");
        assert!(text.contains("DNSSEC"));
        assert!(text.contains("DNS provider"));
    }

    #[test]
    fn test_tld_requirement_text() {
        let text = tld_requirement_text();
        assert!(!text.is_empty());
        assert!(text.contains("_v1.agent.jacs"));
    }

    // --- Task 010: DNS format is registry-agnostic ---

    #[test]
    fn test_build_agent_dns_txt_uses_jacs_version_tag() {
        let txt = build_agent_dns_txt("agent-123", "digest-abc", DigestEncoding::Base64);
        assert!(
            txt.starts_with("v=jacs;"),
            "TXT record must use v=jacs, got: {}",
            txt
        );
    }

    #[test]
    fn test_parse_agent_txt_accepts_jacs_version() {
        let txt = "v=jacs; jacs_agent_id=a1; alg=SHA-256; enc=base64; jac_public_key_hash=abc";
        let result = parse_agent_txt(txt).unwrap();
        assert_eq!(result.v, "jacs");
    }

    #[test]
    fn test_parse_agent_txt_rejects_hai_ai_version() {
        // "hai.ai" is no longer accepted as a version tag in DNS TXT records
        let pubkey = b"test-public-key-bytes-for-digest";
        let digest = pubkey_digest_b64(pubkey);
        let agent_id = "test-agent-id";

        let txt = format!(
            "v=hai.ai; jacs_agent_id={}; alg=SHA-256; enc=base64; jac_public_key_hash={}",
            agent_id, digest
        );

        // parse_agent_txt itself is format-only; it parses any v value.
        // The rejection happens in verify_pubkey_via_dns_or_embedded.
        let fields = parse_agent_txt(&txt).unwrap();
        assert_eq!(fields.v, "hai.ai");
        // The verify function should reject this v field
    }

    #[test]
    fn test_record_owner_uses_jacs_namespace() {
        let owner = record_owner("example.com");
        assert_eq!(owner, "_v1.agent.jacs.example.com.");
        assert!(
            !owner.contains("hai"),
            "DNS record owner must not contain hai references"
        );
    }

    #[test]
    fn test_dns_code_no_hai_references_in_txt_format() {
        // Verify the TXT record format is completely generic
        let txt = build_agent_dns_txt("my-agent", "my-digest", DigestEncoding::Hex);
        assert!(
            !txt.to_lowercase().contains("hai"),
            "TXT format must not contain hai references, got: {}",
            txt
        );
    }

    #[test]
    #[serial(jacs_env)]
    fn test_registry_verification_requires_jacs_registry_url() {
        // Ensure HAI_API_URL is NOT used as a fallback
        // SAFETY: test-only env var manipulation; tests run serially for env var tests
        unsafe {
            std::env::remove_var("JACS_REGISTRY_URL");
            std::env::remove_var("HAI_API_URL");

            // Set only HAI_API_URL -- should NOT be picked up
            std::env::set_var("HAI_API_URL", "https://api.hai.ai");
        }

        let result =
            verify_registry_registration_sync("550e8400-e29b-41d4-a716-446655440000", "some-hash");
        assert!(result.is_err(), "Should fail without JACS_REGISTRY_URL");
        let err_msg = result.unwrap_err().to_string();
        assert!(
            err_msg.contains("JACS_REGISTRY_URL"),
            "Error should mention JACS_REGISTRY_URL, got: {}",
            err_msg
        );

        // Cleanup
        unsafe {
            std::env::remove_var("HAI_API_URL");
        }
    }

    // --- H1 fix: find_jacs_txt_record filters non-JACS TXT records ---

    #[test]
    fn test_find_jacs_txt_record_mixed_spf_and_jacs() {
        let records = vec![
            "v=spf1 include:_spf.google.com ~all".to_string(),
            "v=jacs; jacs_agent_id=a1; alg=SHA-256; enc=base64; jac_public_key_hash=abc"
                .to_string(),
        ];
        let result = find_jacs_txt_record(records, "example.com").unwrap();
        assert!(
            result.starts_with("v=jacs"),
            "Should return JACS record, got: {}",
            result
        );
        assert!(
            result.contains("jacs_agent_id=a1"),
            "Should contain agent ID, got: {}",
            result
        );
    }

    #[test]
    fn test_find_jacs_txt_record_no_jacs_record() {
        let records = vec![
            "v=spf1 include:_spf.google.com ~all".to_string(),
            "google-site-verification=abc123".to_string(),
        ];
        let result = find_jacs_txt_record(records, "example.com");
        assert!(result.is_err(), "Should return error when no JACS record");
        let err_msg = result.unwrap_err().to_string();
        assert!(
            err_msg.contains("No v=jacs TXT record"),
            "Error should mention 'No v=jacs TXT record', got: {}",
            err_msg
        );
    }

    #[test]
    fn test_find_jacs_txt_record_empty_records() {
        let records: Vec<String> = vec![];
        let result = find_jacs_txt_record(records, "example.com");
        assert!(result.is_err(), "Should return error for empty records");
        let err_msg = result.unwrap_err().to_string();
        assert!(
            err_msg.contains("No v=jacs TXT record"),
            "Error should mention 'No v=jacs TXT record', got: {}",
            err_msg
        );
    }

    #[test]
    fn test_find_jacs_txt_record_jacs_among_garbage() {
        // JACS record between other unrelated TXT records
        let records = vec![
            "v=spf1 include:_spf.google.com ~all".to_string(),
            "google-site-verification=abc123".to_string(),
            "v=jacs; jacs_agent_id=agent-42; alg=SHA-256; enc=hex; jac_public_key_hash=deadbeef"
                .to_string(),
            "v=DMARC1; p=reject; rua=mailto:dmarc@example.com".to_string(),
        ];
        let result = find_jacs_txt_record(records, "example.com").unwrap();
        assert!(
            result.contains("jacs_agent_id=agent-42"),
            "Should extract the JACS record, got: {}",
            result
        );
    }

    #[test]
    fn test_verify_pubkey_embedded_fallback_no_hai_references() {
        // Test that embedded fingerprint verification works without any hai.ai involvement
        let pubkey = b"test-key-for-embedded-verification";
        let digest = pubkey_digest_b64(pubkey);

        let result = verify_pubkey_via_dns_or_embedded(
            pubkey,
            "agent-123",
            None,          // no domain
            Some(&digest), // embedded fingerprint
            false,         // not strict
        );
        assert!(
            result.is_ok(),
            "Embedded verification should succeed: {:?}",
            result
        );
    }
}
