use std::collections::HashSet;
use std::sync::{Arc, Mutex};
use std::time::Duration;

use crate::parallel::repo_scheduler::TransportRequestAction;
use crate::parallel::repo_worker::{RepoTransportExecutor, RepoTransportWorkerPool};
use crate::parallel::run_coordinator::GlobalRunCoordinator;
use crate::parallel::transport_prefetch::{
    TransportPrefetchDispatchStats, TransportPrefetchRecorder, TransportPrefetchSnapshot,
};
use crate::parallel::types::{
    RepoIdentity, RepoRequester, RepoRuntimeState, RepoTransportMode, RepoTransportResultEnvelope,
    RepoTransportResultKind,
};
use crate::policy::SyncPreference;
use crate::report::Warning;
use crate::validation::tree::{CaInstanceHandle, DiscoveredChildCaInstance};

#[derive(Clone, Debug, PartialEq, Eq)]
pub struct RepoSyncRuntimeOutcome {
    pub repo_sync_ok: bool,
    pub repo_sync_err: Option<String>,
    pub repo_sync_source: Option<String>,
    pub repo_sync_phase: Option<String>,
    pub repo_sync_duration_ms: u64,
    pub warnings: Vec<Warning>,
}

#[derive(Clone, Debug, PartialEq, Eq)]
pub enum RepoSyncRequestStatus {
    Ready {
        identity: RepoIdentity,
        outcome: RepoSyncRuntimeOutcome,
    },
    Pending {
        identity: RepoIdentity,
        state: RepoRuntimeState,
    },
}

#[derive(Clone, Debug, PartialEq, Eq)]
pub struct RepoSyncRuntimeCompletion {
    pub identity: RepoIdentity,
    pub state: RepoRuntimeState,
    pub outcome: RepoSyncRuntimeOutcome,
}

#[derive(Clone, Debug, PartialEq, Eq)]
pub struct RepoSyncRuntimeEvent {
    pub transport_identity: RepoIdentity,
    pub completions: Vec<RepoSyncRuntimeCompletion>,
}

pub trait RepoSyncRuntime: Send + Sync {
    fn sync_publication_point_repo(
        &self,
        ca: &CaInstanceHandle,
    ) -> Result<RepoSyncRuntimeOutcome, String>;

    fn request_publication_point_repo(
        &self,
        ca: &CaInstanceHandle,
        priority: u8,
    ) -> Result<RepoSyncRequestStatus, String>;

    fn recv_repo_result_timeout(
        &self,
        timeout: Duration,
    ) -> Result<Option<RepoSyncRuntimeEvent>, String>;

    fn drain_repo_results_timeout(
        &self,
        timeout: Duration,
        max_events: usize,
    ) -> Result<Vec<RepoSyncRuntimeEvent>, String> {
        let max_events = max_events.max(1);
        let mut events = Vec::new();
        for index in 0..max_events {
            let poll_timeout = if index == 0 {
                timeout
            } else {
                Duration::from_millis(0)
            };
            let Some(event) = self.recv_repo_result_timeout(poll_timeout)? else {
                break;
            };
            events.push(event);
        }
        Ok(events)
    }

    fn reset_run_state(&self) -> Result<(), String>;

    fn prefetch_discovered_children(
        &self,
        children: &[DiscoveredChildCaInstance],
    ) -> Result<(), String>;

    fn prefetch_transport_requests(
        &self,
        snapshot: &TransportPrefetchSnapshot,
        validation_time: time::OffsetDateTime,
    ) -> Result<TransportPrefetchDispatchStats, String>;

    fn transport_prefetch_snapshot(&self) -> TransportPrefetchSnapshot;

    /// Dead-repo blacklist working copy (#141) for run-end persistence.
    /// `None` when the feature is disabled.
    fn dead_repo_blacklist_state(
        &self,
    ) -> Option<(
        crate::parallel::dead_repo_blacklist::DeadRepoBlacklistConfig,
        crate::parallel::dead_repo_blacklist::DeadRepoBlacklist,
    )> {
        None
    }
}

pub struct Phase1RepoSyncRuntime<E: RepoTransportExecutor> {
    coordinator: Mutex<GlobalRunCoordinator>,
    worker_pool: Mutex<RepoTransportWorkerPool<E>>,
    transport_prefetch_recorder: Option<Mutex<TransportPrefetchRecorder>>,
    retry_short_rsync_scopes: Mutex<HashSet<String>>,
    rsync_scope_resolver: Arc<dyn Fn(&str) -> String + Send + Sync>,
    rsync_failure_scope_resolver: Arc<dyn Fn(&str) -> Option<String> + Send + Sync>,
    sync_preference: SyncPreference,
}

impl<E: RepoTransportExecutor> Phase1RepoSyncRuntime<E> {
    pub fn new(
        coordinator: GlobalRunCoordinator,
        worker_pool: RepoTransportWorkerPool<E>,
        rsync_scope_resolver: Arc<dyn Fn(&str) -> String + Send + Sync>,
        sync_preference: SyncPreference,
    ) -> Self {
        Self::new_with_failure_scope(
            coordinator,
            worker_pool,
            rsync_scope_resolver,
            Arc::new(|_base: &str| None),
            sync_preference,
        )
    }

    pub fn new_with_failure_scope(
        coordinator: GlobalRunCoordinator,
        worker_pool: RepoTransportWorkerPool<E>,
        rsync_scope_resolver: Arc<dyn Fn(&str) -> String + Send + Sync>,
        rsync_failure_scope_resolver: Arc<dyn Fn(&str) -> Option<String> + Send + Sync>,
        sync_preference: SyncPreference,
    ) -> Self {
        Self {
            coordinator: Mutex::new(coordinator),
            worker_pool: Mutex::new(worker_pool),
            transport_prefetch_recorder: None,
            retry_short_rsync_scopes: Mutex::new(HashSet::new()),
            rsync_scope_resolver,
            rsync_failure_scope_resolver,
            sync_preference,
        }
    }

    pub fn new_with_failure_scope_and_prefetch_recording(
        coordinator: GlobalRunCoordinator,
        worker_pool: RepoTransportWorkerPool<E>,
        rsync_scope_resolver: Arc<dyn Fn(&str) -> String + Send + Sync>,
        rsync_failure_scope_resolver: Arc<dyn Fn(&str) -> Option<String> + Send + Sync>,
        sync_preference: SyncPreference,
        record_transport_prefetch_requests: bool,
    ) -> Self {
        Self {
            coordinator: Mutex::new(coordinator),
            worker_pool: Mutex::new(worker_pool),
            transport_prefetch_recorder: record_transport_prefetch_requests
                .then(|| Mutex::new(TransportPrefetchRecorder::default())),
            retry_short_rsync_scopes: Mutex::new(HashSet::new()),
            rsync_scope_resolver,
            rsync_failure_scope_resolver,
            sync_preference,
        }
    }

    fn build_requester(ca: &CaInstanceHandle) -> RepoRequester {
        RepoRequester {
            tal_id: ca.tal_id.clone(),
            rir_id: ca.tal_id.clone(),
            parent_node_id: None,
            ca_instance_handle_id: format!("{}:{}", ca.tal_id, ca.manifest_rsync_uri),
            publication_point_rsync_uri: ca.publication_point_rsync_uri.clone(),
            manifest_rsync_uri: ca.manifest_rsync_uri.clone(),
        }
    }

