use std::sync::{LazyLock, Mutex, RwLock};
use std::time::{Instant, SystemTime, UNIX_EPOCH};

use log::{debug, trace};
use ring::digest;
use ring::signature;

use crate::buffer::BytePacketBuffer;
use crate::cache::{DnsCache, DnssecStatus};
use crate::packet::DnsPacket;
use crate::question::QueryType;
use crate::record::DnsRecord;
use crate::srtt::SrttCache;

#[derive(Debug, Default)]
pub struct ValidationStats {
    pub dnskey_cache_hits: u16,
    pub dnskey_fetches: u16,
    pub ds_cache_hits: u16,
    pub ds_fetches: u16,
    pub elapsed_ms: u64,
}

const MAX_CHAIN_DEPTH: u8 = 10;

// IANA root zone trust anchors (algorithm 8 RSASHA256, flags 257 = KSK).
// Both the incumbent KSK-2017 (tag 20326) and its successor KSK-2024 (tag 38696)
// are pinned: the root publishes both throughout the rollover overlap, and
// KSK-2024 becomes the sole signer at the 2026-10-11 switchover. Pinning both
// means the handover is a non-event for Numa — no flag-day, no release race.
// Source: https://data.iana.org/root-anchors/root-anchors.xml
const ROOT_KSK_ALGORITHM: u8 = 8;
const ROOT_KSK_FLAGS: u16 = 257;

#[cfg(test)]
const ROOT_KSK_2017_KEY_TAG: u16 = 20326;
// Decoded from base64: AwEAAaz/tAm8yTn4Mfeh5eyI96WSVexTBAvkMgJzkKTOiW1vkIbz...
const ROOT_KSK_2017_PUBLIC_KEY: &[u8] = &[
    0x03, 0x01, 0x00, 0x01, 0xac, 0xff, 0xb4, 0x09, 0xbc, 0xc9, 0x39, 0xf8, 0x31, 0xf7, 0xa1, 0xe5,
    0xec, 0x88, 0xf7, 0xa5, 0x92, 0x55, 0xec, 0x53, 0x04, 0x0b, 0xe4, 0x32, 0x02, 0x73, 0x90, 0xa4,
    0xce, 0x89, 0x6d, 0x6f, 0x90, 0x86, 0xf3, 0xc5, 0xe1, 0x77, 0xfb, 0xfe, 0x11, 0x81, 0x63, 0xaa,
    0xec, 0x7a, 0xf1, 0x46, 0x2c, 0x47, 0x94, 0x59, 0x44, 0xc4, 0xe2, 0xc0, 0x26, 0xbe, 0x5e, 0x98,
    0xbb, 0xcd, 0xed, 0x25, 0x97, 0x82, 0x72, 0xe1, 0xe3, 0xe0, 0x79, 0xc5, 0x09, 0x4d, 0x57, 0x3f,
    0x0e, 0x83, 0xc9, 0x2f, 0x02, 0xb3, 0x2d, 0x35, 0x13, 0xb1, 0x55, 0x0b, 0x82, 0x69, 0x29, 0xc8,
    0x0d, 0xd0, 0xf9, 0x2c, 0xac, 0x96, 0x6d, 0x17, 0x76, 0x9f, 0xd5, 0x86, 0x7b, 0x64, 0x7c, 0x3f,
    0x38, 0x02, 0x9a, 0xbd, 0xc4, 0x81, 0x52, 0xeb, 0x8f, 0x20, 0x71, 0x59, 0xec, 0xc5, 0xd2, 0x32,
    0xc7, 0xc1, 0x53, 0x7c, 0x79, 0xf4, 0xb7, 0xac, 0x28, 0xff, 0x11, 0x68, 0x2f, 0x21, 0x68, 0x1b,
    0xf6, 0xd6, 0xab, 0xa5, 0x55, 0x03, 0x2b, 0xf6, 0xf9, 0xf0, 0x36, 0xbe, 0xb2, 0xaa, 0xa5, 0xb3,
    0x77, 0x8d, 0x6e, 0xeb, 0xfb, 0xa6, 0xbf, 0x9e, 0xa1, 0x91, 0xbe, 0x4a, 0xb0, 0xca, 0xea, 0x75,
    0x9e, 0x2f, 0x77, 0x3a, 0x1f, 0x90, 0x29, 0xc7, 0x3e, 0xcb, 0x8d, 0x57, 0x35, 0xb9, 0x32, 0x1d,
    0xb0, 0x85, 0xf1, 0xb8, 0xe2, 0xd8, 0x03, 0x8f, 0xe2, 0x94, 0x19, 0x92, 0x54, 0x8c, 0xee, 0x0d,
    0x67, 0xdd, 0x45, 0x47, 0xe1, 0x1d, 0xd6, 0x3a, 0xf9, 0xc9, 0xfc, 0x1c, 0x54, 0x66, 0xfb, 0x68,
    0x4c, 0xf0, 0x09, 0xd7, 0x19, 0x7c, 0x2c, 0xf7, 0x9e, 0x79, 0x2a, 0xb5, 0x01, 0xe6, 0xa8, 0xa1,
    0xca, 0x51, 0x9a, 0xf2, 0xcb, 0x9b, 0x5f, 0x63, 0x67, 0xe9, 0x4c, 0x0d, 0x47, 0x50, 0x24, 0x51,
    0x35, 0x7b, 0xe1, 0xb5,
];

#[cfg(test)]
const ROOT_KSK_2024_KEY_TAG: u16 = 38696;
// Decoded from base64: AwEAAa96jeuknZlaeSrvyAJj6ZHv28hhOKkx3rLGXVaC6rXTsDc449/c...
const ROOT_KSK_2024_PUBLIC_KEY: &[u8] = &[
    0x03, 0x01, 0x00, 0x01, 0xaf, 0x7a, 0x8d, 0xeb, 0xa4, 0x9d, 0x99, 0x5a, 0x79, 0x2a, 0xef, 0xc8,
    0x02, 0x63, 0xe9, 0x91, 0xef, 0xdb, 0xc8, 0x61, 0x38, 0xa9, 0x31, 0xde, 0xb2, 0xc6, 0x5d, 0x56,
    0x82, 0xea, 0xb5, 0xd3, 0xb0, 0x37, 0x38, 0xe3, 0xdf, 0xdc, 0x89, 0xd9, 0x6d, 0xa6, 0x4c, 0x86,
    0xc0, 0x22, 0x4d, 0x9c, 0xe0, 0x25, 0x14, 0xd2, 0x85, 0xda, 0x30, 0x68, 0xb1, 0x90, 0x54, 0xe5,
    0xe7, 0x87, 0xb2, 0x96, 0x90, 0x58, 0xe9, 0x8e, 0x12, 0x56, 0x6c, 0x8c, 0x80, 0x8c, 0x40, 0xc0,
    0xb7, 0x69, 0xe1, 0xdb, 0x1a, 0x24, 0xa1, 0xbd, 0x9b, 0x31, 0xe3, 0x03, 0x18, 0x4a, 0x31, 0xfc,
    0x7b, 0xb5, 0x6b, 0x85, 0xbb, 0xba, 0x8a, 0xbc, 0x02, 0xcd, 0x50, 0x40, 0xa4, 0x44, 0xa3, 0x6d,
    0x47, 0x69, 0x59, 0x69, 0x84, 0x9e, 0x16, 0xad, 0x85, 0x6b, 0xb5, 0x8e, 0x8f, 0xac, 0x88, 0x55,
    0x22, 0x44, 0x00, 0x31, 0x9b, 0xda, 0xb2, 0x24, 0xd8, 0x3f, 0xc0, 0xe6, 0x6a, 0xab, 0x32, 0xff,
    0x74, 0xbf, 0xea, 0xf0, 0xf9, 0x1c, 0x45, 0x4e, 0x68, 0x50, 0xa1, 0x29, 0x52, 0x07, 0xbb, 0xd4,
    0xcd, 0xde, 0x8f, 0x6f, 0xfb, 0x08, 0xfa, 0xa9, 0x75, 0x5c, 0x2e, 0x32, 0x84, 0xef, 0xa0, 0x1f,
    0x99, 0x39, 0x3e, 0x18, 0x78, 0x6c, 0xb1, 0x32, 0xf1, 0xe6, 0x6e, 0xbc, 0x65, 0x17, 0x31, 0x8e,
    0x1c, 0xe8, 0xa3, 0xb7, 0x33, 0x7e, 0xbb, 0x54, 0xd0, 0x35, 0xab, 0x57, 0xd9, 0x70, 0x6e, 0xcd,
    0x93, 0x50, 0xd4, 0xaf, 0xac, 0xd8, 0x25, 0xe4, 0x3c, 0x86, 0x68, 0xee, 0xce, 0x89, 0x81, 0x9c,
    0xaf, 0x68, 0x17, 0xaf, 0x62, 0xdc, 0x4f, 0xbd, 0x82, 0xf0, 0xe3, 0x3f, 0x66, 0x47, 0xb2, 0xb6,
    0xbd, 0xa1, 0x75, 0xf1, 0x46, 0x07, 0xf5, 0x9f, 0x46, 0x35, 0x45, 0x1e, 0x6b, 0x27, 0xdf, 0x28,
    0x2e, 0xf7, 0x3d, 0x87,
];

static TRUST_ANCHORS: LazyLock<Vec<DnsRecord>> = LazyLock::new(|| {
    [ROOT_KSK_2017_PUBLIC_KEY, ROOT_KSK_2024_PUBLIC_KEY]
        .into_iter()
        .map(|public_key| DnsRecord::DNSKEY {
            domain: ".".into(),
            flags: ROOT_KSK_FLAGS,
            protocol: 3,
            algorithm: ROOT_KSK_ALGORITHM,
            public_key: public_key.to_vec(),
            ttl: 172800,
        })
        .collect()
});

struct ValidationCtx<'a> {
    cache: &'a RwLock<DnsCache>,
    root_hints: &'a [std::net::SocketAddr],
    srtt: &'a RwLock<SrttCache>,
    trust_anchors: &'a [DnsRecord],
    stats: &'a Mutex<ValidationStats>,
}

enum RrsetVerdict {
    Verified,
    Bogus,
}

enum RrsigOutcome {
    Verified,
    ChainBogus,
    NoMatchingKey,
}

enum KeyOutcome {
    Verified,
    ChainBogus,
    Skip,
}

