use super::*;
use crate::data_model::oid::OID_AD_SIGNED_OBJECT;
use crate::data_model::rc::AccessDescription;
use crate::memory_telemetry::{
    MemoryTelemetryCheckpoint, MemoryTelemetrySummary, ProcessMemorySnapshot,
};
use crate::policy::ResourceValidationMode;
use crate::storage::{
    RocksDbMemorySnapshot, RocksDbMemoryTotals, VcirCcrProjectionSizeBreakdown,
    VcirChildResourceSizeBreakdown, VcirCoreFieldSizeBreakdown, VcirFieldSizeBreakdown,
    VcirStorageEntrySummary, VcirStorageSummary,
};

#[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}");
    assert!(err.contains("--rsync-scope"), "{err}");
    assert!(err.contains("--parallel-phase2-object-workers"), "{err}");
    assert!(err.contains("--memory-trim-after-validation"), "{err}");
    assert!(err.contains("--enable-roa-validation-cache"), "{err}");
    assert!(err.contains("--resource-validation-mode"), "{err}");
    assert!(err.contains("--max-ca-depth"), "{err}");
    assert!(err.contains("default: 32"), "{err}");
    assert!(
        err.contains("--publication-point-cache-observe-only"),
        "{err}"
    );
    assert!(
        err.contains("--enable-publication-point-validation-cache"),
        "{err}"
    );
    assert!(!err.contains("--parallel-phase1"), "{err}");
    assert!(!err.contains("--parallel-phase2          "), "{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_accepts_verification_only_without_manual_validation_time() {
    let argv = vec![
        "rpki".to_string(),
        "--verification-only".to_string(),
        "--verification-source-run".to_string(),
        "/run/run_0002".to_string(),
        "--verification-source-state".to_string(),
        "/run/state".to_string(),
        "--verification-out".to_string(),
        "/verification/check_0002".to_string(),
    ];
    let args = parse_args(&argv).expect("parse verification-only args");
    assert!(args.verification_only);
    assert!(args.validation_time.is_none());
    assert_eq!(
        args.db_path,
        PathBuf::from("/verification/check_0002/scratch/work-db")
    );
}

#[test]
fn parse_rejects_manual_validation_time_in_verification_only() {
    let argv = vec![
        "rpki".to_string(),
        "--verification-only".to_string(),
        "--verification-source-run".to_string(),
        "/run/run_0002".to_string(),
        "--verification-source-state".to_string(),
        "/run/state".to_string(),
        "--verification-out".to_string(),
        "/verification/check_0002".to_string(),
        "--validation-time".to_string(),
        "2026-07-16T00:00:00Z".to_string(),
    ];
    let error = parse_args(&argv).expect_err("manual time must be rejected");
    assert!(error.contains("validation time"), "{error}");
}

#[test]
fn parse_accepts_normal_validation_contract_output() {
    let argv = vec![
        "rpki".to_string(),
        "--db".to_string(),
        "db".to_string(),
        "--tal-url".to_string(),
        "https://example.test/root.tal".to_string(),
        "--validation-contract-out".to_string(),
        "out/validation-contract.json".to_string(),
    ];
    let args = parse_args(&argv).expect("parse normal contract output");
    assert_eq!(
        args.validation_contract_out_path,
        Some(PathBuf::from("out/validation-contract.json"))
    );
}

#[test]
fn verification_policy_preserves_contract_identity_and_selects_only_bound_fallbacks() {
    let manifest_rsync_uri = "rsync://example.test/repo/current.mft".to_string();
    let contract = crate::verification_only::ValidationContract {
        schema_version: crate::verification_only::VALIDATION_CONTRACT_SCHEMA_VERSION,
        validation_time: "2026-07-16T00:00:00Z".to_string(),
        binary_sha256: "11".repeat(32),
        policy: Policy {
            sync_preference: crate::policy::SyncPreference::RrdpThenRsync,
            ..Policy::default()
        },
        max_ca_depth: 32,
        max_instances: None,
        cache: crate::verification_only::ValidationCacheContract {
            publication_point: true,
            roa: false,
            child_certificate: false,
            transport_prefetch: false,
            crypto_signature: false,
        },
        rsync_scope_policy: crate::fetch::rsync_system::RsyncScopePolicy::ModuleRoot,
        fallback_replay_selections: vec![crate::verification_only::FallbackReplaySelection {
            manifest_rsync_uri: manifest_rsync_uri.clone(),
            vcir_sha256: "22".repeat(32),
            reuse_identity_sha256: "33".repeat(32),
        }],
    };

    let policy = policy_for_verification_contract(&contract);
    assert_eq!(
        policy.sync_preference,
        crate::policy::SyncPreference::RrdpThenRsync,
        "sync preference is part of the stored VCIR reuse-identity fingerprint"
    );
    assert_eq!(
        policy.ca_failed_fetch_policy,
        crate::policy::CaFailedFetchPolicy::ReuseCurrentInstanceVcir
    );
    assert!(
        policy
            .verification_forced_vcir_reuse_manifest_uris
            .contains(&manifest_rsync_uri)
    );
    assert!(
        !serde_json::to_string(&policy)
            .expect("serialize policy")
            .contains("verification_forced_vcir_reuse_manifest_uris"),
        "the runtime-only override must not perturb the contracted policy fingerprint"
    );

    assert!(
        !transport_prefetch_for_verification_contract(&contract),
        "verification-only must preserve a disabled source transport prefetch setting"
    );
    let mut contract_with_prefetch = contract;
    contract_with_prefetch.cache.transport_prefetch = true;
    assert_eq!(
        contract_with_prefetch.rsync_scope_policy,
        crate::fetch::rsync_system::RsyncScopePolicy::ModuleRoot,
        "transport scope is part of the source contract"
    );
    assert!(
        transport_prefetch_for_verification_contract(&contract_with_prefetch),
        "verification-only must preserve a source transport prefetch setting"
    );
}

#[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("one-or-more --tal-url or one-or-more --tal-path/--ta-path pairs"),
        "{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_rejects_invalid_max_ca_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-ca-depth".to_string(),
        "nope".to_string(),
    ];
    let err = parse_args(&argv).unwrap_err();
    assert!(err.contains("invalid --max-ca-depth"), "{err}");
}

#[test]
fn parse_rejects_both_ca_depth_options() {
    let argv = vec![
        "rpki".to_string(),
        "--db".to_string(),
        "db".to_string(),
        "--tal-url".to_string(),
        "https://example.test/x.tal".to_string(),
        "--max-ca-depth".to_string(),
        "32".to_string(),
        "--max-depth".to_string(),
        "12".to_string(),
    ];
    let err = parse_args(&argv).unwrap_err();
    assert!(err.contains("cannot be combined"), "{err}");
}

#[test]
fn parse_defaults_max_ca_depth_to_32() {
    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.max_ca_depth, 32);
}

#[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_memory_trim_after_validation() {
    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(),
        "--memory-trim-after-validation".to_string(),
    ];
    let args = parse_args(&argv).expect("parse args");
    assert!(args.memory_trim_after_validation);
}

