use crate::ccr::{build_ccr_from_run, write_ccr_file};
use crate::cir::export_cir_from_run;
use std::path::{Path, PathBuf};

use crate::analysis::timing::{TimingHandle, TimingMeta, TimingMetaUpdate};
use crate::audit::{
    AspaOutput, AuditReportV2, AuditRunMeta, AuditWarning, TreeSummary, VrpOutput,
    format_roa_ip_prefix,
};
use crate::fetch::http::{BlockingHttpFetcher, HttpFetcherConfig};
use crate::fetch::rsync::LocalDirRsyncFetcher;
use crate::fetch::rsync_system::{SystemRsyncConfig, SystemRsyncFetcher};
use crate::policy::Policy;
use crate::storage::RocksStore;
use crate::validation::run_tree_from_tal::{
    RunTreeFromTalAuditOutput, run_tree_from_tal_and_ta_der_payload_delta_replay_serial_audit,
    run_tree_from_tal_and_ta_der_payload_delta_replay_serial_audit_with_timing,
    run_tree_from_tal_and_ta_der_payload_replay_serial_audit,
    run_tree_from_tal_and_ta_der_payload_replay_serial_audit_with_timing,
    run_tree_from_tal_and_ta_der_serial_audit,
    run_tree_from_tal_and_ta_der_serial_audit_with_timing, run_tree_from_tal_url_serial_audit,
    run_tree_from_tal_url_serial_audit_with_timing,
};
use crate::validation::tree::TreeRunConfig;

#[derive(Clone, Debug, PartialEq, Eq)]
pub struct CliArgs {
    pub tal_url: Option<String>,
    pub tal_path: Option<PathBuf>,
    pub ta_path: Option<PathBuf>,

    pub db_path: PathBuf,
    pub policy_path: Option<PathBuf>,
    pub report_json_path: Option<PathBuf>,
    pub ccr_out_path: Option<PathBuf>,
    pub cir_enabled: bool,
    pub cir_out_path: Option<PathBuf>,
    pub cir_static_root: Option<PathBuf>,
    pub cir_tal_uri: Option<String>,
    pub payload_replay_archive: Option<PathBuf>,
    pub payload_replay_locks: Option<PathBuf>,
    pub payload_base_archive: Option<PathBuf>,
    pub payload_base_locks: Option<PathBuf>,
    pub payload_base_validation_time: Option<time::OffsetDateTime>,
    pub payload_delta_archive: Option<PathBuf>,
    pub payload_delta_locks: Option<PathBuf>,

    pub rsync_local_dir: Option<PathBuf>,
    pub disable_rrdp: bool,
    pub rsync_command: Option<PathBuf>,

    pub http_timeout_secs: u64,
    pub rsync_timeout_secs: u64,
    pub rsync_mirror_root: Option<PathBuf>,

    pub max_depth: Option<usize>,
    pub max_instances: Option<usize>,
    pub validation_time: Option<time::OffsetDateTime>,

    pub analyze: bool,
    pub profile_cpu: bool,
}

fn usage() -> String {
    let bin = "rpki";
    format!(
        "\
Usage:
  {bin} --db <path> --tal-url <url> [options]
  {bin} --db <path> --tal-path <path> --ta-path <path> [options]

Options:
  --db <path>                RocksDB directory path (required)
  --policy <path>            Policy TOML path (optional)
  --report-json <path>       Write full audit report as JSON (optional)
  --ccr-out <path>           Write CCR DER ContentInfo to this path (optional)
  --cir-enable               Export CIR after the run completes
  --cir-out <path>           Write CIR DER to this path (requires --cir-enable)
  --cir-static-root <path>   Shared static pool root for CIR export (requires --cir-enable)
  --cir-tal-uri <url>        Override TAL URI for CIR export when using --tal-path (optional)
  --payload-replay-archive <path>  Use local payload replay archive root (offline replay mode)
  --payload-replay-locks <path>    Use local payload replay locks.json (offline replay mode)
  --payload-base-archive <path>    Use local base payload archive root (offline delta replay)
  --payload-base-locks <path>      Use local base locks.json (offline delta replay)
  --payload-base-validation-time <rfc3339> Validation time for the base bootstrap inside offline delta replay
  --payload-delta-archive <path>   Use local delta payload archive root (offline delta replay)
  --payload-delta-locks <path>     Use local locks-delta.json (offline delta replay)

  --tal-url <url>            TAL URL (downloads TAL + TA over HTTPS)
  --tal-path <path>          TAL file path (offline-friendly; requires --ta-path)
  --ta-path <path>           TA certificate DER file path (offline-friendly)

  --rsync-local-dir <path>   Use LocalDirRsyncFetcher rooted at this directory (offline tests)
  --disable-rrdp            Disable RRDP and synchronize only via rsync
  --rsync-command <path>    Use this rsync command instead of the default rsync binary
  --http-timeout-secs <n>    HTTP fetch timeout seconds (default: 20)
  --rsync-timeout-secs <n>   rsync I/O timeout seconds (default: 60)
  --rsync-mirror-root <path> Persist rsync mirrors under this directory (default: disabled)
  --max-depth <n>            Max CA instance depth (0 = root only)
  --max-instances <n>        Max number of CA instances to process
  --validation-time <rfc3339> Validation time in RFC3339 (default: now UTC)
  --analyze                  Write timing analysis JSON under target/live/analyze/<timestamp>/
  --profile-cpu              (Requires build feature 'profile') Write CPU flamegraph under analyze dir

  --help                     Show this help
"
    )
}

