use super::*;
use crate::data_model::oid::OID_AD_SIGNED_OBJECT;
use crate::data_model::rc::{
    AccessDescription, AsIdOrRange, AsIdentifierChoice, AsResourceSet, ResourceCertificate,
};
use crate::data_model::roa::RoaAfi;
use crate::fetch::rsync::LocalDirRsyncFetcher;
use crate::fetch::rsync::{RsyncFetchError, RsyncFetcher};
use crate::storage::{
    PackFile, PackTime, PublicationPointCacheProjection,
    PublicationPointCacheProjectionWriteAction, RawByHashEntry, RepositoryViewEntry,
    RepositoryViewState, RocksStore, ValidatedCaInstanceResult, ValidatedManifestMeta,
    VcirArtifactKind, VcirArtifactRole, VcirArtifactValidationStatus, VcirAuditSummary,
    VcirChildEntry, VcirInstanceGate, VcirLocalOutput, VcirLocalOutputPayload, VcirOutputType,
    VcirRelatedArtifact, VcirSourceObjectType, VcirSummary,
};
use crate::sync::rrdp::Fetcher;
use crate::validation::publication_point::PublicationPointSnapshot;
use crate::validation::tree::{DiscoveredChildEntryProjection, PublicationPointRunner};

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

fn sha256_32(input: &[u8]) -> [u8; 32] {
    sha256_hex_to_32(&sha256_hex(input))
}

fn ipv4_addr(octets: [u8; 4]) -> [u8; 16] {
    let mut addr = [0u8; 16];
    addr[..4].copy_from_slice(&octets);
    addr
}

#[test]
fn publication_point_cache_policy_fingerprint_includes_resource_validation_mode() {
    let mut strict_policy = Policy::default();
    strict_policy.resource_validation_mode = ResourceValidationMode::Rfc6487;
    let mut vrs_policy = Policy::default();
    vrs_policy.resource_validation_mode = ResourceValidationMode::ValidationUpdate03;

    assert_ne!(
        publication_point_cache_policy_fingerprint(&strict_policy),
        publication_point_cache_policy_fingerprint(&vrs_policy)
    );
}

#[test]
fn router_asns_for_resource_mode_filters_vrs_and_rejects_strict_overclaim() {
    let issuer_as = AsResourceSet {
        asnum: Some(AsIdentifierChoice::AsIdsOrRanges(vec![
            AsIdOrRange::Range {
                min: 64500,
                max: 64510,
            },
        ])),
        rdi: None,
    };

    let strict = router_asns_for_resource_mode(
        &[64505, 64520],
        Some(&issuer_as),
        ResourceValidationMode::Rfc6487,
    )
    .unwrap_err();
    assert!(strict.contains("not a subset"), "{strict}");

    let vrs = router_asns_for_resource_mode(
        &[64505, 64520],
        Some(&issuer_as),
        ResourceValidationMode::ValidationUpdate03,
    )
    .expect("vrs filters router asns");
    assert_eq!(vrs, vec![64505]);
}

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 sample_runner_with_ccr_accumulator<'a>(
    store: &'a RocksStore,
    policy: &'a Policy,
) -> Rpkiv1PublicationPointRunner<'a> {
    Rpkiv1PublicationPointRunner {
        store,
        policy,
        http_fetcher: &NeverHttpFetcher,
        rsync_fetcher: &FailingRsyncFetcher,
        validation_time: time::OffsetDateTime::now_utc(),
        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()),
        current_repo_index: None,
        repo_sync_runtime: None,
        parallel_phase2_config: None,
        parallel_roa_worker_pool: None,
        ccr_accumulator: Some(Mutex::new(CcrAccumulator::new(Vec::new()))),
        persist_vcir: true,
        enable_roa_validation_cache: false,
        enable_child_certificate_validation_cache: false,
        publication_point_cache_observe_only: false,
        enable_publication_point_validation_cache: false,
    }
}

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>,
    issuer_crl_der_next: 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")));
    run(Command::new("openssl")
        .arg("ca")
        .arg("-gencrl")
        .arg("-config")
        .arg(dir.join("openssl.cnf"))
        .arg("-out")
        .arg(dir.join("issuer-next.crl.pem")));
    run(Command::new("openssl")
        .arg("crl")
        .arg("-in")
        .arg(dir.join("issuer-next.crl.pem"))
        .arg("-outform")
        .arg("DER")
        .arg("-out")
        .arg(dir.join("issuer-next.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"),
        issuer_crl_der_next: std::fs::read(dir.join("issuer-next.crl")).expect("read next 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));
    let ccr_manifest_projection = VcirCcrManifestProjection {
        manifest_rsync_uri: manifest_uri.clone(),
        manifest_sha256: hex::decode(&manifest_hash).expect("decode manifest hash"),
        manifest_size: 2048,
        manifest_ee_aki: vec![0x11; 20],
        manifest_number_be: vec![1],
        manifest_this_update: PackTime::from_utc_offset_datetime(now),
        manifest_sia_locations_der: vec![
            crate::ccr::manifest_location::encode_access_description_der(&AccessDescription {
                access_method_oid: OID_AD_SIGNED_OBJECT.to_string(),
                access_location: manifest_uri.clone(),
            })
            .expect("encode signedObject"),
        ],
        subordinate_skis: vec![vec![0x33; 20]],
    };
    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(),
        },
        ccr_manifest_projection,
        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_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: VcirSourceObjectType::Roa,
                source_object_hash: sha256_hex_to_32(&roa_hash),
                source_ee_cert_hash: sha256_hex_to_32(&ee_hash),
                payload: VcirLocalOutputPayload::Vrp {
                    asn: 64496,
                    afi: RoaAfi::Ipv4,
                    prefix_len: 24,
                    addr: ipv4_addr([203, 0, 113, 0]),
                    max_length: 24,
                },
                rule_hash: sha256_32(b"roa-rule"),
            },
            VcirLocalOutput {
                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: VcirSourceObjectType::Aspa,
                source_object_hash: sha256_hex_to_32(&aspa_hash),
                source_ee_cert_hash: sha256_hex_to_32(&ee_hash),
                payload: VcirLocalOutputPayload::Aspa {
                    customer_as_id: 64496,
                    provider_as_ids: vec![64497, 64498],
                },
                rule_hash: sha256_32(b"aspa-rule"),
            },
            VcirLocalOutput {
                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: VcirSourceObjectType::RouterKey,
                source_object_hash: sha256_hex_to_32(&router_hash),
                source_ee_cert_hash: sha256_hex_to_32(&router_hash),
                payload: VcirLocalOutputPayload::RouterKey {
                    as_id: 64496,
                    ski: vec![0x11; 20],
                    spki_der: vec![0x30, 0x00],
                },
                rule_hash: sha256_32(b"router-key-rule"),
            },
        ],
        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,
                reject_reason: None,
            },
            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,
                reject_reason: None,
            },
            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,
                reject_reason: None,
            },
            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,
                reject_reason: None,
            },
            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,
                reject_reason: None,
            },
        ],
        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(),
        },
    }
}

fn put_vcir_for_failed_fetch_reuse(
    store: &RocksStore,
    ca: &CaInstanceHandle,
    policy: &Policy,
    vcir: &ValidatedCaInstanceResult,
) {
    let validation_time = vcir
        .last_successful_validation_time
        .parse()
        .expect("parse VCIR validation time");
    let identity = failed_fetch_reuse_identity_for_fresh_result(
        ca,
        policy,
        validation_time,
        vcir.instance_gate.instance_effective_until.clone(),
    )
    .expect("build VCIR failed-fetch reuse identity");
    store
        .put_vcir_with_failed_fetch_reuse_identity(vcir, &identity)
        .expect("put reusable VCIR");
}

fn sample_ca_for_failed_fetch_reuse(vcir: &ValidatedCaInstanceResult) -> CaInstanceHandle {
    CaInstanceHandle {
        depth: 0,
        tal_id: "test-tal".to_string(),
        parent_manifest_rsync_uri: None,
        ca_certificate: CaCertificateRef::inline_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()),
    }
}

#[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: CaCertificateRef::inline_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_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: VcirSourceObjectType::Roa,
        source_object_hash: sha256_32(b"cached-roa"),
        source_ee_cert_hash: sha256_32(b"cached-ee"),
        payload: VcirLocalOutputPayload::Vrp {
            asn: 64500,
            afi: RoaAfi::Ipv4,
            prefix_len: 24,
            addr: ipv4_addr([203, 0, 113, 0]),
            max_length: 24,
        },
        rule_hash: sha256_32(b"cached-rule"),
    }];
    let mut objects = 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(),
        roa_cache_stats: crate::validation::objects::RoaValidationCacheStats::default(),
        roa_cache_object_meta: Vec::new(),
    };
    let outputs =
        take_or_build_vcir_local_outputs(&ca, &pack, &mut objects).expect("reuse cached outputs");
    assert_eq!(outputs, cached);
    assert!(objects.local_outputs_cache.is_empty());
}

#[test]
fn persist_vcir_non_repository_evidence_stores_current_ca_cert_only() {
    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 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: CaCertificateRef::inline_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).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"))
    );
    let first_output = objects
        .local_outputs_cache
        .first()
        .expect("first local output");
    assert!(
        store
            .get_raw_by_hash_entry(&first_output.source_ee_cert_hash_hex())
            .expect("load source ee raw")
            .is_none()
    );
}

#[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: CaCertificateRef::inline_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,
        VcirSourceObjectType::RouterKey
    );
    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: CaCertificateRef::inline_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 finalize_fresh_publication_point_releases_local_outputs_cache_after_persist() {
    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 mut 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 store_dir = tempfile::tempdir().expect("store dir");
    let store = RocksStore::open(store_dir.path()).expect("open rocksdb");
    let policy = Policy::default();
    let 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()),
        current_repo_index: None,
        repo_sync_runtime: None,
        parallel_phase2_config: None,
        parallel_roa_worker_pool: None,
        ccr_accumulator: None,
        persist_vcir: true,
        enable_roa_validation_cache: false,
        enable_child_certificate_validation_cache: false,
        publication_point_cache_observe_only: false,
        enable_publication_point_validation_cache: false,
    };
    let ca = CaInstanceHandle {
            depth: 0,
            tal_id: "test-tal".to_string(),
            parent_manifest_rsync_uri: None,
            ca_certificate: CaCertificateRef::inline_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,
        };
    let fresh_point = FreshValidatedPublicationPoint {
        manifest_rsync_uri: pack.manifest_rsync_uri.clone(),
        publication_point_rsync_uri: pack.publication_point_rsync_uri.clone(),
        manifest_number_be: pack.manifest_number_be.clone(),
        this_update: pack.this_update.clone(),
        next_update: pack.next_update.clone(),
        verified_at: pack.verified_at.clone(),
        manifest_bytes: pack.manifest_bytes.clone(),
        files: pack.files.clone(),
    };

    objects.local_outputs_cache.shrink_to_fit();
    let original_cache_capacity = objects.local_outputs_cache.capacity();
    let finalized = runner
        .finalize_fresh_publication_point_from_reducer(
            &ca,
            &fresh_point,
            Vec::new(),
            objects,
            Vec::new(),
            Vec::new(),
            None,
            None,
            0,
            None,
        )
        .expect("finalize fresh publication point");

    assert!(
        finalized.result.objects.local_outputs_cache.is_empty(),
        "local outputs cache should be released after VCIR persistence"
    );
    assert_eq!(
        finalized.result.objects.local_outputs_cache.capacity(),
        0,
        "released cache should not keep its backing allocation"
    );
    assert!(original_cache_capacity > 0);

    let persisted = store
        .get_vcir(&pack.manifest_rsync_uri)
        .expect("load persisted vcir")
        .expect("persisted vcir");
    assert!(
        !persisted.local_outputs.is_empty(),
        "VCIR should still persist local outputs before cache release"
    );
}

#[test]
fn persist_vcir_for_fresh_result_stores_vcir_and_replay_meta_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: CaCertificateRef::inline_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,
        };

    let mut objects = objects;
    persist_vcir_for_fresh_result_with_timing(
        &store,
        &Policy::default(),
        &ca,
        &pack,
        &mut objects,
        &[],
        &[],
        &[],
        validation_time,
        false,
    )
    .map(|_timing| ())
    .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());
    assert_eq!(
        vcir.ccr_manifest_projection.manifest_rsync_uri,
        pack.manifest_rsync_uri
    );
    assert_eq!(
        vcir.ccr_manifest_projection.manifest_number_be,
        pack.manifest_number_be
    );
    assert_eq!(
        vcir.ccr_manifest_projection.manifest_this_update,
        pack.this_update
    );
    assert_eq!(
        vcir.ccr_manifest_projection.manifest_size,
        pack.manifest_bytes.len() as u64
    );
    assert!(vcir.local_outputs.first().is_some(), "local outputs stored");
    let replay_meta = store
        .get_manifest_replay_meta(&pack.manifest_rsync_uri)
        .expect("get replay meta")
        .expect("replay meta exists");
    assert_eq!(replay_meta.manifest_rsync_uri, pack.manifest_rsync_uri);
    assert_eq!(
        replay_meta.manifest_sha256,
        sha2::Sha256::digest(&pack.manifest_bytes).to_vec()
    );
}

