use url::Url;

use crate::analysis::timing::TimingHandle;
use crate::audit::PublicationPointAudit;
use crate::audit_downloads::DownloadLogHandle;
use crate::ccr::CcrAccumulator;
use crate::current_repo_index::{CurrentRepoIndexHandle, CurrentRepoObject};
use crate::data_model::ta::TrustAnchor;
use crate::parallel::config::{ParallelPhase1Config, ParallelPhase2Config};
use crate::parallel::repo_runtime::{Phase1RepoSyncRuntime, RepoSyncRuntime};
use crate::parallel::repo_worker::{
    LiveRepoTransportExecutor, RepoTransportWorkerPool, RepoWorkerPoolConfig,
};
use crate::parallel::run_coordinator::GlobalRunCoordinator;
use crate::parallel::types::{TalInputSpec, TalSource};
use crate::policy::SyncPreference;
use crate::replay::archive::ReplayArchiveIndex;
use crate::replay::delta_archive::ReplayDeltaArchiveIndex;
use crate::replay::delta_fetch_http::PayloadDeltaReplayHttpFetcher;
use crate::replay::delta_fetch_rsync::{
    PayloadDeltaReplayCurrentStoreRsyncFetcher, PayloadDeltaReplayRsyncFetcher,
};
use crate::replay::fetch_http::PayloadReplayHttpFetcher;
use crate::replay::fetch_rsync::PayloadReplayRsyncFetcher;
use crate::sync::rrdp::Fetcher;
use crate::validation::from_tal::{
    DiscoveredRootCaInstance, FromTalError, canonical_tal_rsync_uri_from_bytes,
    discover_root_ca_instance_from_tal_and_ta_der,
    discover_root_ca_instance_from_tal_and_ta_der_with_strict_name,
    discover_root_ca_instance_from_tal_url_with_fetchers,
    discover_root_ca_instance_from_tal_url_with_fetchers_strict_name,
    discover_root_ca_instance_from_tal_with_fetchers,
    discover_root_ca_instance_from_tal_with_fetchers_strict_name,
};
use crate::validation::objects::ParallelRoaWorkerPool;
use crate::validation::tree::CaCertificateRef;
use crate::validation::tree::{
    CaInstanceHandle, TreeRunAuditOutput, TreeRunConfig, TreeRunError, TreeRunOutput,
    run_tree_serial, run_tree_serial_audit, run_tree_serial_audit_multi_root,
};
use crate::validation::tree_parallel::{
    run_tree_parallel_phase2_audit, run_tree_parallel_phase2_audit_multi_root,
};
use crate::validation::tree_runner::Rpkiv1PublicationPointRunner;
use std::collections::HashMap;
use std::sync::{Arc, Mutex};

fn tal_id_from_url_like(s: &str) -> Option<String> {
    let url = Url::parse(s).ok()?;
    if let Some(last) = url
        .path_segments()
        .and_then(|segments| segments.filter(|seg| !seg.is_empty()).next_back())
    {
        let stem = last.rsplit_once('.').map(|(stem, _)| stem).unwrap_or(last);
        let trimmed = stem.trim();
        if !trimmed.is_empty() {
            return Some(trimmed.to_string());
        }
    }
    url.host_str().map(|host| host.to_string())
}

fn derive_tal_id(discovery: &DiscoveredRootCaInstance) -> String {
    discovery
        .tal_url
        .as_deref()
        .and_then(tal_id_from_url_like)
        .or_else(|| {
            discovery
                .trust_anchor
                .resolved_ta_uri
                .as_ref()
                .and_then(|uri| tal_id_from_url_like(uri.as_str()))
        })
        .or_else(|| {
            discovery
                .trust_anchor
                .tal
                .ta_uris
                .first()
                .and_then(|uri| tal_id_from_url_like(uri.as_str()))
        })
        .unwrap_or_else(|| "unknown-tal".to_string())
}

#[derive(Clone, Debug, PartialEq, Eq)]
pub struct RunTreeFromTalOutput {
    pub discovery: DiscoveredRootCaInstance,
    pub tree: TreeRunOutput,
}

#[derive(Clone, Debug, PartialEq, Eq)]
pub struct RunTreeFromTalAuditOutput {
    pub discovery: DiscoveredRootCaInstance,
    pub discoveries: Vec<DiscoveredRootCaInstance>,
    pub successful_tal_inputs: Vec<TalInputSpec>,
    pub tree: TreeRunOutput,
    pub publication_points: Vec<PublicationPointAudit>,
    pub roa_cache_stats: crate::validation::objects::RoaValidationCacheStats,
    pub cir_input: crate::cir::CirInputSnapshot,
    pub downloads: Vec<crate::audit::AuditDownloadEvent>,
    pub download_stats: crate::audit::AuditDownloadStats,
    pub current_repo_objects: Vec<CurrentRepoObject>,
    pub ccr_accumulator: Option<CcrAccumulator>,
}

#[derive(Clone, Debug, PartialEq, Eq)]
pub struct TalRootDiscovery {
    pub tal_input: TalInputSpec,
    pub discovery: DiscoveredRootCaInstance,
    pub root_handle: CaInstanceHandle,
}

fn snapshot_current_repo_objects(
    current_repo_index: Option<&CurrentRepoIndexHandle>,
    collect: bool,
) -> Vec<CurrentRepoObject> {
    if !collect {
        return Vec::new();
    }
    current_repo_index
        .and_then(|handle| handle.read().ok().map(|idx| idx.snapshot_objects()))
        .unwrap_or_default()
}

fn make_live_runner<'a>(
    store: &'a crate::storage::RocksStore,
    policy: &'a crate::policy::Policy,
    http_fetcher: &'a dyn Fetcher,
    rsync_fetcher: &'a dyn crate::fetch::rsync::RsyncFetcher,
    validation_time: time::OffsetDateTime,
    timing: Option<TimingHandle>,
    download_log: Option<DownloadLogHandle>,
    current_repo_index: Option<CurrentRepoIndexHandle>,
    repo_sync_runtime: Option<Arc<dyn RepoSyncRuntime>>,
    parallel_phase2_config: Option<ParallelPhase2Config>,
    ccr_accumulator: Option<CcrAccumulator>,
    persist_vcir: bool,
    enable_roa_validation_cache: bool,
    enable_child_certificate_validation_cache: bool,
    publication_point_cache_observe_only: bool,
    enable_publication_point_validation_cache: bool,
) -> Rpkiv1PublicationPointRunner<'a> {
    let parallel_roa_worker_pool = parallel_phase2_config
        .as_ref()
        .and_then(|config| ParallelRoaWorkerPool::new(config).ok());
    Rpkiv1PublicationPointRunner {
        store,
        policy,
        http_fetcher,
        rsync_fetcher,
        validation_time,
        timing,
        download_log,
        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,
        repo_sync_runtime,
        parallel_phase2_config,
        parallel_roa_worker_pool,
        ccr_accumulator: ccr_accumulator.map(Mutex::new),
        persist_vcir,
        enable_roa_validation_cache,
        enable_child_certificate_validation_cache,
        publication_point_cache_observe_only,
        enable_publication_point_validation_cache,
    }
}

fn build_phase1_repo_sync_runtime<H, R>(
    store: Arc<crate::storage::RocksStore>,
    policy: &crate::policy::Policy,
    http_fetcher: &H,
    rsync_fetcher: &R,
    parallel_config: ParallelPhase1Config,
    timing: Option<TimingHandle>,
    download_log: Option<DownloadLogHandle>,
    tal_inputs: Vec<crate::parallel::types::TalInputSpec>,
    record_transport_prefetch_requests: bool,
) -> Result<(Arc<dyn RepoSyncRuntime>, CurrentRepoIndexHandle), RunTreeFromTalError>
where
    H: Fetcher + Clone + 'static,
    R: crate::fetch::rsync::RsyncFetcher + Clone + 'static,
{
    let coordinator = GlobalRunCoordinator::new(parallel_config.clone(), tal_inputs);
    let current_repo_index = coordinator.current_repo_index_handle();
    let rsync_fetcher_arc = Arc::new(rsync_fetcher.clone());
    let executor = LiveRepoTransportExecutor::new(
        Arc::clone(&store),
        current_repo_index.clone(),
        Arc::new(http_fetcher.clone()),
        Arc::clone(&rsync_fetcher_arc),
        timing.clone(),
        download_log,
    );
    let pool = RepoTransportWorkerPool::new(RepoWorkerPoolConfig::from(&parallel_config), executor)
        .map_err(RunTreeFromTalError::Replay)?;
    let resolver_rsync_fetcher_arc = Arc::clone(&rsync_fetcher_arc);
    let resolver: Arc<dyn Fn(&str) -> String + Send + Sync> =
        Arc::new(move |base: &str| resolver_rsync_fetcher_arc.dedup_key(base));
    let failure_rsync_fetcher_arc = Arc::clone(&rsync_fetcher_arc);
    let failure_resolver: Arc<dyn Fn(&str) -> Option<String> + Send + Sync> =
        Arc::new(move |base: &str| failure_rsync_fetcher_arc.failure_dedup_key(base));
    let _ = policy; // policy reserved for later runtime-level decisions
    let runtime = Arc::new(
        Phase1RepoSyncRuntime::new_with_failure_scope_and_prefetch_recording(
            coordinator,
            pool,
            resolver,
            failure_resolver,
            policy.sync_preference,
            record_transport_prefetch_requests,
        ),
    );
    Ok((runtime, current_repo_index))
}

fn record_transport_prefetch_count(timing: Option<&TimingHandle>, key: &'static str, value: u64) {
    if value == 0 {
        return;
    }
    if let Some(timing) = timing {
        timing.record_count(key, value);
    }
}

fn apply_transport_request_prefetch(
    store: &crate::storage::RocksStore,
    runtime: &Arc<dyn RepoSyncRuntime>,
    sync_preference: SyncPreference,
    validation_time: time::OffsetDateTime,
    config: &TreeRunConfig,
    timing: Option<&TimingHandle>,
) -> Result<(), RunTreeFromTalError> {
    if !config.enable_transport_request_prefetch {
        return Ok(());
    }

    let snapshot = match store.get_transport_prefetch_snapshot() {
        Ok(Some(snapshot)) => snapshot,
        Ok(None) => {
            crate::progress_log::emit(
                "phase1_repo_prefetch_missing_snapshot",
                serde_json::json!({ "status": "missing" }),
            );
            return Ok(());
        }
        Err(err) => {
            record_transport_prefetch_count(timing, "transport_prefetch_load_errors", 1);
            crate::progress_log::emit(
                "phase1_repo_prefetch_load_error",
                serde_json::json!({ "error": err.to_string() }),
            );
            return Ok(());
        }
    };

    if !snapshot.is_compatible_with(sync_preference) {
        record_transport_prefetch_count(
            timing,
            "transport_prefetch_skipped_incompatible_snapshots",
            1,
        );
        crate::progress_log::emit(
            "phase1_repo_prefetch_skipped",
            serde_json::json!({
                "reason": "incompatible_snapshot",
                "schema_version": snapshot.schema_version,
                "request_count": snapshot.requests.len(),
            }),
        );
        return Ok(());
    }

    let stats = runtime
        .prefetch_transport_requests(&snapshot, validation_time)
        .map_err(TreeRunError::Runner)?;
    record_transport_prefetch_count(
        timing.clone(),
        "transport_prefetch_loaded_requests",
        stats.loaded_requests,
    );
    record_transport_prefetch_count(
        timing.clone(),
        "transport_prefetch_enqueued_tasks",
        stats.enqueued_tasks,
    );
    record_transport_prefetch_count(
        timing,
        "transport_prefetch_waiting_requests",
        stats.waiting_requests,
    );
    record_transport_prefetch_count(
        timing,
        "transport_prefetch_reused_results",
        stats.reused_results,
    );
    record_transport_prefetch_count(
        timing,
        "transport_prefetch_skipped_incompatible_requests",
        stats.skipped_incompatible,
    );
    crate::progress_log::emit(
        "phase1_repo_prefetch_applied",
        serde_json::json!({
            "loaded_requests": stats.loaded_requests,
            "enqueued_tasks": stats.enqueued_tasks,
            "waiting_requests": stats.waiting_requests,
            "reused_results": stats.reused_results,
            "skipped_incompatible": stats.skipped_incompatible,
        }),
    );
    Ok(())
}