pub fn parse_args(argv: &[String]) -> Result<CliArgs, String> {
    let mut tal_url: Option<String> = None;
    let mut tal_path: Option<PathBuf> = None;
    let mut ta_path: Option<PathBuf> = None;

    let mut db_path: Option<PathBuf> = None;
    let mut policy_path: Option<PathBuf> = None;
    let mut report_json_path: Option<PathBuf> = None;
    let mut ccr_out_path: Option<PathBuf> = None;
    let mut cir_enabled: bool = false;
    let mut cir_out_path: Option<PathBuf> = None;
    let mut cir_static_root: Option<PathBuf> = None;
    let mut cir_tal_uri: Option<String> = None;
    let mut payload_replay_archive: Option<PathBuf> = None;
    let mut payload_replay_locks: Option<PathBuf> = None;
    let mut payload_base_archive: Option<PathBuf> = None;
    let mut payload_base_locks: Option<PathBuf> = None;
    let mut payload_base_validation_time: Option<time::OffsetDateTime> = None;
    let mut payload_delta_archive: Option<PathBuf> = None;
    let mut payload_delta_locks: Option<PathBuf> = None;

    let mut rsync_local_dir: Option<PathBuf> = None;
    let mut disable_rrdp: bool = false;
    let mut rsync_command: Option<PathBuf> = None;
    let mut http_timeout_secs: u64 = 20;
    let mut rsync_timeout_secs: u64 = 60;
    let mut rsync_mirror_root: Option<PathBuf> = None;
    let mut max_depth: Option<usize> = None;
    let mut max_instances: Option<usize> = None;
    let mut validation_time: Option<time::OffsetDateTime> = None;
    let mut analyze: bool = false;
    let mut profile_cpu: bool = false;

    let mut i = 1usize;
    while i < argv.len() {
        let arg = argv[i].as_str();
        match arg {
            "--help" | "-h" => return Err(usage()),
            "--tal-url" => {
                i += 1;
                let v = argv.get(i).ok_or("--tal-url requires a value")?;
                tal_url = Some(v.clone());
            }
            "--tal-path" => {
                i += 1;
                let v = argv.get(i).ok_or("--tal-path requires a value")?;
                tal_path = Some(PathBuf::from(v));
            }
            "--ta-path" => {
                i += 1;
                let v = argv.get(i).ok_or("--ta-path requires a value")?;
                ta_path = Some(PathBuf::from(v));
            }
            "--db" => {
                i += 1;
                let v = argv.get(i).ok_or("--db requires a value")?;
                db_path = Some(PathBuf::from(v));
            }
            "--policy" => {
                i += 1;
                let v = argv.get(i).ok_or("--policy requires a value")?;
                policy_path = Some(PathBuf::from(v));
            }
            "--report-json" => {
                i += 1;
                let v = argv.get(i).ok_or("--report-json requires a value")?;
                report_json_path = Some(PathBuf::from(v));
            }
            "--ccr-out" => {
                i += 1;
                let v = argv.get(i).ok_or("--ccr-out requires a value")?;
                ccr_out_path = Some(PathBuf::from(v));
            }
            "--cir-enable" => {
                cir_enabled = true;
            }
            "--cir-out" => {
                i += 1;
                let v = argv.get(i).ok_or("--cir-out requires a value")?;
                cir_out_path = Some(PathBuf::from(v));
            }
            "--cir-static-root" => {
                i += 1;
                let v = argv.get(i).ok_or("--cir-static-root requires a value")?;
                cir_static_root = Some(PathBuf::from(v));
            }
            "--cir-tal-uri" => {
                i += 1;
                let v = argv.get(i).ok_or("--cir-tal-uri requires a value")?;
                cir_tal_uri = Some(v.clone());
            }
            "--payload-replay-archive" => {
                i += 1;
                let v = argv
                    .get(i)
                    .ok_or("--payload-replay-archive requires a value")?;
                payload_replay_archive = Some(PathBuf::from(v));
            }
            "--payload-replay-locks" => {
                i += 1;
                let v = argv
                    .get(i)
                    .ok_or("--payload-replay-locks requires a value")?;
                payload_replay_locks = Some(PathBuf::from(v));
            }
            "--payload-base-archive" => {
                i += 1;
                let v = argv
                    .get(i)
                    .ok_or("--payload-base-archive requires a value")?;
                payload_base_archive = Some(PathBuf::from(v));
            }
            "--payload-base-locks" => {
                i += 1;
                let v = argv.get(i).ok_or("--payload-base-locks requires a value")?;
                payload_base_locks = Some(PathBuf::from(v));
            }
            "--payload-base-validation-time" => {
                i += 1;
                let v = argv.get(i).ok_or("--payload-base-validation-time requires a value")?;
                use time::format_description::well_known::Rfc3339;
                let t = time::OffsetDateTime::parse(v, &Rfc3339).map_err(|e| {
                    format!("invalid --payload-base-validation-time (RFC3339 expected): {e}")
                })?;
                payload_base_validation_time = Some(t);
            }
            "--payload-delta-archive" => {
                i += 1;
                let v = argv
                    .get(i)
                    .ok_or("--payload-delta-archive requires a value")?;
                payload_delta_archive = Some(PathBuf::from(v));
            }
            "--payload-delta-locks" => {
                i += 1;
                let v = argv
                    .get(i)
                    .ok_or("--payload-delta-locks requires a value")?;
                payload_delta_locks = Some(PathBuf::from(v));
            }
            "--rsync-local-dir" => {
                i += 1;
                let v = argv.get(i).ok_or("--rsync-local-dir requires a value")?;
                rsync_local_dir = Some(PathBuf::from(v));
            }
            "--disable-rrdp" => {
                disable_rrdp = true;
            }
            "--rsync-command" => {
                i += 1;
                let v = argv.get(i).ok_or("--rsync-command requires a value")?;
                rsync_command = Some(PathBuf::from(v));
            }
            "--http-timeout-secs" => {
                i += 1;
                let v = argv.get(i).ok_or("--http-timeout-secs requires a value")?;
                http_timeout_secs = v
                    .parse::<u64>()
                    .map_err(|_| format!("invalid --http-timeout-secs: {v}"))?;
            }
            "--rsync-timeout-secs" => {
                i += 1;
                let v = argv.get(i).ok_or("--rsync-timeout-secs requires a value")?;
                rsync_timeout_secs = v
                    .parse::<u64>()
                    .map_err(|_| format!("invalid --rsync-timeout-secs: {v}"))?;
            }
            "--rsync-mirror-root" => {
                i += 1;
                let v = argv.get(i).ok_or("--rsync-mirror-root requires a value")?;
                rsync_mirror_root = Some(PathBuf::from(v));
            }
            "--max-depth" => {
                i += 1;
                let v = argv.get(i).ok_or("--max-depth requires a value")?;
                max_depth = Some(
                    v.parse::<usize>()
                        .map_err(|_| format!("invalid --max-depth: {v}"))?,
                );
            }
            "--max-instances" => {
                i += 1;
                let v = argv.get(i).ok_or("--max-instances requires a value")?;
                max_instances = Some(
                    v.parse::<usize>()
                        .map_err(|_| format!("invalid --max-instances: {v}"))?,
                );
            }
            "--validation-time" => {
                i += 1;
                let v = argv.get(i).ok_or("--validation-time requires a value")?;
                use time::format_description::well_known::Rfc3339;
                let t = time::OffsetDateTime::parse(v, &Rfc3339)
                    .map_err(|e| format!("invalid --validation-time (RFC3339 expected): {e}"))?;
                validation_time = Some(t);
            }
            "--analyze" => {
                analyze = true;
            }
            "--profile-cpu" => {
                profile_cpu = true;
            }
            _ => return Err(format!("unknown argument: {arg}\n\n{}", usage())),
        }
        i += 1;
    }

    let db_path = db_path.ok_or_else(|| format!("--db is required\n\n{}", usage()))?;

    let tal_mode_count = tal_url.is_some() as u8 + tal_path.is_some() as u8;
    if tal_mode_count != 1 {
        return Err(format!(
            "must specify exactly one of --tal-url or --tal-path\n\n{}",
            usage()
        ));
    }
    if tal_path.is_some() && ta_path.is_none() && !disable_rrdp {
        return Err(format!(
            "--tal-path requires --ta-path unless --disable-rrdp is set\n\n{}",
            usage()
        ));
    }
    if cir_enabled && (cir_out_path.is_none() || cir_static_root.is_none()) {
        return Err(format!(
            "--cir-enable requires both --cir-out and --cir-static-root\n\n{}",
            usage()
        ));
    }
    if !cir_enabled && (cir_out_path.is_some() || cir_static_root.is_some() || cir_tal_uri.is_some())
    {
        return Err(format!(
            "--cir-out/--cir-static-root/--cir-tal-uri require --cir-enable\n\n{}",
            usage()
        ));
    }
    if cir_enabled && tal_path.is_some() && cir_tal_uri.is_none() {
        return Err(format!(
            "CIR export in --tal-path mode requires --cir-tal-uri\n\n{}",
            usage()
        ));
    }

    let replay_mode_count =
        payload_replay_archive.is_some() as u8 + payload_replay_locks.is_some() as u8;
    if replay_mode_count == 1 {
        return Err(format!(
            "--payload-replay-archive and --payload-replay-locks must be provided together

{}",
            usage()
        ));
    }

    let delta_mode_count = payload_base_archive.is_some() as u8
        + payload_base_locks.is_some() as u8
        + payload_delta_archive.is_some() as u8
        + payload_delta_locks.is_some() as u8;
    if delta_mode_count > 0 && delta_mode_count < 4 {
        return Err(format!(
            "--payload-base-archive, --payload-base-locks, --payload-delta-archive and --payload-delta-locks must be provided together

{}",
            usage()
        ));
    }
    if replay_mode_count == 2 && delta_mode_count == 4 {
        return Err(format!(
            "snapshot replay mode and delta replay mode are mutually exclusive

{}",
            usage()
        ));
    }
    if replay_mode_count == 2 {
        if tal_url.is_some() {
            return Err(format!(
                "payload replay mode requires --tal-path and --ta-path; --tal-url is not supported

{}",
                usage()
            ));
        }
        if tal_path.is_none() || ta_path.is_none() {
            return Err(format!(
                "payload replay mode requires --tal-path and --ta-path

{}",
                usage()
            ));
        }
        if rsync_local_dir.is_some() {
            return Err(format!(
                "payload replay mode cannot be combined with --rsync-local-dir

{}",
                usage()
            ));
        }
    }
    if delta_mode_count == 4 {
        if tal_url.is_some() {
            return Err(format!(
                "payload delta replay mode requires --tal-path and --ta-path; --tal-url is not supported

{}",
                usage()
            ));
        }
        if tal_path.is_none() || ta_path.is_none() {
            return Err(format!(
                "payload delta replay mode requires --tal-path and --ta-path

{}",
                usage()
            ));
        }
        if rsync_local_dir.is_some() {
            return Err(format!(
                "payload delta replay mode cannot be combined with --rsync-local-dir

{}",
                usage()
            ));
        }
    }

    Ok(CliArgs {
        tal_url,
        tal_path,
        ta_path,
        db_path,
        policy_path,
        report_json_path,
        ccr_out_path,
        cir_enabled,
        cir_out_path,
        cir_static_root,
        cir_tal_uri,
        payload_replay_archive,
        payload_replay_locks,
        payload_base_archive,
        payload_base_locks,
        payload_base_validation_time,
        payload_delta_archive,
        payload_delta_locks,
        rsync_local_dir,
        disable_rrdp,
        rsync_command,
        http_timeout_secs,
        rsync_timeout_secs,
        rsync_mirror_root,
        max_depth,
        max_instances,
        validation_time,
        analyze,
        profile_cpu,
    })
}

