use crate::analysis::timing::TimingHandle;
use crate::audit::{AuditObjectKind, AuditObjectResult, ObjectAuditEntry, sha256_hex_from_32};
use crate::data_model::aspa::{AspaDecodeError, AspaObject, AspaValidateError};
use crate::data_model::manifest::ManifestObject;
use crate::data_model::rc::{
    AsIdentifierChoice, AsResourceSet, IpAddressChoice, IpAddressOrRange, IpPrefix as RcIpPrefix,
    ResourceCertificate,
};
use crate::data_model::roa::{IpPrefix, RoaAfi, RoaDecodeError, RoaObject, RoaValidateError};
use crate::data_model::signed_object::SignedObjectVerifyError;
use crate::policy::{Policy, SignedObjectFailurePolicy};
use crate::report::{RfcRef, Warning};
use crate::storage::{PackFile, PackTime, VcirLocalOutput, VcirOutputType};
use crate::validation::cert_path::{
    CertPathError, validate_ee_cert_path_with_predecoded_ee,
};
use crate::validation::manifest::PublicationPointData;
use crate::validation::publication_point::PublicationPointSnapshot;
use x509_parser::prelude::FromDer;
use x509_parser::x509::SubjectPublicKeyInfo;

const RFC_NONE: &[RfcRef] = &[];
const RFC_CRLDP: &[RfcRef] = &[RfcRef("RFC 6487 §4.8.6")];
const RFC_CRLDP_AND_LOCKED_PACK: &[RfcRef] =
    &[RfcRef("RFC 6487 §4.8.6"), RfcRef("RFC 9286 §4.2.1")];

#[derive(Clone, Debug)]
struct VerifiedIssuerCrl {
    crl: crate::data_model::crl::RpkixCrl,
    revoked_serials: std::collections::HashSet<Vec<u8>>,
}

#[derive(Clone, Debug)]
enum CachedIssuerCrl {
    Pending(Vec<u8>),
    Ok(VerifiedIssuerCrl),
}

#[derive(Clone, Debug, Default)]
struct IssuerResourcesIndex {
    ip_v4: Option<Vec<(Vec<u8>, Vec<u8>)>>,
    ip_v6: Option<Vec<(Vec<u8>, Vec<u8>)>>,
    asnum: Option<Vec<(u32, u32)>>,
    rdi: Option<Vec<(u32, u32)>>,
}

fn extra_rfc_refs_for_crl_selection(e: &ObjectValidateError) -> &'static [RfcRef] {
    match e {
        ObjectValidateError::MissingCrlDpUris => RFC_CRLDP,
        ObjectValidateError::CrlNotFound(_) => RFC_CRLDP_AND_LOCKED_PACK,
        _ => RFC_NONE,
    }
}

#[derive(Clone, Debug, PartialEq, Eq)]
pub struct Vrp {
    pub asn: u32,
    pub prefix: IpPrefix,
    pub max_length: u16,
}

#[derive(Clone, Debug, PartialEq, Eq)]
pub struct AspaAttestation {
    pub customer_as_id: u32,
    pub provider_as_ids: Vec<u32>,
}

#[derive(Clone, Debug, PartialEq, Eq)]
pub struct RouterKeyPayload {
    pub as_id: u32,
    pub ski: Vec<u8>,
    pub spki_der: Vec<u8>,
    pub source_object_uri: String,
    pub source_object_hash: String,
    pub source_ee_cert_hash: String,
    pub item_effective_until: PackTime,
}

#[derive(Clone, Debug, PartialEq, Eq)]
pub struct ObjectsOutput {
    pub vrps: Vec<Vrp>,
    pub aspas: Vec<AspaAttestation>,
    pub router_keys: Vec<RouterKeyPayload>,
    pub local_outputs_cache: Vec<VcirLocalOutput>,
    pub warnings: Vec<Warning>,
    pub stats: ObjectsStats,
    pub audit: Vec<ObjectAuditEntry>,
}

#[derive(Clone, Debug, Default, PartialEq, Eq)]
pub struct ObjectsStats {
    pub roa_total: usize,
    pub roa_ok: usize,
    pub aspa_total: usize,
    pub aspa_ok: usize,
    /// Whether this publication point was dropped due to an unrecoverable objects-processing error
    /// (e.g., missing issuer CRL in the pack, or `signed_object_failure_policy=drop_publication_point`).
    pub publication_point_dropped: bool,
}

