use crate::analysis::timing::TimingHandle;
use crate::audit::{
    AuditObjectKind, AuditObjectResult, AuditWarning, ObjectAuditEntry, PublicationPointAudit,
    sha256_hex, sha256_hex_from_32,
};
use crate::audit_downloads::DownloadLogHandle;
use crate::data_model::aspa::AspaObject;
use crate::data_model::crl::RpkixCrl;
use crate::data_model::manifest::ManifestObject;
use crate::data_model::rc::ResourceCertificate;
use crate::data_model::roa::{RoaAfi, RoaObject};
use crate::data_model::router_cert::{
    BgpsecRouterCertificate, BgpsecRouterCertificateDecodeError,
    BgpsecRouterCertificatePathError, BgpsecRouterCertificateProfileError,
};
use crate::fetch::rsync::RsyncFetcher;
use crate::policy::Policy;
use crate::replay::archive::ReplayArchiveIndex;
use crate::replay::delta_archive::ReplayDeltaArchiveIndex;
use crate::report::{RfcRef, Warning};
use crate::storage::{
    PackFile, PackTime, RawByHashEntry, RocksStore,
    ValidatedCaInstanceResult, VcirArtifactKind, VcirArtifactRole, VcirArtifactValidationStatus,
    VcirAuditSummary, VcirChildEntry, VcirInstanceGate, VcirLocalOutput, VcirOutputType,
    VcirRelatedArtifact, VcirSummary,
};
use crate::sync::repo::{
    sync_publication_point, sync_publication_point_replay, sync_publication_point_replay_delta,
};
use crate::sync::rrdp::Fetcher;
use crate::validation::ca_instance::ca_instance_uris_from_ca_certificate;
use crate::validation::ca_path::{
    CaPathError, IssuerEffectiveResourcesIndex, ValidatedSubordinateCaLite,
    validate_subordinate_ca_cert_with_prevalidated_issuer_and_resources,
};
use crate::validation::manifest::{
    ManifestFreshError, PublicationPointData, PublicationPointSource,
    process_manifest_publication_point_fresh_after_repo_sync,
};
use crate::validation::objects::{AspaAttestation, RouterKeyPayload, Vrp, process_publication_point_for_issuer};
use crate::validation::publication_point::PublicationPointSnapshot;
use crate::validation::tree::{
    CaInstanceHandle, DiscoveredChildCaInstance, PublicationPointRunResult, PublicationPointRunner,
};
use std::collections::{HashMap, HashSet};
use std::sync::{Arc, Mutex};

use serde::Deserialize;
use base64::Engine as _;
use serde_json::json;
use x509_parser::prelude::FromDer;
use x509_parser::x509::SubjectPublicKeyInfo;

pub struct Rpkiv1PublicationPointRunner<'a> {
    pub store: &'a RocksStore,
    pub policy: &'a Policy,
    pub http_fetcher: &'a dyn Fetcher,
    pub rsync_fetcher: &'a dyn RsyncFetcher,
    pub validation_time: time::OffsetDateTime,
    pub timing: Option<TimingHandle>,
    pub download_log: Option<DownloadLogHandle>,
    pub replay_archive_index: Option<Arc<ReplayArchiveIndex>>,
    pub replay_delta_index: Option<Arc<ReplayDeltaArchiveIndex>>,
    /// In-run RRDP dedup: when RRDP is enabled, only sync each `rrdp_notification_uri` once per run.
    ///
    /// - If RRDP succeeded for a repo, later publication points referencing that same RRDP repo
    ///   skip network fetches and reuse the already-populated current repository view.
    /// - If RRDP failed for a repo, later publication points skip RRDP attempts and go straight
    ///   to rsync for their own `rsync_base_uri` (still per-publication-point).
    pub rrdp_dedup: bool,
    pub rrdp_repo_cache: Mutex<HashMap<String, bool>>, // notification_uri -> rrdp_ok

    /// In-run rsync dedup: when rsync is used, only sync each `rsync_base_uri` once per run.
    ///
    /// This reduces duplicate rsync network fetches when multiple publication points share the
    /// same `rsync_base_uri` (observed in APNIC full sync timing reports).
    pub rsync_dedup: bool,
    pub rsync_repo_cache: Mutex<HashMap<String, bool>>, // rsync_base_uri -> rsync_ok
}

impl<'a> PublicationPointRunner for Rpkiv1PublicationPointRunner<'a> {
    fn run_publication_point(
        &self,
        ca: &CaInstanceHandle,
    ) -> Result<PublicationPointRunResult, String> {
        let publication_point_started = std::time::Instant::now();
        let _pp_total = self
            .timing
            .as_ref()
            .map(|t| t.span_publication_point(&ca.manifest_rsync_uri));
        if let Some(t) = self.timing.as_ref() {
            t.record_count("publication_points_seen", 1);
            if ca.rrdp_notification_uri.is_some() {
                t.record_count("publication_points_rrdp_notify_present_total", 1);
            } else {
                t.record_count("publication_points_rrdp_notify_missing_total", 1);
            }
        }

        let mut warnings: Vec<Warning> = Vec::new();
        crate::progress_log::emit(
            "publication_point_start",
            serde_json::json!({
                "manifest_rsync_uri": ca.manifest_rsync_uri,
                "publication_point_rsync_uri": ca.publication_point_rsync_uri,
                "rsync_base_uri": ca.rsync_base_uri,
                "rrdp_notification_uri": ca.rrdp_notification_uri,
            }),
        );

        let attempted_rrdp =
            self.policy.sync_preference == crate::policy::SyncPreference::RrdpThenRsync;
        let original_notification_uri = ca.rrdp_notification_uri.as_deref();
        let mut effective_notification_uri = if attempted_rrdp {
            original_notification_uri
        } else {
            None
        };
        let mut skip_sync_due_to_dedup = false;

        if attempted_rrdp && self.rrdp_dedup {
            if let Some(notification_uri) = original_notification_uri {
                if let Some(rrdp_ok) = self
                    .rrdp_repo_cache
                    .lock()
                    .expect("rrdp_repo_cache lock")
                    .get(notification_uri)
                    .copied()
                {
                    if let Some(t) = self.timing.as_ref() {
                        t.record_count("rrdp_repo_dedup_hits", 1);
                    }
                    if rrdp_ok {
                        if let Some(t) = self.timing.as_ref() {
                            t.record_count("rrdp_repo_dedup_rrdp_ok_skip", 1);
                        }
                        skip_sync_due_to_dedup = true;
                    } else {
                        if let Some(t) = self.timing.as_ref() {
                            t.record_count("rrdp_repo_dedup_rrdp_failed_skip", 1);
                        }
                        effective_notification_uri = None;
                    }
                } else if let Some(t) = self.timing.as_ref() {
                    t.record_count("rrdp_repo_dedup_misses", 1);
                }
            }
        }

        if !skip_sync_due_to_dedup && effective_notification_uri.is_none() && self.rsync_dedup {
            let base = self.rsync_fetcher.dedup_key(&ca.rsync_base_uri);
            let hit_ok = self
                .rsync_repo_cache
                .lock()
                .expect("rsync_repo_cache lock")
                .get(&base)
                .copied()
                .unwrap_or(false);
            if hit_ok {
                if let Some(t) = self.timing.as_ref() {
                    t.record_count("rsync_repo_dedup_hits", 1);
                    t.record_count("rsync_repo_dedup_skipped_sync", 1);
                }
                skip_sync_due_to_dedup = true;
            } else if let Some(t) = self.timing.as_ref() {
                t.record_count("rsync_repo_dedup_misses", 1);
            }
        }

        let repo_sync_started = std::time::Instant::now();
        let (repo_sync_ok, repo_sync_err, repo_sync_source): (bool, Option<String>, Option<String>) = if skip_sync_due_to_dedup {
            let source = if effective_notification_uri.is_some() {
                Some("rrdp_dedup_skip".to_string())
            } else {
                Some("rsync_dedup_skip".to_string())
            };
            (true, None, source)
        } else {
            let repo_key = effective_notification_uri.unwrap_or_else(|| ca.rsync_base_uri.as_str());
            let _repo_total = self
                .timing
                .as_ref()
                .map(|t| t.span_phase("repo_sync_total"));
            let _repo_span = self.timing.as_ref().map(|t| t.span_rrdp_repo(repo_key));

            match if let Some(delta_index) = self.replay_delta_index.as_ref() {
                sync_publication_point_replay_delta(
                    self.store,
                    delta_index,
                    effective_notification_uri,
                    &ca.rsync_base_uri,
                    self.http_fetcher,
                    self.rsync_fetcher,
                    self.timing.as_ref(),
                    self.download_log.as_ref(),
                )
            } else if let Some(replay_index) = self.replay_archive_index.as_ref() {
                sync_publication_point_replay(
                    self.store,
                    replay_index,
                    effective_notification_uri,
                    &ca.rsync_base_uri,
                    self.http_fetcher,
                    self.rsync_fetcher,
                    self.timing.as_ref(),
                    self.download_log.as_ref(),
                )
            } else {
                sync_publication_point(
                    self.store,
                    self.policy,
                    effective_notification_uri,
                    &ca.rsync_base_uri,
                    self.http_fetcher,
                    self.rsync_fetcher,
                    self.timing.as_ref(),
                    self.download_log.as_ref(),
                )
            } {
                Ok(res) => {
                    if self.rsync_dedup && res.source == crate::sync::repo::RepoSyncSource::Rsync {
                        let base = self.rsync_fetcher.dedup_key(&ca.rsync_base_uri);
                        self.rsync_repo_cache
                            .lock()
                            .expect("rsync_repo_cache lock")
                            .insert(base, true);
                        if let Some(t) = self.timing.as_ref() {
                            t.record_count("rsync_repo_dedup_mark_ok", 1);
                        }
                    }

                    if attempted_rrdp && self.rrdp_dedup {
                        if let Some(notification_uri) = original_notification_uri {
                            if effective_notification_uri.is_some() {
                                let rrdp_ok = res.source == crate::sync::repo::RepoSyncSource::Rrdp;
                                self.rrdp_repo_cache
                                    .lock()
                                    .expect("rrdp_repo_cache lock")
                                    .insert(notification_uri.to_string(), rrdp_ok);
                                if let Some(t) = self.timing.as_ref() {
                                    if rrdp_ok {
                                        t.record_count("rrdp_repo_dedup_mark_ok", 1);
                                    } else {
                                        t.record_count("rrdp_repo_dedup_mark_failed", 1);
                                    }
                                }
                            }
                        }
                    }

                    warnings.extend(res.warnings);
                    (true, None, Some(repo_sync_source_label(res.source).to_string()))
                }
                Err(e) => {
                    if attempted_rrdp && self.rrdp_dedup {
                        if let Some(notification_uri) = original_notification_uri {
                            if effective_notification_uri.is_some() {
                                self.rrdp_repo_cache
                                    .lock()
                                    .expect("rrdp_repo_cache lock")
                                    .insert(notification_uri.to_string(), false);
                            }
                        }
                    }
                    warnings.push(
                        Warning::new(format!("repo sync failed (fresh processing stopped): {e}"))
                            .with_rfc_refs(&[RfcRef("RFC 8182 §3.4.5"), RfcRef("RFC 9286 §6.6")])
                            .with_context(&ca.rsync_base_uri),
                    );
                    (false, Some(e.to_string()), None)
                }
            }
        };
        let repo_sync_duration_ms = repo_sync_started.elapsed().as_millis() as u64;
        crate::progress_log::emit(
            "publication_point_repo_sync_done",
            serde_json::json!({
                "manifest_rsync_uri": ca.manifest_rsync_uri,
                "publication_point_rsync_uri": ca.publication_point_rsync_uri,
                "repo_sync_ok": repo_sync_ok,
                "repo_sync_source": repo_sync_source,
                "repo_sync_error": repo_sync_err,
                "repo_sync_duration_ms": repo_sync_duration_ms,
            }),
        );

        let fresh_publication_point = {
            let _manifest_total = self
                .timing
                .as_ref()
                .map(|t| t.span_phase("manifest_processing_total"));
            process_manifest_publication_point_fresh_after_repo_sync(
                self.store,
                &ca.manifest_rsync_uri,
                &ca.publication_point_rsync_uri,
                &ca.ca_certificate_der,
                ca.ca_certificate_rsync_uri.as_deref(),
                self.validation_time,
                repo_sync_ok,
                repo_sync_err.as_deref(),
            )
        };

        match fresh_publication_point {
            Ok(fresh_point) => {
                let mut objects = {
                    let _objects_total = self
                        .timing
                        .as_ref()
                        .map(|t| t.span_phase("objects_processing_total"));
                    process_publication_point_for_issuer(
                        &fresh_point,
                        self.policy,
                        &ca.ca_certificate_der,
                        ca.ca_certificate_rsync_uri.as_deref(),
                        ca.effective_ip_resources.as_ref(),
                        ca.effective_as_resources.as_ref(),
                        self.validation_time,
                        self.timing.as_ref(),
                    )
                };

                let out = {
                    let _child_disc_total = self
                        .timing
                        .as_ref()
                        .map(|t| t.span_phase("child_discovery_total"));
                    discover_children_from_fresh_snapshot_with_audit(
                        ca,
                        &fresh_point,
                        self.validation_time,
                        self.timing.as_ref(),
                    )
                };
                let (discovered_children, child_audits, discovered_router_keys) = match out {
                    Ok(out) => (out.children, out.audits, out.router_keys),
                    Err(e) => {
                        warnings.push(
                            Warning::new(format!("child CA discovery failed: {e}"))
                                .with_rfc_refs(&[RfcRef("RFC 6487 §7.2")])
                                .with_context(&ca.manifest_rsync_uri),
                        );
                        (Vec::new(), Vec::new(), Vec::new())
                    }
                };

                objects.router_keys.extend(discovered_router_keys);
                objects
                    .local_outputs_cache
                    .extend(build_router_key_local_outputs(ca, &objects.router_keys));

                let pack = fresh_point.to_publication_point_snapshot();

                persist_vcir_for_fresh_result(
                    self.store,
                    ca,
                    &pack,
                    &objects,
                    &warnings,
                    &child_audits,
                    &discovered_children,
                    self.validation_time,
                )
                .map_err(|e| format!("persist VCIR failed: {e}"))?;

                let audit = build_publication_point_audit_from_snapshot(
                    ca,
                    PublicationPointSource::Fresh,
                    repo_sync_source.as_deref(),
                    Some(repo_sync_duration_ms),
                    repo_sync_err.as_deref(),
                    &pack,
                    &warnings,
                    &objects,
                    &child_audits,
                );
                let result = PublicationPointRunResult {
                    source: PublicationPointSource::Fresh,
                    snapshot: Some(pack),
                    warnings,
                    objects,
                    audit,
                    discovered_children,
                };
                let total_duration_ms = publication_point_started.elapsed().as_millis() as u64;
                crate::progress_log::emit(
                    "publication_point_finish",
                    serde_json::json!({
                        "manifest_rsync_uri": ca.manifest_rsync_uri,
                        "publication_point_rsync_uri": ca.publication_point_rsync_uri,
                        "source": "fresh",
                        "repo_sync_source": repo_sync_source,
                        "repo_sync_duration_ms": repo_sync_duration_ms,
                        "total_duration_ms": total_duration_ms,
                        "warning_count": result.warnings.len(),
                        "vrp_count": result.objects.vrps.len(),
                        "vap_count": result.objects.aspas.len(),
                        "router_key_count": result.objects.router_keys.len(),
                        "child_count": result.discovered_children.len(),
                    }),
                );
                if (total_duration_ms as f64) / 1000.0 >= crate::progress_log::slow_threshold_secs() {
                    crate::progress_log::emit(
                        "publication_point_slow",
                        serde_json::json!({
                            "manifest_rsync_uri": ca.manifest_rsync_uri,
                            "publication_point_rsync_uri": ca.publication_point_rsync_uri,
                            "source": "fresh",
                            "repo_sync_source": repo_sync_source,
                            "repo_sync_duration_ms": repo_sync_duration_ms,
                            "total_duration_ms": total_duration_ms,
                        }),
                    );
                }
                Ok(result)
            }
            Err(fresh_err) => match self.policy.ca_failed_fetch_policy {
                crate::policy::CaFailedFetchPolicy::StopAllOutput => {
                    let total_duration_ms = publication_point_started.elapsed().as_millis() as u64;
                    crate::progress_log::emit(
                        "publication_point_finish",
                        serde_json::json!({
                            "manifest_rsync_uri": ca.manifest_rsync_uri,
                            "publication_point_rsync_uri": ca.publication_point_rsync_uri,
                            "source": "error",
                            "repo_sync_source": repo_sync_source,
                            "repo_sync_duration_ms": repo_sync_duration_ms,
                            "total_duration_ms": total_duration_ms,
                            "error": fresh_err.to_string(),
                        }),
                    );
                    Err(format!("{fresh_err}"))
                }
                crate::policy::CaFailedFetchPolicy::ReuseCurrentInstanceVcir => {
                    let projection = project_current_instance_vcir_on_failed_fetch(
                        self.store,
                        ca,
                        &fresh_err,
                        self.validation_time,
                    )
                    .map_err(|e| format!("failed fetch VCIR projection failed: {e}"))?;
                    warnings.extend(projection.warnings.clone());
                    let audit = build_publication_point_audit_from_vcir(
                        ca,
                        projection.source,
                        repo_sync_source.as_deref(),
                        Some(repo_sync_duration_ms),
                        repo_sync_err.as_deref(),
                        projection.vcir.as_ref(),
                        projection.snapshot.as_ref(),
                        &warnings,
                        &projection.objects,
                        &projection.child_audits,
                    );
                    let result = PublicationPointRunResult {
                        source: projection.source,
                        snapshot: projection.snapshot,
                        warnings,
                        objects: projection.objects,
                        audit,
                        discovered_children: projection.discovered_children,
                    };
                    let total_duration_ms = publication_point_started.elapsed().as_millis() as u64;
                    crate::progress_log::emit(
                        "publication_point_finish",
                        serde_json::json!({
                            "manifest_rsync_uri": ca.manifest_rsync_uri,
                            "publication_point_rsync_uri": ca.publication_point_rsync_uri,
                            "source": source_label(result.source),
                            "repo_sync_source": repo_sync_source,
                            "repo_sync_duration_ms": repo_sync_duration_ms,
                            "total_duration_ms": total_duration_ms,
                            "warning_count": result.warnings.len(),
                            "vrp_count": result.objects.vrps.len(),
                            "vap_count": result.objects.aspas.len(),
                            "router_key_count": result.objects.router_keys.len(),
                            "child_count": result.discovered_children.len(),
                        }),
                    );
                    if (total_duration_ms as f64) / 1000.0 >= crate::progress_log::slow_threshold_secs() {
                        crate::progress_log::emit(
                            "publication_point_slow",
                            serde_json::json!({
                                "manifest_rsync_uri": ca.manifest_rsync_uri,
                                "publication_point_rsync_uri": ca.publication_point_rsync_uri,
                                "source": source_label(result.source),
                                "repo_sync_source": repo_sync_source,
                                "repo_sync_duration_ms": repo_sync_duration_ms,
                                "total_duration_ms": total_duration_ms,
                            }),
                        );
                    }
                    Ok(result)
                }
            },
        }
    }
}

struct ChildDiscoveryOutput {
    children: Vec<DiscoveredChildCaInstance>,
    audits: Vec<ObjectAuditEntry>,
    router_keys: Vec<RouterKeyPayload>,
}

#[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)]
struct VcirReuseProjection {
    source: PublicationPointSource,
    vcir: Option<ValidatedCaInstanceResult>,
    snapshot: Option<PublicationPointSnapshot>,
    objects: crate::validation::objects::ObjectsOutput,
    child_audits: Vec<ObjectAuditEntry>,
    discovered_children: Vec<DiscoveredChildCaInstance>,
    warnings: Vec<Warning>,
}

#[derive(Debug, Deserialize)]
struct VcirVrpPayload {
    asn: u32,
    prefix: String,
    max_length: u16,
}

#[derive(Debug, Deserialize)]
struct VcirAspaPayload {
    customer_as_id: u32,
    provider_as_ids: Vec<u32>,
}

#[derive(Debug, Deserialize)]
struct VcirRouterKeyPayload {
    as_id: u32,
    ski_hex: String,
    spki_der_base64: String,
}