    fn build_identity(ca: &CaInstanceHandle) -> RepoIdentity {
        RepoIdentity::new(ca.rrdp_notification_uri.clone(), ca.rsync_base_uri.clone())
    }

    fn request_transport_for_ca(
        &self,
        ca: &CaInstanceHandle,
        priority: u8,
    ) -> Result<RepoSyncRequestStatus, String> {
        let identity = Self::build_identity(ca);
        let requester = Self::build_requester(ca);
        let rsync_scope_uri = (self.rsync_scope_resolver)(&identity.rsync_base_uri);
        let rsync_failure_scope_uri = (self.rsync_failure_scope_resolver)(&identity.rsync_base_uri);
        if let Some(recorder) = self.transport_prefetch_recorder.as_ref() {
            let mut recorder = recorder
                .lock()
                .expect("transport prefetch recorder lock poisoned");
            recorder.record_registered_request(
                &identity,
                &requester,
                priority,
                rsync_scope_uri.clone(),
                rsync_failure_scope_uri.clone(),
                self.sync_preference,
            );
        }
        let action = {
            let mut coordinator = self.coordinator.lock().expect("coordinator lock poisoned");
            coordinator.register_transport_request(
                identity.clone(),
                requester,
                time::OffsetDateTime::now_utc(),
                priority,
                rsync_scope_uri,
                rsync_failure_scope_uri,
                self.sync_preference,
                false,
            )
        };

        match action {
            TransportRequestAction::Enqueue(task) => {
                crate::progress_log::emit(
                    "phase1_repo_task_enqueued",
                    serde_json::json!({
                        "manifest_rsync_uri": ca.manifest_rsync_uri,
                        "publication_point_rsync_uri": ca.publication_point_rsync_uri,
                        "repo_key_rsync_base_uri": task.repo_identity.rsync_base_uri,
                        "rsync_failure_scope_uri": task.rsync_failure_scope_uri,
                        "repo_key_notification_uri": task.repo_identity.notification_uri,
                        "priority": priority,
                        "transport_mode": match task.mode {
                            RepoTransportMode::Rrdp => "rrdp",
                            RepoTransportMode::Rsync => "rsync",
                        },
                    }),
                );
                self.drain_pending_transport_tasks()?;
                Ok(RepoSyncRequestStatus::Pending {
                    identity,
                    state: self
                        .runtime_state_for_identity(&task.repo_identity)
                        .unwrap_or(RepoRuntimeState::WaitingRrdp),
                })
            }
            TransportRequestAction::Waiting { state } => {
                crate::progress_log::emit(
                    "phase1_repo_task_waiting",
                    serde_json::json!({
                        "manifest_rsync_uri": ca.manifest_rsync_uri,
                        "publication_point_rsync_uri": ca.publication_point_rsync_uri,
                        "repo_key_rsync_base_uri": identity.rsync_base_uri,
                        "rsync_failure_scope_uri": (self.rsync_failure_scope_resolver)(&identity.rsync_base_uri),
                        "repo_key_notification_uri": identity.notification_uri,
                        "priority": priority,
                        "runtime_state": format!("{state:?}"),
                    }),
                );
                Ok(RepoSyncRequestStatus::Pending { identity, state })
            }
            TransportRequestAction::ReusedSuccess(result)
            | TransportRequestAction::ReusedTerminalFailure(result) => {
                crate::progress_log::emit(
                    "phase1_repo_task_reused",
                    serde_json::json!({
                        "manifest_rsync_uri": ca.manifest_rsync_uri,
                        "publication_point_rsync_uri": ca.publication_point_rsync_uri,
                        "repo_key_rsync_base_uri": identity.rsync_base_uri,
                        "rsync_failure_scope_uri": result.rsync_failure_scope_uri,
                        "repo_key_notification_uri": identity.notification_uri,
                        "priority": priority,
                        "transport_mode": match result.mode {
                            RepoTransportMode::Rrdp => "rrdp",
                            RepoTransportMode::Rsync => "rsync",
                        },
                    }),
                );
                Ok(RepoSyncRequestStatus::Ready {
                    outcome: outcome_from_transport_result(
                        &result,
                        self.runtime_state_for_identity(&identity)
                            .unwrap_or(RepoRuntimeState::Init),
                    ),
                    identity,
                })
            }
        }
    }

    fn drain_pending_transport_tasks(&self) -> Result<(), String> {
        loop {
            let maybe_task = {
                let mut coordinator = self.coordinator.lock().expect("coordinator lock poisoned");
                coordinator.pop_next_transport_task()
            };
            let Some(task) = maybe_task else {
                break;
            };
            {
                let mut coordinator = self.coordinator.lock().expect("coordinator lock poisoned");
                coordinator
                    .mark_transport_running(&task.dedup_key, time::OffsetDateTime::now_utc())?;
            }
            crate::progress_log::emit(
                "phase1_repo_task_dispatched",
                serde_json::json!({
                    "repo_key_rsync_base_uri": task.repo_identity.rsync_base_uri,
                    "rsync_failure_scope_uri": task.rsync_failure_scope_uri,
                    "repo_key_notification_uri": task.repo_identity.notification_uri,
                    "requester_count": task.requesters.len(),
                    "priority": task.priority,
                    "transport_mode": match task.mode {
                        RepoTransportMode::Rrdp => "rrdp",
                        RepoTransportMode::Rsync => "rsync",
                    },
                }),
            );
            let pool = self.worker_pool.lock().expect("worker pool lock poisoned");
            pool.submit(task)?;
        }
        Ok(())
    }