fn read_policy(path: Option<&Path>) -> Result<Policy, String> {
    match path {
        None => Ok(Policy::default()),
        Some(p) => {
            let s = std::fs::read_to_string(p)
                .map_err(|e| format!("read policy file failed: {}: {e}", p.display()))?;
            Policy::from_toml_str(&s).map_err(|e| e.to_string())
        }
    }
}

fn write_json(path: &Path, report: &AuditReportV2) -> Result<(), String> {
    let f = std::fs::File::create(path)
        .map_err(|e| format!("create report file failed: {}: {e}", path.display()))?;
    serde_json::to_writer_pretty(f, report)
        .map_err(|e| format!("write report json failed: {e}"))?;
    Ok(())
}

fn unique_rrdp_repos(report: &AuditReportV2) -> usize {
    use std::collections::HashSet;
    let mut set: HashSet<&str> = HashSet::new();
    for pp in &report.publication_points {
        if let Some(u) = pp.rrdp_notification_uri.as_deref() {
            set.insert(u);
        }
    }
    set.len()
}

fn print_summary(report: &AuditReportV2) {
    let rrdp_repos = unique_rrdp_repos(report);
    println!("RPKI stage2 serial run summary");
    println!(
        "validation_time={}",
        report.meta.validation_time_rfc3339_utc
    );
    println!(
        "publication_points_processed={} publication_points_failed={}",
        report.tree.instances_processed, report.tree.instances_failed
    );
    println!("rrdp_repos_unique={rrdp_repos}");
    println!("vrps={}", report.vrps.len());
    println!("aspas={}", report.aspas.len());
    println!(
        "audit_publication_points={}",
        report.publication_points.len()
    );
    println!(
        "warnings_total={}",
        report.tree.warnings.len()
            + report
                .publication_points
                .iter()
                .map(|pp| pp.warnings.len())
                .sum::<usize>()
    );
}

fn build_report(
    policy: &Policy,
    validation_time: time::OffsetDateTime,
    out: RunTreeFromTalAuditOutput,
) -> AuditReportV2 {
    use time::format_description::well_known::Rfc3339;
    let validation_time_rfc3339_utc = validation_time
        .to_offset(time::UtcOffset::UTC)
        .format(&Rfc3339)
        .expect("format validation_time");

    let vrps = out
        .tree
        .vrps
        .iter()
        .map(|v| VrpOutput {
            asn: v.asn,
            prefix: format_roa_ip_prefix(&v.prefix),
            max_length: v.max_length,
        })
        .collect::<Vec<_>>();

    let aspas = out
        .tree
        .aspas
        .iter()
        .map(|a| AspaOutput {
            customer_as_id: a.customer_as_id,
            provider_as_ids: a.provider_as_ids.clone(),
        })
        .collect::<Vec<_>>();

    AuditReportV2 {
        format_version: 2,
        meta: AuditRunMeta {
            validation_time_rfc3339_utc,
        },
        policy: policy.clone(),
        tree: TreeSummary {
            instances_processed: out.tree.instances_processed,
            instances_failed: out.tree.instances_failed,
            warnings: out.tree.warnings.iter().map(AuditWarning::from).collect(),
        },
        publication_points: out.publication_points,
        vrps,
        aspas,
        downloads: out.downloads,
        download_stats: out.download_stats,
    }
}