#[test]
fn build_vcir_ccr_manifest_projection_from_fresh_real_snapshot_matches_manifest_contents() {
    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 ca = CaInstanceHandle {
            depth: 0,
            tal_id: "test-tal".to_string(),
            parent_manifest_rsync_uri: None,
            ca_certificate: CaCertificateRef::inline_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 child_discovery =
        discover_children_from_fresh_snapshot_with_audit(&ca, &pack, validation_time, None)
            .expect("discover children");
    let store_dir = tempfile::tempdir().expect("store dir");
    let store = RocksStore::open(store_dir.path()).expect("open rocksdb");
    let child_entries =
        build_vcir_child_entries(&store, &child_discovery.children, validation_time)
            .expect("build child entries");

    let projection = build_vcir_ccr_manifest_projection_from_fresh(&ca, &pack, &child_entries)
        .expect("build ccr manifest projection");
    let manifest = ManifestObject::decode_der(&pack.manifest_bytes).expect("decode manifest");
    let expected_locations = match manifest.signed_object.signed_data.certificates[0]
        .resource_cert
        .tbs
        .extensions
        .subject_info_access
        .as_ref()
        .expect("manifest sia")
    {
        SubjectInfoAccess::Ee(ee_sia) => vec![
            crate::ccr::manifest_location::select_manifest_signed_object_location(
                &pack.manifest_rsync_uri,
                &ee_sia.access_descriptions,
            )
            .expect("select manifest signedObject"),
        ],
        SubjectInfoAccess::Ca(_) => panic!("manifest ee SIA should not be CA variant"),
    };

    assert_eq!(projection.manifest_rsync_uri, pack.manifest_rsync_uri);
    assert_eq!(
        projection.manifest_sha256,
        sha2::Sha256::digest(&pack.manifest_bytes).to_vec()
    );
    assert_eq!(projection.manifest_size, pack.manifest_bytes.len() as u64);
    assert_eq!(
        projection.manifest_ee_aki,
        manifest.signed_object.signed_data.certificates[0]
            .resource_cert
            .tbs
            .extensions
            .authority_key_identifier
            .clone()
            .expect("manifest aki")
    );
    assert_eq!(
        projection.manifest_number_be,
        manifest.manifest.manifest_number.bytes_be
    );
    assert_eq!(projection.manifest_this_update, pack.this_update);
    assert_eq!(projection.manifest_sia_locations_der, expected_locations);
    let expected_subordinate_skis = child_entries
        .iter()
        .map(|child| hex::decode(&child.child_ski).expect("decode child ski"))
        .collect::<Vec<_>>();
    assert_eq!(projection.subordinate_skis, expected_subordinate_skis);
}

#[test]
fn build_vcir_child_entries_uses_projection_without_repo_bytes() {
    let store_dir = tempfile::tempdir().expect("store dir");
    let store = RocksStore::open(store_dir.path()).expect("open rocksdb");
    let validation_time = time::OffsetDateTime::parse(
        "2026-06-26T00:00:00Z",
        &time::format_description::well_known::Rfc3339,
    )
    .expect("parse time");
    let child = DiscoveredChildCaInstance {
        handle: CaInstanceHandle {
            depth: 1,
            tal_id: "test-tal".to_string(),
            parent_manifest_rsync_uri: Some("rsync://example.test/repo/root.mft".to_string()),
            ca_certificate: CaCertificateRef::repo_bytes("aa".repeat(32)),
            ca_certificate_rsync_uri: Some("rsync://example.test/repo/child.cer".to_string()),
            effective_ip_resources: None,
            effective_as_resources: None,
            rsync_base_uri: "rsync://example.test/repo/child/".to_string(),
            manifest_rsync_uri: "rsync://example.test/repo/child/child.mft".to_string(),
            publication_point_rsync_uri: "rsync://example.test/repo/child/".to_string(),
            rrdp_notification_uri: Some("https://example.test/notify.xml".to_string()),
        },
        discovered_from: crate::audit::DiscoveredFrom {
            parent_manifest_rsync_uri: "rsync://example.test/repo/root.mft".to_string(),
            child_ca_certificate_rsync_uri: "rsync://example.test/repo/child.cer".to_string(),
            child_ca_certificate_sha256_hex: "aa".repeat(32),
        },
        child_entry_projection: Some(DiscoveredChildEntryProjection {
            child_ski: "11".repeat(20),
        }),
    };

    let entries = build_vcir_child_entries(&store, &[child], validation_time)
        .expect("projection should avoid repo-bytes load");
    assert_eq!(entries.len(), 1);
    assert_eq!(entries[0].child_ski, "11".repeat(20));
}

#[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: CaCertificateRef::inline_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 store_dir = tempfile::tempdir().expect("store dir");
    let store = RocksStore::open(store_dir.path()).expect("open rocksdb");
    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()),
            },
        ],
        roa_cache_stats: crate::validation::objects::RoaValidationCacheStats::default(),
        roa_cache_object_meta: Vec::new(),
    };
    let artifacts = build_vcir_related_artifacts(
        &store,
        &ca,
        &pack,
        "rsync://example.test/repo/issuer/issuer.crl",
        &objects,
        &[],
    );
    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
        && artifact.reject_reason.as_deref() == Some("bad roa")));
    assert!(artifacts.iter().any(|artifact| artifact.uri.as_deref()
        == Some("rsync://example.test/repo/issuer/a.asa")
        && artifact.validation_status == VcirArtifactValidationStatus::WarningOnly
        && artifact.reject_reason.is_none()));
    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")),
            "embedded EE cert artifacts should no longer be persisted separately"
        );
}

#[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: CaCertificateRef::inline_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_child_certificate_cache_reuses_successful_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: CaCertificateRef::inline_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 store_dir = tempfile::tempdir().expect("store dir");
    let store = RocksStore::open(store_dir.path()).expect("open rocksdb");
    let policy = Policy::default();
    let cache_context = ChildCertificateValidationCacheContext {
        store: &store,
        issuer_ca_sha256: issuer.ca_certificate_sha256_32().unwrap(),
        ca_validation_context_digest: ca_validation_context_digest_for_ca(&issuer),
        policy_fingerprint: publication_point_cache_policy_fingerprint(&policy),
    };
    let validation_time = time::OffsetDateTime::now_utc();

    let first = discover_children_from_fresh_snapshot_with_audit_cached(
        &issuer,
        &pack,
        validation_time,
        None,
        Some(cache_context),
    )
    .expect("first discovery writes cache");
    assert_eq!(first.children.len(), 1);

    let child_file = pack
        .files
        .iter()
        .find(|file| file.rsync_uri.ends_with("child.cer"))
        .expect("child file");
    let cache_key = child_certificate_cache_key_sha256_hex(
        &child_file.rsync_uri,
        &child_file.sha256,
        &cache_context.issuer_ca_sha256,
        &cache_context.ca_validation_context_digest,
        &cache_context.policy_fingerprint,
    );
    assert!(
        store
            .get_child_certificate_cache_projection(&cache_key)
            .expect("get projection")
            .is_some()
    );

    let timing = TimingHandle::new(crate::analysis::timing::TimingMeta {
        recorded_at_utc_rfc3339: "2026-06-24T00:00:00Z".to_string(),
        validation_time_utc_rfc3339: "2026-06-24T00:00:00Z".to_string(),
        tal_url: None,
        db_path: None,
    });
    let second = discover_children_from_fresh_snapshot_with_audit_cached(
        &issuer,
        &pack,
        validation_time,
        Some(&timing),
        Some(cache_context),
    )
    .expect("second discovery reuses cache");
    assert_eq!(second.children.len(), 1);
    assert_eq!(
        second.children[0].handle.manifest_rsync_uri,
        first.children[0].handle.manifest_rsync_uri
    );
    assert!(second.audits.iter().any(|audit| {
        audit
            .detail
            .as_deref()
            .unwrap_or("")
            .contains("child certificate validation cache")
    }));
    let counts = timing.counts_snapshot();
    assert_eq!(
        counts.get("child_certificate_cache_hit_ca").copied(),
        Some(1)
    );
    assert_eq!(
        counts
            .get("child_certificate_cache_batch_lookup_publication_points")
            .copied(),
        Some(1)
    );
    assert_eq!(
        counts
            .get("child_certificate_cache_batch_lookup_entries")
            .copied(),
        Some(1)
    );
    assert_eq!(
        counts
            .get("child_certificate_der_load_fresh_count")
            .copied(),
        Some(0)
    );
}

#[test]
fn discover_children_child_certificate_cache_rechecks_changed_valid_crl() {
    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 mut changed_pack = pack.clone();
    changed_pack.files[0] = PackFile::from_bytes_compute_sha256(
        "rsync://example.test/repo/issuer/issuer.crl",
        g.issuer_crl_der_next.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: CaCertificateRef::inline_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 store_dir = tempfile::tempdir().expect("store dir");
    let store = RocksStore::open(store_dir.path()).expect("open rocksdb");
    let policy = Policy::default();
    let cache_context = ChildCertificateValidationCacheContext {
        store: &store,
        issuer_ca_sha256: issuer.ca_certificate_sha256_32().unwrap(),
        ca_validation_context_digest: ca_validation_context_digest_for_ca(&issuer),
        policy_fingerprint: publication_point_cache_policy_fingerprint(&policy),
    };
    let validation_time = time::OffsetDateTime::now_utc();
    discover_children_from_fresh_snapshot_with_audit_cached(
        &issuer,
        &pack,
        validation_time,
        None,
        Some(cache_context),
    )
    .expect("populate cache");
    let child_file = pack
        .files
        .iter()
        .find(|file| file.rsync_uri.ends_with("child.cer"))
        .expect("child file");
    let cache_key = child_certificate_cache_key_sha256_hex(
        &child_file.rsync_uri,
        &child_file.sha256,
        &cache_context.issuer_ca_sha256,
        &cache_context.ca_validation_context_digest,
        &cache_context.policy_fingerprint,
    );
    let projection = store
        .get_child_certificate_cache_projection(&cache_key)
        .expect("get child certificate cache projection")
        .expect("child certificate cache projection present");
    let projected_until =
        parse_snapshot_time_value(&projection.effective_until).expect("parse projected until");
    let initial_crl = crate::data_model::crl::RpkixCrl::decode_der(&g.issuer_crl_der)
        .expect("decode initial crl");
    assert!(
        projected_until > initial_crl.next_update.utc,
        "child certificate cache projection must not be hard-capped by the issuing CRL nextUpdate"
    );

    let timing = TimingHandle::new(crate::analysis::timing::TimingMeta {
        recorded_at_utc_rfc3339: "2026-06-24T00:00:00Z".to_string(),
        validation_time_utc_rfc3339: "2026-06-24T00:00:00Z".to_string(),
        tal_url: None,
        db_path: None,
    });
    let out = discover_children_from_fresh_snapshot_with_audit_cached(
        &issuer,
        &changed_pack,
        validation_time,
        Some(&timing),
        Some(cache_context),
    )
    .expect("changed valid CRL should recheck revocation and reuse cache");
    assert_eq!(out.children.len(), 1);
    assert!(
        out.audits
            .iter()
            .any(|audit| audit.detail.as_deref().unwrap_or("").contains("cache"))
    );
    let counts = timing.counts_snapshot();
    assert_eq!(
        counts
            .get("child_certificate_cache_crl_recheck_hit")
            .copied(),
        Some(1)
    );
    assert_eq!(
        counts.get("child_certificate_cache_hit_ca").copied(),
        Some(1)
    );
}

#[test]
fn discover_children_child_certificate_cache_misses_when_current_crl_invalid() {
    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 mut changed_pack = pack.clone();
    changed_pack.files[0] = PackFile::from_bytes_compute_sha256(
        "rsync://example.test/repo/issuer/issuer.crl",
        b"not-a-valid-crl".to_vec(),
    );
    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: CaCertificateRef::inline_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 store_dir = tempfile::tempdir().expect("store dir");
    let store = RocksStore::open(store_dir.path()).expect("open rocksdb");
    let policy = Policy::default();
    let cache_context = ChildCertificateValidationCacheContext {
        store: &store,
        issuer_ca_sha256: issuer.ca_certificate_sha256_32().unwrap(),
        ca_validation_context_digest: ca_validation_context_digest_for_ca(&issuer),
        policy_fingerprint: publication_point_cache_policy_fingerprint(&policy),
    };
    let validation_time = time::OffsetDateTime::now_utc();
    discover_children_from_fresh_snapshot_with_audit_cached(
        &issuer,
        &pack,
        validation_time,
        None,
        Some(cache_context),
    )
    .expect("populate cache");

    let timing = TimingHandle::new(crate::analysis::timing::TimingMeta {
        recorded_at_utc_rfc3339: "2026-06-24T00:00:00Z".to_string(),
        validation_time_utc_rfc3339: "2026-06-24T00:00:00Z".to_string(),
        tal_url: None,
        db_path: None,
    });
    let out = discover_children_from_fresh_snapshot_with_audit_cached(
        &issuer,
        &changed_pack,
        validation_time,
        Some(&timing),
        Some(cache_context),
    )
    .expect("invalid CRL should miss cache and continue with audit error");
    assert!(out.children.is_empty());
    assert!(
        out.audits
            .iter()
            .all(|audit| !audit.detail.as_deref().unwrap_or("").contains("cache"))
    );
    let counts = timing.counts_snapshot();
    assert_eq!(
        counts
            .get("child_certificate_cache_miss_crl_invalid")
            .copied(),
        Some(1)
    );
}

#[test]
fn discover_children_child_certificate_cache_misses_when_unchanged_crl_expired() {
    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: CaCertificateRef::inline_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 store_dir = tempfile::tempdir().expect("store dir");
    let store = RocksStore::open(store_dir.path()).expect("open rocksdb");
    let policy = Policy::default();
    let cache_context = ChildCertificateValidationCacheContext {
        store: &store,
        issuer_ca_sha256: issuer.ca_certificate_sha256_32().unwrap(),
        ca_validation_context_digest: ca_validation_context_digest_for_ca(&issuer),
        policy_fingerprint: publication_point_cache_policy_fingerprint(&policy),
    };
    let validation_time = time::OffsetDateTime::now_utc();
    discover_children_from_fresh_snapshot_with_audit_cached(
        &issuer,
        &pack,
        validation_time,
        None,
        Some(cache_context),
    )
    .expect("populate cache");

    let timing = TimingHandle::new(crate::analysis::timing::TimingMeta {
        recorded_at_utc_rfc3339: "2026-06-24T00:00:00Z".to_string(),
        validation_time_utc_rfc3339: "2026-06-24T00:00:00Z".to_string(),
        tal_url: None,
        db_path: None,
    });
    let out = discover_children_from_fresh_snapshot_with_audit_cached(
        &issuer,
        &pack,
        validation_time + time::Duration::days(2),
        Some(&timing),
        Some(cache_context),
    )
    .expect("expired unchanged CRL should miss cache and continue with audit error");
    assert!(out.children.is_empty());
    assert!(
        out.audits
            .iter()
            .all(|audit| !audit.detail.as_deref().unwrap_or("").contains("cache"))
    );
    let counts = timing.counts_snapshot();
    assert_eq!(
        counts
            .get("child_certificate_cache_miss_crl_expired")
            .copied(),
        Some(1)
    );
}

#[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: CaCertificateRef::inline_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()),
        current_repo_index: None,
        repo_sync_runtime: None,
        parallel_phase2_config: None,
        parallel_roa_worker_pool: None,
        ccr_accumulator: None,
        persist_vcir: true,
        enable_roa_validation_cache: false,
        enable_child_certificate_validation_cache: false,
        publication_point_cache_observe_only: false,
        enable_publication_point_validation_cache: false,
    };

    // 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: CaCertificateRef::inline_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");
    assert!(!first_vrp.rule_hash_hex().is_empty());
    assert!(!first_vrp.output_id().is_empty());
    let replay_meta = store
        .get_manifest_replay_meta(&manifest_rsync_uri)
        .expect("get replay meta")
        .expect("replay meta exists");
    assert_eq!(replay_meta.manifest_rsync_uri, manifest_rsync_uri);
}