fn discover_children_from_fresh_snapshot_with_audit<P: PublicationPointData>(
    issuer: &CaInstanceHandle,
    publication_point: &P,
    validation_time: time::OffsetDateTime,
    timing: Option<&TimingHandle>,
) -> Result<ChildDiscoveryOutput, String> {
    let locked_files = publication_point.files();
    let issuer_ca_der = issuer.ca_certificate_der.as_slice();
    // Issuer CA is only required when we actually attempt to validate a subordinate CA. For some
    // audit-only error paths (e.g., missing CRL in the snapshot), we still want discovery to succeed.
    let issuer_ca_decode_error: Option<String>;
    let issuer_ca = match crate::data_model::rc::ResourceCertificate::decode_der(issuer_ca_der) {
        Ok(v) => {
            issuer_ca_decode_error = None;
            Some(v)
        }
        Err(e) => {
            issuer_ca_decode_error = Some(format!(
                "issuer CA decode failed: {e} (RFC 5280 §4.1; RFC 6487 §4)"
            ));
            None
        }
    };

    let issuer_spki_error: Option<String>;
    let issuer_spki: Option<SubjectPublicKeyInfo<'_>> = if let Some(ca) = issuer_ca.as_ref() {
        match SubjectPublicKeyInfo::from_der(&ca.tbs.subject_public_key_info) {
            Ok((rem, spki)) if rem.is_empty() => {
                issuer_spki_error = None;
                Some(spki)
            }
            Ok((rem, _)) => {
                issuer_spki_error = Some(format!(
                    "trailing bytes after issuer SubjectPublicKeyInfo DER: {} bytes (DER; RFC 5280 §4.1.2.7)",
                    rem.len()
                ));
                None
            }
            Err(e) => {
                issuer_spki_error = Some(format!(
                    "issuer SubjectPublicKeyInfo parse error: {e} (RFC 5280 §4.1.2.7)"
                ));
                None
            }
        }
    } else {
        issuer_spki_error = issuer_ca_decode_error.clone();
        None
    };

    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 mut out: Vec<DiscoveredChildCaInstance> = Vec::new();
    let mut audits: Vec<ObjectAuditEntry> = Vec::new();
    let mut router_keys: Vec<RouterKeyPayload> = Vec::new();
    let issuer_resources_index = IssuerEffectiveResourcesIndex::from_effective_resources(
        issuer.effective_ip_resources.as_ref(),
        issuer.effective_as_resources.as_ref(),
    )
    .map_err(|e| format!("build issuer effective resources index failed: {e}"))?;

    let mut cer_seen: u64 = 0;
    let mut ca_skipped_not_ca: u64 = 0;
    let mut ca_ok: u64 = 0;
    let mut ca_error: u64 = 0;
    let mut router_ok: u64 = 0;
    let mut router_error: u64 = 0;
    let mut router_skipped_non_router: u64 = 0;
    let mut crl_select_error: u64 = 0;
    let mut uri_discovery_error: u64 = 0;

    let mut select_crl_nanos: u64 = 0;
    let mut child_decode_nanos: u64 = 0;
    let mut validate_sub_ca_nanos: u64 = 0;
    let mut validate_router_nanos: u64 = 0;
    let mut uri_discovery_nanos: u64 = 0;
    let mut enqueue_nanos: u64 = 0;

    let mut eff_ip_items_bucket_le_10: u64 = 0;
    let mut eff_ip_items_bucket_le_100: u64 = 0;
    let mut eff_ip_items_bucket_gt_100: u64 = 0;
    let mut eff_as_items_bucket_le_10: u64 = 0;
    let mut eff_as_items_bucket_le_100: u64 = 0;
    let mut eff_as_items_bucket_gt_100: u64 = 0;

    fn bucketize(v: usize) -> u8 {
        if v <= 10 {
            0
        } else if v <= 100 {
            1
        } else {
            2
        }
    }

    fn ip_item_count(ip: Option<&crate::data_model::rc::IpResourceSet>) -> usize {
        let Some(ip) = ip else { return 0 };
        ip.families
            .iter()
            .map(|f| match &f.choice {
                crate::data_model::rc::IpAddressChoice::Inherit => 0usize,
                crate::data_model::rc::IpAddressChoice::AddressesOrRanges(items) => items.len(),
            })
            .sum()
    }

    fn as_item_count(asr: Option<&crate::data_model::rc::AsResourceSet>) -> usize {
        let Some(asr) = asr else { return 0 };
        let mut n = 0usize;
        if let Some(c) = asr.asnum.as_ref() {
            if let crate::data_model::rc::AsIdentifierChoice::AsIdsOrRanges(items) = c {
                n = n.saturating_add(items.len());
            }
        }
        if let Some(c) = asr.rdi.as_ref() {
            if let crate::data_model::rc::AsIdentifierChoice::AsIdsOrRanges(items) = c {
                n = n.saturating_add(items.len());
            }
        }
        n
    }

    for f in locked_files {
        if !f.rsync_uri.ends_with(".cer") {
            continue;
        }
        cer_seen = cer_seen.saturating_add(1);
        let child_der = f.bytes.as_slice();

        let tdecode = std::time::Instant::now();
        let child_cert = match crate::data_model::rc::ResourceCertificate::decode_der(child_der) {
            Ok(v) => v,
            Err(e) => {
                ca_error = ca_error.saturating_add(1);
                audits.push(ObjectAuditEntry {
                    rsync_uri: f.rsync_uri.clone(),
                    sha256_hex: sha256_hex_from_32(&f.sha256),
                    kind: AuditObjectKind::Certificate,
                    result: AuditObjectResult::Error,
                    detail: Some(format!("child certificate decode failed: {e}")),
                });
                continue;
            }
        };
        child_decode_nanos = child_decode_nanos
            .saturating_add(tdecode.elapsed().as_nanos().min(u128::from(u64::MAX)) as u64);

        let t0 = std::time::Instant::now();
        let issuer_crl_uri = match select_issuer_crl_uri_for_child(&child_cert, &crl_cache) {
            Ok(v) => v.to_string(),
            Err(e) => {
                crl_select_error = crl_select_error.saturating_add(1);
                audits.push(ObjectAuditEntry {
                    rsync_uri: f.rsync_uri.clone(),
                    sha256_hex: sha256_hex_from_32(&f.sha256),
                    kind: AuditObjectKind::Certificate,
                    result: AuditObjectResult::Error,
                    detail: Some(format!(
                        "cannot select issuer CRL for child certificate: {e}"
                    )),
                });
                continue;
            }
        };
        select_crl_nanos = select_crl_nanos
            .saturating_add(t0.elapsed().as_nanos().min(u128::from(u64::MAX)) as u64);

        let t1 = std::time::Instant::now();
        let Some(issuer_ca_ref) = issuer_ca.as_ref() else {
            ca_error = ca_error.saturating_add(1);
            audits.push(ObjectAuditEntry {
                rsync_uri: f.rsync_uri.clone(),
                sha256_hex: sha256_hex_from_32(&f.sha256),
                kind: AuditObjectKind::Certificate,
                result: AuditObjectResult::Error,
                detail: Some(
                    issuer_ca_decode_error
                        .clone()
                        .unwrap_or_else(|| "issuer CA decode failed".to_string()),
                ),
            });
            continue;
        };
        let Some(issuer_spki_ref) = issuer_spki.as_ref() else {
            ca_error = ca_error.saturating_add(1);
            audits.push(ObjectAuditEntry {
                rsync_uri: f.rsync_uri.clone(),
                sha256_hex: sha256_hex_from_32(&f.sha256),
                kind: AuditObjectKind::Certificate,
                result: AuditObjectResult::Error,
                detail: Some(
                    issuer_spki_error
                        .clone()
                        .unwrap_or_else(|| "issuer SubjectPublicKeyInfo unavailable".to_string()),
                ),
            });
            continue;
        };
        let validated = match validate_subordinate_ca_cert_with_cached_issuer(
            child_der,
            child_cert,
            issuer_ca_der,
            issuer_ca_ref,
            issuer_spki_ref,
            issuer_crl_uri.as_str(),
            &mut crl_cache,
            issuer.ca_certificate_rsync_uri.as_deref(),
            issuer.effective_ip_resources.as_ref(),
            issuer.effective_as_resources.as_ref(),
            &issuer_resources_index,
            validation_time,
        ) {
            Ok(v) => v,
            Err(CaPathError::ChildNotCa) => {
                let tr = std::time::Instant::now();
                let router_result = match ensure_issuer_crl_verified(
                    issuer_crl_uri.as_str(),
                    &mut crl_cache,
                    issuer_ca_der,
                ) {
                    Ok(verified_crl) => {
                        BgpsecRouterCertificate::validate_path_with_prevalidated_issuer(
                            child_der,
                            issuer_ca_ref,
                            issuer_spki_ref,
                            &verified_crl.crl,
                            &verified_crl.revoked_serials,
                            issuer.ca_certificate_rsync_uri.as_deref(),
                            Some(issuer_crl_uri.as_str()),
                            validation_time,
                        )
                    }
                    Err(err) => {
                        validate_router_nanos = validate_router_nanos.saturating_add(
                            tr.elapsed().as_nanos().min(u128::from(u64::MAX)) as u64,
                        );
                        router_error = router_error.saturating_add(1);
                        audits.push(ObjectAuditEntry {
                            rsync_uri: f.rsync_uri.clone(),
                            sha256_hex: sha256_hex_from_32(&f.sha256),
                            kind: AuditObjectKind::RouterCertificate,
                            result: AuditObjectResult::Error,
                            detail: Some(format!(
                                "router certificate issuer CRL validation failed: {err}"
                            )),
                        });
                        continue;
                    }
                };
                validate_router_nanos = validate_router_nanos
                    .saturating_add(tr.elapsed().as_nanos().min(u128::from(u64::MAX)) as u64);

                match router_result {
                    Ok(router) => {
                        router_ok = router_ok.saturating_add(1);
                        let source_object_hash = sha256_hex_from_32(&f.sha256);
                        let item_effective_until = PackTime::from_utc_offset_datetime(
                            router.resource_cert.tbs.validity_not_after,
                        );
                        for as_id in &router.asns {
                            router_keys.push(RouterKeyPayload {
                                as_id: *as_id,
                                ski: router.subject_key_identifier.clone(),
                                spki_der: router.spki_der.clone(),
                                source_object_uri: f.rsync_uri.clone(),
                                source_object_hash: source_object_hash.clone(),
                                source_ee_cert_hash: source_object_hash.clone(),
                                item_effective_until: item_effective_until.clone(),
                            });
                        }
                        audits.push(ObjectAuditEntry {
                            rsync_uri: f.rsync_uri.clone(),
                            sha256_hex: sha256_hex_from_32(&f.sha256),
                            kind: AuditObjectKind::RouterCertificate,
                            result: AuditObjectResult::Ok,
                            detail: Some(
                                "validated BGPsec router certificate (RFC 8209); no child CA instance enqueued"
                                    .to_string(),
                            ),
                        });
                    }
                    Err(err) if is_non_router_certificate(&err) => {
                        ca_skipped_not_ca = ca_skipped_not_ca.saturating_add(1);
                        router_skipped_non_router = router_skipped_non_router.saturating_add(1);
                        audits.push(ObjectAuditEntry {
                            rsync_uri: f.rsync_uri.clone(),
                            sha256_hex: sha256_hex_from_32(&f.sha256),
                            kind: AuditObjectKind::Certificate,
                            result: AuditObjectResult::Skipped,
                            detail: Some(
                                "skipped: not a CA resource certificate or BGPsec router certificate"
                                    .to_string(),
                            ),
                        });
                    }
                    Err(err) => {
                        router_error = router_error.saturating_add(1);
                        audits.push(ObjectAuditEntry {
                            rsync_uri: f.rsync_uri.clone(),
                            sha256_hex: sha256_hex_from_32(&f.sha256),
                            kind: AuditObjectKind::RouterCertificate,
                            result: AuditObjectResult::Error,
                            detail: Some(format!(
                                "router certificate validation failed: {err}"
                            )),
                        });
                    }
                }
                continue;
            }
            Err(e) => {
                ca_error = ca_error.saturating_add(1);
                audits.push(ObjectAuditEntry {
                    rsync_uri: f.rsync_uri.clone(),
                    sha256_hex: sha256_hex_from_32(&f.sha256),
                    kind: AuditObjectKind::Certificate,
                    result: AuditObjectResult::Error,
                    detail: Some(format!("child CA validation failed: {e}")),
                });
                continue;
            }
        };
        validate_sub_ca_nanos = validate_sub_ca_nanos
            .saturating_add(t1.elapsed().as_nanos().min(u128::from(u64::MAX)) as u64);

        let eff_ip_items = ip_item_count(validated.effective_ip_resources.as_ref());
        match bucketize(eff_ip_items) {
            0 => eff_ip_items_bucket_le_10 = eff_ip_items_bucket_le_10.saturating_add(1),
            1 => eff_ip_items_bucket_le_100 = eff_ip_items_bucket_le_100.saturating_add(1),
            _ => eff_ip_items_bucket_gt_100 = eff_ip_items_bucket_gt_100.saturating_add(1),
        }
        let eff_as_items = as_item_count(validated.effective_as_resources.as_ref());
        match bucketize(eff_as_items) {
            0 => eff_as_items_bucket_le_10 = eff_as_items_bucket_le_10.saturating_add(1),
            1 => eff_as_items_bucket_le_100 = eff_as_items_bucket_le_100.saturating_add(1),
            _ => eff_as_items_bucket_gt_100 = eff_as_items_bucket_gt_100.saturating_add(1),
        }

        let t2 = std::time::Instant::now();
        let uris = match ca_instance_uris_from_ca_certificate(&validated.child_ca) {
            Ok(v) => v,
            Err(e) => {
                uri_discovery_error = uri_discovery_error.saturating_add(1);
                audits.push(ObjectAuditEntry {
                    rsync_uri: f.rsync_uri.clone(),
                    sha256_hex: sha256_hex_from_32(&f.sha256),
                    kind: AuditObjectKind::Certificate,
                    result: AuditObjectResult::Error,
                    detail: Some(format!("CA instance URI discovery failed: {e}")),
                });
                continue;
            }
        };
        uri_discovery_nanos = uri_discovery_nanos
            .saturating_add(t2.elapsed().as_nanos().min(u128::from(u64::MAX)) as u64);

        let t3 = std::time::Instant::now();
        out.push(DiscoveredChildCaInstance {
            handle: CaInstanceHandle {
                depth: 0,
                tal_id: issuer.tal_id.clone(),
                parent_manifest_rsync_uri: Some(issuer.manifest_rsync_uri.clone()),
                ca_certificate_der: child_der.to_vec(),
                ca_certificate_rsync_uri: Some(f.rsync_uri.clone()),
                effective_ip_resources: validated.effective_ip_resources.clone(),
                effective_as_resources: validated.effective_as_resources.clone(),
                rsync_base_uri: uris.rsync_base_uri,
                manifest_rsync_uri: uris.manifest_rsync_uri,
                publication_point_rsync_uri: uris.publication_point_rsync_uri,
                rrdp_notification_uri: uris.rrdp_notification_uri,
            },
            discovered_from: crate::audit::DiscoveredFrom {
                parent_manifest_rsync_uri: issuer.manifest_rsync_uri.clone(),
                child_ca_certificate_rsync_uri: f.rsync_uri.clone(),
                child_ca_certificate_sha256_hex: sha256_hex_from_32(&f.sha256),
            },
        });
        enqueue_nanos =
            enqueue_nanos.saturating_add(t3.elapsed().as_nanos().min(u128::from(u64::MAX)) as u64);

        ca_ok = ca_ok.saturating_add(1);
        audits.push(ObjectAuditEntry {
            rsync_uri: f.rsync_uri.clone(),
            sha256_hex: sha256_hex_from_32(&f.sha256),
            kind: AuditObjectKind::Certificate,
            result: AuditObjectResult::Ok,
            detail: Some("validated subordinate CA certificate; enqueued CA instance".to_string()),
        });
    }

    if let Some(t) = timing {
        t.record_count("child_cer_seen", cer_seen);
        t.record_count("child_ca_ok", ca_ok);
        t.record_count("child_ca_error", ca_error);
        t.record_count("child_ca_skipped_not_ca", ca_skipped_not_ca);
        t.record_count("child_router_ok", router_ok);
        t.record_count("child_router_error", router_error);
        t.record_count("child_router_skipped_non_router", router_skipped_non_router);
        t.record_count("child_crl_select_error", crl_select_error);
        t.record_count("child_uri_discovery_error", uri_discovery_error);

        t.record_count("child_effective_ip_items_le_10", eff_ip_items_bucket_le_10);
        t.record_count(
            "child_effective_ip_items_le_100",
            eff_ip_items_bucket_le_100,
        );
        t.record_count(
            "child_effective_ip_items_gt_100",
            eff_ip_items_bucket_gt_100,
        );
        t.record_count("child_effective_as_items_le_10", eff_as_items_bucket_le_10);
        t.record_count(
            "child_effective_as_items_le_100",
            eff_as_items_bucket_le_100,
        );
        t.record_count(
            "child_effective_as_items_gt_100",
            eff_as_items_bucket_gt_100,
        );

        t.record_phase_nanos("child_select_issuer_crl_total", select_crl_nanos);
        t.record_phase_nanos("child_decode_certificate_total", child_decode_nanos);
        t.record_phase_nanos("child_validate_subordinate_total", validate_sub_ca_nanos);
        t.record_phase_nanos("child_validate_router_certificate_total", validate_router_nanos);
        t.record_phase_nanos("child_ca_instance_uri_discovery_total", uri_discovery_nanos);
        t.record_phase_nanos("child_enqueue_total", enqueue_nanos);
    }

    Ok(ChildDiscoveryOutput {
        children: out,
        audits,
        router_keys,
    })
}

fn is_non_router_certificate(err: &BgpsecRouterCertificatePathError) -> bool {
    matches!(
        err,
        BgpsecRouterCertificatePathError::Decode(BgpsecRouterCertificateDecodeError::Validate(
            BgpsecRouterCertificateProfileError::NotEe
                | BgpsecRouterCertificateProfileError::MissingExtendedKeyUsage
                | BgpsecRouterCertificateProfileError::MissingBgpsecRouterEku
        ))
    )
}

fn select_issuer_crl_uri_for_child<'a>(
    child: &'a crate::data_model::rc::ResourceCertificate,
    crl_cache: &std::collections::HashMap<String, CachedIssuerCrl>,
) -> Result<&'a str, String> {
    if crl_cache.is_empty() {
        return Err(
            "no CRL available in publication point snapshot (cannot validate certificates) (RFC 9286 §7; RFC 6487 §4.8.6)"
                .to_string(),
        );
    }
    let Some(crldp_uris) = child.tbs.extensions.crl_distribution_points_uris.as_ref() else {
        return Err(
            "child certificate CRLDistributionPoints missing (RFC 6487 §4.8.6)".to_string(),
        );
    };

    for u in crldp_uris {
        let s = u.as_str();
        if crl_cache.contains_key(s) {
            return Ok(s);
        }
    }

    Err(format!(
        "CRL referenced by child certificate CRLDistributionPoints not found in publication point snapshot: {} (RFC 6487 §4.8.6; RFC 9286 §4.2.1)",
        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, CaPathError> {
    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)?;
            crl.verify_signature_with_issuer_certificate_der(issuer_ca_der)?;

            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_subordinate_ca_cert_with_cached_issuer(
    child_ca_der: &[u8],
    child_ca: crate::data_model::rc::ResourceCertificate,
    issuer_ca_der: &[u8],
    issuer_ca: &crate::data_model::rc::ResourceCertificate,
    issuer_spki: &SubjectPublicKeyInfo<'_>,
    issuer_crl_rsync_uri: &str,
    crl_cache: &mut std::collections::HashMap<String, CachedIssuerCrl>,
    issuer_ca_rsync_uri: Option<&str>,
    issuer_effective_ip: Option<&crate::data_model::rc::IpResourceSet>,
    issuer_effective_as: Option<&crate::data_model::rc::AsResourceSet>,
    issuer_resources_index: &IssuerEffectiveResourcesIndex,
    validation_time: time::OffsetDateTime,
) -> Result<ValidatedSubordinateCaLite, CaPathError> {
    let verified_crl = ensure_issuer_crl_verified(issuer_crl_rsync_uri, crl_cache, issuer_ca_der)?;

    validate_subordinate_ca_cert_with_prevalidated_issuer_and_resources(
        child_ca_der,
        child_ca,
        issuer_ca,
        issuer_spki,
        &verified_crl.crl,
        &verified_crl.revoked_serials,
        issuer_ca_rsync_uri,
        issuer_crl_rsync_uri,
        issuer_effective_ip,
        issuer_effective_as,
        issuer_resources_index,
        validation_time,
    )
}

#[cfg(test)]
fn select_issuer_crl_from_snapshot<'a>(
    child_cert_der: &[u8],
    pack: &'a PublicationPointSnapshot,
) -> Result<(&'a str, &'a [u8]), String> {
    let child = crate::data_model::rc::ResourceCertificate::decode_der(child_cert_der)
        .map_err(|e| format!("child certificate decode failed: {e}"))?;
    let Some(crldp_uris) = child.tbs.extensions.crl_distribution_points_uris.as_ref() else {
        return Err(
            "child certificate CRLDistributionPoints missing (RFC 6487 §4.8.6)".to_string(),
        );
    };

    for u in crldp_uris {
        let s = u.as_str();
        if let Some(f) = pack.files.iter().find(|f| f.rsync_uri == s) {
            return Ok((f.rsync_uri.as_str(), f.bytes.as_slice()));
        }
    }

    Err(format!(
        "CRL referenced by child certificate CRLDistributionPoints not found in publication point snapshot: {} (RFC 6487 §4.8.6; RFC 9286 §4.2.1)",
        crldp_uris
            .iter()
            .map(|u| u.as_str())
            .collect::<Vec<_>>()
            .join(", ")
    ))
}

fn kind_from_rsync_uri(uri: &str) -> AuditObjectKind {
    if uri.ends_with(".crl") {
        AuditObjectKind::Crl
    } else if uri.ends_with(".cer") {
        AuditObjectKind::Certificate
    } else if uri.ends_with(".roa") {
        AuditObjectKind::Roa
    } else if uri.ends_with(".asa") {
        AuditObjectKind::Aspa
    } else {
        AuditObjectKind::Other
    }
}

fn source_label(source: PublicationPointSource) -> String {
    match source {
        PublicationPointSource::Fresh => "fresh".to_string(),
        PublicationPointSource::VcirCurrentInstance => "vcir_current_instance".to_string(),
        PublicationPointSource::FailedFetchNoCache => "failed_fetch_no_cache".to_string(),
    }
}

fn repo_sync_source_label(source: crate::sync::repo::RepoSyncSource) -> &'static str {
    match source {
        crate::sync::repo::RepoSyncSource::Rrdp => "rrdp",
        crate::sync::repo::RepoSyncSource::Rsync => "rsync",
    }
}

fn kind_from_vcir_artifact_kind(kind: VcirArtifactKind) -> AuditObjectKind {
    match kind {
        VcirArtifactKind::Mft => AuditObjectKind::Manifest,
        VcirArtifactKind::Crl => AuditObjectKind::Crl,
        VcirArtifactKind::Cer => AuditObjectKind::Certificate,
        VcirArtifactKind::Roa => AuditObjectKind::Roa,
        VcirArtifactKind::Aspa => AuditObjectKind::Aspa,
        VcirArtifactKind::Gbr | VcirArtifactKind::Tal | VcirArtifactKind::Other => {
            AuditObjectKind::Other
        }
    }
}

fn audit_result_from_vcir_status(status: VcirArtifactValidationStatus) -> AuditObjectResult {
    match status {
        VcirArtifactValidationStatus::Accepted => AuditObjectResult::Ok,
        VcirArtifactValidationStatus::Rejected => AuditObjectResult::Error,
        VcirArtifactValidationStatus::WarningOnly => AuditObjectResult::Skipped,
    }
}

fn build_publication_point_audit_from_snapshot(
    ca: &CaInstanceHandle,
    source: PublicationPointSource,
    repo_sync_source: Option<&str>,
    repo_sync_duration_ms: Option<u64>,
    repo_sync_error: Option<&str>,
    pack: &PublicationPointSnapshot,
    runner_warnings: &[Warning],
    objects: &crate::validation::objects::ObjectsOutput,
    child_audits: &[ObjectAuditEntry],
) -> PublicationPointAudit {
    use crate::data_model::crl::RpkixCrl;
    use std::collections::HashMap;

    let locked_files = &pack.files;
    let mut audit_by_uri: HashMap<String, ObjectAuditEntry> = HashMap::new();
    for f in locked_files {
        audit_by_uri.insert(
            f.rsync_uri.clone(),
            ObjectAuditEntry {
                rsync_uri: f.rsync_uri.clone(),
                sha256_hex: sha256_hex_from_32(&f.sha256),
                kind: kind_from_rsync_uri(&f.rsync_uri),
                result: AuditObjectResult::Skipped,
                detail: Some("skipped: not processed in stage2".to_string()),
            },
        );
    }

    for f in locked_files {
        if !f.rsync_uri.ends_with(".crl") {
            continue;
        }
        let ok = RpkixCrl::decode_der(&f.bytes).is_ok();
        audit_by_uri.insert(
            f.rsync_uri.clone(),
            ObjectAuditEntry {
                rsync_uri: f.rsync_uri.clone(),
                sha256_hex: sha256_hex_from_32(&f.sha256),
                kind: AuditObjectKind::Crl,
                result: if ok {
                    AuditObjectResult::Ok
                } else {
                    AuditObjectResult::Error
                },
                detail: if ok {
                    None
                } else {
                    Some("CRL decode failed".to_string())
                },
            },
        );
    }

    for e in child_audits {
        audit_by_uri.insert(e.rsync_uri.clone(), e.clone());
    }
    for e in &objects.audit {
        audit_by_uri.insert(e.rsync_uri.clone(), e.clone());
    }

    let mut objects_out: Vec<ObjectAuditEntry> = Vec::with_capacity(pack.files.len() + 1);
    objects_out.push(ObjectAuditEntry {
        rsync_uri: pack.manifest_rsync_uri.clone(),
        sha256_hex: sha256_hex(&pack.manifest_bytes),
        kind: AuditObjectKind::Manifest,
        result: AuditObjectResult::Ok,
        detail: None,
    });
    for f in locked_files {
        if let Some(e) = audit_by_uri.remove(&f.rsync_uri) {
            objects_out.push(e);
        } else {
            objects_out.push(ObjectAuditEntry {
                rsync_uri: f.rsync_uri.clone(),
                sha256_hex: sha256_hex_from_32(&f.sha256),
                kind: kind_from_rsync_uri(&f.rsync_uri),
                result: AuditObjectResult::Skipped,
                detail: Some("skipped: no audit entry".to_string()),
            });
        }
    }

    let mut warnings = Vec::new();
    warnings.extend(runner_warnings.iter().map(AuditWarning::from));
    warnings.extend(objects.warnings.iter().map(AuditWarning::from));

    PublicationPointAudit {
        node_id: None,
        parent_node_id: None,
        discovered_from: None,
        rsync_base_uri: ca.rsync_base_uri.clone(),
        manifest_rsync_uri: ca.manifest_rsync_uri.clone(),
        publication_point_rsync_uri: ca.publication_point_rsync_uri.clone(),
        rrdp_notification_uri: ca.rrdp_notification_uri.clone(),
        source: source_label(source),
        repo_sync_source: repo_sync_source.map(ToString::to_string),
        repo_sync_duration_ms,
        repo_sync_error: repo_sync_error.map(ToString::to_string),
        this_update_rfc3339_utc: pack.this_update.rfc3339_utc.clone(),
        next_update_rfc3339_utc: pack.next_update.rfc3339_utc.clone(),
        verified_at_rfc3339_utc: pack.verified_at.rfc3339_utc.clone(),
        warnings,
        objects: objects_out,
    }
}

fn build_publication_point_audit_from_vcir(
    ca: &CaInstanceHandle,
    source: PublicationPointSource,
    repo_sync_source: Option<&str>,
    repo_sync_duration_ms: Option<u64>,
    repo_sync_error: Option<&str>,
    vcir: Option<&ValidatedCaInstanceResult>,
    pack: Option<&PublicationPointSnapshot>,
    runner_warnings: &[Warning],
    objects: &crate::validation::objects::ObjectsOutput,
    child_audits: &[ObjectAuditEntry],
) -> PublicationPointAudit {
    if let Some(pack) = pack {
        return build_publication_point_audit_from_snapshot(
            ca,
            source,
            repo_sync_source,
            repo_sync_duration_ms,
            repo_sync_error,
            pack,
            runner_warnings,
            objects,
            child_audits,
        );
    }

    let mut warnings = Vec::new();
    warnings.extend(runner_warnings.iter().map(AuditWarning::from));
    warnings.extend(objects.warnings.iter().map(AuditWarning::from));

    let Some(vcir) = vcir else {
        return PublicationPointAudit {
            node_id: None,
            parent_node_id: None,
            discovered_from: None,
            rsync_base_uri: ca.rsync_base_uri.clone(),
            manifest_rsync_uri: ca.manifest_rsync_uri.clone(),
            publication_point_rsync_uri: ca.publication_point_rsync_uri.clone(),
            rrdp_notification_uri: ca.rrdp_notification_uri.clone(),
            source: source_label(source),
            repo_sync_source: repo_sync_source.map(ToString::to_string),
            repo_sync_duration_ms,
            repo_sync_error: repo_sync_error.map(ToString::to_string),
            this_update_rfc3339_utc: String::new(),
            next_update_rfc3339_utc: String::new(),
            verified_at_rfc3339_utc: String::new(),
            warnings,
            objects: Vec::new(),
        };
    };

    let mut audit_by_uri: HashMap<String, ObjectAuditEntry> = HashMap::new();
    for artifact in &vcir.related_artifacts {
        let Some(uri) = artifact.uri.as_ref() else {
            continue;
        };
        audit_by_uri.insert(
            uri.clone(),
            ObjectAuditEntry {
                rsync_uri: uri.clone(),
                sha256_hex: artifact.sha256.clone(),
                kind: kind_from_vcir_artifact_kind(artifact.artifact_kind),
                result: audit_result_from_vcir_status(artifact.validation_status),
                detail: None,
            },
        );
    }
    for e in child_audits {
        audit_by_uri.insert(e.rsync_uri.clone(), e.clone());
    }
    for e in &objects.audit {
        audit_by_uri.insert(e.rsync_uri.clone(), e.clone());
    }

    let mut ordered_uris: Vec<String> = vcir
        .related_artifacts
        .iter()
        .filter_map(|artifact| artifact.uri.clone())
        .collect();
    ordered_uris.sort();
    ordered_uris.dedup();

    let mut objects_out: Vec<ObjectAuditEntry> = Vec::new();
    if let Some(entry) = audit_by_uri.remove(&vcir.current_manifest_rsync_uri) {
        objects_out.push(entry);
    } else {
        objects_out.push(ObjectAuditEntry {
            rsync_uri: vcir.current_manifest_rsync_uri.clone(),
            sha256_hex: vcir
                .related_artifacts
                .iter()
                .find(|artifact| {
                    artifact.artifact_role == VcirArtifactRole::Manifest
                        && artifact.uri.as_deref() == Some(vcir.current_manifest_rsync_uri.as_str())
                })
                .map(|artifact| artifact.sha256.clone())
                .unwrap_or_default(),
            kind: AuditObjectKind::Manifest,
            result: AuditObjectResult::Ok,
            detail: None,
        });
    }

    for uri in ordered_uris {
        if uri == vcir.current_manifest_rsync_uri {
            continue;
        }
        if let Some(entry) = audit_by_uri.remove(&uri) {
            objects_out.push(entry);
        }
    }

    PublicationPointAudit {
        node_id: None,
        parent_node_id: None,
        discovered_from: None,
        rsync_base_uri: ca.rsync_base_uri.clone(),
        manifest_rsync_uri: ca.manifest_rsync_uri.clone(),
        publication_point_rsync_uri: ca.publication_point_rsync_uri.clone(),
        rrdp_notification_uri: ca.rrdp_notification_uri.clone(),
        source: source_label(source),
        repo_sync_source: repo_sync_source.map(ToString::to_string),
        repo_sync_duration_ms,
        repo_sync_error: repo_sync_error.map(ToString::to_string),
        this_update_rfc3339_utc: vcir
            .validated_manifest_meta
            .validated_manifest_this_update
            .rfc3339_utc
            .clone(),
        next_update_rfc3339_utc: vcir
            .validated_manifest_meta
            .validated_manifest_next_update
            .rfc3339_utc
            .clone(),
        verified_at_rfc3339_utc: vcir.last_successful_validation_time.rfc3339_utc.clone(),
        warnings,
        objects: objects_out,
    }
}

fn parse_snapshot_time_value(pack_time: &PackTime) -> Result<time::OffsetDateTime, String> {
    time::OffsetDateTime::parse(
        &pack_time.rfc3339_utc,
        &time::format_description::well_known::Rfc3339,
    )
    .map_err(|e| format!("invalid RFC3339 time '{}': {e}", pack_time.rfc3339_utc))
}

fn empty_objects_output() -> crate::validation::objects::ObjectsOutput {
    crate::validation::objects::ObjectsOutput {
        vrps: Vec::new(),
        aspas: Vec::new(),
        router_keys: Vec::new(),
        local_outputs_cache: Vec::new(),
        warnings: Vec::new(),
        stats: crate::validation::objects::ObjectsStats::default(),
        audit: Vec::new(),
    }
}