    fn pump_one_transport_result(
        &self,
        timeout: Duration,
    ) -> Result<Option<RepoSyncRuntimeEvent>, String> {
        let envelope = {
            let pool = self.worker_pool.lock().expect("worker pool lock poisoned");
            pool.recv_result_timeout(timeout)?
        };
        let Some(envelope) = envelope else {
            return Ok(None);
        };
        let transport_identity = envelope.repo_identity.clone();
        let completed_envelope = envelope.clone();
        if let Some(recorder) = self.transport_prefetch_recorder.as_ref() {
            recorder
                .lock()
                .expect("transport prefetch recorder lock poisoned")
                .record_result(&envelope);
        }
        crate::progress_log::emit(
            "phase1_repo_task_result",
            serde_json::json!({
                "repo_key_rsync_base_uri": envelope.repo_identity.rsync_base_uri,
                "rsync_failure_scope_uri": envelope.rsync_failure_scope_uri,
                "repo_key_notification_uri": envelope.repo_identity.notification_uri,
                "timing_ms": envelope.timing_ms,
                "transport_mode": match envelope.mode {
                    RepoTransportMode::Rrdp => "rrdp",
                    RepoTransportMode::Rsync => "rsync",
                },
                "result": match &envelope.result {
                    RepoTransportResultKind::Success { .. } => "success",
                    RepoTransportResultKind::Failed { .. } => "failed",
                },
            }),
        );
        let finished_at = time::OffsetDateTime::now_utc();
        let completion = {
            let mut coordinator = self.coordinator.lock().expect("coordinator lock poisoned");
            coordinator.complete_transport_result(envelope, finished_at)?
        };
        if !completion.follow_up_tasks.is_empty() {
            let mut coordinator = self.coordinator.lock().expect("coordinator lock poisoned");
            for mut task in completion.follow_up_tasks {
                if let crate::parallel::types::RepoDedupKey::RsyncScope { rsync_scope_uri } =
                    &task.dedup_key
                {
                    if self
                        .retry_short_rsync_scopes
                        .lock()
                        .expect("retry short rsync scopes lock poisoned")
                        .contains(rsync_scope_uri)
                    {
                        task.retry_short_timeout = true;
                    }
                }
                crate::progress_log::emit(
                    "phase1_repo_task_enqueued",
                    serde_json::json!({
                        "manifest_rsync_uri": serde_json::Value::Null,
                        "publication_point_rsync_uri": task.requesters.first().map(|r| r.publication_point_rsync_uri.clone()),
                        "repo_key_rsync_base_uri": task.repo_identity.rsync_base_uri,
                        "repo_key_notification_uri": task.repo_identity.notification_uri,
                        "priority": task.priority,
                        "transport_mode": "rsync",
                    }),
                );
                coordinator.push_transport_task(task);
            }
        }
        self.drain_pending_transport_tasks()?;
        let completions = {
            let coordinator = self.coordinator.lock().expect("coordinator lock poisoned");
            coordinator
                .finalized_runtime_records_for_transport_result(&completed_envelope)
                .into_iter()
                .filter_map(|record| {
                    let outcome = match record.state {
                        RepoRuntimeState::RrdpOk | RepoRuntimeState::RsyncOk => record
                            .last_success
                            .as_ref()
                            .map(|result| outcome_from_transport_result(result, record.state)),
                        RepoRuntimeState::FailedTerminal => record
                            .terminal_failure
                            .as_ref()
                            .map(|result| outcome_from_transport_result(result, record.state)),
                        _ => None,
                    }?;
                    Some(RepoSyncRuntimeCompletion {
                        identity: record.identity,
                        state: record.state,
                        outcome,
                    })
                })
                .collect::<Vec<_>>()
        };
        if completions.is_empty() {
            return Ok(None);
        }
        Ok(Some(RepoSyncRuntimeEvent {
            transport_identity,
            completions,
        }))
    }

    fn pump_transport_results(
        &self,
        timeout: Duration,
        max_events: usize,
    ) -> Result<Vec<RepoSyncRuntimeEvent>, String> {
        let max_events = max_events.max(1);
        let mut events = Vec::new();
        for index in 0..max_events {
            let poll_timeout = if index == 0 {
                timeout
            } else {
                Duration::from_millis(0)
            };
            let Some(event) = self.pump_one_transport_result(poll_timeout)? else {
                break;
            };
            events.push(event);
        }
        Ok(events)
    }

    fn runtime_state_for_identity(&self, identity: &RepoIdentity) -> Option<RepoRuntimeState> {
        let coordinator = self.coordinator.lock().expect("coordinator lock poisoned");
        coordinator
            .runtime_record(identity)
            .map(|record| record.state)
    }

    fn resolved_outcome_for_identity(
        &self,
        identity: &RepoIdentity,
    ) -> Option<RepoSyncRuntimeOutcome> {
        let coordinator = self.coordinator.lock().expect("coordinator lock poisoned");
        let record = coordinator.runtime_record(identity)?;
        match record.state {
            RepoRuntimeState::RrdpOk | RepoRuntimeState::RsyncOk => record
                .last_success
                .as_ref()
                .map(|result| outcome_from_transport_result(result, record.state)),
            RepoRuntimeState::FailedTerminal => record
                .terminal_failure
                .as_ref()
                .map(|result| outcome_from_transport_result(result, record.state)),
            _ => None,
        }
    }
}

impl<E: RepoTransportExecutor> RepoSyncRuntime for Phase1RepoSyncRuntime<E> {
    fn sync_publication_point_repo(
        &self,
        ca: &CaInstanceHandle,
    ) -> Result<RepoSyncRuntimeOutcome, String> {
        if let RepoSyncRequestStatus::Ready { outcome, .. } =
            self.request_publication_point_repo(ca, 0)?
        {
            return Ok(outcome);
        }
        let identity = Self::build_identity(ca);
        loop {
            if let Some(done) = self.resolved_outcome_for_identity(&identity) {
                return Ok(done);
            }
            let _ = self.recv_repo_result_timeout(Duration::from_millis(50))?;
        }
    }

    fn request_publication_point_repo(
        &self,
        ca: &CaInstanceHandle,
        priority: u8,
    ) -> Result<RepoSyncRequestStatus, String> {
        self.request_transport_for_ca(ca, priority)
    }

    fn recv_repo_result_timeout(
        &self,
        timeout: Duration,
    ) -> Result<Option<RepoSyncRuntimeEvent>, String> {
        self.pump_one_transport_result(timeout)
    }

    fn drain_repo_results_timeout(
        &self,
        timeout: Duration,
        max_events: usize,
    ) -> Result<Vec<RepoSyncRuntimeEvent>, String> {
        self.pump_transport_results(timeout, max_events)
    }

    fn reset_run_state(&self) -> Result<(), String> {
        {
            let mut coordinator = self.coordinator.lock().expect("coordinator lock poisoned");
            if coordinator.stats.repo_tasks_running != 0 {
                return Err(format!(
                    "cannot reset repo runtime with {} repo task(s) still running",
                    coordinator.stats.repo_tasks_running
                ));
            }
            coordinator.reset_run_state();
        }
        loop {
            let maybe_result = {
                let pool = self.worker_pool.lock().expect("worker pool lock poisoned");
                pool.recv_result_timeout(Duration::from_millis(0))?
            };
            if maybe_result.is_none() {
                break;
            }
        }
        Ok(())
    }

    fn prefetch_discovered_children(
        &self,
        children: &[DiscoveredChildCaInstance],
    ) -> Result<(), String> {
        for child in children {
            let _ = self.request_publication_point_repo(&child.handle, 1)?;
        }
        Ok(())
    }