pub fn run(argv: &[String]) -> Result<(), String> {
    let args = parse_args(argv)?;

    let mut policy = read_policy(args.policy_path.as_deref())?;
    if args.disable_rrdp {
        policy.sync_preference = crate::policy::SyncPreference::RsyncOnly;
    }
    let validation_time = args
        .validation_time
        .unwrap_or_else(time::OffsetDateTime::now_utc);

    let store = RocksStore::open(&args.db_path).map_err(|e| e.to_string())?;
    let config = TreeRunConfig {
        max_depth: args.max_depth,
        max_instances: args.max_instances,
    };
    let replay_mode = args.payload_replay_archive.is_some();
    let delta_replay_mode = args.payload_base_archive.is_some();

    use time::format_description::well_known::Rfc3339;
    let mut timing: Option<(std::path::PathBuf, TimingHandle)> = None;
    if args.analyze {
        let recorded_at_utc_rfc3339 = time::OffsetDateTime::now_utc()
            .to_offset(time::UtcOffset::UTC)
            .format(&Rfc3339)
            .map_err(|e| format!("format recorded_at_utc failed: {e}"))?;
        let validation_time_utc_rfc3339 = validation_time
            .to_offset(time::UtcOffset::UTC)
            .format(&Rfc3339)
            .map_err(|e| format!("format validation_time failed: {e}"))?;

        let ts_compact = {
            let fmt = time::format_description::parse("[year][month][day]T[hour][minute][second]Z")
                .map_err(|e| format!("format description parse failed: {e}"))?;
            time::OffsetDateTime::now_utc()
                .format(&fmt)
                .map_err(|e| format!("format timestamp failed: {e}"))?
        };

        let out_dir = std::path::PathBuf::from(env!("CARGO_MANIFEST_DIR"))
            .join("target")
            .join("live")
            .join("analyze")
            .join(ts_compact);
        std::fs::create_dir_all(&out_dir)
            .map_err(|e| format!("create analyze out dir failed: {}: {e}", out_dir.display()))?;

        let handle = TimingHandle::new(TimingMeta {
            recorded_at_utc_rfc3339,
            validation_time_utc_rfc3339,
            tal_url: None,
            db_path: None,
        });
        handle.set_meta(TimingMetaUpdate {
            tal_url: args.tal_url.as_deref(),
            db_path: Some(args.db_path.to_string_lossy().as_ref()),
        });
        timing = Some((out_dir, handle));
    }

    if args.profile_cpu && !args.analyze {
        return Err("--profile-cpu requires --analyze".to_string());
    }

    #[cfg(not(feature = "profile"))]
    if args.profile_cpu {
        return Err("CPU profiling requires building with: --features profile".to_string());
    }

    #[cfg(feature = "profile")]
    let mut profiler_guard: Option<pprof::ProfilerGuard<'static>> = if args.profile_cpu {
        Some(
            pprof::ProfilerGuard::new(100)
                .map_err(|e| format!("pprof ProfilerGuard init failed: {e}"))?,
        )
    } else {
        None
    };

    let out = if delta_replay_mode {
        let tal_path = args
            .tal_path
            .as_ref()
            .expect("validated by parse_args for delta replay mode");
        let ta_path = args
            .ta_path
            .as_ref()
            .expect("validated by parse_args for delta replay mode");
        let base_archive = args
            .payload_base_archive
            .as_ref()
            .expect("validated by parse_args for delta replay mode");
        let base_locks = args
            .payload_base_locks
            .as_ref()
            .expect("validated by parse_args for delta replay mode");
        let base_validation_time = args.payload_base_validation_time.unwrap_or(validation_time);
        let delta_archive = args
            .payload_delta_archive
            .as_ref()
            .expect("validated by parse_args for delta replay mode");
        let delta_locks = args
            .payload_delta_locks
            .as_ref()
            .expect("validated by parse_args for delta replay mode");
        let tal_bytes = std::fs::read(tal_path)
            .map_err(|e| format!("read tal failed: {}: {e}", tal_path.display()))?;
        let ta_der = std::fs::read(ta_path)
            .map_err(|e| format!("read ta failed: {}: {e}", ta_path.display()))?;
        if let Some((_, t)) = timing.as_ref() {
            run_tree_from_tal_and_ta_der_payload_delta_replay_serial_audit_with_timing(
                &store,
                &policy,
                &tal_bytes,
                &ta_der,
                None,
                base_archive,
                base_locks,
                delta_archive,
                delta_locks,
                base_validation_time,
                validation_time,
                &config,
                t,
            )
            .map_err(|e| e.to_string())?
        } else {
            run_tree_from_tal_and_ta_der_payload_delta_replay_serial_audit(
                &store,
                &policy,
                &tal_bytes,
                &ta_der,
                None,
                base_archive,
                base_locks,
                delta_archive,
                delta_locks,
                base_validation_time,
                validation_time,
                &config,
            )
            .map_err(|e| e.to_string())?
        }
    } else if replay_mode {
        let tal_path = args
            .tal_path
            .as_ref()
            .expect("validated by parse_args for replay mode");
        let ta_path = args
            .ta_path
            .as_ref()
            .expect("validated by parse_args for replay mode");
        let archive_root = args
            .payload_replay_archive
            .as_ref()
            .expect("validated by parse_args for replay mode");
        let locks_path = args
            .payload_replay_locks
            .as_ref()
            .expect("validated by parse_args for replay mode");
        let tal_bytes = std::fs::read(tal_path)
            .map_err(|e| format!("read tal failed: {}: {e}", tal_path.display()))?;
        let ta_der = std::fs::read(ta_path)
            .map_err(|e| format!("read ta failed: {}: {e}", ta_path.display()))?;
        if let Some((_, t)) = timing.as_ref() {
            run_tree_from_tal_and_ta_der_payload_replay_serial_audit_with_timing(
                &store,
                &policy,
                &tal_bytes,
                &ta_der,
                None,
                archive_root,
                locks_path,
                validation_time,
                &config,
                t,
            )
            .map_err(|e| e.to_string())?
        } else {
            run_tree_from_tal_and_ta_der_payload_replay_serial_audit(
                &store,
                &policy,
                &tal_bytes,
                &ta_der,
                None,
                archive_root,
                locks_path,
                validation_time,
                &config,
            )
            .map_err(|e| e.to_string())?
        }
    } else if let Some(dir) = args.rsync_local_dir.as_ref() {
        let http = BlockingHttpFetcher::new(HttpFetcherConfig {
            timeout: std::time::Duration::from_secs(args.http_timeout_secs.max(1)),
            ..HttpFetcherConfig::default()
        })
        .map_err(|e| e.to_string())?;
        let rsync = LocalDirRsyncFetcher::new(dir);
        match (
            args.tal_url.as_ref(),
            args.tal_path.as_ref(),
            args.ta_path.as_ref(),
        ) {
            (Some(url), _, _) => {
                if let Some((_, t)) = timing.as_ref() {
                    run_tree_from_tal_url_serial_audit_with_timing(
                        &store,
                        &policy,
                        url,
                        &http,
                        &rsync,
                        validation_time,
                        &config,
                        t,
                    )
                    .map_err(|e| e.to_string())?
                } else {
                    run_tree_from_tal_url_serial_audit(
                        &store,
                        &policy,
                        url,
                        &http,
                        &rsync,
                        validation_time,
                        &config,
                    )
                    .map_err(|e| e.to_string())?
                }
            }
            (None, Some(tal_path), Some(ta_path)) => {
                let tal_bytes = std::fs::read(tal_path)
                    .map_err(|e| format!("read tal failed: {}: {e}", tal_path.display()))?;
                let ta_der = std::fs::read(ta_path)
                    .map_err(|e| format!("read ta failed: {}: {e}", ta_path.display()))?;
                if let Some((_, t)) = timing.as_ref() {
                    run_tree_from_tal_and_ta_der_serial_audit_with_timing(
                        &store,
                        &policy,
                        &tal_bytes,
                        &ta_der,
                        None,
                        &http,
                        &rsync,
                        validation_time,
                        &config,
                        t,
                    )
                    .map_err(|e| e.to_string())?
                } else {
                    run_tree_from_tal_and_ta_der_serial_audit(
                        &store,
                        &policy,
                        &tal_bytes,
                        &ta_der,
                        None,
                        &http,
                        &rsync,
                        validation_time,
                        &config,
                    )
                    .map_err(|e| e.to_string())?
                }
            }
            (None, Some(tal_path), None) => {
                let tal_bytes = std::fs::read(tal_path)
                    .map_err(|e| format!("read tal failed: {}: {e}", tal_path.display()))?;
                let tal_uri = args.cir_tal_uri.clone();
                if let Some((_, t)) = timing.as_ref() {
                    crate::validation::run_tree_from_tal::run_tree_from_tal_bytes_serial_audit_with_timing(
                        &store,
                        &policy,
                        &tal_bytes,
                        tal_uri,
                        &http,
                        &rsync,
                        validation_time,
                        &config,
                        t,
                    )
                    .map_err(|e| e.to_string())?
                } else {
                    crate::validation::run_tree_from_tal::run_tree_from_tal_bytes_serial_audit(
                        &store,
                        &policy,
                        &tal_bytes,
                        tal_uri,
                        &http,
                        &rsync,
                        validation_time,
                        &config,
                    )
                    .map_err(|e| e.to_string())?
                }
            }
            _ => unreachable!("validated by parse_args"),
        }
    } else {
        let http = BlockingHttpFetcher::new(HttpFetcherConfig {
            timeout: std::time::Duration::from_secs(args.http_timeout_secs.max(1)),
            ..HttpFetcherConfig::default()
        })
        .map_err(|e| e.to_string())?;
        let rsync = SystemRsyncFetcher::new(SystemRsyncConfig {
            rsync_bin: args
                .rsync_command
                .clone()
                .unwrap_or_else(|| PathBuf::from("rsync")),
            timeout: std::time::Duration::from_secs(args.rsync_timeout_secs.max(1)),
            mirror_root: args.rsync_mirror_root.clone(),
            ..SystemRsyncConfig::default()
        });
        match (
            args.tal_url.as_ref(),
            args.tal_path.as_ref(),
            args.ta_path.as_ref(),
        ) {
            (Some(url), _, _) => {
                if let Some((_, t)) = timing.as_ref() {
                    run_tree_from_tal_url_serial_audit_with_timing(
                        &store,
                        &policy,
                        url,
                        &http,
                        &rsync,
                        validation_time,
                        &config,
                        t,
                    )
                    .map_err(|e| e.to_string())?
                } else {
                    run_tree_from_tal_url_serial_audit(
                        &store,
                        &policy,
                        url,
                        &http,
                        &rsync,
                        validation_time,
                        &config,
                    )
                    .map_err(|e| e.to_string())?
                }
            }
            (None, Some(tal_path), Some(ta_path)) => {
                let tal_bytes = std::fs::read(tal_path)
                    .map_err(|e| format!("read tal failed: {}: {e}", tal_path.display()))?;
                let ta_der = std::fs::read(ta_path)
                    .map_err(|e| format!("read ta failed: {}: {e}", ta_path.display()))?;
                if let Some((_, t)) = timing.as_ref() {
                    run_tree_from_tal_and_ta_der_serial_audit_with_timing(
                        &store,
                        &policy,
                        &tal_bytes,
                        &ta_der,
                        None,
                        &http,
                        &rsync,
                        validation_time,
                        &config,
                        t,
                    )
                    .map_err(|e| e.to_string())?
                } else {
                    run_tree_from_tal_and_ta_der_serial_audit(
                        &store,
                        &policy,
                        &tal_bytes,
                        &ta_der,
                        None,
                        &http,
                        &rsync,
                        validation_time,
                        &config,
                    )
                    .map_err(|e| e.to_string())?
                }
            }
            (None, Some(tal_path), None) => {
                let tal_bytes = std::fs::read(tal_path)
                    .map_err(|e| format!("read tal failed: {}: {e}", tal_path.display()))?;
                let tal_uri = args.cir_tal_uri.clone();
                if let Some((_, t)) = timing.as_ref() {
                    crate::validation::run_tree_from_tal::run_tree_from_tal_bytes_serial_audit_with_timing(
                        &store,
                        &policy,
                        &tal_bytes,
                        tal_uri,
                        &http,
                        &rsync,
                        validation_time,
                        &config,
                        t,
                    )
                    .map_err(|e| e.to_string())?
                } else {
                    crate::validation::run_tree_from_tal::run_tree_from_tal_bytes_serial_audit(
                        &store,
                        &policy,
                        &tal_bytes,
                        tal_uri,
                        &http,
                        &rsync,
                        validation_time,
                        &config,
                    )
                    .map_err(|e| e.to_string())?
                }
            }
            _ => unreachable!("validated by parse_args"),
        }
    };

    if let Some((_out_dir, t)) = timing.as_ref() {
        t.record_count("instances_processed", out.tree.instances_processed as u64);
        t.record_count("instances_failed", out.tree.instances_failed as u64);
    }

    #[cfg(feature = "profile")]
    let profiler_report = if let Some(guard) = profiler_guard.take() {
        Some(
            guard
                .report()
                .build()
                .map_err(|e| format!("pprof report build failed: {e}"))?,
        )
    } else {
        None
    };

    if let Some(path) = args.ccr_out_path.as_deref() {
        let ccr = build_ccr_from_run(
            &store,
            &[out.discovery.trust_anchor.clone()],
            &out.tree.vrps,
            &out.tree.aspas,
            &out.tree.router_keys,
            time::OffsetDateTime::now_utc(),
        )
        .map_err(|e| e.to_string())?;
        write_ccr_file(path, &ccr).map_err(|e| e.to_string())?;
        eprintln!("wrote CCR: {}", path.display());
    }

    if args.cir_enabled {
        let cir_tal_uri = args
            .tal_url
            .clone()
            .or(args.cir_tal_uri.clone())
            .ok_or_else(|| "CIR export requires a TAL URI source".to_string())?;
        let cir_out_path = args
            .cir_out_path
            .as_deref()
            .expect("validated by parse_args for cir");
        let cir_static_root = args
            .cir_static_root
            .as_deref()
            .expect("validated by parse_args for cir");
        let summary = export_cir_from_run(
            &store,
            &out.discovery.trust_anchor,
            &cir_tal_uri,
            validation_time,
            cir_out_path,
            cir_static_root,
            time::OffsetDateTime::now_utc().date(),
        )
        .map_err(|e| e.to_string())?;
        eprintln!(
            "wrote CIR: {} (objects={}, tals={}, static_written={}, static_reused={})",
            cir_out_path.display(),
            summary.object_count,
            summary.tal_count,
            summary.static_pool.written_files,
            summary.static_pool.reused_files
        );
    }

    let report = build_report(&policy, validation_time, out);

    if let Some(p) = args.report_json_path.as_deref() {
        write_json(p, &report)?;
    }

    if let Some((out_dir, t)) = timing.as_ref() {
        t.record_count("vrps", report.vrps.len() as u64);
        t.record_count("aspas", report.aspas.len() as u64);
        t.record_count(
            "audit_publication_points",
            report.publication_points.len() as u64,
        );
        let timing_json_path = out_dir.join("timing.json");
        t.write_json(&timing_json_path, 20)?;
        eprintln!("analysis: wrote {}", timing_json_path.display());
    }

    #[cfg(feature = "profile")]
    if let (Some((out_dir, _)), Some(report)) = (timing.as_ref(), profiler_report) {
        let svg_path = out_dir.join("flamegraph.svg");
        let svg_file = std::fs::File::create(&svg_path)
            .map_err(|e| format!("create flamegraph failed: {}: {e}", svg_path.display()))?;
        report
            .flamegraph(svg_file)
            .map_err(|e| format!("write flamegraph failed: {e}"))?;
        eprintln!("analysis: wrote {}", svg_path.display());

        let pb_path = out_dir.join("pprof.pb.gz");
        let pprof_profile = report
            .pprof()
            .map_err(|e| format!("pprof export failed: {e}"))?;
        use pprof::protos::Message;
        let mut body = Vec::with_capacity(pprof_profile.encoded_len());
        pprof_profile
            .encode(&mut body)
            .map_err(|e| format!("pprof encode failed: {e}"))?;
        let gz = flate2::write::GzEncoder::new(
            std::fs::File::create(&pb_path)
                .map_err(|e| format!("create pprof.pb.gz failed: {}: {e}", pb_path.display()))?,
            flate2::Compression::default(),
        );
        let mut gz = gz;
        use std::io::Write;
        gz.write_all(&body)
            .map_err(|e| format!("write pprof.pb.gz failed: {e}"))?;
        gz.finish()
            .map_err(|e| format!("finish pprof.pb.gz failed: {e}"))?;
        eprintln!("analysis: wrote {}", pb_path.display());
    }

    print_summary(&report);
    Ok(())
}