/// Process objects from a publication point snapshot using a known issuer CA certificate
/// and its effective resources (resolved via the resource-path, RFC 6487 §7.2).
pub fn process_publication_point_for_issuer<P: PublicationPointData>(
    publication_point: &P,
    policy: &Policy,
    issuer_ca_der: &[u8],
    issuer_ca_rsync_uri: Option<&str>,
    issuer_effective_ip: Option<&crate::data_model::rc::IpResourceSet>,
    issuer_effective_as: Option<&crate::data_model::rc::AsResourceSet>,
    validation_time: time::OffsetDateTime,
    timing: Option<&TimingHandle>,
) -> ObjectsOutput {
    let manifest_rsync_uri = publication_point.manifest_rsync_uri();
    let manifest_bytes = publication_point.manifest_bytes();
    let locked_files = publication_point.files();
    let mut warnings: Vec<Warning> = Vec::new();
    let mut stats = ObjectsStats::default();
    stats.roa_total = locked_files
        .iter()
        .filter(|f| f.rsync_uri.ends_with(".roa"))
        .count();
    stats.aspa_total = locked_files
        .iter()
        .filter(|f| f.rsync_uri.ends_with(".asa"))
        .count();
    let mut audit: Vec<ObjectAuditEntry> = Vec::new();

    // Enforce that `manifest_bytes` is actually a manifest object.
    let _manifest = ManifestObject::decode_der(manifest_bytes)
        .expect("publication point snapshot manifest decodes");

    // Decode issuer CA once; if it fails we cannot validate ROA/ASPA EE certificates.
    let issuer_ca = match ResourceCertificate::decode_der(issuer_ca_der) {
        Ok(v) => v,
        Err(e) => {
            stats.publication_point_dropped = true;
            warnings.push(
                Warning::new(format!(
                    "dropping publication point: issuer CA decode failed: {e}"
                ))
                .with_rfc_refs(&[RfcRef("RFC 6487 §7.2"), RfcRef("RFC 5280 §6.1")])
                .with_context(manifest_rsync_uri),
            );
            for f in locked_files {
                if f.rsync_uri.ends_with(".roa") {
                    audit.push(ObjectAuditEntry {
                        rsync_uri: f.rsync_uri.clone(),
                        sha256_hex: sha256_hex_from_32(&f.sha256),
                        kind: AuditObjectKind::Roa,
                        result: AuditObjectResult::Skipped,
                        detail: Some("skipped: issuer CA decode failed".to_string()),
                    });
                } else if f.rsync_uri.ends_with(".asa") {
                    audit.push(ObjectAuditEntry {
                        rsync_uri: f.rsync_uri.clone(),
                        sha256_hex: sha256_hex_from_32(&f.sha256),
                        kind: AuditObjectKind::Aspa,
                        result: AuditObjectResult::Skipped,
                        detail: Some("skipped: issuer CA decode failed".to_string()),
                    });
                }
            }
            return ObjectsOutput {
                vrps: Vec::new(),
                aspas: Vec::new(),
                router_keys: Vec::new(),
                local_outputs_cache: Vec::new(),
                warnings,
                stats,
                audit,
            };
        }
    };

    // Parse issuer SubjectPublicKeyInfo once and reuse for all EE certificate signature checks.
    let issuer_spki = match SubjectPublicKeyInfo::from_der(&issuer_ca.tbs.subject_public_key_info) {
        Ok((rem, spki)) if rem.is_empty() => spki,
        Ok((rem, _)) => {
            stats.publication_point_dropped = true;
            warnings.push(
                Warning::new(format!(
                    "dropping publication point: trailing bytes after issuer SPKI DER: {} bytes",
                    rem.len()
                ))
                .with_rfc_refs(&[RfcRef("RFC 5280 §4.1.2.7")])
                .with_context(manifest_rsync_uri),
            );
            return ObjectsOutput {
                vrps: Vec::new(),
                aspas: Vec::new(),
                router_keys: Vec::new(),
                local_outputs_cache: Vec::new(),
                warnings,
                stats,
                audit,
            };
        }
        Err(e) => {
            stats.publication_point_dropped = true;
            warnings.push(
                Warning::new(format!(
                    "dropping publication point: issuer SPKI parse failed: {e}"
                ))
                .with_rfc_refs(&[RfcRef("RFC 5280 §4.1.2.7")])
                .with_context(manifest_rsync_uri),
            );
            return ObjectsOutput {
                vrps: Vec::new(),
                aspas: Vec::new(),
                router_keys: Vec::new(),
                local_outputs_cache: Vec::new(),
                warnings,
                stats,
                audit,
            };
        }
    };

    let mut crl_cache: std::collections::HashMap<String, CachedIssuerCrl> = locked_files
        .iter()
        .filter(|f| f.rsync_uri.ends_with(".crl"))
        .map(|f| {
            (
                f.rsync_uri.clone(),
                CachedIssuerCrl::Pending(f.bytes.clone()),
            )
        })
        .collect();

    let issuer_resources_index =
        build_issuer_resources_index(issuer_effective_ip, issuer_effective_as);

    // If the snapshot has signed objects but no CRLs at all, we cannot validate any embedded EE
    // certificate paths deterministically (EE CRLDP must reference an rsync URI in the snapshot).
    if crl_cache.is_empty() && (stats.roa_total > 0 || stats.aspa_total > 0) {
        stats.publication_point_dropped = true;
        warnings.push(
            Warning::new("dropping publication point: no CRL files in validated publication point")
                .with_rfc_refs(&[RfcRef("RFC 6487 §4.8.6"), RfcRef("RFC 9286 §7")])
                .with_context(manifest_rsync_uri),
        );
        for f in locked_files {
            if f.rsync_uri.ends_with(".roa") {
                audit.push(ObjectAuditEntry {
                    rsync_uri: f.rsync_uri.clone(),
                    sha256_hex: sha256_hex_from_32(&f.sha256),
                    kind: AuditObjectKind::Roa,
                    result: AuditObjectResult::Skipped,
                    detail: Some(
                        "skipped due to missing CRL files in validated publication point"
                            .to_string(),
                    ),
                });
            } else if f.rsync_uri.ends_with(".asa") {
                audit.push(ObjectAuditEntry {
                    rsync_uri: f.rsync_uri.clone(),
                    sha256_hex: sha256_hex_from_32(&f.sha256),
                    kind: AuditObjectKind::Aspa,
                    result: AuditObjectResult::Skipped,
                    detail: Some(
                        "skipped due to missing CRL files in validated publication point"
                            .to_string(),
                    ),
                });
            }
        }
        return ObjectsOutput {
            vrps: Vec::new(),
            aspas: Vec::new(),
            router_keys: Vec::new(),
            local_outputs_cache: Vec::new(),
            warnings,
            stats,
            audit,
        };
    }

    let mut vrps: Vec<Vrp> = Vec::new();
    let mut aspas: Vec<AspaAttestation> = Vec::new();
    let mut local_outputs_cache: Vec<VcirLocalOutput> = Vec::new();

    for (idx, file) in locked_files.iter().enumerate() {
        if file.rsync_uri.ends_with(".roa") {
            let _t = timing.as_ref().map(|t| t.span_phase("objects_roa_total"));
            match process_roa_with_issuer(
                file,
                manifest_rsync_uri,
                issuer_ca_der,
                &issuer_ca,
                &issuer_spki,
                issuer_ca_rsync_uri,
                &mut crl_cache,
                &issuer_resources_index,
                issuer_effective_ip,
                issuer_effective_as,
                validation_time,
                timing,
            ) {
                Ok((mut out, local_outputs)) => {
                    stats.roa_ok += 1;
                    vrps.append(&mut out);
                    local_outputs_cache.extend(local_outputs);
                    audit.push(ObjectAuditEntry {
                        rsync_uri: file.rsync_uri.clone(),
                        sha256_hex: sha256_hex_from_32(&file.sha256),
                        kind: AuditObjectKind::Roa,
                        result: AuditObjectResult::Ok,
                        detail: None,
                    });
                }
                Err(e) => match policy.signed_object_failure_policy {
                    SignedObjectFailurePolicy::DropObject => {
                        audit.push(ObjectAuditEntry {
                            rsync_uri: file.rsync_uri.clone(),
                            sha256_hex: sha256_hex_from_32(&file.sha256),
                            kind: AuditObjectKind::Roa,
                            result: AuditObjectResult::Error,
                            detail: Some(e.to_string()),
                        });
                        let mut refs = vec![RfcRef("RFC 6488 §3"), RfcRef("RFC 9582 §4-§5")];
                        refs.extend_from_slice(extra_rfc_refs_for_crl_selection(&e));
                        warnings.push(
                            Warning::new(format!("dropping invalid ROA: {}: {e}", file.rsync_uri))
                                .with_rfc_refs(&refs)
                                .with_context(&file.rsync_uri),
                        )
                    }
                    SignedObjectFailurePolicy::DropPublicationPoint => {
                        stats.publication_point_dropped = true;
                        audit.push(ObjectAuditEntry {
                            rsync_uri: file.rsync_uri.clone(),
                            sha256_hex: sha256_hex_from_32(&file.sha256),
                            kind: AuditObjectKind::Roa,
                            result: AuditObjectResult::Error,
                            detail: Some(e.to_string()),
                        });
                        for f in locked_files.iter().skip(idx + 1) {
                            if f.rsync_uri.ends_with(".roa") {
                                audit.push(ObjectAuditEntry {
                                    rsync_uri: f.rsync_uri.clone(),
                                    sha256_hex: sha256_hex_from_32(&f.sha256),
                                    kind: AuditObjectKind::Roa,
                                    result: AuditObjectResult::Skipped,
                                    detail: Some(
                                        "skipped due to policy=signed_object_failure_policy=drop_publication_point"
                                            .to_string(),
                                    ),
                                });
                            } else if f.rsync_uri.ends_with(".asa") {
                                audit.push(ObjectAuditEntry {
                                    rsync_uri: f.rsync_uri.clone(),
                                    sha256_hex: sha256_hex_from_32(&f.sha256),
                                    kind: AuditObjectKind::Aspa,
                                    result: AuditObjectResult::Skipped,
                                    detail: Some(
                                        "skipped due to policy=signed_object_failure_policy=drop_publication_point"
                                            .to_string(),
                                    ),
                                });
                            }
                        }
                        let mut refs = vec![RfcRef("RFC 6488 §3"), RfcRef("RFC 9582 §4-§5")];
                        refs.extend_from_slice(extra_rfc_refs_for_crl_selection(&e));
                        warnings.push(
                            Warning::new(format!(
                                "dropping publication point due to invalid ROA: {}: {e}",
                                file.rsync_uri
                            ))
                            .with_rfc_refs(&refs)
                            .with_context(manifest_rsync_uri),
                        );
                        return ObjectsOutput {
                            vrps: Vec::new(),
                            aspas: Vec::new(),
                            router_keys: Vec::new(),
                            local_outputs_cache: Vec::new(),
                            warnings,
                            stats,
                            audit,
                        };
                    }
                },
            }
        } else if file.rsync_uri.ends_with(".asa") {
            let _t = timing.as_ref().map(|t| t.span_phase("objects_aspa_total"));
            match process_aspa_with_issuer(
                file,
                manifest_rsync_uri,
                issuer_ca_der,
                &issuer_ca,
                &issuer_spki,
                issuer_ca_rsync_uri,
                &mut crl_cache,
                &issuer_resources_index,
                issuer_effective_ip,
                issuer_effective_as,
                validation_time,
                timing,
            ) {
                Ok((att, local_output)) => {
                    stats.aspa_ok += 1;
                    aspas.push(att);
                    local_outputs_cache.push(local_output);
                    audit.push(ObjectAuditEntry {
                        rsync_uri: file.rsync_uri.clone(),
                        sha256_hex: sha256_hex_from_32(&file.sha256),
                        kind: AuditObjectKind::Aspa,
                        result: AuditObjectResult::Ok,
                        detail: None,
                    });
                }
                Err(e) => match policy.signed_object_failure_policy {
                    SignedObjectFailurePolicy::DropObject => {
                        audit.push(ObjectAuditEntry {
                            rsync_uri: file.rsync_uri.clone(),
                            sha256_hex: sha256_hex_from_32(&file.sha256),
                            kind: AuditObjectKind::Aspa,
                            result: AuditObjectResult::Error,
                            detail: Some(e.to_string()),
                        });
                        let mut refs = vec![RfcRef("RFC 6488 §3")];
                        refs.extend_from_slice(extra_rfc_refs_for_crl_selection(&e));
                        warnings.push(
                            Warning::new(format!("dropping invalid ASPA: {}: {e}", file.rsync_uri))
                                .with_rfc_refs(&refs)
                                .with_context(&file.rsync_uri),
                        )
                    }
                    SignedObjectFailurePolicy::DropPublicationPoint => {
                        stats.publication_point_dropped = true;
                        audit.push(ObjectAuditEntry {
                            rsync_uri: file.rsync_uri.clone(),
                            sha256_hex: sha256_hex_from_32(&file.sha256),
                            kind: AuditObjectKind::Aspa,
                            result: AuditObjectResult::Error,
                            detail: Some(e.to_string()),
                        });
                        for f in locked_files.iter().skip(idx + 1) {
                            if f.rsync_uri.ends_with(".roa") {
                                audit.push(ObjectAuditEntry {
                                    rsync_uri: f.rsync_uri.clone(),
                                    sha256_hex: sha256_hex_from_32(&f.sha256),
                                    kind: AuditObjectKind::Roa,
                                    result: AuditObjectResult::Skipped,
                                    detail: Some(
                                        "skipped due to policy=signed_object_failure_policy=drop_publication_point"
                                            .to_string(),
                                    ),
                                });
                            } else if f.rsync_uri.ends_with(".asa") {
                                audit.push(ObjectAuditEntry {
                                    rsync_uri: f.rsync_uri.clone(),
                                    sha256_hex: sha256_hex_from_32(&f.sha256),
                                    kind: AuditObjectKind::Aspa,
                                    result: AuditObjectResult::Skipped,
                                    detail: Some(
                                        "skipped due to policy=signed_object_failure_policy=drop_publication_point"
                                            .to_string(),
                                    ),
                                });
                            }
                        }
                        let mut refs = vec![RfcRef("RFC 6488 §3")];
                        refs.extend_from_slice(extra_rfc_refs_for_crl_selection(&e));
                        warnings.push(
                            Warning::new(format!(
                                "dropping publication point due to invalid ASPA: {}: {e}",
                                file.rsync_uri
                            ))
                            .with_rfc_refs(&refs)
                            .with_context(manifest_rsync_uri),
                        );
                        return ObjectsOutput {
                            vrps: Vec::new(),
                            aspas: Vec::new(),
                            router_keys: Vec::new(),
                            local_outputs_cache: Vec::new(),
                            warnings,
                            stats,
                            audit,
                        };
                    }
                },
            }
        }
    }

    ObjectsOutput {
        vrps,
        aspas,
        router_keys: Vec::new(),
        local_outputs_cache,
        warnings,
        stats,
        audit,
    }
}
/// Compatibility wrapper that processes a publication point snapshot.
pub fn process_publication_point_snapshot_for_issuer(
    pack: &PublicationPointSnapshot,
    policy: &Policy,
    issuer_ca_der: &[u8],
    issuer_ca_rsync_uri: Option<&str>,
    issuer_effective_ip: Option<&crate::data_model::rc::IpResourceSet>,
    issuer_effective_as: Option<&crate::data_model::rc::AsResourceSet>,
    validation_time: time::OffsetDateTime,
    timing: Option<&TimingHandle>,
) -> ObjectsOutput {
    process_publication_point_for_issuer(
        pack,
        policy,
        issuer_ca_der,
        issuer_ca_rsync_uri,
        issuer_effective_ip,
        issuer_effective_as,
        validation_time,
        timing,
    )
}