    fn prefetch_transport_requests(
        &self,
        snapshot: &TransportPrefetchSnapshot,
        validation_time: time::OffsetDateTime,
    ) -> Result<TransportPrefetchDispatchStats, String> {
        let mut stats = TransportPrefetchDispatchStats {
            loaded_requests: snapshot.requests.len() as u64,
            ..TransportPrefetchDispatchStats::default()
        };

        for request in &snapshot.requests {
            let identity = request.to_identity();
            let current_rsync_scope_uri = (self.rsync_scope_resolver)(&identity.rsync_base_uri);
            let current_rsync_failure_scope_uri =
                (self.rsync_failure_scope_resolver)(&identity.rsync_base_uri);
            if current_rsync_scope_uri != request.rsync_scope_uri
                || current_rsync_failure_scope_uri != request.rsync_failure_scope_uri
            {
                stats.skipped_incompatible += 1;
                continue;
            }
            if request.retry_short_rsync_timeout() {
                self.retry_short_rsync_scopes
                    .lock()
                    .expect("retry short rsync scopes lock poisoned")
                    .insert(current_rsync_scope_uri.clone());
            }
            let action = {
                let mut coordinator = self.coordinator.lock().expect("coordinator lock poisoned");
                coordinator.register_transport_request(
                    identity,
                    request.to_requester(),
                    validation_time,
                    request.priority,
                    current_rsync_scope_uri,
                    current_rsync_failure_scope_uri,
                    self.sync_preference,
                    request.retry_short_timeout(),
                )
            };

            match action {
                TransportRequestAction::Enqueue(task) => {
                    stats.enqueued_tasks += 1;
                    crate::progress_log::emit(
                        "phase1_repo_prefetch_enqueued",
                        serde_json::json!({
                            "repo_key_rsync_base_uri": task.repo_identity.rsync_base_uri,
                            "rsync_failure_scope_uri": task.rsync_failure_scope_uri,
                            "repo_key_notification_uri": task.repo_identity.notification_uri,
                            "priority": task.priority,
                            "transport_mode": match task.mode {
                                RepoTransportMode::Rrdp => "rrdp",
                                RepoTransportMode::Rsync => "rsync",
                            },
                        }),
                    );
                }
                TransportRequestAction::Waiting { .. } => {
                    stats.waiting_requests += 1;
                }
                TransportRequestAction::ReusedSuccess(_)
                | TransportRequestAction::ReusedTerminalFailure(_) => {
                    stats.reused_results += 1;
                }
            }
        }

        self.drain_pending_transport_tasks()?;
        Ok(stats)
    }

    fn transport_prefetch_snapshot(&self) -> TransportPrefetchSnapshot {
        self.transport_prefetch_recorder
            .as_ref()
            .map(|recorder| {
                recorder
                    .lock()
                    .expect("transport prefetch recorder lock poisoned")
                    .snapshot(self.sync_preference)
            })
            .unwrap_or_else(|| TransportPrefetchSnapshot::new(self.sync_preference, Vec::new()))
    }

    fn dead_repo_blacklist_state(
        &self,
    ) -> Option<(
        crate::parallel::dead_repo_blacklist::DeadRepoBlacklistConfig,
        crate::parallel::dead_repo_blacklist::DeadRepoBlacklist,
    )> {
        let coordinator = self.coordinator.lock().expect("coordinator lock poisoned");
        let config = coordinator.config.dead_repo_blacklist.clone()?;
        let blacklist = coordinator.dead_repo_blacklist().cloned()?;
        Some((config, blacklist))
    }
}

fn outcome_from_transport_result(
    envelope: &RepoTransportResultEnvelope,
    state: RepoRuntimeState,
) -> RepoSyncRuntimeOutcome {
    match (&envelope.result, state) {
        (RepoTransportResultKind::Success { source, warnings }, RepoRuntimeState::RrdpOk) => {
            RepoSyncRuntimeOutcome {
                repo_sync_ok: true,
                repo_sync_err: None,
                repo_sync_source: Some(source.clone()),
                repo_sync_phase: Some("rrdp_ok".to_string()),
                repo_sync_duration_ms: envelope.timing_ms,
                warnings: warnings.clone(),
            }
        }
        (RepoTransportResultKind::Success { source, warnings }, RepoRuntimeState::RsyncOk) => {
            RepoSyncRuntimeOutcome {
                repo_sync_ok: true,
                repo_sync_err: None,
                repo_sync_source: Some(source.clone()),
                repo_sync_phase: Some(if envelope.repo_identity.notification_uri.is_some() {
                    "rrdp_failed_rsync_ok".to_string()
                } else {
                    "rsync_only_ok".to_string()
                }),
                repo_sync_duration_ms: envelope.timing_ms,
                warnings: warnings.clone(),
            }
        }
        (
            RepoTransportResultKind::Failed {
                detail, warnings, ..
            },
            RepoRuntimeState::FailedTerminal,
        ) => RepoSyncRuntimeOutcome {
            repo_sync_ok: false,
            repo_sync_err: Some(detail.clone()),
            repo_sync_source: None,
            repo_sync_phase: Some(if envelope.repo_identity.notification_uri.is_some() {
                "rrdp_failed_rsync_failed".to_string()
            } else {
                "rsync_failed".to_string()
            }),
            repo_sync_duration_ms: envelope.timing_ms,
            warnings: warnings.clone(),
        },
        _ => RepoSyncRuntimeOutcome {
            repo_sync_ok: false,
            repo_sync_err: Some("repo runtime state unresolved".to_string()),
            repo_sync_source: None,
            repo_sync_phase: Some("repo_runtime_unresolved".to_string()),
            repo_sync_duration_ms: envelope.timing_ms,
            warnings: Vec::new(),
        },
    }
}

#[cfg(test)]
mod tests {
    use std::sync::Arc;
    use std::sync::atomic::{AtomicUsize, Ordering};
    use std::time::{Duration, Instant};

    use crate::parallel::config::ParallelPhase1Config;
    use crate::parallel::repo_runtime::{Phase1RepoSyncRuntime, RepoSyncRuntime};
    use crate::parallel::repo_worker::{
        RepoTransportExecutor, RepoTransportWorkerPool, RepoWorkerPoolConfig,
    };
    use crate::parallel::run_coordinator::GlobalRunCoordinator;
    use crate::parallel::transport_prefetch::TransportPrefetchSnapshot;
    use crate::parallel::types::{
        RepoRuntimeState, RepoTransportMode, RepoTransportResultEnvelope, RepoTransportResultKind,
        RepoTransportTask, TalInputSpec,
    };
    use crate::policy::SyncPreference;
    use crate::report::Warning;
    use crate::validation::tree::{CaCertificateRef, CaInstanceHandle, DiscoveredChildCaInstance};

    fn sample_ca(manifest: &str) -> CaInstanceHandle {
        CaInstanceHandle {
            depth: 0,
            tal_id: "arin".to_string(),
            parent_manifest_rsync_uri: None,
            ca_certificate: CaCertificateRef::inline_der(vec![1, 2, 3]),
            ca_certificate_rsync_uri: None,
            effective_ip_resources: None,
            effective_as_resources: None,
            rsync_base_uri: "rsync://example.test/repo/".to_string(),
            manifest_rsync_uri: manifest.to_string(),
            publication_point_rsync_uri: "rsync://example.test/repo/".to_string(),
            rrdp_notification_uri: Some("https://example.test/notify.xml".to_string()),
        }
    }

    struct SuccessTransportExecutor;

    impl RepoTransportExecutor for SuccessTransportExecutor {
        fn execute_transport(&self, task: RepoTransportTask) -> RepoTransportResultEnvelope {
            RepoTransportResultEnvelope {
                dedup_key: task.dedup_key,
                rsync_failure_scope_uri: task.rsync_failure_scope_uri.clone(),
                repo_identity: task.repo_identity,
                mode: task.mode,
                tal_id: task.tal_id,
                rir_id: task.rir_id,
                timing_ms: 7,
                result: RepoTransportResultKind::Success {
                    source: match task.mode {
                        RepoTransportMode::Rrdp => "rrdp".to_string(),
                        RepoTransportMode::Rsync => "rsync".to_string(),
                    },
                    warnings: vec![Warning::new("transport ok")],
                },
            }
        }
    }