/// Top-level validation: verify the DNSSEC chain of trust for a response.
pub async fn validate_response(
    response: &DnsPacket,
    cache: &RwLock<DnsCache>,
    root_hints: &[std::net::SocketAddr],
    srtt: &RwLock<SrttCache>,
) -> (DnssecStatus, ValidationStats) {
    let start = Instant::now();
    let stats = Mutex::new(ValidationStats::default());
    let trust_anchors = &*TRUST_ANCHORS;

    // Extract RRSIGs from all sections
    let all_rrsigs: Vec<&DnsRecord> = response
        .answers
        .iter()
        .chain(response.authorities.iter())
        .chain(response.resources.iter())
        .filter(|r| matches!(r, DnsRecord::RRSIG { .. }))
        .collect();

    if all_rrsigs.is_empty() {
        return finish(start, stats, DnssecStatus::Insecure);
    }

    let ctx = ValidationCtx {
        cache,
        root_hints,
        srtt,
        trust_anchors,
        stats: &stats,
    };

    prefetch_signer_dnskeys(&all_rrsigs, &ctx).await;

    let rrsets = group_rrsets(&response.answers);

    for (name, qtype, rrset) in &rrsets {
        let matching_rrsigs = matching_rrsigs_for(&all_rrsigs, name, *qtype);
        if matching_rrsigs.is_empty() {
            continue; // No RRSIG for this RRset — might be Insecure
        }
        if let RrsetVerdict::Bogus = verify_rrset(name, *qtype, rrset, &matching_rrsigs, &ctx).await
        {
            debug!("dnssec: no valid signature for {} {:?}", name, qtype);
            return finish(start, stats, DnssecStatus::Bogus);
        }
    }

    if rrsets.is_empty() {
        // NXDOMAIN or NODATA — check authority section for NSEC/NSEC3 proofs
        let (qname, qtype_num) = response
            .questions
            .first()
            .map(|q| (q.name.as_str(), q.qtype.to_num()))
            .unwrap_or(("", 0));
        let is_nxdomain = response.header.rescode == crate::header::ResultCode::NXDOMAIN;

        let denial = validate_denial(
            &response.authorities,
            &all_rrsigs,
            qname,
            qtype_num,
            is_nxdomain,
            cache,
        );
        return finish(start, stats, denial);
    }

    finish(start, stats, DnssecStatus::Secure)
}

fn finish(
    start: Instant,
    stats: Mutex<ValidationStats>,
    status: DnssecStatus,
) -> (DnssecStatus, ValidationStats) {
    let mut s = stats.into_inner().unwrap_or_else(|e| e.into_inner());
    s.elapsed_ms = start.elapsed().as_millis() as u64;
    (status, s)
}

async fn prefetch_signer_dnskeys(all_rrsigs: &[&DnsRecord], ctx: &ValidationCtx<'_>) {
    let mut signer_zones: Vec<String> = Vec::new();
    for r in all_rrsigs {
        if let DnsRecord::RRSIG { signer_name, .. } = r {
            let lower = signer_name.to_lowercase();
            if !signer_zones.contains(&lower) {
                signer_zones.push(lower);
            }
        }
    }
    for zone in &signer_zones {
        fetch_dnskeys(zone, ctx.cache, ctx.root_hints, ctx.srtt, ctx.stats).await;
    }
}

fn matching_rrsigs_for<'a>(
    all_rrsigs: &[&'a DnsRecord],
    name: &str,
    qtype: QueryType,
) -> Vec<&'a DnsRecord> {
    all_rrsigs
        .iter()
        .copied()
        .filter(|r| {
            if let DnsRecord::RRSIG {
                domain,
                type_covered,
                ..
            } = r
            {
                domain.eq_ignore_ascii_case(name) && QueryType::from_num(*type_covered) == qtype
            } else {
                false
            }
        })
        .collect()
}

async fn verify_rrset(
    name: &str,
    qtype: QueryType,
    rrset: &[&DnsRecord],
    matching_rrsigs: &[&DnsRecord],
    ctx: &ValidationCtx<'_>,
) -> RrsetVerdict {
    for rrsig in matching_rrsigs {
        match try_verify_rrsig(rrsig, name, qtype, rrset, ctx).await {
            RrsigOutcome::Verified => return RrsetVerdict::Verified,
            RrsigOutcome::ChainBogus => return RrsetVerdict::Bogus,
            RrsigOutcome::NoMatchingKey => continue,
        }
    }
    RrsetVerdict::Bogus
}

async fn try_verify_rrsig(
    rrsig: &DnsRecord,
    name: &str,
    qtype: QueryType,
    rrset: &[&DnsRecord],
    ctx: &ValidationCtx<'_>,
) -> RrsigOutcome {
    let DnsRecord::RRSIG {
        signer_name,
        key_tag,
        algorithm,
        ..
    } = rrsig
    else {
        return RrsigOutcome::NoMatchingKey;
    };

    let dnskey_response =
        fetch_dnskeys(signer_name, ctx.cache, ctx.root_hints, ctx.srtt, ctx.stats).await;
    let dnskeys: Vec<&DnsRecord> = dnskey_response
        .iter()
        .filter(|r| matches!(r, DnsRecord::DNSKEY { .. }))
        .collect();
    if dnskeys.is_empty() {
        trace!("dnssec: no DNSKEY found for signer '{}'", signer_name);
        return RrsigOutcome::NoMatchingKey;
    }

    trace!(
        "dnssec: verifying {} {:?} | signer={} key_tag={} algo={} | {} DNSKEYs available",
        name,
        qtype,
        signer_name,
        key_tag,
        algorithm,
        dnskeys.len()
    );

    for dk in &dnskeys {
        match try_verify_with_key(dk, rrsig, rrset, signer_name, &dnskey_response, ctx).await {
            KeyOutcome::Verified => return RrsigOutcome::Verified,
            KeyOutcome::ChainBogus => return RrsigOutcome::ChainBogus,
            KeyOutcome::Skip => continue,
        }
    }
    RrsigOutcome::NoMatchingKey
}

async fn try_verify_with_key(
    dk: &DnsRecord,
    rrsig: &DnsRecord,
    rrset: &[&DnsRecord],
    signer_name: &str,
    dnskey_response: &[DnsRecord],
    ctx: &ValidationCtx<'_>,
) -> KeyOutcome {
    if !rrsig_verified_by(rrsig, dk, rrset) {
        return KeyOutcome::Skip;
    }

    let chain_status = validate_chain(
        signer_name,
        dnskey_response,
        ctx.cache,
        ctx.root_hints,
        ctx.srtt,
        ctx.trust_anchors,
        0,
        ctx.stats,
    )
    .await;
    trace!(
        "dnssec:   chain_status for '{}': {:?}",
        signer_name,
        chain_status
    );
    match chain_status {
        DnssecStatus::Secure => KeyOutcome::Verified,
        DnssecStatus::Bogus => KeyOutcome::ChainBogus,
        _ => KeyOutcome::Skip,
    }
}

/// Walk the chain of trust from zone DNSKEY up to root trust anchor.
/// `zone_records` contains both DNSKEY and RRSIG records from the DNSKEY response.
#[allow(clippy::too_many_arguments)]
fn validate_chain<'a>(
    zone: &'a str,
    zone_records: &'a [DnsRecord],
    cache: &'a RwLock<DnsCache>,
    root_hints: &'a [std::net::SocketAddr],
    srtt: &'a RwLock<SrttCache>,
    trust_anchors: &'a [DnsRecord],
    depth: u8,
    stats: &'a Mutex<ValidationStats>,
) -> std::pin::Pin<Box<dyn std::future::Future<Output = DnssecStatus> + Send + 'a>> {
    Box::pin(async move {
        let zone_dnskeys: Vec<&DnsRecord> = zone_records
            .iter()
            .filter(|r| matches!(r, DnsRecord::DNSKEY { .. }))
            .collect();

        trace!(
            "dnssec: validate_chain zone='{}' depth={} dnskeys={}",
            zone,
            depth,
            zone_dnskeys.len()
        );
        if depth > MAX_CHAIN_DEPTH {
            return DnssecStatus::Indeterminate;
        }

        // Root base case: a trust anchor must be present *and* have signed the
        // DNSKEY RRset. Present-but-unsigned is an attack signal → Bogus (fail
        // closed), not a fall-through to the Indeterminate KSK-rollover path.
        let anchor_present = zone_dnskeys
            .iter()
            .any(|dk| trust_anchors.iter().any(|ta| same_dnskey(dk, ta)));
        if anchor_present {
            return if verify_rrset_signed(zone_records, QueryType::DNSKEY, trust_anchors) {
                debug!("dnssec: root DNSKEY signed by trust anchor for '{}'", zone);
                DnssecStatus::Secure
            } else {
                debug!(
                    "dnssec: anchor present but RRset not signed by it: '{}'",
                    zone
                );
                DnssecStatus::Bogus
            };
        }

        // Not a trust anchor — need to verify via parent DS
        if zone == "." || zone.is_empty() {
            log::warn!(
                "dnssec: root zone DNSKEY does not match trust anchor — possible KSK rollover. \
                 Update Numa to get the new root trust anchor."
            );
            return DnssecStatus::Indeterminate;
        }
        let parent = parent_zone(zone);
        let ds_response = fetch_ds(zone, cache, root_hints, srtt, stats).await;
        let ds_records: Vec<&DnsRecord> = ds_response
            .iter()
            .filter(|r| matches!(r, DnsRecord::DS { .. }))
            .collect();

        if ds_records.is_empty() {
            debug!("dnssec: no DS for zone '{}' at parent '{}'", zone, parent);
            return DnssecStatus::Insecure;
        }

        // RFC 4035 §5.2: the RRset must be signed by the *same* KSK the DS
        // commits to, so verify only against DS-matched keys (not any KSK).
        let ds_authenticated_ksks: Vec<DnsRecord> = zone_dnskeys
            .iter()
            .copied()
            .filter(|dk| ds_records.iter().any(|ds| verify_ds(ds, dk, zone)))
            .cloned()
            .collect();
        if ds_authenticated_ksks.is_empty() {
            debug!("dnssec: DS digest mismatch for zone '{}'", zone);
            return DnssecStatus::Bogus;
        }
        if !verify_rrset_signed(zone_records, QueryType::DNSKEY, &ds_authenticated_ksks) {
            debug!(
                "dnssec: DNSKEY RRset not signed by a DS-matched KSK: '{}'",
                zone
            );
            return DnssecStatus::Bogus;
        }

        // Walk up: validate the parent's DNSKEY
        trace!("dnssec: fetching parent DNSKEY for '{}'", parent);
        let parent_records = fetch_dnskeys(&parent, cache, root_hints, srtt, stats).await;
        if parent_records.is_empty() {
            debug!("dnssec: no parent DNSKEY for '{}' — Indeterminate", parent);
            return DnssecStatus::Indeterminate;
        }

        let parent_status = validate_chain(
            &parent,
            &parent_records,
            cache,
            root_hints,
            srtt,
            trust_anchors,
            depth + 1,
            stats,
        )
        .await;
        if parent_status != DnssecStatus::Secure {
            return parent_status;
        }

        // The DS RRset must itself be signed by the (now-validated) parent.
        // A digest match alone lets a forged DS endorse an attacker's KSK.
        if !verify_rrset_signed(&ds_response, QueryType::DS, &parent_records) {
            debug!("dnssec: DS RRset for '{}' not signed by parent", zone);
            return DnssecStatus::Bogus;
        }

        DnssecStatus::Secure
    })
}