#[derive(Debug, thiserror::Error)]
enum ObjectValidateError {
    #[error("ROA decode failed: {0}")]
    RoaDecode(#[from] RoaDecodeError),

    #[error("ROA embedded EE resource validation failed: {0}")]
    RoaEeResources(#[from] RoaValidateError),

    #[error("ASPA decode failed: {0}")]
    AspaDecode(#[from] AspaDecodeError),

    #[error("ASPA embedded EE resource validation failed: {0}")]
    AspaEeResources(#[from] AspaValidateError),

    #[error("CMS signature verification failed: {0}")]
    Signature(#[from] SignedObjectVerifyError),

    #[error("EE certificate path validation failed: {0}")]
    CertPath(#[from] CertPathError),

    #[error(
        "certificate CRLDistributionPoints URIs missing (cannot select issuer CRL) (RFC 6487 §4.8.6)"
    )]
    MissingCrlDpUris,

    #[error(
        "no CRL available in publication point snapshot (cannot validate certificates) (RFC 9286 §7; RFC 6487 §4.8.6)"
    )]
    MissingCrlInPack,

    #[error(
        "CRL referenced by CRLDistributionPoints not found in publication point snapshot: {0} (RFC 6487 §4.8.6; RFC 9286 §4.2.1)"
    )]
    CrlNotFound(String),

    #[error(
        "issuer effective IP resources missing (cannot validate EE IP resources subset) (RFC 6487 §7.2; RFC 3779 §2.3)"
    )]
    MissingIssuerEffectiveIp,

    #[error(
        "issuer effective AS resources missing (cannot validate EE AS resources subset) (RFC 6487 §7.2; RFC 3779 §3.3)"
    )]
    MissingIssuerEffectiveAs,

    #[error(
        "EE certificate resources are not a subset of issuer effective resources (RFC 6487 §7.2; RFC 3779)"
    )]
    EeResourcesNotSubset,
}

fn process_roa_with_issuer(
    file: &PackFile,
    manifest_rsync_uri: &str,
    issuer_ca_der: &[u8],
    issuer_ca: &ResourceCertificate,
    issuer_spki: &SubjectPublicKeyInfo<'_>,
    issuer_ca_rsync_uri: Option<&str>,
    crl_cache: &mut std::collections::HashMap<String, CachedIssuerCrl>,
    issuer_resources_index: &IssuerResourcesIndex,
    issuer_effective_ip: Option<&crate::data_model::rc::IpResourceSet>,
    issuer_effective_as: Option<&crate::data_model::rc::AsResourceSet>,
    validation_time: time::OffsetDateTime,
    timing: Option<&TimingHandle>,
) -> Result<(Vec<Vrp>, Vec<VcirLocalOutput>), ObjectValidateError> {
    let _decode = timing
        .as_ref()
        .map(|t| t.span_phase("objects_roa_decode_and_validate_total"));
    let roa = RoaObject::decode_der(&file.bytes)?;
    drop(_decode);

    let _ee_profile = timing
        .as_ref()
        .map(|t| t.span_phase("objects_roa_validate_embedded_ee_total"));
    roa.validate_embedded_ee_cert()?;
    drop(_ee_profile);

    let _verify = timing
        .as_ref()
        .map(|t| t.span_phase("objects_roa_verify_signature_total"));
    roa.signed_object.verify()?;
    drop(_verify);

    let ee = &roa.signed_object.signed_data.certificates[0].resource_cert;
    let ee_der = &roa.signed_object.signed_data.certificates[0].raw_der;
    let ee_crldp_uris = ee
        .tbs
        .extensions
        .crl_distribution_points_uris
        .as_ref();
    let issuer_crl_rsync_uri = choose_crl_uri_for_certificate(ee_crldp_uris, crl_cache)?;
    let verified_crl = ensure_issuer_crl_verified(issuer_crl_rsync_uri, crl_cache, issuer_ca_der)?;

    let _cert_path = timing
        .as_ref()
        .map(|t| t.span_phase("objects_roa_validate_ee_cert_path_total"));
    validate_ee_cert_path_with_predecoded_ee(
        ee,
        ee_der,
        issuer_ca,
        issuer_spki,
        &verified_crl.crl,
        &verified_crl.revoked_serials,
        issuer_ca_rsync_uri,
        Some(issuer_crl_rsync_uri),
        validation_time,
    )?;
    drop(_cert_path);

    let _subset = timing
        .as_ref()
        .map(|t| t.span_phase("objects_roa_validate_ee_resources_subset_total"));
    validate_ee_resources_subset(
        &ee,
        issuer_effective_ip,
        issuer_effective_as,
        issuer_resources_index,
    )?;
    drop(_subset);

    let vrps = roa_to_vrps(&roa);
    let source_object_hash = sha256_hex_from_32(&file.sha256);
    let source_ee_cert_hash = crate::audit::sha256_hex(ee.raw_der.as_slice());
    let item_effective_until = PackTime::from_utc_offset_datetime(ee.tbs.validity_not_after);
    let local_outputs = vrps
        .iter()
        .map(|vrp| {
            let prefix = vrp_prefix_to_string(vrp);
            let payload_json = serde_json::json!({
                "asn": vrp.asn,
                "prefix": prefix,
                "max_length": vrp.max_length,
            })
            .to_string();
            let rule_hash = crate::audit::sha256_hex(
                format!(
                    "roa-rule:{}:{}:{}:{}",
                    source_object_hash, vrp.asn, prefix, vrp.max_length
                )
                .as_bytes(),
            );
            VcirLocalOutput {
                output_id: rule_hash.clone(),
                output_type: VcirOutputType::Vrp,
                item_effective_until: item_effective_until.clone(),
                source_object_uri: file.rsync_uri.clone(),
                source_object_type: "roa".to_string(),
                source_object_hash: source_object_hash.clone(),
                source_ee_cert_hash: source_ee_cert_hash.clone(),
                payload_json,
                rule_hash,
                validation_path_hint: vec![
                    manifest_rsync_uri.to_string(),
                    file.rsync_uri.clone(),
                    source_object_hash.clone(),
                ],
            }
        })
        .collect();

    Ok((vrps, local_outputs))
}