    struct CountingSuccessTransportExecutor {
        count: Arc<AtomicUsize>,
    }

    impl RepoTransportExecutor for CountingSuccessTransportExecutor {
        fn execute_transport(&self, task: RepoTransportTask) -> RepoTransportResultEnvelope {
            self.count.fetch_add(1, Ordering::SeqCst);
            RepoTransportResultEnvelope {
                dedup_key: task.dedup_key,
                rsync_failure_scope_uri: task.rsync_failure_scope_uri.clone(),
                repo_identity: task.repo_identity,
                mode: task.mode,
                tal_id: task.tal_id,
                rir_id: task.rir_id,
                timing_ms: 7,
                result: RepoTransportResultKind::Success {
                    source: match task.mode {
                        RepoTransportMode::Rrdp => "rrdp".to_string(),
                        RepoTransportMode::Rsync => "rsync".to_string(),
                    },
                    warnings: vec![Warning::new("transport ok")],
                },
            }
        }
    }

    struct FailRrdpThenSucceedRsyncExecutor {
        rrdp_count: Arc<AtomicUsize>,
        rsync_count: Arc<AtomicUsize>,
    }

    impl RepoTransportExecutor for FailRrdpThenSucceedRsyncExecutor {
        fn execute_transport(&self, task: RepoTransportTask) -> RepoTransportResultEnvelope {
            match task.mode {
                RepoTransportMode::Rrdp => {
                    self.rrdp_count.fetch_add(1, Ordering::SeqCst);
                    RepoTransportResultEnvelope {
                        dedup_key: task.dedup_key,
                        rsync_failure_scope_uri: task.rsync_failure_scope_uri.clone(),
                        repo_identity: task.repo_identity,
                        mode: RepoTransportMode::Rrdp,
                        tal_id: task.tal_id,
                        rir_id: task.rir_id,
                        timing_ms: 10,
                        result: RepoTransportResultKind::Failed {
                            detail: "rrdp failed".to_string(),
                            warnings: vec![Warning::new("rrdp failed")],
                            error_class: crate::parallel::types::RepoTransportErrorClass::Unknown,
                        },
                    }
                }
                RepoTransportMode::Rsync => {
                    self.rsync_count.fetch_add(1, Ordering::SeqCst);
                    RepoTransportResultEnvelope {
                        dedup_key: task.dedup_key,
                        rsync_failure_scope_uri: task.rsync_failure_scope_uri.clone(),
                        repo_identity: task.repo_identity,
                        mode: RepoTransportMode::Rsync,
                        tal_id: task.tal_id,
                        rir_id: task.rir_id,
                        timing_ms: 12,
                        result: RepoTransportResultKind::Success {
                            source: "rsync".to_string(),
                            warnings: vec![Warning::new("rsync ok")],
                        },
                    }
                }
            }
        }
    }

    struct FailRrdpThenFailRsyncExecutor {
        rrdp_count: Arc<AtomicUsize>,
        rsync_count: Arc<AtomicUsize>,
    }

    impl RepoTransportExecutor for FailRrdpThenFailRsyncExecutor {
        fn execute_transport(&self, task: RepoTransportTask) -> RepoTransportResultEnvelope {
            match task.mode {
                RepoTransportMode::Rrdp => {
                    self.rrdp_count.fetch_add(1, Ordering::SeqCst);
                    RepoTransportResultEnvelope {
                        dedup_key: task.dedup_key,
                        rsync_failure_scope_uri: task.rsync_failure_scope_uri.clone(),
                        repo_identity: task.repo_identity,
                        mode: RepoTransportMode::Rrdp,
                        tal_id: task.tal_id,
                        rir_id: task.rir_id,
                        timing_ms: 10,
                        result: RepoTransportResultKind::Failed {
                            detail: "rrdp failed".to_string(),
                            warnings: vec![Warning::new("rrdp failed")],
                            error_class: crate::parallel::types::RepoTransportErrorClass::Unknown,
                        },
                    }
                }
                RepoTransportMode::Rsync => {
                    self.rsync_count.fetch_add(1, Ordering::SeqCst);
                    RepoTransportResultEnvelope {
                        dedup_key: task.dedup_key,
                        rsync_failure_scope_uri: task.rsync_failure_scope_uri.clone(),
                        repo_identity: task.repo_identity,
                        mode: RepoTransportMode::Rsync,
                        tal_id: task.tal_id,
                        rir_id: task.rir_id,
                        timing_ms: 12,
                        result: RepoTransportResultKind::Failed {
                            detail: "rsync failed".to_string(),
                            warnings: vec![Warning::new("rsync failed")],
                            error_class: crate::parallel::types::RepoTransportErrorClass::Unknown,
                        },
                    }
                }
            }
        }
    }

    #[test]
    fn phase1_runtime_waits_for_rrdp_transport_and_returns_rrdp_outcome() {
        let coordinator = GlobalRunCoordinator::new(
            ParallelPhase1Config::default(),
            vec![TalInputSpec::from_url("https://example.test/arin.tal")],
        );
        let pool = RepoTransportWorkerPool::new(
            RepoWorkerPoolConfig { max_workers: 1 },
            SuccessTransportExecutor,
        )
        .expect("pool");
        let runtime = Phase1RepoSyncRuntime::new(
            coordinator,
            pool,
            Arc::new(|base: &str| base.to_string()),
            SyncPreference::RrdpThenRsync,
        );

        let outcome = runtime
            .sync_publication_point_repo(&sample_ca("rsync://example.test/repo/root.mft"))
            .expect("sync repo");
        assert!(outcome.repo_sync_ok);
        assert_eq!(outcome.repo_sync_source.as_deref(), Some("rrdp"));
        assert_eq!(outcome.repo_sync_phase.as_deref(), Some("rrdp_ok"));
    }

    #[test]
    fn phase1_runtime_request_repo_returns_pending_then_repo_ready_event() {
        let coordinator = GlobalRunCoordinator::new(
            ParallelPhase1Config::default(),
            vec![TalInputSpec::from_url("https://example.test/arin.tal")],
        );
        let pool = RepoTransportWorkerPool::new(
            RepoWorkerPoolConfig { max_workers: 1 },
            SuccessTransportExecutor,
        )
        .expect("pool");
        let runtime = Phase1RepoSyncRuntime::new(
            coordinator,
            pool,
            Arc::new(|base: &str| base.to_string()),
            SyncPreference::RrdpThenRsync,
        );
        let ca = sample_ca("rsync://example.test/repo/root.mft");

        let status = runtime
            .request_publication_point_repo(&ca, 0)
            .expect("request repo");
        let identity = match status {
            super::RepoSyncRequestStatus::Pending { identity, state } => {
                assert_eq!(state, RepoRuntimeState::WaitingRrdp);
                identity
            }
            other => panic!("expected pending, got {other:?}"),
        };

        let event = runtime
            .recv_repo_result_timeout(Duration::from_secs(1))
            .expect("repo event")
            .expect("event");
        assert_eq!(event.transport_identity, identity);
        assert_eq!(event.completions.len(), 1);
        assert_eq!(event.completions[0].identity, identity);
        assert_eq!(event.completions[0].state, RepoRuntimeState::RrdpOk);
        assert!(event.completions[0].outcome.repo_sync_ok);
        assert_eq!(
            event.completions[0].outcome.repo_sync_source.as_deref(),
            Some("rrdp")
        );
    }