#[test]
fn runner_roa_validation_cache_reuses_vcir_outputs_on_second_fixture_run() {
    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: CaCertificateRef::inline_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,
        rrdp_notification_uri: None,
    };

    let first_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()),
        current_repo_index: None,
        repo_sync_runtime: None,
        parallel_phase2_config: None,
        parallel_roa_worker_pool: None,
        ccr_accumulator: None,
        persist_vcir: true,
        enable_roa_validation_cache: false,
        enable_child_certificate_validation_cache: false,
        publication_point_cache_observe_only: false,
        enable_publication_point_validation_cache: false,
    };
    let first = first_runner
        .run_publication_point(&handle)
        .expect("first fresh run");
    assert!(first.objects.vrps.len() > 1);
    assert_eq!(first.objects.roa_cache_stats.hit_roas, 0);

    let second_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()),
        current_repo_index: None,
        repo_sync_runtime: None,
        parallel_phase2_config: None,
        parallel_roa_worker_pool: None,
        ccr_accumulator: None,
        persist_vcir: true,
        enable_roa_validation_cache: true,
        enable_child_certificate_validation_cache: false,
        publication_point_cache_observe_only: false,
        enable_publication_point_validation_cache: false,
    };
    let second = second_runner
        .run_publication_point(&handle)
        .expect("second cache-enabled run");

    assert_eq!(second.objects.vrps, first.objects.vrps);
    assert_eq!(second.objects.roa_cache_stats.enabled_publication_points, 1);
    assert_eq!(
        second.objects.roa_cache_stats.vcir_hit_publication_points,
        1
    );
    assert_eq!(
        second.objects.roa_cache_stats.vcir_miss_publication_points,
        0
    );
    assert!(second.objects.roa_cache_stats.hit_roas > 1);
    assert_eq!(second.objects.roa_cache_stats.miss_roas, 0);
    assert_eq!(second.objects.roa_cache_stats.blocked_roas, 0);
    assert_eq!(second.objects.roa_cache_stats.fresh_roas, 0);
}

#[test]
fn runner_publication_point_cache_observe_and_reuse_path() {
    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: CaCertificateRef::inline_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,
        rrdp_notification_uri: None,
    };

    let first_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()),
        current_repo_index: None,
        repo_sync_runtime: None,
        parallel_phase2_config: None,
        parallel_roa_worker_pool: None,
        ccr_accumulator: None,
        persist_vcir: true,
        enable_roa_validation_cache: false,
        enable_child_certificate_validation_cache: false,
        publication_point_cache_observe_only: true,
        enable_publication_point_validation_cache: false,
    };
    let first = first_runner
        .run_publication_point(&handle)
        .expect("first fresh run");
    assert_eq!(first.source, PublicationPointSource::Fresh);
    assert!(
        store
            .get_publication_point_cache_projection(&manifest_rsync_uri)
            .expect("load publication-point projection")
            .is_some()
    );

    let timing = crate::analysis::timing::TimingHandle::new(crate::analysis::timing::TimingMeta {
        recorded_at_utc_rfc3339: "2026-01-01T00:00:00Z".to_string(),
        validation_time_utc_rfc3339: "2026-01-01T00:00:00Z".to_string(),
        tal_url: None,
        db_path: None,
    });
    let observe_runner = Rpkiv1PublicationPointRunner {
        store: &store,
        policy: &policy,
        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: false,
        rrdp_repo_cache: Mutex::new(HashMap::new()),
        rsync_dedup: false,
        rsync_repo_cache: Mutex::new(HashMap::new()),
        current_repo_index: None,
        repo_sync_runtime: None,
        parallel_phase2_config: None,
        parallel_roa_worker_pool: None,
        ccr_accumulator: None,
        persist_vcir: true,
        enable_roa_validation_cache: false,
        enable_child_certificate_validation_cache: false,
        publication_point_cache_observe_only: true,
        enable_publication_point_validation_cache: false,
    };
    let observed = observe_runner
        .run_publication_point(&handle)
        .expect("observe-only run");
    assert_eq!(observed.source, PublicationPointSource::Fresh);
    assert_eq!(observed.objects.vrps, first.objects.vrps);
    assert_eq!(
        timing
            .counts_snapshot()
            .get("publication_point_cache_theoretical_hits")
            .copied(),
        Some(1)
    );

    let cache_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()),
        current_repo_index: None,
        repo_sync_runtime: None,
        parallel_phase2_config: None,
        parallel_roa_worker_pool: None,
        ccr_accumulator: None,
        persist_vcir: true,
        enable_roa_validation_cache: false,
        enable_child_certificate_validation_cache: false,
        publication_point_cache_observe_only: false,
        enable_publication_point_validation_cache: true,
    };
    let cached = cache_runner
        .run_publication_point(&handle)
        .expect("publication-point cache run");
    assert_eq!(cached.source, PublicationPointSource::PublicationPointCache);
    assert_eq!(cached.objects.vrps, first.objects.vrps);
    assert_eq!(cached.objects.aspas, first.objects.aspas);
    assert_eq!(
        cached.discovered_children.len(),
        first.discovered_children.len()
    );
    assert!(!cached.cir_cached_objects.is_empty());
}

fn seed_publication_point_cache_projection(
    store: &RocksStore,
    policy: &Policy,
    ca: &CaInstanceHandle,
    validation_time: time::OffsetDateTime,
) -> ValidatedCaInstanceResult {
    let child_bytes = b"child-cert".to_vec();
    let child_hash = sha256_hex(&child_bytes);
    let vcir = sample_vcir_for_projection(validation_time, &child_hash);
    store
        .put_blob_bytes_batch(&[
            (child_hash, child_bytes),
            (sha256_hex(b"manifest-bytes"), b"manifest-bytes".to_vec()),
        ])
        .expect("put cache bytes");
    store
        .put_repository_view_entry(&RepositoryViewEntry {
            rsync_uri: ca.manifest_rsync_uri.clone(),
            current_hash: Some(sha256_hex(b"manifest-bytes")),
            repository_source: Some(ca.publication_point_rsync_uri.clone()),
            object_type: Some("mft".to_string()),
            state: RepositoryViewState::Present,
        })
        .expect("put manifest current view");
    let projection = PublicationPointCacheProjection::from_vcir_with_context(
        &vcir,
        ca.publication_point_rsync_uri.clone(),
        ca.ca_certificate_rsync_uri.clone(),
        ca.ca_certificate_sha256_32().unwrap(),
        sha256_32(b"manifest-bytes"),
        ta_context_digest_for_ca(ca),
        ca_validation_context_digest_for_ca(ca),
        publication_point_cache_policy_fingerprint(policy),
    )
    .expect("build publication point projection");
    store
        .put_vcir_with_publication_point_cache_projection(&vcir, Some(&projection))
        .expect("put publication point projection");
    vcir
}

#[test]
fn publication_point_cache_future_notbefore_guard_detects_future_roa_notbefore_only() {
    let uri = "rsync://example.test/repo/issuer/future.roa";
    let bytes = std::fs::read(
        std::path::PathBuf::from(env!("CARGO_MANIFEST_DIR"))
            .join("tests/fixtures/repository/rpki.cernet.net/repo/cernet/0/AS4538.roa"),
    )
    .expect("read ROA fixture");
    let roa = RoaObject::decode_der(&bytes).expect("decode ROA fixture");
    let ee = &roa.signed_object.signed_data.certificates[0].resource_cert;
    let file = PackFile::from_bytes_compute_sha256(uri, bytes);
    let pack = dummy_pack_with_files(vec![file]);
    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: Default::default(),
        audit: vec![ObjectAuditEntry {
            rsync_uri: uri.to_string(),
            sha256_hex: sha256_hex_from_32(&pack.files[0].sha256),
            kind: AuditObjectKind::Roa,
            result: AuditObjectResult::Error,
            detail: Some(
                "EE certificate path validation failed: certificate not valid at validation_time"
                    .to_string(),
            ),
        }],
        roa_cache_stats: Default::default(),
        roa_cache_object_meta: Vec::new(),
    };

    assert!(publication_point_cache_has_future_not_before_risk(
        &pack,
        &objects,
        &[],
        ee.tbs.validity_not_before - time::Duration::seconds(1),
        &Policy::default(),
    ));
    assert!(!publication_point_cache_has_future_not_before_risk(
        &pack,
        &objects,
        &[],
        ee.tbs.validity_not_after + time::Duration::seconds(1),
        &Policy::default(),
    ));
}

#[test]
fn publication_point_cache_delete_action_removes_existing_projection() {
    let store_dir = tempfile::tempdir().expect("store dir");
    let store = RocksStore::open(store_dir.path()).expect("open rocksdb");
    let policy = Policy::default();
    let validation_time = time::OffsetDateTime::UNIX_EPOCH + time::Duration::minutes(1);
    let ca = publication_point_cache_fixture_ca();
    let vcir = seed_publication_point_cache_projection(&store, &policy, &ca, validation_time);

    assert!(
        store
            .get_publication_point_cache_projection_cached(&ca.manifest_rsync_uri)
            .expect("load projection")
            .is_some()
    );

    store
        .replace_vcir_manifest_replay_meta_and_projection_action(
            &vcir,
            None,
            PublicationPointCacheProjectionWriteAction::Delete {
                manifest_rsync_uri: &vcir.manifest_rsync_uri,
            },
        )
        .expect("delete projection");

    assert!(
        store
            .get_publication_point_cache_projection_cached(&ca.manifest_rsync_uri)
            .expect("load projection after delete")
            .is_none()
    );
}