fn persist_transport_request_prefetch_snapshot(
    store: &crate::storage::RocksStore,
    runtime: &Arc<dyn RepoSyncRuntime>,
    config: &TreeRunConfig,
    timing: Option<&TimingHandle>,
) -> Result<(), RunTreeFromTalError> {
    if !config.enable_transport_request_prefetch {
        return Ok(());
    }
    let snapshot = runtime.transport_prefetch_snapshot();
    let recorded = snapshot.requests.len() as u64;
    let persist_started = std::time::Instant::now();
    store
        .put_transport_prefetch_snapshot(&snapshot)
        .map_err(|err| TreeRunError::Runner(err.to_string()))?;
    let persist_ms = persist_started.elapsed().as_millis() as u64;
    record_transport_prefetch_count(timing, "transport_prefetch_recorded_requests", recorded);
    crate::progress_log::emit(
        "phase1_repo_prefetch_persisted",
        serde_json::json!({
            "recorded_requests": recorded,
            "schema_version": snapshot.schema_version,
            "persist_ms": persist_ms,
        }),
    );
    Ok(())
}

/// Persist the dead-repo blacklist working copy at run end (#141). Failures
/// degrade to a progress event; a broken blacklist file must never fail a run.
fn persist_dead_repo_blacklist(
    runtime: &Arc<dyn RepoSyncRuntime>,
    timing: Option<&TimingHandle>,
) -> Result<(), RunTreeFromTalError> {
    let Some((blacklist_config, blacklist)) = runtime.dead_repo_blacklist_state() else {
        return Ok(());
    };
    let persist_started = std::time::Instant::now();
    let now_unix = std::time::SystemTime::now()
        .duration_since(std::time::UNIX_EPOCH)
        .map(|duration| duration.as_secs())
        .unwrap_or(0);
    match blacklist.store_atomic(&blacklist_config.path, now_unix) {
        Ok(()) => {
            let persist_ms = persist_started.elapsed().as_millis() as u64;
            record_transport_prefetch_count(
                timing,
                "dead_repo_blacklist_entries",
                blacklist.len() as u64,
            );
            record_transport_prefetch_count(
                timing,
                "dead_repo_blacklist_blacklisted",
                blacklist.blacklisted_len() as u64,
            );
            crate::progress_log::emit(
                "dead_repo_blacklist_persisted",
                serde_json::json!({
                    "path": blacklist_config.path.display().to_string(),
                    "entries": blacklist.len(),
                    "blacklisted": blacklist.blacklisted_len(),
                    "persist_ms": persist_ms,
                }),
            );
        }
        Err(err) => {
            crate::progress_log::emit(
                "dead_repo_blacklist_persist_error",
                serde_json::json!({
                    "path": blacklist_config.path.display().to_string(),
                    "error": err.to_string(),
                }),
            );
        }
    }
    Ok(())
}

fn root_discovery_from_tal_input(
    tal_input: &TalInputSpec,
    http_fetcher: &dyn Fetcher,
    rsync_fetcher: &dyn crate::fetch::rsync::RsyncFetcher,
    strict_name: bool,
) -> Result<DiscoveredRootCaInstance, FromTalError> {
    match &tal_input.source {
        TalSource::Url(url) => {
            if strict_name {
                discover_root_ca_instance_from_tal_url_with_fetchers_strict_name(
                    http_fetcher,
                    rsync_fetcher,
                    url,
                )
            } else {
                discover_root_ca_instance_from_tal_url_with_fetchers(
                    http_fetcher,
                    rsync_fetcher,
                    url,
                )
            }
        }
        TalSource::DerBytes {
            tal_bytes, ta_der, ..
        } => {
            if strict_name {
                discover_root_ca_instance_from_tal_and_ta_der_with_strict_name(
                    tal_bytes, ta_der, None,
                )
            } else {
                discover_root_ca_instance_from_tal_and_ta_der(tal_bytes, ta_der, None)
            }
        }
        TalSource::FilePath(path) => {
            let tal_bytes = std::fs::read(path).map_err(|e| {
                FromTalError::TalFetch(format!("read TAL file failed: {}: {e}", path.display()))
            })?;
            let tal = crate::data_model::tal::Tal::decode_bytes(&tal_bytes)
                .map_err(FromTalError::from)?;
            if strict_name {
                discover_root_ca_instance_from_tal_with_fetchers_strict_name(
                    http_fetcher,
                    rsync_fetcher,
                    tal,
                    None,
                )
            } else {
                discover_root_ca_instance_from_tal_with_fetchers(
                    http_fetcher,
                    rsync_fetcher,
                    tal,
                    None,
                )
            }
        }
        TalSource::FilePathWithTa { tal_path, ta_path } => {
            let tal_bytes = std::fs::read(tal_path).map_err(|e| {
                FromTalError::TalFetch(format!("read TAL file failed: {}: {e}", tal_path.display()))
            })?;
            let ta_der = std::fs::read(ta_path).map_err(|e| {
                FromTalError::TaFetch(format!("read TA file failed: {}: {e}", ta_path.display()))
            })?;
            let resolved_ta_uri = canonical_tal_rsync_uri_from_bytes(&tal_bytes)?;
            if strict_name {
                discover_root_ca_instance_from_tal_and_ta_der_with_strict_name(
                    &tal_bytes,
                    &ta_der,
                    Some(&resolved_ta_uri),
                )
            } else {
                discover_root_ca_instance_from_tal_and_ta_der(
                    &tal_bytes,
                    &ta_der,
                    Some(&resolved_ta_uri),
                )
            }
        }
    }
}

fn discover_root_ca_instance_from_tal_url_with_policy(
    policy: &crate::policy::Policy,
    http_fetcher: &dyn Fetcher,
    rsync_fetcher: &dyn crate::fetch::rsync::RsyncFetcher,
    tal_url: &str,
) -> Result<DiscoveredRootCaInstance, FromTalError> {
    if policy.strict.name {
        discover_root_ca_instance_from_tal_url_with_fetchers_strict_name(
            http_fetcher,
            rsync_fetcher,
            tal_url,
        )
    } else {
        discover_root_ca_instance_from_tal_url_with_fetchers(http_fetcher, rsync_fetcher, tal_url)
    }
}

fn discover_root_ca_instance_from_tal_and_ta_der_with_policy(
    policy: &crate::policy::Policy,
    tal_bytes: &[u8],
    ta_der: &[u8],
    resolved_ta_uri: Option<&Url>,
) -> Result<DiscoveredRootCaInstance, FromTalError> {
    if policy.strict.name {
        discover_root_ca_instance_from_tal_and_ta_der_with_strict_name(
            tal_bytes,
            ta_der,
            resolved_ta_uri,
        )
    } else {
        discover_root_ca_instance_from_tal_and_ta_der(tal_bytes, ta_der, resolved_ta_uri)
    }
}

fn discover_multiple_roots_from_tal_inputs(
    tal_inputs: &[TalInputSpec],
    http_fetcher: &dyn Fetcher,
    rsync_fetcher: &dyn crate::fetch::rsync::RsyncFetcher,
    strict_name: bool,
) -> Result<Vec<TalRootDiscovery>, RunTreeFromTalError> {
    let mut roots = Vec::with_capacity(tal_inputs.len());
    for tal_input in tal_inputs {
        let discovery = match root_discovery_from_tal_input(
            tal_input,
            http_fetcher,
            rsync_fetcher,
            strict_name,
        ) {
            Ok(discovery) => discovery,
            Err(error)
                if should_isolate_multi_tal_strict_name_failure(
                    tal_inputs.len(),
                    strict_name,
                    &error,
                ) =>
            {
                eprintln!(
                    "warning: skipping TAL '{}' because strict name validation failed during trust anchor discovery: {error}",
                    tal_input.tal_id
                );
                continue;
            }
            Err(error) => return Err(error.into()),
        };
        let root_handle = root_handle_from_trust_anchor(
            &discovery.trust_anchor,
            tal_input.tal_id.clone(),
            None,
            &discovery.ca_instance,
        );
        roots.push(TalRootDiscovery {
            tal_input: tal_input.clone(),
            discovery,
            root_handle,
        });
    }
    if roots.is_empty() {
        return Err(RunTreeFromTalError::Replay(
            "multi-TAL root discovery returned no usable roots after strict name filtering"
                .to_string(),
        ));
    }
    Ok(roots)
}

fn should_isolate_multi_tal_strict_name_failure(
    tal_input_count: usize,
    strict_name: bool,
    error: &FromTalError,
) -> bool {
    strict_name
        && tal_input_count > 1
        && error.to_string().contains("Name strict validation failed")
}