#[test]
fn parse_disables_memory_trim_after_validation_by_default() {
    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(),
    ];
    let args = parse_args(&argv).expect("parse args");
    assert!(!args.memory_trim_after_validation);
}

#[test]
fn parse_accepts_enable_roa_validation_cache() {
    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(),
        "--enable-roa-validation-cache".to_string(),
    ];
    let args = parse_args(&argv).expect("parse args");
    assert!(args.enable_roa_validation_cache);
}

#[test]
fn parse_accepts_enable_child_certificate_validation_cache() {
    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(),
        "--enable-child-certificate-validation-cache".to_string(),
    ];
    let args = parse_args(&argv).expect("parse args");
    assert!(args.enable_child_certificate_validation_cache);
}

#[test]
fn parse_accepts_publication_point_cache_flags() {
    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(),
        "--publication-point-cache-observe-only".to_string(),
        "--enable-publication-point-validation-cache".to_string(),
    ];
    let args = parse_args(&argv).expect("parse args");
    assert!(args.publication_point_cache_observe_only);
    assert!(args.enable_publication_point_validation_cache);
}

#[test]
fn parse_disables_crypto_signature_cache_observe_only_by_default() {
    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(),
    ];
    let args = parse_args(&argv).expect("parse args");
    assert!(!args.crypto_signature_cache_observe_only);
    assert!(!args.enable_crypto_signature_cache);
}

#[test]
fn parse_accepts_crypto_signature_cache_observe_only() {
    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(),
        "--crypto-signature-cache-observe-only".to_string(),
    ];
    let args = parse_args(&argv).expect("parse args");
    assert!(args.crypto_signature_cache_observe_only);
    assert!(!args.enable_crypto_signature_cache);
}

#[test]
fn parse_accepts_enable_crypto_signature_cache() {
    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(),
        "--enable-crypto-signature-cache".to_string(),
    ];
    let args = parse_args(&argv).expect("parse args");
    assert!(args.enable_crypto_signature_cache);
    assert!(!args.crypto_signature_cache_observe_only);
}

#[test]
fn parse_accepts_enable_transport_request_prefetch() {
    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(),
        "--enable-transport-request-prefetch".to_string(),
    ];
    let args = parse_args(&argv).expect("parse args");
    assert!(args.enable_transport_request_prefetch);
}

#[test]
fn parse_disables_roa_validation_cache_by_default() {
    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(),
    ];
    let args = parse_args(&argv).expect("parse args");
    assert!(!args.enable_roa_validation_cache);
    assert!(!args.enable_child_certificate_validation_cache);
    assert!(!args.publication_point_cache_observe_only);
    assert!(!args.enable_publication_point_validation_cache);
    assert!(!args.enable_transport_request_prefetch);
}

#[test]
fn parse_accepts_analysis_out_and_implies_analyze() {
    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(),
        "--analysis-out".to_string(),
        "run/analyze".to_string(),
    ];
    let args = parse_args(&argv).expect("parse args");
    assert!(args.analyze);
    assert_eq!(
        args.analysis_out_path.as_deref(),
        Some(std::path::Path::new("run/analyze"))
    );
}

#[test]
fn parse_accepts_report_json_compact_when_report_json_is_set() {
    let argv = vec![
        "rpki".to_string(),
        "--db".to_string(),
        "db".to_string(),
        "--tal-url".to_string(),
        "https://example.test/x.tal".to_string(),
        "--report-json".to_string(),
        "out/report.json".to_string(),
        "--report-json-compact".to_string(),
    ];
    let args = parse_args(&argv).expect("parse args");
    assert_eq!(
        args.report_json_path.as_deref(),
        Some(std::path::Path::new("out/report.json"))
    );
    assert!(args.report_json_compact);
}

#[test]
fn parse_accepts_rsync_scope_policy() {
    let argv = vec![
        "rpki".to_string(),
        "--db".to_string(),
        "db".to_string(),
        "--tal-url".to_string(),
        "https://example.test/x.tal".to_string(),
        "--rsync-scope".to_string(),
        "module-root".to_string(),
    ];
    let args = parse_args(&argv).expect("parse args");
    assert_eq!(args.rsync_scope_policy, RsyncScopePolicy::ModuleRoot);
}

#[test]
fn parse_accepts_host_rsync_scope_policy() {
    let argv = vec![
        "rpki".to_string(),
        "--db".to_string(),
        "db".to_string(),
        "--tal-url".to_string(),
        "https://example.test/x.tal".to_string(),
        "--rsync-scope".to_string(),
        "host".to_string(),
    ];
    let args = parse_args(&argv).expect("parse args");
    assert_eq!(args.rsync_scope_policy, RsyncScopePolicy::Host);
}

#[test]
fn parse_accepts_repeatable_http_root_cert_paths() {
    let argv = vec![
        "rpki".to_string(),
        "--db".to_string(),
        "db".to_string(),
        "--tal-url".to_string(),
        "https://example.test/x.tal".to_string(),
        "--http-root-cert".to_string(),
        "local-ca-1.pem".to_string(),
        "--http-root-cert".to_string(),
        "local-ca-2.pem".to_string(),
    ];
    let args = parse_args(&argv).expect("parse args");
    assert_eq!(
        args.http_root_cert_paths,
        vec![
            PathBuf::from("local-ca-1.pem"),
            PathBuf::from("local-ca-2.pem")
        ]
    );
}

#[test]
fn parse_rejects_invalid_rsync_scope_policy() {
    let argv = vec![
        "rpki".to_string(),
        "--db".to_string(),
        "db".to_string(),
        "--tal-url".to_string(),
        "https://example.test/x.tal".to_string(),
        "--rsync-scope".to_string(),
        "wide".to_string(),
    ];
    let err = parse_args(&argv).expect_err("invalid rsync scope should fail");
    assert!(err.contains("invalid --rsync-scope"), "{err}");
}

#[test]
fn parse_accepts_strict_policy_list() {
    let argv = vec![
        "rpki".to_string(),
        "--db".to_string(),
        "db".to_string(),
        "--tal-url".to_string(),
        "https://example.test/x.tal".to_string(),
        "--strict".to_string(),
        "name,cms-der".to_string(),
    ];
    let args = parse_args(&argv).expect("parse args");
    assert_eq!(
        args.strict_policy,
        Some(StrictPolicy {
            name: true,
            cms_der: true,
            signed_attrs: false,
        })
    );
}

#[test]
fn parse_accepts_strict_without_value_as_all() {
    let argv = vec![
        "rpki".to_string(),
        "--db".to_string(),
        "db".to_string(),
        "--tal-url".to_string(),
        "https://example.test/x.tal".to_string(),
        "--strict".to_string(),
        "--report-json".to_string(),
        "out/report.json".to_string(),
    ];
    let args = parse_args(&argv).expect("parse args");
    assert_eq!(args.strict_policy, Some(StrictPolicy::all()));
}