fn publication_point_cache_fixture_ca() -> CaInstanceHandle {
    CaInstanceHandle {
        depth: 1,
        tal_id: "test-tal".to_string(),
        parent_manifest_rsync_uri: None,
        ca_certificate: CaCertificateRef::inline_der(b"ca-cert".to_vec()),
        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: "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: Some("https://example.test/notification.xml".to_string()),
    }
}

#[test]
fn runner_publication_point_cache_reuses_projection_outputs_children_and_ccr() {
    let store_dir = tempfile::tempdir().expect("store dir");
    let store = RocksStore::open(store_dir.path()).expect("open rocksdb");
    let policy = Policy::default();
    let validation_time = time::OffsetDateTime::UNIX_EPOCH + time::Duration::minutes(1);
    let ca = publication_point_cache_fixture_ca();
    let vcir = seed_publication_point_cache_projection(&store, &policy, &ca, validation_time);
    let timing = crate::analysis::timing::TimingHandle::new(crate::analysis::timing::TimingMeta {
        recorded_at_utc_rfc3339: "2026-01-01T00:00:00Z".to_string(),
        validation_time_utc_rfc3339: "2026-01-01T00:00:00Z".to_string(),
        tal_url: None,
        db_path: None,
    });
    let runner = Rpkiv1PublicationPointRunner {
        store: &store,
        policy: &policy,
        http_fetcher: &NeverHttpFetcher,
        rsync_fetcher: &FailingRsyncFetcher,
        validation_time,
        timing: Some(timing.clone()),
        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()),
        current_repo_index: None,
        repo_sync_runtime: None,
        parallel_phase2_config: None,
        parallel_roa_worker_pool: None,
        ccr_accumulator: Some(Mutex::new(CcrAccumulator::new(Vec::new()))),
        persist_vcir: true,
        enable_roa_validation_cache: false,
        enable_child_certificate_validation_cache: false,
        publication_point_cache_observe_only: false,
        enable_publication_point_validation_cache: true,
    };

    let result = runner
        .observe_or_reuse_publication_point_cache(&ca, Some("rrdp"), Some("delta"), 7, None, &[])
        .expect("cache result");

    assert_eq!(result.source, PublicationPointSource::PublicationPointCache);
    assert_eq!(result.objects.vrps.len(), 1);
    assert_eq!(result.objects.aspas.len(), 1);
    assert_eq!(result.objects.router_keys.len(), 1);
    assert_eq!(result.discovered_children.len(), 1);
    assert_eq!(
        result.discovered_children[0].handle.manifest_rsync_uri,
        vcir.child_entries[0].child_manifest_rsync_uri
    );
    assert!(result.cir_fresh_objects.is_empty());
    assert!(!result.cir_cached_objects.is_empty());
    assert_eq!(
        runner
            .ccr_accumulator_snapshot()
            .expect("ccr accumulator")
            .manifest_count(),
        1
    );
    let counts = timing.counts_snapshot();
    assert_eq!(
        counts.get("publication_point_cache_reuse_hits").copied(),
        Some(1)
    );
    assert_eq!(
        counts
            .get("publication_point_cache_outputs_reused")
            .copied(),
        Some(3)
    );
    assert_eq!(
        counts
            .get("publication_point_cache_children_reused")
            .copied(),
        Some(1)
    );
    assert_eq!(
        counts
            .get("publication_point_cache_related_objects_reused")
            .copied(),
        Some(vcir.related_artifacts.len() as u64)
    );
    assert_eq!(
        counts
            .get("publication_point_cache_audit_objects_reused")
            .copied(),
        Some(result.cir_cached_objects.len() as u64)
    );
    let timing_dir = tempfile::tempdir().expect("timing dir");
    let timing_path = timing_dir.path().join("timing.json");
    timing.write_json(&timing_path, 20).expect("write timing");
    let timing_json: serde_json::Value =
        serde_json::from_slice(&std::fs::read(&timing_path).expect("read timing"))
            .expect("parse timing");
    let phase_keys = timing_json["phases"]
        .as_object()
        .expect("phases")
        .keys()
        .map(|key| key.as_str())
        .collect::<std::collections::HashSet<_>>();
    assert!(phase_keys.contains("publication_point_cache_lookup_hit_total"));
    assert!(phase_keys.contains("publication_point_cache_reuse_build_total"));
    assert!(phase_keys.contains("publication_point_cache_build_objects_total"));
}

#[test]
fn publication_point_cache_restore_children_parallel_keeps_order_and_audit() {
    let store_dir = tempfile::tempdir().expect("store dir");
    let store = RocksStore::open(store_dir.path()).expect("open rocksdb");
    let policy = Policy::default();
    let validation_time = time::OffsetDateTime::UNIX_EPOCH + time::Duration::minutes(1);
    let ca = publication_point_cache_fixture_ca();
    seed_publication_point_cache_projection(&store, &policy, &ca, validation_time);
    let mut projection = store
        .get_publication_point_cache_projection(&ca.manifest_rsync_uri)
        .expect("load projection")
        .expect("projection");
    let template = projection.children[0].clone();
    let mut blobs = Vec::new();
    let mut children = Vec::new();
    for index in 0..300 {
        let bytes = format!("child-cert-{index}").into_bytes();
        let child_hash = sha256_hex(&bytes);
        let mut child = template.clone();
        child.child_cert_hash = child_hash.clone();
        child.child_cert_rsync_uri = format!("rsync://example.test/repo/issuer/child-{index}.cer");
        child.child_manifest_rsync_uri =
            format!("rsync://example.test/repo/child-{index}/child.mft");
        child.child_publication_point_rsync_uri =
            format!("rsync://example.test/repo/child-{index}/");
        child.child_rsync_base_uri = child.child_publication_point_rsync_uri.clone();
        blobs.push((child_hash, bytes));
        children.push(child);
    }
    projection.children = children;
    store.put_blob_bytes_batch(&blobs).expect("put child blobs");
    let timing = crate::analysis::timing::TimingHandle::new(crate::analysis::timing::TimingMeta {
        recorded_at_utc_rfc3339: "2026-01-01T00:00:00Z".to_string(),
        validation_time_utc_rfc3339: "2026-01-01T00:00:00Z".to_string(),
        tal_url: None,
        db_path: None,
    });
    let mut warnings = Vec::new();

    let (restored_children, audits) = restore_children_from_publication_point_cache(
        &store,
        &ca,
        &projection,
        validation_time,
        &mut warnings,
        4,
        Some(&timing),
    );

    assert!(warnings.is_empty());
    assert_eq!(restored_children.len(), 300);
    assert_eq!(audits.len(), 300);
    assert_eq!(
        restored_children[0].handle.ca_certificate_sha256_hex(),
        Some(sha256_hex(b"child-cert-0").as_str())
    );
    assert_eq!(
        restored_children[299]
            .handle
            .ca_certificate_der(&store)
            .unwrap()
            .as_ref(),
        b"child-cert-299"
    );
    assert_eq!(
        restored_children[0].handle.manifest_rsync_uri,
        "rsync://example.test/repo/child-0/child.mft"
    );
    assert!(
        audits
            .iter()
            .all(|audit| audit.result == AuditObjectResult::Ok)
    );
    let counts = timing.counts_snapshot();
    assert_eq!(
        counts
            .get("publication_point_cache_restore_children_parallel_publication_points")
            .copied(),
        Some(1)
    );
    assert_eq!(
        counts
            .get("publication_point_cache_restore_children_parallel_children")
            .copied(),
        Some(300)
    );
    assert_eq!(
        counts
            .get("publication_point_cache_restore_children_workers_total")
            .copied(),
        Some(4)
    );
}

#[test]
fn publication_point_cache_restore_children_does_not_require_child_der_bytes() {
    let store_dir = tempfile::tempdir().expect("store dir");
    let store = RocksStore::open(store_dir.path()).expect("open rocksdb");
    let policy = Policy::default();
    let validation_time = time::OffsetDateTime::UNIX_EPOCH + time::Duration::minutes(1);
    let ca = publication_point_cache_fixture_ca();
    seed_publication_point_cache_projection(&store, &policy, &ca, validation_time);
    let mut projection = store
        .get_publication_point_cache_projection(&ca.manifest_rsync_uri)
        .expect("load projection")
        .expect("projection");
    projection.children[0].child_cert_hash = "22".repeat(32);
    let mut warnings = Vec::new();

    let (restored_children, audits) = restore_children_from_publication_point_cache(
        &store,
        &ca,
        &projection,
        validation_time,
        &mut warnings,
        1,
        None,
    );

    assert!(warnings.is_empty());
    assert!(!restored_children.is_empty());
    assert_eq!(audits.len(), restored_children.len());
    assert_eq!(
        restored_children[0].handle.ca_certificate_sha256_hex(),
        Some(projection.children[0].child_cert_hash.as_str())
    );
    assert!(
        restored_children[0]
            .handle
            .ca_certificate_der(&store)
            .is_err(),
        "lazy child handle should not require repo bytes until a fresh path asks for DER"
    );
}

#[test]
fn runner_publication_point_cache_blocks_parent_policy_and_output_time_mismatch() {
    let store_dir = tempfile::tempdir().expect("store dir");
    let store = RocksStore::open(store_dir.path()).expect("open rocksdb");
    let policy = Policy::default();
    let validation_time = time::OffsetDateTime::UNIX_EPOCH + time::Duration::minutes(1);
    let ca = publication_point_cache_fixture_ca();
    seed_publication_point_cache_projection(&store, &policy, &ca, validation_time);

    let timing = crate::analysis::timing::TimingHandle::new(crate::analysis::timing::TimingMeta {
        recorded_at_utc_rfc3339: "2026-01-01T00:00:00Z".to_string(),
        validation_time_utc_rfc3339: "2026-01-01T00:00:00Z".to_string(),
        tal_url: None,
        db_path: None,
    });
    let mut parent_changed_ca = ca.clone();
    parent_changed_ca.parent_manifest_rsync_uri =
        Some("rsync://example.test/repo/other-parent.mft".to_string());
    let parent_changed_runner = Rpkiv1PublicationPointRunner {
        store: &store,
        policy: &policy,
        http_fetcher: &NeverHttpFetcher,
        rsync_fetcher: &FailingRsyncFetcher,
        validation_time,
        timing: Some(timing.clone()),
        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()),
        current_repo_index: None,
        repo_sync_runtime: None,
        parallel_phase2_config: None,
        parallel_roa_worker_pool: None,
        ccr_accumulator: None,
        persist_vcir: true,
        enable_roa_validation_cache: false,
        enable_child_certificate_validation_cache: false,
        publication_point_cache_observe_only: false,
        enable_publication_point_validation_cache: true,
    };
    assert!(
        parent_changed_runner
            .observe_or_reuse_publication_point_cache(
                &parent_changed_ca,
                Some("rrdp"),
                Some("delta"),
                7,
                None,
                &[]
            )
            .is_none()
    );
    assert_eq!(
        timing
            .counts_snapshot()
            .get("publication_point_cache_miss_parent_context_mismatch")
            .copied(),
        Some(1)
    );

    let strict_policy = Policy {
        strict: crate::policy::StrictPolicy::all(),
        ..Policy::default()
    };
    let policy_changed_runner = Rpkiv1PublicationPointRunner {
        store: &store,
        policy: &strict_policy,
        http_fetcher: &NeverHttpFetcher,
        rsync_fetcher: &FailingRsyncFetcher,
        validation_time,
        timing: Some(timing.clone()),
        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()),
        current_repo_index: None,
        repo_sync_runtime: None,
        parallel_phase2_config: None,
        parallel_roa_worker_pool: None,
        ccr_accumulator: None,
        persist_vcir: true,
        enable_roa_validation_cache: false,
        enable_child_certificate_validation_cache: false,
        publication_point_cache_observe_only: false,
        enable_publication_point_validation_cache: true,
    };
    assert!(
        policy_changed_runner
            .observe_or_reuse_publication_point_cache(
                &ca,
                Some("rrdp"),
                Some("delta"),
                7,
                None,
                &[]
            )
            .is_none()
    );
    assert_eq!(
        timing
            .counts_snapshot()
            .get("publication_point_cache_miss_policy_mismatch")
            .copied(),
        Some(1)
    );

    let output_expired_runner = Rpkiv1PublicationPointRunner {
        store: &store,
        policy: &policy,
        http_fetcher: &NeverHttpFetcher,
        rsync_fetcher: &FailingRsyncFetcher,
        validation_time: validation_time + time::Duration::minutes(40),
        timing: Some(timing.clone()),
        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()),
        current_repo_index: None,
        repo_sync_runtime: None,
        parallel_phase2_config: None,
        parallel_roa_worker_pool: None,
        ccr_accumulator: None,
        persist_vcir: true,
        enable_roa_validation_cache: false,
        enable_child_certificate_validation_cache: false,
        publication_point_cache_observe_only: false,
        enable_publication_point_validation_cache: true,
    };
    assert!(
        output_expired_runner
            .observe_or_reuse_publication_point_cache(
                &ca,
                Some("rrdp"),
                Some("delta"),
                7,
                None,
                &[]
            )
            .is_none()
    );
    assert_eq!(
        timing
            .counts_snapshot()
            .get("publication_point_cache_miss_output_time_gate")
            .copied(),
        Some(1)
    );
}