fn project_current_instance_vcir_on_failed_fetch(
    store: &RocksStore,
    ca: &CaInstanceHandle,
    fresh_err: &ManifestFreshError,
    validation_time: time::OffsetDateTime,
) -> Result<VcirReuseProjection, String> {
    let mut warnings = vec![
        Warning::new(format!("manifest failed fetch: {fresh_err}"))
            .with_rfc_refs(&[RfcRef("RFC 9286 §6.6")])
            .with_context(&ca.manifest_rsync_uri),
    ];

    let Some(vcir) = store
        .get_vcir(&ca.manifest_rsync_uri)
        .map_err(|e| format!("load VCIR failed: {e}"))?
    else {
        warnings.push(
            Warning::new(
                "no latest validated result for current CA instance; no cached output reused",
            )
            .with_rfc_refs(&[RfcRef("RFC 9286 §6.6")])
            .with_context(&ca.manifest_rsync_uri),
        );
        return Ok(VcirReuseProjection {
            source: PublicationPointSource::FailedFetchNoCache,
            vcir: None,
            snapshot: None,
            objects: empty_objects_output(),
            child_audits: Vec::new(),
            discovered_children: Vec::new(),
            warnings,
        });
    };

    if !vcir.audit_summary.failed_fetch_eligible {
        warnings.push(
            Warning::new(
                "latest VCIR is not marked failed-fetch eligible; no cached output reused",
            )
            .with_rfc_refs(&[RfcRef("RFC 9286 §6.6")])
            .with_context(&ca.manifest_rsync_uri),
        );
        return Ok(VcirReuseProjection {
            source: PublicationPointSource::FailedFetchNoCache,
            vcir: Some(vcir),
            snapshot: None,
            objects: empty_objects_output(),
            child_audits: Vec::new(),
            discovered_children: Vec::new(),
            warnings,
        });
    }

    let instance_effective_until =
        parse_snapshot_time_value(&vcir.instance_gate.instance_effective_until)?;
    if validation_time > instance_effective_until {
        warnings.push(
            Warning::new(
                "latest VCIR instance_gate expired; current instance contributes no cached output",
            )
            .with_rfc_refs(&[RfcRef("RFC 9286 §6.6")])
            .with_context(&ca.manifest_rsync_uri),
        );
        return Ok(VcirReuseProjection {
            source: PublicationPointSource::FailedFetchNoCache,
            vcir: Some(vcir),
            snapshot: None,
            objects: empty_objects_output(),
            child_audits: Vec::new(),
            discovered_children: Vec::new(),
            warnings,
        });
    }

    warnings.push(
        Warning::new("using latest validated result for current CA instance")
            .with_rfc_refs(&[RfcRef("RFC 9286 §6.6")])
            .with_context(&ca.manifest_rsync_uri),
    );

    let snapshot = reconstruct_snapshot_from_vcir(store, ca, &vcir, &mut warnings);
    let objects = build_objects_output_from_vcir(&vcir, validation_time, &mut warnings);
    let (discovered_children, child_audits) =
        restore_children_from_vcir(store, ca, &vcir, &mut warnings);

    Ok(VcirReuseProjection {
        source: PublicationPointSource::VcirCurrentInstance,
        vcir: Some(vcir),
        snapshot,
        objects,
        child_audits,
        discovered_children,
        warnings,
    })
}

fn reconstruct_snapshot_from_vcir(
    store: &RocksStore,
    ca: &CaInstanceHandle,
    vcir: &ValidatedCaInstanceResult,
    warnings: &mut Vec<Warning>,
) -> Option<PublicationPointSnapshot> {
    let manifest_artifact = vcir.related_artifacts.iter().find(|artifact| {
        artifact.artifact_role == VcirArtifactRole::Manifest
            && artifact.uri.as_deref() == Some(ca.manifest_rsync_uri.as_str())
    })?;
    let manifest_entry = match store.get_raw_by_hash_entry(&manifest_artifact.sha256) {
        Ok(Some(entry)) => entry,
        Ok(None) => {
            warnings.push(
                Warning::new("manifest raw bytes missing for VCIR audit reconstruction")
                    .with_context(&ca.manifest_rsync_uri),
            );
            return None;
        }
        Err(e) => {
            warnings.push(
                Warning::new(format!(
                    "manifest raw bytes load failed for VCIR audit reconstruction: {e}"
                ))
                .with_context(&ca.manifest_rsync_uri),
            );
            return None;
        }
    };

    let mut seen = HashSet::new();
    let mut files = Vec::new();
    for artifact in &vcir.related_artifacts {
        let Some(uri) = artifact.uri.as_ref() else {
            continue;
        };
        if artifact.artifact_role == VcirArtifactRole::Manifest
            || artifact.artifact_role == VcirArtifactRole::IssuerCert
            || artifact.artifact_role == VcirArtifactRole::TrustAnchorCert
            || artifact.artifact_role == VcirArtifactRole::Tal
        {
            continue;
        }
        if !seen.insert(uri.clone()) {
            continue;
        }
        match store.get_raw_by_hash_entry(&artifact.sha256) {
            Ok(Some(entry)) => files.push(PackFile::from_bytes_compute_sha256(uri, entry.bytes)),
            Ok(None) => warnings.push(
                Warning::new("related artifact raw bytes missing for VCIR audit reconstruction")
                    .with_context(uri),
            ),
            Err(e) => warnings.push(
                Warning::new(format!(
                    "related artifact raw bytes load failed for VCIR audit reconstruction: {e}"
                ))
                .with_context(uri),
            ),
        }
    }

    Some(PublicationPointSnapshot {
        format_version: PublicationPointSnapshot::FORMAT_VERSION_V1,
        publication_point_rsync_uri: ca.publication_point_rsync_uri.clone(),
        manifest_rsync_uri: ca.manifest_rsync_uri.clone(),
        manifest_number_be: vcir
            .validated_manifest_meta
            .validated_manifest_number
            .clone(),
        this_update: vcir
            .validated_manifest_meta
            .validated_manifest_this_update
            .clone(),
        next_update: vcir
            .validated_manifest_meta
            .validated_manifest_next_update
            .clone(),
        verified_at: vcir.last_successful_validation_time.clone(),
        manifest_bytes: manifest_entry.bytes,
        files,
    })
}

fn audit_kind_for_vcir_output_type(output_type: VcirOutputType) -> AuditObjectKind {
    match output_type {
        VcirOutputType::Vrp => AuditObjectKind::Roa,
        VcirOutputType::Aspa => AuditObjectKind::Aspa,
        VcirOutputType::RouterKey => AuditObjectKind::RouterCertificate,
    }
}

fn build_objects_output_from_vcir(
    vcir: &ValidatedCaInstanceResult,
    validation_time: time::OffsetDateTime,
    warnings: &mut Vec<Warning>,
) -> crate::validation::objects::ObjectsOutput {
    let mut output = empty_objects_output();
    let mut audit_by_uri: HashMap<String, ObjectAuditEntry> = HashMap::new();
    let mut roa_total: HashSet<String> = HashSet::new();
    let mut aspa_total: HashSet<String> = HashSet::new();
    let mut roa_ok: HashSet<String> = HashSet::new();
    let mut aspa_ok: HashSet<String> = HashSet::new();

    for artifact in &vcir.related_artifacts {
        if artifact.artifact_role != VcirArtifactRole::SignedObject {
            continue;
        }
        if let Some(uri) = artifact.uri.as_ref() {
            match artifact.artifact_kind {
                VcirArtifactKind::Roa => {
                    roa_total.insert(uri.clone());
                }
                VcirArtifactKind::Aspa => {
                    aspa_total.insert(uri.clone());
                }
                _ => {}
            }
        }
    }

    for local in &vcir.local_outputs {
        let effective_until = match parse_snapshot_time_value(&local.item_effective_until) {
            Ok(v) => v,
            Err(e) => {
                warnings.push(
                    Warning::new(format!(
                        "cached local output has invalid item_effective_until: {e}"
                    ))
                    .with_context(&local.source_object_uri),
                );
                audit_by_uri.insert(
                    local.source_object_uri.clone(),
                    ObjectAuditEntry {
                        rsync_uri: local.source_object_uri.clone(),
                        sha256_hex: local.source_object_hash.clone(),
                        kind: audit_kind_for_vcir_output_type(local.output_type),
                        result: AuditObjectResult::Error,
                        detail: Some(
                            "cached local output has invalid item_effective_until".to_string(),
                        ),
                    },
                );
                continue;
            }
        };
        if validation_time > effective_until {
            audit_by_uri
                .entry(local.source_object_uri.clone())
                .or_insert_with(|| ObjectAuditEntry {
                    rsync_uri: local.source_object_uri.clone(),
                    sha256_hex: local.source_object_hash.clone(),
                    kind: audit_kind_for_vcir_output_type(local.output_type),
                    result: AuditObjectResult::Skipped,
                    detail: Some("skipped: cached local output expired".to_string()),
                });
            continue;
        }

        match local.output_type {
            VcirOutputType::Vrp => match parse_vcir_vrp_output(local) {
                Ok(vrp) => {
                    roa_ok.insert(local.source_object_uri.clone());
                    output.vrps.push(vrp);
                    audit_by_uri.insert(
                        local.source_object_uri.clone(),
                        ObjectAuditEntry {
                            rsync_uri: local.source_object_uri.clone(),
                            sha256_hex: local.source_object_hash.clone(),
                            kind: AuditObjectKind::Roa,
                            result: AuditObjectResult::Ok,
                            detail: None,
                        },
                    );
                }
                Err(e) => {
                    warnings.push(
                        Warning::new(format!("cached ROA local output parse failed: {e}"))
                            .with_context(&local.source_object_uri),
                    );
                    audit_by_uri.insert(
                        local.source_object_uri.clone(),
                        ObjectAuditEntry {
                            rsync_uri: local.source_object_uri.clone(),
                            sha256_hex: local.source_object_hash.clone(),
                            kind: AuditObjectKind::Roa,
                            result: AuditObjectResult::Error,
                            detail: Some(format!("cached ROA local output parse failed: {e}")),
                        },
                    );
                }
            },
            VcirOutputType::Aspa => match parse_vcir_aspa_output(local) {
                Ok(aspa) => {
                    aspa_ok.insert(local.source_object_uri.clone());
                    output.aspas.push(aspa);
                    audit_by_uri.insert(
                        local.source_object_uri.clone(),
                        ObjectAuditEntry {
                            rsync_uri: local.source_object_uri.clone(),
                            sha256_hex: local.source_object_hash.clone(),
                            kind: AuditObjectKind::Aspa,
                            result: AuditObjectResult::Ok,
                            detail: None,
                        },
                    );
                }
                Err(e) => {
                    warnings.push(
                        Warning::new(format!("cached ASPA local output parse failed: {e}"))
                            .with_context(&local.source_object_uri),
                    );
                    audit_by_uri.insert(
                        local.source_object_uri.clone(),
                        ObjectAuditEntry {
                            rsync_uri: local.source_object_uri.clone(),
                            sha256_hex: local.source_object_hash.clone(),
                            kind: AuditObjectKind::Aspa,
                            result: AuditObjectResult::Error,
                            detail: Some(format!("cached ASPA local output parse failed: {e}")),
                        },
                    );
                }
            },
            VcirOutputType::RouterKey => match parse_vcir_router_key_output(local) {
                Ok(router_key) => {
                    output.router_keys.push(router_key);
                    audit_by_uri.insert(
                        local.source_object_uri.clone(),
                        ObjectAuditEntry {
                            rsync_uri: local.source_object_uri.clone(),
                            sha256_hex: local.source_object_hash.clone(),
                            kind: AuditObjectKind::RouterCertificate,
                            result: AuditObjectResult::Ok,
                            detail: Some("cached Router Key local output restored".to_string()),
                        },
                    );
                }
                Err(e) => {
                    warnings.push(
                        Warning::new(format!("cached Router Key local output parse failed: {e}"))
                            .with_context(&local.source_object_uri),
                    );
                    audit_by_uri.insert(
                        local.source_object_uri.clone(),
                        ObjectAuditEntry {
                            rsync_uri: local.source_object_uri.clone(),
                            sha256_hex: local.source_object_hash.clone(),
                            kind: AuditObjectKind::RouterCertificate,
                            result: AuditObjectResult::Error,
                            detail: Some(format!("cached Router Key local output parse failed: {e}")),
                        },
                    );
                }
            },
        }
    }

    output.stats.roa_total = roa_total.len();
    output.stats.roa_ok = roa_ok.len();
    output.stats.aspa_total = aspa_total.len();
    output.stats.aspa_ok = aspa_ok.len();
    let mut audit: Vec<_> = audit_by_uri.into_values().collect();
    audit.sort_by(|a, b| a.rsync_uri.cmp(&b.rsync_uri));
    output.audit = audit;
    output
}

fn parse_vcir_vrp_output(local: &VcirLocalOutput) -> Result<Vrp, String> {
    let payload: VcirVrpPayload = serde_json::from_str(&local.payload_json)
        .map_err(|e| format!("invalid VRP payload JSON: {e}"))?;
    Ok(Vrp {
        asn: payload.asn,
        prefix: parse_vcir_prefix(&payload.prefix)?,
        max_length: payload.max_length,
    })
}

fn parse_vcir_aspa_output(local: &VcirLocalOutput) -> Result<AspaAttestation, String> {
    let payload: VcirAspaPayload = serde_json::from_str(&local.payload_json)
        .map_err(|e| format!("invalid ASPA payload JSON: {e}"))?;
    Ok(AspaAttestation {
        customer_as_id: payload.customer_as_id,
        provider_as_ids: payload.provider_as_ids,
    })
}

fn parse_vcir_router_key_output(local: &VcirLocalOutput) -> Result<RouterKeyPayload, String> {
    let payload: VcirRouterKeyPayload = serde_json::from_str(&local.payload_json)
        .map_err(|e| format!("invalid Router Key payload JSON: {e}"))?;
    let ski = hex::decode(&payload.ski_hex)
        .map_err(|e| format!("invalid Router Key SKI hex: {e}"))?;
    let spki_der = base64::engine::general_purpose::STANDARD.decode(&payload.spki_der_base64)
        .map_err(|e| format!("invalid Router Key SPKI base64: {e}"))?;
    Ok(RouterKeyPayload {
        as_id: payload.as_id,
        ski,
        spki_der,
        source_object_uri: local.source_object_uri.clone(),
        source_object_hash: local.source_object_hash.clone(),
        source_ee_cert_hash: local.source_ee_cert_hash.clone(),
        item_effective_until: local.item_effective_until.clone(),
    })
}

fn parse_vcir_prefix(prefix: &str) -> Result<crate::data_model::roa::IpPrefix, String> {
    let (addr, len) = prefix
        .split_once('/')
        .ok_or_else(|| format!("prefix missing '/': {prefix}"))?;
    let prefix_len = len
        .parse::<u16>()
        .map_err(|e| format!("invalid prefix length '{len}': {e}"))?;
    let ip = addr
        .parse::<std::net::IpAddr>()
        .map_err(|e| format!("invalid IP address '{addr}': {e}"))?;
    match ip {
        std::net::IpAddr::V4(v4) => Ok(crate::data_model::roa::IpPrefix {
            afi: RoaAfi::Ipv4,
            prefix_len,
            addr: {
                let mut addr = [0u8; 16];
                addr[..4].copy_from_slice(&v4.octets());
                addr
            },
        }),
        std::net::IpAddr::V6(v6) => Ok(crate::data_model::roa::IpPrefix {
            afi: RoaAfi::Ipv6,
            prefix_len,
            addr: v6.octets(),
        }),
    }
}

fn restore_children_from_vcir(
    store: &RocksStore,
    ca: &CaInstanceHandle,
    vcir: &ValidatedCaInstanceResult,
    warnings: &mut Vec<Warning>,
) -> (Vec<DiscoveredChildCaInstance>, Vec<ObjectAuditEntry>) {
    let mut children = Vec::new();
    let mut audits = Vec::new();
    for child in &vcir.child_entries {
        match store.get_raw_by_hash_entry(&child.child_cert_hash) {
            Ok(Some(entry)) => {
                children.push(DiscoveredChildCaInstance {
                    handle: CaInstanceHandle {
                        depth: 0,
                        tal_id: ca.tal_id.clone(),
                        parent_manifest_rsync_uri: Some(ca.manifest_rsync_uri.clone()),
                        ca_certificate_der: entry.bytes,
                        ca_certificate_rsync_uri: Some(child.child_cert_rsync_uri.clone()),
                        effective_ip_resources: child.child_effective_ip_resources.clone(),
                        effective_as_resources: child.child_effective_as_resources.clone(),
                        rsync_base_uri: child.child_rsync_base_uri.clone(),
                        manifest_rsync_uri: child.child_manifest_rsync_uri.clone(),
                        publication_point_rsync_uri: child
                            .child_publication_point_rsync_uri
                            .clone(),
                        rrdp_notification_uri: child.child_rrdp_notification_uri.clone(),
                    },
                    discovered_from: crate::audit::DiscoveredFrom {
                        parent_manifest_rsync_uri: ca.manifest_rsync_uri.clone(),
                        child_ca_certificate_rsync_uri: child.child_cert_rsync_uri.clone(),
                        child_ca_certificate_sha256_hex: child.child_cert_hash.clone(),
                    },
                });
                audits.push(ObjectAuditEntry {
                    rsync_uri: child.child_cert_rsync_uri.clone(),
                    sha256_hex: child.child_cert_hash.clone(),
                    kind: AuditObjectKind::Certificate,
                    result: AuditObjectResult::Ok,
                    detail: Some("restored child CA instance from VCIR".to_string()),
                });
            }
            Ok(None) => {
                warnings.push(
                    Warning::new("child certificate raw bytes missing for VCIR child restoration")
                        .with_context(&child.child_cert_rsync_uri),
                );
                audits.push(ObjectAuditEntry {
                    rsync_uri: child.child_cert_rsync_uri.clone(),
                    sha256_hex: child.child_cert_hash.clone(),
                    kind: AuditObjectKind::Certificate,
                    result: AuditObjectResult::Error,
                    detail: Some(
                        "child certificate raw bytes missing for VCIR child restoration"
                            .to_string(),
                    ),
                });
            }
            Err(e) => {
                warnings.push(
                    Warning::new(format!(
                        "child certificate raw bytes load failed for VCIR child restoration: {e}"
                    ))
                    .with_context(&child.child_cert_rsync_uri),
                );
                audits.push(ObjectAuditEntry {
                    rsync_uri: child.child_cert_rsync_uri.clone(),
                    sha256_hex: child.child_cert_hash.clone(),
                    kind: AuditObjectKind::Certificate,
                    result: AuditObjectResult::Error,
                    detail: Some(format!(
                        "child certificate raw bytes load failed for VCIR child restoration: {e}"
                    )),
                });
            }
        }
    }
    (children, audits)
}

fn persist_vcir_for_fresh_result(
    store: &RocksStore,
    ca: &CaInstanceHandle,
    pack: &PublicationPointSnapshot,
    objects: &crate::validation::objects::ObjectsOutput,
    warnings: &[Warning],
    child_audits: &[ObjectAuditEntry],
    discovered_children: &[DiscoveredChildCaInstance],
    validation_time: time::OffsetDateTime,
) -> Result<(), String> {
    if objects.stats.publication_point_dropped {
        return Ok(());
    }

    let embedded_evidence = collect_vcir_embedded_evidence(pack, objects)?;
    persist_vcir_non_repository_evidence(store, ca, &embedded_evidence)
        .map_err(|e| format!("store VCIR audit evidence failed: {e}"))?;

    let vcir = build_vcir_from_fresh_result(
        ca,
        pack,
        objects,
        warnings,
        child_audits,
        discovered_children,
        validation_time,
        &embedded_evidence,
    )?;
    let previous = store
        .get_vcir(&pack.manifest_rsync_uri)
        .map_err(|e| format!("load existing VCIR failed: {e}"))?;

    store
        .replace_vcir_and_audit_rule_indexes(previous.as_ref(), &vcir)
        .map_err(|e| format!("store VCIR and audit rule index failed: {e}"))?;

    Ok(())
}

fn build_vcir_from_fresh_result(
    ca: &CaInstanceHandle,
    pack: &PublicationPointSnapshot,
    objects: &crate::validation::objects::ObjectsOutput,
    warnings: &[Warning],
    child_audits: &[ObjectAuditEntry],
    discovered_children: &[DiscoveredChildCaInstance],
    validation_time: time::OffsetDateTime,
    embedded_evidence: &[VcirEmbeddedEvidence],
) -> Result<ValidatedCaInstanceResult, String> {
    let current_crl = select_manifest_current_crl_from_snapshot(pack)?;
    let ca_cert = ResourceCertificate::decode_der(&ca.ca_certificate_der)
        .map_err(|e| format!("decode current CA certificate failed: {e}"))?;
    let local_outputs = build_vcir_local_outputs(ca, pack, objects)?;
    let child_entries = build_vcir_child_entries(discovered_children, validation_time)?;
    let related_artifacts = build_vcir_related_artifacts(
        ca,
        pack,
        current_crl.file.rsync_uri.as_str(),
        objects,
        child_audits,
        embedded_evidence,
    );
    let accepted_object_count = related_artifacts
        .iter()
        .filter(|artifact| artifact.validation_status == VcirArtifactValidationStatus::Accepted)
        .count() as u32;
    let rejected_object_count = related_artifacts
        .iter()
        .filter(|artifact| artifact.validation_status == VcirArtifactValidationStatus::Rejected)
        .count() as u32;
    let ca_ski = hex::encode(
        ca_cert
            .tbs
            .extensions
            .subject_key_identifier
            .as_ref()
            .ok_or_else(|| "current CA certificate missing SubjectKeyIdentifier".to_string())?,
    );
    let issuer_ski = hex::encode(
        ca_cert
            .tbs
            .extensions
            .authority_key_identifier
            .as_ref()
            .or(ca_cert.tbs.extensions.subject_key_identifier.as_ref())
            .ok_or_else(|| "current CA certificate missing AuthorityKeyIdentifier".to_string())?,
    );

    let vcir = ValidatedCaInstanceResult {
        manifest_rsync_uri: pack.manifest_rsync_uri.clone(),
        parent_manifest_rsync_uri: ca.parent_manifest_rsync_uri.clone(),
        tal_id: ca.tal_id.clone(),
        ca_subject_name: ca_cert.tbs.subject_name.to_string(),
        ca_ski,
        issuer_ski,
        last_successful_validation_time: PackTime::from_utc_offset_datetime(validation_time),
        current_manifest_rsync_uri: pack.manifest_rsync_uri.clone(),
        current_crl_rsync_uri: current_crl.file.rsync_uri.clone(),
        validated_manifest_meta: crate::storage::ValidatedManifestMeta {
            validated_manifest_number: pack.manifest_number_be.clone(),
            validated_manifest_this_update: pack.this_update.clone(),
            validated_manifest_next_update: pack.next_update.clone(),
        },
        instance_gate: VcirInstanceGate {
            manifest_next_update: pack.next_update.clone(),
            current_crl_next_update: PackTime::from_utc_offset_datetime(
                current_crl.crl.next_update.utc,
            ),
            self_ca_not_after: PackTime::from_utc_offset_datetime(ca_cert.tbs.validity_not_after),
            instance_effective_until: PackTime::from_utc_offset_datetime(
                pack.next_update
                    .parse()
                    .map_err(|e| format!("parse snapshot next_update failed: {e}"))?
                    .min(current_crl.crl.next_update.utc)
                    .min(ca_cert.tbs.validity_not_after),
            ),
        },
        child_entries,
        local_outputs: local_outputs.clone(),
        related_artifacts: related_artifacts.clone(),
        summary: VcirSummary {
            local_vrp_count: local_outputs
                .iter()
                .filter(|output| output.output_type == VcirOutputType::Vrp)
                .count() as u32,
            local_aspa_count: local_outputs
                .iter()
                .filter(|output| output.output_type == VcirOutputType::Aspa)
                .count() as u32,
            local_router_key_count: local_outputs
                .iter()
                .filter(|output| output.output_type == VcirOutputType::RouterKey)
                .count() as u32,
            child_count: discovered_children.len() as u32,
            accepted_object_count,
            rejected_object_count,
        },
        audit_summary: VcirAuditSummary {
            failed_fetch_eligible: true,
            last_failed_fetch_reason: None,
            warning_count: (warnings.len() + objects.warnings.len()) as u32,
            audit_flags: Vec::new(),
        },
    };
    vcir.validate_internal().map_err(|e| e.to_string())?;
    Ok(vcir)
}

struct CurrentCrlRef<'a> {
    file: &'a PackFile,
    crl: RpkixCrl,
}

fn select_manifest_current_crl_from_snapshot(
    pack: &PublicationPointSnapshot,
) -> Result<CurrentCrlRef<'_>, String> {
    let manifest = ManifestObject::decode_der(&pack.manifest_bytes)
        .map_err(|e| format!("decode snapshot manifest for VCIR failed: {e}"))?;
    let ee = &manifest.signed_object.signed_data.certificates[0].resource_cert;
    let crldp_uris = ee
        .tbs
        .extensions
        .crl_distribution_points_uris
        .as_ref()
        .ok_or_else(|| "manifest EE certificate missing CRLDistributionPoints".to_string())?;
    for uri in crldp_uris {
        if let Some(file) = pack
            .files
            .iter()
            .find(|candidate| candidate.rsync_uri == *uri)
        {
            let crl = RpkixCrl::decode_der(&file.bytes)
                .map_err(|e| format!("decode current CRL for VCIR failed: {e}"))?;
            return Ok(CurrentCrlRef { file, crl });
        }
    }
    Err(format!(
        "manifest EE certificate CRLDistributionPoints not found in pack: {}",
        crldp_uris.join(", ")
    ))
}

fn build_vcir_local_outputs(
    ca: &CaInstanceHandle,
    pack: &PublicationPointSnapshot,
    objects: &crate::validation::objects::ObjectsOutput,
) -> Result<Vec<VcirLocalOutput>, String> {
    if !objects.local_outputs_cache.is_empty() {
        return Ok(objects.local_outputs_cache.clone());
    }

    let accepted_roa_uris: HashSet<&str> = objects
        .audit
        .iter()
        .filter(|entry| entry.kind == AuditObjectKind::Roa && entry.result == AuditObjectResult::Ok)
        .map(|entry| entry.rsync_uri.as_str())
        .collect();
    let accepted_aspa_uris: HashSet<&str> = objects
        .audit
        .iter()
        .filter(|entry| {
            entry.kind == AuditObjectKind::Aspa && entry.result == AuditObjectResult::Ok
        })
        .map(|entry| entry.rsync_uri.as_str())
        .collect();

    let mut out = Vec::new();
    for file in &pack.files {
        let source_object_hash = sha256_hex_from_32(&file.sha256);
        if accepted_roa_uris.contains(file.rsync_uri.as_str()) {
            let roa = RoaObject::decode_der(&file.bytes)
                .map_err(|e| format!("decode accepted ROA for VCIR failed: {e}"))?;
            let ee = &roa.signed_object.signed_data.certificates[0];
            let source_ee_cert_hash = sha256_hex(ee.raw_der.as_slice());
            let item_effective_until =
                PackTime::from_utc_offset_datetime(ee.resource_cert.tbs.validity_not_after);
            for vrp in roa_to_vrps_for_vcir(&roa) {
                let prefix = vrp_prefix_to_string(&vrp);
                let payload_json = json!({
                    "asn": vrp.asn,
                    "prefix": prefix,
                    "max_length": vrp.max_length,
                })
                .to_string();
                let rule_hash = sha256_hex(
                    format!(
                        "roa-rule:{}:{}:{}:{}",
                        source_object_hash, vrp.asn, prefix, vrp.max_length
                    )
                    .as_bytes(),
                );
                out.push(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![
                        ca.manifest_rsync_uri.clone(),
                        file.rsync_uri.clone(),
                        source_object_hash.clone(),
                    ],
                });
            }
        } else if accepted_aspa_uris.contains(file.rsync_uri.as_str()) {
            let aspa = AspaObject::decode_der(&file.bytes)
                .map_err(|e| format!("decode accepted ASPA for VCIR failed: {e}"))?;
            let ee = &aspa.signed_object.signed_data.certificates[0];
            let source_ee_cert_hash = sha256_hex(ee.raw_der.as_slice());
            let item_effective_until =
                PackTime::from_utc_offset_datetime(ee.resource_cert.tbs.validity_not_after);
            let payload_json = json!({
                "customer_as_id": aspa.aspa.customer_as_id,
                "provider_as_ids": aspa.aspa.provider_as_ids,
            })
            .to_string();
            let providers = aspa
                .aspa
                .provider_as_ids
                .iter()
                .map(u32::to_string)
                .collect::<Vec<_>>()
                .join(",");
            let rule_hash = sha256_hex(
                format!(
                    "aspa-rule:{}:{}:{}",
                    source_object_hash, aspa.aspa.customer_as_id, providers
                )
                .as_bytes(),
            );
            out.push(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,
                rule_hash,
                validation_path_hint: vec![
                    ca.manifest_rsync_uri.clone(),
                    file.rsync_uri.clone(),
                    source_object_hash,
                ],
            });
        }
    }
    Ok(out)
}