#[test]
fn parse_accepts_resource_validation_mode() {
    let argv = vec![
        "rpki".to_string(),
        "--db".to_string(),
        "db".to_string(),
        "--tal-url".to_string(),
        "https://example.test/x.tal".to_string(),
        "--resource-validation-mode".to_string(),
        "rfc6487".to_string(),
    ];
    let args = parse_args(&argv).expect("parse args");
    assert_eq!(
        args.resource_validation_mode,
        Some(ResourceValidationMode::Rfc6487)
    );
}

#[test]
fn parse_rejects_unknown_resource_validation_mode() {
    let argv = vec![
        "rpki".to_string(),
        "--db".to_string(),
        "db".to_string(),
        "--tal-url".to_string(),
        "https://example.test/x.tal".to_string(),
        "--resource-validation-mode".to_string(),
        "bogus".to_string(),
    ];
    let err = parse_args(&argv).expect_err("unknown mode should fail");
    assert!(err.contains("unknown resource validation mode"), "{err}");
}

#[test]
fn parse_rejects_unknown_strict_policy() {
    let argv = vec![
        "rpki".to_string(),
        "--db".to_string(),
        "db".to_string(),
        "--tal-url".to_string(),
        "https://example.test/x.tal".to_string(),
        "--strict=unknown".to_string(),
    ];
    let err = parse_args(&argv).expect_err("unknown strict policy should fail");
    assert!(err.contains("unknown strict policy"), "{err}");
}

#[test]
fn effective_cir_tal_uris_filters_skipped_multi_tal_inputs() {
    let argv = vec![
        "rpki".to_string(),
        "--db".to_string(),
        "db".to_string(),
        "--tal-path".to_string(),
        "afrinic.tal".to_string(),
        "--ta-path".to_string(),
        "afrinic.cer".to_string(),
        "--tal-path".to_string(),
        "apnic.tal".to_string(),
        "--ta-path".to_string(),
        "apnic.cer".to_string(),
        "--tal-path".to_string(),
        "arin.tal".to_string(),
        "--ta-path".to_string(),
        "arin.cer".to_string(),
        "--cir-enable".to_string(),
        "--cir-out".to_string(),
        "out.cir".to_string(),
        "--cir-tal-uri".to_string(),
        "https://example.test/afrinic.cer".to_string(),
        "--cir-tal-uri".to_string(),
        "https://example.test/apnic.cer".to_string(),
        "--cir-tal-uri".to_string(),
        "https://example.test/arin.cer".to_string(),
    ];
    let args = parse_args(&argv).expect("parse args");
    let mut shared = synthetic_post_validation_shared();
    shared.discoveries = vec![shared.discovery.clone(), shared.discovery.clone()].into();
    shared.successful_tal_inputs =
        vec![args.tal_inputs[0].clone(), args.tal_inputs[2].clone()].into();

    let effective = effective_cir_tal_uris_for_discoveries(
        &args,
        &shared,
        resolve_cir_export_tal_uris(&args).expect("resolve cir tal uris"),
    )
    .expect("map effective cir tal uris");

    assert_eq!(
        effective,
        vec![
            "https://example.test/afrinic.cer".to_string(),
            "https://example.test/arin.cer".to_string(),
        ]
    );
}

#[test]
fn parse_rejects_report_json_compact_without_report_json() {
    let argv = vec![
        "rpki".to_string(),
        "--db".to_string(),
        "db".to_string(),
        "--tal-url".to_string(),
        "https://example.test/x.tal".to_string(),
        "--report-json-compact".to_string(),
    ];
    let err = parse_args(&argv).expect_err("compact flag without report path should fail");
    assert!(
        err.contains("--report-json-compact requires --report-json"),
        "{err}"
    );
}

#[test]
fn parse_accepts_skip_report_build_without_report_json() {
    let argv = vec![
        "rpki".to_string(),
        "--db".to_string(),
        "db".to_string(),
        "--tal-url".to_string(),
        "https://example.test/x.tal".to_string(),
        "--ccr-out".to_string(),
        "out/result.ccr".to_string(),
        "--skip-report-build".to_string(),
    ];
    let args = parse_args(&argv).expect("parse args");
    assert!(args.skip_report_build);
    assert_eq!(
        args.ccr_out_path.as_deref(),
        Some(std::path::Path::new("out/result.ccr"))
    );
}

#[test]
fn parse_accepts_skip_vcir_persist() {
    let argv = vec![
        "rpki".to_string(),
        "--db".to_string(),
        "db".to_string(),
        "--tal-url".to_string(),
        "https://example.test/x.tal".to_string(),
        "--skip-vcir-persist".to_string(),
    ];
    let args = parse_args(&argv).expect("parse args");
    assert!(args.skip_vcir_persist);
}

#[test]
fn parse_rejects_skip_report_build_with_report_json() {
    let argv = vec![
        "rpki".to_string(),
        "--db".to_string(),
        "db".to_string(),
        "--tal-url".to_string(),
        "https://example.test/x.tal".to_string(),
        "--report-json".to_string(),
        "out/report.json".to_string(),
        "--skip-report-build".to_string(),
    ];
    let err = parse_args(&argv).expect_err("skip report build with report path should fail");
    assert!(
        err.contains("--skip-report-build cannot be combined with --report-json"),
        "{err}"
    );
}

#[test]
fn parse_accepts_direct_compare_view_csv_outputs() {
    let argv = vec![
        "rpki".to_string(),
        "--db".to_string(),
        "db".to_string(),
        "--tal-url".to_string(),
        "https://example.test/x.tal".to_string(),
        "--vrps-csv-out".to_string(),
        "out/vrps.csv".to_string(),
        "--vaps-csv-out".to_string(),
        "out/vaps.csv".to_string(),
        "--compare-view-trust-anchor".to_string(),
        "unknown".to_string(),
    ];
    let args = parse_args(&argv).expect("parse args");
    assert_eq!(
        args.vrps_csv_out_path.as_deref(),
        Some(std::path::Path::new("out/vrps.csv"))
    );
    assert_eq!(
        args.vaps_csv_out_path.as_deref(),
        Some(std::path::Path::new("out/vaps.csv"))
    );
    assert_eq!(args.compare_view_trust_anchor.as_deref(), Some("unknown"));
}

#[test]
fn parse_rejects_partial_direct_compare_view_csv_outputs() {
    let argv = vec![
        "rpki".to_string(),
        "--db".to_string(),
        "db".to_string(),
        "--tal-url".to_string(),
        "https://example.test/x.tal".to_string(),
        "--vrps-csv-out".to_string(),
        "out/vrps.csv".to_string(),
    ];
    let err = parse_args(&argv).expect_err("partial direct compare view output should fail");
    assert!(
        err.contains("--vrps-csv-out and --vaps-csv-out must be provided together"),
        "{err}"
    );
}

#[test]
fn parse_accepts_external_raw_store_db() {
    let argv = vec![
        "rpki".to_string(),
        "--db".to_string(),
        "db".to_string(),
        "--raw-store-db".to_string(),
        "raw-store.db".to_string(),
        "--tal-url".to_string(),
        "https://example.test/x.tal".to_string(),
    ];
    let args = parse_args(&argv).expect("parse args");
    assert_eq!(
        args.raw_store_db.as_deref(),
        Some(std::path::Path::new("raw-store.db"))
    );
}