/// Same DNSKEY identity (algorithm + public key); flags/protocol are not part
/// of key identity, so a tag comparison would be redundant.
fn same_dnskey(a: &DnsRecord, b: &DnsRecord) -> bool {
    matches!(
        (a, b),
        (
            DnsRecord::DNSKEY { algorithm: aa, public_key: ak, .. },
            DnsRecord::DNSKEY { algorithm: ba, public_key: bk, .. },
        ) if aa == ba && ak == bk
    )
}

/// The chain's single signature gate: does `dk` make a time-valid RRSIG `rrsig`
/// over `rrset`? Matches algorithm + key tag, checks validity window, then
/// verifies the signature over the canonical RRset bytes.
fn rrsig_verified_by(rrsig: &DnsRecord, dk: &DnsRecord, rrset: &[&DnsRecord]) -> bool {
    let (
        DnsRecord::RRSIG {
            algorithm,
            key_tag,
            expiration,
            inception,
            signature,
            ..
        },
        DnsRecord::DNSKEY {
            flags,
            protocol,
            algorithm: dk_algo,
            public_key,
            ..
        },
    ) = (rrsig, dk)
    else {
        return false;
    };
    dk_algo == algorithm
        && compute_key_tag(*flags, *protocol, *dk_algo, public_key) == *key_tag
        && is_rrsig_time_valid(*expiration, *inception)
        && verify_signature(
            *algorithm,
            public_key,
            &build_signed_data(rrsig, rrset),
            signature,
        )
}

/// Does the `rrset_type` RRset in `records` carry an RRSIG made by one of
/// `signing_keys` (which the caller must already trust)? Callers pass the
/// specific authenticated key(s) — the trust anchor for the root DNSKEY, or the
/// DS-matched KSKs for a child — so a signer the chain never committed to cannot
/// satisfy the check.
fn verify_rrset_signed(
    records: &[DnsRecord],
    rrset_type: QueryType,
    signing_keys: &[DnsRecord],
) -> bool {
    let rrset: Vec<&DnsRecord> = records
        .iter()
        .filter(|r| r.query_type() == rrset_type)
        .collect();
    if rrset.is_empty() {
        return false;
    }
    records.iter().any(|r| {
        matches!(r, DnsRecord::RRSIG { type_covered, .. }
            if QueryType::from_num(*type_covered) == rrset_type)
            && signing_keys
                .iter()
                .any(|dk| rrsig_verified_by(r, dk, &rrset))
    })
}

// -- Fetching helpers --

/// Fetch DNSKEY response for a zone. Returns all answer records (DNSKEY + RRSIG)
/// so the caller can verify the DNSKEY RRset is self-signed.
async fn fetch_dnskeys(
    zone: &str,
    cache: &RwLock<DnsCache>,
    root_hints: &[std::net::SocketAddr],
    srtt: &RwLock<SrttCache>,
    stats: &Mutex<ValidationStats>,
) -> Vec<DnsRecord> {
    if let Some(pkt) = cache.read().unwrap().lookup(zone, QueryType::DNSKEY) {
        stats.lock().unwrap().dnskey_cache_hits += 1;
        trace!(
            "dnssec: fetch_dnskeys('{}') cache hit — {} records",
            zone,
            pkt.answers.len()
        );
        return pkt.answers;
    }

    trace!("dnssec: fetch_dnskeys('{}') cache miss — resolving", zone);
    stats.lock().unwrap().dnskey_fetches += 1;
    if let Ok(pkt) = crate::recursive::resolve_iterative(
        zone,
        QueryType::DNSKEY,
        cache,
        root_hints,
        srtt,
        0,
        0,
        &std::sync::atomic::AtomicUsize::new(0),
    )
    .await
    {
        cache.write().unwrap().insert(zone, QueryType::DNSKEY, &pkt);
        return pkt.answers;
    }

    Vec::new()
}

async fn fetch_ds(
    child: &str,
    cache: &RwLock<DnsCache>,
    root_hints: &[std::net::SocketAddr],
    srtt: &RwLock<SrttCache>,
    stats: &Mutex<ValidationStats>,
) -> Vec<DnsRecord> {
    // Returns the full answer set (DS records *and* their RRSIGs); the caller
    // both digest-matches the DS and verifies the parent's signature over it.
    if let Some(pkt) = cache.read().unwrap().lookup(child, QueryType::DS) {
        stats.lock().unwrap().ds_cache_hits += 1;
        return pkt.answers;
    }

    stats.lock().unwrap().ds_fetches += 1;
    if let Ok(pkt) = crate::recursive::resolve_iterative(
        child,
        QueryType::DS,
        cache,
        root_hints,
        srtt,
        0,
        0,
        &std::sync::atomic::AtomicUsize::new(0),
    )
    .await
    {
        cache.write().unwrap().insert(child, QueryType::DS, &pkt);
        return pkt.answers;
    }

    Vec::new()
}

// -- Crypto primitives --

pub fn compute_key_tag(flags: u16, protocol: u8, algorithm: u8, public_key: &[u8]) -> u16 {
    // RFC 4034 Appendix B: sum all 16-bit words of DNSKEY RDATA
    let mut rdata = Vec::with_capacity(4 + public_key.len());
    rdata.push((flags >> 8) as u8);
    rdata.push((flags & 0xFF) as u8);
    rdata.push(protocol);
    rdata.push(algorithm);
    rdata.extend_from_slice(public_key);

    let mut ac: u32 = 0;
    for (i, &byte) in rdata.iter().enumerate() {
        if i % 2 == 0 {
            ac += (byte as u32) << 8;
        } else {
            ac += byte as u32;
        }
    }
    ac += (ac >> 16) & 0xFFFF;
    (ac & 0xFFFF) as u16
}

pub fn verify_signature(algorithm: u8, public_key: &[u8], signed_data: &[u8], sig: &[u8]) -> bool {
    match algorithm {
        8 => verify_rsa_sha256(public_key, signed_data, sig),
        13 => verify_ecdsa_p256(public_key, signed_data, sig),
        15 => verify_ed25519(public_key, signed_data, sig),
        _ => {
            debug!("dnssec: unsupported algorithm {}", algorithm);
            false
        }
    }
}

fn verify_rsa_sha256(public_key: &[u8], signed_data: &[u8], sig: &[u8]) -> bool {
    let der = match rsa_dnskey_to_der(public_key) {
        Some(d) => d,
        None => return false,
    };
    let key = signature::UnparsedPublicKey::new(&signature::RSA_PKCS1_2048_8192_SHA256, &der);
    key.verify(signed_data, sig).is_ok()
}

fn verify_ecdsa_p256(public_key: &[u8], signed_data: &[u8], sig: &[u8]) -> bool {
    if public_key.len() != 64 || sig.len() != 64 {
        return false;
    }
    // Ring expects uncompressed point: 0x04 + x(32) + y(32)
    let mut uncompressed = Vec::with_capacity(65);
    uncompressed.push(0x04);
    uncompressed.extend_from_slice(public_key);

    let key = signature::UnparsedPublicKey::new(&signature::ECDSA_P256_SHA256_FIXED, &uncompressed);
    key.verify(signed_data, sig).is_ok()
}

fn verify_ed25519(public_key: &[u8], signed_data: &[u8], sig: &[u8]) -> bool {
    if public_key.len() != 32 || sig.len() != 64 {
        return false;
    }
    let key = signature::UnparsedPublicKey::new(&signature::ED25519, public_key);
    key.verify(signed_data, sig).is_ok()
}

/// Convert RFC 3110 RSA public key to DER-encoded RSAPublicKey (PKCS#1)
fn rsa_dnskey_to_der(public_key: &[u8]) -> Option<Vec<u8>> {
    if public_key.is_empty() {
        return None;
    }

    // RFC 3110: first byte is exponent length (if non-zero) or 0 followed by 2-byte length
    let (exp_len, exp_start) = if public_key[0] == 0 {
        if public_key.len() < 3 {
            return None;
        }
        let len = u16::from_be_bytes([public_key[1], public_key[2]]) as usize;
        (len, 3)
    } else {
        (public_key[0] as usize, 1)
    };

    if public_key.len() < exp_start + exp_len {
        return None;
    }

    let exponent = &public_key[exp_start..exp_start + exp_len];
    let modulus = &public_key[exp_start + exp_len..];

    if modulus.is_empty() {
        return None;
    }

    // Build ASN.1 DER: SEQUENCE { INTEGER modulus, INTEGER exponent }
    let mod_der = asn1_integer(modulus);
    let exp_der = asn1_integer(exponent);

    let seq_content_len = mod_der.len() + exp_der.len();
    let mut der = Vec::with_capacity(4 + seq_content_len);
    der.push(0x30); // SEQUENCE tag
    der.extend(asn1_length(seq_content_len));
    der.extend(&mod_der);
    der.extend(&exp_der);

    Some(der)
}

fn asn1_integer(bytes: &[u8]) -> Vec<u8> {
    // Strip leading zeros but keep at least one byte
    let stripped = match bytes.iter().position(|&b| b != 0) {
        Some(pos) => &bytes[pos..],
        None => &[0],
    };

    // Add leading zero if high bit set (to keep it positive)
    let needs_pad = stripped[0] & 0x80 != 0;
    let len = stripped.len() + if needs_pad { 1 } else { 0 };

    let mut result = Vec::with_capacity(2 + len);
    result.push(0x02); // INTEGER tag
    result.extend(asn1_length(len));
    if needs_pad {
        result.push(0x00);
    }
    result.extend(stripped);
    result
}

fn asn1_length(len: usize) -> Vec<u8> {
    if len < 128 {
        vec![len as u8]
    } else if len < 256 {
        vec![0x81, len as u8]
    } else {
        vec![0x82, (len >> 8) as u8, (len & 0xFF) as u8]
    }
}