#[derive(Debug, thiserror::Error)]
pub enum RunTreeFromTalError {
    #[error("{0}")]
    FromTal(#[from] FromTalError),

    #[error("payload replay setup failed: {0}")]
    Replay(String),

    #[error("{0}")]
    Tree(#[from] TreeRunError),
}

pub fn root_handle_from_trust_anchor(
    trust_anchor: &TrustAnchor,
    tal_id: String,
    ca_certificate_rsync_uri: Option<String>,
    ca_instance: &crate::validation::ca_instance::CaInstanceUris,
) -> CaInstanceHandle {
    let ta_rc = trust_anchor.ta_certificate.rc_ca.clone();
    CaInstanceHandle {
        depth: 0,
        tal_id,
        parent_manifest_rsync_uri: None,
        ca_certificate: CaCertificateRef::inline_der(trust_anchor.ta_certificate.raw_der.clone()),
        ca_certificate_rsync_uri,
        effective_ip_resources: ta_rc.tbs.extensions.ip_resources.clone(),
        effective_as_resources: ta_rc.tbs.extensions.as_resources.clone(),
        rsync_base_uri: ca_instance.rsync_base_uri.clone(),
        manifest_rsync_uri: ca_instance.manifest_rsync_uri.clone(),
        publication_point_rsync_uri: ca_instance.publication_point_rsync_uri.clone(),
        rrdp_notification_uri: ca_instance.rrdp_notification_uri.clone(),
    }
}

pub fn run_tree_from_tal_url_serial(
    store: &crate::storage::RocksStore,
    policy: &crate::policy::Policy,
    tal_url: &str,
    http_fetcher: &dyn Fetcher,
    rsync_fetcher: &dyn crate::fetch::rsync::RsyncFetcher,
    validation_time: time::OffsetDateTime,
    config: &TreeRunConfig,
) -> Result<RunTreeFromTalOutput, RunTreeFromTalError> {
    let discovery = discover_root_ca_instance_from_tal_url_with_policy(
        policy,
        http_fetcher,
        rsync_fetcher,
        tal_url,
    )?;

    let runner = make_live_runner(
        store,
        policy,
        http_fetcher,
        rsync_fetcher,
        validation_time,
        None,
        None,
        None,
        None,
        None,
        None,
        config.persist_vcir,
        config.enable_roa_validation_cache,
        config.enable_child_certificate_validation_cache,
        config.publication_point_cache_observe_only,
        config.enable_publication_point_validation_cache,
    );

    let root = root_handle_from_trust_anchor(
        &discovery.trust_anchor,
        derive_tal_id(&discovery),
        None,
        &discovery.ca_instance,
    );
    let tree = run_tree_serial(root, &runner, config)?;

    Ok(RunTreeFromTalOutput { discovery, tree })
}

pub fn run_tree_from_tal_url_serial_audit(
    store: &crate::storage::RocksStore,
    policy: &crate::policy::Policy,
    tal_url: &str,
    http_fetcher: &dyn Fetcher,
    rsync_fetcher: &dyn crate::fetch::rsync::RsyncFetcher,
    validation_time: time::OffsetDateTime,
    config: &TreeRunConfig,
) -> Result<RunTreeFromTalAuditOutput, RunTreeFromTalError> {
    let discovery = discover_root_ca_instance_from_tal_url_with_policy(
        policy,
        http_fetcher,
        rsync_fetcher,
        tal_url,
    )?;

    let download_log = DownloadLogHandle::new();
    let runner = make_live_runner(
        store,
        policy,
        http_fetcher,
        rsync_fetcher,
        validation_time,
        None,
        Some(download_log.clone()),
        None,
        None,
        None,
        None,
        config.persist_vcir,
        config.enable_roa_validation_cache,
        config.enable_child_certificate_validation_cache,
        config.publication_point_cache_observe_only,
        config.enable_publication_point_validation_cache,
    );

    let root = root_handle_from_trust_anchor(
        &discovery.trust_anchor,
        derive_tal_id(&discovery),
        None,
        &discovery.ca_instance,
    );
    let TreeRunAuditOutput {
        tree,
        publication_points,
        roa_cache_stats,
        cir_input,
    } = run_tree_serial_audit(root, &runner, config)?;

    let downloads = download_log.snapshot_events();
    let download_stats = DownloadLogHandle::stats_from_events(&downloads);
    Ok(RunTreeFromTalAuditOutput {
        discovery: discovery.clone(),
        discoveries: vec![discovery],
        successful_tal_inputs: Vec::new(),
        tree,
        publication_points,
        roa_cache_stats,
        cir_input,
        downloads,
        download_stats,
        current_repo_objects: Vec::new(),
        ccr_accumulator: None,
    })
}

pub fn run_tree_from_tal_url_serial_audit_with_timing(
    store: &crate::storage::RocksStore,
    policy: &crate::policy::Policy,
    tal_url: &str,
    http_fetcher: &dyn Fetcher,
    rsync_fetcher: &dyn crate::fetch::rsync::RsyncFetcher,
    validation_time: time::OffsetDateTime,
    config: &TreeRunConfig,
    timing: &TimingHandle,
) -> Result<RunTreeFromTalAuditOutput, RunTreeFromTalError> {
    let _tal = timing.span_phase("tal_bootstrap");
    let discovery = discover_root_ca_instance_from_tal_url_with_policy(
        policy,
        http_fetcher,
        rsync_fetcher,
        tal_url,
    )?;
    drop(_tal);

    let download_log = DownloadLogHandle::new();
    let runner = make_live_runner(
        store,
        policy,
        http_fetcher,
        rsync_fetcher,
        validation_time,
        Some(timing.clone()),
        Some(download_log.clone()),
        None,
        None,
        None,
        None,
        config.persist_vcir,
        config.enable_roa_validation_cache,
        config.enable_child_certificate_validation_cache,
        config.publication_point_cache_observe_only,
        config.enable_publication_point_validation_cache,
    );

    let root = root_handle_from_trust_anchor(
        &discovery.trust_anchor,
        derive_tal_id(&discovery),
        None,
        &discovery.ca_instance,
    );
    let _tree = timing.span_phase("tree_run_total");
    let TreeRunAuditOutput {
        tree,
        publication_points,
        roa_cache_stats,
        cir_input,
    } = run_tree_serial_audit(root, &runner, config)?;

    let downloads = download_log.snapshot_events();
    let download_stats = DownloadLogHandle::stats_from_events(&downloads);
    Ok(RunTreeFromTalAuditOutput {
        discovery: discovery.clone(),
        discoveries: vec![discovery],
        successful_tal_inputs: Vec::new(),
        tree,
        publication_points,
        roa_cache_stats,
        cir_input,
        downloads,
        download_stats,
        current_repo_objects: Vec::new(),
        ccr_accumulator: None,
    })
}

fn run_single_root_parallel_audit_inner<H, R>(
    store: Arc<crate::storage::RocksStore>,
    policy: &crate::policy::Policy,
    discovery: DiscoveredRootCaInstance,
    tal_inputs: Vec<TalInputSpec>,
    http_fetcher: &H,
    rsync_fetcher: &R,
    validation_time: time::OffsetDateTime,
    config: &TreeRunConfig,
    parallel_config: ParallelPhase1Config,
    phase2_config: Option<ParallelPhase2Config>,
    collect_current_repo_objects: bool,
    timing: Option<TimingHandle>,
) -> Result<RunTreeFromTalAuditOutput, RunTreeFromTalError>
where
    H: Fetcher + Clone + 'static,
    R: crate::fetch::rsync::RsyncFetcher + Clone + 'static,
{
    let phase2_enabled = phase2_config.is_some();
    let download_log = DownloadLogHandle::new();
    let (runtime, current_repo_index) = build_phase1_repo_sync_runtime(
        Arc::clone(&store),
        policy,
        http_fetcher,
        rsync_fetcher,
        parallel_config,
        timing.clone(),
        Some(download_log.clone()),
        tal_inputs,
        config.enable_transport_request_prefetch,
    )?;
    apply_transport_request_prefetch(
        store.as_ref(),
        &runtime,
        policy.sync_preference,
        validation_time,
        config,
        timing.as_ref(),
    )?;
    let current_repo_index_for_output = current_repo_index.clone();
    let runner = make_live_runner(
        store.as_ref(),
        policy,
        http_fetcher,
        rsync_fetcher,
        validation_time,
        timing.clone(),
        Some(download_log.clone()),
        Some(current_repo_index),
        Some(Arc::clone(&runtime)),
        phase2_config,
        (phase2_enabled && config.build_ccr_accumulator)
            .then(|| CcrAccumulator::new(vec![discovery.trust_anchor.clone()])),
        config.persist_vcir,
        config.enable_roa_validation_cache,
        config.enable_child_certificate_validation_cache,
        config.publication_point_cache_observe_only,
        config.enable_publication_point_validation_cache,
    );

    let root = root_handle_from_trust_anchor(
        &discovery.trust_anchor,
        derive_tal_id(&discovery),
        None,
        &discovery.ca_instance,
    );
    let TreeRunAuditOutput {
        tree,
        publication_points,
        roa_cache_stats,
        cir_input,
    } = if phase2_enabled {
        run_tree_parallel_phase2_audit(root, &runner, config)?
    } else {
        run_tree_serial_audit(root, &runner, config)?
    };
    persist_transport_request_prefetch_snapshot(store.as_ref(), &runtime, config, timing.as_ref())?;
    persist_dead_repo_blacklist(&runtime, timing.as_ref())?;
    let downloads = download_log.snapshot_events();
    let download_stats = DownloadLogHandle::stats_from_events(&downloads);
    Ok(RunTreeFromTalAuditOutput {
        discovery: discovery.clone(),
        discoveries: vec![discovery],
        successful_tal_inputs: Vec::new(),
        tree,
        publication_points,
        roa_cache_stats,
        cir_input,
        downloads,
        download_stats,
        current_repo_objects: snapshot_current_repo_objects(
            Some(&current_repo_index_for_output),
            collect_current_repo_objects,
        ),
        ccr_accumulator: runner.ccr_accumulator_snapshot(),
    })
}

fn run_multi_root_parallel_audit_inner<H, R>(
    store: Arc<crate::storage::RocksStore>,
    policy: &crate::policy::Policy,
    tal_inputs: Vec<TalInputSpec>,
    http_fetcher: &H,
    rsync_fetcher: &R,
    validation_time: time::OffsetDateTime,
    config: &TreeRunConfig,
    parallel_config: ParallelPhase1Config,
    phase2_config: Option<ParallelPhase2Config>,
    collect_current_repo_objects: bool,
    timing: Option<TimingHandle>,
) -> Result<RunTreeFromTalAuditOutput, RunTreeFromTalError>
where
    H: Fetcher + Clone + 'static,
    R: crate::fetch::rsync::RsyncFetcher + Clone + 'static,
{
    let phase2_enabled = phase2_config.is_some();
    if tal_inputs.is_empty() {
        return Err(RunTreeFromTalError::Replay(
            "multi-TAL run requires at least one TAL input".to_string(),
        ));
    }
    let roots = discover_multiple_roots_from_tal_inputs(
        &tal_inputs,
        http_fetcher,
        rsync_fetcher,
        policy.strict.name,
    )?;
    let primary = roots.first().cloned().ok_or_else(|| {
        RunTreeFromTalError::Replay("multi-TAL root discovery returned no roots".to_string())
    })?;
    let discoveries = roots
        .iter()
        .map(|item| item.discovery.clone())
        .collect::<Vec<_>>();
    let successful_tal_inputs = roots
        .iter()
        .map(|item| item.tal_input.clone())
        .collect::<Vec<_>>();
    let root_handles = roots
        .iter()
        .map(|item| item.root_handle.clone())
        .collect::<Vec<_>>();

    let download_log = DownloadLogHandle::new();
    let (runtime, current_repo_index) = build_phase1_repo_sync_runtime(
        Arc::clone(&store),
        policy,
        http_fetcher,
        rsync_fetcher,
        parallel_config,
        timing.clone(),
        Some(download_log.clone()),
        successful_tal_inputs.clone(),
        config.enable_transport_request_prefetch,
    )?;
    apply_transport_request_prefetch(
        store.as_ref(),
        &runtime,
        policy.sync_preference,
        validation_time,
        config,
        timing.as_ref(),
    )?;
    let current_repo_index_for_output = current_repo_index.clone();
    let runner = make_live_runner(
        store.as_ref(),
        policy,
        http_fetcher,
        rsync_fetcher,
        validation_time,
        timing.clone(),
        Some(download_log.clone()),
        Some(current_repo_index),
        Some(Arc::clone(&runtime)),
        phase2_config,
        (phase2_enabled && config.build_ccr_accumulator).then(|| {
            CcrAccumulator::new(
                discoveries
                    .iter()
                    .map(|item| item.trust_anchor.clone())
                    .collect::<Vec<_>>(),
            )
        }),
        config.persist_vcir,
        config.enable_roa_validation_cache,
        config.enable_child_certificate_validation_cache,
        config.publication_point_cache_observe_only,
        config.enable_publication_point_validation_cache,
    );

    let TreeRunAuditOutput {
        tree,
        publication_points,
        roa_cache_stats,
        cir_input,
    } = if phase2_enabled {
        run_tree_parallel_phase2_audit_multi_root(root_handles, &runner, config)?
    } else {
        run_tree_serial_audit_multi_root(root_handles, &runner, config)?
    };
    persist_transport_request_prefetch_snapshot(store.as_ref(), &runtime, config, timing.as_ref())?;
    persist_dead_repo_blacklist(&runtime, timing.as_ref())?;
    let downloads = download_log.snapshot_events();
    let download_stats = DownloadLogHandle::stats_from_events(&downloads);
    Ok(RunTreeFromTalAuditOutput {
        discovery: primary.discovery.clone(),
        discoveries,
        successful_tal_inputs,
        tree,
        publication_points,
        roa_cache_stats,
        cir_input,
        downloads,
        download_stats,
        current_repo_objects: snapshot_current_repo_objects(
            Some(&current_repo_index_for_output),
            collect_current_repo_objects,
        ),
        ccr_accumulator: runner.ccr_accumulator_snapshot(),
    })
}

pub fn run_tree_from_tal_url_parallel_phase1_audit<H, R>(
    store: Arc<crate::storage::RocksStore>,
    policy: &crate::policy::Policy,
    tal_url: &str,
    http_fetcher: &H,
    rsync_fetcher: &R,
    validation_time: time::OffsetDateTime,
    config: &TreeRunConfig,
    parallel_config: ParallelPhase1Config,
    collect_current_repo_objects: bool,
) -> Result<RunTreeFromTalAuditOutput, RunTreeFromTalError>
where
    H: Fetcher + Clone + 'static,
    R: crate::fetch::rsync::RsyncFetcher + Clone + 'static,
{
    let discovery = discover_root_ca_instance_from_tal_url_with_policy(
        policy,
        http_fetcher,
        rsync_fetcher,
        tal_url,
    )?;
    run_single_root_parallel_audit_inner(
        store,
        policy,
        discovery,
        vec![TalInputSpec::from_url(tal_url.to_string())],
        http_fetcher,
        rsync_fetcher,
        validation_time,
        config,
        parallel_config,
        None,
        collect_current_repo_objects,
        None,
    )
}

pub fn run_tree_from_tal_and_ta_der_parallel_phase1_audit<H, R>(
    store: Arc<crate::storage::RocksStore>,
    policy: &crate::policy::Policy,
    tal_bytes: &[u8],
    ta_der: &[u8],
    resolved_ta_uri: Option<&url::Url>,
    http_fetcher: &H,
    rsync_fetcher: &R,
    validation_time: time::OffsetDateTime,
    config: &TreeRunConfig,
    parallel_config: ParallelPhase1Config,
    collect_current_repo_objects: bool,
) -> Result<RunTreeFromTalAuditOutput, RunTreeFromTalError>
where
    H: Fetcher + Clone + 'static,
    R: crate::fetch::rsync::RsyncFetcher + Clone + 'static,
{
    let discovery = discover_root_ca_instance_from_tal_and_ta_der_with_policy(
        policy,
        tal_bytes,
        ta_der,
        resolved_ta_uri,
    )?;
    let derived_tal_id = derive_tal_id(&discovery);
    let tal_inputs = vec![TalInputSpec {
        tal_id: derived_tal_id.clone(),
        rir_id: derived_tal_id,
        source: TalSource::DerBytes {
            tal_url: discovery
                .tal_url
                .clone()
                .unwrap_or_else(|| "embedded-tal".to_string()),
            tal_bytes: tal_bytes.to_vec(),
            ta_der: ta_der.to_vec(),
        },
    }];
    run_single_root_parallel_audit_inner(
        store,
        policy,
        discovery,
        tal_inputs,
        http_fetcher,
        rsync_fetcher,
        validation_time,
        config,
        parallel_config,
        None,
        collect_current_repo_objects,
        None,
    )
}

pub fn run_tree_from_multiple_tals_parallel_phase1_audit<H, R>(
    store: Arc<crate::storage::RocksStore>,
    policy: &crate::policy::Policy,
    tal_inputs: Vec<TalInputSpec>,
    http_fetcher: &H,
    rsync_fetcher: &R,
    validation_time: time::OffsetDateTime,
    config: &TreeRunConfig,
    parallel_config: ParallelPhase1Config,
    collect_current_repo_objects: bool,
) -> Result<RunTreeFromTalAuditOutput, RunTreeFromTalError>
where
    H: Fetcher + Clone + 'static,
    R: crate::fetch::rsync::RsyncFetcher + Clone + 'static,
{
    run_multi_root_parallel_audit_inner(
        store,
        policy,
        tal_inputs,
        http_fetcher,
        rsync_fetcher,
        validation_time,
        config,
        parallel_config,
        None,
        collect_current_repo_objects,
        None,
    )
}

pub fn run_tree_from_tal_url_parallel_phase2_audit<H, R>(
    store: Arc<crate::storage::RocksStore>,
    policy: &crate::policy::Policy,
    tal_url: &str,
    http_fetcher: &H,
    rsync_fetcher: &R,
    validation_time: time::OffsetDateTime,
    config: &TreeRunConfig,
    parallel_config: ParallelPhase1Config,
    phase2_config: ParallelPhase2Config,
    collect_current_repo_objects: bool,
) -> Result<RunTreeFromTalAuditOutput, RunTreeFromTalError>
where
    H: Fetcher + Clone + 'static,
    R: crate::fetch::rsync::RsyncFetcher + Clone + 'static,
{
    let discovery = discover_root_ca_instance_from_tal_url_with_policy(
        policy,
        http_fetcher,
        rsync_fetcher,
        tal_url,
    )?;
    run_single_root_parallel_audit_inner(
        store,
        policy,
        discovery,
        vec![TalInputSpec::from_url(tal_url.to_string())],
        http_fetcher,
        rsync_fetcher,
        validation_time,
        config,
        parallel_config,
        Some(phase2_config),
        collect_current_repo_objects,
        None,
    )
}

pub fn run_tree_from_tal_url_parallel_phase2_audit_with_timing<H, R>(
    store: Arc<crate::storage::RocksStore>,
    policy: &crate::policy::Policy,
    tal_url: &str,
    http_fetcher: &H,
    rsync_fetcher: &R,
    validation_time: time::OffsetDateTime,
    config: &TreeRunConfig,
    parallel_config: ParallelPhase1Config,
    phase2_config: ParallelPhase2Config,
    collect_current_repo_objects: bool,
    timing: &TimingHandle,
) -> Result<RunTreeFromTalAuditOutput, RunTreeFromTalError>
where
    H: Fetcher + Clone + 'static,
    R: crate::fetch::rsync::RsyncFetcher + Clone + 'static,
{
    let discovery = discover_root_ca_instance_from_tal_url_with_policy(
        policy,
        http_fetcher,
        rsync_fetcher,
        tal_url,
    )?;
    run_single_root_parallel_audit_inner(
        store,
        policy,
        discovery,
        vec![TalInputSpec::from_url(tal_url.to_string())],
        http_fetcher,
        rsync_fetcher,
        validation_time,
        config,
        parallel_config,
        Some(phase2_config),
        collect_current_repo_objects,
        Some(timing.clone()),
    )
}

pub fn run_tree_from_tal_and_ta_der_parallel_phase2_audit<H, R>(
    store: Arc<crate::storage::RocksStore>,
    policy: &crate::policy::Policy,
    tal_bytes: &[u8],
    ta_der: &[u8],
    resolved_ta_uri: Option<&url::Url>,
    http_fetcher: &H,
    rsync_fetcher: &R,
    validation_time: time::OffsetDateTime,
    config: &TreeRunConfig,
    parallel_config: ParallelPhase1Config,
    phase2_config: ParallelPhase2Config,
    collect_current_repo_objects: bool,
) -> Result<RunTreeFromTalAuditOutput, RunTreeFromTalError>
where
    H: Fetcher + Clone + 'static,
    R: crate::fetch::rsync::RsyncFetcher + Clone + 'static,
{
    let discovery = discover_root_ca_instance_from_tal_and_ta_der_with_policy(
        policy,
        tal_bytes,
        ta_der,
        resolved_ta_uri,
    )?;
    let derived_tal_id = derive_tal_id(&discovery);
    let tal_inputs = vec![TalInputSpec {
        tal_id: derived_tal_id.clone(),
        rir_id: derived_tal_id,
        source: TalSource::DerBytes {
            tal_url: discovery
                .tal_url
                .clone()
                .unwrap_or_else(|| "embedded-tal".to_string()),
            tal_bytes: tal_bytes.to_vec(),
            ta_der: ta_der.to_vec(),
        },
    }];
    run_single_root_parallel_audit_inner(
        store,
        policy,
        discovery,
        tal_inputs,
        http_fetcher,
        rsync_fetcher,
        validation_time,
        config,
        parallel_config,
        Some(phase2_config),
        collect_current_repo_objects,
        None,
    )
}

pub fn run_tree_from_tal_and_ta_der_parallel_phase2_audit_with_timing<H, R>(
    store: Arc<crate::storage::RocksStore>,
    policy: &crate::policy::Policy,
    tal_bytes: &[u8],
    ta_der: &[u8],
    resolved_ta_uri: Option<&url::Url>,
    http_fetcher: &H,
    rsync_fetcher: &R,
    validation_time: time::OffsetDateTime,
    config: &TreeRunConfig,
    parallel_config: ParallelPhase1Config,
    phase2_config: ParallelPhase2Config,
    collect_current_repo_objects: bool,
    timing: &TimingHandle,
) -> Result<RunTreeFromTalAuditOutput, RunTreeFromTalError>
where
    H: Fetcher + Clone + 'static,
    R: crate::fetch::rsync::RsyncFetcher + Clone + 'static,
{
    let discovery = discover_root_ca_instance_from_tal_and_ta_der_with_policy(
        policy,
        tal_bytes,
        ta_der,
        resolved_ta_uri,
    )?;
    let derived_tal_id = derive_tal_id(&discovery);
    let tal_inputs = vec![TalInputSpec {
        tal_id: derived_tal_id.clone(),
        rir_id: derived_tal_id,
        source: TalSource::DerBytes {
            tal_url: discovery
                .tal_url
                .clone()
                .unwrap_or_else(|| "embedded-tal".to_string()),
            tal_bytes: tal_bytes.to_vec(),
            ta_der: ta_der.to_vec(),
        },
    }];
    run_single_root_parallel_audit_inner(
        store,
        policy,
        discovery,
        tal_inputs,
        http_fetcher,
        rsync_fetcher,
        validation_time,
        config,
        parallel_config,
        Some(phase2_config),
        collect_current_repo_objects,
        Some(timing.clone()),
    )
}

pub fn run_tree_from_multiple_tals_parallel_phase2_audit<H, R>(
    store: Arc<crate::storage::RocksStore>,
    policy: &crate::policy::Policy,
    tal_inputs: Vec<TalInputSpec>,
    http_fetcher: &H,
    rsync_fetcher: &R,
    validation_time: time::OffsetDateTime,
    config: &TreeRunConfig,
    parallel_config: ParallelPhase1Config,
    phase2_config: ParallelPhase2Config,
    collect_current_repo_objects: bool,
) -> Result<RunTreeFromTalAuditOutput, RunTreeFromTalError>
where
    H: Fetcher + Clone + 'static,
    R: crate::fetch::rsync::RsyncFetcher + Clone + 'static,
{
    run_multi_root_parallel_audit_inner(
        store,
        policy,
        tal_inputs,
        http_fetcher,
        rsync_fetcher,
        validation_time,
        config,
        parallel_config,
        Some(phase2_config),
        collect_current_repo_objects,
        None,
    )
}

pub fn run_tree_from_multiple_tals_parallel_phase2_audit_with_timing<H, R>(
    store: Arc<crate::storage::RocksStore>,
    policy: &crate::policy::Policy,
    tal_inputs: Vec<TalInputSpec>,
    http_fetcher: &H,
    rsync_fetcher: &R,
    validation_time: time::OffsetDateTime,
    config: &TreeRunConfig,
    parallel_config: ParallelPhase1Config,
    phase2_config: ParallelPhase2Config,
    collect_current_repo_objects: bool,
    timing: &TimingHandle,
) -> Result<RunTreeFromTalAuditOutput, RunTreeFromTalError>
where
    H: Fetcher + Clone + 'static,
    R: crate::fetch::rsync::RsyncFetcher + Clone + 'static,
{
    run_multi_root_parallel_audit_inner(
        store,
        policy,
        tal_inputs,
        http_fetcher,
        rsync_fetcher,
        validation_time,
        config,
        parallel_config,
        Some(phase2_config),
        collect_current_repo_objects,
        Some(timing.clone()),
    )
}

pub fn run_tree_from_tal_and_ta_der_serial(
    store: &crate::storage::RocksStore,
    policy: &crate::policy::Policy,
    tal_bytes: &[u8],
    ta_der: &[u8],
    resolved_ta_uri: Option<&Url>,
    http_fetcher: &dyn Fetcher,
    rsync_fetcher: &dyn crate::fetch::rsync::RsyncFetcher,
    validation_time: time::OffsetDateTime,
    config: &TreeRunConfig,
) -> Result<RunTreeFromTalOutput, RunTreeFromTalError> {
    let discovery = discover_root_ca_instance_from_tal_and_ta_der_with_policy(
        policy,
        tal_bytes,
        ta_der,
        resolved_ta_uri,
    )?;

    let runner = Rpkiv1PublicationPointRunner {
        store,
        policy,
        http_fetcher,
        rsync_fetcher,
        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: config.enable_roa_validation_cache,
        enable_child_certificate_validation_cache: config.enable_child_certificate_validation_cache,
        publication_point_cache_observe_only: config.publication_point_cache_observe_only,
        enable_publication_point_validation_cache: config.enable_publication_point_validation_cache,
    };

    let root = root_handle_from_trust_anchor(
        &discovery.trust_anchor,
        derive_tal_id(&discovery),
        None,
        &discovery.ca_instance,
    );
    let tree = run_tree_serial(root, &runner, config)?;

    Ok(RunTreeFromTalOutput { discovery, tree })
}

pub fn run_tree_from_tal_bytes_serial_audit(
    store: &crate::storage::RocksStore,
    policy: &crate::policy::Policy,
    tal_bytes: &[u8],
    tal_uri: Option<String>,
    http_fetcher: &dyn Fetcher,
    rsync_fetcher: &dyn crate::fetch::rsync::RsyncFetcher,
    validation_time: time::OffsetDateTime,
    config: &TreeRunConfig,
) -> Result<RunTreeFromTalAuditOutput, RunTreeFromTalError> {
    let tal = crate::data_model::tal::Tal::decode_bytes(tal_bytes).map_err(FromTalError::from)?;
    let discovery = discover_root_ca_instance_from_tal_with_fetchers(
        http_fetcher,
        rsync_fetcher,
        tal,
        tal_uri,
    )?;

    let download_log = DownloadLogHandle::new();
    let runner = Rpkiv1PublicationPointRunner {
        store,
        policy,
        http_fetcher,
        rsync_fetcher,
        validation_time,
        timing: None,
        download_log: Some(download_log.clone()),
        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: config.enable_roa_validation_cache,
        enable_child_certificate_validation_cache: config.enable_child_certificate_validation_cache,
        publication_point_cache_observe_only: config.publication_point_cache_observe_only,
        enable_publication_point_validation_cache: config.enable_publication_point_validation_cache,
    };

    let root = root_handle_from_trust_anchor(
        &discovery.trust_anchor,
        derive_tal_id(&discovery),
        None,
        &discovery.ca_instance,
    );
    let TreeRunAuditOutput {
        tree,
        publication_points,
        roa_cache_stats,
        cir_input,
    } = run_tree_serial_audit(root, &runner, config)?;

    let downloads = download_log.snapshot_events();
    let download_stats = DownloadLogHandle::stats_from_events(&downloads);
    Ok(RunTreeFromTalAuditOutput {
        discovery: discovery.clone(),
        discoveries: vec![discovery],
        successful_tal_inputs: Vec::new(),
        tree,
        publication_points,
        roa_cache_stats,
        cir_input,
        downloads,
        download_stats,
        current_repo_objects: Vec::new(),
        ccr_accumulator: None,
    })
}

pub fn run_tree_from_tal_bytes_serial_audit_with_timing(
    store: &crate::storage::RocksStore,
    policy: &crate::policy::Policy,
    tal_bytes: &[u8],
    tal_uri: Option<String>,
    http_fetcher: &dyn Fetcher,
    rsync_fetcher: &dyn crate::fetch::rsync::RsyncFetcher,
    validation_time: time::OffsetDateTime,
    config: &TreeRunConfig,
    timing: &TimingHandle,
) -> Result<RunTreeFromTalAuditOutput, RunTreeFromTalError> {
    let _tal = timing.span_phase("tal_bootstrap");
    let tal = crate::data_model::tal::Tal::decode_bytes(tal_bytes).map_err(FromTalError::from)?;
    let discovery = discover_root_ca_instance_from_tal_with_fetchers(
        http_fetcher,
        rsync_fetcher,
        tal,
        tal_uri,
    )?;
    drop(_tal);

    let download_log = DownloadLogHandle::new();
    let runner = Rpkiv1PublicationPointRunner {
        store,
        policy,
        http_fetcher,
        rsync_fetcher,
        validation_time,
        timing: Some(timing.clone()),
        download_log: Some(download_log.clone()),
        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: config.enable_roa_validation_cache,
        enable_child_certificate_validation_cache: config.enable_child_certificate_validation_cache,
        publication_point_cache_observe_only: config.publication_point_cache_observe_only,
        enable_publication_point_validation_cache: config.enable_publication_point_validation_cache,
    };

    let root = root_handle_from_trust_anchor(
        &discovery.trust_anchor,
        derive_tal_id(&discovery),
        None,
        &discovery.ca_instance,
    );
    let _tree = timing.span_phase("tree_run");
    let TreeRunAuditOutput {
        tree,
        publication_points,
        roa_cache_stats,
        cir_input,
    } = run_tree_serial_audit(root, &runner, config)?;
    drop(_tree);

    let downloads = download_log.snapshot_events();
    let download_stats = DownloadLogHandle::stats_from_events(&downloads);
    Ok(RunTreeFromTalAuditOutput {
        discovery: discovery.clone(),
        discoveries: vec![discovery],
        successful_tal_inputs: Vec::new(),
        tree,
        publication_points,
        roa_cache_stats,
        cir_input,
        downloads,
        download_stats,
        current_repo_objects: Vec::new(),
        ccr_accumulator: None,
    })
}

pub fn run_tree_from_tal_and_ta_der_serial_audit(
    store: &crate::storage::RocksStore,
    policy: &crate::policy::Policy,
    tal_bytes: &[u8],
    ta_der: &[u8],
    resolved_ta_uri: Option<&Url>,
    http_fetcher: &dyn Fetcher,
    rsync_fetcher: &dyn crate::fetch::rsync::RsyncFetcher,
    validation_time: time::OffsetDateTime,
    config: &TreeRunConfig,
) -> Result<RunTreeFromTalAuditOutput, RunTreeFromTalError> {
    let discovery = discover_root_ca_instance_from_tal_and_ta_der_with_policy(
        policy,
        tal_bytes,
        ta_der,
        resolved_ta_uri,
    )?;

    let download_log = DownloadLogHandle::new();
    let runner = Rpkiv1PublicationPointRunner {
        store,
        policy,
        http_fetcher,
        rsync_fetcher,
        validation_time,
        timing: None,
        download_log: Some(download_log.clone()),
        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: config.enable_roa_validation_cache,
        enable_child_certificate_validation_cache: config.enable_child_certificate_validation_cache,
        publication_point_cache_observe_only: config.publication_point_cache_observe_only,
        enable_publication_point_validation_cache: config.enable_publication_point_validation_cache,
    };

    let root = root_handle_from_trust_anchor(
        &discovery.trust_anchor,
        derive_tal_id(&discovery),
        None,
        &discovery.ca_instance,
    );
    let TreeRunAuditOutput {
        tree,
        publication_points,
        roa_cache_stats,
        cir_input,
    } = run_tree_serial_audit(root, &runner, config)?;

    let downloads = download_log.snapshot_events();
    let download_stats = DownloadLogHandle::stats_from_events(&downloads);
    Ok(RunTreeFromTalAuditOutput {
        discovery: discovery.clone(),
        discoveries: vec![discovery],
        successful_tal_inputs: Vec::new(),
        tree,
        publication_points,
        roa_cache_stats,
        cir_input,
        downloads,
        download_stats,
        current_repo_objects: Vec::new(),
        ccr_accumulator: None,
    })
}

pub fn run_tree_from_tal_and_ta_der_serial_audit_with_timing(
    store: &crate::storage::RocksStore,
    policy: &crate::policy::Policy,
    tal_bytes: &[u8],
    ta_der: &[u8],
    resolved_ta_uri: Option<&Url>,
    http_fetcher: &dyn Fetcher,
    rsync_fetcher: &dyn crate::fetch::rsync::RsyncFetcher,
    validation_time: time::OffsetDateTime,
    config: &TreeRunConfig,
    timing: &TimingHandle,
) -> Result<RunTreeFromTalAuditOutput, RunTreeFromTalError> {
    let _tal = timing.span_phase("tal_bootstrap");
    let discovery = discover_root_ca_instance_from_tal_and_ta_der_with_policy(
        policy,
        tal_bytes,
        ta_der,
        resolved_ta_uri,
    )?;
    drop(_tal);

    let download_log = DownloadLogHandle::new();
    let runner = Rpkiv1PublicationPointRunner {
        store,
        policy,
        http_fetcher,
        rsync_fetcher,
        validation_time,
        timing: Some(timing.clone()),
        download_log: Some(download_log.clone()),
        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: config.enable_roa_validation_cache,
        enable_child_certificate_validation_cache: config.enable_child_certificate_validation_cache,
        publication_point_cache_observe_only: config.publication_point_cache_observe_only,
        enable_publication_point_validation_cache: config.enable_publication_point_validation_cache,
    };

    let root = root_handle_from_trust_anchor(
        &discovery.trust_anchor,
        derive_tal_id(&discovery),
        None,
        &discovery.ca_instance,
    );
    let _tree = timing.span_phase("tree_run_total");
    let TreeRunAuditOutput {
        tree,
        publication_points,
        roa_cache_stats,
        cir_input,
    } = run_tree_serial_audit(root, &runner, config)?;

    let downloads = download_log.snapshot_events();
    let download_stats = DownloadLogHandle::stats_from_events(&downloads);
    Ok(RunTreeFromTalAuditOutput {
        discovery: discovery.clone(),
        discoveries: vec![discovery],
        successful_tal_inputs: Vec::new(),
        tree,
        publication_points,
        roa_cache_stats,
        cir_input,
        downloads,
        download_stats,
        current_repo_objects: Vec::new(),
        ccr_accumulator: None,
    })
}

pub fn run_tree_from_tal_and_ta_der_payload_replay_serial(
    store: &crate::storage::RocksStore,
    policy: &crate::policy::Policy,
    tal_bytes: &[u8],
    ta_der: &[u8],
    resolved_ta_uri: Option<&Url>,
    payload_archive_root: &std::path::Path,
    payload_locks_path: &std::path::Path,
    validation_time: time::OffsetDateTime,
    config: &TreeRunConfig,
) -> Result<RunTreeFromTalOutput, RunTreeFromTalError> {
    let discovery = discover_root_ca_instance_from_tal_and_ta_der_with_policy(
        policy,
        tal_bytes,
        ta_der,
        resolved_ta_uri,
    )?;
    let replay_index = Arc::new(
        ReplayArchiveIndex::load_allow_missing_rsync_modules(
            payload_archive_root,
            payload_locks_path,
        )
        .map_err(|e| RunTreeFromTalError::Replay(e.to_string()))?,
    );
    let http_fetcher = PayloadReplayHttpFetcher::new(replay_index.clone())
        .map_err(|e| RunTreeFromTalError::Replay(e.to_string()))?;
    let rsync_fetcher = PayloadReplayRsyncFetcher::new(replay_index.clone());

    let runner = Rpkiv1PublicationPointRunner {
        store,
        policy,
        http_fetcher: &http_fetcher,
        rsync_fetcher: &rsync_fetcher,
        validation_time,
        timing: None,
        download_log: None,
        replay_archive_index: Some(replay_index),
        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: config.enable_roa_validation_cache,
        enable_child_certificate_validation_cache: config.enable_child_certificate_validation_cache,
        publication_point_cache_observe_only: config.publication_point_cache_observe_only,
        enable_publication_point_validation_cache: config.enable_publication_point_validation_cache,
    };

    let root = root_handle_from_trust_anchor(
        &discovery.trust_anchor,
        derive_tal_id(&discovery),
        None,
        &discovery.ca_instance,
    );
    let tree = run_tree_serial(root, &runner, config)?;

    Ok(RunTreeFromTalOutput { discovery, tree })
}

pub fn run_tree_from_tal_and_ta_der_payload_replay_serial_audit(
    store: &crate::storage::RocksStore,
    policy: &crate::policy::Policy,
    tal_bytes: &[u8],
    ta_der: &[u8],
    resolved_ta_uri: Option<&Url>,
    payload_archive_root: &std::path::Path,
    payload_locks_path: &std::path::Path,
    validation_time: time::OffsetDateTime,
    config: &TreeRunConfig,
) -> Result<RunTreeFromTalAuditOutput, RunTreeFromTalError> {
    let discovery = discover_root_ca_instance_from_tal_and_ta_der_with_policy(
        policy,
        tal_bytes,
        ta_der,
        resolved_ta_uri,
    )?;
    let replay_index = Arc::new(
        ReplayArchiveIndex::load_allow_missing_rsync_modules(
            payload_archive_root,
            payload_locks_path,
        )
        .map_err(|e| RunTreeFromTalError::Replay(e.to_string()))?,
    );
    let http_fetcher = PayloadReplayHttpFetcher::new(replay_index.clone())
        .map_err(|e| RunTreeFromTalError::Replay(e.to_string()))?;
    let rsync_fetcher = PayloadReplayRsyncFetcher::new(replay_index.clone());
    let download_log = DownloadLogHandle::new();

    let runner = Rpkiv1PublicationPointRunner {
        store,
        policy,
        http_fetcher: &http_fetcher,
        rsync_fetcher: &rsync_fetcher,
        validation_time,
        timing: None,
        download_log: Some(download_log.clone()),
        replay_archive_index: Some(replay_index),
        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: config.enable_roa_validation_cache,
        enable_child_certificate_validation_cache: config.enable_child_certificate_validation_cache,
        publication_point_cache_observe_only: config.publication_point_cache_observe_only,
        enable_publication_point_validation_cache: config.enable_publication_point_validation_cache,
    };

    let root = root_handle_from_trust_anchor(
        &discovery.trust_anchor,
        derive_tal_id(&discovery),
        None,
        &discovery.ca_instance,
    );
    let TreeRunAuditOutput {
        tree,
        publication_points,
        roa_cache_stats,
        cir_input,
    } = run_tree_serial_audit(root, &runner, config)?;

    let downloads = download_log.snapshot_events();
    let download_stats = DownloadLogHandle::stats_from_events(&downloads);
    Ok(RunTreeFromTalAuditOutput {
        discovery: discovery.clone(),
        discoveries: vec![discovery],
        successful_tal_inputs: Vec::new(),
        tree,
        publication_points,
        roa_cache_stats,
        cir_input,
        downloads,
        download_stats,
        current_repo_objects: Vec::new(),
        ccr_accumulator: None,
    })
}

pub fn run_tree_from_tal_and_ta_der_payload_replay_serial_audit_with_timing(
    store: &crate::storage::RocksStore,
    policy: &crate::policy::Policy,
    tal_bytes: &[u8],
    ta_der: &[u8],
    resolved_ta_uri: Option<&Url>,
    payload_archive_root: &std::path::Path,
    payload_locks_path: &std::path::Path,
    validation_time: time::OffsetDateTime,
    config: &TreeRunConfig,
    timing: &TimingHandle,
) -> Result<RunTreeFromTalAuditOutput, RunTreeFromTalError> {
    let _tal = timing.span_phase("tal_bootstrap");
    let discovery = discover_root_ca_instance_from_tal_and_ta_der_with_policy(
        policy,
        tal_bytes,
        ta_der,
        resolved_ta_uri,
    )?;
    drop(_tal);
    let replay_index = Arc::new(
        ReplayArchiveIndex::load_allow_missing_rsync_modules(
            payload_archive_root,
            payload_locks_path,
        )
        .map_err(|e| RunTreeFromTalError::Replay(e.to_string()))?,
    );
    let http_fetcher = PayloadReplayHttpFetcher::new(replay_index.clone())
        .map_err(|e| RunTreeFromTalError::Replay(e.to_string()))?;
    let rsync_fetcher = PayloadReplayRsyncFetcher::new(replay_index.clone());
    let download_log = DownloadLogHandle::new();

    let runner = Rpkiv1PublicationPointRunner {
        store,
        policy,
        http_fetcher: &http_fetcher,
        rsync_fetcher: &rsync_fetcher,
        validation_time,
        timing: Some(timing.clone()),
        download_log: Some(download_log.clone()),
        replay_archive_index: Some(replay_index),
        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: config.enable_roa_validation_cache,
        enable_child_certificate_validation_cache: config.enable_child_certificate_validation_cache,
        publication_point_cache_observe_only: config.publication_point_cache_observe_only,
        enable_publication_point_validation_cache: config.enable_publication_point_validation_cache,
    };

    let root = root_handle_from_trust_anchor(
        &discovery.trust_anchor,
        derive_tal_id(&discovery),
        None,
        &discovery.ca_instance,
    );
    let _tree = timing.span_phase("tree_run_total");
    let TreeRunAuditOutput {
        tree,
        publication_points,
        roa_cache_stats,
        cir_input,
    } = run_tree_serial_audit(root, &runner, config)?;

    let downloads = download_log.snapshot_events();
    let download_stats = DownloadLogHandle::stats_from_events(&downloads);
    Ok(RunTreeFromTalAuditOutput {
        discovery: discovery.clone(),
        discoveries: vec![discovery],
        successful_tal_inputs: Vec::new(),
        tree,
        publication_points,
        roa_cache_stats,
        cir_input,
        downloads,
        download_stats,
        current_repo_objects: Vec::new(),
        ccr_accumulator: None,
    })
}

fn build_payload_replay_runner<'a>(
    store: &'a crate::storage::RocksStore,
    policy: &'a crate::policy::Policy,
    replay_index: Arc<ReplayArchiveIndex>,
    http_fetcher: &'a PayloadReplayHttpFetcher,
    rsync_fetcher: &'a PayloadReplayRsyncFetcher,
    validation_time: time::OffsetDateTime,
    timing: Option<TimingHandle>,
    download_log: Option<DownloadLogHandle>,
    enable_roa_validation_cache: bool,
) -> Rpkiv1PublicationPointRunner<'a> {
    Rpkiv1PublicationPointRunner {
        store,
        policy,
        http_fetcher,
        rsync_fetcher,
        validation_time,
        timing,
        download_log,
        replay_archive_index: Some(replay_index),
        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,
        enable_child_certificate_validation_cache: false,
        publication_point_cache_observe_only: false,
        enable_publication_point_validation_cache: false,
    }
}