fn process_aspa_with_issuer(
    file: &PackFile,
    manifest_rsync_uri: &str,
    issuer_ca_der: &[u8],
    issuer_ca: &ResourceCertificate,
    issuer_spki: &SubjectPublicKeyInfo<'_>,
    issuer_ca_rsync_uri: Option<&str>,
    crl_cache: &mut std::collections::HashMap<String, CachedIssuerCrl>,
    issuer_resources_index: &IssuerResourcesIndex,
    issuer_effective_ip: Option<&crate::data_model::rc::IpResourceSet>,
    issuer_effective_as: Option<&crate::data_model::rc::AsResourceSet>,
    validation_time: time::OffsetDateTime,
    timing: Option<&TimingHandle>,
) -> Result<(AspaAttestation, VcirLocalOutput), ObjectValidateError> {
    let _decode = timing
        .as_ref()
        .map(|t| t.span_phase("objects_aspa_decode_and_validate_total"));
    let aspa = AspaObject::decode_der(&file.bytes)?;
    drop(_decode);

    let _ee_profile = timing
        .as_ref()
        .map(|t| t.span_phase("objects_aspa_validate_embedded_ee_total"));
    aspa.validate_embedded_ee_cert()?;
    drop(_ee_profile);

    let _verify = timing
        .as_ref()
        .map(|t| t.span_phase("objects_aspa_verify_signature_total"));
    aspa.signed_object.verify()?;
    drop(_verify);

    let ee = &aspa.signed_object.signed_data.certificates[0].resource_cert;
    let ee_der = &aspa.signed_object.signed_data.certificates[0].raw_der;
    let ee_crldp_uris = ee
        .tbs
        .extensions
        .crl_distribution_points_uris
        .as_ref();
    let issuer_crl_rsync_uri = choose_crl_uri_for_certificate(ee_crldp_uris, crl_cache)?;
    let verified_crl = ensure_issuer_crl_verified(issuer_crl_rsync_uri, crl_cache, issuer_ca_der)?;

    let _cert_path = timing
        .as_ref()
        .map(|t| t.span_phase("objects_aspa_validate_ee_cert_path_total"));
    validate_ee_cert_path_with_predecoded_ee(
        ee,
        ee_der,
        issuer_ca,
        issuer_spki,
        &verified_crl.crl,
        &verified_crl.revoked_serials,
        issuer_ca_rsync_uri,
        Some(issuer_crl_rsync_uri),
        validation_time,
    )?;
    drop(_cert_path);

    let _subset = timing
        .as_ref()
        .map(|t| t.span_phase("objects_aspa_validate_ee_resources_subset_total"));
    validate_ee_resources_subset(
        &ee,
        issuer_effective_ip,
        issuer_effective_as,
        issuer_resources_index,
    )?;
    drop(_subset);

    let attestation = AspaAttestation {
        customer_as_id: aspa.aspa.customer_as_id,
        provider_as_ids: aspa.aspa.provider_as_ids.clone(),
    };
    let source_object_hash = sha256_hex_from_32(&file.sha256);
    let source_ee_cert_hash = crate::audit::sha256_hex(ee.raw_der.as_slice());
    let item_effective_until = PackTime::from_utc_offset_datetime(ee.tbs.validity_not_after);
    let providers = attestation
        .provider_as_ids
        .iter()
        .map(u32::to_string)
        .collect::<Vec<_>>()
        .join(",");
    let rule_hash = crate::audit::sha256_hex(
        format!(
            "aspa-rule:{}:{}:{}",
            source_object_hash, attestation.customer_as_id, providers
        )
        .as_bytes(),
    );
    let local_output = VcirLocalOutput {
        output_id: rule_hash.clone(),
        output_type: VcirOutputType::Aspa,
        item_effective_until,
        source_object_uri: file.rsync_uri.clone(),
        source_object_type: "aspa".to_string(),
        source_object_hash: source_object_hash.clone(),
        source_ee_cert_hash,
        payload_json: serde_json::json!({
            "customer_as_id": attestation.customer_as_id,
            "provider_as_ids": attestation.provider_as_ids,
        })
        .to_string(),
        rule_hash,
        validation_path_hint: vec![
            manifest_rsync_uri.to_string(),
            file.rsync_uri.clone(),
            source_object_hash,
        ],
    };

    Ok((attestation, local_output))
}


fn vrp_prefix_to_string(vrp: &Vrp) -> String {
    let prefix = &vrp.prefix;
    match prefix.afi {
        RoaAfi::Ipv4 => {
            let addr = std::net::Ipv4Addr::new(
                prefix.addr[0],
                prefix.addr[1],
                prefix.addr[2],
                prefix.addr[3],
            );
            format!("{addr}/{}", prefix.prefix_len)
        }
        RoaAfi::Ipv6 => {
            let mut octets = [0u8; 16];
            octets.copy_from_slice(&prefix.addr[..16]);
            let addr = std::net::Ipv6Addr::from(octets);
            format!("{addr}/{}", prefix.prefix_len)
        }
    }
}

fn choose_crl_uri_for_certificate<'a>(
    crldp_uris: Option<&'a Vec<String>>,
    crl_cache: &std::collections::HashMap<String, CachedIssuerCrl>,
) -> Result<&'a str, ObjectValidateError> {
    if crl_cache.is_empty() {
        return Err(ObjectValidateError::MissingCrlInPack);
    }

    let Some(crldp_uris) = crldp_uris else {
        return Err(ObjectValidateError::MissingCrlDpUris);
    };

    for u in crldp_uris {
        let s = u.as_str();
        if crl_cache.contains_key(s) {
            return Ok(s);
        }
    }
    Err(ObjectValidateError::CrlNotFound(
        crldp_uris
            .iter()
            .map(|u| u.as_str())
            .collect::<Vec<_>>()
            .join(", "),
    ))
}

fn ensure_issuer_crl_verified<'a>(
    crl_rsync_uri: &str,
    crl_cache: &'a mut std::collections::HashMap<String, CachedIssuerCrl>,
    issuer_ca_der: &[u8],
) -> Result<&'a VerifiedIssuerCrl, CertPathError> {
    let entry = crl_cache
        .get_mut(crl_rsync_uri)
        .expect("CRL must exist in cache");
    match entry {
        CachedIssuerCrl::Ok(v) => Ok(v),
        CachedIssuerCrl::Pending(bytes) => {
            let der = std::mem::take(bytes);
            let crl = crate::data_model::crl::RpkixCrl::decode_der(&der)
                .map_err(CertPathError::CrlDecode)?;
            crl.verify_signature_with_issuer_certificate_der(issuer_ca_der)
                .map_err(CertPathError::CrlVerify)?;

            let mut revoked_serials: std::collections::HashSet<Vec<u8>> =
                std::collections::HashSet::with_capacity(crl.revoked_certs.len());
            for rc in &crl.revoked_certs {
                revoked_serials.insert(rc.serial_number.bytes_be.clone());
            }

            *entry = CachedIssuerCrl::Ok(VerifiedIssuerCrl {
                crl,
                revoked_serials,
            });
            match entry {
                CachedIssuerCrl::Ok(v) => Ok(v),
                _ => unreachable!(),
            }
        }
    }
}

fn validate_ee_resources_subset(
    ee: &ResourceCertificate,
    issuer_effective_ip: Option<&crate::data_model::rc::IpResourceSet>,
    issuer_effective_as: Option<&crate::data_model::rc::AsResourceSet>,
    issuer_resources_index: &IssuerResourcesIndex,
) -> Result<(), ObjectValidateError> {
    if let Some(child_ip) = ee.tbs.extensions.ip_resources.as_ref() {
        let Some(parent_ip) = issuer_effective_ip else {
            return Err(ObjectValidateError::MissingIssuerEffectiveIp);
        };
        if !ip_resources_is_subset_indexed(child_ip, parent_ip, issuer_resources_index) {
            return Err(ObjectValidateError::EeResourcesNotSubset);
        }
    }

    if let Some(child_as) = ee.tbs.extensions.as_resources.as_ref() {
        let Some(parent_as) = issuer_effective_as else {
            return Err(ObjectValidateError::MissingIssuerEffectiveAs);
        };
        if !as_resources_is_subset_indexed(child_as, parent_as, issuer_resources_index) {
            return Err(ObjectValidateError::EeResourcesNotSubset);
        }
    }

    Ok(())
}