pub fn verify_ds(ds: &DnsRecord, dnskey: &DnsRecord, owner: &str) -> bool {
    if let (
        DnsRecord::DS {
            key_tag: ds_tag,
            algorithm: ds_algo,
            digest_type,
            digest,
            ..
        },
        DnsRecord::DNSKEY {
            flags,
            protocol,
            algorithm: dk_algo,
            public_key,
            ..
        },
    ) = (ds, dnskey)
    {
        // Key tag and algorithm must match
        let computed_tag = compute_key_tag(*flags, *protocol, *dk_algo, public_key);
        if computed_tag != *ds_tag || dk_algo != ds_algo {
            return false;
        }

        // Compute digest: SHA-256(owner_wire + DNSKEY_RDATA)
        let owner_wire = name_to_wire(owner);
        let mut dnskey_rdata = Vec::with_capacity(4 + public_key.len());
        dnskey_rdata.push((*flags >> 8) as u8);
        dnskey_rdata.push((*flags & 0xFF) as u8);
        dnskey_rdata.push(*protocol);
        dnskey_rdata.push(*dk_algo);
        dnskey_rdata.extend_from_slice(public_key);

        let mut input = Vec::with_capacity(owner_wire.len() + dnskey_rdata.len());
        input.extend(&owner_wire);
        input.extend(&dnskey_rdata);

        match *digest_type {
            2 => {
                // SHA-256
                let computed = digest::digest(&digest::SHA256, &input);
                computed.as_ref() == digest.as_slice()
            }
            4 => {
                // SHA-384
                let computed = digest::digest(&digest::SHA384, &input);
                computed.as_ref() == digest.as_slice()
            }
            _ => false,
        }
    } else {
        false
    }
}

// -- Canonical wire format --

/// Encode a DNS name in canonical wire form per RFC 4034 §6.2:
/// uncompressed, with ASCII letters lowercased.
///
/// Lowercasing happens *after* escape resolution because `\065` yields
/// `'A'`, which canonical form must convert to `'a'`.
pub fn name_to_wire(name: &str) -> Vec<u8> {
    let mut buf = BytePacketBuffer::new();
    buf.write_qname(name)
        .expect("name_to_wire: input must parse as a valid DNS name");
    let mut wire = buf.filled().to_vec();
    downcase_wire_name(&mut wire, 0);
    wire
}

pub fn build_signed_data(rrsig: &DnsRecord, rrset: &[&DnsRecord]) -> Vec<u8> {
    let mut data = Vec::with_capacity(256);

    if let DnsRecord::RRSIG {
        type_covered,
        algorithm,
        labels,
        original_ttl,
        expiration,
        inception,
        key_tag,
        signer_name,
        ..
    } = rrsig
    {
        // RRSIG RDATA (without signature)
        data.extend(&type_covered.to_be_bytes());
        data.push(*algorithm);
        data.push(*labels);
        data.extend(&original_ttl.to_be_bytes());
        data.extend(&expiration.to_be_bytes());
        data.extend(&inception.to_be_bytes());
        data.extend(&key_tag.to_be_bytes());
        data.extend(name_to_wire(signer_name));

        // Sort RRset records by canonical wire form
        let mut canonical_records: Vec<Vec<u8>> = rrset
            .iter()
            .map(|r| record_to_canonical_wire(r, *original_ttl))
            .collect();
        canonical_records.sort();

        for rec_wire in &canonical_records {
            data.extend(rec_wire);
        }
    }

    data
}

fn record_to_canonical_wire(record: &DnsRecord, original_ttl: u32) -> Vec<u8> {
    let mut wire = Vec::with_capacity(128);

    // Owner name (lowercased, uncompressed)
    wire.extend(name_to_wire(record.domain()));

    // Type
    wire.extend(&record.query_type().to_num().to_be_bytes());

    // Class IN
    wire.extend(&1u16.to_be_bytes());

    // Original TTL (from RRSIG, not the record's current TTL)
    wire.extend(&original_ttl.to_be_bytes());

    // RDATA — write the record to a temporary buffer to get the canonical RDATA
    let rdata = record_rdata_canonical(record);
    wire.extend(&(rdata.len() as u16).to_be_bytes());
    wire.extend(&rdata);

    wire
}

fn record_rdata_canonical(record: &DnsRecord) -> Vec<u8> {
    match record {
        DnsRecord::A { addr, .. } => addr.octets().to_vec(),
        DnsRecord::AAAA { addr, .. } => addr.octets().to_vec(),
        DnsRecord::NS { host, .. }
        | DnsRecord::CNAME { host, .. }
        | DnsRecord::PTR { host, .. } => name_to_wire(host),
        DnsRecord::MX { priority, host, .. } => {
            let mut rdata = Vec::with_capacity(2 + host.len() + 2);
            rdata.extend(&priority.to_be_bytes());
            rdata.extend(name_to_wire(host));
            rdata
        }
        DnsRecord::DNSKEY {
            flags,
            protocol,
            algorithm,
            public_key,
            ..
        } => {
            let mut rdata = Vec::with_capacity(4 + public_key.len());
            rdata.extend(&flags.to_be_bytes());
            rdata.push(*protocol);
            rdata.push(*algorithm);
            rdata.extend(public_key);
            rdata
        }
        DnsRecord::DS {
            key_tag,
            algorithm,
            digest_type,
            digest,
            ..
        } => {
            let mut rdata = Vec::with_capacity(4 + digest.len());
            rdata.extend(&key_tag.to_be_bytes());
            rdata.push(*algorithm);
            rdata.push(*digest_type);
            rdata.extend(digest);
            rdata
        }
        DnsRecord::NSEC {
            next_domain,
            type_bitmap,
            ..
        } => {
            let wire = name_to_wire(next_domain);
            let mut rdata = Vec::with_capacity(wire.len() + type_bitmap.len());
            rdata.extend(&wire);
            rdata.extend(type_bitmap);
            rdata
        }
        DnsRecord::NSEC3 {
            hash_algorithm,
            flags,
            iterations,
            salt,
            next_hashed_owner,
            type_bitmap,
            ..
        } => {
            let mut rdata =
                Vec::with_capacity(6 + salt.len() + next_hashed_owner.len() + type_bitmap.len());
            rdata.push(*hash_algorithm);
            rdata.push(*flags);
            rdata.extend(&iterations.to_be_bytes());
            rdata.push(salt.len() as u8);
            rdata.extend(salt);
            rdata.push(next_hashed_owner.len() as u8);
            rdata.extend(next_hashed_owner);
            rdata.extend(type_bitmap);
            rdata
        }
        DnsRecord::SOA {
            mname,
            rname,
            serial,
            refresh,
            retry,
            expire,
            minimum,
            ..
        } => {
            let mname_wire = name_to_wire(mname);
            let rname_wire = name_to_wire(rname);
            let mut rdata = Vec::with_capacity(mname_wire.len() + rname_wire.len() + 20);
            rdata.extend(&mname_wire);
            rdata.extend(&rname_wire);
            rdata.extend(&serial.to_be_bytes());
            rdata.extend(&refresh.to_be_bytes());
            rdata.extend(&retry.to_be_bytes());
            rdata.extend(&expire.to_be_bytes());
            rdata.extend(&minimum.to_be_bytes());
            rdata
        }
        DnsRecord::UNKNOWN { data, .. } => canonical_unknown_rdata(record.query_type(), data),
        DnsRecord::RRSIG { .. } => Vec::new(),
    }
}

// RFC 4034 §6.2: SRV and NAPTR embed a domain name that is downcased in
// canonical form; every other type carried as UNKNOWN (TXT, HTTPS, SVCB, LOC)
// signs its rdata verbatim.
fn canonical_unknown_rdata(qtype: QueryType, data: &[u8]) -> Vec<u8> {
    let name_offset = match qtype {
        QueryType::SRV => Some(6), // priority + weight + port
        QueryType::NAPTR => naptr_replacement_offset(data),
        _ => None,
    };
    let mut rdata = data.to_vec();
    if let Some(off) = name_offset {
        downcase_wire_name(&mut rdata, off);
    }
    rdata
}

// order + preference (4 bytes), then three character-strings (flags, services,
// regexp), then the replacement name.
fn naptr_replacement_offset(data: &[u8]) -> Option<usize> {
    let mut off = 4usize;
    for _ in 0..3 {
        off += 1 + *data.get(off)? as usize;
    }
    Some(off)
}

fn downcase_wire_name(buf: &mut [u8], mut off: usize) {
    while let Some(&len) = buf.get(off) {
        if len == 0 || len > 63 {
            return; // root, compression pointer, or malformed — stop
        }
        let end = (off + 1 + len as usize).min(buf.len());
        buf[off + 1..end].make_ascii_lowercase();
        off = end;
    }
}

fn group_rrsets(records: &[DnsRecord]) -> Vec<(String, QueryType, Vec<&DnsRecord>)> {
    let mut groups: Vec<(String, QueryType, Vec<&DnsRecord>)> = Vec::new();
    for record in records {
        if matches!(record, DnsRecord::RRSIG { .. }) {
            continue;
        }
        let domain = record.domain().to_lowercase();
        let qtype = record.query_type();
        if let Some(group) = groups
            .iter_mut()
            .find(|(d, t, _)| *d == domain && *t == qtype)
        {
            group.2.push(record);
        } else {
            groups.push((domain, qtype, vec![record]));
        }
    }
    groups
}

fn is_rrsig_time_valid(expiration: u32, inception: u32) -> bool {
    const FUDGE: u32 = 300; // 5-minute clock skew tolerance (BIND uses 300s)
    let now = SystemTime::now()
        .duration_since(UNIX_EPOCH)
        .unwrap_or_default()
        .as_secs() as u32;
    // RFC 4034 §3.1.5: use serial number arithmetic for wrap-safe comparison
    let inception_ok = now.wrapping_sub(inception) < (1u32 << 31);
    let expiration_ok = expiration.wrapping_sub(now) < (1u32 << 31);
    (inception_ok || now.wrapping_add(FUDGE) >= inception) && expiration_ok
}

// -- NSEC/NSEC3 denial of existence --

pub fn type_bitmap_contains(bitmap: &[u8], qtype: u16) -> bool {
    let target_window = (qtype / 256) as u8;
    let target_bit = (qtype % 256) as u8;
    let byte_offset = (target_bit / 8) as usize;
    let bit_mask = 0x80 >> (target_bit % 8);

    let mut pos = 0;
    while pos + 2 <= bitmap.len() {
        let window = bitmap[pos];
        let bmap_len = bitmap[pos + 1] as usize;
        if pos + 2 + bmap_len > bitmap.len() {
            break;
        }
        if window == target_window && byte_offset < bmap_len {
            return bitmap[pos + 2 + byte_offset] & bit_mask != 0;
        }
        pos += 2 + bmap_len;
    }
    false
}