#[test]
fn parse_accepts_external_repo_bytes_db() {
    let argv = vec![
        "rpki".to_string(),
        "--db".to_string(),
        "db".to_string(),
        "--repo-bytes-db".to_string(),
        "repo-bytes.db".to_string(),
        "--tal-url".to_string(),
        "https://example.test/x.tal".to_string(),
    ];
    let args = parse_args(&argv).expect("parse args");
    assert_eq!(
        args.repo_bytes_db.as_deref(),
        Some(std::path::Path::new("repo-bytes.db"))
    );
}

#[test]
fn parse_accepts_cir_enable_with_raw_store_backend() {
    let argv = vec![
        "rpki".to_string(),
        "--db".to_string(),
        "db".to_string(),
        "--raw-store-db".to_string(),
        "raw-store.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-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.raw_store_db.as_deref(),
        Some(std::path::Path::new("raw-store.db"))
    );
    assert_eq!(args.cir_static_root, None);
}

#[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-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_tal_uri.as_deref(),
        Some("https://example.test/root.tal")
    );
    assert_eq!(
        args.cir_tal_uris,
        vec!["https://example.test/root.tal".to_string()]
    );
}

#[test]
fn parse_rejects_deprecated_cir_static_root() {
    let argv = 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(),
        "--cir-static-root".to_string(),
        "out/static".to_string(),
    ];
    let err = parse_args(&argv).expect_err("cir-static-root should be rejected");
    assert!(err.contains("no longer supported"), "{err}");
}

#[test]
fn parse_accepts_default_parallel_config_and_phase2_overrides() {
    let argv = vec![
        "rpki".to_string(),
        "--db".to_string(),
        "db".to_string(),
        "--tal-url".to_string(),
        "https://example.test/root.tal".to_string(),
        "--parallel-phase2-object-workers".to_string(),
        "3".to_string(),
        "--parallel-phase2-worker-queue-capacity".to_string(),
        "17".to_string(),
        "--parallel-phase2-ready-batch-size".to_string(),
        "31".to_string(),
        "--parallel-phase2-ready-batch-wall-time-budget-ms".to_string(),
        "43".to_string(),
        "--parallel-phase2-result-drain-batch-size".to_string(),
        "37".to_string(),
        "--parallel-phase2-finalize-batch-size".to_string(),
        "41".to_string(),
        "--parallel-phase2-finalize-batch-wall-time-budget-ms".to_string(),
        "47".to_string(),
        "--parallel-phase2-finalize-queue-capacity".to_string(),
        "8192".to_string(),
    ];
    let args = parse_args(&argv).expect("parse args");
    assert_eq!(args.parallel_phase2_config.object_workers, 3);
    assert_eq!(args.parallel_phase2_config.worker_queue_capacity, 17);
    assert_eq!(args.parallel_phase2_config.ready_batch_size, 31);
    assert_eq!(
        args.parallel_phase2_config.ready_batch_wall_time_budget_ms,
        43
    );
    assert_eq!(
        args.parallel_phase2_config.object_result_drain_batch_size,
        37
    );
    assert_eq!(
        args.parallel_phase2_config
            .publication_point_finalize_batch_size,
        41
    );
    assert_eq!(
        args.parallel_phase2_config
            .publication_point_finalize_wall_time_budget_ms,
        47
    );
    assert_eq!(
        args.parallel_phase2_config
            .publication_point_finalize_queue_capacity,
        8192
    );
    assert_eq!(args.parallel_phase1_config, ParallelPhase1Config::default());
}

#[test]
fn parse_rejects_zero_phase2_ready_batch_size() {
    let argv = vec![
        "rpki".to_string(),
        "--db".to_string(),
        "db".to_string(),
        "--tal-url".to_string(),
        "https://example.test/root.tal".to_string(),
        "--parallel-phase2-ready-batch-size".to_string(),
        "0".to_string(),
    ];
    let err = parse_args(&argv).expect_err("zero ready batch must fail");
    assert!(err.contains("--parallel-phase2-ready-batch-size"), "{err}");
}

#[test]
fn parse_rejects_zero_phase2_result_drain_batch_size() {
    let argv = vec![
        "rpki".to_string(),
        "--db".to_string(),
        "db".to_string(),
        "--tal-url".to_string(),
        "https://example.test/root.tal".to_string(),
        "--parallel-phase2-result-drain-batch-size".to_string(),
        "0".to_string(),
    ];
    let err = parse_args(&argv).expect_err("zero result drain batch must fail");
    assert!(
        err.contains("--parallel-phase2-result-drain-batch-size"),
        "{err}"
    );
}

#[test]
fn parse_rejects_zero_phase2_ready_batch_wall_time_budget_ms() {
    let argv = vec![
        "rpki".to_string(),
        "--db".to_string(),
        "db".to_string(),
        "--tal-url".to_string(),
        "https://example.test/root.tal".to_string(),
        "--parallel-phase2-ready-batch-wall-time-budget-ms".to_string(),
        "0".to_string(),
    ];
    let err = parse_args(&argv).expect_err("zero ready time budget must fail");
    assert!(
        err.contains("--parallel-phase2-ready-batch-wall-time-budget-ms"),
        "{err}"
    );
}

#[test]
fn parse_rejects_zero_phase2_finalize_batch_size() {
    let argv = vec![
        "rpki".to_string(),
        "--db".to_string(),
        "db".to_string(),
        "--tal-url".to_string(),
        "https://example.test/root.tal".to_string(),
        "--parallel-phase2-finalize-batch-size".to_string(),
        "0".to_string(),
    ];
    let err = parse_args(&argv).expect_err("zero finalize batch must fail");
    assert!(
        err.contains("--parallel-phase2-finalize-batch-size"),
        "{err}"
    );
}

#[test]
fn parse_rejects_zero_phase2_finalize_batch_wall_time_budget_ms() {
    let argv = vec![
        "rpki".to_string(),
        "--db".to_string(),
        "db".to_string(),
        "--tal-url".to_string(),
        "https://example.test/root.tal".to_string(),
        "--parallel-phase2-finalize-batch-wall-time-budget-ms".to_string(),
        "0".to_string(),
    ];
    let err = parse_args(&argv).expect_err("zero finalize time budget must fail");
    assert!(
        err.contains("--parallel-phase2-finalize-batch-wall-time-budget-ms"),
        "{err}"
    );
}

#[test]
fn parse_rejects_zero_phase2_finalize_queue_capacity() {
    let argv = vec![
        "rpki".to_string(),
        "--db".to_string(),
        "db".to_string(),
        "--tal-url".to_string(),
        "https://example.test/root.tal".to_string(),
        "--parallel-phase2-finalize-queue-capacity".to_string(),
        "0".to_string(),
    ];
    let err = parse_args(&argv).expect_err("zero finalize queue capacity must fail");
    assert!(
        err.contains("--parallel-phase2-finalize-queue-capacity"),
        "{err}"
    );
}