fn build_router_key_local_outputs(
    ca: &CaInstanceHandle,
    router_keys: &[RouterKeyPayload],
) -> Vec<VcirLocalOutput> {
    router_keys
        .iter()
        .map(|router_key| {
            let ski_hex = hex::encode(&router_key.ski);
            let spki_der_base64 = base64::engine::general_purpose::STANDARD.encode(&router_key.spki_der);
            let rule_hash = sha256_hex(
                format!(
                    "router-key-rule:{}:{}:{}:{}",
                    router_key.source_object_hash, router_key.as_id, ski_hex, spki_der_base64
                )
                .as_bytes(),
            );
            VcirLocalOutput {
                output_id: rule_hash.clone(),
                output_type: VcirOutputType::RouterKey,
                item_effective_until: router_key.item_effective_until.clone(),
                source_object_uri: router_key.source_object_uri.clone(),
                source_object_type: "router_key".to_string(),
                source_object_hash: router_key.source_object_hash.clone(),
                source_ee_cert_hash: router_key.source_ee_cert_hash.clone(),
                payload_json: json!({
                    "as_id": router_key.as_id,
                    "ski_hex": ski_hex,
                    "spki_der_base64": spki_der_base64,
                })
                .to_string(),
                rule_hash,
                validation_path_hint: vec![
                    ca.manifest_rsync_uri.clone(),
                    router_key.source_object_uri.clone(),
                    router_key.source_object_hash.clone(),
                ],
            }
        })
        .collect()
}

fn build_vcir_child_entries(
    discovered_children: &[DiscoveredChildCaInstance],
    validation_time: time::OffsetDateTime,
) -> Result<Vec<VcirChildEntry>, String> {
    let mut out = Vec::with_capacity(discovered_children.len());
    for child in discovered_children {
        let child_cert = ResourceCertificate::decode_der(&child.handle.ca_certificate_der)
            .map_err(|e| format!("decode child certificate for VCIR failed: {e}"))?;
        let child_ski = child_cert
            .tbs
            .extensions
            .subject_key_identifier
            .as_ref()
            .ok_or_else(|| "child certificate missing SubjectKeyIdentifier".to_string())?;
        out.push(VcirChildEntry {
            child_manifest_rsync_uri: child.handle.manifest_rsync_uri.clone(),
            child_cert_rsync_uri: child.discovered_from.child_ca_certificate_rsync_uri.clone(),
            child_cert_hash: child
                .discovered_from
                .child_ca_certificate_sha256_hex
                .clone(),
            child_ski: hex::encode(child_ski),
            child_rsync_base_uri: child.handle.rsync_base_uri.clone(),
            child_publication_point_rsync_uri: child.handle.publication_point_rsync_uri.clone(),
            child_rrdp_notification_uri: child.handle.rrdp_notification_uri.clone(),
            child_effective_ip_resources: child.handle.effective_ip_resources.clone(),
            child_effective_as_resources: child.handle.effective_as_resources.clone(),
            accepted_at_validation_time: PackTime::from_utc_offset_datetime(validation_time),
        });
    }
    Ok(out)
}

#[derive(Clone, Debug)]
struct VcirEmbeddedEvidence {
    artifact: VcirRelatedArtifact,
    raw_entry: RawByHashEntry,
}

fn collect_vcir_embedded_evidence(
    pack: &PublicationPointSnapshot,
    objects: &crate::validation::objects::ObjectsOutput,
) -> Result<Vec<VcirEmbeddedEvidence>, String> {
    let mut evidence = Vec::new();
    let mut seen_hashes = HashSet::new();

    let manifest = ManifestObject::decode_der(&pack.manifest_bytes)
        .map_err(|e| format!("decode manifest for VCIR embedded evidence failed: {e}"))?;
    if let Some(ee) = manifest.signed_object.signed_data.certificates.first() {
        let ee_hash = sha256_hex(ee.raw_der.as_slice());
        if seen_hashes.insert(ee_hash.clone()) {
            evidence.push(VcirEmbeddedEvidence {
                artifact: VcirRelatedArtifact {
                    artifact_role: VcirArtifactRole::EeCert,
                    artifact_kind: VcirArtifactKind::Cer,
                    uri: None,
                    sha256: ee_hash.clone(),
                    object_type: Some("cer".to_string()),
                    validation_status: VcirArtifactValidationStatus::Accepted,
                },
                raw_entry: embedded_raw_entry(ee_hash, ee.raw_der.to_vec()),
            });
        }
    }

    let accepted_uris: HashSet<&str> = objects
        .audit
        .iter()
        .filter(|entry| matches!(entry.result, AuditObjectResult::Ok))
        .map(|entry| entry.rsync_uri.as_str())
        .collect();

    for file in &pack.files {
        let Some(kind) = signed_object_kind_from_uri(file.rsync_uri.as_str()) else {
            continue;
        };
        if !accepted_uris.contains(file.rsync_uri.as_str()) {
            continue;
        }
        let ee_der = match kind {
            VcirArtifactKind::Roa => RoaObject::decode_der(&file.bytes)
                .map_err(|e| format!("decode accepted ROA for VCIR embedded evidence failed: {e}"))?
                .signed_object
                .signed_data
                .certificates
                .first()
                .map(|cert| cert.raw_der.to_vec()),
            VcirArtifactKind::Aspa => AspaObject::decode_der(&file.bytes)
                .map_err(|e| {
                    format!("decode accepted ASPA for VCIR embedded evidence failed: {e}")
                })?
                .signed_object
                .signed_data
                .certificates
                .first()
                .map(|cert| cert.raw_der.to_vec()),
            _ => None,
        };
        let Some(ee_der) = ee_der else {
            continue;
        };
        let ee_hash = sha256_hex(ee_der.as_slice());
        if !seen_hashes.insert(ee_hash.clone()) {
            continue;
        }
        evidence.push(VcirEmbeddedEvidence {
            artifact: VcirRelatedArtifact {
                artifact_role: VcirArtifactRole::EeCert,
                artifact_kind: VcirArtifactKind::Cer,
                uri: None,
                sha256: ee_hash.clone(),
                object_type: Some("cer".to_string()),
                validation_status: VcirArtifactValidationStatus::Accepted,
            },
            raw_entry: embedded_raw_entry(ee_hash, ee_der),
        });
    }

    Ok(evidence)
}

fn persist_vcir_non_repository_evidence(
    store: &RocksStore,
    ca: &CaInstanceHandle,
    embedded_evidence: &[VcirEmbeddedEvidence],
) -> Result<(), String> {
    let current_ca_hash = sha256_hex(&ca.ca_certificate_der);
    let mut current_ca_entry =
        RawByHashEntry::from_bytes(current_ca_hash, ca.ca_certificate_der.clone());
    if let Some(uri) = ca.ca_certificate_rsync_uri.as_ref() {
        current_ca_entry.origin_uris.push(uri.clone());
    }
    current_ca_entry.object_type = Some("cer".to_string());
    current_ca_entry.encoding = Some("der".to_string());
    upsert_raw_by_hash_entry(store, current_ca_entry)?;

    for evidence in embedded_evidence {
        upsert_raw_by_hash_entry(store, evidence.raw_entry.clone())?;
    }
    Ok(())
}

fn upsert_raw_by_hash_entry(store: &RocksStore, entry: RawByHashEntry) -> Result<(), String> {
    match store.get_raw_by_hash_entry(&entry.sha256_hex) {
        Ok(Some(existing)) => {
            if existing.bytes != entry.bytes {
                return Err(format!(
                    "raw_by_hash collision for sha256 {} while storing VCIR audit evidence",
                    entry.sha256_hex
                ));
            }
            let mut merged = existing;
            let mut changed = false;
            for uri in entry.origin_uris {
                if !merged
                    .origin_uris
                    .iter()
                    .any(|existing_uri| existing_uri == &uri)
                {
                    merged.origin_uris.push(uri);
                    changed = true;
                }
            }
            if merged.object_type.is_none() && entry.object_type.is_some() {
                merged.object_type = entry.object_type;
                changed = true;
            }
            if merged.encoding.is_none() && entry.encoding.is_some() {
                merged.encoding = entry.encoding;
                changed = true;
            }
            if changed {
                store
                    .put_raw_by_hash_entry(&merged)
                    .map_err(|e| format!("update raw_by_hash entry failed: {e}"))?;
            }
            Ok(())
        }
        Ok(None) => store
            .put_raw_by_hash_entry(&entry)
            .map_err(|e| format!("store raw_by_hash entry failed: {e}")),
        Err(e) => Err(format!("load raw_by_hash entry failed: {e}")),
    }
}

fn embedded_raw_entry(sha256_hex: String, bytes: Vec<u8>) -> RawByHashEntry {
    let mut entry = RawByHashEntry::from_bytes(sha256_hex, bytes);
    entry.object_type = Some("cer".to_string());
    entry.encoding = Some("der".to_string());
    entry
}

fn signed_object_kind_from_uri(uri: &str) -> Option<VcirArtifactKind> {
    if uri.ends_with(".roa") {
        Some(VcirArtifactKind::Roa)
    } else if uri.ends_with(".asa") {
        Some(VcirArtifactKind::Aspa)
    } else if uri.ends_with(".gbr") {
        Some(VcirArtifactKind::Gbr)
    } else {
        None
    }
}

fn build_vcir_related_artifacts(
    ca: &CaInstanceHandle,
    pack: &PublicationPointSnapshot,
    current_crl_rsync_uri: &str,
    objects: &crate::validation::objects::ObjectsOutput,
    child_audits: &[ObjectAuditEntry],
    embedded_evidence: &[VcirEmbeddedEvidence],
) -> Vec<VcirRelatedArtifact> {
    let mut audit_by_uri: HashMap<&str, AuditObjectResult> = HashMap::new();
    for entry in child_audits.iter().chain(objects.audit.iter()) {
        audit_by_uri.insert(entry.rsync_uri.as_str(), entry.result.clone());
    }

    let mut artifacts = Vec::with_capacity(pack.files.len() + 2);
    artifacts.push(VcirRelatedArtifact {
        artifact_role: VcirArtifactRole::Manifest,
        artifact_kind: VcirArtifactKind::Mft,
        uri: Some(pack.manifest_rsync_uri.clone()),
        sha256: sha256_hex(&pack.manifest_bytes),
        object_type: Some("mft".to_string()),
        validation_status: VcirArtifactValidationStatus::Accepted,
    });
    artifacts.push(VcirRelatedArtifact {
        artifact_role: if ca.parent_manifest_rsync_uri.is_none() {
            VcirArtifactRole::TrustAnchorCert
        } else {
            VcirArtifactRole::IssuerCert
        },
        artifact_kind: VcirArtifactKind::Cer,
        uri: ca.ca_certificate_rsync_uri.clone(),
        sha256: sha256_hex(&ca.ca_certificate_der),
        object_type: Some("cer".to_string()),
        validation_status: VcirArtifactValidationStatus::Accepted,
    });

    for file in &pack.files {
        let result = audit_by_uri
            .get(file.rsync_uri.as_str())
            .cloned()
            .unwrap_or(AuditObjectResult::Ok);
        let (artifact_role, artifact_kind) = artifact_role_and_kind(file, current_crl_rsync_uri);
        artifacts.push(VcirRelatedArtifact {
            artifact_role,
            artifact_kind,
            uri: Some(file.rsync_uri.clone()),
            sha256: sha256_hex_from_32(&file.sha256),
            object_type: object_type_from_uri(file.rsync_uri.as_str()),
            validation_status: audit_result_to_vcir_status(&result),
        });
    }

    artifacts.extend(
        embedded_evidence
            .iter()
            .map(|evidence| evidence.artifact.clone()),
    );
    artifacts
}

fn artifact_role_and_kind(
    file: &PackFile,
    current_crl_rsync_uri: &str,
) -> (VcirArtifactRole, VcirArtifactKind) {
    if file.rsync_uri == current_crl_rsync_uri {
        (VcirArtifactRole::CurrentCrl, VcirArtifactKind::Crl)
    } else if file.rsync_uri.ends_with(".cer") {
        (VcirArtifactRole::ChildCaCert, VcirArtifactKind::Cer)
    } else if file.rsync_uri.ends_with(".roa") {
        (VcirArtifactRole::SignedObject, VcirArtifactKind::Roa)
    } else if file.rsync_uri.ends_with(".asa") {
        (VcirArtifactRole::SignedObject, VcirArtifactKind::Aspa)
    } else if file.rsync_uri.ends_with(".gbr") {
        (VcirArtifactRole::SignedObject, VcirArtifactKind::Gbr)
    } else if file.rsync_uri.ends_with(".crl") {
        (VcirArtifactRole::Other, VcirArtifactKind::Crl)
    } else if file.rsync_uri.ends_with(".mft") {
        (VcirArtifactRole::Manifest, VcirArtifactKind::Mft)
    } else {
        (VcirArtifactRole::Other, VcirArtifactKind::Other)
    }
}

fn object_type_from_uri(uri: &str) -> Option<String> {
    uri.rsplit_once('.')
        .map(|(_, ext)| ext.to_ascii_lowercase())
}

fn audit_result_to_vcir_status(result: &AuditObjectResult) -> VcirArtifactValidationStatus {
    match result {
        AuditObjectResult::Ok => VcirArtifactValidationStatus::Accepted,
        AuditObjectResult::Error => VcirArtifactValidationStatus::Rejected,
        AuditObjectResult::Skipped => VcirArtifactValidationStatus::WarningOnly,
    }
}


fn roa_to_vrps_for_vcir(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
}

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

#[cfg(test)]
mod tests {
    use super::*;
    use crate::data_model::rc::ResourceCertificate;
    use crate::fetch::rsync::LocalDirRsyncFetcher;
    use crate::fetch::rsync::{RsyncFetchError, RsyncFetcher};
    use crate::storage::{
        PackFile, PackTime, RawByHashEntry, RocksStore, ValidatedCaInstanceResult, ValidatedManifestMeta,
        VcirArtifactKind, VcirArtifactRole, VcirArtifactValidationStatus, VcirAuditSummary,
        VcirChildEntry, VcirInstanceGate, VcirLocalOutput, VcirOutputType, VcirRelatedArtifact,
        VcirSummary,
    };
    use crate::sync::rrdp::Fetcher;
    use crate::validation::publication_point::PublicationPointSnapshot;
    use crate::validation::tree::PublicationPointRunner;

    use std::process::Command;
    use std::sync::Arc;
    use std::sync::atomic::{AtomicUsize, Ordering};

    struct NeverHttpFetcher;
    impl Fetcher for NeverHttpFetcher {
        fn fetch(&self, _uri: &str) -> Result<Vec<u8>, String> {
            Err("http fetch disabled in test".to_string())
        }
    }

    struct FailingRsyncFetcher;
    impl RsyncFetcher for FailingRsyncFetcher {
        fn fetch_objects(
            &self,
            _rsync_base_uri: &str,
        ) -> Result<Vec<(String, Vec<u8>)>, RsyncFetchError> {
            Err(RsyncFetchError::Fetch("rsync disabled in test".to_string()))
        }
    }

    fn openssl_available() -> bool {
        Command::new("openssl")
            .arg("version")
            .output()
            .map(|o| o.status.success())
            .unwrap_or(false)
    }

    struct Generated {
        issuer_ca_der: Vec<u8>,
        child_ca_der: Vec<u8>,
        issuer_crl_der: Vec<u8>,
    }

    fn run(cmd: &mut Command) {
        let out = cmd.output().expect("run command");
        if !out.status.success() {
            panic!(
                "command failed: {:?}\nstdout={}\nstderr={}",
                cmd,
                String::from_utf8_lossy(&out.stdout),
                String::from_utf8_lossy(&out.stderr)
            );
        }
    }

    fn generate_chain_and_crl() -> Generated {
        assert!(openssl_available(), "openssl is required for this test");

        let td = tempfile::tempdir().expect("tempdir");
        let dir = td.path();

        std::fs::create_dir_all(dir.join("newcerts")).expect("newcerts");
        std::fs::write(dir.join("index.txt"), b"").expect("index");
        std::fs::write(dir.join("serial"), b"1000\n").expect("serial");
        std::fs::write(dir.join("crlnumber"), b"1000\n").expect("crlnumber");

        let cnf = format!(
            r#"
[ ca ]
default_ca = CA_default

[ CA_default ]
dir = {dir}
database = $dir/index.txt
new_certs_dir = $dir/newcerts
certificate = $dir/issuer.pem
private_key = $dir/issuer.key
serial = $dir/serial
crlnumber = $dir/crlnumber
default_md = sha256
default_days = 365
default_crl_days = 1
policy = policy_any
x509_extensions = v3_issuer_ca
crl_extensions = crl_ext
unique_subject = no
copy_extensions = none

[ policy_any ]
commonName = supplied

[ req ]
prompt = no
distinguished_name = dn

[ dn ]
CN = Test Issuer CA

[ v3_issuer_ca ]
basicConstraints = critical,CA:true
keyUsage = critical, keyCertSign, cRLSign
subjectKeyIdentifier = hash
authorityKeyIdentifier = keyid:always
certificatePolicies = critical, 1.3.6.1.5.5.7.14.2
subjectInfoAccess = caRepository;URI:rsync://example.test/repo/issuer/, rpkiManifest;URI:rsync://example.test/repo/issuer/issuer.mft, rpkiNotify;URI:https://example.test/notification.xml
sbgp-ipAddrBlock = critical, IPv4:10.0.0.0/8
sbgp-autonomousSysNum = critical, AS:64496-64511

[ v3_child_ca ]
basicConstraints = critical,CA:true
keyUsage = critical, keyCertSign, cRLSign
subjectKeyIdentifier = hash
authorityKeyIdentifier = keyid:always
crlDistributionPoints = URI:rsync://example.test/repo/issuer/issuer.crl
authorityInfoAccess = caIssuers;URI:rsync://example.test/repo/issuer/issuer.cer
certificatePolicies = critical, 1.3.6.1.5.5.7.14.2
subjectInfoAccess = caRepository;URI:rsync://example.test/repo/child/, rpkiManifest;URI:rsync://example.test/repo/child/child.mft, rpkiNotify;URI:https://example.test/notification.xml
sbgp-ipAddrBlock = critical, IPv4:10.0.0.0/16
sbgp-autonomousSysNum = critical, AS:64496

[ crl_ext ]
authorityKeyIdentifier = keyid:always
"#,
            dir = dir.display()
        );
        std::fs::write(dir.join("openssl.cnf"), cnf.as_bytes()).expect("write cnf");

        run(Command::new("openssl")
            .arg("genrsa")
            .arg("-out")
            .arg(dir.join("issuer.key"))
            .arg("2048"));
        run(Command::new("openssl")
            .arg("req")
            .arg("-new")
            .arg("-x509")
            .arg("-sha256")
            .arg("-days")
            .arg("365")
            .arg("-key")
            .arg(dir.join("issuer.key"))
            .arg("-config")
            .arg(dir.join("openssl.cnf"))
            .arg("-extensions")
            .arg("v3_issuer_ca")
            .arg("-out")
            .arg(dir.join("issuer.pem")));

        run(Command::new("openssl")
            .arg("genrsa")
            .arg("-out")
            .arg(dir.join("child.key"))
            .arg("2048"));
        run(Command::new("openssl")
            .arg("req")
            .arg("-new")
            .arg("-key")
            .arg(dir.join("child.key"))
            .arg("-subj")
            .arg("/CN=Test Child CA")
            .arg("-out")
            .arg(dir.join("child.csr")));

        run(Command::new("openssl")
            .arg("ca")
            .arg("-batch")
            .arg("-config")
            .arg(dir.join("openssl.cnf"))
            .arg("-in")
            .arg(dir.join("child.csr"))
            .arg("-extensions")
            .arg("v3_child_ca")
            .arg("-out")
            .arg(dir.join("child.pem")));

        run(Command::new("openssl")
            .arg("x509")
            .arg("-in")
            .arg(dir.join("issuer.pem"))
            .arg("-outform")
            .arg("DER")
            .arg("-out")
            .arg(dir.join("issuer.cer")));
        run(Command::new("openssl")
            .arg("x509")
            .arg("-in")
            .arg(dir.join("child.pem"))
            .arg("-outform")
            .arg("DER")
            .arg("-out")
            .arg(dir.join("child.cer")));

        run(Command::new("openssl")
            .arg("ca")
            .arg("-gencrl")
            .arg("-config")
            .arg(dir.join("openssl.cnf"))
            .arg("-out")
            .arg(dir.join("issuer.crl.pem")));
        run(Command::new("openssl")
            .arg("crl")
            .arg("-in")
            .arg(dir.join("issuer.crl.pem"))
            .arg("-outform")
            .arg("DER")
            .arg("-out")
            .arg(dir.join("issuer.crl")));

        Generated {
            issuer_ca_der: std::fs::read(dir.join("issuer.cer")).expect("read issuer der"),
            child_ca_der: std::fs::read(dir.join("child.cer")).expect("read child der"),
            issuer_crl_der: std::fs::read(dir.join("issuer.crl")).expect("read crl der"),
        }
    }



    struct GeneratedRouter {
        issuer_ca_der: Vec<u8>,
        router_der: Vec<u8>,
        issuer_crl_der: Vec<u8>,
    }

    fn generate_router_cert_with_variant(key_spec: &str, include_eku: bool) -> GeneratedRouter {
        assert!(openssl_available(), "openssl is required for this test");

        let td = tempfile::tempdir().expect("tempdir");
        let dir = td.path();

        std::fs::create_dir_all(dir.join("newcerts")).expect("newcerts");
        std::fs::write(dir.join("index.txt"), b"").expect("index");
        std::fs::write(dir.join("serial"), b"1000\n").expect("serial");
        std::fs::write(dir.join("crlnumber"), b"1000\n").expect("crlnumber");

        let eku_line = if include_eku {
            "extendedKeyUsage = 1.3.6.1.5.5.7.3.30"
        } else {
            ""
        };
        let cnf = format!(
            r#"
[ ca ]
default_ca = CA_default

[ CA_default ]
dir = {dir}
database = $dir/index.txt
new_certs_dir = $dir/newcerts
certificate = $dir/issuer.pem
private_key = $dir/issuer.key
serial = $dir/serial
crlnumber = $dir/crlnumber
default_md = sha256
default_days = 365
default_crl_days = 1
policy = policy_any
x509_extensions = v3_issuer_ca
crl_extensions = crl_ext
unique_subject = no
copy_extensions = none

[ policy_any ]
commonName = supplied

[ req ]
prompt = no
distinguished_name = dn

[ dn ]
CN = Test Issuer CA

[ v3_issuer_ca ]
basicConstraints = critical,CA:true
keyUsage = critical, keyCertSign, cRLSign
subjectKeyIdentifier = hash
authorityKeyIdentifier = keyid:always
certificatePolicies = critical, 1.3.6.1.5.5.7.14.2
subjectInfoAccess = caRepository;URI:rsync://example.test/repo/issuer/, rpkiManifest;URI:rsync://example.test/repo/issuer/issuer.mft, rpkiNotify;URI:https://example.test/notification.xml
sbgp-ipAddrBlock = critical, IPv4:10.0.0.0/8
sbgp-autonomousSysNum = critical, AS:64496-64511

[ v3_router ]
keyUsage = critical, digitalSignature
{eku_line}
authorityKeyIdentifier = keyid:always
crlDistributionPoints = URI:rsync://example.test/repo/issuer/issuer.crl
authorityInfoAccess = caIssuers;URI:rsync://example.test/repo/issuer/issuer.cer
certificatePolicies = critical, 1.3.6.1.5.5.7.14.2
sbgp-autonomousSysNum = critical, AS:64496

[ crl_ext ]
authorityKeyIdentifier = keyid:always
"#,
            dir = dir.display(),
            eku_line = eku_line,
        );
        std::fs::write(dir.join("openssl.cnf"), cnf.as_bytes()).expect("write cnf");

        run(Command::new("openssl")
            .arg("genrsa")
            .arg("-out")
            .arg(dir.join("issuer.key"))
            .arg("2048"));
        run(Command::new("openssl")
            .arg("req")
            .arg("-new")
            .arg("-x509")
            .arg("-sha256")
            .arg("-days")
            .arg("365")
            .arg("-key")
            .arg(dir.join("issuer.key"))
            .arg("-config")
            .arg(dir.join("openssl.cnf"))
            .arg("-extensions")
            .arg("v3_issuer_ca")
            .arg("-out")
            .arg(dir.join("issuer.pem")));

        match key_spec {
            "ec-p256" => run(Command::new("openssl")
                .arg("ecparam")
                .arg("-name")
                .arg("prime256v1")
                .arg("-genkey")
                .arg("-noout")
                .arg("-out")
                .arg(dir.join("router.key"))),
            "ec-p384" => run(Command::new("openssl")
                .arg("ecparam")
                .arg("-name")
                .arg("secp384r1")
                .arg("-genkey")
                .arg("-noout")
                .arg("-out")
                .arg(dir.join("router.key"))),
            other => panic!("unsupported key_spec {other}"),
        }

        run(Command::new("openssl")
            .arg("req")
            .arg("-new")
            .arg("-key")
            .arg(dir.join("router.key"))
            .arg("-subj")
            .arg("/CN=ROUTER-0000FC10/serialNumber=01020304")
            .arg("-out")
            .arg(dir.join("router.csr")));

        run(Command::new("openssl")
            .arg("ca")
            .arg("-batch")
            .arg("-config")
            .arg(dir.join("openssl.cnf"))
            .arg("-in")
            .arg(dir.join("router.csr"))
            .arg("-extensions")
            .arg("v3_router")
            .arg("-out")
            .arg(dir.join("router.pem")));

        run(Command::new("openssl")
            .arg("x509")
            .arg("-in")
            .arg(dir.join("issuer.pem"))
            .arg("-outform")
            .arg("DER")
            .arg("-out")
            .arg(dir.join("issuer.cer")));
        run(Command::new("openssl")
            .arg("x509")
            .arg("-in")
            .arg(dir.join("router.pem"))
            .arg("-outform")
            .arg("DER")
            .arg("-out")
            .arg(dir.join("router.cer")));

        run(Command::new("openssl")
            .arg("ca")
            .arg("-gencrl")
            .arg("-config")
            .arg(dir.join("openssl.cnf"))
            .arg("-out")
            .arg(dir.join("issuer.crl.pem")));
        run(Command::new("openssl")
            .arg("crl")
            .arg("-in")
            .arg(dir.join("issuer.crl.pem"))
            .arg("-outform")
            .arg("DER")
            .arg("-out")
            .arg(dir.join("issuer.crl")));

        GeneratedRouter {
            issuer_ca_der: std::fs::read(dir.join("issuer.cer")).expect("read issuer der"),
            router_der: std::fs::read(dir.join("router.cer")).expect("read router der"),
            issuer_crl_der: std::fs::read(dir.join("issuer.crl")).expect("read crl der"),
        }
    }
    fn dummy_pack_with_files(files: Vec<PackFile>) -> PublicationPointSnapshot {
        let now = time::OffsetDateTime::now_utc();
        PublicationPointSnapshot {
            format_version: PublicationPointSnapshot::FORMAT_VERSION_V1,
            manifest_rsync_uri: "rsync://example.test/repo/issuer/issuer.mft".to_string(),
            publication_point_rsync_uri: "rsync://example.test/repo/issuer/".to_string(),
            manifest_number_be: vec![1],
            this_update: PackTime::from_utc_offset_datetime(now),
            next_update: PackTime::from_utc_offset_datetime(now + time::Duration::hours(1)),
            verified_at: PackTime::from_utc_offset_datetime(now),
            manifest_bytes: vec![0x01],
            files,
        }
    }