    #[test]
    fn phase1_runtime_request_repo_reuses_ready_event_result() {
        let coordinator = GlobalRunCoordinator::new(
            ParallelPhase1Config::default(),
            vec![TalInputSpec::from_url("https://example.test/arin.tal")],
        );
        let pool = RepoTransportWorkerPool::new(
            RepoWorkerPoolConfig { max_workers: 1 },
            SuccessTransportExecutor,
        )
        .expect("pool");
        let runtime = Phase1RepoSyncRuntime::new(
            coordinator,
            pool,
            Arc::new(|base: &str| base.to_string()),
            SyncPreference::RrdpThenRsync,
        );
        let ca = sample_ca("rsync://example.test/repo/root.mft");
        let first = runtime
            .request_publication_point_repo(&ca, 0)
            .expect("request repo");
        assert!(matches!(
            first,
            super::RepoSyncRequestStatus::Pending { .. }
        ));
        let _ = runtime
            .recv_repo_result_timeout(Duration::from_secs(1))
            .expect("repo event")
            .expect("event");

        let second = runtime
            .request_publication_point_repo(&ca, 0)
            .expect("request repo reused");
        match second {
            super::RepoSyncRequestStatus::Ready { outcome, .. } => {
                assert!(outcome.repo_sync_ok);
                assert_eq!(outcome.repo_sync_phase.as_deref(), Some("rrdp_ok"));
            }
            other => panic!("expected ready reuse, got {other:?}"),
        }
    }

    #[test]
    fn phase1_runtime_repo_event_reports_all_finalized_identities_for_shared_rrdp() {
        let coordinator = GlobalRunCoordinator::new(
            ParallelPhase1Config::default(),
            vec![TalInputSpec::from_url("https://example.test/arin.tal")],
        );
        let pool = RepoTransportWorkerPool::new(
            RepoWorkerPoolConfig { max_workers: 1 },
            SuccessTransportExecutor,
        )
        .expect("pool");
        let runtime = Phase1RepoSyncRuntime::new(
            coordinator,
            pool,
            Arc::new(|base: &str| base.to_string()),
            SyncPreference::RrdpThenRsync,
        );
        let ca1 = sample_ca("rsync://example.test/repo/root.mft");
        let mut ca2 = sample_ca("rsync://example.test/other/root.mft");
        ca2.rsync_base_uri = "rsync://example.test/other/".to_string();
        ca2.publication_point_rsync_uri = "rsync://example.test/other/".to_string();

        let id1 = match runtime
            .request_publication_point_repo(&ca1, 0)
            .expect("request first")
        {
            super::RepoSyncRequestStatus::Pending { identity, .. } => identity,
            other => panic!("expected first pending, got {other:?}"),
        };
        let id2 = match runtime
            .request_publication_point_repo(&ca2, 0)
            .expect("request second")
        {
            super::RepoSyncRequestStatus::Pending { identity, .. } => identity,
            other => panic!("expected second pending, got {other:?}"),
        };
        assert_ne!(id1, id2);

        let event = runtime
            .recv_repo_result_timeout(Duration::from_secs(1))
            .expect("repo event")
            .expect("event");
        let mut identities = event
            .completions
            .iter()
            .map(|completion| completion.identity.clone())
            .collect::<Vec<_>>();
        identities.sort_by(|a, b| a.rsync_base_uri.cmp(&b.rsync_base_uri));
        let mut expected = vec![id1, id2];
        expected.sort_by(|a, b| a.rsync_base_uri.cmp(&b.rsync_base_uri));
        assert_eq!(identities, expected);
        assert!(
            event
                .completions
                .iter()
                .all(|completion| completion.state == RepoRuntimeState::RrdpOk)
        );
    }

    #[test]
    fn phase1_runtime_drains_multiple_ready_transport_events() {
        let count = Arc::new(AtomicUsize::new(0));
        let coordinator = GlobalRunCoordinator::new(
            ParallelPhase1Config::default(),
            vec![TalInputSpec::from_url("https://example.test/arin.tal")],
        );
        let pool = RepoTransportWorkerPool::new(
            RepoWorkerPoolConfig { max_workers: 2 },
            CountingSuccessTransportExecutor {
                count: Arc::clone(&count),
            },
        )
        .expect("pool");
        let runtime = Phase1RepoSyncRuntime::new(
            coordinator,
            pool,
            Arc::new(|base: &str| base.to_string()),
            SyncPreference::RrdpThenRsync,
        );
        let ca1 = sample_ca("rsync://example.test/repo/root.mft");
        let mut ca2 = sample_ca("rsync://example.net/repo/root.mft");
        ca2.rsync_base_uri = "rsync://example.net/repo/".to_string();
        ca2.publication_point_rsync_uri = "rsync://example.net/repo/".to_string();
        ca2.rrdp_notification_uri = Some("https://example.net/notify.xml".to_string());

        assert!(matches!(
            runtime
                .request_publication_point_repo(&ca1, 0)
                .expect("request ca1"),
            super::RepoSyncRequestStatus::Pending { .. }
        ));
        assert!(matches!(
            runtime
                .request_publication_point_repo(&ca2, 0)
                .expect("request ca2"),
            super::RepoSyncRequestStatus::Pending { .. }
        ));

        let started = Instant::now();
        while count.load(Ordering::SeqCst) < 2 && started.elapsed() < Duration::from_secs(1) {
            std::thread::sleep(Duration::from_millis(5));
        }
        assert_eq!(count.load(Ordering::SeqCst), 2);

        let events = runtime
            .drain_repo_results_timeout(Duration::from_millis(0), 8)
            .expect("drain events");
        assert_eq!(events.len(), 2);
        assert_eq!(
            events
                .iter()
                .map(|event| event.completions.len())
                .sum::<usize>(),
            2
        );
    }

    #[test]
    fn phase1_runtime_reset_run_state_clears_completed_transport_reuse() {
        let count = Arc::new(AtomicUsize::new(0));
        let coordinator = GlobalRunCoordinator::new(
            ParallelPhase1Config::default(),
            vec![TalInputSpec::from_url("https://example.test/arin.tal")],
        );
        let pool = RepoTransportWorkerPool::new(
            RepoWorkerPoolConfig { max_workers: 1 },
            CountingSuccessTransportExecutor {
                count: Arc::clone(&count),
            },
        )
        .expect("pool");
        let runtime = Phase1RepoSyncRuntime::new(
            coordinator,
            pool,
            Arc::new(|base: &str| base.to_string()),
            SyncPreference::RrdpThenRsync,
        );
        let ca = sample_ca("rsync://example.test/repo/root.mft");

        assert!(matches!(
            runtime
                .request_publication_point_repo(&ca, 0)
                .expect("first request"),
            super::RepoSyncRequestStatus::Pending { .. }
        ));
        let _ = runtime
            .recv_repo_result_timeout(Duration::from_secs(1))
            .expect("first event")
            .expect("event");
        assert_eq!(count.load(Ordering::SeqCst), 1);
        assert!(matches!(
            runtime
                .request_publication_point_repo(&ca, 0)
                .expect("ready reuse before reset"),
            super::RepoSyncRequestStatus::Ready { .. }
        ));

        runtime.reset_run_state().expect("reset");

        assert!(matches!(
            runtime
                .request_publication_point_repo(&ca, 0)
                .expect("second request after reset"),
            super::RepoSyncRequestStatus::Pending { .. }
        ));
        let _ = runtime
            .recv_repo_result_timeout(Duration::from_secs(1))
            .expect("second event")
            .expect("event");
        assert_eq!(count.load(Ordering::SeqCst), 2);
    }