fn build_payload_delta_replay_runner<'a>(
    store: &'a crate::storage::RocksStore,
    policy: &'a crate::policy::Policy,
    delta_index: Arc<ReplayDeltaArchiveIndex>,
    http_fetcher: &'a PayloadDeltaReplayHttpFetcher,
    rsync_fetcher: &'a PayloadDeltaReplayRsyncFetcher,
    validation_time: time::OffsetDateTime,
    timing: Option<TimingHandle>,
    download_log: Option<DownloadLogHandle>,
    enable_roa_validation_cache: bool,
) -> Rpkiv1PublicationPointRunner<'a> {
    Rpkiv1PublicationPointRunner {
        store,
        policy,
        http_fetcher,
        rsync_fetcher,
        validation_time,
        timing,
        download_log,
        replay_archive_index: None,
        replay_delta_index: Some(delta_index),
        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,
        enable_child_certificate_validation_cache: false,
        publication_point_cache_observe_only: false,
        enable_publication_point_validation_cache: false,
    }
}

fn build_payload_delta_replay_current_store_runner<'a>(
    store: &'a crate::storage::RocksStore,
    policy: &'a crate::policy::Policy,
    delta_index: Arc<ReplayDeltaArchiveIndex>,
    http_fetcher: &'a PayloadDeltaReplayHttpFetcher,
    rsync_fetcher: &'a PayloadDeltaReplayCurrentStoreRsyncFetcher<'a>,
    validation_time: time::OffsetDateTime,
    timing: Option<TimingHandle>,
    download_log: Option<DownloadLogHandle>,
    enable_roa_validation_cache: bool,
) -> Rpkiv1PublicationPointRunner<'a> {
    Rpkiv1PublicationPointRunner {
        store,
        policy,
        http_fetcher,
        rsync_fetcher,
        validation_time,
        timing,
        download_log,
        replay_archive_index: None,
        replay_delta_index: Some(delta_index),
        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,
        enable_child_certificate_validation_cache: false,
        publication_point_cache_observe_only: false,
        enable_publication_point_validation_cache: false,
    }
}