    fn cernet_publication_point_snapshot_for_vcir_tests(
    ) -> (PublicationPointSnapshot, Vec<u8>, time::OffsetDateTime) {
        let dir = std::path::PathBuf::from(env!("CARGO_MANIFEST_DIR"))
            .join("tests/fixtures/repository/rpki.cernet.net/repo/cernet/0");
        let rsync_base_uri = "rsync://rpki.cernet.net/repo/cernet/0/";
        let manifest_file = "05FC9C5B88506F7C0D3F862C8895BED67E9F8EBA.mft";
        let manifest_rsync_uri = format!("{rsync_base_uri}{manifest_file}");
        let manifest_bytes = std::fs::read(dir.join(manifest_file)).expect("read manifest fixture");
        let manifest = ManifestObject::decode_der(&manifest_bytes).expect("decode manifest fixture");
        let candidate = manifest.manifest.this_update + time::Duration::seconds(60);
        let validation_time = if candidate < manifest.manifest.next_update {
            candidate
        } else {
            manifest.manifest.this_update
        };

        let issuer_ca_der = std::fs::read(
            std::path::PathBuf::from(env!("CARGO_MANIFEST_DIR")).join(
                "tests/fixtures/repository/rpki.apnic.net/repository/B527EF581D6611E2BB468F7C72FD1FF2/BfycW4hQb3wNP4YsiJW-1n6fjro.cer",
            ),
        )
        .expect("read issuer ca fixture");

        let store_dir = tempfile::tempdir().expect("store dir");
        let store = RocksStore::open(store_dir.path()).expect("open rocksdb");
        let policy = Policy {
            sync_preference: crate::policy::SyncPreference::RsyncOnly,
            ..Policy::default()
        };

        sync_publication_point(
            &store,
            &policy,
            None,
            rsync_base_uri,
            &NeverHttpFetcher,
            &LocalDirRsyncFetcher::new(&dir),
            None,
            None,
        )
        .expect("sync cernet fixture");

        let pp = crate::validation::manifest::process_manifest_publication_point(
            &store,
            &policy,
            &manifest_rsync_uri,
            rsync_base_uri,
            issuer_ca_der.as_slice(),
            Some(
                "rsync://rpki.apnic.net/repository/B527EF581D6611E2BB468F7C72FD1FF2/BfycW4hQb3wNP4YsiJW-1n6fjro.cer",
            ),
            validation_time,
        )
        .expect("process manifest publication point");

        (pp.snapshot, issuer_ca_der, validation_time)
    }

    fn sample_vcir_for_projection(
        now: time::OffsetDateTime,
        child_cert_hash: &str,
    ) -> ValidatedCaInstanceResult {
        let manifest_uri = "rsync://example.test/repo/issuer/issuer.mft".to_string();
        let current_crl_uri = "rsync://example.test/repo/issuer/issuer.crl".to_string();
        let child_cert_uri = "rsync://example.test/repo/issuer/child.cer".to_string();
        let child_manifest_uri = "rsync://example.test/repo/child/child.mft".to_string();
        let roa_uri = "rsync://example.test/repo/issuer/a.roa".to_string();
        let aspa_uri = "rsync://example.test/repo/issuer/a.asa".to_string();
        let router_uri = "rsync://example.test/repo/issuer/router.cer".to_string();
        let manifest_hash = sha256_hex(b"manifest-bytes");
        let current_crl_hash = sha256_hex(b"current-crl-bytes");
        let roa_hash = sha256_hex(b"roa-bytes");
        let aspa_hash = sha256_hex(b"aspa-bytes");
        let router_hash = sha256_hex(b"router-bytes");
        let ee_hash = sha256_hex(b"ee-cert-bytes");
        let gate_until = PackTime::from_utc_offset_datetime(now + time::Duration::hours(1));
        ValidatedCaInstanceResult {
            manifest_rsync_uri: manifest_uri.clone(),
            parent_manifest_rsync_uri: None,
            tal_id: "test-tal".to_string(),
            ca_subject_name: "CN=Issuer".to_string(),
            ca_ski: "11".repeat(20),
            issuer_ski: "22".repeat(20),
            last_successful_validation_time: PackTime::from_utc_offset_datetime(now),
            current_manifest_rsync_uri: manifest_uri.clone(),
            current_crl_rsync_uri: current_crl_uri.clone(),
            validated_manifest_meta: ValidatedManifestMeta {
                validated_manifest_number: vec![1],
                validated_manifest_this_update: PackTime::from_utc_offset_datetime(now),
                validated_manifest_next_update: gate_until.clone(),
            },
            instance_gate: VcirInstanceGate {
                manifest_next_update: gate_until.clone(),
                current_crl_next_update: gate_until.clone(),
                self_ca_not_after: PackTime::from_utc_offset_datetime(now + time::Duration::hours(2)),
                instance_effective_until: gate_until.clone(),
            },
            child_entries: vec![VcirChildEntry {
                child_manifest_rsync_uri: child_manifest_uri,
                child_cert_rsync_uri: child_cert_uri.clone(),
                child_cert_hash: child_cert_hash.to_string(),
                child_ski: "33".repeat(20),
                child_rsync_base_uri: "rsync://example.test/repo/child/".to_string(),
                child_publication_point_rsync_uri: "rsync://example.test/repo/child/".to_string(),
                child_rrdp_notification_uri: Some("https://example.test/child-notify.xml".to_string()),
                child_effective_ip_resources: None,
                child_effective_as_resources: None,
                accepted_at_validation_time: PackTime::from_utc_offset_datetime(now),
            }],
            local_outputs: vec![
                VcirLocalOutput {
                    output_id: sha256_hex(b"vrp-out"),
                    output_type: VcirOutputType::Vrp,
                    item_effective_until: PackTime::from_utc_offset_datetime(now + time::Duration::minutes(30)),
                    source_object_uri: roa_uri.clone(),
                    source_object_type: "roa".to_string(),
                    source_object_hash: roa_hash.clone(),
                    source_ee_cert_hash: ee_hash.clone(),
                    payload_json: serde_json::json!({"asn": 64496, "prefix": "203.0.113.0/24", "max_length": 24}).to_string(),
                    rule_hash: sha256_hex(b"roa-rule"),
                    validation_path_hint: vec![manifest_uri.clone(), roa_uri.clone()],
                },
                VcirLocalOutput {
                    output_id: sha256_hex(b"aspa-out"),
                    output_type: VcirOutputType::Aspa,
                    item_effective_until: PackTime::from_utc_offset_datetime(now + time::Duration::minutes(30)),
                    source_object_uri: aspa_uri.clone(),
                    source_object_type: "aspa".to_string(),
                    source_object_hash: aspa_hash.clone(),
                    source_ee_cert_hash: ee_hash,
                    payload_json: serde_json::json!({"customer_as_id": 64496, "provider_as_ids": [64497, 64498]}).to_string(),
                    rule_hash: sha256_hex(b"aspa-rule"),
                    validation_path_hint: vec![manifest_uri.clone(), aspa_uri.clone()],
                },
                VcirLocalOutput {
                    output_id: sha256_hex(b"router-key-out"),
                    output_type: VcirOutputType::RouterKey,
                    item_effective_until: PackTime::from_utc_offset_datetime(now + time::Duration::minutes(30)),
                    source_object_uri: router_uri.clone(),
                    source_object_type: "router_key".to_string(),
                    source_object_hash: router_hash.clone(),
                    source_ee_cert_hash: router_hash.clone(),
                    payload_json: serde_json::json!({
                        "as_id": 64496,
                        "ski_hex": "11".repeat(20),
                        "spki_der_base64": base64::engine::general_purpose::STANDARD.encode([0x30u8, 0x00]),
                    }).to_string(),
                    rule_hash: sha256_hex(b"router-key-rule"),
                    validation_path_hint: vec![manifest_uri.clone(), router_uri.clone()],
                },
            ],
            related_artifacts: vec![
                VcirRelatedArtifact {
                    artifact_role: VcirArtifactRole::Manifest,
                    artifact_kind: VcirArtifactKind::Mft,
                    uri: Some(manifest_uri.clone()),
                    sha256: manifest_hash,
                    object_type: Some("mft".to_string()),
                    validation_status: VcirArtifactValidationStatus::Accepted,
                },
                VcirRelatedArtifact {
                    artifact_role: VcirArtifactRole::CurrentCrl,
                    artifact_kind: VcirArtifactKind::Crl,
                    uri: Some(current_crl_uri),
                    sha256: current_crl_hash,
                    object_type: Some("crl".to_string()),
                    validation_status: VcirArtifactValidationStatus::Accepted,
                },
                VcirRelatedArtifact {
                    artifact_role: VcirArtifactRole::ChildCaCert,
                    artifact_kind: VcirArtifactKind::Cer,
                    uri: Some(child_cert_uri),
                    sha256: child_cert_hash.to_string(),
                    object_type: Some("cer".to_string()),
                    validation_status: VcirArtifactValidationStatus::Accepted,
                },
                VcirRelatedArtifact {
                    artifact_role: VcirArtifactRole::SignedObject,
                    artifact_kind: VcirArtifactKind::Roa,
                    uri: Some(roa_uri),
                    sha256: roa_hash,
                    object_type: Some("roa".to_string()),
                    validation_status: VcirArtifactValidationStatus::Accepted,
                },
                VcirRelatedArtifact {
                    artifact_role: VcirArtifactRole::SignedObject,
                    artifact_kind: VcirArtifactKind::Aspa,
                    uri: Some(aspa_uri),
                    sha256: aspa_hash,
                    object_type: Some("aspa".to_string()),
                    validation_status: VcirArtifactValidationStatus::Accepted,
                },
            ],
            summary: VcirSummary {
                local_vrp_count: 1,
                local_aspa_count: 1,
                local_router_key_count: 1,
                child_count: 1,
                accepted_object_count: 4,
                rejected_object_count: 0,
            },
            audit_summary: VcirAuditSummary {
                failed_fetch_eligible: true,
                last_failed_fetch_reason: None,
                warning_count: 0,
                audit_flags: Vec::new(),
            },
        }
    }

    #[test]
    fn never_http_fetcher_returns_error() {
        let f = NeverHttpFetcher;
        let err = f.fetch("https://example.test/").unwrap_err();
        assert!(err.contains("disabled"), "{err}");
    }

    #[test]
    fn kind_from_rsync_uri_classifies_known_extensions() {
        assert_eq!(
            kind_from_rsync_uri("rsync://example.test/x.crl"),
            AuditObjectKind::Crl
        );
        assert_eq!(
            kind_from_rsync_uri("rsync://example.test/x.cer"),
            AuditObjectKind::Certificate
        );
        assert_eq!(
            kind_from_rsync_uri("rsync://example.test/x.roa"),
            AuditObjectKind::Roa
        );
        assert_eq!(
            kind_from_rsync_uri("rsync://example.test/x.asa"),
            AuditObjectKind::Aspa
        );
        assert_eq!(
            kind_from_rsync_uri("rsync://example.test/x.bin"),
            AuditObjectKind::Other
        );
    }

    #[test]
    fn build_vcir_local_outputs_prefers_cached_outputs() {
        let pack = dummy_pack_with_files(vec![]);
        let ca = CaInstanceHandle {
            depth: 0,
            tal_id: "test-tal".to_string(),
            parent_manifest_rsync_uri: None,
            ca_certificate_der: vec![1],
            ca_certificate_rsync_uri: None,
            effective_ip_resources: None,
            effective_as_resources: None,
            rsync_base_uri: pack.publication_point_rsync_uri.clone(),
            manifest_rsync_uri: pack.manifest_rsync_uri.clone(),
            publication_point_rsync_uri: pack.publication_point_rsync_uri.clone(),
            rrdp_notification_uri: None,
        };
        let cached = vec![VcirLocalOutput {
            output_id: "cached-output".to_string(),
            output_type: VcirOutputType::Vrp,
            item_effective_until: pack.next_update.clone(),
            source_object_uri: "rsync://example.test/repo/issuer/a.roa".to_string(),
            source_object_type: "roa".to_string(),
            source_object_hash: sha256_hex(b"cached-roa"),
            source_ee_cert_hash: sha256_hex(b"cached-ee"),
            payload_json: "{\"asn\":64500}".to_string(),
            rule_hash: sha256_hex(b"cached-rule"),
            validation_path_hint: vec![pack.manifest_rsync_uri.clone()],
        }];
        let outputs = build_vcir_local_outputs(
            &ca,
            &pack,
            &crate::validation::objects::ObjectsOutput {
                vrps: Vec::new(),
                aspas: Vec::new(),
            router_keys: Vec::new(),
                local_outputs_cache: cached.clone(),
                warnings: Vec::new(),
                stats: crate::validation::objects::ObjectsStats::default(),
                audit: Vec::new(),
            },
        )
        .expect("reuse cached outputs");
        assert_eq!(outputs, cached);
    }

    #[test]
    fn collect_and_persist_vcir_embedded_evidence_for_real_signed_objects() {
        let (pack, issuer_ca_der, validation_time) = cernet_publication_point_snapshot_for_vcir_tests();
        let issuer_ca = ResourceCertificate::decode_der(&issuer_ca_der).expect("decode issuer ca");
        let objects = crate::validation::objects::process_publication_point_snapshot_for_issuer(
            &pack,
            &Policy::default(),
            issuer_ca_der.as_slice(),
            Some("rsync://rpki.apnic.net/repository/B527EF581D6611E2BB468F7C72FD1FF2/BfycW4hQb3wNP4YsiJW-1n6fjro.cer"),
            issuer_ca.tbs.extensions.ip_resources.as_ref(),
            issuer_ca.tbs.extensions.as_resources.as_ref(),
            validation_time,
            None,
        );
        assert!(!objects.local_outputs_cache.is_empty(), "expected local outputs from signed objects");

        let evidence = collect_vcir_embedded_evidence(&pack, &objects).expect("collect embedded evidence");
        assert!(evidence.len() >= 2, "expected manifest EE and signed-object EE evidence");

        let store_dir = tempfile::tempdir().expect("store dir");
        let store = RocksStore::open(store_dir.path()).expect("open rocksdb");
        let ca = CaInstanceHandle {
            depth: 0,
            tal_id: "test-tal".to_string(),
            parent_manifest_rsync_uri: None,
            ca_certificate_der: issuer_ca_der.clone(),
            ca_certificate_rsync_uri: Some(
                "rsync://rpki.apnic.net/repository/B527EF581D6611E2BB468F7C72FD1FF2/BfycW4hQb3wNP4YsiJW-1n6fjro.cer".to_string(),
            ),
            effective_ip_resources: issuer_ca.tbs.extensions.ip_resources.clone(),
            effective_as_resources: issuer_ca.tbs.extensions.as_resources.clone(),
            rsync_base_uri: pack.publication_point_rsync_uri.clone(),
            manifest_rsync_uri: pack.manifest_rsync_uri.clone(),
            publication_point_rsync_uri: pack.publication_point_rsync_uri.clone(),
            rrdp_notification_uri: None,
        };
        persist_vcir_non_repository_evidence(&store, &ca, &evidence)
            .expect("persist embedded evidence");

        let issuer_hash = sha256_hex(&issuer_ca_der);
        let issuer_entry = store
            .get_raw_by_hash_entry(&issuer_hash)
            .expect("load issuer raw entry")
            .expect("issuer raw entry present");
        assert!(issuer_entry
            .origin_uris
            .iter()
            .any(|uri| uri.ends_with("BfycW4hQb3wNP4YsiJW-1n6fjro.cer")));
        for entry in &evidence {
            assert!(store
                .get_raw_by_hash_entry(&entry.raw_entry.sha256_hex)
                .expect("load evidence raw entry")
                .is_some());
        }
    }

    #[test]
    fn build_router_key_local_outputs_encodes_router_key_payloads() {
        let ca = CaInstanceHandle {
            depth: 0,
            tal_id: "test-tal".to_string(),
            parent_manifest_rsync_uri: None,
            ca_certificate_der: Vec::new(),
            ca_certificate_rsync_uri: None,
            effective_ip_resources: None,
            effective_as_resources: None,
            rsync_base_uri: "rsync://example.test/repo/issuer/".to_string(),
            manifest_rsync_uri: "rsync://example.test/repo/issuer/issuer.mft".to_string(),
            publication_point_rsync_uri: "rsync://example.test/repo/issuer/".to_string(),
            rrdp_notification_uri: None,
        };
        let outputs = build_router_key_local_outputs(
            &ca,
            &[RouterKeyPayload {
                as_id: 64496,
                ski: vec![0x11; 20],
                spki_der: vec![0x30, 0x00],
                source_object_uri: "rsync://example.test/repo/issuer/router.cer".to_string(),
                source_object_hash: "11".repeat(32),
                source_ee_cert_hash: "11".repeat(32),
                item_effective_until: PackTime { rfc3339_utc: "2026-12-31T00:00:00Z".to_string() },
            }],
        );
        assert_eq!(outputs.len(), 1);
        assert_eq!(outputs[0].output_type, VcirOutputType::RouterKey);
        assert_eq!(outputs[0].source_object_type, "router_key");
        assert!(outputs[0].payload_json.contains("spki_der_base64"));
    }

    #[test]
    fn build_vcir_local_outputs_falls_back_to_decoding_accepted_objects_when_cache_is_empty() {
        let (pack, issuer_ca_der, validation_time) = cernet_publication_point_snapshot_for_vcir_tests();
        let issuer_ca = ResourceCertificate::decode_der(&issuer_ca_der).expect("decode issuer ca");
        let objects = crate::validation::objects::process_publication_point_snapshot_for_issuer(
            &pack,
            &Policy::default(),
            issuer_ca_der.as_slice(),
            Some("rsync://rpki.apnic.net/repository/B527EF581D6611E2BB468F7C72FD1FF2/BfycW4hQb3wNP4YsiJW-1n6fjro.cer"),
            issuer_ca.tbs.extensions.ip_resources.as_ref(),
            issuer_ca.tbs.extensions.as_resources.as_ref(),
            validation_time,
            None,
        );
        let mut objects_without_cache = objects.clone();
        objects_without_cache.local_outputs_cache.clear();
        let ca = CaInstanceHandle {
            depth: 0,
            tal_id: "test-tal".to_string(),
            parent_manifest_rsync_uri: None,
            ca_certificate_der: issuer_ca_der,
            ca_certificate_rsync_uri: Some(
                "rsync://rpki.apnic.net/repository/B527EF581D6611E2BB468F7C72FD1FF2/BfycW4hQb3wNP4YsiJW-1n6fjro.cer".to_string(),
            ),
            effective_ip_resources: issuer_ca.tbs.extensions.ip_resources.clone(),
            effective_as_resources: issuer_ca.tbs.extensions.as_resources.clone(),
            rsync_base_uri: pack.publication_point_rsync_uri.clone(),
            manifest_rsync_uri: pack.manifest_rsync_uri.clone(),
            publication_point_rsync_uri: pack.publication_point_rsync_uri.clone(),
            rrdp_notification_uri: None,
        };
        let local_outputs = build_vcir_local_outputs(&ca, &pack, &objects_without_cache)
            .expect("rebuild vcir local outputs");
        assert!(!local_outputs.is_empty());
        assert_eq!(local_outputs.len(), objects.vrps.len());
        assert!(local_outputs
            .iter()
            .all(|output| output.output_type == VcirOutputType::Vrp));
    }

    #[test]
    fn persist_vcir_for_fresh_result_stores_vcir_and_audit_indexes_for_real_snapshot() {
        let (pack, issuer_ca_der, validation_time) = cernet_publication_point_snapshot_for_vcir_tests();
        let issuer_ca = ResourceCertificate::decode_der(&issuer_ca_der).expect("decode issuer ca");
        let objects = crate::validation::objects::process_publication_point_snapshot_for_issuer(
            &pack,
            &Policy::default(),
            issuer_ca_der.as_slice(),
            Some("rsync://rpki.apnic.net/repository/B527EF581D6611E2BB468F7C72FD1FF2/BfycW4hQb3wNP4YsiJW-1n6fjro.cer"),
            issuer_ca.tbs.extensions.ip_resources.as_ref(),
            issuer_ca.tbs.extensions.as_resources.as_ref(),
            validation_time,
            None,
        );
        let store_dir = tempfile::tempdir().expect("store dir");
        let store = RocksStore::open(store_dir.path()).expect("open rocksdb");
        let ca = CaInstanceHandle {
            depth: 0,
            tal_id: "test-tal".to_string(),
            parent_manifest_rsync_uri: None,
            ca_certificate_der: issuer_ca_der.clone(),
            ca_certificate_rsync_uri: Some(
                "rsync://rpki.apnic.net/repository/B527EF581D6611E2BB468F7C72FD1FF2/BfycW4hQb3wNP4YsiJW-1n6fjro.cer".to_string(),
            ),
            effective_ip_resources: issuer_ca.tbs.extensions.ip_resources.clone(),
            effective_as_resources: issuer_ca.tbs.extensions.as_resources.clone(),
            rsync_base_uri: pack.publication_point_rsync_uri.clone(),
            manifest_rsync_uri: pack.manifest_rsync_uri.clone(),
            publication_point_rsync_uri: pack.publication_point_rsync_uri.clone(),
            rrdp_notification_uri: None,
        };

        persist_vcir_for_fresh_result(
            &store,
            &ca,
            &pack,
            &objects,
            &[],
            &[],
            &[],
            validation_time,
        )
        .expect("persist vcir for fresh result");

        let vcir = store
            .get_vcir(&pack.manifest_rsync_uri)
            .expect("get vcir")
            .expect("vcir exists");
        assert_eq!(vcir.manifest_rsync_uri, pack.manifest_rsync_uri);
        assert_eq!(vcir.summary.local_vrp_count as usize, objects.vrps.len());
        let first_output = vcir.local_outputs.first().expect("local outputs stored");
        assert!(store
            .get_audit_rule_index_entry(crate::storage::AuditRuleKind::Roa, &first_output.rule_hash)
            .expect("get audit rule index entry")
            .is_some());
    }

    #[test]
    fn build_vcir_related_artifacts_classifies_snapshot_files_and_audit_statuses() {
        let manifest_bytes = std::fs::read(
            std::path::PathBuf::from(env!("CARGO_MANIFEST_DIR")).join(
                "tests/fixtures/repository/rpki.cernet.net/repo/cernet/0/05FC9C5B88506F7C0D3F862C8895BED67E9F8EBA.mft",
            ),
        )
        .expect("read manifest fixture");
        let crl_bytes = std::fs::read(
            std::path::PathBuf::from(env!("CARGO_MANIFEST_DIR")).join(
                "tests/fixtures/repository/rpki.cernet.net/repo/cernet/0/05FC9C5B88506F7C0D3F862C8895BED67E9F8EBA.crl",
            ),
        )
        .expect("read crl fixture");
        let pack = PublicationPointSnapshot {
            format_version: PublicationPointSnapshot::FORMAT_VERSION_V1,
            manifest_rsync_uri: "rsync://example.test/repo/issuer/issuer.mft".to_string(),
            publication_point_rsync_uri: "rsync://example.test/repo/issuer/".to_string(),
            manifest_number_be: vec![1],
            this_update: PackTime::from_utc_offset_datetime(time::OffsetDateTime::now_utc()),
            next_update: PackTime::from_utc_offset_datetime(
                time::OffsetDateTime::now_utc() + time::Duration::hours(1),
            ),
            verified_at: PackTime::from_utc_offset_datetime(time::OffsetDateTime::now_utc()),
            manifest_bytes,
            files: vec![
                PackFile::from_bytes_compute_sha256(
                    "rsync://example.test/repo/issuer/issuer.crl",
                    crl_bytes,
                ),
                PackFile::from_bytes_compute_sha256(
                    "rsync://example.test/repo/issuer/child.cer",
                    vec![1u8, 2],
                ),
                PackFile::from_bytes_compute_sha256(
                    "rsync://example.test/repo/issuer/a.roa",
                    vec![3u8, 4],
                ),
                PackFile::from_bytes_compute_sha256(
                    "rsync://example.test/repo/issuer/a.asa",
                    vec![5u8, 6],
                ),
                PackFile::from_bytes_compute_sha256(
                    "rsync://example.test/repo/issuer/a.gbr",
                    vec![7u8, 8],
                ),
                PackFile::from_bytes_compute_sha256(
                    "rsync://example.test/repo/issuer/extra.bin",
                    vec![9u8],
                ),
            ],
        };
        let ca = CaInstanceHandle {
            depth: 0,
            tal_id: "test-tal".to_string(),
            parent_manifest_rsync_uri: None,
            ca_certificate_der: vec![0x11, 0x22],
            ca_certificate_rsync_uri: Some("rsync://example.test/repo/issuer/issuer.cer".to_string()),
            effective_ip_resources: None,
            effective_as_resources: None,
            rsync_base_uri: pack.publication_point_rsync_uri.clone(),
            manifest_rsync_uri: pack.manifest_rsync_uri.clone(),
            publication_point_rsync_uri: pack.publication_point_rsync_uri.clone(),
            rrdp_notification_uri: None,
        };
        let objects = crate::validation::objects::ObjectsOutput {
            vrps: Vec::new(),
            aspas: Vec::new(),
            router_keys: Vec::new(),
            local_outputs_cache: Vec::new(),
            warnings: Vec::new(),
            stats: crate::validation::objects::ObjectsStats::default(),
            audit: vec![
                ObjectAuditEntry {
                    rsync_uri: "rsync://example.test/repo/issuer/a.roa".to_string(),
                    sha256_hex: sha256_hex_from_32(&pack.files[2].sha256),
                    kind: AuditObjectKind::Roa,
                    result: AuditObjectResult::Error,
                    detail: Some("bad roa".to_string()),
                },
                ObjectAuditEntry {
                    rsync_uri: "rsync://example.test/repo/issuer/a.asa".to_string(),
                    sha256_hex: sha256_hex_from_32(&pack.files[3].sha256),
                    kind: AuditObjectKind::Aspa,
                    result: AuditObjectResult::Skipped,
                    detail: Some("skipped aspa".to_string()),
                },
            ],
        };
        let embedded = vec![VcirEmbeddedEvidence {
            artifact: VcirRelatedArtifact {
                artifact_role: VcirArtifactRole::EeCert,
                artifact_kind: VcirArtifactKind::Cer,
                uri: None,
                sha256: sha256_hex(b"embedded-ee"),
                object_type: Some("cer".to_string()),
                validation_status: VcirArtifactValidationStatus::Accepted,
            },
            raw_entry: embedded_raw_entry(sha256_hex(b"embedded-ee"), vec![1u8, 2, 3]),
        }];
        let artifacts = build_vcir_related_artifacts(
            &ca,
            &pack,
            "rsync://example.test/repo/issuer/issuer.crl",
            &objects,
            &[],
            &embedded,
        );
        assert!(artifacts.iter().any(|artifact| artifact.artifact_role == VcirArtifactRole::Manifest));
        assert!(artifacts.iter().any(|artifact| artifact.artifact_role == VcirArtifactRole::TrustAnchorCert));
        assert!(artifacts.iter().any(|artifact| artifact.uri.as_deref() == Some("rsync://example.test/repo/issuer/issuer.crl")
            && artifact.artifact_role == VcirArtifactRole::CurrentCrl));
        assert!(artifacts.iter().any(|artifact| artifact.uri.as_deref() == Some("rsync://example.test/repo/issuer/child.cer")
            && artifact.artifact_role == VcirArtifactRole::ChildCaCert));
        assert!(artifacts.iter().any(|artifact| artifact.uri.as_deref() == Some("rsync://example.test/repo/issuer/a.roa")
            && artifact.validation_status == VcirArtifactValidationStatus::Rejected));
        assert!(artifacts.iter().any(|artifact| artifact.uri.as_deref() == Some("rsync://example.test/repo/issuer/a.asa")
            && artifact.validation_status == VcirArtifactValidationStatus::WarningOnly));
        assert!(artifacts.iter().any(|artifact| artifact.uri.as_deref() == Some("rsync://example.test/repo/issuer/a.gbr")
            && artifact.artifact_kind == VcirArtifactKind::Gbr));
        assert!(artifacts.iter().any(|artifact| artifact.uri.as_deref() == Some("rsync://example.test/repo/issuer/extra.bin")
            && artifact.artifact_kind == VcirArtifactKind::Other));
        assert!(artifacts.iter().any(|artifact| artifact.uri.is_none() && artifact.sha256 == sha256_hex(b"embedded-ee")));
    }