fn canonical_dns_name_order(a: &str, b: &str) -> std::cmp::Ordering {
    // RFC 4034 §6.1: compare labels right-to-left, case-insensitive.
    // Two-phase: zip compares common labels from the root, then label count
    // breaks ties (shorter name sorts first, e.g., "com" < "a.com").
    let a_iter = a.rsplit('.').filter(|l| !l.is_empty());
    let b_iter = b.rsplit('.').filter(|l| !l.is_empty());

    for (la, lb) in a_iter.zip(b_iter) {
        match la
            .as_bytes()
            .iter()
            .map(|b| b.to_ascii_lowercase())
            .cmp(lb.as_bytes().iter().map(|b| b.to_ascii_lowercase()))
        {
            std::cmp::Ordering::Equal => continue,
            other => return other,
        }
    }

    let a_count = a.split('.').filter(|l| !l.is_empty()).count();
    let b_count = b.split('.').filter(|l| !l.is_empty()).count();
    a_count.cmp(&b_count)
}

fn nsec_covers_name(owner: &str, next: &str, qname: &str) -> bool {
    use std::cmp::Ordering;

    let on = canonical_dns_name_order(owner, next);
    let qo = canonical_dns_name_order(qname, owner);
    let qn = canonical_dns_name_order(qname, next);
    if matches!(on, Ordering::Greater | Ordering::Equal) {
        qo == Ordering::Greater || qn == Ordering::Less
    } else {
        qo == Ordering::Greater && qn == Ordering::Less
    }
}

/// RFC 4035 §5.4: compute the closest encloser, then derive the wildcard name.
fn closest_encloser(qname: &str, zone_nsecs: &[&DnsRecord]) -> Option<String> {
    let labels: Vec<&str> = qname.split('.').filter(|l| !l.is_empty()).collect();
    // Walk from longest candidate down: qname itself, then parent, then grandparent...
    for i in 0..labels.len() {
        let candidate: String = labels[i..].join(".");
        // Closest encloser must match an NSEC owner exactly
        let is_owner = zone_nsecs.iter().any(|r| {
            if let DnsRecord::NSEC { domain, .. } = r {
                domain.eq_ignore_ascii_case(&candidate)
            } else {
                false
            }
        });
        if is_owner {
            return Some(candidate);
        }
    }
    None
}

fn nsec_proves_nodata(owner: &str, qname: &str, bitmap: &[u8], qtype: u16) -> bool {
    owner.eq_ignore_ascii_case(qname)
        && !type_bitmap_contains(bitmap, qtype)
        && !type_bitmap_contains(bitmap, QueryType::CNAME.to_num())
}

/// RFC 9276 recommends 0 iterations; we reject anything above this as a DoS vector.
const MAX_NSEC3_ITERATIONS: u16 = 500;

fn nsec3_hash(name: &str, algorithm: u8, iterations: u16, salt: &[u8]) -> Option<Vec<u8>> {
    if algorithm != 1 {
        return None; // Only SHA-1 (algorithm 1) defined
    }
    if iterations > MAX_NSEC3_ITERATIONS {
        return None;
    }

    let wire_name = name_to_wire(name);
    let mut buf = Vec::with_capacity(wire_name.len() + salt.len());
    buf.extend(&wire_name);
    buf.extend(salt);

    let mut hash = digest::digest(&digest::SHA1_FOR_LEGACY_USE_ONLY, &buf);

    for _ in 0..iterations {
        buf.clear();
        buf.extend(hash.as_ref());
        buf.extend(salt);
        hash = digest::digest(&digest::SHA1_FOR_LEGACY_USE_ONLY, &buf);
    }

    Some(hash.as_ref().to_vec())
}

fn base32hex_decode(input: &str) -> Option<Vec<u8>> {
    // Lookup table: ASCII byte -> base32hex value (0xFF = invalid)
    const LUT: [u8; 128] = {
        let mut t = [0xFFu8; 128];
        // 0-9 -> 0-9
        t[b'0' as usize] = 0;
        t[b'1' as usize] = 1;
        t[b'2' as usize] = 2;
        t[b'3' as usize] = 3;
        t[b'4' as usize] = 4;
        t[b'5' as usize] = 5;
        t[b'6' as usize] = 6;
        t[b'7' as usize] = 7;
        t[b'8' as usize] = 8;
        t[b'9' as usize] = 9;
        // A-V -> 10-31 (uppercase)
        t[b'A' as usize] = 10;
        t[b'B' as usize] = 11;
        t[b'C' as usize] = 12;
        t[b'D' as usize] = 13;
        t[b'E' as usize] = 14;
        t[b'F' as usize] = 15;
        t[b'G' as usize] = 16;
        t[b'H' as usize] = 17;
        t[b'I' as usize] = 18;
        t[b'J' as usize] = 19;
        t[b'K' as usize] = 20;
        t[b'L' as usize] = 21;
        t[b'M' as usize] = 22;
        t[b'N' as usize] = 23;
        t[b'O' as usize] = 24;
        t[b'P' as usize] = 25;
        t[b'Q' as usize] = 26;
        t[b'R' as usize] = 27;
        t[b'S' as usize] = 28;
        t[b'T' as usize] = 29;
        t[b'U' as usize] = 30;
        t[b'V' as usize] = 31;
        // a-v -> 10-31 (lowercase)
        t[b'a' as usize] = 10;
        t[b'b' as usize] = 11;
        t[b'c' as usize] = 12;
        t[b'd' as usize] = 13;
        t[b'e' as usize] = 14;
        t[b'f' as usize] = 15;
        t[b'g' as usize] = 16;
        t[b'h' as usize] = 17;
        t[b'i' as usize] = 18;
        t[b'j' as usize] = 19;
        t[b'k' as usize] = 20;
        t[b'l' as usize] = 21;
        t[b'm' as usize] = 22;
        t[b'n' as usize] = 23;
        t[b'o' as usize] = 24;
        t[b'p' as usize] = 25;
        t[b'q' as usize] = 26;
        t[b'r' as usize] = 27;
        t[b's' as usize] = 28;
        t[b't' as usize] = 29;
        t[b'u' as usize] = 30;
        t[b'v' as usize] = 31;
        t
    };

    let mut bits = 0u64;
    let mut bit_count = 0u8;
    let mut output = Vec::with_capacity(input.len() * 5 / 8);

    for &ch in input.as_bytes() {
        if ch == b'=' {
            break;
        }
        if ch >= 128 {
            return None;
        }
        let val = LUT[ch as usize];
        if val == 0xFF {
            return None;
        }
        bits = (bits << 5) | val as u64;
        bit_count += 5;
        if bit_count >= 8 {
            bit_count -= 8;
            output.push((bits >> bit_count) as u8);
            bits &= (1 << bit_count) - 1;
        }
    }
    Some(output)
}

fn nsec3_owner_hash(domain: &str) -> Option<Vec<u8>> {
    let first_label = domain.split('.').next()?;
    base32hex_decode(first_label)
}

fn nsec3_hash_in_range(owner_hash: &[u8], next_hash: &[u8], target_hash: &[u8]) -> bool {
    if owner_hash < next_hash {
        target_hash > owner_hash && target_hash < next_hash
    } else {
        // Wrap-around
        target_hash > owner_hash || target_hash < next_hash
    }
}

/// Check if any pre-decoded NSEC3 record's range covers the target hash.
fn nsec3_any_covers(decoded: &[(Vec<u8>, &DnsRecord)], target: &[u8]) -> bool {
    decoded.iter().any(|(oh, r)| {
        if let DnsRecord::NSEC3 {
            next_hashed_owner, ..
        } = r
        {
            nsec3_hash_in_range(oh, next_hashed_owner, target)
        } else {
            false
        }
    })
}

/// Verify that authority-section NSEC/NSEC3 RRSIGs are cryptographically valid.
fn verify_authority_rrsigs(
    authorities: &[DnsRecord],
    all_rrsigs: &[&DnsRecord],
    denial_type: QueryType,
    cache: &RwLock<DnsCache>,
) -> bool {
    // Group authority denial records into RRsets
    let denial_records: Vec<DnsRecord> = authorities
        .iter()
        .filter(|r| r.query_type() == denial_type)
        .cloned()
        .collect();
    let denial_rrsets = group_rrsets(&denial_records);

    for (name, qtype, rrset) in &denial_rrsets {
        let covering_rrsig = all_rrsigs.iter().find(|r| {
            if let DnsRecord::RRSIG {
                domain,
                type_covered,
                ..
            } = r
            {
                domain.eq_ignore_ascii_case(name) && QueryType::from_num(*type_covered) == *qtype
            } else {
                false
            }
        });

        let rrsig = match covering_rrsig {
            Some(r) => r,
            None => return false,
        };

        if let DnsRecord::RRSIG {
            signer_name,
            key_tag,
            algorithm,
            signature,
            expiration,
            inception,
            ..
        } = rrsig
        {
            if !is_rrsig_time_valid(*expiration, *inception) {
                return false;
            }

            // Look up signer DNSKEY in cache
            let dnskeys = match cache.read().unwrap().lookup(signer_name, QueryType::DNSKEY) {
                Some(pkt) => pkt.answers,
                None => return false,
            };

            let signed_data = build_signed_data(rrsig, rrset);
            let verified = dnskeys.iter().any(|dk| {
                if let DnsRecord::DNSKEY {
                    flags,
                    protocol,
                    algorithm: dk_algo,
                    public_key,
                    ..
                } = dk
                {
                    if dk_algo != algorithm {
                        return false;
                    }
                    let tag = compute_key_tag(*flags, *protocol, *dk_algo, public_key);
                    if tag != *key_tag {
                        return false;
                    }
                    verify_signature(*algorithm, public_key, &signed_data, signature)
                } else {
                    false
                }
            });

            if !verified {
                return false;
            }
        }
    }

    !denial_rrsets.is_empty()
}