#[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: CaCertificateRef::inline_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()),
        current_repo_index: None,
        repo_sync_runtime: None,
        parallel_phase2_config: None,
        parallel_roa_worker_pool: None,
        ccr_accumulator: None,
        persist_vcir: true,
        enable_roa_validation_cache: false,
        enable_child_certificate_validation_cache: false,
        publication_point_cache_observe_only: false,
        enable_publication_point_validation_cache: false,
    };

    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: CaCertificateRef::inline_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()),
        current_repo_index: None,
        repo_sync_runtime: None,
        parallel_phase2_config: None,
        parallel_roa_worker_pool: None,
        ccr_accumulator: None,
        persist_vcir: true,
        enable_roa_validation_cache: false,
        enable_child_certificate_validation_cache: false,
        publication_point_cache_observe_only: false,
        enable_publication_point_validation_cache: false,
    };

    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: CaCertificateRef::inline_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()),
        current_repo_index: None,
        repo_sync_runtime: None,
        parallel_phase2_config: None,
        parallel_roa_worker_pool: None,
        ccr_accumulator: None,
        persist_vcir: true,
        enable_roa_validation_cache: false,
        enable_child_certificate_validation_cache: false,
        publication_point_cache_observe_only: false,
        enable_publication_point_validation_cache: false,
    };

    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: CaCertificateRef::inline_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()),
        current_repo_index: None,
        repo_sync_runtime: None,
        parallel_phase2_config: None,
        parallel_roa_worker_pool: None,
        ccr_accumulator: None,
        persist_vcir: true,
        enable_roa_validation_cache: false,
        enable_child_certificate_validation_cache: false,
        publication_point_cache_observe_only: false,
        enable_publication_point_validation_cache: false,
    };
    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()),
        current_repo_index: None,
        repo_sync_runtime: None,
        parallel_phase2_config: None,
        parallel_roa_worker_pool: None,
        ccr_accumulator: None,
        persist_vcir: true,
        enable_roa_validation_cache: false,
        enable_child_certificate_validation_cache: false,
        publication_point_cache_observe_only: false,
        enable_publication_point_validation_cache: false,
    };
    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"
    );

    // Verification-only replays have the frozen repository bytes available,
    // so a fresh read would succeed and never naturally enter the fallback
    // branch.  A contract-selected URI must therefore select the same VCIR
    // projection without attempting fresh manifest/object validation.
    let mut forced_policy = policy.clone();
    forced_policy
        .verification_forced_vcir_reuse_manifest_uris
        .insert(manifest_rsync_uri.clone());
    let forced_runner = Rpkiv1PublicationPointRunner {
        store: &store,
        policy: &forced_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()),
        current_repo_index: None,
        repo_sync_runtime: None,
        parallel_phase2_config: None,
        parallel_roa_worker_pool: None,
        ccr_accumulator: None,
        persist_vcir: true,
        enable_roa_validation_cache: false,
        enable_child_certificate_validation_cache: false,
        publication_point_cache_observe_only: false,
        enable_publication_point_validation_cache: false,
    };
    let forced = forced_runner
        .run_publication_point(&handle)
        .expect("contract-selected fallback should reuse current-instance VCIR");
    assert_eq!(forced.source, PublicationPointSource::VcirCurrentInstance);
    assert!(forced.discovered_children.is_empty());
}

#[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: CaCertificateRef::inline_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(),
        roa_cache_stats: crate::validation::objects::RoaValidationCacheStats::default(),
        roa_cache_object_meta: Vec::new(),
    };

    let audit = build_publication_point_audit_from_snapshot(
        &ca,
        pp.source,
        None,
        None,
        None,
        None,
        &pp.snapshot,
        &[],
        &objects,
        &[],
    );
    assert_eq!(audit.source, "vcir_current_instance");
    assert_eq!(audit.repo_sync_phase, None);
    assert_eq!(audit.repo_terminal_state, "fallback_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: CaCertificateRef::inline_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,
        }],
        roa_cache_stats: crate::validation::objects::RoaValidationCacheStats::default(),
        roa_cache_object_meta: Vec::new(),
    };

    let audit = build_publication_point_audit_from_snapshot(
        &issuer,
        pp.source,
        Some("rsync"),
        Some("rsync_only_ok"),
        Some(123),
        Some("none"),
        &pp.snapshot,
        &[],
        &objects,
        &[],
    );
    assert_eq!(audit.objects[0].kind, AuditObjectKind::Manifest);
    assert_eq!(audit.repo_sync_source.as_deref(), Some("rsync"));
    assert_eq!(audit.repo_sync_phase.as_deref(), Some("rsync_only_ok"));
    assert_eq!(audit.repo_sync_duration_ms, Some(123));
    assert_eq!(audit.repo_sync_error.as_deref(), Some("none"));
    assert_eq!(audit.repo_terminal_state, "fresh");

    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: CaCertificateRef::inline_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: CaCertificateRef::inline_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: CaCertificateRef::inline_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: CaCertificateRef::inline_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 {
            bytes: g.issuer_crl_der.clone(),
            sha256_hex: None,
        },
    );
    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 {
            bytes: g.issuer_crl_der,
            sha256_hex: None,
        },
    );
    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 {
            bytes: g.issuer_crl_der.clone(),
            sha256_hex: None,
        },
    );

    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: CaCertificateRef::inline_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 main_db = store_dir.path().join("work-db");
    let repo_bytes_db = store_dir.path().join("repo-bytes.db");
    let store = RocksStore::open_with_external_repo_bytes(&main_db, &repo_bytes_db)
        .expect("open rocksdb with external repo bytes");
    store
        .put_blob_bytes_batch(&[(child_cert_hash.clone(), g.child_ca_der.clone())])
        .expect("put child cert repo bytes");
    assert!(
        store
            .get_raw_by_hash_entry(&child_cert_hash)
            .expect("lookup child raw_by_hash")
            .is_none(),
        "child cert restoration should not require raw_by_hash entries"
    );

    let ca = CaInstanceHandle {
        depth: 0,
        tal_id: "test-tal".to_string(),
        parent_manifest_rsync_uri: None,
        ca_certificate: CaCertificateRef::inline_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 policy = Policy::default();
    put_vcir_for_failed_fetch_reuse(&store, &ca, &policy, &vcir);

    let projection = project_current_instance_vcir_on_failed_fetch(
        &store,
        &ca,
        &ManifestFreshError::RepoSyncFailed {
            detail: "synthetic".to_string(),
        },
        &policy,
        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_eq!(
        projection.ccr_manifest_projection.as_ref(),
        Some(&vcir.ccr_manifest_projection)
    );
    assert!(
        projection.snapshot.is_none(),
        "current-instance reuse should not reconstruct a byte-backed snapshot"
    );
    assert!(
        !projection
            .warnings
            .iter()
            .any(|warning| warning.message.contains("manifest failed fetch")),
        "successful current-instance reuse should not duplicate the fresh fetch error"
    );
    assert!(
        !projection
            .warnings
            .iter()
            .any(|warning| warning.message.contains("using latest validated result")),
        "successful current-instance reuse should be tracked by source, not warning"
    );
    assert!(
        !projection
            .warnings
            .iter()
            .any(|warning| warning.message.contains("manifest raw bytes missing")),
        "successful current-instance reuse should not load repo bytes for audit reconstruction"
    );
    assert!(
        !projection
            .warnings
            .iter()
            .any(|warning| warning.message.contains("child certificate bytes missing")),
        "child discovery restoration should read child certs from repo bytes"
    );
}

#[test]
fn project_current_instance_vcir_does_not_reuse_without_identity() {
    let now = time::OffsetDateTime::now_utc();
    let child_cert_hash = sha256_hex(b"child-cert");
    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 legacy-style VCIR");
    let ca = sample_ca_for_failed_fetch_reuse(&vcir);

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

    assert_eq!(
        projection.source,
        PublicationPointSource::FailedFetchNoCache
    );
    assert!(projection.objects.vrps.is_empty());
    assert!(projection.discovered_children.is_empty());
    assert!(
        projection
            .warnings
            .iter()
            .any(|warning| warning.message.contains("reuse identity is missing"))
    );
}

#[test]
fn project_current_instance_vcir_does_not_reuse_when_ta_context_changes() {
    let now = time::OffsetDateTime::now_utc();
    let child_cert_hash = sha256_hex(b"child-cert");
    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");
    let policy = Policy::default();
    let original_ca = sample_ca_for_failed_fetch_reuse(&vcir);
    put_vcir_for_failed_fetch_reuse(&store, &original_ca, &policy, &vcir);
    let mut changed_ca = original_ca;
    changed_ca.tal_id = "different-tal".to_string();

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

    assert_eq!(
        projection.source,
        PublicationPointSource::FailedFetchNoCache
    );
    assert!(projection.objects.vrps.is_empty());
    assert!(projection.discovered_children.is_empty());
    assert!(
        projection
            .warnings
            .iter()
            .any(|warning| warning.message.contains("identity does not match"))
    );
}

#[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: CaCertificateRef::inline_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(),
        },
        &Policy::default(),
        now,
    )
    .expect("project vcir");

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

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

    let store_dir = tempfile::tempdir().expect("store dir");
    let main_db = store_dir.path().join("work-db");
    let repo_bytes_db = store_dir.path().join("repo-bytes.db");
    let store = RocksStore::open_with_external_repo_bytes(&main_db, &repo_bytes_db)
        .expect("open rocksdb with external repo bytes");
    store
        .put_blob_bytes_batch(&[(child_cert_hash, b"child-cert".to_vec())])
        .expect("put child cert repo bytes");

    let ca = CaInstanceHandle {
        depth: 0,
        tal_id: "test-tal".to_string(),
        parent_manifest_rsync_uri: None,
        ca_certificate: CaCertificateRef::inline_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 policy = Policy::default();
    put_vcir_for_failed_fetch_reuse(&store, &ca, &policy, &vcir);

    let projection = project_current_instance_vcir_on_failed_fetch(
        &store,
        &ca,
        &ManifestFreshError::HashMismatch {
            rsync_uri: "rsync://example.test/repo/issuer/a.roa".to_string(),
        },
        &policy,
        now,
    )
    .expect("project vcir");

    assert_eq!(
        projection.source,
        PublicationPointSource::VcirCurrentInstance
    );
    assert!(
        projection
            .warnings
            .iter()
            .any(|warning| { warning.message.contains("manifest file hash mismatch") })
    );
    assert!(
        !projection
            .warnings
            .iter()
            .any(|warning| warning.message.contains("using latest validated result")),
        "successful current-instance reuse should not emit bookkeeping warnings"
    );
}

#[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: CaCertificateRef::inline_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(),
        },
        &Policy::default(),
        now,
    )
    .expect("project without cached vcir");

    assert_eq!(
        projection.source,
        PublicationPointSource::FailedFetchNoCache
    );
    assert!(projection.vcir.is_none());
    assert!(projection.ccr_manifest_projection.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: CaCertificateRef::inline_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(),
        },
        &Policy::default(),
        now,
    )
    .expect("project ineligible vcir");

    assert_eq!(
        projection.source,
        PublicationPointSource::FailedFetchNoCache
    );
    assert!(projection.vcir.is_some());
    assert!(projection.ccr_manifest_projection.is_none());
    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 project_current_instance_vcir_rejects_mismatched_ccr_projection_uri() {
    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.ccr_manifest_projection.manifest_rsync_uri =
        "rsync://example.test/repo/issuer/other.mft".to_string();

    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: CaCertificateRef::inline_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 policy = Policy::default();
    put_vcir_for_failed_fetch_reuse(&store, &ca, &policy, &vcir);

    let err = project_current_instance_vcir_on_failed_fetch(
        &store,
        &ca,
        &ManifestFreshError::RepoSyncFailed {
            detail: "synthetic".to_string(),
        },
        &policy,
        now,
    )
    .unwrap_err();

    assert!(
        err.contains("vcir CCR manifest projection URI mismatch"),
        "{err}"
    );
}