fn as_resources_is_subset(child: &AsResourceSet, parent: &AsResourceSet) -> bool {
    as_choice_subset(child.asnum.as_ref(), parent.asnum.as_ref())
        && as_choice_subset(child.rdi.as_ref(), parent.rdi.as_ref())
}

fn as_resources_is_subset_indexed(
    child: &AsResourceSet,
    parent: &AsResourceSet,
    idx: &IssuerResourcesIndex,
) -> bool {
    let _ = parent;
    as_choice_subset_indexed(child.asnum.as_ref(), idx.asnum.as_deref())
        && as_choice_subset_indexed(child.rdi.as_ref(), idx.rdi.as_deref())
}

fn as_choice_subset_indexed(
    child: Option<&AsIdentifierChoice>,
    parent_intervals: Option<&[(u32, u32)]>,
) -> bool {
    let Some(child) = child else {
        return true;
    };
    let Some(parent_intervals) = parent_intervals else {
        return false;
    };

    if matches!(child, AsIdentifierChoice::Inherit) {
        return false;
    }

    let child_intervals = as_choice_to_merged_intervals(child);
    for (cmin, cmax) in &child_intervals {
        if !as_interval_is_covered(parent_intervals, *cmin, *cmax) {
            return false;
        }
    }
    true
}

fn as_choice_subset(
    child: Option<&AsIdentifierChoice>,
    parent: Option<&AsIdentifierChoice>,
) -> bool {
    let Some(child) = child else {
        return true;
    };
    let Some(parent) = parent else {
        return false;
    };

    match (child, parent) {
        (AsIdentifierChoice::Inherit, _) => return false,
        (_, AsIdentifierChoice::Inherit) => return false,
        _ => {}
    }

    let child_intervals = as_choice_to_merged_intervals(child);
    let parent_intervals = as_choice_to_merged_intervals(parent);
    for (cmin, cmax) in &child_intervals {
        if !as_interval_is_covered(&parent_intervals, *cmin, *cmax) {
            return false;
        }
    }
    true
}

fn as_choice_to_merged_intervals(choice: &AsIdentifierChoice) -> Vec<(u32, u32)> {
    let mut v = Vec::new();
    match choice {
        AsIdentifierChoice::Inherit => {}
        AsIdentifierChoice::AsIdsOrRanges(items) => {
            for item in items {
                match item {
                    crate::data_model::rc::AsIdOrRange::Id(id) => v.push((*id, *id)),
                    crate::data_model::rc::AsIdOrRange::Range { min, max } => v.push((*min, *max)),
                }
            }
        }
    }
    v.sort_by_key(|(a, _)| *a);
    merge_as_intervals(&v)
}

fn merge_as_intervals(v: &[(u32, u32)]) -> Vec<(u32, u32)> {
    let mut out: Vec<(u32, u32)> = Vec::new();
    for (min, max) in v {
        let Some(last) = out.last_mut() else {
            out.push((*min, *max));
            continue;
        };
        if *min <= last.1.saturating_add(1) {
            last.1 = last.1.max(*max);
            continue;
        }
        out.push((*min, *max));
    }
    out
}

fn as_interval_is_covered(parent: &[(u32, u32)], min: u32, max: u32) -> bool {
    for (pmin, pmax) in parent {
        if *pmin <= min && max <= *pmax {
            return true;
        }
        if *pmin > min {
            break;
        }
    }
    false
}

fn ip_resources_is_subset(
    child: &crate::data_model::rc::IpResourceSet,
    parent: &crate::data_model::rc::IpResourceSet,
) -> bool {
    let parent_by_afi = ip_resources_to_merged_intervals(parent);
    let child_by_afi = match ip_resources_to_merged_intervals_strict(child) {
        Ok(v) => v,
        Err(()) => return false,
    };

    for (afi, child_intervals) in child_by_afi {
        let Some(parent_intervals) = parent_by_afi.get(&afi) else {
            return false;
        };
        for (cmin, cmax) in &child_intervals {
            if !interval_is_covered(parent_intervals, cmin, cmax) {
                return false;
            }
        }
    }
    true
}

fn ip_resources_is_subset_indexed(
    child: &crate::data_model::rc::IpResourceSet,
    parent: &crate::data_model::rc::IpResourceSet,
    idx: &IssuerResourcesIndex,
) -> bool {
    let _ = parent;

    for fam in &child.families {
        let parent_intervals = match fam.afi {
            crate::data_model::rc::Afi::Ipv4 => idx.ip_v4.as_deref(),
            crate::data_model::rc::Afi::Ipv6 => idx.ip_v6.as_deref(),
        };
        let Some(parent_intervals) = parent_intervals else {
            return false;
        };
        let items = match &fam.choice {
            IpAddressChoice::Inherit => return false,
            IpAddressChoice::AddressesOrRanges(items) => items,
        };

        let mut child_intervals: Vec<(Vec<u8>, Vec<u8>)> = Vec::new();
        for item in items {
            match item {
                IpAddressOrRange::Prefix(p) => child_intervals.push(prefix_to_range(p)),
                IpAddressOrRange::Range(r) => child_intervals.push((r.min.clone(), r.max.clone())),
            }
        }
        if child_intervals.is_empty() {
            continue;
        }
        child_intervals.sort_by(|(a, _), (b, _)| a.cmp(b));
        merge_ip_intervals_in_place(&mut child_intervals);
        if !intervals_are_covered(parent_intervals, &child_intervals) {
            return false;
        }
    }
    true
}

#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash)]
enum AfiKey {
    V4,
    V6,
}

fn ip_resources_to_merged_intervals(
    set: &crate::data_model::rc::IpResourceSet,
) -> std::collections::HashMap<AfiKey, Vec<(Vec<u8>, Vec<u8>)>> {
    let mut m: std::collections::HashMap<AfiKey, Vec<(Vec<u8>, Vec<u8>)>> =
        std::collections::HashMap::new();

    for fam in &set.families {
        let afi = match fam.afi {
            crate::data_model::rc::Afi::Ipv4 => AfiKey::V4,
            crate::data_model::rc::Afi::Ipv6 => AfiKey::V6,
        };
        match &fam.choice {
            IpAddressChoice::Inherit => {
                // Effective resource sets should not contain inherit, but if they do we treat it
                // as "unknown" by leaving it empty here (subset checks will fail).
            }
            IpAddressChoice::AddressesOrRanges(items) => {
                let ent = m.entry(afi).or_default();
                for item in items {
                    match item {
                        IpAddressOrRange::Prefix(p) => ent.push(prefix_to_range(p)),
                        IpAddressOrRange::Range(r) => ent.push((r.min.clone(), r.max.clone())),
                    }
                }
            }
        }
    }

    for (_afi, v) in m.iter_mut() {
        v.sort_by(|(a, _), (b, _)| a.cmp(b));
        merge_ip_intervals_in_place(v);
    }

    m
}

fn ip_resources_to_merged_intervals_strict(
    set: &crate::data_model::rc::IpResourceSet,
) -> Result<std::collections::HashMap<AfiKey, Vec<(Vec<u8>, Vec<u8>)>>, ()> {
    let mut m: std::collections::HashMap<AfiKey, Vec<(Vec<u8>, Vec<u8>)>> =
        std::collections::HashMap::new();

    for fam in &set.families {
        let afi = match fam.afi {
            crate::data_model::rc::Afi::Ipv4 => AfiKey::V4,
            crate::data_model::rc::Afi::Ipv6 => AfiKey::V6,
        };
        match &fam.choice {
            IpAddressChoice::Inherit => return Err(()),
            IpAddressChoice::AddressesOrRanges(items) => {
                let ent = m.entry(afi).or_default();
                for item in items {
                    match item {
                        IpAddressOrRange::Prefix(p) => ent.push(prefix_to_range(p)),
                        IpAddressOrRange::Range(r) => ent.push((r.min.clone(), r.max.clone())),
                    }
                }
            }
        }
    }

    for (_afi, v) in m.iter_mut() {
        v.sort_by(|(a, _), (b, _)| a.cmp(b));
        merge_ip_intervals_in_place(v);
    }

    Ok(m)
}