#[test]
fn parse_rejects_removed_parallel_enable_flags() {
    let argv = vec![
        "rpki".to_string(),
        "--db".to_string(),
        "db".to_string(),
        "--tal-url".to_string(),
        "https://example.test/root.tal".to_string(),
        "--parallel-phase1".to_string(),
    ];
    let err = parse_args(&argv).expect_err("removed phase flag should fail");
    assert!(err.contains("unknown argument: --parallel-phase1"), "{err}");

    let argv = vec![
        "rpki".to_string(),
        "--db".to_string(),
        "db".to_string(),
        "--tal-url".to_string(),
        "https://example.test/root.tal".to_string(),
        "--parallel-phase2".to_string(),
    ];
    let err = parse_args(&argv).expect_err("removed phase flag should fail");
    assert!(err.contains("unknown argument: --parallel-phase2"), "{err}");
}

#[test]
fn parse_accepts_multi_tal_cir_overrides_in_file_mode() {
    let argv = vec![
        "rpki".to_string(),
        "--db".to_string(),
        "db".to_string(),
        "--tal-path".to_string(),
        "apnic.tal".to_string(),
        "--ta-path".to_string(),
        "apnic.cer".to_string(),
        "--tal-path".to_string(),
        "arin.tal".to_string(),
        "--ta-path".to_string(),
        "arin.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-tal-uri".to_string(),
        "https://example.test/apnic.tal".to_string(),
        "--cir-tal-uri".to_string(),
        "https://example.test/arin.tal".to_string(),
    ];
    let args = parse_args(&argv).expect("parse args");
    assert_eq!(
        args.cir_tal_uris,
        vec![
            "https://example.test/apnic.tal".to_string(),
            "https://example.test/arin.tal".to_string()
        ]
    );
}

#[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(),
    ];
    let err = parse_args(&argv_missing).unwrap_err();
    assert!(err.contains("--cir-enable requires --cir-out"), "{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(),
    ];
    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_eq!(
        args.tal_urls,
        vec!["https://example.test/x.tal".to_string()]
    );
    assert!(args.tal_path.is_none());
    assert!(args.ta_path.is_none());
    assert_eq!(args.tal_inputs.len(), 1);
    assert_eq!(args.tal_inputs[0].tal_id, "x");
    assert_eq!(args.parallel_phase1_config, ParallelPhase1Config::default());
    assert_eq!(args.parallel_phase2_config, ParallelPhase2Config::default());
}

#[test]
fn parse_accepts_multi_tal_without_parallel_flags() {
    let argv = vec![
        "rpki".to_string(),
        "--db".to_string(),
        "db".to_string(),
        "--tal-url".to_string(),
        "https://example.test/arin.tal".to_string(),
        "--tal-url".to_string(),
        "https://example.test/apnic.tal".to_string(),
        "--tal-url".to_string(),
        "https://example.test/ripe.tal".to_string(),
        "--parallel-max-repo-sync-workers-global".to_string(),
        "8".to_string(),
    ];
    let args = parse_args(&argv).expect("parse");
    assert_eq!(args.tal_urls.len(), 3);
    assert_eq!(args.tal_inputs.len(), 3);
    assert_eq!(args.tal_inputs[0].tal_id, "arin");
    assert_eq!(args.tal_inputs[1].tal_id, "apnic");
    assert_eq!(args.tal_inputs[2].tal_id, "ripe");
    assert_eq!(args.parallel_phase1_config.max_repo_sync_workers_global, 8);
}

#[test]
fn parse_accepts_multi_tal_urls_by_default() {
    let argv = vec![
        "rpki".to_string(),
        "--db".to_string(),
        "db".to_string(),
        "--tal-url".to_string(),
        "https://example.test/arin.tal".to_string(),
        "--tal-url".to_string(),
        "https://example.test/apnic.tal".to_string(),
    ];
    let args = parse_args(&argv).expect("parse");
    assert_eq!(args.tal_urls.len(), 2);
    assert_eq!(args.tal_inputs.len(), 2);
}

#[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_paths, vec![PathBuf::from("a.tal")]);
    assert_eq!(args.ta_paths, vec![PathBuf::from("ta.cer")]);
    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_ca_depth, 0);
}

#[test]
fn parse_accepts_multiple_tal_path_pairs_by_default() {
    let argv = vec![
        "rpki".to_string(),
        "--db".to_string(),
        "db".to_string(),
        "--tal-path".to_string(),
        "apnic.tal".to_string(),
        "--ta-path".to_string(),
        "apnic-ta.cer".to_string(),
        "--tal-path".to_string(),
        "arin.tal".to_string(),
        "--ta-path".to_string(),
        "arin-ta.cer".to_string(),
    ];
    let args = parse_args(&argv).expect("parse");
    assert_eq!(args.tal_paths.len(), 2);
    assert_eq!(args.ta_paths.len(), 2);
    assert_eq!(args.tal_inputs.len(), 2);
}

#[test]
fn parse_rejects_mixed_tal_url_and_tal_path_modes() {
    let argv = vec![
        "rpki".to_string(),
        "--db".to_string(),
        "db".to_string(),
        "--tal-url".to_string(),
        "https://example.test/arin.tal".to_string(),
        "--tal-path".to_string(),
        "apnic.tal".to_string(),
        "--ta-path".to_string(),
        "apnic-ta.cer".to_string(),
    ];
    let err = parse_args(&argv).unwrap_err();
    assert!(
        err.contains(
            "must specify either one-or-more --tal-url or one-or-more --tal-path/--ta-path pairs"
        ),
        "{err}"
    );
}

#[test]
fn parse_rejects_mismatched_tal_path_and_ta_path_counts() {
    let argv = vec![
        "rpki".to_string(),
        "--db".to_string(),
        "db".to_string(),
        "--tal-path".to_string(),
        "apnic.tal".to_string(),
        "--tal-path".to_string(),
        "arin.tal".to_string(),
        "--ta-path".to_string(),
        "apnic-ta.cer".to_string(),
    ];
    let err = parse_args(&argv).unwrap_err();
    assert!(
        err.contains("--tal-path and --ta-path counts must match"),
        "{err}"
    );
}

#[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_multiple_tal_paths_without_ta_when_disable_rrdp() {
    let argv = vec![
        "rpki".to_string(),
        "--db".to_string(),
        "db".to_string(),
        "--tal-path".to_string(),
        "a.tal".to_string(),
        "--tal-path".to_string(),
        "b.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_paths,
        vec![PathBuf::from("a.tal"), PathBuf::from("b.tal")]
    );
    assert!(args.ta_paths.is_empty());
    assert_eq!(args.tal_inputs.len(), 2);
    assert!(args.disable_rrdp);
}

#[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
    );
    assert_eq!(
        policy.resource_validation_mode,
        ResourceValidationMode::ValidationUpdate03
    );
    assert_eq!(policy.strict, StrictPolicy::default());
}