fn run_payload_delta_replay_audit_inner(
    store: &crate::storage::RocksStore,
    policy: &crate::policy::Policy,
    discovery: DiscoveredRootCaInstance,
    base_payload_archive_root: &std::path::Path,
    base_locks_path: &std::path::Path,
    delta_payload_archive_root: &std::path::Path,
    delta_locks_path: &std::path::Path,
    base_validation_time: time::OffsetDateTime,
    validation_time: time::OffsetDateTime,
    config: &TreeRunConfig,
    timing: Option<TimingHandle>,
) -> Result<RunTreeFromTalAuditOutput, RunTreeFromTalError> {
    let base_index = Arc::new(
        ReplayArchiveIndex::load_allow_missing_rsync_modules(
            base_payload_archive_root,
            base_locks_path,
        )
        .map_err(|e| RunTreeFromTalError::Replay(e.to_string()))?,
    );
    let delta_index = Arc::new(
        ReplayDeltaArchiveIndex::load(delta_payload_archive_root, delta_locks_path)
            .map_err(|e| RunTreeFromTalError::Replay(e.to_string()))?,
    );
    delta_index
        .validate_base_locks_sha256_file(base_locks_path)
        .map_err(|e| RunTreeFromTalError::Replay(e.to_string()))?;

    let root = root_handle_from_trust_anchor(
        &discovery.trust_anchor,
        derive_tal_id(&discovery),
        None,
        &discovery.ca_instance,
    );

    let base_http_fetcher = PayloadReplayHttpFetcher::new(base_index.clone())
        .map_err(|e| RunTreeFromTalError::Replay(e.to_string()))?;
    let base_rsync_fetcher = PayloadReplayRsyncFetcher::new(base_index.clone());
    if let Some(t) = timing.as_ref() {
        let _phase = t.span_phase("payload_delta_replay_base_total");
        let base_runner = build_payload_replay_runner(
            store,
            policy,
            base_index.clone(),
            &base_http_fetcher,
            &base_rsync_fetcher,
            base_validation_time,
            Some(t.clone()),
            None,
            config.enable_roa_validation_cache,
        );
        let _base = run_tree_serial(root.clone(), &base_runner, config)?;
    } else {
        let base_runner = build_payload_replay_runner(
            store,
            policy,
            base_index.clone(),
            &base_http_fetcher,
            &base_rsync_fetcher,
            base_validation_time,
            None,
            None,
            config.enable_roa_validation_cache,
        );
        let _base = run_tree_serial(root.clone(), &base_runner, config)?;
    }

    let delta_http_fetcher = PayloadDeltaReplayHttpFetcher::from_index(delta_index.clone())
        .map_err(|e| RunTreeFromTalError::Replay(e.to_string()))?;
    let delta_rsync_fetcher = PayloadDeltaReplayRsyncFetcher::new(base_index, delta_index.clone());
    let download_log = DownloadLogHandle::new();
    let (tree, publication_points, roa_cache_stats, cir_input) = if let Some(t) = timing.as_ref() {
        let _phase = t.span_phase("payload_delta_replay_target_total");
        let delta_runner = build_payload_delta_replay_runner(
            store,
            policy,
            delta_index,
            &delta_http_fetcher,
            &delta_rsync_fetcher,
            base_validation_time,
            Some(t.clone()),
            Some(download_log.clone()),
            config.enable_roa_validation_cache,
        );
        let TreeRunAuditOutput {
            tree,
            publication_points,
            roa_cache_stats,
            cir_input,
        } = run_tree_serial_audit(root, &delta_runner, config)?;
        (tree, publication_points, roa_cache_stats, cir_input)
    } else {
        let delta_runner = build_payload_delta_replay_runner(
            store,
            policy,
            delta_index,
            &delta_http_fetcher,
            &delta_rsync_fetcher,
            validation_time,
            None,
            Some(download_log.clone()),
            config.enable_roa_validation_cache,
        );
        let TreeRunAuditOutput {
            tree,
            publication_points,
            roa_cache_stats,
            cir_input,
        } = run_tree_serial_audit(root, &delta_runner, config)?;
        (tree, publication_points, roa_cache_stats, cir_input)
    };
    let downloads = download_log.snapshot_events();
    let download_stats = DownloadLogHandle::stats_from_events(&downloads);
    Ok(RunTreeFromTalAuditOutput {
        discovery: discovery.clone(),
        discoveries: vec![discovery],
        successful_tal_inputs: Vec::new(),
        tree,
        publication_points,
        roa_cache_stats,
        cir_input,
        downloads,
        download_stats,
        current_repo_objects: Vec::new(),
        ccr_accumulator: None,
    })
}