    #[test]
    fn phase1_runtime_records_prefetch_snapshot_only_when_enabled() {
        let coordinator = GlobalRunCoordinator::new(
            ParallelPhase1Config::default(),
            vec![TalInputSpec::from_url("https://example.test/arin.tal")],
        );
        let pool = RepoTransportWorkerPool::new(
            RepoWorkerPoolConfig { max_workers: 1 },
            SuccessTransportExecutor,
        )
        .expect("pool");
        let runtime = Phase1RepoSyncRuntime::new_with_failure_scope_and_prefetch_recording(
            coordinator,
            pool,
            Arc::new(|base: &str| base.to_string()),
            Arc::new(|_base: &str| Some("rsync://example.test/".to_string())),
            SyncPreference::RrdpThenRsync,
            true,
        );
        let ca = sample_ca("rsync://example.test/repo/root.mft");

        let _ = runtime
            .request_publication_point_repo(&ca, 0)
            .expect("request repo");
        let snapshot = runtime.transport_prefetch_snapshot();
        assert_eq!(snapshot.requests.len(), 1);
        assert_eq!(
            snapshot.requests[0].repo_identity.rsync_base_uri,
            "rsync://example.test/repo/"
        );
        assert_eq!(
            snapshot.requests[0].rsync_failure_scope_uri.as_deref(),
            Some("rsync://example.test/")
        );

        let coordinator = GlobalRunCoordinator::new(
            ParallelPhase1Config::default(),
            vec![TalInputSpec::from_url("https://example.test/arin.tal")],
        );
        let pool = RepoTransportWorkerPool::new(
            RepoWorkerPoolConfig { max_workers: 1 },
            SuccessTransportExecutor,
        )
        .expect("pool");
        let runtime = Phase1RepoSyncRuntime::new(
            coordinator,
            pool,
            Arc::new(|base: &str| base.to_string()),
            SyncPreference::RrdpThenRsync,
        );
        let _ = runtime
            .request_publication_point_repo(&ca, 0)
            .expect("request repo without recording");
        assert!(runtime.transport_prefetch_snapshot().requests.is_empty());
    }

    #[test]
    fn phase1_runtime_prefetch_dispatches_and_later_request_waits_on_same_task() {
        let count = Arc::new(AtomicUsize::new(0));
        let coordinator = GlobalRunCoordinator::new(
            ParallelPhase1Config::default(),
            vec![TalInputSpec::from_url("https://example.test/arin.tal")],
        );
        let pool = RepoTransportWorkerPool::new(
            RepoWorkerPoolConfig { max_workers: 1 },
            CountingSuccessTransportExecutor {
                count: Arc::clone(&count),
            },
        )
        .expect("pool");
        let runtime = Phase1RepoSyncRuntime::new_with_failure_scope_and_prefetch_recording(
            coordinator,
            pool,
            Arc::new(|base: &str| base.to_string()),
            Arc::new(|_base: &str| Some("rsync://example.test/".to_string())),
            SyncPreference::RrdpThenRsync,
            true,
        );
        let ca = sample_ca("rsync://example.test/repo/root.mft");
        let mut recorder =
            crate::parallel::transport_prefetch::TransportPrefetchRecorder::default();
        recorder.record_registered_request(
            &super::Phase1RepoSyncRuntime::<CountingSuccessTransportExecutor>::build_identity(&ca),
            &super::Phase1RepoSyncRuntime::<CountingSuccessTransportExecutor>::build_requester(&ca),
            0,
            "rsync://example.test/repo/".to_string(),
            Some("rsync://example.test/".to_string()),
            SyncPreference::RrdpThenRsync,
        );
        let snapshot = recorder.snapshot(SyncPreference::RrdpThenRsync);

        let stats = runtime
            .prefetch_transport_requests(&snapshot, time::OffsetDateTime::UNIX_EPOCH)
            .expect("prefetch transport requests");
        assert_eq!(stats.loaded_requests, 1);
        assert_eq!(stats.enqueued_tasks, 1);

        let status = runtime
            .request_publication_point_repo(&ca, 0)
            .expect("request after prefetch");
        assert!(matches!(
            status,
            super::RepoSyncRequestStatus::Pending {
                state: RepoRuntimeState::WaitingRrdp,
                ..
            }
        ));
        let _ = runtime
            .recv_repo_result_timeout(Duration::from_secs(1))
            .expect("repo event")
            .expect("event");
        assert_eq!(count.load(Ordering::SeqCst), 1);
    }

    #[test]
    fn phase1_runtime_does_not_persist_prefetch_only_requests() {
        let coordinator = GlobalRunCoordinator::new(
            ParallelPhase1Config::default(),
            vec![TalInputSpec::from_url("https://example.test/arin.tal")],
        );
        let pool = RepoTransportWorkerPool::new(
            RepoWorkerPoolConfig { max_workers: 1 },
            SuccessTransportExecutor,
        )
        .expect("pool");
        let runtime = Phase1RepoSyncRuntime::new_with_failure_scope_and_prefetch_recording(
            coordinator,
            pool,
            Arc::new(|base: &str| base.to_string()),
            Arc::new(|_base: &str| Some("rsync://example.test/".to_string())),
            SyncPreference::RrdpThenRsync,
            true,
        );
        let snapshot = TransportPrefetchSnapshot::new(
            SyncPreference::RrdpThenRsync,
            vec![crate::parallel::transport_prefetch::TransportPrefetchRequest::from_registered_request(
                &crate::parallel::types::RepoIdentity::new(
                    Some("https://example.test/notify.xml".to_string()),
                    "rsync://example.test/repo/",
                ),
                &crate::parallel::types::RepoRequester::with_tal_rir(
                    "arin",
                    "arin",
                    "rsync://example.test/repo/root.mft",
                    "rsync://example.test/repo/",
                    "arin:root",
                ),
                0,
                "rsync://example.test/repo/".to_string(),
                Some("rsync://example.test/".to_string()),
                SyncPreference::RrdpThenRsync,
            )],
        );

        let stats = runtime
            .prefetch_transport_requests(&snapshot, time::OffsetDateTime::UNIX_EPOCH)
            .expect("prefetch transport requests");
        assert_eq!(stats.enqueued_tasks, 1);
        assert!(
            runtime.transport_prefetch_snapshot().requests.is_empty(),
            "prefetch-only requests should not be carried forward forever"
        );
    }