#[test]
fn read_policy_accepts_strict_table() {
    let dir = tempfile::tempdir().expect("tmpdir");
    let p = dir.path().join("policy.toml");
    std::fs::write(
        &p,
        r#"
            [strict]
            name = true
            cms_der = true
            "#,
    )
    .expect("write policy");

    let policy = read_policy(Some(&p)).expect("parse policy");
    assert_eq!(
        policy.strict,
        StrictPolicy {
            name: true,
            cms_der: true,
            signed_attrs: false,
        }
    );
}

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

    let policy = read_policy(Some(&p)).expect("parse policy");
    assert_eq!(
        policy.resource_validation_mode,
        ResourceValidationMode::Rfc6487
    );
}

#[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}");
}

fn synthetic_post_validation_shared() -> PostValidationShared {
    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,
            },
            crate::validation::objects::Vrp {
                asn: 64497,
                prefix: crate::data_model::roa::IpPrefix {
                    afi: crate::data_model::roa::RoaAfi::Ipv6,
                    prefix_len: 48,
                    addr: [0x20, 0x01, 0x0d, 0xb8, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0],
                },
                max_length: 64,
            },
        ],
        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.source = "fresh".to_string();
    pp1.rrdp_notification_uri = Some("https://example.test/n1.xml".to_string());
    pp1.manifest_rsync_uri = "rsync://example.test/repo/pp1/manifest.mft".to_string();
    pp1.objects.push(crate::audit::ObjectAuditEntry {
        rsync_uri: "rsync://example.test/repo/pp1/a.roa".to_string(),
        sha256_hex: "11".repeat(32),
        kind: crate::audit::AuditObjectKind::Roa,
        result: crate::audit::AuditObjectResult::Ok,
        detail: None,
    });
    let mut pp2 = crate::audit::PublicationPointAudit::default();
    pp2.source = "fresh".to_string();
    pp2.rrdp_notification_uri = Some("https://example.test/n1.xml".to_string());
    let mut pp3 = crate::audit::PublicationPointAudit::default();
    pp3.source = "fresh".to_string();
    pp3.rrdp_notification_uri = Some("https://example.test/n2.xml".to_string());

    let out = crate::validation::run_tree_from_tal::RunTreeFromTalAuditOutput {
        discovery: discovery.clone(),
        discoveries: vec![discovery],
        successful_tal_inputs: Vec::new(),
        tree,
        publication_points: vec![pp1, pp2, pp3],
        roa_cache_stats: crate::validation::objects::RoaValidationCacheStats::default(),
        downloads: Vec::new(),
        download_stats: crate::audit::AuditDownloadStats::default(),
        current_repo_objects: Vec::new(),
        ccr_accumulator: None,
        cir_input: crate::cir::CirInputSnapshot::default(),
    };
    PostValidationShared::from_run_output(out)
}

fn sample_cli_ccr_accumulator() -> CcrAccumulator {
    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 mut accumulator = CcrAccumulator::new(vec![discovery.trust_anchor.clone()]);
    let manifest_uri = "rsync://example.test/repo/current.mft".to_string();
    let projection = crate::storage::VcirCcrManifestProjection {
        manifest_rsync_uri: manifest_uri.clone(),
        manifest_sha256: vec![0x44; 32],
        manifest_size: 2048,
        manifest_ee_aki: vec![0x55; 20],
        manifest_number_be: vec![1],
        manifest_this_update: crate::storage::PackTime::from_utc_offset_datetime(
            time::OffsetDateTime::now_utc(),
        ),
        manifest_sia_locations_der: vec![
            crate::ccr::manifest_location::encode_access_description_der(&AccessDescription {
                access_method_oid: OID_AD_SIGNED_OBJECT.to_string(),
                access_location: manifest_uri,
            })
            .expect("encode signedObject"),
        ],
        subordinate_skis: vec![vec![0x33; 20]],
    };
    accumulator
        .append_manifest_projection(&projection)
        .expect("append manifest projection");
    accumulator
}

#[test]
fn build_report_and_helpers_work_on_synthetic_output() {
    let shared = synthetic_post_validation_shared();
    let policy = Policy::default();
    let validation_time = time::OffsetDateTime::now_utc();
    let report = build_report(&policy, validation_time, &shared);

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

    print_summary(&report);
}