    #[test]
    fn select_issuer_crl_from_snapshot_reports_missing_crldp_for_self_signed_cert() {
        let ta_der = std::fs::read(
            std::path::PathBuf::from(env!("CARGO_MANIFEST_DIR"))
                .join("tests/fixtures/ta/apnic-ta.cer"),
        )
        .expect("read TA fixture");

        let pack = dummy_pack_with_files(vec![]);
        let err = select_issuer_crl_from_snapshot(&ta_der, &pack).unwrap_err();
        assert!(err.contains("CRLDistributionPoints missing"), "{err}");
    }

    #[test]
    fn select_issuer_crl_from_snapshot_finds_matching_crl() {
        // Use real fixtures to ensure child cert has CRLDP rsync URI and CRL exists.
        let child_cert_der =
            std::fs::read(std::path::PathBuf::from(env!("CARGO_MANIFEST_DIR")).join(
                "tests/fixtures/repository/ca.rg.net/rpki/RGnet-OU/R-lVU1XGsAeqzV1Fv0HjOD6ZFkE.cer",
            ))
            .expect("read child cert fixture");
        let crl_der = std::fs::read(std::path::PathBuf::from(env!("CARGO_MANIFEST_DIR")).join(
            "tests/fixtures/repository/ca.rg.net/rpki/RGnet-OU/bW-_qXU9uNhGQz21NR2ansB8lr0.crl",
        ))
        .expect("read crl fixture");

        let pack = dummy_pack_with_files(vec![PackFile::from_bytes_compute_sha256(
            "rsync://ca.rg.net/rpki/RGnet-OU/bW-_qXU9uNhGQz21NR2ansB8lr0.crl",
            crl_der.clone(),
        )]);

        let (uri, found) =
            select_issuer_crl_from_snapshot(child_cert_der.as_slice(), &pack).expect("find crl");
        assert_eq!(
            uri,
            "rsync://ca.rg.net/rpki/RGnet-OU/bW-_qXU9uNhGQz21NR2ansB8lr0.crl"
        );
        assert_eq!(found, crl_der.as_slice());
    }

    #[test]
    fn discover_children_from_fresh_pack_discovers_child_ca() {
        let g = generate_chain_and_crl();

        let pack = dummy_pack_with_files(vec![
            PackFile::from_bytes_compute_sha256(
                "rsync://example.test/repo/issuer/issuer.crl",
                g.issuer_crl_der.clone(),
            ),
            PackFile::from_bytes_compute_sha256(
                "rsync://example.test/repo/issuer/child.cer",
                g.child_ca_der.clone(),
            ),
        ]);

        let issuer_ca = ResourceCertificate::decode_der(&g.issuer_ca_der).expect("decode issuer");
        let issuer = CaInstanceHandle {
            depth: 0,
            tal_id: "test-tal".to_string(),
            parent_manifest_rsync_uri: None,
            ca_certificate_der: g.issuer_ca_der.clone(),
            ca_certificate_rsync_uri: None,
            effective_ip_resources: issuer_ca.tbs.extensions.ip_resources.clone(),
            effective_as_resources: issuer_ca.tbs.extensions.as_resources.clone(),
            rsync_base_uri: "rsync://example.test/repo/issuer/".to_string(),
            manifest_rsync_uri: "rsync://example.test/repo/issuer/issuer.mft".to_string(),
            publication_point_rsync_uri: "rsync://example.test/repo/issuer/".to_string(),
            rrdp_notification_uri: None,
        };

        let now = time::OffsetDateTime::now_utc();
        let children = discover_children_from_fresh_snapshot_with_audit(&issuer, &pack, now, None)
            .expect("discover children")
            .children;
        assert_eq!(children.len(), 1);
        assert_eq!(
            children[0].discovered_from.parent_manifest_rsync_uri,
            issuer.manifest_rsync_uri
        );
        assert_eq!(
            children[0].discovered_from.child_ca_certificate_rsync_uri,
            "rsync://example.test/repo/issuer/child.cer"
        );
        assert_eq!(
            children[0].handle.rsync_base_uri,
            "rsync://example.test/repo/child/".to_string()
        );
        assert_eq!(
            children[0].handle.manifest_rsync_uri,
            "rsync://example.test/repo/child/child.mft".to_string()
        );
        assert_eq!(
            children[0].handle.publication_point_rsync_uri,
            "rsync://example.test/repo/child/".to_string()
        );
        assert_eq!(
            children[0].handle.rrdp_notification_uri.as_deref(),
            Some("https://example.test/notification.xml")
        );
    }

    #[test]
    fn discover_children_with_audit_records_missing_crl_for_child_certificate() {
        let now = time::OffsetDateTime::now_utc();

        let child_ca_der =
            std::fs::read(std::path::PathBuf::from(env!("CARGO_MANIFEST_DIR")).join(
                "tests/fixtures/repository/ca.rg.net/rpki/RGnet-OU/R-lVU1XGsAeqzV1Fv0HjOD6ZFkE.cer",
            ))
            .expect("read child ca fixture");

        // Pack contains the child CA cert but does not contain the CRL referenced by the child
        // certificate CRLDistributionPoints extension.
        let pack = dummy_pack_with_files(vec![PackFile::from_bytes_compute_sha256(
            "rsync://ca.rg.net/rpki/RGnet-OU/R-lVU1XGsAeqzV1Fv0HjOD6ZFkE.cer",
            child_ca_der,
        )]);

        let issuer = CaInstanceHandle {
            depth: 0,
            tal_id: "test-tal".to_string(),
            parent_manifest_rsync_uri: None,
            ca_certificate_der: vec![1],
            ca_certificate_rsync_uri: None,
            effective_ip_resources: None,
            effective_as_resources: None,
            rsync_base_uri: "rsync://example.test/repo/issuer/".to_string(),
            manifest_rsync_uri: pack.manifest_rsync_uri.clone(),
            publication_point_rsync_uri: pack.publication_point_rsync_uri.clone(),
            rrdp_notification_uri: None,
        };

        let out = discover_children_from_fresh_snapshot_with_audit(&issuer, &pack, now, None)
            .expect("discovery should succeed with audit error");
        assert_eq!(out.children.len(), 0);
        assert_eq!(out.audits.len(), 1);
        assert_eq!(
            out.audits[0].rsync_uri,
            "rsync://ca.rg.net/rpki/RGnet-OU/R-lVU1XGsAeqzV1Fv0HjOD6ZFkE.cer"
        );
        assert_eq!(out.audits[0].result, AuditObjectResult::Error);
        assert!(
            out.audits[0]
                .detail
                .as_deref()
                .unwrap_or("")
                .contains("cannot select issuer CRL"),
            "expected deterministic CRL selection failure to be recorded"
        );
    }

    #[test]
    fn runner_offline_rsync_fixture_produces_pack_and_warnings() {
        let fixture_dir = std::path::PathBuf::from(env!("CARGO_MANIFEST_DIR"))
            .join("tests/fixtures/repository/rpki.cernet.net/repo/cernet/0");
        assert!(fixture_dir.is_dir(), "fixture directory must exist");

        let rsync_base_uri = "rsync://rpki.cernet.net/repo/cernet/0/".to_string();
        let manifest_file = "05FC9C5B88506F7C0D3F862C8895BED67E9F8EBA.mft";
        let manifest_rsync_uri = format!("{rsync_base_uri}{manifest_file}");

        // Pick a validation_time inside the fixture manifest's validity window to keep this
        // test stable across wall-clock time.
        let fixture_manifest_bytes =
            std::fs::read(fixture_dir.join(manifest_file)).expect("read manifest fixture");
        let fixture_manifest =
            crate::data_model::manifest::ManifestObject::decode_der(&fixture_manifest_bytes)
                .expect("decode manifest fixture");
        let validation_time = {
            let this_update = fixture_manifest.manifest.this_update;
            let next_update = fixture_manifest.manifest.next_update;
            let candidate = this_update + time::Duration::seconds(60);
            if candidate < next_update {
                candidate
            } else {
                this_update
            }
        };

        let store_dir = tempfile::tempdir().expect("store dir");
        let store = RocksStore::open(store_dir.path()).expect("open rocksdb");
        let policy = Policy {
            sync_preference: crate::policy::SyncPreference::RsyncOnly,
            ..Policy::default()
        };

        let runner = Rpkiv1PublicationPointRunner {
            store: &store,
            policy: &policy,
            http_fetcher: &NeverHttpFetcher,
            rsync_fetcher: &LocalDirRsyncFetcher::new(&fixture_dir),
            validation_time,
            timing: None,
            download_log: None,
            replay_archive_index: None,
            replay_delta_index: None,
            rrdp_dedup: false,
            rrdp_repo_cache: Mutex::new(HashMap::new()),
            rsync_dedup: false,
            rsync_repo_cache: Mutex::new(HashMap::new()),
        };

        // For this fixture-driven smoke, we provide the correct issuer CA certificate (the CA for
        // this publication point) so ROA EE certificate paths can validate.
        let issuer_ca_der = std::fs::read(
            std::path::PathBuf::from(env!("CARGO_MANIFEST_DIR")).join(
                "tests/fixtures/repository/rpki.apnic.net/repository/B527EF581D6611E2BB468F7C72FD1FF2/BfycW4hQb3wNP4YsiJW-1n6fjro.cer",
            ),
        )
        .expect("read issuer ca fixture");
        let issuer_ca = ResourceCertificate::decode_der(&issuer_ca_der).expect("decode issuer ca");

        let handle = CaInstanceHandle {
            depth: 0,
            tal_id: "test-tal".to_string(),
            parent_manifest_rsync_uri: None,
            ca_certificate_der: issuer_ca_der,
            ca_certificate_rsync_uri: Some("rsync://rpki.apnic.net/repository/B527EF581D6611E2BB468F7C72FD1FF2/BfycW4hQb3wNP4YsiJW-1n6fjro.cer".to_string()),
            effective_ip_resources: issuer_ca.tbs.extensions.ip_resources.clone(),
            effective_as_resources: issuer_ca.tbs.extensions.as_resources.clone(),
            rsync_base_uri: rsync_base_uri.clone(),
            manifest_rsync_uri: manifest_rsync_uri.clone(),
            publication_point_rsync_uri: rsync_base_uri.clone(),
            rrdp_notification_uri: None,
        };

        let out = runner
            .run_publication_point(&handle)
            .expect("run publication point");
        assert_eq!(out.source, PublicationPointSource::Fresh);
        let pack = out.snapshot.expect("fresh run pack");
        assert_eq!(pack.manifest_rsync_uri, manifest_rsync_uri);
        assert!(pack.files.len() > 1);
        assert!(
            out.objects.vrps.len() > 1,
            "expected to extract VRPs from ROAs"
        );

        let vcir = store
            .get_vcir(&manifest_rsync_uri)
            .expect("get vcir")
            .expect("vcir exists after fresh run");
        assert_eq!(vcir.manifest_rsync_uri, manifest_rsync_uri);
        assert_eq!(vcir.tal_id, "test-tal");
        assert!(
            vcir.local_outputs
                .iter()
                .any(|output| output.output_type == crate::storage::VcirOutputType::Vrp),
            "expected VCIR local_outputs to contain VRP entries"
        );
        let first_vrp = vcir
            .local_outputs
            .iter()
            .find(|output| output.output_type == crate::storage::VcirOutputType::Vrp)
            .expect("first VCIR VRP output");
        let audit_rule = store
            .get_audit_rule_index_entry(crate::storage::AuditRuleKind::Roa, &first_vrp.rule_hash)
            .expect("get audit rule index")
            .expect("audit rule index exists");
        assert_eq!(audit_rule.manifest_rsync_uri, manifest_rsync_uri);
        assert_eq!(audit_rule.output_id, first_vrp.output_id);
        assert!(vcir.related_artifacts.iter().any(|artifact| {
            artifact.artifact_role == VcirArtifactRole::EeCert
                && artifact.artifact_kind == VcirArtifactKind::Cer
        }));
        let ee_entry = store
            .get_raw_by_hash_entry(&first_vrp.source_ee_cert_hash)
            .expect("get source ee raw")
            .expect("source ee raw exists");
        assert_eq!(ee_entry.object_type.as_deref(), Some("cer"));
        assert_eq!(ee_entry.encoding.as_deref(), Some("der"));

        let trace = crate::audit_trace::trace_rule_to_root(
            &store,
            crate::storage::AuditRuleKind::Roa,
            &first_vrp.rule_hash,
        )
        .expect("trace rule")
        .expect("trace exists");
        assert_eq!(trace.chain_leaf_to_root.len(), 1);
        assert_eq!(
            trace.chain_leaf_to_root[0].manifest_rsync_uri,
            manifest_rsync_uri
        );
        assert!(trace.source_object_raw.raw_present);
        assert!(trace.source_ee_cert_raw.raw_present);
    }

    #[test]
    fn runner_rsync_dedup_skips_second_sync_for_same_base() {
        let fixture_dir = std::path::PathBuf::from(env!("CARGO_MANIFEST_DIR"))
            .join("tests/fixtures/repository/rpki.cernet.net/repo/cernet/0");
        assert!(fixture_dir.is_dir(), "fixture directory must exist");

        let rsync_base_uri = "rsync://rpki.cernet.net/repo/cernet/0/".to_string();
        let manifest_file = "05FC9C5B88506F7C0D3F862C8895BED67E9F8EBA.mft";
        let manifest_rsync_uri = format!("{rsync_base_uri}{manifest_file}");

        let fixture_manifest_bytes =
            std::fs::read(fixture_dir.join(manifest_file)).expect("read manifest fixture");
        let fixture_manifest =
            crate::data_model::manifest::ManifestObject::decode_der(&fixture_manifest_bytes)
                .expect("decode manifest fixture");
        let validation_time = fixture_manifest.manifest.this_update + time::Duration::seconds(60);

        let store_dir = tempfile::tempdir().expect("store dir");
        let store = RocksStore::open(store_dir.path()).expect("open rocksdb");
        let policy = Policy {
            sync_preference: crate::policy::SyncPreference::RsyncOnly,
            ..Policy::default()
        };

        let issuer_ca_der = std::fs::read(
            std::path::PathBuf::from(env!("CARGO_MANIFEST_DIR")).join(
                "tests/fixtures/repository/rpki.apnic.net/repository/B527EF581D6611E2BB468F7C72FD1FF2/BfycW4hQb3wNP4YsiJW-1n6fjro.cer",
            ),
        )
        .expect("read issuer ca fixture");
        let issuer_ca = ResourceCertificate::decode_der(&issuer_ca_der).expect("decode issuer ca");

        let handle = CaInstanceHandle {
            depth: 0,
            tal_id: "test-tal".to_string(),
            parent_manifest_rsync_uri: None,
            ca_certificate_der: issuer_ca_der,
            ca_certificate_rsync_uri: Some("rsync://rpki.apnic.net/repository/B527EF581D6611E2BB468F7C72FD1FF2/BfycW4hQb3wNP4YsiJW-1n6fjro.cer".to_string()),
            effective_ip_resources: issuer_ca.tbs.extensions.ip_resources.clone(),
            effective_as_resources: issuer_ca.tbs.extensions.as_resources.clone(),
            rsync_base_uri: rsync_base_uri.clone(),
            manifest_rsync_uri: manifest_rsync_uri.clone(),
            publication_point_rsync_uri: rsync_base_uri.clone(),
            rrdp_notification_uri: None,
        };

        struct CountingRsyncFetcher {
            inner: LocalDirRsyncFetcher,
            calls: Arc<AtomicUsize>,
        }
        impl RsyncFetcher for CountingRsyncFetcher {
            fn fetch_objects(
                &self,
                rsync_base_uri: &str,
            ) -> Result<Vec<(String, Vec<u8>)>, RsyncFetchError> {
                self.calls.fetch_add(1, Ordering::SeqCst);
                self.inner.fetch_objects(rsync_base_uri)
            }
        }

        let calls = Arc::new(AtomicUsize::new(0));
        let rsync = CountingRsyncFetcher {
            inner: LocalDirRsyncFetcher::new(&fixture_dir),
            calls: calls.clone(),
        };

        let runner = Rpkiv1PublicationPointRunner {
            store: &store,
            policy: &policy,
            http_fetcher: &NeverHttpFetcher,
            rsync_fetcher: &rsync,
            validation_time,
            timing: None,
            download_log: None,
            replay_archive_index: None,
            replay_delta_index: None,
            rrdp_dedup: false,
            rrdp_repo_cache: Mutex::new(HashMap::new()),
            rsync_dedup: true,
            rsync_repo_cache: Mutex::new(HashMap::new()),
        };

        let first = runner.run_publication_point(&handle).expect("first run ok");
        assert_eq!(first.source, PublicationPointSource::Fresh);

        let second = runner
            .run_publication_point(&handle)
            .expect("second run ok");
        assert_eq!(second.source, PublicationPointSource::Fresh);

        assert_eq!(
            calls.load(Ordering::SeqCst),
            1,
            "rsync should be called once"
        );
    }

    #[test]
    fn runner_rsync_dedup_skips_second_sync_for_same_module_scope() {
        let fixture_dir = std::path::PathBuf::from(env!("CARGO_MANIFEST_DIR"))
            .join("tests/fixtures/repository/rpki.cernet.net/repo/cernet/0");
        assert!(fixture_dir.is_dir(), "fixture directory must exist");

        let first_base_uri = "rsync://rpki.cernet.net/repo/cernet/0/".to_string();
        let second_base_uri = "rsync://rpki.cernet.net/repo/cernet/0/sub/".to_string();
        let manifest_file = "05FC9C5B88506F7C0D3F862C8895BED67E9F8EBA.mft";
        let manifest_rsync_uri = format!("{first_base_uri}{manifest_file}");

        let fixture_manifest_bytes =
            std::fs::read(fixture_dir.join(manifest_file)).expect("read manifest fixture");
        let fixture_manifest =
            crate::data_model::manifest::ManifestObject::decode_der(&fixture_manifest_bytes)
                .expect("decode manifest fixture");
        let validation_time = fixture_manifest.manifest.this_update + time::Duration::seconds(60);

        let store_dir = tempfile::tempdir().expect("store dir");
        let store = RocksStore::open(store_dir.path()).expect("open rocksdb");
        let policy = Policy {
            sync_preference: crate::policy::SyncPreference::RsyncOnly,
            ..Policy::default()
        };

        let issuer_ca_der = std::fs::read(
            std::path::PathBuf::from(env!("CARGO_MANIFEST_DIR")).join(
                "tests/fixtures/repository/rpki.apnic.net/repository/B527EF581D6611E2BB468F7C72FD1FF2/BfycW4hQb3wNP4YsiJW-1n6fjro.cer",
            ),
        )
        .expect("read issuer ca fixture");
        let issuer_ca = ResourceCertificate::decode_der(&issuer_ca_der).expect("decode issuer ca");

        let handle = CaInstanceHandle {
            depth: 0,
            tal_id: "test-tal".to_string(),
            parent_manifest_rsync_uri: None,
            ca_certificate_der: issuer_ca_der,
            ca_certificate_rsync_uri: Some("rsync://rpki.apnic.net/repository/B527EF581D6611E2BB468F7C72FD1FF2/BfycW4hQb3wNP4YsiJW-1n6fjro.cer".to_string()),
            effective_ip_resources: issuer_ca.tbs.extensions.ip_resources.clone(),
            effective_as_resources: issuer_ca.tbs.extensions.as_resources.clone(),
            rsync_base_uri: first_base_uri.clone(),
            manifest_rsync_uri: manifest_rsync_uri.clone(),
            publication_point_rsync_uri: first_base_uri.clone(),
            rrdp_notification_uri: None,
        };
        let second_handle = CaInstanceHandle {
            rsync_base_uri: second_base_uri.clone(),
            publication_point_rsync_uri: second_base_uri.clone(),
            ..handle.clone()
        };

        struct ModuleScopeRsyncFetcher {
            inner: LocalDirRsyncFetcher,
            calls: Arc<AtomicUsize>,
        }
        impl RsyncFetcher for ModuleScopeRsyncFetcher {
            fn fetch_objects(
                &self,
                rsync_base_uri: &str,
            ) -> Result<Vec<(String, Vec<u8>)>, RsyncFetchError> {
                self.calls.fetch_add(1, Ordering::SeqCst);
                self.inner.fetch_objects(rsync_base_uri)
            }

            fn dedup_key(&self, _rsync_base_uri: &str) -> String {
                "rsync://rpki.cernet.net/repo/".to_string()
            }
        }

        let calls = Arc::new(AtomicUsize::new(0));
        let rsync = ModuleScopeRsyncFetcher {
            inner: LocalDirRsyncFetcher::new(&fixture_dir),
            calls: calls.clone(),
        };

        let runner = Rpkiv1PublicationPointRunner {
            store: &store,
            policy: &policy,
            http_fetcher: &NeverHttpFetcher,
            rsync_fetcher: &rsync,
            validation_time,
            timing: None,
            download_log: None,
            replay_archive_index: None,
            replay_delta_index: None,
            rrdp_dedup: false,
            rrdp_repo_cache: Mutex::new(HashMap::new()),
            rsync_dedup: true,
            rsync_repo_cache: Mutex::new(HashMap::new()),
        };

        let first = runner.run_publication_point(&handle).expect("first run ok");
        assert_eq!(first.source, PublicationPointSource::Fresh);

        let second = runner
            .run_publication_point(&second_handle)
            .expect("second run ok");
        assert!(matches!(
            second.source,
            PublicationPointSource::Fresh | PublicationPointSource::VcirCurrentInstance
        ));

        assert_eq!(calls.load(Ordering::SeqCst), 1, "module-scope dedup should skip second sync");
    }

    #[test]
    fn runner_rsync_dedup_works_in_rsync_only_mode_even_when_rrdp_notify_exists() {
        let fixture_dir = std::path::PathBuf::from(env!("CARGO_MANIFEST_DIR"))
            .join("tests/fixtures/repository/rpki.cernet.net/repo/cernet/0");
        assert!(fixture_dir.is_dir(), "fixture directory must exist");

        let first_base_uri = "rsync://rpki.cernet.net/repo/cernet/0/".to_string();
        let second_base_uri = "rsync://rpki.cernet.net/repo/cernet/0/sub/".to_string();
        let manifest_file = "05FC9C5B88506F7C0D3F862C8895BED67E9F8EBA.mft";
        let manifest_rsync_uri = format!("{first_base_uri}{manifest_file}");

        let fixture_manifest_bytes =
            std::fs::read(fixture_dir.join(manifest_file)).expect("read manifest fixture");
        let fixture_manifest =
            crate::data_model::manifest::ManifestObject::decode_der(&fixture_manifest_bytes)
                .expect("decode manifest fixture");
        let validation_time = fixture_manifest.manifest.this_update + time::Duration::seconds(60);

        let store_dir = tempfile::tempdir().expect("store dir");
        let store = RocksStore::open(store_dir.path()).expect("open rocksdb");
        let policy = Policy {
            sync_preference: crate::policy::SyncPreference::RsyncOnly,
            ..Policy::default()
        };

        let issuer_ca_der = std::fs::read(
            std::path::PathBuf::from(env!("CARGO_MANIFEST_DIR")).join(
                "tests/fixtures/repository/rpki.apnic.net/repository/B527EF581D6611E2BB468F7C72FD1FF2/BfycW4hQb3wNP4YsiJW-1n6fjro.cer",
            ),
        )
        .expect("read issuer ca fixture");
        let issuer_ca = ResourceCertificate::decode_der(&issuer_ca_der).expect("decode issuer ca");

        let handle = CaInstanceHandle {
            depth: 0,
            tal_id: "test-tal".to_string(),
            parent_manifest_rsync_uri: None,
            ca_certificate_der: issuer_ca_der,
            ca_certificate_rsync_uri: Some("rsync://rpki.apnic.net/repository/B527EF581D6611E2BB468F7C72FD1FF2/BfycW4hQb3wNP4YsiJW-1n6fjro.cer".to_string()),
            effective_ip_resources: issuer_ca.tbs.extensions.ip_resources.clone(),
            effective_as_resources: issuer_ca.tbs.extensions.as_resources.clone(),
            rsync_base_uri: first_base_uri.clone(),
            manifest_rsync_uri: manifest_rsync_uri.clone(),
            publication_point_rsync_uri: first_base_uri.clone(),
            rrdp_notification_uri: Some("https://rrdp.example.test/notification.xml".to_string()),
        };
        let second_handle = CaInstanceHandle {
            rsync_base_uri: second_base_uri.clone(),
            publication_point_rsync_uri: second_base_uri.clone(),
            ..handle.clone()
        };

        struct ModuleScopeRsyncFetcher {
            inner: LocalDirRsyncFetcher,
            calls: Arc<AtomicUsize>,
        }
        impl RsyncFetcher for ModuleScopeRsyncFetcher {
            fn fetch_objects(
                &self,
                rsync_base_uri: &str,
            ) -> Result<Vec<(String, Vec<u8>)>, RsyncFetchError> {
                self.calls.fetch_add(1, Ordering::SeqCst);
                self.inner.fetch_objects(rsync_base_uri)
            }

            fn dedup_key(&self, _rsync_base_uri: &str) -> String {
                "rsync://rpki.cernet.net/repo/".to_string()
            }
        }

        let calls = Arc::new(AtomicUsize::new(0));
        let rsync = ModuleScopeRsyncFetcher {
            inner: LocalDirRsyncFetcher::new(&fixture_dir),
            calls: calls.clone(),
        };

        let runner = Rpkiv1PublicationPointRunner {
            store: &store,
            policy: &policy,
            http_fetcher: &NeverHttpFetcher,
            rsync_fetcher: &rsync,
            validation_time,
            timing: None,
            download_log: None,
            replay_archive_index: None,
            replay_delta_index: None,
            rrdp_dedup: true,
            rrdp_repo_cache: Mutex::new(HashMap::new()),
            rsync_dedup: true,
            rsync_repo_cache: Mutex::new(HashMap::new()),
        };

        let first = runner.run_publication_point(&handle).expect("first run ok");
        assert_eq!(first.source, PublicationPointSource::Fresh);

        let second = runner
            .run_publication_point(&second_handle)
            .expect("second run ok");
        assert!(matches!(
            second.source,
            PublicationPointSource::Fresh | PublicationPointSource::VcirCurrentInstance
        ));

        assert_eq!(
            calls.load(Ordering::SeqCst),
            1,
            "rsync-only mode must deduplicate by rsync scope even when RRDP notification is present"
        );
    }