pub fn run_tree_from_tal_and_ta_der_payload_delta_replay_serial_audit(
    store: &crate::storage::RocksStore,
    policy: &crate::policy::Policy,
    tal_bytes: &[u8],
    ta_der: &[u8],
    resolved_ta_uri: Option<&Url>,
    base_payload_archive_root: &std::path::Path,
    base_locks_path: &std::path::Path,
    delta_payload_archive_root: &std::path::Path,
    delta_locks_path: &std::path::Path,
    base_validation_time: time::OffsetDateTime,
    validation_time: time::OffsetDateTime,
    config: &TreeRunConfig,
) -> Result<RunTreeFromTalAuditOutput, RunTreeFromTalError> {
    let discovery = discover_root_ca_instance_from_tal_and_ta_der_with_policy(
        policy,
        tal_bytes,
        ta_der,
        resolved_ta_uri,
    )?;
    run_payload_delta_replay_audit_inner(
        store,
        policy,
        discovery,
        base_payload_archive_root,
        base_locks_path,
        delta_payload_archive_root,
        delta_locks_path,
        base_validation_time,
        validation_time,
        config,
        None,
    )
}

pub fn run_tree_from_tal_and_ta_der_payload_delta_replay_serial_audit_with_timing(
    store: &crate::storage::RocksStore,
    policy: &crate::policy::Policy,
    tal_bytes: &[u8],
    ta_der: &[u8],
    resolved_ta_uri: Option<&Url>,
    base_payload_archive_root: &std::path::Path,
    base_locks_path: &std::path::Path,
    delta_payload_archive_root: &std::path::Path,
    delta_locks_path: &std::path::Path,
    base_validation_time: time::OffsetDateTime,
    validation_time: time::OffsetDateTime,
    config: &TreeRunConfig,
    timing: &TimingHandle,
) -> Result<RunTreeFromTalAuditOutput, RunTreeFromTalError> {
    let _tal = timing.span_phase("tal_bootstrap");
    let discovery = discover_root_ca_instance_from_tal_and_ta_der_with_policy(
        policy,
        tal_bytes,
        ta_der,
        resolved_ta_uri,
    )?;
    drop(_tal);
    run_payload_delta_replay_audit_inner(
        store,
        policy,
        discovery,
        base_payload_archive_root,
        base_locks_path,
        delta_payload_archive_root,
        delta_locks_path,
        base_validation_time,
        validation_time,
        config,
        Some(timing.clone()),
    )
}

fn run_payload_delta_replay_step_audit_inner(
    store: &crate::storage::RocksStore,
    policy: &crate::policy::Policy,
    discovery: DiscoveredRootCaInstance,
    delta_payload_archive_root: &std::path::Path,
    previous_locks_path: &std::path::Path,
    delta_locks_path: &std::path::Path,
    validation_time: time::OffsetDateTime,
    config: &TreeRunConfig,
    timing: Option<TimingHandle>,
) -> Result<RunTreeFromTalAuditOutput, RunTreeFromTalError> {
    let delta_index = Arc::new(
        ReplayDeltaArchiveIndex::load(delta_payload_archive_root, delta_locks_path)
            .map_err(|e| RunTreeFromTalError::Replay(e.to_string()))?,
    );
    delta_index
        .validate_base_locks_sha256_file(previous_locks_path)
        .map_err(|e| RunTreeFromTalError::Replay(e.to_string()))?;

    let root = root_handle_from_trust_anchor(
        &discovery.trust_anchor,
        derive_tal_id(&discovery),
        None,
        &discovery.ca_instance,
    );

    let delta_http_fetcher = PayloadDeltaReplayHttpFetcher::from_index(delta_index.clone())
        .map_err(|e| RunTreeFromTalError::Replay(e.to_string()))?;
    let delta_rsync_fetcher =
        PayloadDeltaReplayCurrentStoreRsyncFetcher::new(store, delta_index.clone());
    let download_log = DownloadLogHandle::new();

    let (tree, publication_points, roa_cache_stats, cir_input) = if let Some(t) = timing.as_ref() {
        let _phase = t.span_phase("payload_delta_replay_step_total");
        let delta_runner = build_payload_delta_replay_current_store_runner(
            store,
            policy,
            delta_index,
            &delta_http_fetcher,
            &delta_rsync_fetcher,
            validation_time,
            Some(t.clone()),
            Some(download_log.clone()),
            config.enable_roa_validation_cache,
        );
        let TreeRunAuditOutput {
            tree,
            publication_points,
            roa_cache_stats,
            cir_input,
        } = run_tree_serial_audit(root, &delta_runner, config)?;
        (tree, publication_points, roa_cache_stats, cir_input)
    } else {
        let delta_runner = build_payload_delta_replay_current_store_runner(
            store,
            policy,
            delta_index,
            &delta_http_fetcher,
            &delta_rsync_fetcher,
            validation_time,
            None,
            Some(download_log.clone()),
            config.enable_roa_validation_cache,
        );
        let TreeRunAuditOutput {
            tree,
            publication_points,
            roa_cache_stats,
            cir_input,
        } = run_tree_serial_audit(root, &delta_runner, config)?;
        (tree, publication_points, roa_cache_stats, cir_input)
    };
    let downloads = download_log.snapshot_events();
    let download_stats = DownloadLogHandle::stats_from_events(&downloads);
    Ok(RunTreeFromTalAuditOutput {
        discovery: discovery.clone(),
        discoveries: vec![discovery],
        successful_tal_inputs: Vec::new(),
        tree,
        publication_points,
        roa_cache_stats,
        cir_input,
        downloads,
        download_stats,
        current_repo_objects: Vec::new(),
        ccr_accumulator: None,
    })
}