#[cfg(test)]
mod tests {
    use super::*;

    #[test]
    fn parse_help_returns_usage() {
        let argv = vec!["rpki".to_string(), "--help".to_string()];
        let err = parse_args(&argv).unwrap_err();
        assert!(err.contains("Usage:"), "{err}");
        assert!(err.contains("--db"), "{err}");
        assert!(err.contains("--rsync-mirror-root"), "{err}");
    }

    #[test]
    fn parse_rejects_unknown_argument() {
        let argv = vec![
            "rpki".to_string(),
            "--db".to_string(),
            "db".to_string(),
            "--tal-url".to_string(),
            "https://example.test/x.tal".to_string(),
            "--nope".to_string(),
        ];
        let err = parse_args(&argv).unwrap_err();
        assert!(err.contains("unknown argument"), "{err}");
    }

    #[test]
    fn parse_rejects_both_tal_url_and_tal_path() {
        let argv = vec![
            "rpki".to_string(),
            "--db".to_string(),
            "db".to_string(),
            "--tal-url".to_string(),
            "https://example.test/x.tal".to_string(),
            "--tal-path".to_string(),
            "x.tal".to_string(),
        ];
        let err = parse_args(&argv).unwrap_err();
        assert!(
            err.contains("exactly one of --tal-url or --tal-path"),
            "{err}"
        );
    }