    #[test]
    fn runner_when_repo_sync_fails_uses_current_instance_vcir_and_keeps_children_empty_for_fixture()
    {
        let fixture_dir = std::path::PathBuf::from(env!("CARGO_MANIFEST_DIR"))
            .join("tests/fixtures/repository/rpki.cernet.net/repo/cernet/0");
        assert!(fixture_dir.is_dir(), "fixture directory must exist");

        let rsync_base_uri = "rsync://rpki.cernet.net/repo/cernet/0/".to_string();
        let manifest_file = "05FC9C5B88506F7C0D3F862C8895BED67E9F8EBA.mft";
        let manifest_rsync_uri = format!("{rsync_base_uri}{manifest_file}");

        let fixture_manifest_bytes =
            std::fs::read(fixture_dir.join(manifest_file)).expect("read manifest fixture");
        let fixture_manifest =
            crate::data_model::manifest::ManifestObject::decode_der(&fixture_manifest_bytes)
                .expect("decode manifest fixture");
        let validation_time = fixture_manifest.manifest.this_update + time::Duration::seconds(60);

        let store_dir = tempfile::tempdir().expect("store dir");
        let store = RocksStore::open(store_dir.path()).expect("open rocksdb");
        let policy = Policy {
            sync_preference: crate::policy::SyncPreference::RsyncOnly,
            ..Policy::default()
        };

        let issuer_ca_der = std::fs::read(
            std::path::PathBuf::from(env!("CARGO_MANIFEST_DIR")).join(
                "tests/fixtures/repository/rpki.apnic.net/repository/B527EF581D6611E2BB468F7C72FD1FF2/BfycW4hQb3wNP4YsiJW-1n6fjro.cer",
            ),
        )
        .expect("read issuer ca fixture");
        let issuer_ca = ResourceCertificate::decode_der(&issuer_ca_der).expect("decode issuer ca");

        let handle = CaInstanceHandle {
            depth: 0,
            tal_id: "test-tal".to_string(),
            parent_manifest_rsync_uri: None,
            ca_certificate_der: issuer_ca_der,
            ca_certificate_rsync_uri: Some("rsync://rpki.apnic.net/repository/B527EF581D6611E2BB468F7C72FD1FF2/BfycW4hQb3wNP4YsiJW-1n6fjro.cer".to_string()),
            effective_ip_resources: issuer_ca.tbs.extensions.ip_resources.clone(),
            effective_as_resources: issuer_ca.tbs.extensions.as_resources.clone(),
            rsync_base_uri: rsync_base_uri.clone(),
            manifest_rsync_uri: manifest_rsync_uri.clone(),
            publication_point_rsync_uri: rsync_base_uri.clone(),
            rrdp_notification_uri: None,
        };

        // First: successful fresh run to populate the latest VCIR baseline.
        let ok_runner = Rpkiv1PublicationPointRunner {
            store: &store,
            policy: &policy,
            http_fetcher: &NeverHttpFetcher,
            rsync_fetcher: &LocalDirRsyncFetcher::new(&fixture_dir),
            validation_time,
            timing: None,
            download_log: None,
            replay_archive_index: None,
            replay_delta_index: None,
            rrdp_dedup: false,
            rrdp_repo_cache: Mutex::new(HashMap::new()),
            rsync_dedup: false,
            rsync_repo_cache: Mutex::new(HashMap::new()),
        };
        let first = ok_runner
            .run_publication_point(&handle)
            .expect("first run ok");
        assert_eq!(first.source, PublicationPointSource::Fresh);
        assert!(
            first.discovered_children.is_empty(),
            "fixture has no child .cer"
        );

        // Second: repo sync fails, but we can still reuse current-instance VCIR.
        let bad_runner = Rpkiv1PublicationPointRunner {
            store: &store,
            policy: &policy,
            http_fetcher: &NeverHttpFetcher,
            rsync_fetcher: &FailingRsyncFetcher,
            validation_time,
            timing: None,
            download_log: None,
            replay_archive_index: None,
            replay_delta_index: None,
            rrdp_dedup: false,
            rrdp_repo_cache: Mutex::new(HashMap::new()),
            rsync_dedup: false,
            rsync_repo_cache: Mutex::new(HashMap::new()),
        };
        let second = bad_runner
            .run_publication_point(&handle)
            .expect("should reuse current-instance VCIR");
        assert_eq!(second.source, PublicationPointSource::VcirCurrentInstance);
        assert!(second.discovered_children.is_empty());
        assert!(
            second
                .warnings
                .iter()
                .any(|w| w.message.contains("repo sync failed")),
            "expected warning about repo sync failure"
        );
    }

    #[test]
    fn build_publication_point_audit_emits_no_audit_entry_for_duplicate_pack_uri() {
        let pack = dummy_pack_with_files(vec![
            PackFile::from_bytes_compute_sha256("rsync://example.test/repo/dup.roa", vec![1u8]),
            PackFile::from_bytes_compute_sha256("rsync://example.test/repo/dup.roa", vec![2u8]),
        ]);
        let pp = crate::validation::manifest::PublicationPointResult {
            source: crate::validation::manifest::PublicationPointSource::VcirCurrentInstance,
            snapshot: pack.clone(),
            warnings: Vec::new(),
        };
        let ca = CaInstanceHandle {
            depth: 0,
            tal_id: "test-tal".to_string(),
            parent_manifest_rsync_uri: None,
            ca_certificate_der: vec![1],
            ca_certificate_rsync_uri: None,
            effective_ip_resources: None,
            effective_as_resources: None,
            rsync_base_uri: pack.publication_point_rsync_uri.clone(),
            manifest_rsync_uri: pack.manifest_rsync_uri.clone(),
            publication_point_rsync_uri: pack.publication_point_rsync_uri.clone(),
            rrdp_notification_uri: None,
        };
        let objects = crate::validation::objects::ObjectsOutput {
            vrps: Vec::new(),
            aspas: Vec::new(),
            router_keys: Vec::new(),
            local_outputs_cache: Vec::new(),
            warnings: Vec::new(),
            stats: crate::validation::objects::ObjectsStats::default(),
            audit: Vec::new(),
        };

        let audit = build_publication_point_audit_from_snapshot(
            &ca,
            pp.source,
            None,
            None,
            None,
            &pp.snapshot,
            &[],
            &objects,
            &[],
        );
        assert_eq!(audit.source, "vcir_current_instance");
        assert!(
            audit
                .objects
                .iter()
                .any(|e| e.detail.as_deref() == Some("skipped: no audit entry")),
            "expected a duplicate key to produce a 'no audit entry' placeholder"
        );
    }

    #[test]
    fn build_publication_point_audit_marks_invalid_crl_as_error_and_overlays_roa_audit() {
        let now = time::OffsetDateTime::now_utc();
        let pack = dummy_pack_with_files(vec![
            PackFile::from_bytes_compute_sha256(
                "rsync://example.test/repo/issuer/bad.crl",
                vec![0u8],
            ),
            PackFile::from_bytes_compute_sha256(
                "rsync://example.test/repo/issuer/x.roa",
                vec![1u8],
            ),
        ]);

        let pp = crate::validation::manifest::PublicationPointResult {
            source: crate::validation::manifest::PublicationPointSource::Fresh,
            snapshot: pack.clone(),
            warnings: Vec::new(),
        };

        let issuer = CaInstanceHandle {
            depth: 0,
            tal_id: "test-tal".to_string(),
            parent_manifest_rsync_uri: None,
            ca_certificate_der: vec![1],
            ca_certificate_rsync_uri: None,
            effective_ip_resources: None,
            effective_as_resources: None,
            rsync_base_uri: "rsync://example.test/repo/issuer/".to_string(),
            manifest_rsync_uri: pack.manifest_rsync_uri.clone(),
            publication_point_rsync_uri: pack.publication_point_rsync_uri.clone(),
            rrdp_notification_uri: None,
        };

        let objects = crate::validation::objects::ObjectsOutput {
            vrps: Vec::new(),
            aspas: Vec::new(),
            router_keys: Vec::new(),
            local_outputs_cache: Vec::new(),
            warnings: Vec::new(),
            stats: crate::validation::objects::ObjectsStats::default(),
            audit: vec![ObjectAuditEntry {
                rsync_uri: "rsync://example.test/repo/issuer/x.roa".to_string(),
                sha256_hex: sha256_hex_from_32(&pack.files[1].sha256),
                kind: AuditObjectKind::Roa,
                result: AuditObjectResult::Ok,
                detail: None,
            }],
        };

        let audit = build_publication_point_audit_from_snapshot(
            &issuer,
            pp.source,
            None,
            None,
            None,
            &pp.snapshot,
            &[],
            &objects,
            &[],
        );
        assert_eq!(audit.objects[0].kind, AuditObjectKind::Manifest);

        let crl = audit
            .objects
            .iter()
            .find(|e| e.rsync_uri.ends_with("bad.crl"))
            .expect("crl entry");
        assert!(matches!(crl.result, AuditObjectResult::Error));

        let roa = audit
            .objects
            .iter()
            .find(|e| e.rsync_uri.ends_with("x.roa"))
            .expect("roa entry");
        assert!(matches!(roa.result, AuditObjectResult::Ok));

        // Smoke that time fields are populated from pack.
        assert!(audit.verified_at_rfc3339_utc.contains('T'));
        assert!(audit.this_update_rfc3339_utc.contains('T'));
        assert!(audit.next_update_rfc3339_utc.contains('T'));
        let _ = now;
    }


    #[test]
    fn discover_children_with_router_certificate_records_ok_audit_and_no_child() {
        let g = generate_router_cert_with_variant("ec-p256", true);
        let pack = dummy_pack_with_files(vec![
            PackFile::from_bytes_compute_sha256(
                "rsync://example.test/repo/issuer/issuer.crl",
                g.issuer_crl_der.clone(),
            ),
            PackFile::from_bytes_compute_sha256(
                "rsync://example.test/repo/issuer/router.cer",
                g.router_der.clone(),
            ),
        ]);

        let issuer_ca = ResourceCertificate::decode_der(&g.issuer_ca_der).expect("decode issuer");
        let issuer = CaInstanceHandle {
            depth: 0,
            tal_id: "test-tal".to_string(),
            parent_manifest_rsync_uri: None,
            ca_certificate_der: g.issuer_ca_der.clone(),
            ca_certificate_rsync_uri: Some("rsync://example.test/repo/issuer/issuer.cer".to_string()),
            effective_ip_resources: issuer_ca.tbs.extensions.ip_resources.clone(),
            effective_as_resources: issuer_ca.tbs.extensions.as_resources.clone(),
            rsync_base_uri: "rsync://example.test/repo/issuer/".to_string(),
            manifest_rsync_uri: "rsync://example.test/repo/issuer/issuer.mft".to_string(),
            publication_point_rsync_uri: "rsync://example.test/repo/issuer/".to_string(),
            rrdp_notification_uri: None,
        };

        let out = discover_children_from_fresh_snapshot_with_audit(
            &issuer,
            &pack,
            time::OffsetDateTime::now_utc(),
            None,
        )
        .expect("discover router cert");
        assert!(out.children.is_empty());
        assert_eq!(out.audits.len(), 1);
        assert!(matches!(out.audits[0].result, AuditObjectResult::Ok));
        assert!(out.audits[0]
            .detail
            .as_deref()
            .unwrap_or("")
            .contains("validated BGPsec router certificate"));
    }

    #[test]
    fn discover_children_with_non_router_ee_certificate_records_skipped_audit() {
        let g = generate_router_cert_with_variant("ec-p256", false);
        let pack = dummy_pack_with_files(vec![
            PackFile::from_bytes_compute_sha256(
                "rsync://example.test/repo/issuer/issuer.crl",
                g.issuer_crl_der.clone(),
            ),
            PackFile::from_bytes_compute_sha256(
                "rsync://example.test/repo/issuer/router-no-eku.cer",
                g.router_der.clone(),
            ),
        ]);

        let issuer_ca = ResourceCertificate::decode_der(&g.issuer_ca_der).expect("decode issuer");
        let issuer = CaInstanceHandle {
            depth: 0,
            tal_id: "test-tal".to_string(),
            parent_manifest_rsync_uri: None,
            ca_certificate_der: g.issuer_ca_der.clone(),
            ca_certificate_rsync_uri: Some("rsync://example.test/repo/issuer/issuer.cer".to_string()),
            effective_ip_resources: issuer_ca.tbs.extensions.ip_resources.clone(),
            effective_as_resources: issuer_ca.tbs.extensions.as_resources.clone(),
            rsync_base_uri: "rsync://example.test/repo/issuer/".to_string(),
            manifest_rsync_uri: "rsync://example.test/repo/issuer/issuer.mft".to_string(),
            publication_point_rsync_uri: "rsync://example.test/repo/issuer/".to_string(),
            rrdp_notification_uri: None,
        };

        let out = discover_children_from_fresh_snapshot_with_audit(
            &issuer,
            &pack,
            time::OffsetDateTime::now_utc(),
            None,
        )
        .expect("discover non-router cert");
        assert!(out.children.is_empty());
        assert_eq!(out.audits.len(), 1);
        assert!(matches!(out.audits[0].result, AuditObjectResult::Skipped));
        assert!(out.audits[0]
            .detail
            .as_deref()
            .unwrap_or("")
            .contains("not a CA resource certificate or BGPsec router certificate"));
    }

    #[test]
    fn discover_children_with_invalid_router_certificate_records_error_audit() {
        let g = generate_router_cert_with_variant("ec-p384", true);
        let pack = dummy_pack_with_files(vec![
            PackFile::from_bytes_compute_sha256(
                "rsync://example.test/repo/issuer/issuer.crl",
                g.issuer_crl_der.clone(),
            ),
            PackFile::from_bytes_compute_sha256(
                "rsync://example.test/repo/issuer/router-invalid.cer",
                g.router_der.clone(),
            ),
        ]);

        let issuer_ca = ResourceCertificate::decode_der(&g.issuer_ca_der).expect("decode issuer");
        let issuer = CaInstanceHandle {
            depth: 0,
            tal_id: "test-tal".to_string(),
            parent_manifest_rsync_uri: None,
            ca_certificate_der: g.issuer_ca_der.clone(),
            ca_certificate_rsync_uri: Some("rsync://example.test/repo/issuer/issuer.cer".to_string()),
            effective_ip_resources: issuer_ca.tbs.extensions.ip_resources.clone(),
            effective_as_resources: issuer_ca.tbs.extensions.as_resources.clone(),
            rsync_base_uri: "rsync://example.test/repo/issuer/".to_string(),
            manifest_rsync_uri: "rsync://example.test/repo/issuer/issuer.mft".to_string(),
            publication_point_rsync_uri: "rsync://example.test/repo/issuer/".to_string(),
            rrdp_notification_uri: None,
        };

        let out = discover_children_from_fresh_snapshot_with_audit(
            &issuer,
            &pack,
            time::OffsetDateTime::now_utc(),
            None,
        )
        .expect("discover invalid router cert");
        assert!(out.children.is_empty());
        assert_eq!(out.audits.len(), 1);
        assert!(matches!(out.audits[0].result, AuditObjectResult::Error));
        assert!(out.audits[0]
            .detail
            .as_deref()
            .unwrap_or("")
            .contains("router certificate validation failed"));
    }

    #[test]
    fn discover_children_with_audit_records_decode_error_for_corrupt_cer() {
        let g = generate_chain_and_crl();

        let pack = dummy_pack_with_files(vec![
            PackFile::from_bytes_compute_sha256(
                "rsync://example.test/repo/issuer/issuer.crl",
                g.issuer_crl_der.clone(),
            ),
            PackFile::from_bytes_compute_sha256(
                "rsync://example.test/repo/issuer/corrupt.cer",
                vec![0u8],
            ),
        ]);

        let issuer_ca = ResourceCertificate::decode_der(&g.issuer_ca_der).expect("decode issuer");
        let issuer = CaInstanceHandle {
            depth: 0,
            tal_id: "test-tal".to_string(),
            parent_manifest_rsync_uri: None,
            ca_certificate_der: g.issuer_ca_der.clone(),
            ca_certificate_rsync_uri: None,
            effective_ip_resources: issuer_ca.tbs.extensions.ip_resources.clone(),
            effective_as_resources: issuer_ca.tbs.extensions.as_resources.clone(),
            rsync_base_uri: "rsync://example.test/repo/issuer/".to_string(),
            manifest_rsync_uri: "rsync://example.test/repo/issuer/issuer.mft".to_string(),
            publication_point_rsync_uri: "rsync://example.test/repo/issuer/".to_string(),
            rrdp_notification_uri: None,
        };

        let now = time::OffsetDateTime::now_utc();
        let out = discover_children_from_fresh_snapshot_with_audit(&issuer, &pack, now, None)
            .expect("discover children");
        assert!(out.children.is_empty());
        assert_eq!(out.audits.len(), 1);
        assert!(matches!(out.audits[0].result, AuditObjectResult::Error));
    }

    #[test]
    fn select_issuer_crl_uri_for_child_covers_missing_and_not_found_paths() {
        let g = generate_chain_and_crl();
        let child = ResourceCertificate::decode_der(&g.child_ca_der).expect("decode child cert");

        let empty: std::collections::HashMap<String, CachedIssuerCrl> =
            std::collections::HashMap::new();
        let err = select_issuer_crl_uri_for_child(&child, &empty).unwrap_err();
        assert!(err.contains("no CRL available"), "{err}");

        let ta_der = std::fs::read(
            std::path::PathBuf::from(env!("CARGO_MANIFEST_DIR"))
                .join("tests/fixtures/ta/apnic-ta.cer"),
        )
        .expect("read TA fixture");
        let ta = ResourceCertificate::decode_der(&ta_der).expect("decode TA fixture");
        let mut cache = std::collections::HashMap::new();
        cache.insert(
            "rsync://example.test/repo/issuer/issuer.crl".to_string(),
            CachedIssuerCrl::Pending(g.issuer_crl_der.clone()),
        );
        let err = select_issuer_crl_uri_for_child(&ta, &cache).unwrap_err();
        assert!(err.contains("CRLDistributionPoints missing"), "{err}");

        let mut wrong = std::collections::HashMap::new();
        wrong.insert(
            "rsync://example.test/repo/issuer/other.crl".to_string(),
            CachedIssuerCrl::Pending(g.issuer_crl_der),
        );
        let err = select_issuer_crl_uri_for_child(&child, &wrong).unwrap_err();
        assert!(
            err.contains("not found in publication point snapshot"),
            "{err}"
        );
    }

    #[test]
    fn ensure_issuer_crl_verified_promotes_pending_cache_entry() {
        let g = generate_chain_and_crl();
        let mut cache = std::collections::HashMap::new();
        let crl_uri = "rsync://example.test/repo/issuer/issuer.crl".to_string();
        cache.insert(
            crl_uri.clone(),
            CachedIssuerCrl::Pending(g.issuer_crl_der.clone()),
        );

        let first = ensure_issuer_crl_verified(&crl_uri, &mut cache, &g.issuer_ca_der)
            .expect("verify pending CRL");
        assert!(first.revoked_serials.is_empty());
        assert!(matches!(cache.get(&crl_uri), Some(CachedIssuerCrl::Ok(_))));

        let second = ensure_issuer_crl_verified(&crl_uri, &mut cache, &g.issuer_ca_der)
            .expect("reuse verified CRL");
        assert!(second.revoked_serials.is_empty());
    }

    #[test]
    fn discover_children_with_invalid_issuer_der_records_error_audit() {
        let g = generate_chain_and_crl();
        let pack = dummy_pack_with_files(vec![
            PackFile::from_bytes_compute_sha256(
                "rsync://example.test/repo/issuer/issuer.crl",
                g.issuer_crl_der.clone(),
            ),
            PackFile::from_bytes_compute_sha256(
                "rsync://example.test/repo/issuer/child.cer",
                g.child_ca_der.clone(),
            ),
        ]);

        let issuer = CaInstanceHandle {
            depth: 0,
            tal_id: "test-tal".to_string(),
            parent_manifest_rsync_uri: None,
            ca_certificate_der: vec![0u8],
            ca_certificate_rsync_uri: None,
            effective_ip_resources: None,
            effective_as_resources: None,
            rsync_base_uri: "rsync://example.test/repo/issuer/".to_string(),
            manifest_rsync_uri: "rsync://example.test/repo/issuer/issuer.mft".to_string(),
            publication_point_rsync_uri: "rsync://example.test/repo/issuer/".to_string(),
            rrdp_notification_uri: None,
        };

        let out = discover_children_from_fresh_snapshot_with_audit(
            &issuer,
            &pack,
            time::OffsetDateTime::now_utc(),
            None,
        )
        .expect("discover children with invalid issuer der");
        assert!(out.children.is_empty());
        assert_eq!(out.audits.len(), 1);
        assert!(matches!(out.audits[0].result, AuditObjectResult::Error));
        assert!(
            out.audits[0]
                .detail
                .as_deref()
                .unwrap_or("")
                .contains("issuer CA decode failed")
        );
    }

    #[test]
    fn project_current_instance_vcir_reuses_local_outputs_and_restores_children() {
        let now = time::OffsetDateTime::now_utc();
        let g = generate_chain_and_crl();
        let child_cert_hash = sha256_hex(&g.child_ca_der);
        let vcir = sample_vcir_for_projection(now, &child_cert_hash);

        let store_dir = tempfile::tempdir().expect("store dir");
        let store = RocksStore::open(store_dir.path()).expect("open rocksdb");
        store.put_vcir(&vcir).expect("put vcir");
        for (bytes, uri, object_type) in [
            (
                b"manifest-bytes".to_vec(),
                Some(vcir.current_manifest_rsync_uri.clone()),
                Some("mft".to_string()),
            ),
            (
                b"current-crl-bytes".to_vec(),
                Some(vcir.current_crl_rsync_uri.clone()),
                Some("crl".to_string()),
            ),
            (
                g.child_ca_der.clone(),
                Some(vcir.child_entries[0].child_cert_rsync_uri.clone()),
                Some("cer".to_string()),
            ),
            (
                b"roa-bytes".to_vec(),
                Some("rsync://example.test/repo/issuer/a.roa".to_string()),
                Some("roa".to_string()),
            ),
            (
                b"aspa-bytes".to_vec(),
                Some("rsync://example.test/repo/issuer/a.asa".to_string()),
                Some("aspa".to_string()),
            ),
        ] {
            let mut entry = RawByHashEntry::from_bytes(sha256_hex(&bytes), bytes);
            if let Some(uri) = uri {
                entry.origin_uris.push(uri);
            }
            entry.object_type = object_type;
            entry.encoding = Some("der".to_string());
            store
                .put_raw_by_hash_entry(&entry)
                .expect("put raw_by_hash");
        }

        let ca = CaInstanceHandle {
            depth: 0,
            tal_id: "test-tal".to_string(),
            parent_manifest_rsync_uri: None,
            ca_certificate_der: Vec::new(),
            ca_certificate_rsync_uri: None,
            effective_ip_resources: None,
            effective_as_resources: None,
            rsync_base_uri: "rsync://example.test/repo/issuer/".to_string(),
            manifest_rsync_uri: vcir.manifest_rsync_uri.clone(),
            publication_point_rsync_uri: "rsync://example.test/repo/issuer/".to_string(),
            rrdp_notification_uri: Some("https://example.test/notify.xml".to_string()),
        };

        let projection = project_current_instance_vcir_on_failed_fetch(
            &store,
            &ca,
            &ManifestFreshError::RepoSyncFailed {
                detail: "synthetic".to_string(),
            },
            now,
        )
        .expect("project vcir");

        assert_eq!(
            projection.source,
            PublicationPointSource::VcirCurrentInstance
        );
        assert_eq!(projection.objects.vrps.len(), 1);
        assert_eq!(projection.objects.aspas.len(), 1);
        assert_eq!(projection.objects.router_keys.len(), 1);
        assert_eq!(projection.discovered_children.len(), 1);
        assert_eq!(
            projection.discovered_children[0].handle.manifest_rsync_uri,
            "rsync://example.test/repo/child/child.mft"
        );
        assert!(
            projection.snapshot.is_some(),
            "expected reconstructed snapshot"
        );
    }