pub fn run_tree_from_tal_and_ta_der_payload_delta_replay_step_serial_audit(
    store: &crate::storage::RocksStore,
    policy: &crate::policy::Policy,
    tal_bytes: &[u8],
    ta_der: &[u8],
    resolved_ta_uri: Option<&Url>,
    delta_payload_archive_root: &std::path::Path,
    previous_locks_path: &std::path::Path,
    delta_locks_path: &std::path::Path,
    validation_time: time::OffsetDateTime,
    config: &TreeRunConfig,
) -> Result<RunTreeFromTalAuditOutput, RunTreeFromTalError> {
    let discovery = discover_root_ca_instance_from_tal_and_ta_der_with_policy(
        policy,
        tal_bytes,
        ta_der,
        resolved_ta_uri,
    )?;
    run_payload_delta_replay_step_audit_inner(
        store,
        policy,
        discovery,
        delta_payload_archive_root,
        previous_locks_path,
        delta_locks_path,
        validation_time,
        config,
        None,
    )
}

#[cfg(test)]
mod multi_tal_tests {
    use super::*;
    use crate::current_repo_index::CurrentRepoIndex;
    use crate::storage::{RepositoryViewEntry, RepositoryViewState};

    struct RejectingHttpFetcher;

    impl Fetcher for RejectingHttpFetcher {
        fn fetch(&self, uri: &str) -> Result<Vec<u8>, String> {
            Err(format!("unexpected http fetch: {uri}"))
        }
    }

    struct RejectingRsyncFetcher;

    impl crate::fetch::rsync::RsyncFetcher for RejectingRsyncFetcher {
        fn fetch_objects(
            &self,
            _rsync_base_uri: &str,
        ) -> crate::fetch::rsync::RsyncFetchResult<Vec<(String, Vec<u8>)>> {
            Err(crate::fetch::rsync::RsyncFetchError::Fetch(
                "unexpected rsync fetch".to_string(),
            ))
        }

        fn dedup_key(&self, base_uri: &str) -> String {
            base_uri.to_string()
        }
    }

    #[test]
    fn snapshot_current_repo_objects_is_on_demand() {
        let handle = CurrentRepoIndex::shared();
        handle
            .write()
            .expect("write-lock index")
            .apply_repository_view_entries(&[RepositoryViewEntry {
                rsync_uri: "rsync://example.test/repo/a.roa".to_string(),
                current_hash: Some("11".repeat(32)),
                repository_source: Some("rsync://example.test/repo/".to_string()),
                object_type: Some("roa".to_string()),
                state: RepositoryViewState::Present,
            }])
            .expect("apply present entry");

        assert!(
            snapshot_current_repo_objects(Some(&handle), false).is_empty(),
            "collection should be skipped when disabled"
        );

        let collected = snapshot_current_repo_objects(Some(&handle), true);
        assert_eq!(collected.len(), 1);
        assert_eq!(collected[0].rsync_uri, "rsync://example.test/repo/a.roa");
    }

    #[test]
    fn discover_multiple_roots_from_tal_inputs_builds_multiple_root_handles() {
        let apnic_tal =
            std::fs::read("tests/fixtures/tal/apnic-rfc7730-https.tal").expect("read apnic tal");
        let apnic_ta = std::fs::read("tests/fixtures/ta/apnic-ta.cer").expect("read apnic ta");
        let arin_tal = std::fs::read("tests/fixtures/tal/arin.tal").expect("read arin tal");
        let arin_ta = std::fs::read("tests/fixtures/ta/arin-ta.cer").expect("read arin ta");

        let tal_inputs = vec![
            TalInputSpec::from_ta_der("https://example.test/apnic.tal", apnic_tal, apnic_ta),
            TalInputSpec::from_ta_der("https://example.test/arin.tal", arin_tal, arin_ta),
        ];

        let roots = discover_multiple_roots_from_tal_inputs(
            &tal_inputs,
            &RejectingHttpFetcher,
            &RejectingRsyncFetcher,
            false,
        )
        .expect("discover roots");

        assert_eq!(roots.len(), 2);
        assert_eq!(roots[0].tal_input.tal_id, "apnic");
        assert_eq!(roots[1].tal_input.tal_id, "arin");
        assert_eq!(roots[0].root_handle.tal_id, "apnic");
        assert_eq!(roots[1].root_handle.tal_id, "arin");
        assert_ne!(
            roots[0].root_handle.manifest_rsync_uri,
            roots[1].root_handle.manifest_rsync_uri
        );
    }

    #[test]
    fn discover_multiple_roots_isolates_strict_name_failure() {
        let apnic_tal =
            std::fs::read("tests/fixtures/tal/apnic-rfc7730-https.tal").expect("read apnic tal");
        let apnic_ta = std::fs::read("tests/fixtures/ta/apnic-ta.cer").expect("read apnic ta");
        let arin_tal = std::fs::read("tests/fixtures/tal/arin.tal").expect("read arin tal");
        let arin_ta = std::fs::read("tests/fixtures/ta/arin-ta.cer").expect("read arin ta");

        let tal_inputs = vec![
            TalInputSpec::from_ta_der("https://example.test/apnic.tal", apnic_tal, apnic_ta),
            TalInputSpec::from_ta_der("https://example.test/arin.tal", arin_tal, arin_ta),
        ];

        let roots = discover_multiple_roots_from_tal_inputs(
            &tal_inputs,
            &RejectingHttpFetcher,
            &RejectingRsyncFetcher,
            true,
        )
        .expect("strict discovery should keep usable roots");

        assert_eq!(roots.len(), 1);
        assert_eq!(roots[0].tal_input.tal_id, "arin");
        assert_eq!(roots[0].root_handle.tal_id, "arin");
    }

    #[test]
    fn discover_single_root_keeps_strict_name_failure_fatal() {
        let apnic_tal =
            std::fs::read("tests/fixtures/tal/apnic-rfc7730-https.tal").expect("read apnic tal");
        let apnic_ta = std::fs::read("tests/fixtures/ta/apnic-ta.cer").expect("read apnic ta");
        let tal_inputs = vec![TalInputSpec::from_ta_der(
            "https://example.test/apnic.tal",
            apnic_tal,
            apnic_ta,
        )];

        let error = discover_multiple_roots_from_tal_inputs(
            &tal_inputs,
            &RejectingHttpFetcher,
            &RejectingRsyncFetcher,
            true,
        )
        .expect_err("single-TAL strict failure should remain fatal");

        assert!(error.to_string().contains("Name strict validation failed"));
    }
}

#[cfg(test)]
mod replay_api_tests {
    use super::*;
    use crate::analysis::timing::{TimingHandle, TimingMeta};
    use time::format_description::well_known::Rfc3339;

    fn apnic_replay_inputs() -> (
        Vec<u8>,
        Vec<u8>,
        std::path::PathBuf,
        std::path::PathBuf,
        time::OffsetDateTime,
    ) {
        let tal_bytes = std::fs::read("tests/fixtures/tal/apnic-rfc7730-https.tal")
            .expect("read apnic tal fixture");
        let ta_der =
            std::fs::read("tests/fixtures/ta/apnic-ta.cer").expect("read apnic ta fixture");
        let archive_root = std::path::PathBuf::from("target/live/payload_replay/payload-archive");
        let locks_path = std::path::PathBuf::from("target/live/payload_replay/locks.json");
        let validation_time = time::OffsetDateTime::parse("2026-03-13T02:30:00Z", &Rfc3339)
            .expect("parse validation time");
        (tal_bytes, ta_der, archive_root, locks_path, validation_time)
    }

    fn apnic_multi_rir_replay_inputs() -> (
        Vec<u8>,
        Vec<u8>,
        std::path::PathBuf,
        std::path::PathBuf,
        time::OffsetDateTime,
    ) {
        let tal_bytes = std::fs::read("tests/fixtures/tal/apnic-rfc7730-https.tal")
            .expect("read apnic tal fixture");
        let ta_der =
            std::fs::read("tests/fixtures/ta/apnic-ta.cer").expect("read apnic ta fixture");
        let archive_root = std::path::PathBuf::from(
            "../../rpki/target/live/20260316-112341-multi-final3/apnic/base-payload-archive",
        );
        let locks_path = std::path::PathBuf::from(
            "../../rpki/target/live/20260316-112341-multi-final3/apnic/base-locks.json",
        );
        let validation_time = time::OffsetDateTime::parse("2026-03-16T11:49:48+08:00", &Rfc3339)
            .expect("parse validation time");
        (tal_bytes, ta_der, archive_root, locks_path, validation_time)
    }

    fn apnic_delta_replay_inputs() -> (
        Vec<u8>,
        Vec<u8>,
        std::path::PathBuf,
        std::path::PathBuf,
        std::path::PathBuf,
        std::path::PathBuf,
        time::OffsetDateTime,
    ) {
        let tal_bytes = std::fs::read("tests/fixtures/tal/apnic-rfc7730-https.tal")
            .expect("read apnic tal fixture");
        let ta_der =
            std::fs::read("tests/fixtures/ta/apnic-ta.cer").expect("read apnic ta fixture");
        let root =
            std::path::PathBuf::from("target/live/apnic_delta_demo/20260315-170223-autoplay");
        let base_archive = root.join("base-payload-archive");
        let base_locks = root.join("base-locks.json");
        let delta_archive = root.join("payload-delta-archive");
        let delta_locks = root.join("locks-delta.json");
        let validation_time = time::OffsetDateTime::parse("2026-03-15T10:00:00Z", &Rfc3339)
            .expect("parse validation time");
        (
            tal_bytes,
            ta_der,
            base_archive,
            base_locks,
            delta_archive,
            delta_locks,
            validation_time,
        )
    }

    #[test]
    fn payload_replay_api_reports_setup_error_for_missing_archive() {
        let temp = tempfile::tempdir().expect("tempdir");
        let store = crate::storage::RocksStore::open(&temp.path().join("db")).expect("open db");
        let tal_bytes = std::fs::read("tests/fixtures/tal/apnic-rfc7730-https.tal")
            .expect("read apnic tal fixture");
        let ta_der =
            std::fs::read("tests/fixtures/ta/apnic-ta.cer").expect("read apnic ta fixture");
        let err = run_tree_from_tal_and_ta_der_payload_replay_serial_audit(
            &store,
            &crate::policy::Policy::default(),
            &tal_bytes,
            &ta_der,
            None,
            std::path::Path::new("tests/fixtures/missing-payload-archive"),
            std::path::Path::new("tests/fixtures/missing-locks.json"),
            time::OffsetDateTime::now_utc(),
            &TreeRunConfig {
                max_depth: Some(0),
                max_instances: Some(1),
                compact_audit: false,
                persist_vcir: true,
                build_ccr_accumulator: true,
                enable_roa_validation_cache: false,
                enable_child_certificate_validation_cache: false,
                publication_point_cache_observe_only: false,
                enable_publication_point_validation_cache: false,
                enable_transport_request_prefetch: false,
            },
        )
        .unwrap_err();
        assert!(matches!(err, RunTreeFromTalError::Replay(_)), "{err}");
    }