    #[test]
    fn parse_rejects_invalid_max_depth() {
        let argv = vec![
            "rpki".to_string(),
            "--db".to_string(),
            "db".to_string(),
            "--tal-url".to_string(),
            "https://example.test/x.tal".to_string(),
            "--max-depth".to_string(),
            "nope".to_string(),
        ];
        let err = parse_args(&argv).unwrap_err();
        assert!(err.contains("invalid --max-depth"), "{err}");
    }

    #[test]
    fn parse_accepts_ccr_out_path() {
        let argv = vec![
            "rpki".to_string(),
            "--db".to_string(),
            "db".to_string(),
            "--tal-path".to_string(),
            "x.tal".to_string(),
            "--ta-path".to_string(),
            "x.cer".to_string(),
            "--rsync-local-dir".to_string(),
            "repo".to_string(),
            "--ccr-out".to_string(),
            "out/example.ccr".to_string(),
        ];
        let args = parse_args(&argv).expect("parse args");
        assert_eq!(args.ccr_out_path.as_deref(), Some(std::path::Path::new("out/example.ccr")));
    }

    #[test]
    fn parse_accepts_cir_enable_with_required_paths_and_tal_override() {
        let argv = vec![
            "rpki".to_string(),
            "--db".to_string(),
            "db".to_string(),
            "--tal-path".to_string(),
            "x.tal".to_string(),
            "--ta-path".to_string(),
            "x.cer".to_string(),
            "--rsync-local-dir".to_string(),
            "repo".to_string(),
            "--cir-enable".to_string(),
            "--cir-out".to_string(),
            "out/example.cir".to_string(),
            "--cir-static-root".to_string(),
            "out/static".to_string(),
            "--cir-tal-uri".to_string(),
            "https://example.test/root.tal".to_string(),
        ];
        let args = parse_args(&argv).expect("parse args");
        assert!(args.cir_enabled);
        assert_eq!(args.cir_out_path.as_deref(), Some(std::path::Path::new("out/example.cir")));
        assert_eq!(args.cir_static_root.as_deref(), Some(std::path::Path::new("out/static")));
        assert_eq!(args.cir_tal_uri.as_deref(), Some("https://example.test/root.tal"));
    }

    #[test]
    fn parse_rejects_incomplete_or_invalid_cir_flags() {
        let argv_missing = vec![
            "rpki".to_string(),
            "--db".to_string(),
            "db".to_string(),
            "--tal-url".to_string(),
            "https://example.test/root.tal".to_string(),
            "--cir-enable".to_string(),
            "--cir-out".to_string(),
            "out/example.cir".to_string(),
        ];
        let err = parse_args(&argv_missing).unwrap_err();
        assert!(err.contains("--cir-enable requires both --cir-out and --cir-static-root"), "{err}");

        let argv_needs_enable = vec![
            "rpki".to_string(),
            "--db".to_string(),
            "db".to_string(),
            "--tal-url".to_string(),
            "https://example.test/root.tal".to_string(),
            "--cir-out".to_string(),
            "out/example.cir".to_string(),
        ];
        let err = parse_args(&argv_needs_enable).unwrap_err();
        assert!(err.contains("require --cir-enable"), "{err}");

        let argv_offline_missing_uri = vec![
            "rpki".to_string(),
            "--db".to_string(),
            "db".to_string(),
            "--tal-path".to_string(),
            "x.tal".to_string(),
            "--ta-path".to_string(),
            "x.cer".to_string(),
            "--rsync-local-dir".to_string(),
            "repo".to_string(),
            "--cir-enable".to_string(),
            "--cir-out".to_string(),
            "out/example.cir".to_string(),
            "--cir-static-root".to_string(),
            "out/static".to_string(),
        ];
        let err = parse_args(&argv_offline_missing_uri).unwrap_err();
        assert!(err.contains("requires --cir-tal-uri"), "{err}");
    }

    #[test]
    fn parse_rejects_invalid_validation_time() {
        let argv = vec![
            "rpki".to_string(),
            "--db".to_string(),
            "db".to_string(),
            "--tal-url".to_string(),
            "https://example.test/x.tal".to_string(),
            "--validation-time".to_string(),
            "not-a-time".to_string(),
        ];
        let err = parse_args(&argv).unwrap_err();
        assert!(err.contains("invalid --validation-time"), "{err}");
    }

    #[test]
    fn parse_rejects_invalid_max_instances() {
        let argv = vec![
            "rpki".to_string(),
            "--db".to_string(),
            "db".to_string(),
            "--tal-url".to_string(),
            "https://example.test/x.tal".to_string(),
            "--max-instances".to_string(),
            "nope".to_string(),
        ];
        let err = parse_args(&argv).unwrap_err();
        assert!(err.contains("invalid --max-instances"), "{err}");
    }