fn merge_ip_intervals_in_place(v: &mut Vec<(Vec<u8>, Vec<u8>)>) {
    if v.is_empty() {
        return;
    }
    let mut out: Vec<(Vec<u8>, Vec<u8>)> = Vec::with_capacity(v.len());
    for (min, max) in v.drain(..) {
        let Some(last) = out.last_mut() else {
            out.push((min, max));
            continue;
        };
        if bytes_leq(&min, &last.1) || bytes_is_next(&min, &last.1) {
            if bytes_leq(&last.1, &max) {
                last.1 = max;
            }
            continue;
        }
        out.push((min, max));
    }
    *v = out;
}

fn interval_is_covered(parent: &[(Vec<u8>, Vec<u8>)], min: &[u8], max: &[u8]) -> bool {
    for (pmin, pmax) in parent {
        if bytes_leq(pmin, min) && bytes_leq(max, pmax) {
            return true;
        }
        if pmin.as_slice() > min {
            break;
        }
    }
    false
}

fn intervals_are_covered(parent: &[(Vec<u8>, Vec<u8>)], child: &[(Vec<u8>, Vec<u8>)]) -> bool {
    let mut i = 0usize;
    for (cmin, cmax) in child {
        while i < parent.len() && parent[i].1.as_slice() < cmin.as_slice() {
            i += 1;
        }
        if i >= parent.len() {
            return false;
        }
        let (pmin, pmax) = &parent[i];
        if !bytes_leq(pmin, cmin) || !bytes_leq(cmax, pmax) {
            return false;
        }
    }
    true
}

fn prefix_to_range(prefix: &RcIpPrefix) -> (Vec<u8>, Vec<u8>) {
    let mut min = prefix.addr.clone();
    let mut max = prefix.addr.clone();
    let bitlen = prefix.afi.ub();
    let plen = prefix.prefix_len.min(bitlen);
    for bit in plen..bitlen {
        let byte = (bit / 8) as usize;
        let offset = 7 - (bit % 8);
        let mask = 1u8 << offset;
        min[byte] &= !mask;
        max[byte] |= mask;
    }
    (min, max)
}

fn bytes_leq(a: &[u8], b: &[u8]) -> bool {
    a <= b
}

fn increment_bytes(v: &[u8]) -> Vec<u8> {
    let mut out = v.to_vec();
    for i in (0..out.len()).rev() {
        if out[i] != 0xFF {
            out[i] += 1;
            for j in i + 1..out.len() {
                out[j] = 0;
            }
            return out;
        }
    }
    vec![0u8; out.len()]
}

fn bytes_is_next(a: &[u8], b: &[u8]) -> bool {
    if a.len() != b.len() {
        return false;
    }
    let mut carry: u16 = 1;
    for i in (0..b.len()).rev() {
        let sum = (b[i] as u16) + carry;
        let expected = (sum & 0xFF) as u8;
        carry = sum >> 8;
        if a[i] != expected {
            return false;
        }
    }
    true
}

fn build_issuer_resources_index(
    issuer_effective_ip: Option<&crate::data_model::rc::IpResourceSet>,
    issuer_effective_as: Option<&crate::data_model::rc::AsResourceSet>,
) -> IssuerResourcesIndex {
    let mut idx = IssuerResourcesIndex::default();

    if let Some(ip) = issuer_effective_ip {
        let mut v4: Vec<(Vec<u8>, Vec<u8>)> = Vec::new();
        let mut v6: Vec<(Vec<u8>, Vec<u8>)> = Vec::new();
        for fam in &ip.families {
            let ent = match fam.afi {
                crate::data_model::rc::Afi::Ipv4 => &mut v4,
                crate::data_model::rc::Afi::Ipv6 => &mut v6,
            };
            match &fam.choice {
                IpAddressChoice::Inherit => {
                    // Effective resources should not contain inherit; leave empty so subset fails.
                }
                IpAddressChoice::AddressesOrRanges(items) => {
                    for item in items {
                        match item {
                            IpAddressOrRange::Prefix(p) => ent.push(prefix_to_range(p)),
                            IpAddressOrRange::Range(r) => ent.push((r.min.clone(), r.max.clone())),
                        }
                    }
                }
            }
        }
        if !v4.is_empty() {
            v4.sort_by(|(a, _), (b, _)| a.cmp(b));
            merge_ip_intervals_in_place(&mut v4);
            idx.ip_v4 = Some(v4);
        }
        if !v6.is_empty() {
            v6.sort_by(|(a, _), (b, _)| a.cmp(b));
            merge_ip_intervals_in_place(&mut v6);
            idx.ip_v6 = Some(v6);
        }
    }

    if let Some(asr) = issuer_effective_as {
        if let Some(choice) = asr.asnum.as_ref() {
            if !matches!(choice, AsIdentifierChoice::Inherit) {
                idx.asnum = Some(as_choice_to_merged_intervals(choice));
            }
        }
        if let Some(choice) = asr.rdi.as_ref() {
            if !matches!(choice, AsIdentifierChoice::Inherit) {
                idx.rdi = Some(as_choice_to_merged_intervals(choice));
            }
        }
    }

    idx
}

fn roa_to_vrps(roa: &RoaObject) -> Vec<Vrp> {
    let asn = roa.roa.as_id;
    let mut out = Vec::new();
    for fam in &roa.roa.ip_addr_blocks {
        for entry in &fam.addresses {
            let max_length = entry.max_length.unwrap_or(entry.prefix.prefix_len);
            out.push(Vrp {
                asn,
                prefix: entry.prefix.clone(),
                max_length,
            });
        }
    }
    out
}

#[allow(dead_code)]
fn roa_afi_to_string(afi: RoaAfi) -> &'static str {
    match afi {
        RoaAfi::Ipv4 => "ipv4",
        RoaAfi::Ipv6 => "ipv6",
    }
}

#[cfg(test)]
mod tests {
    use super::*;
    use crate::data_model::rc::{
        Afi, AsIdOrRange, AsIdentifierChoice, IpAddressFamily, IpAddressOrRange, IpAddressRange,
        IpPrefix, IpResourceSet,
    };
    use std::collections::HashMap;

    fn fixture_bytes(path: &str) -> Vec<u8> {
        std::fs::read(std::path::PathBuf::from(env!("CARGO_MANIFEST_DIR")).join(path))
            .unwrap_or_else(|e| panic!("read fixture {path}: {e}"))
    }

    #[test]
    fn merge_as_intervals_merges_overlapping_and_adjacent() {
        let v = vec![(1, 2), (3, 5), (10, 10), (11, 12)];
        let merged = merge_as_intervals(&v);
        assert_eq!(merged, vec![(1, 5), (10, 12)]);
    }

    #[test]
    fn as_choice_subset_rejects_inherit() {
        let child = Some(&AsIdentifierChoice::Inherit);
        let parent = Some(&AsIdentifierChoice::AsIdsOrRanges(vec![
            AsIdOrRange::Range { min: 1, max: 10 },
        ]));
        assert!(!as_choice_subset(child, parent));
    }

    #[test]
    fn as_choice_subset_checks_ranges() {
        let child = Some(&AsIdentifierChoice::AsIdsOrRanges(vec![
            AsIdOrRange::Id(5),
            AsIdOrRange::Range { min: 7, max: 9 },
        ]));
        let parent = Some(&AsIdentifierChoice::AsIdsOrRanges(vec![
            AsIdOrRange::Range { min: 1, max: 10 },
        ]));
        assert!(as_choice_subset(child, parent));
    }

    #[test]
    fn ip_resources_is_subset_accepts_prefixes_and_ranges() {
        let parent = IpResourceSet {
            families: vec![IpAddressFamily {
                afi: Afi::Ipv4,
                choice: IpAddressChoice::AddressesOrRanges(vec![
                    IpAddressOrRange::Prefix(IpPrefix {
                        afi: Afi::Ipv4,
                        prefix_len: 8,
                        addr: vec![10, 0, 0, 0],
                    }),
                    IpAddressOrRange::Range(IpAddressRange {
                        min: vec![192, 0, 2, 0],
                        max: vec![192, 0, 2, 255],
                    }),
                ]),
            }],
        };
        let child = IpResourceSet {
            families: vec![IpAddressFamily {
                afi: Afi::Ipv4,
                choice: IpAddressChoice::AddressesOrRanges(vec![
                    IpAddressOrRange::Prefix(IpPrefix {
                        afi: Afi::Ipv4,
                        prefix_len: 16,
                        addr: vec![10, 1, 0, 0],
                    }),
                    IpAddressOrRange::Range(IpAddressRange {
                        min: vec![192, 0, 2, 10],
                        max: vec![192, 0, 2, 20],
                    }),
                ]),
            }],
        };

        assert!(ip_resources_is_subset(&child, &parent));
    }