#[test]
fn run_report_task_and_stage_timing_work() {
    let shared = synthetic_post_validation_shared();
    let policy = Policy::default();
    let validation_time = time::OffsetDateTime::now_utc();
    let dir = tempfile::tempdir().expect("tmpdir");
    let report_path = dir.path().join("report.json");
    let report_output = run_report_task(
        &policy,
        validation_time,
        &shared,
        Some(&report_path),
        ReportJsonFormat::Compact,
    )
    .expect("run report task");

    assert!(report_output.report_write_ms.is_some());

    let report_json = std::fs::read_to_string(&report_path).expect("read report json");
    assert!(!report_json.contains('\n'), "{report_json}");
    let report: serde_json::Value =
        serde_json::from_str(&report_json).expect("parse compact report json");
    assert_eq!(report["vrps"].as_array().unwrap().len(), 2);
    assert_eq!(report["aspas"].as_array().unwrap().len(), 1);
    assert_eq!(report["queryAudit"]["status"].as_str(), Some("complete"));
    assert!(report["queryAudit"]["eventsCount"].as_u64().unwrap() > 0);
    let events_path = dir.path().join(
        report["queryAudit"]["eventsPath"]
            .as_str()
            .expect("events path"),
    );
    let events = std::fs::read_to_string(events_path).expect("read validation events");
    assert!(
        events
            .lines()
            .any(|line| line.contains("\"eventType\":\"object\""))
    );

    let stage_timing = RunStageTiming {
        validation_ms: 1,
        enable_roa_validation_cache: false,
        enable_child_certificate_validation_cache: false,
        publication_point_cache_observe_only: false,
        enable_publication_point_validation_cache: false,
        crypto_signature_cache_observe: None,
        enable_transport_request_prefetch: false,
        report_build_ms: report_output.report_build_ms,
        report_write_ms: report_output.report_write_ms,
        ccr_build_ms: Some(2),
        ccr_build_breakdown: None,
        ccr_write_ms: Some(3),
        compare_view_build_ms: Some(4),
        compare_view_write_ms: Some(5),
        cir_build_cir_ms: Some(6),
        cir_write_cir_ms: Some(7),
        cir_total_ms: Some(8),
        total_ms: 9,
        publication_points: shared.publication_points.len(),
        repo_sync_ms_total: 10,
        publication_point_repo_sync_ms_total: 11,
        download_event_count: 12,
        rrdp_download_ms_total: 13,
        rsync_download_ms_total: 14,
        download_bytes_total: 15,
        roa_validation_cache: crate::validation::objects::RoaValidationCacheStats::default(),
        analysis_counts: std::collections::HashMap::new(),
        analysis_phases: std::collections::HashMap::new(),
        analysis_top_publication_points: Vec::new(),
        analysis_top_publication_point_steps: Vec::new(),
        analysis_top_publication_point_cache_steps: Vec::new(),
        vcir_storage_summary_ms: Some(16),
        vcir_storage: Some(VcirStorageSummary {
            entry_count: 2,
            vcir_value_bytes: 100,
            vcir_value_bytes_max: 60,
            vcir_value_bytes_max_manifest_rsync_uri: Some(
                "rsync://example.test/repo/max.mft".to_string(),
            ),
            core_fields: VcirCoreFieldSizeBreakdown {
                manifest_rsync_uri_bytes: 10,
                ..VcirCoreFieldSizeBreakdown::default()
            },
            ccr_projection: VcirCcrProjectionSizeBreakdown {
                manifest_sha256_bytes: 32,
                ..VcirCcrProjectionSizeBreakdown::default()
            },
            child_resources: VcirChildResourceSizeBreakdown {
                effective_ip_resource_cbor_bytes: 12,
                effective_as_resource_cbor_bytes: 6,
            },
            field_sizes: VcirFieldSizeBreakdown {
                local_output_count: 1,
                local_output_payload_json_bytes: 70,
                local_output_payload_typed_body_bytes: 20,
                ..VcirFieldSizeBreakdown::default()
            },
            local_output_old_projection_bytes: 80,
            local_output_typed_projection_bytes: 30,
            local_output_projection_saved_bytes: 50,
            top_entries_by_vcir_value_bytes: vec![VcirStorageEntrySummary {
                manifest_rsync_uri: "rsync://example.test/repo/max.mft".to_string(),
                vcir_value_bytes: 60,
                local_vrp_count: 1,
                local_aspa_count: 0,
                local_router_key_count: 0,
                accepted_object_count: 1,
                rejected_object_count: 0,
                child_count: 0,
                core_fields: VcirCoreFieldSizeBreakdown::default(),
                ccr_projection: VcirCcrProjectionSizeBreakdown::default(),
                child_resources: VcirChildResourceSizeBreakdown::default(),
                field_sizes: VcirFieldSizeBreakdown::default(),
                local_output_old_projection_bytes: 1,
                local_output_typed_projection_bytes: 1,
                local_output_projection_saved_bytes: 0,
            }],
        }),
        publication_point_cache_index_load: None,
        publication_point_cache_index_refresh: None,
        memory_telemetry: None,
    };
    write_stage_timing(Some(&report_path), &stage_timing).expect("write stage timing");
    let stage_timing_json =
        std::fs::read_to_string(dir.path().join("stage-timing.json")).expect("read timing");
    assert!(stage_timing_json.contains("\"validation_ms\""));
    assert!(stage_timing_json.contains("\"ccr_build_ms\""));
    assert!(stage_timing_json.contains("\"vcir_storage\""));
    assert!(stage_timing_json.contains("\"local_output_projection_saved_bytes\""));

    let ccr_path = dir.path().join("result.ccr");
    write_stage_timing(Some(&ccr_path), &stage_timing).expect("write stage timing via ccr path");
    assert!(
        dir.path().join("stage-timing.json").exists(),
        "stage timing should use parent directory of the anchor path"
    );

    let skipped = ReportTaskOutput::skipped();
    assert_eq!(skipped.report_build_ms, 0);
    assert!(skipped.report_write_ms.is_none());
}

#[test]
fn stage_timing_serializes_memory_telemetry() {
    let dir = tempfile::tempdir().expect("tmpdir");
    let report_path = dir.path().join("report.json");
    let stage_timing = RunStageTiming {
        validation_ms: 1,
        enable_roa_validation_cache: true,
        enable_child_certificate_validation_cache: true,
        publication_point_cache_observe_only: false,
        enable_publication_point_validation_cache: false,
        crypto_signature_cache_observe: None,
        enable_transport_request_prefetch: true,
        report_build_ms: 2,
        report_write_ms: None,
        ccr_build_ms: None,
        ccr_build_breakdown: None,
        ccr_write_ms: None,
        compare_view_build_ms: None,
        compare_view_write_ms: None,
        cir_build_cir_ms: None,
        cir_write_cir_ms: None,
        cir_total_ms: None,
        total_ms: 3,
        publication_points: 4,
        repo_sync_ms_total: 5,
        publication_point_repo_sync_ms_total: 6,
        download_event_count: 7,
        rrdp_download_ms_total: 8,
        rsync_download_ms_total: 9,
        download_bytes_total: 10,
        roa_validation_cache: crate::validation::objects::RoaValidationCacheStats {
            hit_roas: 2,
            ..crate::validation::objects::RoaValidationCacheStats::default()
        },
        analysis_counts: std::collections::HashMap::from([(
            "roa_validation_cache_hit_roas".to_string(),
            2,
        )]),
        analysis_phases: std::collections::HashMap::new(),
        analysis_top_publication_points: Vec::new(),
        analysis_top_publication_point_steps: Vec::new(),
        analysis_top_publication_point_cache_steps: Vec::new(),
        vcir_storage_summary_ms: None,
        vcir_storage: None,
        publication_point_cache_index_load: None,
        publication_point_cache_index_refresh: None,
        memory_telemetry: Some(MemoryTelemetrySummary {
            checkpoints: vec![MemoryTelemetryCheckpoint {
                label: "after_validation".to_string(),
                elapsed_ms: 11,
                process: ProcessMemorySnapshot {
                    label: "after_validation".to_string(),
                    vm_rss_kb: Some(12),
                    vm_size_kb: None,
                    vm_data_kb: None,
                    vm_swap_kb: None,
                    rss_anon_kb: Some(13),
                    rss_file_kb: None,
                    rss_shmem_kb: None,
                    threads: Some(14),
                    fd_count: Some(15),
                    smaps_rollup: None,
                    smaps_mapping_summary: None,
                    errors: Vec::new(),
                },
                rocksdb: RocksDbMemorySnapshot {
                    databases: Vec::new(),
                    totals: RocksDbMemoryTotals {
                        cur_size_all_mem_tables: 16,
                        size_all_mem_tables: 17,
                        estimate_table_readers_mem: 18,
                        block_cache_capacity: 19,
                        block_cache_usage: 20,
                        block_cache_pinned_usage: 21,
                    },
                },
            }],
            object_graph: None,
            malloc_trim_probes: Vec::new(),
        }),
    };

    write_stage_timing(Some(&report_path), &stage_timing).expect("write stage timing");
    let value: serde_json::Value = serde_json::from_str(
        &std::fs::read_to_string(dir.path().join("stage-timing.json")).unwrap(),
    )
    .expect("parse stage timing json");
    let checkpoint = &value["memory_telemetry"]["checkpoints"][0];
    assert_eq!(checkpoint["label"], "after_validation");
    assert_eq!(checkpoint["process"]["vm_rss_kb"], 12);
    assert_eq!(
        checkpoint["rocksdb"]["totals"]["cur_size_all_mem_tables"],
        16
    );
    assert_eq!(value["analysis_counts"]["roa_validation_cache_hit_roas"], 2);
    assert_eq!(value["roa_validation_cache"]["hit_roas"], 2);
    assert_eq!(value["publication_point_cache_observe_only"], false);
    assert_eq!(value["enable_publication_point_validation_cache"], false);
    assert!(
        value["memory_telemetry"]
            .as_object()
            .expect("memory telemetry object")
            .get("malloc_trim_probes")
            .is_none()
    );
}