    #[test]
    fn payload_replay_api_root_only_apnic_archive_runs() {
        let temp = tempfile::tempdir().expect("tempdir");
        let store = crate::storage::RocksStore::open(&temp.path().join("db")).expect("open db");
        let (tal_bytes, ta_der, archive_root, locks_path, validation_time) = apnic_replay_inputs();
        if !archive_root.is_dir() || !locks_path.is_file() {
            eprintln!(
                "skipping payload replay api test; missing fixtures: archive={} locks={}",
                archive_root.display(),
                locks_path.display()
            );
            return;
        }

        let out = run_tree_from_tal_and_ta_der_payload_replay_serial_audit(
            &store,
            &crate::policy::Policy::default(),
            &tal_bytes,
            &ta_der,
            None,
            &archive_root,
            &locks_path,
            validation_time,
            &TreeRunConfig {
                max_depth: Some(0),
                max_instances: Some(1),
                compact_audit: false,
                persist_vcir: true,
                build_ccr_accumulator: true,
                enable_roa_validation_cache: false,
                enable_child_certificate_validation_cache: false,
                publication_point_cache_observe_only: false,
                enable_publication_point_validation_cache: false,
                enable_transport_request_prefetch: false,
            },
        )
        .expect("run replay root-only audit");

        assert_eq!(out.tree.instances_processed, 1);
        assert_eq!(out.tree.instances_failed, 0);
        assert_eq!(out.publication_points.len(), 1);
        assert_eq!(out.discovery.trust_anchor.resolved_ta_uri, None);
        assert!(!out.downloads.is_empty());
        assert!(
            out.downloads.iter().all(|d| d.success),
            "expected successful replay downloads"
        );
    }

    #[test]
    fn payload_replay_api_root_only_apnic_multi_rir_bundle_runs_with_lenient_rsync_modules() {
        let temp = tempfile::tempdir().expect("tempdir");
        let store = crate::storage::RocksStore::open(&temp.path().join("db")).expect("open db");
        let (tal_bytes, ta_der, archive_root, locks_path, validation_time) =
            apnic_multi_rir_replay_inputs();
        if !archive_root.is_dir() || !locks_path.is_file() {
            eprintln!(
                "skipping multi-rir payload replay api test; missing fixtures: archive={} locks={}",
                archive_root.display(),
                locks_path.display()
            );
            return;
        }

        let out = run_tree_from_tal_and_ta_der_payload_replay_serial_audit(
            &store,
            &crate::policy::Policy::default(),
            &tal_bytes,
            &ta_der,
            None,
            &archive_root,
            &locks_path,
            validation_time,
            &TreeRunConfig {
                max_depth: Some(0),
                max_instances: Some(1),
                compact_audit: false,
                persist_vcir: true,
                build_ccr_accumulator: true,
                enable_roa_validation_cache: false,
                enable_child_certificate_validation_cache: false,
                publication_point_cache_observe_only: false,
                enable_publication_point_validation_cache: false,
                enable_transport_request_prefetch: false,
            },
        )
        .expect("run replay root-only audit");

        assert_eq!(out.tree.instances_processed, 1);
        assert_eq!(out.tree.instances_failed, 0);
        assert_eq!(out.publication_points.len(), 1);
    }

    #[test]
    fn payload_replay_api_root_only_apnic_archive_runs_with_timing() {
        let temp = tempfile::tempdir().expect("tempdir");
        let db_path = temp.path().join("db");
        let store = crate::storage::RocksStore::open(&db_path).expect("open db");
        let (tal_bytes, ta_der, archive_root, locks_path, validation_time) = apnic_replay_inputs();
        if !archive_root.is_dir() || !locks_path.is_file() {
            eprintln!(
                "skipping payload replay api timing test; missing fixtures: archive={} locks={}",
                archive_root.display(),
                locks_path.display()
            );
            return;
        }

        let timing = TimingHandle::new(TimingMeta {
            recorded_at_utc_rfc3339: "2026-03-13T03:00:00Z".to_string(),
            validation_time_utc_rfc3339: "2026-03-13T02:30:00Z".to_string(),
            tal_url: None,
            db_path: Some(db_path.to_string_lossy().into_owned()),
        });

        let out = run_tree_from_tal_and_ta_der_payload_replay_serial_audit_with_timing(
            &store,
            &crate::policy::Policy::default(),
            &tal_bytes,
            &ta_der,
            None,
            &archive_root,
            &locks_path,
            validation_time,
            &TreeRunConfig {
                max_depth: Some(0),
                max_instances: Some(1),
                compact_audit: false,
                persist_vcir: true,
                build_ccr_accumulator: true,
                enable_roa_validation_cache: false,
                enable_child_certificate_validation_cache: false,
                publication_point_cache_observe_only: false,
                enable_publication_point_validation_cache: false,
                enable_transport_request_prefetch: false,
            },
            &timing,
        )
        .expect("run replay root-only audit with timing");

        assert_eq!(out.tree.instances_processed, 1);
        let timing_json = temp.path().join("timing_replay.json");
        timing
            .write_json(&timing_json, 20)
            .expect("write timing json");
        let json: serde_json::Value =
            serde_json::from_slice(&std::fs::read(&timing_json).expect("read timing json"))
                .expect("parse timing json");
        let counts = json.get("counts").expect("counts");
        assert!(
            counts
                .get("repo_sync_rrdp_ok_total")
                .and_then(|v| v.as_u64())
                .unwrap_or(0)
                >= 1
        );
    }

    #[test]
    fn payload_delta_replay_api_rejects_base_locks_sha_mismatch() {
        let temp = tempfile::tempdir().expect("tempdir");
        let store = crate::storage::RocksStore::open(&temp.path().join("db")).expect("open db");
        let (
            tal_bytes,
            ta_der,
            base_archive,
            _base_locks,
            delta_archive,
            delta_locks,
            validation_time,
        ) = apnic_delta_replay_inputs();
        let wrong_base_locks = temp.path().join("wrong-base-locks.json");
        std::fs::write(&wrong_base_locks, b"wrong-base-locks").expect("write wrong base locks");
        let err = run_tree_from_tal_and_ta_der_payload_delta_replay_serial_audit(
            &store,
            &crate::policy::Policy::default(),
            &tal_bytes,
            &ta_der,
            None,
            &base_archive,
            &wrong_base_locks,
            &delta_archive,
            &delta_locks,
            validation_time,
            validation_time,
            &TreeRunConfig {
                max_depth: Some(0),
                max_instances: Some(1),
                compact_audit: false,
                persist_vcir: true,
                build_ccr_accumulator: true,
                enable_roa_validation_cache: false,
                enable_child_certificate_validation_cache: false,
                publication_point_cache_observe_only: false,
                enable_publication_point_validation_cache: false,
                enable_transport_request_prefetch: false,
            },
        )
        .unwrap_err();
        assert!(matches!(err, RunTreeFromTalError::Replay(_)), "{err}");
    }

    #[test]
    fn payload_delta_replay_api_reports_setup_error_for_missing_inputs() {
        let temp = tempfile::tempdir().expect("tempdir");
        let store = crate::storage::RocksStore::open(&temp.path().join("db")).expect("open db");
        let tal_bytes = std::fs::read("tests/fixtures/tal/apnic-rfc7730-https.tal")
            .expect("read apnic tal fixture");
        let ta_der =
            std::fs::read("tests/fixtures/ta/apnic-ta.cer").expect("read apnic ta fixture");
        let err = run_tree_from_tal_and_ta_der_payload_delta_replay_serial_audit(
            &store,
            &crate::policy::Policy::default(),
            &tal_bytes,
            &ta_der,
            None,
            std::path::Path::new("tests/fixtures/missing-base-archive"),
            std::path::Path::new("tests/fixtures/missing-base-locks.json"),
            std::path::Path::new("tests/fixtures/missing-delta-archive"),
            std::path::Path::new("tests/fixtures/missing-delta-locks.json"),
            time::OffsetDateTime::now_utc(),
            time::OffsetDateTime::now_utc(),
            &TreeRunConfig {
                max_depth: Some(0),
                max_instances: Some(1),
                compact_audit: false,
                persist_vcir: true,
                build_ccr_accumulator: true,
                enable_roa_validation_cache: false,
                enable_child_certificate_validation_cache: false,
                publication_point_cache_observe_only: false,
                enable_publication_point_validation_cache: false,
                enable_transport_request_prefetch: false,
            },
        )
        .unwrap_err();
        assert!(matches!(err, RunTreeFromTalError::Replay(_)), "{err}");
    }

    #[test]
    fn payload_delta_replay_api_root_only_apnic_bundle_runs() {
        let temp = tempfile::tempdir().expect("tempdir");
        let store = crate::storage::RocksStore::open(&temp.path().join("db")).expect("open db");
        let (
            tal_bytes,
            ta_der,
            base_archive,
            base_locks,
            delta_archive,
            delta_locks,
            validation_time,
        ) = apnic_delta_replay_inputs();
        if !base_archive.is_dir()
            || !base_locks.is_file()
            || !delta_archive.is_dir()
            || !delta_locks.is_file()
        {
            eprintln!(
                "skipping payload delta replay api test; missing fixtures: base_archive={} base_locks={} delta_archive={} delta_locks={}",
                base_archive.display(),
                base_locks.display(),
                delta_archive.display(),
                delta_locks.display()
            );
            return;
        }

        let out = run_tree_from_tal_and_ta_der_payload_delta_replay_serial_audit(
            &store,
            &crate::policy::Policy::default(),
            &tal_bytes,
            &ta_der,
            None,
            &base_archive,
            &base_locks,
            &delta_archive,
            &delta_locks,
            validation_time,
            validation_time,
            &TreeRunConfig {
                max_depth: Some(0),
                max_instances: Some(1),
                compact_audit: false,
                persist_vcir: true,
                build_ccr_accumulator: true,
                enable_roa_validation_cache: false,
                enable_child_certificate_validation_cache: false,
                publication_point_cache_observe_only: false,
                enable_publication_point_validation_cache: false,
                enable_transport_request_prefetch: false,
            },
        )
        .expect("run delta replay root-only audit");

        assert_eq!(out.tree.instances_processed, 1);
        assert_eq!(out.tree.instances_failed, 0);
        assert_eq!(out.publication_points.len(), 1);
    }

    #[test]
    fn payload_delta_replay_api_root_only_apnic_bundle_runs_with_timing() {
        let temp = tempfile::tempdir().expect("tempdir");
        let db_path = temp.path().join("db");
        let store = crate::storage::RocksStore::open(&db_path).expect("open db");
        let (
            tal_bytes,
            ta_der,
            base_archive,
            base_locks,
            delta_archive,
            delta_locks,
            validation_time,
        ) = apnic_delta_replay_inputs();
        if !base_archive.is_dir()
            || !base_locks.is_file()
            || !delta_archive.is_dir()
            || !delta_locks.is_file()
        {
            eprintln!(
                "skipping payload delta replay timing test; missing fixtures: base_archive={} base_locks={} delta_archive={} delta_locks={}",
                base_archive.display(),
                base_locks.display(),
                delta_archive.display(),
                delta_locks.display()
            );
            return;
        }
        let timing = TimingHandle::new(TimingMeta {
            recorded_at_utc_rfc3339: "2026-03-16T00:00:00Z".to_string(),
            validation_time_utc_rfc3339: "2026-03-15T10:00:00Z".to_string(),
            tal_url: None,
            db_path: Some(db_path.to_string_lossy().into_owned()),
        });
        let out = run_tree_from_tal_and_ta_der_payload_delta_replay_serial_audit_with_timing(
            &store,
            &crate::policy::Policy::default(),
            &tal_bytes,
            &ta_der,
            None,
            &base_archive,
            &base_locks,
            &delta_archive,
            &delta_locks,
            validation_time,
            validation_time,
            &TreeRunConfig {
                max_depth: Some(0),
                max_instances: Some(1),
                compact_audit: false,
                persist_vcir: true,
                build_ccr_accumulator: true,
                enable_roa_validation_cache: false,
                enable_child_certificate_validation_cache: false,
                publication_point_cache_observe_only: false,
                enable_publication_point_validation_cache: false,
                enable_transport_request_prefetch: false,
            },
            &timing,
        )
        .expect("run delta replay root-only audit with timing");
        assert_eq!(out.tree.instances_processed, 1);
        let timing_json = temp.path().join("timing_delta_replay.json");
        timing
            .write_json(&timing_json, 20)
            .expect("write timing json");
        let json: serde_json::Value =
            serde_json::from_slice(&std::fs::read(&timing_json).expect("read timing json"))
                .expect("parse timing json");
        assert_eq!(
            json["phases"]["payload_delta_replay_base_total"]["count"].as_u64(),
            Some(1)
        );
        assert_eq!(
            json["phases"]["payload_delta_replay_target_total"]["count"].as_u64(),
            Some(1)
        );
    }
}