/// Validate denial of existence using NSEC or NSEC3 records from authority section.
fn validate_denial(
    authorities: &[DnsRecord],
    all_rrsigs: &[&DnsRecord],
    qname: &str,
    qtype: u16,
    is_nxdomain: bool,
    cache: &RwLock<DnsCache>,
) -> DnssecStatus {
    // Try NSEC first
    let nsecs: Vec<&DnsRecord> = authorities
        .iter()
        .filter(|r| matches!(r, DnsRecord::NSEC { .. }))
        .collect();

    if !nsecs.is_empty() {
        if !verify_authority_rrsigs(authorities, all_rrsigs, QueryType::NSEC, cache) {
            debug!("dnssec: NSEC authority RRSIGs failed verification");
            return DnssecStatus::Indeterminate;
        }

        if is_nxdomain {
            // RFC 4035 §5.4: need (1) NSEC covering the name gap AND (2) NSEC proving
            // no wildcard at *.closest_encloser
            let name_covered = nsecs.iter().any(|r| {
                if let DnsRecord::NSEC {
                    domain,
                    next_domain,
                    ..
                } = r
                {
                    nsec_covers_name(domain, next_domain, qname)
                } else {
                    false
                }
            });

            let wildcard_denied = if let Some(ce) = closest_encloser(qname, &nsecs) {
                let wildcard = format!("*.{}", ce);
                // Wildcard must either be covered by a gap or matched with the type absent
                nsecs.iter().any(|r| {
                    if let DnsRecord::NSEC {
                        domain,
                        next_domain,
                        ..
                    } = r
                    {
                        nsec_covers_name(domain, next_domain, &wildcard)
                            || domain.eq_ignore_ascii_case(&wildcard)
                    } else {
                        false
                    }
                })
            } else {
                // No closest encloser found — can't prove wildcard absence,
                // but some zones don't use wildcards; accept name coverage alone
                true
            };

            if name_covered && wildcard_denied {
                debug!("dnssec: NSEC proves NXDOMAIN for '{}'", qname);
                return DnssecStatus::Secure;
            }
        } else {
            // NODATA — name exists but type doesn't
            let nodata_proven = nsecs.iter().any(|r| {
                if let DnsRecord::NSEC {
                    domain,
                    type_bitmap,
                    ..
                } = r
                {
                    nsec_proves_nodata(domain, qname, type_bitmap, qtype)
                } else {
                    false
                }
            });
            if nodata_proven {
                debug!("dnssec: NSEC proves NODATA for '{}' type {}", qname, qtype);
                return DnssecStatus::Secure;
            }
        }

        return DnssecStatus::Bogus;
    }

    // Try NSEC3
    let nsec3s: Vec<&DnsRecord> = authorities
        .iter()
        .filter(|r| matches!(r, DnsRecord::NSEC3 { .. }))
        .collect();

    if !nsec3s.is_empty() {
        if !verify_authority_rrsigs(authorities, all_rrsigs, QueryType::NSEC3, cache) {
            debug!("dnssec: NSEC3 authority RRSIGs failed verification");
            return DnssecStatus::Indeterminate;
        }

        // Get hash params from first NSEC3
        if let Some(DnsRecord::NSEC3 {
            hash_algorithm,
            iterations,
            salt,
            ..
        }) = nsec3s.first().copied()
        {
            let qname_hash = match nsec3_hash(qname, *hash_algorithm, *iterations, salt) {
                Some(h) => h,
                None => return DnssecStatus::Indeterminate,
            };

            // Pre-decode all NSEC3 owner hashes once
            let decoded: Vec<(Vec<u8>, &DnsRecord)> = nsec3s
                .iter()
                .filter_map(|r| {
                    if let DnsRecord::NSEC3 { domain, .. } = r {
                        match nsec3_owner_hash(domain) {
                            Some(h) => Some((h, *r)),
                            None => {
                                trace!("dnssec: malformed NSEC3 owner '{}' — skipping", domain);
                                None
                            }
                        }
                    } else {
                        None
                    }
                })
                .collect();

            if is_nxdomain {
                // RFC 5155 §8.4: need (1) closest encloser match, (2) next closer covered,
                // (3) wildcard at closest encloser denied
                let labels: Vec<&str> = qname.split('.').filter(|l| !l.is_empty()).collect();

                // Pre-compute hashes for all ancestor names + wildcards
                let mut ancestor_hashes: Vec<Option<Vec<u8>>> = Vec::with_capacity(labels.len());
                for i in 0..labels.len() {
                    let name: String = labels[i..].join(".");
                    ancestor_hashes.push(nsec3_hash(&name, *hash_algorithm, *iterations, salt));
                }

                let mut proven = false;
                for i in 1..labels.len() {
                    let ce_hash = match &ancestor_hashes[i] {
                        Some(h) => h,
                        None => continue,
                    };

                    // (1) Closest encloser: exact hash match
                    if !decoded.iter().any(|(oh, _)| oh == ce_hash) {
                        continue;
                    }

                    // (2) Next closer name covered by range
                    // ancestor_hashes[i-1] is the hash of labels[i-1..] (one label prepended to CE)
                    let nc_hash = match &ancestor_hashes[i - 1] {
                        Some(h) => h,
                        None => continue,
                    };
                    if !nsec3_any_covers(&decoded, nc_hash) {
                        continue;
                    }

                    // (3) Wildcard at closest encloser denied
                    let wildcard = format!("*.{}", labels[i..].join("."));
                    let wc_hash = match nsec3_hash(&wildcard, *hash_algorithm, *iterations, salt) {
                        Some(h) => h,
                        None => continue,
                    };
                    if nsec3_any_covers(&decoded, &wc_hash) {
                        proven = true;
                        break;
                    }
                }

                if proven {
                    debug!("dnssec: NSEC3 proves NXDOMAIN for '{}'", qname);
                    return DnssecStatus::Secure;
                }
            } else {
                // NODATA — exact hash match with type not in bitmap
                let nodata = decoded.iter().any(|(oh, r)| {
                    if let DnsRecord::NSEC3 { type_bitmap, .. } = r {
                        oh == &qname_hash
                            && !type_bitmap_contains(type_bitmap, qtype)
                            && !type_bitmap_contains(type_bitmap, QueryType::CNAME.to_num())
                    } else {
                        false
                    }
                });
                if nodata {
                    debug!("dnssec: NSEC3 proves NODATA for '{}' type {}", qname, qtype);
                    return DnssecStatus::Secure;
                }
            }

            return DnssecStatus::Bogus;
        }
    }

    DnssecStatus::Indeterminate
}