    #[test]
    fn parse_rejects_missing_value_for_db() {
        let argv = vec!["rpki".to_string(), "--db".to_string()];
        let err = parse_args(&argv).unwrap_err();
        assert!(err.contains("--db requires a value"), "{err}");
    }

    #[test]
    fn parse_rejects_missing_value_for_tal_url() {
        let argv = vec![
            "rpki".to_string(),
            "--db".to_string(),
            "db".to_string(),
            "--tal-url".to_string(),
        ];
        let err = parse_args(&argv).unwrap_err();
        assert!(err.contains("--tal-url requires a value"), "{err}");
    }

    #[test]
    fn parse_rejects_missing_db() {
        let argv = vec!["rpki".to_string(), "--tal-url".to_string(), "x".to_string()];
        let err = parse_args(&argv).unwrap_err();
        assert!(err.contains("--db is required"), "{err}");
    }

    #[test]
    fn parse_rejects_missing_tal_mode() {
        let argv = vec!["rpki".to_string(), "--db".to_string(), "db".to_string()];
        let err = parse_args(&argv).unwrap_err();
        assert!(
            err.contains("--tal-url") || err.contains("--tal-path"),
            "{err}"
        );
    }

    #[test]
    fn parse_accepts_tal_url_mode() {
        let argv = vec![
            "rpki".to_string(),
            "--db".to_string(),
            "db".to_string(),
            "--tal-url".to_string(),
            "https://example.test/x.tal".to_string(),
        ];
        let args = parse_args(&argv).expect("parse");
        assert_eq!(args.tal_url.as_deref(), Some("https://example.test/x.tal"));
        assert!(args.tal_path.is_none());
        assert!(args.ta_path.is_none());
    }

    #[test]
    fn parse_accepts_offline_mode_requires_ta() {
        let argv = vec![
            "rpki".to_string(),
            "--db".to_string(),
            "db".to_string(),
            "--tal-path".to_string(),
            "a.tal".to_string(),
            "--ta-path".to_string(),
            "ta.cer".to_string(),
            "--max-depth".to_string(),
            "0".to_string(),
        ];
        let args = parse_args(&argv).expect("parse");
        assert_eq!(args.tal_path.as_deref(), Some(Path::new("a.tal")));
        assert_eq!(args.ta_path.as_deref(), Some(Path::new("ta.cer")));
        assert_eq!(args.max_depth, Some(0));
    }

    #[test]
    fn parse_accepts_tal_path_without_ta_when_disable_rrdp_is_set() {
        let argv = vec![
            "rpki".to_string(),
            "--db".to_string(),
            "db".to_string(),
            "--tal-path".to_string(),
            "a.tal".to_string(),
            "--disable-rrdp".to_string(),
            "--rsync-command".to_string(),
            "/tmp/fake-rsync".to_string(),
        ];
        let args = parse_args(&argv).expect("parse");
        assert_eq!(args.tal_path.as_deref(), Some(Path::new("a.tal")));
        assert!(args.ta_path.is_none());
        assert!(args.disable_rrdp);
        assert_eq!(
            args.rsync_command.as_deref(),
            Some(Path::new("/tmp/fake-rsync"))
        );
    }

    #[test]
    fn parse_accepts_payload_delta_replay_mode_with_offline_tal_and_ta() {
        let argv = vec![
            "rpki".to_string(),
            "--db".to_string(),
            "db".to_string(),
            "--tal-path".to_string(),
            "a.tal".to_string(),
            "--ta-path".to_string(),
            "ta.cer".to_string(),
            "--payload-base-archive".to_string(),
            "base-archive".to_string(),
            "--payload-base-locks".to_string(),
            "base-locks.json".to_string(),
            "--payload-delta-archive".to_string(),
            "delta-archive".to_string(),
            "--payload-delta-locks".to_string(),
            "delta-locks.json".to_string(),
        ];
        let args = parse_args(&argv).expect("parse delta replay mode");
        assert_eq!(
            args.payload_base_archive.as_deref(),
            Some(Path::new("base-archive"))
        );
        assert_eq!(
            args.payload_base_locks.as_deref(),
            Some(Path::new("base-locks.json"))
        );
        assert_eq!(
            args.payload_delta_archive.as_deref(),
            Some(Path::new("delta-archive"))
        );
        assert_eq!(
            args.payload_delta_locks.as_deref(),
            Some(Path::new("delta-locks.json"))
        );
    }

    #[test]
    fn parse_rejects_partial_payload_delta_arguments_and_mutual_exclusion() {
        let argv_partial = vec![
            "rpki".to_string(),
            "--db".to_string(),
            "db".to_string(),
            "--tal-path".to_string(),
            "a.tal".to_string(),
            "--ta-path".to_string(),
            "ta.cer".to_string(),
            "--payload-base-archive".to_string(),
            "base-archive".to_string(),
        ];
        let err = parse_args(&argv_partial).unwrap_err();
        assert!(err.contains("must be provided together"), "{err}");

        let argv_both = vec![
            "rpki".to_string(),
            "--db".to_string(),
            "db".to_string(),
            "--tal-path".to_string(),
            "a.tal".to_string(),
            "--ta-path".to_string(),
            "ta.cer".to_string(),
            "--payload-replay-archive".to_string(),
            "archive".to_string(),
            "--payload-replay-locks".to_string(),
            "locks.json".to_string(),
            "--payload-base-archive".to_string(),
            "base-archive".to_string(),
            "--payload-base-locks".to_string(),
            "base-locks.json".to_string(),
            "--payload-delta-archive".to_string(),
            "delta-archive".to_string(),
            "--payload-delta-locks".to_string(),
            "delta-locks.json".to_string(),
        ];
        let err = parse_args(&argv_both).unwrap_err();
        assert!(err.contains("mutually exclusive"), "{err}");
    }

    #[test]
    fn parse_rejects_payload_delta_with_tal_url_or_rsync_local_dir() {
        let argv_url = vec![
            "rpki".to_string(),
            "--db".to_string(),
            "db".to_string(),
            "--tal-url".to_string(),
            "https://example.test/x.tal".to_string(),
            "--payload-base-archive".to_string(),
            "base-archive".to_string(),
            "--payload-base-locks".to_string(),
            "base-locks.json".to_string(),
            "--payload-delta-archive".to_string(),
            "delta-archive".to_string(),
            "--payload-delta-locks".to_string(),
            "delta-locks.json".to_string(),
        ];
        let err = parse_args(&argv_url).unwrap_err();
        assert!(err.contains("--tal-url is not supported"), "{err}");

        let argv_rsync = vec![
            "rpki".to_string(),
            "--db".to_string(),
            "db".to_string(),
            "--tal-path".to_string(),
            "a.tal".to_string(),
            "--ta-path".to_string(),
            "ta.cer".to_string(),
            "--payload-base-archive".to_string(),
            "base-archive".to_string(),
            "--payload-base-locks".to_string(),
            "base-locks.json".to_string(),
            "--payload-delta-archive".to_string(),
            "delta-archive".to_string(),
            "--payload-delta-locks".to_string(),
            "delta-locks.json".to_string(),
            "--rsync-local-dir".to_string(),
            "repo".to_string(),
        ];
        let err = parse_args(&argv_rsync).unwrap_err();
        assert!(
            err.contains("payload delta replay mode cannot be combined with --rsync-local-dir"),
            "{err}"
        );
    }