    #[test]
    fn ip_resources_is_subset_rejects_inherit_in_child() {
        let parent = IpResourceSet {
            families: vec![IpAddressFamily {
                afi: Afi::Ipv6,
                choice: IpAddressChoice::AddressesOrRanges(vec![IpAddressOrRange::Prefix(
                    IpPrefix {
                        afi: Afi::Ipv6,
                        prefix_len: 32,
                        addr: vec![0x20, 0x01, 0x0d, 0xb8, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0],
                    },
                )]),
            }],
        };
        let child = IpResourceSet {
            families: vec![IpAddressFamily {
                afi: Afi::Ipv6,
                choice: IpAddressChoice::Inherit,
            }],
        };
        assert!(!ip_resources_is_subset(&child, &parent));
    }

    #[test]
    fn increment_bytes_wraps_all_ff_to_zero() {
        assert_eq!(increment_bytes(&[0xFF, 0xFF]), vec![0x00, 0x00]);
    }

    #[test]
    fn merge_ip_intervals_merges_contiguous() {
        let mut v = vec![
            (vec![0, 0, 0, 0], vec![0, 0, 0, 10]),
            (vec![0, 0, 0, 11], vec![0, 0, 0, 20]),
        ];
        merge_ip_intervals_in_place(&mut v);
        assert_eq!(v, vec![(vec![0, 0, 0, 0], vec![0, 0, 0, 20])]);
    }

    #[test]
    fn choose_crl_for_certificate_reports_missing_crl_in_snapshot() {
        let roa_der =
            fixture_bytes("tests/fixtures/repository/rpki.cernet.net/repo/cernet/0/AS4538.roa");
        let roa = RoaObject::decode_der(&roa_der).expect("decode roa");
        let ee_crldp_uris = roa.signed_object.signed_data.certificates[0]
            .resource_cert
            .tbs
            .extensions
            .crl_distribution_points_uris
            .as_ref();
        let crl_cache: HashMap<String, CachedIssuerCrl> = HashMap::new();
        let err = choose_crl_uri_for_certificate(ee_crldp_uris, &crl_cache).unwrap_err();
        assert!(matches!(err, ObjectValidateError::MissingCrlInPack));
    }

    #[test]
    fn choose_crl_for_certificate_reports_missing_crldp_uris() {
        let mut crl_cache: HashMap<String, CachedIssuerCrl> = HashMap::new();
        crl_cache.insert(
            "rsync://example.test/a.crl".to_string(),
            CachedIssuerCrl::Pending(vec![0x01]),
        );
        let err = choose_crl_uri_for_certificate(None, &crl_cache).unwrap_err();
        assert!(matches!(err, ObjectValidateError::MissingCrlDpUris));
    }

    #[test]
    fn choose_crl_for_certificate_prefers_matching_crldp_uri_in_order() {
        let roa_der =
            fixture_bytes("tests/fixtures/repository/rpki.cernet.net/repo/cernet/0/AS4538.roa");
        let roa = RoaObject::decode_der(&roa_der).expect("decode roa");
        let ee_crldp_uris = roa.signed_object.signed_data.certificates[0]
            .resource_cert
            .tbs
            .extensions
            .crl_distribution_points_uris
            .as_ref()
            .expect("fixture ee has crldp");

        let matching_uri = ee_crldp_uris[0].as_str().to_string();
        let mut crl_cache: HashMap<String, CachedIssuerCrl> = HashMap::new();
        crl_cache.insert(
            "rsync://example.test/other.crl".to_string(),
            CachedIssuerCrl::Pending(vec![0x00]),
        );
        crl_cache.insert(matching_uri.clone(), CachedIssuerCrl::Pending(vec![0x01]));

        let uri = choose_crl_uri_for_certificate(Some(ee_crldp_uris), &crl_cache).unwrap();
        assert_eq!(uri, matching_uri);
    }

    #[test]
    fn choose_crl_for_certificate_reports_not_found_when_crldp_does_not_match_snapshot() {
        let roa_der =
            fixture_bytes("tests/fixtures/repository/rpki.cernet.net/repo/cernet/0/AS4538.roa");
        let roa = RoaObject::decode_der(&roa_der).expect("decode roa");
        let ee_crldp_uris = roa.signed_object.signed_data.certificates[0]
            .resource_cert
            .tbs
            .extensions
            .crl_distribution_points_uris
            .as_ref();

        let mut crl_cache: HashMap<String, CachedIssuerCrl> = HashMap::new();
        crl_cache.insert(
            "rsync://example.test/other.crl".to_string(),
            CachedIssuerCrl::Pending(vec![0x01]),
        );
        let err = choose_crl_uri_for_certificate(ee_crldp_uris, &crl_cache).unwrap_err();
        assert!(matches!(err, ObjectValidateError::CrlNotFound(_)));
    }

    #[test]
    fn validate_ee_resources_subset_reports_missing_issuer_effective_ip() {
        let roa_path = std::path::PathBuf::from(env!("CARGO_MANIFEST_DIR"))
            .join("tests/fixtures/repository/rpki.cernet.net/repo/cernet/0/AS4538.roa");
        let roa_der = std::fs::read(roa_path).expect("read roa");
        let roa = RoaObject::decode_der(&roa_der).expect("decode roa");
        let ee = &roa.signed_object.signed_data.certificates[0].resource_cert;

        let idx = IssuerResourcesIndex::default();
        let err = validate_ee_resources_subset(ee, None, None, &idx).unwrap_err();
        assert!(matches!(err, ObjectValidateError::MissingIssuerEffectiveIp));
    }

    #[test]
    fn validate_ee_resources_subset_reports_missing_issuer_effective_as() {
        let aspa_path = std::path::PathBuf::from(env!("CARGO_MANIFEST_DIR")).join(
            "tests/fixtures/repository/chloe.sobornost.net/rpki/RIPE-nljobsnijders/5m80fwYws_3FiFD7JiQjAqZ1RYQ.asa",
        );
        let aspa_der = std::fs::read(aspa_path).expect("read aspa");
        let aspa = AspaObject::decode_der(&aspa_der).expect("decode aspa");
        let ee = &aspa.signed_object.signed_data.certificates[0].resource_cert;

        let issuer_ip = IpResourceSet {
            families: vec![IpAddressFamily {
                afi: Afi::Ipv6,
                choice: IpAddressChoice::AddressesOrRanges(vec![IpAddressOrRange::Prefix(
                    IpPrefix {
                        afi: Afi::Ipv6,
                        prefix_len: 32,
                        addr: vec![0x20, 0x01, 0x0d, 0xb8, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0],
                    },
                )]),
            }],
        };

        let idx = build_issuer_resources_index(Some(&issuer_ip), None);
        let err = validate_ee_resources_subset(ee, Some(&issuer_ip), None, &idx).unwrap_err();
        assert!(matches!(err, ObjectValidateError::MissingIssuerEffectiveAs));
    }

    #[test]
    fn validate_ee_resources_subset_reports_not_subset() {
        let roa_path = std::path::PathBuf::from(env!("CARGO_MANIFEST_DIR"))
            .join("tests/fixtures/repository/rpki.cernet.net/repo/cernet/0/AS4538.roa");
        let roa_der = std::fs::read(roa_path).expect("read roa");
        let roa = RoaObject::decode_der(&roa_der).expect("decode roa");
        let ee = &roa.signed_object.signed_data.certificates[0].resource_cert;

        // Unrelated parent resources.
        let issuer_ip = IpResourceSet {
            families: vec![IpAddressFamily {
                afi: Afi::Ipv6,
                choice: IpAddressChoice::AddressesOrRanges(vec![IpAddressOrRange::Prefix(
                    IpPrefix {
                        afi: Afi::Ipv6,
                        prefix_len: 32,
                        addr: vec![0x26, 0x20, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0],
                    },
                )]),
            }],
        };

        let idx = build_issuer_resources_index(Some(&issuer_ip), None);
        let err = validate_ee_resources_subset(ee, Some(&issuer_ip), None, &idx).unwrap_err();
        assert!(matches!(err, ObjectValidateError::EeResourcesNotSubset));
    }