    #[test]
    fn phase1_runtime_prefetch_skips_requests_when_scope_resolver_differs() {
        let coordinator = GlobalRunCoordinator::new(
            ParallelPhase1Config::default(),
            vec![TalInputSpec::from_url("https://example.test/arin.tal")],
        );
        let pool = RepoTransportWorkerPool::new(
            RepoWorkerPoolConfig { max_workers: 1 },
            SuccessTransportExecutor,
        )
        .expect("pool");
        let runtime = Phase1RepoSyncRuntime::new_with_failure_scope_and_prefetch_recording(
            coordinator,
            pool,
            Arc::new(|_base: &str| "rsync://example.test/different/".to_string()),
            Arc::new(|_base: &str| Some("rsync://example.test/".to_string())),
            SyncPreference::RrdpThenRsync,
            true,
        );
        let snapshot = TransportPrefetchSnapshot::new(
            SyncPreference::RrdpThenRsync,
            vec![crate::parallel::transport_prefetch::TransportPrefetchRequest::from_registered_request(
                &crate::parallel::types::RepoIdentity::new(
                    Some("https://example.test/notify.xml".to_string()),
                    "rsync://example.test/repo/",
                ),
                &crate::parallel::types::RepoRequester::with_tal_rir(
                    "arin",
                    "arin",
                    "rsync://example.test/repo/root.mft",
                    "rsync://example.test/repo/",
                    "arin:root",
                ),
                0,
                "rsync://example.test/repo/".to_string(),
                Some("rsync://example.test/".to_string()),
                SyncPreference::RrdpThenRsync,
            )],
        );

        let stats = runtime
            .prefetch_transport_requests(&snapshot, time::OffsetDateTime::UNIX_EPOCH)
            .expect("prefetch transport requests");
        assert_eq!(stats.loaded_requests, 1);
        assert_eq!(stats.enqueued_tasks, 0);
        assert_eq!(stats.skipped_incompatible, 1);
    }

    #[test]
    fn phase1_runtime_transitions_rrdp_failure_to_rsync_success() {
        let rrdp_count = Arc::new(AtomicUsize::new(0));
        let rsync_count = Arc::new(AtomicUsize::new(0));
        let coordinator = GlobalRunCoordinator::new(
            ParallelPhase1Config::default(),
            vec![TalInputSpec::from_url("https://example.test/arin.tal")],
        );
        let pool = RepoTransportWorkerPool::new(
            RepoWorkerPoolConfig { max_workers: 1 },
            FailRrdpThenSucceedRsyncExecutor {
                rrdp_count: Arc::clone(&rrdp_count),
                rsync_count: Arc::clone(&rsync_count),
            },
        )
        .expect("pool");
        let runtime = Phase1RepoSyncRuntime::new(
            coordinator,
            pool,
            Arc::new(|_base: &str| "rsync://example.test/module/".to_string()),
            SyncPreference::RrdpThenRsync,
        );

        let outcome = runtime
            .sync_publication_point_repo(&sample_ca("rsync://example.test/repo/root.mft"))
            .expect("sync repo");
        assert!(outcome.repo_sync_ok);
        assert_eq!(outcome.repo_sync_source.as_deref(), Some("rsync"));
        assert_eq!(
            outcome.repo_sync_phase.as_deref(),
            Some("rrdp_failed_rsync_ok")
        );
        assert_eq!(rrdp_count.load(Ordering::SeqCst), 1);
        assert_eq!(rsync_count.load(Ordering::SeqCst), 1);
    }

    #[test]
    fn phase1_runtime_terminal_failure_keeps_rsync_failure_duration() {
        let rrdp_count = Arc::new(AtomicUsize::new(0));
        let rsync_count = Arc::new(AtomicUsize::new(0));
        let coordinator = GlobalRunCoordinator::new(
            ParallelPhase1Config::default(),
            vec![TalInputSpec::from_url("https://example.test/arin.tal")],
        );
        let pool = RepoTransportWorkerPool::new(
            RepoWorkerPoolConfig { max_workers: 1 },
            FailRrdpThenFailRsyncExecutor {
                rrdp_count: Arc::clone(&rrdp_count),
                rsync_count: Arc::clone(&rsync_count),
            },
        )
        .expect("pool");
        let runtime = Phase1RepoSyncRuntime::new(
            coordinator,
            pool,
            Arc::new(|_base: &str| "rsync://example.test/module/".to_string()),
            SyncPreference::RrdpThenRsync,
        );

        let outcome = runtime
            .sync_publication_point_repo(&sample_ca("rsync://example.test/repo/root.mft"))
            .expect("sync repo");
        assert!(!outcome.repo_sync_ok);
        assert_eq!(
            outcome.repo_sync_phase.as_deref(),
            Some("rrdp_failed_rsync_failed")
        );
        assert_eq!(outcome.repo_sync_duration_ms, 12);
        assert_eq!(rrdp_count.load(Ordering::SeqCst), 1);
        assert_eq!(rsync_count.load(Ordering::SeqCst), 1);
    }

    #[test]
    fn phase1_runtime_prefetch_submits_transport_task_before_consumption() {
        let rrdp_count = Arc::new(AtomicUsize::new(0));
        let rsync_count = Arc::new(AtomicUsize::new(0));
        let coordinator = GlobalRunCoordinator::new(
            ParallelPhase1Config::default(),
            vec![TalInputSpec::from_url("https://example.test/arin.tal")],
        );
        let pool = RepoTransportWorkerPool::new(
            RepoWorkerPoolConfig { max_workers: 1 },
            FailRrdpThenSucceedRsyncExecutor {
                rrdp_count: Arc::clone(&rrdp_count),
                rsync_count: Arc::clone(&rsync_count),
            },
        )
        .expect("pool");
        let runtime = Arc::new(Phase1RepoSyncRuntime::new(
            coordinator,
            pool,
            Arc::new(|_base: &str| "rsync://example.test/module/".to_string()),
            SyncPreference::RrdpThenRsync,
        ));

        let child = DiscoveredChildCaInstance {
            handle: sample_ca("rsync://example.test/repo/child.mft"),
            discovered_from: crate::audit::DiscoveredFrom {
                parent_manifest_rsync_uri: "rsync://example.test/repo/root.mft".to_string(),
                child_ca_certificate_rsync_uri: "rsync://example.test/repo/child.cer".to_string(),
                child_ca_certificate_sha256_hex: "00".repeat(32),
            },
            child_entry_projection: None,
        };

        runtime
            .prefetch_discovered_children(std::slice::from_ref(&child))
            .expect("prefetch");

        let started = Instant::now();
        while rrdp_count.load(Ordering::SeqCst) == 0 && started.elapsed() < Duration::from_secs(1) {
            std::thread::sleep(Duration::from_millis(10));
        }
        assert_eq!(rrdp_count.load(Ordering::SeqCst), 1);

        let outcome = runtime
            .sync_publication_point_repo(&child.handle)
            .expect("sync child repo");
        assert!(outcome.repo_sync_ok);
        assert_eq!(rsync_count.load(Ordering::SeqCst), 1);
    }
}