#[test]
fn fresh_and_reuse_paths_produce_equivalent_ccr_manifest_projection() {
    let (pack, issuer_ca_der, validation_time) = cernet_publication_point_snapshot_for_vcir_tests();
    let ca = CaInstanceHandle {
            depth: 0,
            tal_id: "test-tal".to_string(),
            parent_manifest_rsync_uri: None,
            ca_certificate: CaCertificateRef::inline_der(issuer_ca_der),
            ca_certificate_rsync_uri: Some("rsync://rpki.apnic.net/repository/B527EF581D6611E2BB468F7C72FD1FF2/BfycW4hQb3wNP4YsiJW-1n6fjro.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 child_discovery =
        discover_children_from_fresh_snapshot_with_audit(&ca, &pack, validation_time, None)
            .expect("discover children");
    let mut objects = empty_objects_output();
    let store_dir = tempfile::tempdir().expect("store dir");
    let store = RocksStore::open(store_dir.path()).expect("open rocksdb");
    let (fresh_vcir, _timing) = build_vcir_from_fresh_result_with_timing(
        &store,
        &ca,
        &pack,
        &mut objects,
        &[],
        &child_discovery.audits,
        &child_discovery.children,
        validation_time,
    )
    .expect("build fresh vcir");

    let reuse_projection = reuse_ccr_manifest_projection_from_vcir(&ca, &fresh_vcir)
        .expect("reuse projection from vcir");

    assert_eq!(fresh_vcir.ccr_manifest_projection, reuse_projection);
}

#[test]
fn append_ccr_manifest_projection_from_reuse_requires_projection_for_current_instance() {
    let store_dir = tempfile::tempdir().expect("store dir");
    let store = RocksStore::open(store_dir.path()).expect("open rocksdb");
    let policy = Policy::default();
    let runner = sample_runner_with_ccr_accumulator(&store, &policy);

    let err = runner
        .append_ccr_manifest_projection_from_reuse(&VcirReuseProjection {
            source: PublicationPointSource::VcirCurrentInstance,
            vcir: None,
            ccr_manifest_projection: None,
            snapshot: None,
            objects: empty_objects_output(),
            child_audits: Vec::new(),
            discovered_children: Vec::new(),
            warnings: Vec::new(),
        })
        .unwrap_err();

    assert!(err.contains("missing CCR manifest projection"), "{err}");
    assert_eq!(
        runner
            .ccr_accumulator_snapshot()
            .expect("ccr accumulator snapshot")
            .manifest_count(),
        0
    );
}

#[test]
fn append_ccr_manifest_projection_from_reuse_skips_failed_fetch_no_cache() {
    let store_dir = tempfile::tempdir().expect("store dir");
    let store = RocksStore::open(store_dir.path()).expect("open rocksdb");
    let policy = Policy::default();
    let runner = sample_runner_with_ccr_accumulator(&store, &policy);

    runner
        .append_ccr_manifest_projection_from_reuse(&VcirReuseProjection {
            source: PublicationPointSource::FailedFetchNoCache,
            vcir: None,
            ccr_manifest_projection: None,
            snapshot: None,
            objects: empty_objects_output(),
            child_audits: Vec::new(),
            discovered_children: Vec::new(),
            warnings: Vec::new(),
        })
        .expect("failed-fetch no-cache should not append");

    assert_eq!(
        runner
            .ccr_accumulator_snapshot()
            .expect("ccr accumulator snapshot")
            .manifest_count(),
        0
    );
}

#[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 runner_roa_validation_cache_uses_projection_not_full_vcir_fallback() {
    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.local_outputs
        .retain(|output| output.source_object_type != VcirSourceObjectType::Roa);
    vcir.summary.local_vrp_count = 0;
    vcir.summary.local_aspa_count = 1;
    vcir.summary.local_router_key_count = 1;

    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 without projection");
    assert!(
        store
            .get_vcir(&vcir.manifest_rsync_uri)
            .expect("get vcir")
            .is_some()
    );
    assert!(
        store
            .get_roa_cache_projection(&vcir.manifest_rsync_uri)
            .expect("get projection")
            .is_none()
    );

    let timing = crate::analysis::timing::TimingHandle::new(crate::analysis::timing::TimingMeta {
        recorded_at_utc_rfc3339: "2026-06-07T00:00:00Z".to_string(),
        validation_time_utc_rfc3339: "2026-06-07T00:00:00Z".to_string(),
        tal_url: None,
        db_path: None,
    });
    let policy = Policy::default();
    let runner = Rpkiv1PublicationPointRunner {
        store: &store,
        policy: &policy,
        http_fetcher: &NeverHttpFetcher,
        rsync_fetcher: &FailingRsyncFetcher,
        validation_time: now,
        timing: Some(timing.clone()),
        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()),
        current_repo_index: None,
        repo_sync_runtime: None,
        parallel_phase2_config: None,
        parallel_roa_worker_pool: None,
        ccr_accumulator: None,
        persist_vcir: true,
        enable_roa_validation_cache: true,
        enable_child_certificate_validation_cache: false,
        publication_point_cache_observe_only: false,
        enable_publication_point_validation_cache: false,
    };

    assert!(
        runner
            .roa_validation_cache_view_for_fresh_point(&vcir.manifest_rsync_uri)
            .is_none()
    );
    let dir = tempfile::tempdir().expect("timing dir");
    let path = dir.path().join("timing.json");
    timing.write_json(&path, 10).expect("write timing");
    let report: serde_json::Value =
        serde_json::from_slice(&std::fs::read(path).expect("read timing")).expect("parse timing");
    assert_eq!(
        report["counts"]["roa_validation_cache_projection_missing_publication_points"],
        1
    );
    assert!(
        report["phases"]["roa_validation_cache_projection_load_total"]["count"]
            .as_u64()
            .unwrap_or_default()
            >= 1
    );
}

#[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,
            reject_reason: None,
        });
    }

    vcir.local_outputs.push(VcirLocalOutput {
        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: VcirSourceObjectType::Roa,
        source_object_hash: sha256_32(b"bad-time-src"),
        source_ee_cert_hash: sha256_32(b"bad-time-ee"),
        payload: VcirLocalOutputPayload::Vrp {
            asn: 64496,
            afi: RoaAfi::Ipv4,
            prefix_len: 24,
            addr: ipv4_addr([203, 0, 113, 0]),
            max_length: 24,
        },
        rule_hash: sha256_32(b"bad-time-rule"),
    });
    vcir.local_outputs.push(VcirLocalOutput {
        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: VcirSourceObjectType::Aspa,
        source_object_hash: sha256_32(b"expired-src"),
        source_ee_cert_hash: sha256_32(b"expired-ee"),
        payload: VcirLocalOutputPayload::Aspa {
            customer_as_id: 64500,
            provider_as_ids: vec![64501],
        },
        rule_hash: sha256_32(b"expired-rule"),
    });
    vcir.local_outputs.push(VcirLocalOutput {
        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: VcirSourceObjectType::Roa,
        source_object_hash: sha256_32(b"bad-json-src"),
        source_ee_cert_hash: sha256_32(b"bad-json-ee"),
        payload: VcirLocalOutputPayload::Aspa {
            customer_as_id: 64510,
            provider_as_ids: vec![64511],
        },
        rule_hash: sha256_32(b"bad-json-rule"),
    });
    vcir.local_outputs.push(VcirLocalOutput {
        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: VcirSourceObjectType::Roa,
        source_object_hash: sha256_32(b"bad-prefix-src"),
        source_ee_cert_hash: sha256_32(b"bad-prefix-ee"),
        payload: VcirLocalOutputPayload::Aspa {
            customer_as_id: 64512,
            provider_as_ids: vec![64513],
        },
        rule_hash: sha256_32(b"bad-prefix-rule"),
    });
    vcir.local_outputs.push(VcirLocalOutput {
        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: VcirSourceObjectType::Aspa,
        source_object_hash: sha256_32(b"bad-aspa-src"),
        source_ee_cert_hash: sha256_32(b"bad-aspa-ee"),
        payload: VcirLocalOutputPayload::Vrp {
            asn: 64520,
            afi: RoaAfi::Ipv4,
            prefix_len: 24,
            addr: ipv4_addr([198, 51, 100, 0]),
            max_length: 24,
        },
        rule_hash: sha256_32(b"bad-aspa-rule"),
    });

    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: CaCertificateRef::inline_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()),
        }],
        roa_cache_stats: crate::validation::objects::RoaValidationCacheStats::default(),
        roa_cache_object_meta: Vec::new(),
    };
    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,
        Some("rsync"),
        Some("rrdp_failed_rsync_failed"),
        Some(456),
        Some("rsync failed"),
        Some(&vcir),
        None,
        &runner_warnings,
        &objects,
        &child_audits,
        &[],
    );

    assert_eq!(audit.source, "vcir_current_instance");
    assert_eq!(audit.repo_sync_source.as_deref(), Some("rsync"));
    assert_eq!(
        audit.repo_sync_phase.as_deref(),
        Some("rrdp_failed_rsync_failed")
    );
    assert_eq!(audit.repo_sync_duration_ms, Some(456));
    assert_eq!(audit.repo_sync_error.as_deref(), Some("rsync failed"));
    assert_eq!(audit.repo_terminal_state, "fallback_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_restores_reject_reason_with_legacy_fallback() {
    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.push(VcirRelatedArtifact {
        artifact_role: VcirArtifactRole::SignedObject,
        artifact_kind: VcirArtifactKind::Roa,
        uri: Some("rsync://example.test/repo/issuer/rejected-with-reason.roa".to_string()),
        sha256: sha256_hex(b"rejected-with-reason"),
        object_type: Some("roa".to_string()),
        validation_status: VcirArtifactValidationStatus::Rejected,
        reject_reason: Some("EE certificate path validation failed: test".to_string()),
    });
    vcir.related_artifacts.push(VcirRelatedArtifact {
        artifact_role: VcirArtifactRole::SignedObject,
        artifact_kind: VcirArtifactKind::Roa,
        uri: Some("rsync://example.test/repo/issuer/rejected-legacy.roa".to_string()),
        sha256: sha256_hex(b"rejected-legacy"),
        object_type: Some("roa".to_string()),
        validation_status: VcirArtifactValidationStatus::Rejected,
        reject_reason: None,
    });

    let ca = CaInstanceHandle {
        depth: 0,
        tal_id: "test-tal".to_string(),
        parent_manifest_rsync_uri: None,
        ca_certificate: CaCertificateRef::inline_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 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(),
        roa_cache_stats: crate::validation::objects::RoaValidationCacheStats::default(),
        roa_cache_object_meta: Vec::new(),
    };

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

    assert!(audit.objects.iter().any(|entry| {
        entry.rsync_uri == "rsync://example.test/repo/issuer/rejected-with-reason.roa"
            && matches!(entry.result, AuditObjectResult::Error)
            && entry.detail.as_deref() == Some("EE certificate path validation failed: test")
    }));
    assert!(audit.objects.iter().any(|entry| {
        entry.rsync_uri == "rsync://example.test/repo/issuer/rejected-legacy.roa"
            && matches!(entry.result, AuditObjectResult::Error)
            && entry.detail.as_deref() == Some(CACHED_REJECT_REASON_NOT_RECORDED)
    }));
    assert!(
        audit.objects.iter().all(|entry| {
            !matches!(entry.result, AuditObjectResult::Ok) || entry.detail.is_none()
        })
    );
}