    #[test]
    fn project_current_instance_vcir_returns_no_output_when_instance_gate_expired() {
        let now = time::OffsetDateTime::now_utc();
        let child_cert_hash = sha256_hex(b"child-cert");
        let mut vcir = sample_vcir_for_projection(now, &child_cert_hash);
        let this_update = PackTime::from_utc_offset_datetime(now - time::Duration::minutes(2));
        let expired = PackTime::from_utc_offset_datetime(now - time::Duration::minutes(1));
        vcir.validated_manifest_meta.validated_manifest_this_update = this_update;
        vcir.validated_manifest_meta.validated_manifest_next_update = expired.clone();
        vcir.instance_gate.manifest_next_update = expired.clone();
        vcir.instance_gate.current_crl_next_update = expired.clone();
        vcir.instance_gate.instance_effective_until = expired;

        let store_dir = tempfile::tempdir().expect("store dir");
        let store = RocksStore::open(store_dir.path()).expect("open rocksdb");
        store.put_vcir(&vcir).expect("put vcir");

        let ca = CaInstanceHandle {
            depth: 0,
            tal_id: "test-tal".to_string(),
            parent_manifest_rsync_uri: None,
            ca_certificate_der: Vec::new(),
            ca_certificate_rsync_uri: None,
            effective_ip_resources: None,
            effective_as_resources: None,
            rsync_base_uri: "rsync://example.test/repo/issuer/".to_string(),
            manifest_rsync_uri: vcir.manifest_rsync_uri.clone(),
            publication_point_rsync_uri: "rsync://example.test/repo/issuer/".to_string(),
            rrdp_notification_uri: None,
        };

        let projection = project_current_instance_vcir_on_failed_fetch(
            &store,
            &ca,
            &ManifestFreshError::RepoSyncFailed {
                detail: "synthetic".to_string(),
            },
            now,
        )
        .expect("project vcir");

        assert_eq!(
            projection.source,
            PublicationPointSource::FailedFetchNoCache
        );
        assert!(projection.objects.vrps.is_empty());
        assert!(projection.objects.aspas.is_empty());
        assert!(projection.discovered_children.is_empty());
    }

    #[test]
    fn project_current_instance_vcir_returns_no_output_when_latest_result_missing() {
        let now = time::OffsetDateTime::now_utc();
        let store_dir = tempfile::tempdir().expect("store dir");
        let store = RocksStore::open(store_dir.path()).expect("open rocksdb");
        let ca = CaInstanceHandle {
            depth: 0,
            tal_id: "test-tal".to_string(),
            parent_manifest_rsync_uri: None,
            ca_certificate_der: Vec::new(),
            ca_certificate_rsync_uri: None,
            effective_ip_resources: None,
            effective_as_resources: None,
            rsync_base_uri: "rsync://example.test/repo/issuer/".to_string(),
            manifest_rsync_uri: "rsync://example.test/repo/issuer/issuer.mft".to_string(),
            publication_point_rsync_uri: "rsync://example.test/repo/issuer/".to_string(),
            rrdp_notification_uri: None,
        };

        let projection = project_current_instance_vcir_on_failed_fetch(
            &store,
            &ca,
            &ManifestFreshError::RepoSyncFailed {
                detail: "synthetic".to_string(),
            },
            now,
        )
        .expect("project without cached vcir");

        assert_eq!(
            projection.source,
            PublicationPointSource::FailedFetchNoCache
        );
        assert!(projection.vcir.is_none());
        assert!(projection.snapshot.is_none());
        assert!(projection.objects.audit.is_empty());
        assert!(projection.discovered_children.is_empty());
        assert!(projection.warnings.iter().any(|warning| {
            warning
                .message
                .contains("no latest validated result for current CA instance")
        }));
    }

    #[test]
    fn project_current_instance_vcir_returns_no_output_when_latest_result_is_ineligible() {
        let now = time::OffsetDateTime::now_utc();
        let child_cert_hash = sha256_hex(b"child-cert");
        let mut vcir = sample_vcir_for_projection(now, &child_cert_hash);
        vcir.audit_summary.failed_fetch_eligible = false;

        let store_dir = tempfile::tempdir().expect("store dir");
        let store = RocksStore::open(store_dir.path()).expect("open rocksdb");
        store.put_vcir(&vcir).expect("put vcir");

        let ca = CaInstanceHandle {
            depth: 0,
            tal_id: "test-tal".to_string(),
            parent_manifest_rsync_uri: None,
            ca_certificate_der: Vec::new(),
            ca_certificate_rsync_uri: None,
            effective_ip_resources: None,
            effective_as_resources: None,
            rsync_base_uri: "rsync://example.test/repo/issuer/".to_string(),
            manifest_rsync_uri: vcir.manifest_rsync_uri.clone(),
            publication_point_rsync_uri: "rsync://example.test/repo/issuer/".to_string(),
            rrdp_notification_uri: None,
        };

        let projection = project_current_instance_vcir_on_failed_fetch(
            &store,
            &ca,
            &ManifestFreshError::RepoSyncFailed {
                detail: "synthetic".to_string(),
            },
            now,
        )
        .expect("project ineligible vcir");

        assert_eq!(
            projection.source,
            PublicationPointSource::FailedFetchNoCache
        );
        assert!(projection.vcir.is_some());
        assert!(projection.snapshot.is_none());
        assert!(projection.discovered_children.is_empty());
        assert!(projection.warnings.iter().any(|warning| {
            warning
                .message
                .contains("latest VCIR is not marked failed-fetch eligible")
        }));
    }

    #[test]
    fn parse_snapshot_time_value_reports_invalid_timestamp() {
        let err = parse_snapshot_time_value(&PackTime {
            rfc3339_utc: "not-a-time".to_string(),
        })
        .unwrap_err();

        assert!(err.contains("invalid RFC3339 time 'not-a-time'"), "{err}");
    }

    #[test]
    fn build_objects_output_from_vcir_tracks_expired_and_invalid_cached_outputs() {
        let now = time::OffsetDateTime::now_utc();
        let child_cert_hash = sha256_hex(b"child-cert");
        let mut vcir = sample_vcir_for_projection(now, &child_cert_hash);

        let bad_time_uri = "rsync://example.test/repo/issuer/bad-time.roa".to_string();
        let expired_uri = "rsync://example.test/repo/issuer/expired.asa".to_string();
        let bad_json_uri = "rsync://example.test/repo/issuer/bad-json.roa".to_string();
        let bad_prefix_uri = "rsync://example.test/repo/issuer/bad-prefix.roa".to_string();
        let bad_aspa_uri = "rsync://example.test/repo/issuer/bad-aspa.asa".to_string();

        for (uri, kind) in [
            (bad_time_uri.clone(), VcirArtifactKind::Roa),
            (expired_uri.clone(), VcirArtifactKind::Aspa),
            (bad_json_uri.clone(), VcirArtifactKind::Roa),
            (bad_prefix_uri.clone(), VcirArtifactKind::Roa),
            (bad_aspa_uri.clone(), VcirArtifactKind::Aspa),
        ] {
            vcir.related_artifacts.push(VcirRelatedArtifact {
                artifact_role: VcirArtifactRole::SignedObject,
                artifact_kind: kind,
                uri: Some(uri.clone()),
                sha256: sha256_hex(uri.as_bytes()),
                object_type: Some(
                    match kind {
                        VcirArtifactKind::Aspa => "aspa",
                        _ => "roa",
                    }
                    .to_string(),
                ),
                validation_status: VcirArtifactValidationStatus::Accepted,
            });
        }

        vcir.local_outputs.push(VcirLocalOutput {
            output_id: sha256_hex(b"bad-time"),
            output_type: VcirOutputType::Vrp,
            item_effective_until: PackTime {
                rfc3339_utc: "bad-time-value".to_string(),
            },
            source_object_uri: bad_time_uri.clone(),
            source_object_type: "roa".to_string(),
            source_object_hash: sha256_hex(b"bad-time-src"),
            source_ee_cert_hash: sha256_hex(b"bad-time-ee"),
            payload_json:
                serde_json::json!({"asn": 64496, "prefix": "203.0.113.0/24", "max_length": 24})
                    .to_string(),
            rule_hash: sha256_hex(b"bad-time-rule"),
            validation_path_hint: vec![vcir.manifest_rsync_uri.clone(), bad_time_uri.clone()],
        });
        vcir.local_outputs.push(VcirLocalOutput {
            output_id: sha256_hex(b"expired"),
            output_type: VcirOutputType::Aspa,
            item_effective_until: PackTime::from_utc_offset_datetime(
                now - time::Duration::minutes(1),
            ),
            source_object_uri: expired_uri.clone(),
            source_object_type: "aspa".to_string(),
            source_object_hash: sha256_hex(b"expired-src"),
            source_ee_cert_hash: sha256_hex(b"expired-ee"),
            payload_json: serde_json::json!({"customer_as_id": 64500, "provider_as_ids": [64501]})
                .to_string(),
            rule_hash: sha256_hex(b"expired-rule"),
            validation_path_hint: vec![vcir.manifest_rsync_uri.clone(), expired_uri.clone()],
        });
        vcir.local_outputs.push(VcirLocalOutput {
            output_id: sha256_hex(b"bad-json"),
            output_type: VcirOutputType::Vrp,
            item_effective_until: PackTime::from_utc_offset_datetime(
                now + time::Duration::minutes(5),
            ),
            source_object_uri: bad_json_uri.clone(),
            source_object_type: "roa".to_string(),
            source_object_hash: sha256_hex(b"bad-json-src"),
            source_ee_cert_hash: sha256_hex(b"bad-json-ee"),
            payload_json: "{not-json".to_string(),
            rule_hash: sha256_hex(b"bad-json-rule"),
            validation_path_hint: vec![vcir.manifest_rsync_uri.clone(), bad_json_uri.clone()],
        });
        vcir.local_outputs.push(VcirLocalOutput {
            output_id: sha256_hex(b"bad-prefix"),
            output_type: VcirOutputType::Vrp,
            item_effective_until: PackTime::from_utc_offset_datetime(
                now + time::Duration::minutes(5),
            ),
            source_object_uri: bad_prefix_uri.clone(),
            source_object_type: "roa".to_string(),
            source_object_hash: sha256_hex(b"bad-prefix-src"),
            source_ee_cert_hash: sha256_hex(b"bad-prefix-ee"),
            payload_json:
                serde_json::json!({"asn": 64510, "prefix": "203.0.113.0", "max_length": 24})
                    .to_string(),
            rule_hash: sha256_hex(b"bad-prefix-rule"),
            validation_path_hint: vec![vcir.manifest_rsync_uri.clone(), bad_prefix_uri.clone()],
        });
        vcir.local_outputs.push(VcirLocalOutput {
            output_id: sha256_hex(b"bad-aspa"),
            output_type: VcirOutputType::Aspa,
            item_effective_until: PackTime::from_utc_offset_datetime(
                now + time::Duration::minutes(5),
            ),
            source_object_uri: bad_aspa_uri.clone(),
            source_object_type: "aspa".to_string(),
            source_object_hash: sha256_hex(b"bad-aspa-src"),
            source_ee_cert_hash: sha256_hex(b"bad-aspa-ee"),
            payload_json: serde_json::json!({"customer_as_id": 64520}).to_string(),
            rule_hash: sha256_hex(b"bad-aspa-rule"),
            validation_path_hint: vec![vcir.manifest_rsync_uri.clone(), bad_aspa_uri.clone()],
        });

        let mut warnings = Vec::new();
        let output = build_objects_output_from_vcir(&vcir, now, &mut warnings);

        assert_eq!(output.vrps.len(), 1);
        assert_eq!(output.aspas.len(), 1);
        assert_eq!(output.stats.roa_total, 4);
        assert_eq!(output.stats.roa_ok, 1);
        assert_eq!(output.stats.aspa_total, 3);
        assert_eq!(output.stats.aspa_ok, 1);
        assert!(warnings.iter().any(|warning| {
            warning
                .message
                .contains("cached local output has invalid item_effective_until")
        }));
        assert!(warnings.iter().any(|warning| {
            warning
                .message
                .contains("cached ROA local output parse failed")
        }));
        assert!(warnings.iter().any(|warning| {
            warning
                .message
                .contains("cached ASPA local output parse failed")
        }));
        assert!(output.audit.iter().any(|entry| {
            entry.rsync_uri == expired_uri
                && matches!(entry.result, AuditObjectResult::Skipped)
                && entry.detail.as_deref() == Some("skipped: cached local output expired")
        }));
        assert!(output.audit.iter().any(|entry| {
            entry.rsync_uri == bad_time_uri && matches!(entry.result, AuditObjectResult::Error)
        }));
        assert!(output.audit.iter().any(|entry| {
            entry.rsync_uri == bad_prefix_uri
                && matches!(entry.result, AuditObjectResult::Error)
                && entry
                    .detail
                    .as_deref()
                    .unwrap_or("")
                    .contains("cached ROA local output parse failed")
        }));
    }

    #[test]
    fn build_publication_point_audit_from_vcir_uses_vcir_metadata_and_overlays_child_and_object_audits()
     {
        let now = time::OffsetDateTime::now_utc();
        let child_cert_hash = sha256_hex(b"child-cert");
        let mut vcir = sample_vcir_for_projection(now, &child_cert_hash);
        vcir.related_artifacts
            .retain(|artifact| artifact.artifact_role != VcirArtifactRole::Manifest);

        let ca = CaInstanceHandle {
            depth: 0,
            tal_id: "test-tal".to_string(),
            parent_manifest_rsync_uri: None,
            ca_certificate_der: Vec::new(),
            ca_certificate_rsync_uri: None,
            effective_ip_resources: None,
            effective_as_resources: None,
            rsync_base_uri: "rsync://example.test/repo/issuer/".to_string(),
            manifest_rsync_uri: vcir.manifest_rsync_uri.clone(),
            publication_point_rsync_uri: "rsync://example.test/repo/issuer/".to_string(),
            rrdp_notification_uri: Some("https://example.test/notify.xml".to_string()),
        };
        let runner_warnings = vec![Warning::new("runner warning")];
        let objects = crate::validation::objects::ObjectsOutput {
            vrps: Vec::new(),
            aspas: Vec::new(),
            router_keys: Vec::new(),
            local_outputs_cache: Vec::new(),
            warnings: vec![Warning::new("objects warning")],
            stats: crate::validation::objects::ObjectsStats::default(),
            audit: vec![ObjectAuditEntry {
                rsync_uri: "rsync://example.test/repo/issuer/a.roa".to_string(),
                sha256_hex: sha256_hex(b"override-roa"),
                kind: AuditObjectKind::Roa,
                result: AuditObjectResult::Error,
                detail: Some("overridden from object audit".to_string()),
            }],
        };
        let child_audits = vec![ObjectAuditEntry {
            rsync_uri: vcir.child_entries[0].child_cert_rsync_uri.clone(),
            sha256_hex: vcir.child_entries[0].child_cert_hash.clone(),
            kind: AuditObjectKind::Certificate,
            result: AuditObjectResult::Ok,
            detail: Some("restored child CA instance from VCIR".to_string()),
        }];

        let audit = build_publication_point_audit_from_vcir(
            &ca,
            PublicationPointSource::VcirCurrentInstance,
            None,
            None,
            None,
            Some(&vcir),
            None,
            &runner_warnings,
            &objects,
            &child_audits,
        );

        assert_eq!(audit.source, "vcir_current_instance");
        assert_eq!(audit.objects[0].rsync_uri, vcir.current_manifest_rsync_uri);
        assert_eq!(audit.objects[0].kind, AuditObjectKind::Manifest);
        assert_eq!(
            audit.this_update_rfc3339_utc,
            vcir.validated_manifest_meta
                .validated_manifest_this_update
                .rfc3339_utc
        );
        assert_eq!(
            audit.next_update_rfc3339_utc,
            vcir.validated_manifest_meta
                .validated_manifest_next_update
                .rfc3339_utc
        );
        assert_eq!(
            audit.verified_at_rfc3339_utc,
            vcir.last_successful_validation_time.rfc3339_utc
        );
        assert_eq!(audit.warnings.len(), 2);
        assert!(audit.objects.iter().any(|entry| {
            entry.rsync_uri == "rsync://example.test/repo/issuer/a.roa"
                && matches!(entry.result, AuditObjectResult::Error)
                && entry.detail.as_deref() == Some("overridden from object audit")
        }));
        assert!(audit.objects.iter().any(|entry| {
            entry.rsync_uri == vcir.child_entries[0].child_cert_rsync_uri
                && matches!(entry.result, AuditObjectResult::Ok)
        }));
    }

    #[test]
    fn build_publication_point_audit_from_vcir_without_cached_inputs_returns_empty_listing() {
        let ca = CaInstanceHandle {
            depth: 0,
            tal_id: "test-tal".to_string(),
            parent_manifest_rsync_uri: None,
            ca_certificate_der: Vec::new(),
            ca_certificate_rsync_uri: None,
            effective_ip_resources: None,
            effective_as_resources: None,
            rsync_base_uri: "rsync://example.test/repo/issuer/".to_string(),
            manifest_rsync_uri: "rsync://example.test/repo/issuer/issuer.mft".to_string(),
            publication_point_rsync_uri: "rsync://example.test/repo/issuer/".to_string(),
            rrdp_notification_uri: None,
        };

        let audit = build_publication_point_audit_from_vcir(
            &ca,
            PublicationPointSource::FailedFetchNoCache,
            None,
            None,
            None,
            None,
            None,
            &[Warning::new("runner warning")],
            &crate::validation::objects::ObjectsOutput {
                vrps: Vec::new(),
                aspas: Vec::new(),
            router_keys: Vec::new(),
                local_outputs_cache: Vec::new(),
                warnings: vec![Warning::new("object warning")],
                stats: crate::validation::objects::ObjectsStats::default(),
                audit: Vec::new(),
            },
            &[],
        );

        assert_eq!(audit.source, "failed_fetch_no_cache");
        assert!(audit.this_update_rfc3339_utc.is_empty());
        assert!(audit.next_update_rfc3339_utc.is_empty());
        assert!(audit.verified_at_rfc3339_utc.is_empty());
        assert_eq!(audit.warnings.len(), 2);
        assert!(audit.objects.is_empty());
    }

    #[test]
    fn reconstruct_snapshot_from_vcir_reports_missing_manifest_and_related_raw_bytes() {
        let now = time::OffsetDateTime::now_utc();
        let child_cert_hash = sha256_hex(b"child-cert");
        let mut vcir = sample_vcir_for_projection(now, &child_cert_hash);
        let dup_uri = "rsync://example.test/repo/issuer/dup.roa".to_string();
        vcir.related_artifacts.push(VcirRelatedArtifact {
            artifact_role: VcirArtifactRole::SignedObject,
            artifact_kind: VcirArtifactKind::Roa,
            uri: Some(dup_uri.clone()),
            sha256: sha256_hex(b"dup-roa-1"),
            object_type: Some("roa".to_string()),
            validation_status: VcirArtifactValidationStatus::Accepted,
        });
        vcir.related_artifacts.push(VcirRelatedArtifact {
            artifact_role: VcirArtifactRole::SignedObject,
            artifact_kind: VcirArtifactKind::Roa,
            uri: Some(dup_uri.clone()),
            sha256: sha256_hex(b"dup-roa-2"),
            object_type: Some("roa".to_string()),
            validation_status: VcirArtifactValidationStatus::Accepted,
        });
        vcir.related_artifacts.push(VcirRelatedArtifact {
            artifact_role: VcirArtifactRole::IssuerCert,
            artifact_kind: VcirArtifactKind::Cer,
            uri: Some("rsync://example.test/repo/issuer/issuer.cer".to_string()),
            sha256: sha256_hex(b"issuer-cert"),
            object_type: Some("cer".to_string()),
            validation_status: VcirArtifactValidationStatus::Accepted,
        });

        let ca = CaInstanceHandle {
            depth: 0,
            tal_id: "test-tal".to_string(),
            parent_manifest_rsync_uri: None,
            ca_certificate_der: Vec::new(),
            ca_certificate_rsync_uri: None,
            effective_ip_resources: None,
            effective_as_resources: None,
            rsync_base_uri: "rsync://example.test/repo/issuer/".to_string(),
            manifest_rsync_uri: vcir.manifest_rsync_uri.clone(),
            publication_point_rsync_uri: "rsync://example.test/repo/issuer/".to_string(),
            rrdp_notification_uri: None,
        };

        let store_dir = tempfile::tempdir().expect("store dir");
        let store = RocksStore::open(store_dir.path()).expect("open rocksdb");
        let mut warnings = Vec::new();
        assert!(reconstruct_snapshot_from_vcir(&store, &ca, &vcir, &mut warnings).is_none());
        assert!(warnings.iter().any(|warning| {
            warning
                .message
                .contains("manifest raw bytes missing for VCIR audit reconstruction")
        }));

        let manifest_bytes = b"manifest-bytes".to_vec();
        let current_crl_bytes = b"current-crl-bytes".to_vec();
        let child_bytes = b"child-cert".to_vec();
        let roa_bytes = b"roa-bytes".to_vec();
        for (bytes, uri, object_type) in [
            (
                manifest_bytes.clone(),
                Some(vcir.manifest_rsync_uri.clone()),
                Some("mft".to_string()),
            ),
            (
                current_crl_bytes,
                Some(vcir.current_crl_rsync_uri.clone()),
                Some("crl".to_string()),
            ),
            (
                child_bytes,
                Some(vcir.child_entries[0].child_cert_rsync_uri.clone()),
                Some("cer".to_string()),
            ),
            (
                roa_bytes,
                Some("rsync://example.test/repo/issuer/a.roa".to_string()),
                Some("roa".to_string()),
            ),
        ] {
            let mut entry = RawByHashEntry::from_bytes(sha256_hex(&bytes), bytes);
            if let Some(uri) = uri {
                entry.origin_uris.push(uri);
            }
            entry.object_type = object_type;
            entry.encoding = Some("der".to_string());
            store.put_raw_by_hash_entry(&entry).expect("put raw entry");
        }

        warnings.clear();
        let pack = reconstruct_snapshot_from_vcir(&store, &ca, &vcir, &mut warnings)
            .expect("reconstruct pack with partial related artifacts");
        assert_eq!(pack.manifest_bytes, manifest_bytes);
        assert_eq!(pack.files.len(), 3, "crl + child cert + roa only");
        assert!(
            pack.files
                .iter()
                .any(|file| file.rsync_uri.ends_with("issuer.crl"))
        );
        assert!(
            pack.files
                .iter()
                .any(|file| file.rsync_uri.ends_with("child.cer"))
        );
        assert!(
            pack.files
                .iter()
                .any(|file| file.rsync_uri.ends_with("a.roa"))
        );
        assert!(
            !pack
                .files
                .iter()
                .any(|file| file.rsync_uri.ends_with("issuer.cer"))
        );
        assert!(warnings.iter().any(|warning| {
            warning
                .message
                .contains("related artifact raw bytes missing for VCIR audit reconstruction")
        }));
    }

    #[test]
    fn runner_dedup_paths_execute_with_timing_enabled() {
        let fixture_dir = std::path::PathBuf::from(env!("CARGO_MANIFEST_DIR"))
            .join("tests/fixtures/repository/rpki.cernet.net/repo/cernet/0");
        let rsync_base_uri = "rsync://rpki.cernet.net/repo/cernet/0/".to_string();
        let manifest_file = "05FC9C5B88506F7C0D3F862C8895BED67E9F8EBA.mft";
        let manifest_rsync_uri = format!("{rsync_base_uri}{manifest_file}");
        let fixture_manifest_bytes =
            std::fs::read(fixture_dir.join(manifest_file)).expect("read manifest fixture");
        let fixture_manifest =
            crate::data_model::manifest::ManifestObject::decode_der(&fixture_manifest_bytes)
                .expect("decode manifest fixture");
        let validation_time = fixture_manifest.manifest.this_update + time::Duration::seconds(60);
        let store_dir = tempfile::tempdir().expect("store dir");
        let store = RocksStore::open(store_dir.path()).expect("open rocksdb");
        let issuer_ca_der = std::fs::read(
            std::path::PathBuf::from(env!("CARGO_MANIFEST_DIR")).join(
                "tests/fixtures/repository/rpki.apnic.net/repository/B527EF581D6611E2BB468F7C72FD1FF2/BfycW4hQb3wNP4YsiJW-1n6fjro.cer",
            ),
        )
        .expect("read issuer ca fixture");
        let issuer_ca = ResourceCertificate::decode_der(&issuer_ca_der).expect("decode issuer ca");
        let handle = CaInstanceHandle {
            depth: 0,
            tal_id: "test-tal".to_string(),
            parent_manifest_rsync_uri: None,
            ca_certificate_der: issuer_ca_der,
            ca_certificate_rsync_uri: Some("rsync://rpki.apnic.net/repository/B527EF581D6611E2BB468F7C72FD1FF2/BfycW4hQb3wNP4YsiJW-1n6fjro.cer".to_string()),
            effective_ip_resources: issuer_ca.tbs.extensions.ip_resources.clone(),
            effective_as_resources: issuer_ca.tbs.extensions.as_resources.clone(),
            rsync_base_uri: rsync_base_uri.clone(),
            manifest_rsync_uri: manifest_rsync_uri.clone(),
            publication_point_rsync_uri: rsync_base_uri.clone(),
            rrdp_notification_uri: Some("https://example.test/notification.xml".to_string()),
        };
        let timing =
            crate::analysis::timing::TimingHandle::new(crate::analysis::timing::TimingMeta {
                recorded_at_utc_rfc3339: "2026-03-11T00:00:00Z".to_string(),
                validation_time_utc_rfc3339: "2026-03-11T00:00:00Z".to_string(),
                tal_url: None,
                db_path: None,
            });
        let policy_rrdp = Policy::default();
        let runner_rrdp = Rpkiv1PublicationPointRunner {
            store: &store,
            policy: &policy_rrdp,
            http_fetcher: &NeverHttpFetcher,
            rsync_fetcher: &LocalDirRsyncFetcher::new(&fixture_dir),
            validation_time,
            timing: Some(timing.clone()),
            download_log: None,
            replay_archive_index: None,
            replay_delta_index: None,
            rrdp_dedup: true,
            rrdp_repo_cache: Mutex::new(HashMap::new()),
            rsync_dedup: false,
            rsync_repo_cache: Mutex::new(HashMap::new()),
        };
        let first = runner_rrdp
            .run_publication_point(&handle)
            .expect("rrdp fallback to rsync");
        assert_eq!(first.source, PublicationPointSource::Fresh);
        let second = runner_rrdp
            .run_publication_point(&handle)
            .expect("rrdp dedup skip");
        assert_eq!(second.source, PublicationPointSource::Fresh);

        let policy_rsync = Policy {
            sync_preference: crate::policy::SyncPreference::RsyncOnly,
            ..Policy::default()
        };
        let runner_rsync = Rpkiv1PublicationPointRunner {
            store: &store,
            policy: &policy_rsync,
            http_fetcher: &NeverHttpFetcher,
            rsync_fetcher: &LocalDirRsyncFetcher::new(&fixture_dir),
            validation_time,
            timing: Some(timing),
            download_log: None,
            replay_archive_index: None,
            replay_delta_index: None,
            rrdp_dedup: false,
            rrdp_repo_cache: Mutex::new(HashMap::new()),
            rsync_dedup: true,
            rsync_repo_cache: Mutex::new(HashMap::new()),
        };
        let third = runner_rsync
            .run_publication_point(&handle)
            .expect("rsync first run");
        assert_eq!(third.source, PublicationPointSource::Fresh);
        let fourth = runner_rsync
            .run_publication_point(&handle)
            .expect("rsync dedup run");
        assert_eq!(fourth.source, PublicationPointSource::Fresh);
        assert_eq!(
            crate::fetch::rsync::normalize_rsync_base_uri("rsync://example.test/repo"),
            "rsync://example.test/repo/"
        );
    }
}