    #[test]
    fn parse_accepts_payload_replay_mode_with_offline_tal_and_ta() {
        let argv = vec![
            "rpki".to_string(),
            "--db".to_string(),
            "db".to_string(),
            "--tal-path".to_string(),
            "a.tal".to_string(),
            "--ta-path".to_string(),
            "ta.cer".to_string(),
            "--payload-replay-archive".to_string(),
            "archive".to_string(),
            "--payload-replay-locks".to_string(),
            "locks.json".to_string(),
        ];
        let args = parse_args(&argv).expect("parse replay mode");
        assert_eq!(
            args.payload_replay_archive.as_deref(),
            Some(Path::new("archive"))
        );
        assert_eq!(
            args.payload_replay_locks.as_deref(),
            Some(Path::new("locks.json"))
        );
    }

    #[test]
    fn parse_rejects_partial_payload_replay_arguments() {
        let argv = vec![
            "rpki".to_string(),
            "--db".to_string(),
            "db".to_string(),
            "--tal-path".to_string(),
            "a.tal".to_string(),
            "--ta-path".to_string(),
            "ta.cer".to_string(),
            "--payload-replay-archive".to_string(),
            "archive".to_string(),
        ];
        let err = parse_args(&argv).unwrap_err();
        assert!(err.contains("must be provided together"), "{err}");
    }

    #[test]
    fn parse_rejects_payload_replay_with_tal_url_or_rsync_local_dir() {
        let argv_url = vec![
            "rpki".to_string(),
            "--db".to_string(),
            "db".to_string(),
            "--tal-url".to_string(),
            "https://example.test/x.tal".to_string(),
            "--payload-replay-archive".to_string(),
            "archive".to_string(),
            "--payload-replay-locks".to_string(),
            "locks.json".to_string(),
        ];
        let err = parse_args(&argv_url).unwrap_err();
        assert!(err.contains("--tal-url is not supported"), "{err}");

        let argv_rsync = vec![
            "rpki".to_string(),
            "--db".to_string(),
            "db".to_string(),
            "--tal-path".to_string(),
            "a.tal".to_string(),
            "--ta-path".to_string(),
            "ta.cer".to_string(),
            "--payload-replay-archive".to_string(),
            "archive".to_string(),
            "--payload-replay-locks".to_string(),
            "locks.json".to_string(),
            "--rsync-local-dir".to_string(),
            "repo".to_string(),
        ];
        let err = parse_args(&argv_rsync).unwrap_err();
        assert!(
            err.contains("cannot be combined with --rsync-local-dir"),
            "{err}"
        );
    }

    #[test]
    fn parse_accepts_validation_time_rfc3339() {
        let argv = vec![
            "rpki".to_string(),
            "--db".to_string(),
            "db".to_string(),
            "--tal-url".to_string(),
            "https://example.test/x.tal".to_string(),
            "--validation-time".to_string(),
            "2026-01-01T00:00:00Z".to_string(),
        ];
        let args = parse_args(&argv).expect("parse");
        assert!(args.validation_time.is_some());
    }

    #[test]
    fn parse_rejects_removed_revalidate_only_flag() {
        let argv = vec![
            "rpki".to_string(),
            "--db".to_string(),
            "db".to_string(),
            "--tal-url".to_string(),
            "https://example.test/x.tal".to_string(),
            "--revalidate-only".to_string(),
        ];
        let err = parse_args(&argv).unwrap_err();
        assert!(err.contains("unknown argument: --revalidate-only"), "{err}");
    }

    #[test]
    fn read_policy_accepts_valid_toml() {
        let dir = tempfile::tempdir().expect("tmpdir");
        let p = dir.path().join("policy.toml");
        std::fs::write(
            &p,
            "signed_object_failure_policy = \"drop_publication_point\"\n",
        )
        .expect("write policy");

        let policy = read_policy(Some(&p)).expect("parse policy");
        assert_eq!(
            policy.signed_object_failure_policy,
            crate::policy::SignedObjectFailurePolicy::DropPublicationPoint
        );
    }

    #[test]
    fn read_policy_reports_missing_file() {
        let dir = tempfile::tempdir().expect("tmpdir");
        let p = dir.path().join("missing.toml");
        let err = read_policy(Some(&p)).unwrap_err();
        assert!(err.contains("read policy file failed"), "{err}");
    }

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

        let discovery = crate::validation::from_tal::discover_root_ca_instance_from_tal_and_ta_der(
            &tal_bytes, &ta_der, None,
        )
        .expect("discover root");

        let tree = crate::validation::tree::TreeRunOutput {
            instances_processed: 1,
            instances_failed: 0,
            warnings: vec![
                crate::report::Warning::new("synthetic warning")
                    .with_rfc_refs(&[crate::report::RfcRef("RFC 6487 §4.8.8.1")])
                    .with_context("rsync://example.test/repo/pp/"),
            ],
            vrps: vec![crate::validation::objects::Vrp {
                asn: 64496,
                prefix: crate::data_model::roa::IpPrefix {
                    afi: crate::data_model::roa::RoaAfi::Ipv4,
                    prefix_len: 24,
                    addr: [192, 0, 2, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0],
                },
                max_length: 24,
            }],
            aspas: vec![crate::validation::objects::AspaAttestation {
                customer_as_id: 64496,
                provider_as_ids: vec![64497, 64498],
            }],
            router_keys: Vec::new(),
        };

        let mut pp1 = crate::audit::PublicationPointAudit::default();
        pp1.rrdp_notification_uri = Some("https://example.test/n1.xml".to_string());
        let mut pp2 = crate::audit::PublicationPointAudit::default();
        pp2.rrdp_notification_uri = Some("https://example.test/n1.xml".to_string());
        let mut pp3 = crate::audit::PublicationPointAudit::default();
        pp3.rrdp_notification_uri = Some("https://example.test/n2.xml".to_string());

        let out = crate::validation::run_tree_from_tal::RunTreeFromTalAuditOutput {
            discovery,
            tree,
            publication_points: vec![pp1, pp2, pp3],
            downloads: Vec::new(),
            download_stats: crate::audit::AuditDownloadStats::default(),
        };

        let policy = Policy::default();
        let validation_time = time::OffsetDateTime::now_utc();
        let report = build_report(&policy, validation_time, out);

        assert_eq!(unique_rrdp_repos(&report), 2);
        assert_eq!(report.vrps.len(), 1);
        assert_eq!(report.aspas.len(), 1);

        print_summary(&report);
    }

    #[test]
    fn write_json_writes_report() {
        let report = AuditReportV2 {
            format_version: 2,
            meta: AuditRunMeta {
                validation_time_rfc3339_utc: "2026-01-01T00:00:00Z".to_string(),
            },
            policy: Policy::default(),
            tree: TreeSummary {
                instances_processed: 0,
                instances_failed: 0,
                warnings: Vec::new(),
            },
            publication_points: Vec::new(),
            vrps: Vec::new(),
            aspas: Vec::new(),
            downloads: Vec::new(),
            download_stats: crate::audit::AuditDownloadStats::default(),
        };

        let dir = tempfile::tempdir().expect("tmpdir");
        let p = dir.path().join("report.json");
        write_json(&p, &report).expect("write json");
        let s = std::fs::read_to_string(&p).expect("read report");
        assert!(s.contains("\"format_version\""));
        assert!(s.contains("\"policy\""));
    }
}