#[test]
fn build_publication_point_audit_from_pp_cache_projection_restores_reject_reason() {
    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.push(VcirRelatedArtifact {
        artifact_role: VcirArtifactRole::SignedObject,
        artifact_kind: VcirArtifactKind::Roa,
        uri: Some("rsync://example.test/repo/issuer/rejected-with-reason.roa".to_string()),
        sha256: sha256_hex(b"rejected-with-reason"),
        object_type: Some("roa".to_string()),
        validation_status: VcirArtifactValidationStatus::Rejected,
        reject_reason: Some("EE certificate path validation failed: test".to_string()),
    });
    vcir.related_artifacts.push(VcirRelatedArtifact {
        artifact_role: VcirArtifactRole::SignedObject,
        artifact_kind: VcirArtifactKind::Roa,
        uri: Some("rsync://example.test/repo/issuer/rejected-legacy.roa".to_string()),
        sha256: sha256_hex(b"rejected-legacy"),
        object_type: Some("roa".to_string()),
        validation_status: VcirArtifactValidationStatus::Rejected,
        reject_reason: None,
    });
    let projection = PublicationPointCacheProjection::from_vcir_with_context(
        &vcir,
        "rsync://example.test/repo/issuer/".to_string(),
        None,
        [0x11; 32],
        [0x22; 32],
        [0x33; 32],
        [0x44; 32],
        [0x55; 32],
    )
    .expect("build publication point projection");

    let ca = CaInstanceHandle {
        depth: 0,
        tal_id: "test-tal".to_string(),
        parent_manifest_rsync_uri: None,
        ca_certificate: CaCertificateRef::inline_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 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(),
        roa_cache_stats: crate::validation::objects::RoaValidationCacheStats::default(),
        roa_cache_object_meta: Vec::new(),
    };

    let audit = build_publication_point_audit_from_publication_point_cache_projection(
        &ca,
        PublicationPointSource::PublicationPointCache,
        None,
        None,
        None,
        None,
        &projection,
        now,
        &[],
        &objects,
        &[],
    );

    assert!(audit.objects.iter().any(|entry| {
        entry.rsync_uri == "rsync://example.test/repo/issuer/rejected-with-reason.roa"
            && matches!(entry.result, AuditObjectResult::Error)
            && entry.detail.as_deref() == Some("EE certificate path validation failed: test")
    }));
    assert!(audit.objects.iter().any(|entry| {
        entry.rsync_uri == "rsync://example.test/repo/issuer/rejected-legacy.roa"
            && matches!(entry.result, AuditObjectResult::Error)
            && entry.detail.as_deref() == Some(CACHED_REJECT_REASON_NOT_RECORDED)
    }));
}

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

    let ca = CaInstanceHandle {
        depth: 0,
        tal_id: "test-tal".to_string(),
        parent_manifest_rsync_uri: None,
        ca_certificate: CaCertificateRef::inline_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 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: vcir.current_manifest_rsync_uri.clone(),
            sha256_hex: sha256_hex(b"current-manifest"),
            kind: AuditObjectKind::Manifest,
            result: AuditObjectResult::Error,
            detail: Some("manifest is not valid at validation_time".to_string()),
        }],
        roa_cache_stats: crate::validation::objects::RoaValidationCacheStats::default(),
        roa_cache_object_meta: Vec::new(),
    };

    let audit = build_publication_point_audit_from_vcir(
        &ca,
        PublicationPointSource::FailedFetchNoCache,
        Some("rsync"),
        Some("rsync_only_ok"),
        Some(123),
        None,
        Some(&vcir),
        None,
        &[Warning::new("latest VCIR instance_gate expired")],
        &objects,
        &[],
        &[],
    );

    assert_eq!(audit.source, "failed_fetch_no_cache");
    assert_eq!(audit.repo_terminal_state, "failed_no_cache");
    assert_eq!(
        audit.this_update_rfc3339_utc,
        vcir.validated_manifest_meta
            .validated_manifest_this_update
            .rfc3339_utc
    );
    assert_eq!(audit.objects.len(), 1);
    assert_eq!(audit.objects[0].rsync_uri, vcir.current_manifest_rsync_uri);
    assert!(matches!(audit.objects[0].result, AuditObjectResult::Error));
    assert!(
        !audit
            .objects
            .iter()
            .any(|entry| entry.rsync_uri == "rsync://example.test/repo/issuer/a.roa"),
        "failed-no-cache must not expand old VCIR related artifacts into current-run audit",
    );
    assert!(
        !audit
            .objects
            .iter()
            .any(|entry| entry.rsync_uri == "rsync://example.test/repo/issuer/issuer.crl"),
        "failed-no-cache must not expose old CRL as current-run CIR input",
    );
}

#[test]
fn rejected_manifest_audit_entry_for_failed_fetch_uses_current_repo_hash() {
    let store_dir = tempfile::tempdir().expect("store dir");
    let store = RocksStore::open(store_dir.path()).expect("open rocksdb");
    let policy = Policy::default();
    let runner = sample_runner_with_ccr_accumulator(&store, &policy);
    let manifest_uri = "rsync://example.test/repo/issuer/issuer.mft";
    let manifest_hash = sha256_hex(b"manifest-bytes");
    store
        .put_blob_bytes_batch(&[(manifest_hash.clone(), b"manifest-bytes".to_vec())])
        .expect("put manifest bytes");
    store
        .put_repository_view_entry(&crate::storage::RepositoryViewEntry {
            rsync_uri: manifest_uri.to_string(),
            current_hash: Some(manifest_hash.clone()),
            repository_source: Some("rsync://example.test/repo/issuer/".to_string()),
            object_type: Some("mft".to_string()),
            state: crate::storage::RepositoryViewState::Present,
        })
        .expect("put repository view");
    let ca = CaInstanceHandle {
        depth: 0,
        tal_id: "test-tal".to_string(),
        parent_manifest_rsync_uri: None,
        ca_certificate: CaCertificateRef::inline_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: manifest_uri.to_string(),
        publication_point_rsync_uri: "rsync://example.test/repo/issuer/".to_string(),
        rrdp_notification_uri: None,
    };

    let entry = runner
        .rejected_manifest_audit_entry_for_failed_fetch(
            &ca,
            &ManifestFreshError::StaleOrEarly {
                this_update_rfc3339_utc: "2026-05-27T08:37:07Z".to_string(),
                next_update_rfc3339_utc: "2026-05-28T10:01:07Z".to_string(),
                validation_time_rfc3339_utc: "2026-05-28T10:11:00Z".to_string(),
            },
        )
        .expect("rejected manifest audit entry");

    assert_eq!(entry.rsync_uri, manifest_uri);
    assert_eq!(entry.sha256_hex, manifest_hash);
    assert_eq!(entry.kind, AuditObjectKind::Manifest);
    assert_eq!(entry.result, AuditObjectResult::Error);
    assert!(
        entry
            .detail
            .as_deref()
            .unwrap_or("")
            .contains("manifest is not valid at validation_time")
    );
}

#[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: CaCertificateRef::inline_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,
        Some("rsync"),
        Some("rsync_only_failed"),
        Some(789),
        Some("load from network failed, fallback to cache"),
        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(),
            roa_cache_stats: crate::validation::objects::RoaValidationCacheStats::default(),
            roa_cache_object_meta: Vec::new(),
        },
        &[],
        &[],
    );

    assert_eq!(audit.source, "failed_fetch_no_cache");
    assert_eq!(audit.repo_sync_source.as_deref(), Some("rsync"));
    assert_eq!(audit.repo_sync_phase.as_deref(), Some("rsync_only_failed"));
    assert_eq!(audit.repo_sync_duration_ms, Some(789));
    assert_eq!(
        audit.repo_sync_error.as_deref(),
        Some("load from network failed, fallback to cache")
    );
    assert_eq!(audit.repo_terminal_state, "failed_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 effective_repo_sync_duration_uses_runtime_duration_for_failures() {
    assert_eq!(effective_repo_sync_duration_ms(0, Some(12), false), 12);
    assert_eq!(effective_repo_sync_duration_ms(5, Some(12), false), 12);
    assert_eq!(effective_repo_sync_duration_ms(20, Some(12), false), 20);
    assert_eq!(effective_repo_sync_duration_ms(5, None, false), 5);
    assert_eq!(effective_repo_sync_duration_ms(5, Some(12), true), 5);
}

#[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,
        reject_reason: None,
    });
    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,
        reject_reason: None,
    });
    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,
        reject_reason: None,
    });

    let ca = CaInstanceHandle {
        depth: 0,
        tal_id: "test-tal".to_string(),
        parent_manifest_rsync_uri: None,
        ca_certificate: CaCertificateRef::inline_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 reconstruct_snapshot_from_vcir_reads_repo_bytes_without_raw_entries() {
    let now = time::OffsetDateTime::now_utc();
    let child_cert_hash = sha256_hex(b"child-cert");
    let vcir = sample_vcir_for_projection(now, &child_cert_hash);
    let ca = CaInstanceHandle {
        depth: 0,
        tal_id: "test-tal".to_string(),
        parent_manifest_rsync_uri: None,
        ca_certificate: CaCertificateRef::inline_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 main_db = store_dir.path().join("work-db");
    let repo_bytes_db = store_dir.path().join("repo-bytes.db");
    let store = RocksStore::open_with_external_repo_bytes(&main_db, &repo_bytes_db)
        .expect("open rocksdb with external repo bytes");
    let repo_blobs = [
        b"manifest-bytes".to_vec(),
        b"current-crl-bytes".to_vec(),
        b"child-cert".to_vec(),
        b"roa-bytes".to_vec(),
        b"aspa-bytes".to_vec(),
    ]
    .into_iter()
    .map(|bytes| (sha256_hex(&bytes), bytes))
    .collect::<Vec<_>>();
    store
        .put_blob_bytes_batch(&repo_blobs)
        .expect("put external repo bytes");

    let manifest_hash = vcir
        .related_artifacts
        .iter()
        .find(|artifact| artifact.artifact_role == VcirArtifactRole::Manifest)
        .expect("manifest artifact")
        .sha256
        .clone();
    assert!(
        store
            .get_raw_by_hash_entry(&manifest_hash)
            .expect("raw manifest lookup")
            .is_none(),
        "repo object bytes must not require raw_by_hash entries"
    );

    let mut warnings = Vec::new();
    let pack = reconstruct_snapshot_from_vcir(&store, &ca, &vcir, &mut warnings)
        .expect("reconstruct pack from external repo bytes");
    assert_eq!(pack.manifest_bytes, b"manifest-bytes".to_vec());
    assert_eq!(pack.files.len(), 4, "crl + child cert + roa + aspa");
    assert!(
        warnings.iter().all(|warning| {
            !warning
                .message
                .contains("raw bytes missing for VCIR audit reconstruction")
        }),
        "external repo bytes should satisfy VCIR audit reconstruction without raw warnings"
    );
}

#[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: CaCertificateRef::inline_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()),
        current_repo_index: None,
        repo_sync_runtime: None,
        parallel_phase2_config: None,
        parallel_roa_worker_pool: None,
        ccr_accumulator: None,
        persist_vcir: true,
        enable_roa_validation_cache: false,
        enable_child_certificate_validation_cache: false,
        publication_point_cache_observe_only: false,
        enable_publication_point_validation_cache: false,
    };
    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()),
        current_repo_index: None,
        repo_sync_runtime: None,
        parallel_phase2_config: None,
        parallel_roa_worker_pool: None,
        ccr_accumulator: None,
        persist_vcir: true,
        enable_roa_validation_cache: false,
        enable_child_certificate_validation_cache: false,
        publication_point_cache_observe_only: false,
        enable_publication_point_validation_cache: false,
    };
    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/"
    );
}

#[derive(Debug, serde::Deserialize)]
#[serde(rename_all = "camelCase")]
struct RipeRootFinalizeFixtureEvent {
    event_type: String,
    validation_time: String,
    pp_manifest_uri: Option<String>,
    object_uri: Option<String>,
    sha256: Option<String>,
    object_type: Option<String>,
    result: Option<String>,
    reason: Option<String>,
}

#[derive(Debug)]
struct RipeRootFinalizeObject {
    uri: String,
    sha256_hex: String,
    object_type: String,
    result: String,
    reason: Option<String>,
}

struct RipeRootFinalizeFixture {
    manifest_uri: String,
    publication_point_uri: String,
    validation_time: time::OffsetDateTime,
    manifest_sha256_hex: String,
    objects: Vec<RipeRootFinalizeObject>,
}

fn load_ripe_root_finalize_fixture(
    fixture_root: &std::path::Path,
    run_id: &str,
) -> RipeRootFinalizeFixture {
    let events_path = fixture_root
        .join(format!("run_{run_id}"))
        .join("ripe-root-events.jsonl");
    let file = std::fs::File::open(&events_path)
        .unwrap_or_else(|e| panic!("open fixture events {} failed: {e}", events_path.display()));
    let reader = std::io::BufReader::new(file);
    let mut manifest_uri = None;
    let mut validation_time = None;
    let mut manifest_sha256_hex = None;
    let mut objects = Vec::new();

    for line in std::io::BufRead::lines(reader) {
        let line = line.expect("read fixture event line");
        let event: RipeRootFinalizeFixtureEvent =
            serde_json::from_str(&line).expect("decode fixture event");
        if event.event_type == "publication_point" {
            manifest_uri = event.pp_manifest_uri;
            validation_time = Some(
                time::OffsetDateTime::parse(
                    &event.validation_time,
                    &time::format_description::well_known::Rfc3339,
                )
                .expect("parse validation_time"),
            );
            continue;
        }
        if event.event_type != "object" {
            continue;
        }
        let uri = event.object_uri.expect("fixture object uri");
        let sha256_hex = event.sha256.expect("fixture object sha256");
        let object_type = event.object_type.expect("fixture object type");
        let result = event.result.expect("fixture object result");
        if object_type == "manifest" {
            manifest_sha256_hex = Some(sha256_hex.clone());
        }
        objects.push(RipeRootFinalizeObject {
            uri,
            sha256_hex,
            object_type,
            result,
            reason: event.reason,
        });
    }

    let manifest_uri = manifest_uri.expect("fixture publication_point event");
    let publication_point_uri = manifest_uri
        .rsplit_once('/')
        .map(|(parent, _)| format!("{parent}/"))
        .expect("manifest uri parent");
    RipeRootFinalizeFixture {
        manifest_uri,
        publication_point_uri,
        validation_time: validation_time.expect("fixture validation_time"),
        manifest_sha256_hex: manifest_sha256_hex.expect("fixture manifest object"),
        objects,
    }
}

fn pack_file_from_fixture_object(
    object: &RipeRootFinalizeObject,
    repo_bytes: &Arc<crate::blob_store::ExternalRepoBytesDb>,
) -> PackFile {
    PackFile::from_lazy_repo_bytes(
        object.uri.clone(),
        object.sha256_hex.clone(),
        sha256_hex_to_32(&object.sha256_hex),
        repo_bytes.clone(),
    )
}