fn parent_zone(zone: &str) -> String {
    if zone == "." || zone.is_empty() {
        return ".".into();
    }
    match zone.find('.') {
        Some(pos) => {
            let parent = &zone[pos + 1..];
            if parent.is_empty() {
                ".".into()
            } else {
                parent.into()
            }
        }
        None => ".".into(),
    }
}

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

    #[test]
    fn key_tags_match_published_root_anchors() {
        for (public_key, want_tag) in [
            (ROOT_KSK_2017_PUBLIC_KEY, ROOT_KSK_2017_KEY_TAG),
            (ROOT_KSK_2024_PUBLIC_KEY, ROOT_KSK_2024_KEY_TAG),
        ] {
            let tag = compute_key_tag(ROOT_KSK_FLAGS, 3, ROOT_KSK_ALGORITHM, public_key);
            assert_eq!(tag, want_tag);
        }
    }

    #[test]
    fn name_to_wire_root() {
        assert_eq!(name_to_wire("."), vec![0]);
        assert_eq!(name_to_wire(""), vec![0]);
    }

    #[test]
    fn name_to_wire_domain() {
        let wire = name_to_wire("Example.COM");
        assert_eq!(
            wire,
            vec![7, b'e', b'x', b'a', b'm', b'p', b'l', b'e', 3, b'c', b'o', b'm', 0]
        );
    }

    #[test]
    fn name_to_wire_escaped_dot_in_label_is_not_a_separator() {
        // `exa\.mple.com` is two labels: `exa.mple` (8 bytes including the 0x2E) and `com`.
        let wire = name_to_wire("exa\\.mple.com");
        assert_eq!(
            wire,
            vec![8, b'e', b'x', b'a', b'.', b'm', b'p', b'l', b'e', 3, b'c', b'o', b'm', 0]
        );
    }

    #[test]
    fn name_to_wire_decimal_escape_is_lowercased() {
        // \065 = 'A', must become 'a' in canonical form.
        let wire = name_to_wire("\\065bc.com");
        assert_eq!(wire, vec![3, b'a', b'b', b'c', 3, b'c', b'o', b'm', 0]);
    }

    #[test]
    fn parent_zone_cases() {
        assert_eq!(parent_zone("example.com"), "com");
        assert_eq!(parent_zone("com"), ".");
        assert_eq!(parent_zone("."), ".");
        assert_eq!(parent_zone("sub.example.com"), "example.com");
    }

    #[test]
    fn ds_verification() {
        // Verify DS digest: SHA-256(owner_wire + DNSKEY_RDATA) must match DS.digest
        let dk = DnsRecord::DNSKEY {
            domain: "test.example".into(),
            flags: 257,
            protocol: 3,
            algorithm: 8,
            public_key: vec![1, 2, 3, 4],
            ttl: 3600,
        };

        // Compute expected digest
        let owner_wire = name_to_wire("test.example");
        let mut dnskey_rdata = vec![1u8, 1, 3, 8]; // flags=257, proto=3, algo=8
        dnskey_rdata.extend(&[1, 2, 3, 4]);

        let mut input = Vec::new();
        input.extend(&owner_wire);
        input.extend(&dnskey_rdata);
        let expected = ring::digest::digest(&ring::digest::SHA256, &input);

        let ds = DnsRecord::DS {
            domain: "test.example".into(),
            key_tag: compute_key_tag(257, 3, 8, &[1, 2, 3, 4]),
            algorithm: 8,
            digest_type: 2,
            digest: expected.as_ref().to_vec(),
            ttl: 3600,
        };

        assert!(verify_ds(&ds, &dk, "test.example"));
    }

    #[test]
    fn rsa_der_conversion() {
        // Minimal RSA key: 3-byte exponent (65537 = 0x010001), 4-byte modulus
        let mut key = vec![3u8]; // exp_len = 3
        key.extend(&[0x01, 0x00, 0x01]); // exponent = 65537
        key.extend(&[0xFF, 0xAA, 0xBB, 0xCC]); // modulus

        let der = rsa_dnskey_to_der(&key).unwrap();
        // Should be a valid ASN.1 SEQUENCE containing two INTEGERs
        assert_eq!(der[0], 0x30); // SEQUENCE
    }

    #[test]
    fn group_rrsets_basic() {
        let records = vec![
            DnsRecord::A {
                domain: "example.com".into(),
                addr: "1.2.3.4".parse().unwrap(),
                ttl: 300,
            },
            DnsRecord::A {
                domain: "example.com".into(),
                addr: "5.6.7.8".parse().unwrap(),
                ttl: 300,
            },
        ];
        let groups = group_rrsets(&records);
        assert_eq!(groups.len(), 1);
        assert_eq!(groups[0].2.len(), 2);
    }

    #[test]
    fn type_bitmap_contains_a() {
        // Window 0, bitmap: A(1) + NS(2) + SOA(6) + MX(15) + AAAA(28)
        // Byte 0: bits 1,2 set = 0x60; byte 0 also has SOA(6) = 0x02 → 0x62
        // Actually: bit N means type N. Byte 0 covers types 0-7, byte 1 covers 8-15, etc.
        // Type 1 (A) = byte 0, bit 6 (0x40); Type 2 (NS) = byte 0, bit 5 (0x20)
        // Window 0, length 4, bitmap covers types 0-31
        let bitmap = vec![
            0u8, 4,    // window 0, 4 bytes
            0x62, // byte 0: types 1(A), 2(NS), 6(SOA) → bits 6,5,1 → 0x40|0x20|0x02
            0x01, // byte 1: type 15(MX) → bit 0 → 0x01
            0x00, // byte 2: nothing
            0x08, // byte 3: type 28(AAAA) → bit 3 → 0x08
        ];
        assert!(type_bitmap_contains(&bitmap, 1)); // A
        assert!(type_bitmap_contains(&bitmap, 2)); // NS
        assert!(type_bitmap_contains(&bitmap, 6)); // SOA
        assert!(type_bitmap_contains(&bitmap, 15)); // MX
        assert!(type_bitmap_contains(&bitmap, 28)); // AAAA
        assert!(!type_bitmap_contains(&bitmap, 5)); // CNAME — not set
        assert!(!type_bitmap_contains(&bitmap, 16)); // TXT — not set
    }

    #[test]
    fn canonical_name_ordering() {
        use std::cmp::Ordering;
        assert_eq!(
            canonical_dns_name_order("a.example.com", "b.example.com"),
            Ordering::Less
        );
        assert_eq!(
            canonical_dns_name_order("z.example.com", "a.example.org"),
            Ordering::Less // .com < .org
        );
        assert_eq!(
            canonical_dns_name_order("example.com", "a.example.com"),
            Ordering::Less // shorter sorts first
        );
        assert_eq!(
            canonical_dns_name_order("example.com", "example.com"),
            Ordering::Equal
        );
    }

    #[test]
    fn nsec_covers_name_basic() {
        // gap: alpha.example.com -> gamma.example.com
        assert!(nsec_covers_name(
            "alpha.example.com",
            "gamma.example.com",
            "beta.example.com"
        ));
        assert!(nsec_covers_name(
            "alpha.example.com",
            "gamma.example.com",
            "delta.example.com"
        ));
        assert!(!nsec_covers_name(
            "alpha.example.com",
            "gamma.example.com",
            "zebra.example.com"
        ));
    }

    #[test]
    fn nsec3_hash_rejects_high_iterations() {
        assert!(nsec3_hash("example.com", 1, 500, &[]).is_some());
        assert!(nsec3_hash("example.com", 1, 501, &[]).is_none());
    }

    #[test]
    fn closest_encloser_finds_parent() {
        let nsec1 = DnsRecord::NSEC {
            domain: "example.com".into(),
            next_domain: "z.example.com".into(),
            type_bitmap: vec![],
            ttl: 300,
        };
        let nsecs: Vec<&DnsRecord> = vec![&nsec1];
        // foo.example.com doesn't exist; closest encloser is example.com (the NSEC owner)
        assert_eq!(
            closest_encloser("foo.example.com", &nsecs),
            Some("example.com".into())
        );
        // example.com is itself an NSEC owner, so it IS a closest encloser
        assert_eq!(
            closest_encloser("example.com", &nsecs),
            Some("example.com".into())
        );
        // nothing.org has no matching owner
        assert_eq!(closest_encloser("nothing.org", &nsecs), None);
    }

    #[test]
    fn nsec_nodata_proof() {
        // NSEC at example.com with A and NS in bitmap, but not AAAA
        let bitmap = vec![0u8, 1, 0x62]; // A(1), NS(2), SOA(6)
        assert!(nsec_proves_nodata(
            "example.com",
            "example.com",
            &bitmap,
            28
        )); // AAAA not in bitmap
        assert!(!nsec_proves_nodata(
            "example.com",
            "example.com",
            &bitmap,
            1
        )); // A IS in bitmap
    }

    #[test]
    fn nsec3_hash_basic() {
        // Hash with 0 iterations, empty salt
        let hash = nsec3_hash("example.com", 1, 0, &[]).unwrap();
        assert_eq!(hash.len(), 20); // SHA-1 output
    }

    #[test]
    fn nsec3_range_check() {
        assert!(nsec3_hash_in_range(&[1], &[3], &[2])); // 1 < 2 < 3
        assert!(!nsec3_hash_in_range(&[1], &[3], &[4])); // 4 not in range
                                                         // Wrap-around: [250] -> [10] covers [255] and [5]
        assert!(nsec3_hash_in_range(&[250], &[10], &[255]));
        assert!(nsec3_hash_in_range(&[250], &[10], &[5]));
        assert!(!nsec3_hash_in_range(&[250], &[10], &[100])); // not in wrapped range
    }

    #[test]
    fn base32hex_decode_known_values() {
        // "00000000" in base32hex = all zeros
        assert_eq!(base32hex_decode("00000000").unwrap(), vec![0, 0, 0, 0, 0]);
        // "10" = 0x08 (1 << 3)
        assert_eq!(base32hex_decode("10").unwrap(), vec![0x08]);
        // case-insensitive: "vv" = "VV" = [0xFF, 0x80..] -> 31<<5|31 = 0x03FF -> bytes [0xFF]
        assert_eq!(base32hex_decode("VV"), base32hex_decode("vv"));
        // invalid char
        assert!(base32hex_decode("!!").is_none());
    }

    // Chain-of-trust authentication (issue #235). These tests mint throwaway
    // ECDSA P-256 (algo 13) keys and use a test KSK as the root trust anchor —
    // the real root's private key is, by design, unforgeable.
    use ring::rand::SystemRandom;
    use ring::signature::{EcdsaKeyPair, KeyPair, ECDSA_P256_SHA256_FIXED_SIGNING};

    struct TestSigner {
        key: EcdsaKeyPair,
        pubkey: Vec<u8>, // DNSSEC form: raw x||y, no 0x04 prefix
        flags: u16,
    }

    fn mk_signer(flags: u16) -> TestSigner {
        let rng = SystemRandom::new();
        let pkcs8 = EcdsaKeyPair::generate_pkcs8(&ECDSA_P256_SHA256_FIXED_SIGNING, &rng).unwrap();
        let key = EcdsaKeyPair::from_pkcs8(&ECDSA_P256_SHA256_FIXED_SIGNING, pkcs8.as_ref(), &rng)
            .unwrap();
        let pubkey = key.public_key().as_ref()[1..].to_vec();
        TestSigner { key, pubkey, flags }
    }

    fn mk_dnskey(owner: &str, s: &TestSigner) -> DnsRecord {
        DnsRecord::DNSKEY {
            domain: owner.into(),
            flags: s.flags,
            protocol: 3,
            algorithm: 13,
            public_key: s.pubkey.clone(),
            ttl: 3600,
        }
    }

    /// RRSIG made by `signer` over `rrset`, signing the exact bytes the
    /// validator reconstructs via `build_signed_data`.
    fn mk_rrsig(
        signer: &TestSigner,
        signer_name: &str,
        type_covered: QueryType,
        rrset: &[&DnsRecord],
    ) -> DnsRecord {
        let mut rrsig = DnsRecord::RRSIG {
            domain: rrset[0].domain().to_string(),
            type_covered: type_covered.to_num(),
            algorithm: 13,
            labels: rrset[0]
                .domain()
                .split('.')
                .filter(|l| !l.is_empty())
                .count() as u8,
            original_ttl: 3600,
            expiration: 2_147_483_647,
            inception: 1,
            key_tag: compute_key_tag(signer.flags, 3, 13, &signer.pubkey),
            signer_name: signer_name.to_string(),
            signature: Vec::new(),
            ttl: 3600,
        };
        let signed_data = build_signed_data(&rrsig, rrset);
        let sig = signer.key.sign(&SystemRandom::new(), &signed_data).unwrap();
        if let DnsRecord::RRSIG { signature, .. } = &mut rrsig {
            *signature = sig.as_ref().to_vec();
        }
        rrsig
    }

    fn mk_ds(child_owner: &str, child_ksk: &DnsRecord) -> DnsRecord {
        let DnsRecord::DNSKEY {
            flags,
            protocol,
            algorithm,
            public_key,
            ..
        } = child_ksk
        else {
            unreachable!()
        };
        let mut input = name_to_wire(child_owner);
        input.extend(&flags.to_be_bytes());
        input.push(*protocol);
        input.push(*algorithm);
        input.extend(public_key);
        DnsRecord::DS {
            domain: child_owner.into(),
            key_tag: compute_key_tag(*flags, *protocol, *algorithm, public_key),
            algorithm: *algorithm,
            digest_type: 2,
            digest: digest::digest(&digest::SHA256, &input).as_ref().to_vec(),
            ttl: 3600,
        }
    }

    fn mk_pkt(answers: Vec<DnsRecord>) -> DnsPacket {
        let mut p = DnsPacket::new();
        p.answers = answers;
        p
    }

    fn empty_ctx() -> (RwLock<DnsCache>, RwLock<SrttCache>, Mutex<ValidationStats>) {
        (
            RwLock::new(DnsCache::new(1000, 0, 100_000)),
            RwLock::new(SrttCache::new(false)),
            Mutex::new(ValidationStats::default()),
        )
    }

    // Positive control: a fully-signed delegation must stay Secure.
    #[tokio::test]
    async fn properly_signed_delegation_validates() {
        let (cache, srtt, stats) = empty_ctx();

        let root_ksk = mk_signer(257);
        let root_zsk = mk_signer(256);
        let root_ksk_dk = mk_dnskey(".", &root_ksk);
        let root_zsk_dk = mk_dnskey(".", &root_zsk);
        let trust_anchors = vec![root_ksk_dk.clone()];

        let root_set = vec![root_ksk_dk.clone(), root_zsk_dk.clone()];
        let root_refs: Vec<&DnsRecord> = root_set.iter().collect();
        let root_selfsig = mk_rrsig(&root_ksk, ".", QueryType::DNSKEY, &root_refs);
        let mut root_answers = root_set.clone();
        root_answers.push(root_selfsig);
        cache
            .write()
            .unwrap()
            .insert(".", QueryType::DNSKEY, &mk_pkt(root_answers));

        let test_ksk = mk_signer(257);
        let test_dk = mk_dnskey("test", &test_ksk);
        let test_set = [test_dk.clone()];
        let test_refs: Vec<&DnsRecord> = test_set.iter().collect();
        let test_selfsig = mk_rrsig(&test_ksk, "test", QueryType::DNSKEY, &test_refs);
        let test_records = vec![test_dk.clone(), test_selfsig];

        // DS for "test", signed by the root ZSK — a legitimate delegation.
        let ds = mk_ds("test", &test_dk);
        let ds_set = [ds.clone()];
        let ds_refs: Vec<&DnsRecord> = ds_set.iter().collect();
        let ds_sig = mk_rrsig(&root_zsk, ".", QueryType::DS, &ds_refs);
        cache
            .write()
            .unwrap()
            .insert("test", QueryType::DS, &mk_pkt(vec![ds, ds_sig]));

        let status = validate_chain(
            "test",
            &test_records,
            &cache,
            &[],
            &srtt,
            &trust_anchors,
            0,
            &stats,
        )
        .await;
        assert_eq!(status, DnssecStatus::Secure);
    }

    #[tokio::test]
    async fn unsigned_ds_must_not_validate() {
        let (cache, srtt, stats) = empty_ctx();

        let root_ksk = mk_signer(257);
        let root_ksk_dk = mk_dnskey(".", &root_ksk);
        let trust_anchors = vec![root_ksk_dk.clone()];
        let root_set = [root_ksk_dk.clone()];
        let root_refs: Vec<&DnsRecord> = root_set.iter().collect();
        let root_selfsig = mk_rrsig(&root_ksk, ".", QueryType::DNSKEY, &root_refs);
        cache.write().unwrap().insert(
            ".",
            QueryType::DNSKEY,
            &mk_pkt(vec![root_ksk_dk.clone(), root_selfsig]),
        );

        // Attacker zone "test": own KSK, self-signed DNSKEY set.
        let test_ksk = mk_signer(257);
        let test_dk = mk_dnskey("test", &test_ksk);
        let test_set = [test_dk.clone()];
        let test_refs: Vec<&DnsRecord> = test_set.iter().collect();
        let test_selfsig = mk_rrsig(&test_ksk, "test", QueryType::DNSKEY, &test_refs);
        let test_records = vec![test_dk.clone(), test_selfsig];

        // Forged DS — digest matches the attacker KSK, but UNSIGNED by root.
        let forged_ds = mk_ds("test", &test_dk);
        cache
            .write()
            .unwrap()
            .insert("test", QueryType::DS, &mk_pkt(vec![forged_ds]));

        let status = validate_chain(
            "test",
            &test_records,
            &cache,
            &[],
            &srtt,
            &trust_anchors,
            0,
            &stats,
        )
        .await;
        assert_ne!(
            status,
            DnssecStatus::Secure,
            "unsigned DS must not yield Secure — the parent must sign the DS RRset"
        );
    }

    #[tokio::test]
    async fn root_anchor_without_self_signature_must_not_validate() {
        let (cache, srtt, stats) = empty_ctx();
        let root_ksk = mk_signer(257);
        let root_ksk_dk = mk_dnskey(".", &root_ksk);
        let trust_anchors = vec![root_ksk_dk.clone()];
        let root_records = vec![root_ksk_dk.clone()]; // no RRSIG
        let status = validate_chain(
            ".",
            &root_records,
            &cache,
            &[],
            &srtt,
            &trust_anchors,
            0,
            &stats,
        )
        .await;
        assert_ne!(
            status,
            DnssecStatus::Secure,
            "trust-anchor presence alone must not yield Secure"
        );
    }

    #[tokio::test]
    async fn root_with_forged_ksk_must_not_validate() {
        let (cache, srtt, stats) = empty_ctx();
        let anchor = mk_signer(257);
        let anchor_dk = mk_dnskey(".", &anchor);
        let trust_anchors = [anchor_dk.clone()];

        // Set carries the (public) anchor key + an attacker KSK; signed by the
        // attacker KSK, which the attacker controls — never by the anchor.
        let attacker = mk_signer(257);
        let attacker_dk = mk_dnskey(".", &attacker);
        let root_set = vec![anchor_dk.clone(), attacker_dk.clone()];
        let root_refs: Vec<&DnsRecord> = root_set.iter().collect();
        let forged = mk_rrsig(&attacker, ".", QueryType::DNSKEY, &root_refs);
        let mut root_records = root_set.clone();
        root_records.push(forged);

        let status = validate_chain(
            ".",
            &root_records,
            &cache,
            &[],
            &srtt,
            &trust_anchors,
            0,
            &stats,
        )
        .await;
        assert_ne!(
            status,
            DnssecStatus::Secure,
            "root set signed by a non-anchor KSK must not validate"
        );
    }

    #[tokio::test]
    async fn child_with_forged_ksk_must_not_validate() {
        let (cache, srtt, stats) = empty_ctx();

        let root_ksk = mk_signer(257);
        let root_dk = mk_dnskey(".", &root_ksk);
        let trust_anchors = [root_dk.clone()];
        let root_set = [root_dk.clone()];
        let root_refs: Vec<&DnsRecord> = root_set.iter().collect();
        let root_selfsig = mk_rrsig(&root_ksk, ".", QueryType::DNSKEY, &root_refs);
        cache.write().unwrap().insert(
            ".",
            QueryType::DNSKEY,
            &mk_pkt(vec![root_dk.clone(), root_selfsig]),
        );

        // Real child KSK — the genuine DS commits to it.
        let real_ksk = mk_signer(257);
        let real_dk = mk_dnskey("test", &real_ksk);

        // Attacker's child set: real (public) KSK to satisfy the DS digest, plus
        // an attacker KSK that actually signs the set.
        let attacker = mk_signer(257);
        let attacker_dk = mk_dnskey("test", &attacker);
        let child_set = vec![real_dk.clone(), attacker_dk.clone()];
        let child_refs: Vec<&DnsRecord> = child_set.iter().collect();
        let forged = mk_rrsig(&attacker, "test", QueryType::DNSKEY, &child_refs);
        let mut child_records = child_set.clone();
        child_records.push(forged);

        // Genuine DS (digest of the REAL KSK), signed by the root.
        let ds = mk_ds("test", &real_dk);
        let ds_set = [ds.clone()];
        let ds_refs: Vec<&DnsRecord> = ds_set.iter().collect();
        let ds_sig = mk_rrsig(&root_ksk, ".", QueryType::DS, &ds_refs);
        cache
            .write()
            .unwrap()
            .insert("test", QueryType::DS, &mk_pkt(vec![ds, ds_sig]));

        let status = validate_chain(
            "test",
            &child_records,
            &cache,
            &[],
            &srtt,
            &trust_anchors,
            0,
            &stats,
        )
        .await;
        assert_ne!(
            status,
            DnssecStatus::Secure,
            "child set signed by a KSK the DS does not commit to must not validate"
        );
    }

    // Sign `record` in zone "test" (DNSKEY pre-seeded in cache), corrupt the
    // signature, and validate the full response.
    async fn tampered_rrset_status(record: DnsRecord) -> DnssecStatus {
        let (cache, srtt, _stats) = empty_ctx();
        let zsk = mk_signer(256);
        cache.write().unwrap().insert(
            "test",
            QueryType::DNSKEY,
            &mk_pkt(vec![mk_dnskey("test", &zsk)]),
        );

        let mut rrsig = mk_rrsig(&zsk, "test", record.query_type(), &[&record]);
        if let DnsRecord::RRSIG { signature, .. } = &mut rrsig {
            signature[0] ^= 0xFF;
        }

        let response = mk_pkt(vec![record, rrsig]);
        validate_response(&response, &cache, &[], &srtt).await.0
    }

    // Issue #324: TXT has no DnsRecord variant, so group_rrsets keyed the rrset
    // UNKNOWN(16) while matching_rrsigs_for compares against from_num(16) = TXT.
    // No RRSIG ever matched, the rrset was skipped, and a tampered answer
    // fell through to Secure.
    #[tokio::test]
    async fn issue_324_tampered_txt_must_not_report_secure() {
        let txt = mk_unknown("www.test", QueryType::TXT, b"\x10v=spf1 -all TAMPERED");
        assert_eq!(
            tampered_rrset_status(txt).await,
            DnssecStatus::Bogus,
            "TXT rrset with an unverifiable signature must not report Secure"
        );
    }

    // Control: the identical tamper on a type WITH a DnsRecord variant is caught.
    #[tokio::test]
    async fn issue_324_control_tampered_a_is_bogus() {
        let a = DnsRecord::A {
            domain: "www.test".into(),
            addr: "6.6.6.6".parse().unwrap(),
            ttl: 3600,
        };
        assert_eq!(tampered_rrset_status(a).await, DnssecStatus::Bogus);
    }

    fn mk_unknown(domain: &str, qtype: QueryType, data: &[u8]) -> DnsRecord {
        DnsRecord::UNKNOWN {
            domain: domain.into(),
            qtype: qtype.to_num(),
            data: data.to_vec(),
            ttl: 3600,
        }
    }

    #[test]
    fn genuine_txt_and_https_signatures_verify() {
        let zsk = mk_signer(256);
        let dk = mk_dnskey("test", &zsk);

        for (qtype, rdata) in [
            (QueryType::TXT, &b"\x0bv=spf1 -all"[..]),
            // HTTPS: priority=1, target=root, alpn="h2"
            (QueryType::HTTPS, &b"\x00\x01\x00\x00\x01\x00\x03\x02h2"[..]),
        ] {
            let rr = mk_unknown("www.test", qtype, rdata);
            let refs = [&rr];
            let rrsig = mk_rrsig(&zsk, "test", qtype, &refs);
            assert!(
                rrsig_verified_by(&rrsig, &dk, &refs),
                "genuine {qtype:?} signature must verify"
            );

            let tampered = mk_unknown("www.test", qtype, b"\x08TAMPERED");
            assert!(
                !rrsig_verified_by(&rrsig, &dk, &[&tampered]),
                "tampered {qtype:?} rdata must not verify"
            );
        }
    }

    // RFC 4034 §6.2: the signer canonicalizes the SRV target to lowercase, so a
    // response carrying the target with its original casing must still verify.
    #[test]
    fn srv_with_uppercase_target_verifies_against_canonical_signature() {
        let zsk = mk_signer(256);
        let dk = mk_dnskey("test", &zsk);

        let srv_rdata = |target_case: &[u8]| {
            let mut d = vec![0, 10, 0, 5, 0x13, 0xc4]; // prio=10 weight=5 port=5060
            d.push(3);
            d.extend(target_case);
            d.extend(b"\x04test\x00");
            d
        };
        let canonical = mk_unknown("_sip._udp.test", QueryType::SRV, &srv_rdata(b"sip"));
        let as_served = mk_unknown("_sip._udp.test", QueryType::SRV, &srv_rdata(b"SIP"));

        let rrsig = mk_rrsig(&zsk, "test", QueryType::SRV, &[&canonical]);
        assert!(rrsig_verified_by(&rrsig, &dk, &[&as_served]));
    }

    #[test]
    fn naptr_canonical_downcases_only_the_replacement_name() {
        // order=100 pref=10, flags="S", services="SIP+D2U", regexp="", replacement=Sip.Test.
        let mut rdata = vec![0, 100, 0, 10];
        rdata.extend(b"\x01S\x07SIP+D2U\x00");
        rdata.extend(b"\x03Sip\x04Test\x00");

        let canon = canonical_unknown_rdata(QueryType::NAPTR, &rdata);
        let mut want = vec![0, 100, 0, 10];
        want.extend(b"\x01S\x07SIP+D2U\x00"); // character-strings untouched
        want.extend(b"\x03sip\x04test\x00");
        assert_eq!(canon, want);
    }
}