#[test]
fn shared_object_graph_estimate_counts_audit_and_outputs() {
    let mut shared = synthetic_post_validation_shared();
    let mut publication_points = shared
        .publication_points
        .iter()
        .cloned()
        .collect::<Vec<_>>();
    publication_points[0].rsync_base_uri = "rsync://example.test/repo/".to_string();
    publication_points[0].manifest_rsync_uri = "rsync://example.test/repo/a.mft".to_string();
    publication_points[0].publication_point_rsync_uri = "rsync://example.test/repo/".to_string();
    publication_points[0].objects = vec![crate::audit::ObjectAuditEntry {
        rsync_uri: "rsync://example.test/repo/a.roa".to_string(),
        sha256_hex: "11".repeat(32),
        kind: crate::audit::AuditObjectKind::Roa,
        result: crate::audit::AuditObjectResult::Ok,
        detail: None,
    }];
    shared.publication_points = publication_points.into();

    let graph = estimate_shared_object_graph(&shared);
    let publication_points_section = graph
        .sections
        .iter()
        .find(|section| section.name == "publication_points")
        .expect("publication points section");
    let object_count = publication_points_section
        .details
        .iter()
        .find(|metric| metric.name == "object_audit_entry_count")
        .expect("object count metric");
    assert_eq!(object_count.value, 1);
    assert!(publication_points_section.estimated_bytes > 0);

    let vrps_section = graph
        .sections
        .iter()
        .find(|section| section.name == "vrps")
        .expect("vrps section");
    assert_eq!(vrps_section.item_count, 2);
    assert!(graph.total_estimated_bytes >= publication_points_section.estimated_bytes);
}

#[test]
fn run_compare_view_task_writes_csv_from_shared_output() {
    let shared = synthetic_post_validation_shared();
    let dir = tempfile::tempdir().expect("tmpdir");
    let vrps_path = dir.path().join("vrps.csv");
    let vaps_path = dir.path().join("vaps.csv");

    let output = run_compare_view_task(&shared, Some(&vrps_path), Some(&vaps_path), "unknown")
        .expect("write direct compare views");

    assert!(output.build_ms.is_some());
    assert!(output.write_ms.is_some());
    let vrps_csv = std::fs::read_to_string(vrps_path).expect("read vrps csv");
    let vaps_csv = std::fs::read_to_string(vaps_path).expect("read vaps csv");
    assert!(vrps_csv.contains("ASN,IP Prefix,Max Length,Trust Anchor"));
    assert!(vrps_csv.contains("AS64496,192.0.2.0/24,24,unknown"));
    assert!(vrps_csv.contains("AS64497,2001:db8::/48,64,unknown"));
    assert!(vaps_csv.contains("Customer ASN,Providers,Trust Anchor"));
    assert!(vaps_csv.contains("AS64496,AS64497;AS64498,unknown"));
}

#[test]
fn run_ccr_task_uses_accumulator_when_phase2_output_contains_reuse_sources() {
    let mut shared = synthetic_post_validation_shared();
    shared.ccr_accumulator = Some(sample_cli_ccr_accumulator());
    let mut publication_points = shared
        .publication_points
        .iter()
        .cloned()
        .collect::<Vec<_>>();
    publication_points[1].source = "vcir_current_instance".to_string();
    publication_points[2].source = "failed_no_cache".to_string();
    shared.publication_points = publication_points.into();
    let dir = tempfile::tempdir().expect("tmpdir");
    let ccr_path = dir.path().join("result.ccr");
    let store = RocksStore::open(&dir.path().join("db")).expect("open empty store");

    let output = run_ccr_task(
        &store,
        &shared,
        Some(&ccr_path),
        time::OffsetDateTime::now_utc(),
    )
    .expect("run ccr task");

    assert!(output.ccr_build_ms.is_some());
    assert!(output.ccr_build_breakdown.is_none());
    let der = std::fs::read(&ccr_path).expect("read ccr");
    let ci = crate::ccr::decode_content_info(&der).expect("decode ccr");
    assert_eq!(
        ci.content
            .mfts
            .as_ref()
            .map(|manifest_state| manifest_state.mis.len()),
        Some(1)
    );
}

#[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(),
        repo_sync_stats: crate::audit::AuditRepoSyncStats::default(),
        query_audit: None,
    };

    let dir = tempfile::tempdir().expect("tmpdir");
    let pretty_path = dir.path().join("report-pretty.json");
    write_json(&pretty_path, &report, ReportJsonFormat::Pretty).expect("write pretty json");
    let pretty = std::fs::read_to_string(&pretty_path).expect("read pretty report");
    assert!(pretty.contains("\"format_version\""));
    assert!(pretty.contains("\"policy\""));
    assert!(pretty.contains("\n  \"format_version\""), "{pretty}");

    let compact_path = dir.path().join("report-compact.json");
    write_json(&compact_path, &report, ReportJsonFormat::Compact).expect("write compact json");
    let compact = std::fs::read_to_string(&compact_path).expect("read compact report");
    assert!(compact.contains("\"format_version\""));
    assert!(compact.contains("\"policy\""));
    assert!(!compact.contains('\n'), "{compact}");
}

#[test]
fn build_repo_sync_stats_aggregates_phase_and_terminal_state() {
    let mut pp1 = crate::audit::PublicationPointAudit::default();
    pp1.repo_sync_phase = Some("rrdp_ok".to_string());
    pp1.repo_sync_duration_ms = Some(10);
    pp1.repo_terminal_state = "fresh".to_string();

    let mut pp2 = crate::audit::PublicationPointAudit::default();
    pp2.repo_sync_phase = Some("rrdp_failed_rsync_failed".to_string());
    pp2.repo_sync_duration_ms = Some(20);
    pp2.repo_terminal_state = "failed_no_cache".to_string();

    let mut pp3 = crate::audit::PublicationPointAudit::default();
    pp3.repo_sync_phase = Some("rrdp_failed_rsync_failed".to_string());
    pp3.repo_sync_duration_ms = Some(30);
    pp3.repo_terminal_state = "failed_no_cache".to_string();

    let stats = build_repo_sync_stats(&[pp1, pp2, pp3]);
    assert_eq!(stats.publication_points_total, 3);
    assert_eq!(stats.by_phase["rrdp_ok"].count, 1);
    assert_eq!(stats.by_phase["rrdp_ok"].duration_ms_total, 10);
    assert_eq!(stats.by_phase["rrdp_failed_rsync_failed"].count, 2);
    assert_eq!(
        stats.by_phase["rrdp_failed_rsync_failed"].duration_ms_total,
        50
    );
    assert_eq!(stats.by_terminal_state["fresh"].count, 1);
    assert_eq!(stats.by_terminal_state["failed_no_cache"].count, 2);
    assert_eq!(
        stats.by_terminal_state["failed_no_cache"].duration_ms_total,
        50
    );
}