fn child_audit_from_fixture_object(object: &RipeRootFinalizeObject) -> ObjectAuditEntry {
    ObjectAuditEntry {
        rsync_uri: object.uri.clone(),
        sha256_hex: object.sha256_hex.clone(),
        kind: AuditObjectKind::Certificate,
        result: match object.result.as_str() {
            "ok" => AuditObjectResult::Ok,
            "skipped" => AuditObjectResult::Skipped,
            _ => AuditObjectResult::Error,
        },
        detail: object.reason.clone(),
    }
}

fn discovered_child_from_fixture_object(
    issuer: &CaInstanceHandle,
    object: &RipeRootFinalizeObject,
    child_entry_projection: Option<DiscoveredChildEntryProjection>,
) -> DiscoveredChildCaInstance {
    let stem = object
        .uri
        .rsplit_once('/')
        .map(|(_, file)| file.trim_end_matches(".cer"))
        .unwrap_or("child");
    let child_publication_point = format!("{}synthetic-child-{stem}/", issuer.rsync_base_uri);
    let child_manifest = format!("{child_publication_point}child.mft");
    DiscoveredChildCaInstance {
        handle: CaInstanceHandle {
            depth: issuer.depth + 1,
            tal_id: issuer.tal_id.clone(),
            parent_manifest_rsync_uri: Some(issuer.manifest_rsync_uri.clone()),
            ca_certificate: CaCertificateRef::repo_bytes(object.sha256_hex.clone()),
            ca_certificate_rsync_uri: Some(object.uri.clone()),
            effective_ip_resources: None,
            effective_as_resources: None,
            rsync_base_uri: child_publication_point.clone(),
            manifest_rsync_uri: child_manifest,
            publication_point_rsync_uri: child_publication_point,
            rrdp_notification_uri: issuer.rrdp_notification_uri.clone(),
        },
        discovered_from: crate::audit::DiscoveredFrom {
            parent_manifest_rsync_uri: issuer.manifest_rsync_uri.clone(),
            child_ca_certificate_rsync_uri: object.uri.clone(),
            child_ca_certificate_sha256_hex: object.sha256_hex.clone(),
        },
        child_entry_projection,
    }
}

fn child_entry_projection_from_fixture_object(
    store: &RocksStore,
    object: &RipeRootFinalizeObject,
) -> DiscoveredChildEntryProjection {
    let child_der = store
        .get_blob_bytes(&object.sha256_hex)
        .expect("load child certificate bytes for projection")
        .expect("child certificate bytes exist for projection");
    let child_cert =
        ResourceCertificate::decode_der(&child_der).expect("decode child certificate projection");
    let child_ski = child_cert
        .tbs
        .extensions
        .subject_key_identifier
        .as_ref()
        .expect("child certificate projection SKI");
    DiscoveredChildEntryProjection {
        child_ski: hex::encode(child_ski),
    }
}

struct RipeRootChildEntryProfile {
    count: usize,
    load_der_nanos: u128,
    decode_cert_nanos: u128,
    build_entry_nanos: u128,
}

fn profile_ripe_root_child_entry_build(
    store: &RocksStore,
    discovered_children: &[DiscoveredChildCaInstance],
    validation_time: time::OffsetDateTime,
) -> Result<RipeRootChildEntryProfile, String> {
    let mut out = Vec::with_capacity(discovered_children.len());
    let mut load_der_nanos = 0;
    let mut decode_cert_nanos = 0;
    let mut build_entry_nanos = 0;
    for child in discovered_children {
        let load_started = std::time::Instant::now();
        let child_der = child.handle.ca_certificate_der(store)?;
        load_der_nanos += load_started.elapsed().as_nanos();

        let decode_started = std::time::Instant::now();
        let child_cert = ResourceCertificate::decode_der(child_der.as_ref())
            .map_err(|e| format!("decode child certificate for VCIR failed: {e}"))?;
        decode_cert_nanos += decode_started.elapsed().as_nanos();

        let build_started = std::time::Instant::now();
        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),
        });
        build_entry_nanos += build_started.elapsed().as_nanos();
    }
    Ok(RipeRootChildEntryProfile {
        count: out.len(),
        load_der_nanos,
        decode_cert_nanos,
        build_entry_nanos,
    })
}

#[test]
#[ignore = "manual performance repro: requires target/ripe-root-finalize-repro repo-bytes fixture"]
fn ripe_root_finalize_repro_from_remote_fixture() {
    let fixture_root = std::env::var("RPKI_RIPE_ROOT_FINALIZE_FIXTURE")
        .map(std::path::PathBuf::from)
        .unwrap_or_else(|_| {
            std::path::PathBuf::from(env!("CARGO_MANIFEST_DIR"))
                .join("target/ripe-root-finalize-repro")
        });
    let run_id = std::env::var("RPKI_RIPE_ROOT_FINALIZE_RUN").unwrap_or_else(|_| "0262".into());
    assert!(
        fixture_root.exists(),
        "fixture root missing: {}; expected copied remote fixture",
        fixture_root.display()
    );

    let fixture = load_ripe_root_finalize_fixture(&fixture_root, &run_id);
    let repo_bytes_db = fixture_root.join("db/repo-bytes.db");
    assert!(
        repo_bytes_db.exists(),
        "repo-bytes fixture missing: {}",
        repo_bytes_db.display()
    );

    let store_dir = tempfile::tempdir().expect("store dir");
    let work_db = store_dir.path().join("work-db");
    let store = RocksStore::open_with_external_repo_bytes(&work_db, &repo_bytes_db)
        .expect("open work-db with external repo-bytes");
    let repo_bytes = Arc::new(
        store
            .external_repo_bytes_ref()
            .expect("external repo bytes")
            .clone(),
    );

    let manifest_bytes = store
        .get_blob_bytes(&fixture.manifest_sha256_hex)
        .expect("load fixture manifest bytes")
        .expect("fixture manifest bytes exist");
    let manifest = ManifestObject::decode_der(&manifest_bytes).expect("decode fixture manifest");
    let mut files = Vec::with_capacity(fixture.objects.len().saturating_sub(1));
    let mut child_audits = Vec::new();
    let current_ca_hash = "007ad0c291b01ede4bd60e1204074ce3f7192186a022c9577cef5d8e91d5171a";
    let current_ca_uri = "rsync://rpki.ripe.net/repository/aca/KpSo3VVK5wEHIJnHC2QHVV3d5mk.cer";
    let parent_manifest_uri =
        "rsync://rpki.ripe.net/repository/aca/7DNNDzoYvgAht7joQih2Qayxcxo.mft";
    let ca = CaInstanceHandle {
        depth: 1,
        tal_id: "ripe-ncc".to_string(),
        parent_manifest_rsync_uri: Some(parent_manifest_uri.to_string()),
        ca_certificate: CaCertificateRef::repo_bytes(current_ca_hash.to_string()),
        ca_certificate_rsync_uri: Some(current_ca_uri.to_string()),
        effective_ip_resources: None,
        effective_as_resources: None,
        rsync_base_uri: fixture.publication_point_uri.clone(),
        manifest_rsync_uri: fixture.manifest_uri.clone(),
        publication_point_rsync_uri: fixture.publication_point_uri.clone(),
        rrdp_notification_uri: Some("https://rrdp.ripe.net/notification.xml".to_string()),
    };
    let mut discovered_children = Vec::new();
    let use_child_projection = std::env::var("RPKI_RIPE_ROOT_FINALIZE_USE_CHILD_PROJECTION")
        .map(|value| value != "0" && value.to_ascii_lowercase() != "false")
        .unwrap_or(false);
    for object in &fixture.objects {
        if object.uri == fixture.manifest_uri {
            continue;
        }
        files.push(pack_file_from_fixture_object(object, &repo_bytes));
        if object.object_type == "certificate" {
            child_audits.push(child_audit_from_fixture_object(object));
            if object.result == "ok" {
                let child_entry_projection = use_child_projection
                    .then(|| child_entry_projection_from_fixture_object(&store, object));
                discovered_children.push(discovered_child_from_fixture_object(
                    &ca,
                    object,
                    child_entry_projection,
                ));
            }
        }
    }
    if std::env::var("RPKI_RIPE_ROOT_FINALIZE_PROFILE_CHILD").is_ok() {
        let profile_children = fixture
            .objects
            .iter()
            .filter(|object| object.object_type == "certificate" && object.result == "ok")
            .map(|object| discovered_child_from_fixture_object(&ca, object, None))
            .collect::<Vec<_>>();
        let profile_started = std::time::Instant::now();
        let profile =
            profile_ripe_root_child_entry_build(&store, &profile_children, fixture.validation_time)
                .expect("profile child entry build");
        eprintln!(
            "ripe root child-entry profile: run={} count={} total_ms={} load_der_ms={:.3} decode_cert_ms={:.3} build_entry_ms={:.3}",
            run_id,
            profile.count,
            profile_started.elapsed().as_millis(),
            profile.load_der_nanos as f64 / 1_000_000.0,
            profile.decode_cert_nanos as f64 / 1_000_000.0,
            profile.build_entry_nanos as f64 / 1_000_000.0,
        );
        if std::env::var("RPKI_RIPE_ROOT_FINALIZE_ONLY_CHILD_PROFILE").is_ok() {
            assert!(profile.count > 22_000);
            return;
        }
    }

    let fresh_point = FreshValidatedPublicationPoint {
        manifest_rsync_uri: fixture.manifest_uri.clone(),
        publication_point_rsync_uri: fixture.publication_point_uri.clone(),
        manifest_number_be: manifest.manifest.manifest_number.bytes_be.clone(),
        this_update: PackTime::from_utc_offset_datetime(manifest.manifest.this_update),
        next_update: PackTime::from_utc_offset_datetime(manifest.manifest.next_update),
        verified_at: PackTime::from_utc_offset_datetime(fixture.validation_time),
        manifest_bytes,
        files,
    };
    let policy = Policy::default();
    let enable_ccr_accumulator = std::env::var("RPKI_RIPE_ROOT_FINALIZE_CCR")
        .map(|value| value != "0" && value.to_ascii_lowercase() != "false")
        .unwrap_or(true);
    let runner = Rpkiv1PublicationPointRunner {
        store: &store,
        policy: &policy,
        http_fetcher: &NeverHttpFetcher,
        rsync_fetcher: &FailingRsyncFetcher,
        validation_time: fixture.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()),
        current_repo_index: None,
        repo_sync_runtime: None,
        parallel_phase2_config: None,
        parallel_roa_worker_pool: None,
        ccr_accumulator: enable_ccr_accumulator
            .then(|| Mutex::new(CcrAccumulator::new(Vec::new()))),
        persist_vcir: true,
        enable_roa_validation_cache: false,
        enable_child_certificate_validation_cache: true,
        publication_point_cache_observe_only: false,
        enable_publication_point_validation_cache: true,
    };

    eprintln!(
        "ripe root finalize repro setup: run={} ccr_accumulator={} child_projection={} objects={} files={} child_audits={} discovered_children={}",
        run_id,
        enable_ccr_accumulator,
        use_child_projection,
        fixture.objects.len(),
        fresh_point.files.len(),
        child_audits.len(),
        discovered_children.len()
    );
    let started = std::time::Instant::now();
    let output = runner
        .finalize_fresh_publication_point_from_reducer(
            &ca,
            &fresh_point,
            Vec::new(),
            empty_objects_output(),
            child_audits,
            discovered_children,
            Some("rrdp"),
            Some("rrdp_ok"),
            0,
            None,
        )
        .expect("finalize fixture publication point");
    let finalize_ms = started.elapsed().as_millis();
    eprintln!(
        "ripe root finalize repro timing: run={} finalize_ms={} snapshot_pack_ms={} persist_vcir_ms={} build_vcir_ms={} child_entries_ms={} related_artifacts_ms={} replace_vcir_ms={} replace_vcir_encode_ms={} replace_vcir_write_batch_ms={} ccr_projection_build_ms={} audit_build_ms={}",
        run_id,
        finalize_ms,
        output.snapshot_pack_ms,
        output.persist_vcir_ms,
        output.persist_vcir_timing.build_vcir_ms,
        output.persist_vcir_timing.build_vcir.child_entries_ms,
        output.persist_vcir_timing.build_vcir.related_artifacts_ms,
        output.persist_vcir_timing.replace_vcir_ms,
        output.persist_vcir_timing.replace_vcir.vcir_encode_ms,
        output.persist_vcir_timing.replace_vcir.write_batch_ms,
        output.ccr_projection_build_ms,
        output.audit_build_ms,
    );
    eprintln!(
        "ripe root finalize repro result: audit_objects={} discovered_children={} ccr_manifest_count={}",
        output.result.audit.objects.len(),
        output.result.discovered_children.len(),
        runner
            .ccr_accumulator_snapshot()
            .map(|snapshot| snapshot.manifest_count())
            .unwrap_or(0),
    );

    assert!(fresh_point.files.len() > 22_000);
    assert!(output.result.discovered_children.len() > 22_000);
    if enable_ccr_accumulator {
        assert_eq!(
            runner
                .ccr_accumulator_snapshot()
                .expect("ccr snapshot")
                .manifest_count(),
            1
        );
    }
}