    #[test]
    fn extra_rfc_refs_for_crl_selection_distinguishes_crl_errors() {
        assert_eq!(
            extra_rfc_refs_for_crl_selection(&ObjectValidateError::MissingCrlDpUris),
            RFC_CRLDP
        );
        assert_eq!(
            extra_rfc_refs_for_crl_selection(&ObjectValidateError::CrlNotFound(
                "rsync://example.test/x.crl".to_string(),
            )),
            RFC_CRLDP_AND_LOCKED_PACK
        );
        assert!(
            extra_rfc_refs_for_crl_selection(&ObjectValidateError::MissingCrlInPack).is_empty()
        );
    }

    #[test]
    fn as_subset_helpers_cover_success_and_failure_paths() {
        let child = AsIdentifierChoice::AsIdsOrRanges(vec![
            AsIdOrRange::Id(5),
            AsIdOrRange::Range { min: 7, max: 9 },
        ]);
        let parent =
            AsIdentifierChoice::AsIdsOrRanges(vec![AsIdOrRange::Range { min: 1, max: 10 }]);
        let parent_intervals = [(1, 10)];

        assert!(as_choice_subset(None, Some(&parent)));
        assert!(!as_choice_subset(Some(&child), None));
        assert!(!as_choice_subset(
            Some(&AsIdentifierChoice::Inherit),
            Some(&parent)
        ));
        assert!(!as_choice_subset(
            Some(&child),
            Some(&AsIdentifierChoice::Inherit)
        ));
        assert!(as_choice_subset(Some(&child), Some(&parent)));

        assert!(as_choice_subset_indexed(None, Some(&parent_intervals)));
        assert!(!as_choice_subset_indexed(Some(&child), None));
        assert!(!as_choice_subset_indexed(
            Some(&AsIdentifierChoice::Inherit),
            Some(&parent_intervals),
        ));
        assert!(as_choice_subset_indexed(
            Some(&child),
            Some(&parent_intervals)
        ));
        assert!(!as_choice_subset_indexed(
            Some(&AsIdentifierChoice::AsIdsOrRanges(vec![
                AsIdOrRange::Range { min: 11, max: 12 }
            ])),
            Some(&parent_intervals),
        ));

        let child_set = AsResourceSet {
            asnum: Some(child.clone()),
            rdi: Some(AsIdentifierChoice::AsIdsOrRanges(vec![AsIdOrRange::Id(42)])),
        };
        let parent_set = AsResourceSet {
            asnum: Some(parent.clone()),
            rdi: Some(AsIdentifierChoice::AsIdsOrRanges(vec![
                AsIdOrRange::Range { min: 40, max: 50 },
            ])),
        };
        assert!(as_resources_is_subset(&child_set, &parent_set));
        assert!(as_resources_is_subset_indexed(
            &child_set,
            &parent_set,
            &IssuerResourcesIndex {
                asnum: Some(vec![(1, 10)]),
                rdi: Some(vec![(40, 50)]),
                ..IssuerResourcesIndex::default()
            },
        ));
    }

    #[test]
    fn ip_subset_helpers_cover_strict_and_indexed_paths() {
        let parent = IpResourceSet {
            families: vec![
                IpAddressFamily {
                    afi: Afi::Ipv4,
                    choice: IpAddressChoice::AddressesOrRanges(vec![
                        IpAddressOrRange::Prefix(IpPrefix {
                            afi: Afi::Ipv4,
                            prefix_len: 8,
                            addr: vec![10, 0, 0, 0],
                        }),
                        IpAddressOrRange::Range(IpAddressRange {
                            min: vec![192, 0, 2, 0],
                            max: vec![192, 0, 2, 255],
                        }),
                    ]),
                },
                IpAddressFamily {
                    afi: Afi::Ipv6,
                    choice: IpAddressChoice::Inherit,
                },
            ],
        };
        let child = IpResourceSet {
            families: vec![IpAddressFamily {
                afi: Afi::Ipv4,
                choice: IpAddressChoice::AddressesOrRanges(vec![
                    IpAddressOrRange::Prefix(IpPrefix {
                        afi: Afi::Ipv4,
                        prefix_len: 16,
                        addr: vec![10, 1, 0, 0],
                    }),
                    IpAddressOrRange::Range(IpAddressRange {
                        min: vec![192, 0, 2, 10],
                        max: vec![192, 0, 2, 20],
                    }),
                ]),
            }],
        };
        let strict_bad = IpResourceSet {
            families: vec![IpAddressFamily {
                afi: Afi::Ipv4,
                choice: IpAddressChoice::Inherit,
            }],
        };

        assert!(ip_resources_is_subset(&child, &parent));
        assert!(ip_resources_to_merged_intervals(&parent).contains_key(&AfiKey::V4));
        assert!(ip_resources_to_merged_intervals_strict(&child).is_ok());
        assert!(ip_resources_to_merged_intervals_strict(&strict_bad).is_err());

        let idx = build_issuer_resources_index(
            Some(&parent),
            Some(&AsResourceSet {
                asnum: Some(AsIdentifierChoice::AsIdsOrRanges(vec![
                    AsIdOrRange::Range {
                        min: 64500,
                        max: 64510,
                    },
                ])),
                rdi: Some(AsIdentifierChoice::Inherit),
            }),
        );
        assert!(idx.ip_v4.is_some());
        assert!(idx.ip_v6.is_none());
        assert!(idx.asnum.is_some());
        assert!(idx.rdi.is_none());
        assert!(ip_resources_is_subset_indexed(&child, &parent, &idx));
        assert!(!ip_resources_is_subset_indexed(&strict_bad, &parent, &idx));
    }

    #[test]
    fn interval_and_byte_helpers_cover_edge_cases() {
        let parent = vec![(vec![0, 0, 0, 0], vec![0, 0, 0, 10])];
        assert!(interval_is_covered(&parent, &[0, 0, 0, 1], &[0, 0, 0, 2]));
        assert!(!interval_is_covered(
            &parent,
            &[0, 0, 0, 11],
            &[0, 0, 0, 12]
        ));
        assert!(intervals_are_covered(
            &parent,
            &[(vec![0, 0, 0, 1], vec![0, 0, 0, 2])]
        ));
        assert!(!intervals_are_covered(
            &parent,
            &[(vec![0, 0, 0, 9], vec![0, 0, 0, 11])],
        ));

        let prefix = RcIpPrefix {
            afi: Afi::Ipv4,
            prefix_len: 24,
            addr: vec![203, 0, 113, 7],
        };
        assert_eq!(
            prefix_to_range(&prefix),
            (vec![203, 0, 113, 0], vec![203, 0, 113, 255])
        );
        assert_eq!(increment_bytes(&[0, 0, 0, 255]), vec![0, 0, 1, 0]);
        assert!(bytes_is_next(&[0, 0, 1, 0], &[0, 0, 0, 255]));
        assert!(!bytes_is_next(&[1, 2], &[1]));
    }

    #[test]
    fn merged_interval_helpers_cover_empty_and_break_paths() {
        let mut empty: Vec<(Vec<u8>, Vec<u8>)> = Vec::new();
        merge_ip_intervals_in_place(&mut empty);
        assert!(empty.is_empty());

        let mut v = vec![
            (vec![0, 0, 0, 20], vec![0, 0, 0, 30]),
            (vec![0, 0, 0, 0], vec![0, 0, 0, 10]),
            (vec![0, 0, 0, 11], vec![0, 0, 0, 19]),
        ];
        v.sort_by(|(a, _), (b, _)| a.cmp(b));
        merge_ip_intervals_in_place(&mut v);
        assert_eq!(v, vec![(vec![0, 0, 0, 0], vec![0, 0, 0, 30])]);

        assert_eq!(
            as_choice_to_merged_intervals(&AsIdentifierChoice::AsIdsOrRanges(vec![
                AsIdOrRange::Id(1),
                AsIdOrRange::Range { min: 2, max: 3 },
                AsIdOrRange::Range { min: 7, max: 9 },
            ])),
            vec![(1, 3), (7, 9)]
        );
        assert!(as_interval_is_covered(&[(1, 3), (7, 9)], 2, 3));
        assert!(!as_interval_is_covered(&[(7, 9)], 2, 3));
    }

    #[test]
    fn roa_output_helpers_cover_vrps_and_afi_strings() {
        let roa_der =
            fixture_bytes("tests/fixtures/repository/rpki.cernet.net/repo/cernet/0/AS4538.roa");
        let roa = RoaObject::decode_der(&roa_der).expect("decode roa");
        let vrps = roa_to_vrps(&roa);
        assert!(!vrps.is_empty());
        assert!(vrps.iter().all(|vrp| vrp.asn == roa.roa.as_id));
        assert_eq!(roa_afi_to_string(RoaAfi::Ipv4), "ipv4");
        assert_eq!(roa_afi_to_string(RoaAfi::Ipv6), "ipv6");
    }
}
