use std::collections::HashMap;

use rpki::audit::{DiscoveredFrom, PublicationPointAudit};
use rpki::report::Warning;
use rpki::storage::{PackFile, PackTime};
use rpki::validation::manifest::PublicationPointSource;
use rpki::validation::objects::{ObjectsOutput, ObjectsStats, RouterKeyPayload};
use rpki::validation::publication_point::PublicationPointSnapshot;
use rpki::validation::tree::{
    CaCertificateRef, CaInstanceHandle, DiscoveredChildCaInstance, PublicationPointRunResult,
    PublicationPointRunner, TreeRunConfig, run_tree_serial, run_tree_serial_audit,
};

#[derive(Default)]
struct MockRunner {
    by_manifest: HashMap<String, PublicationPointRunResult>,
    calls: std::sync::Mutex<Vec<String>>,
}

impl MockRunner {
    fn with(mut self, manifest: &str, res: PublicationPointRunResult) -> Self {
        self.by_manifest.insert(manifest.to_string(), res);
        self
    }

    fn called(&self) -> Vec<String> {
        self.calls.lock().unwrap().clone()
    }
}

impl PublicationPointRunner for MockRunner {
    fn run_publication_point(
        &self,
        ca: &CaInstanceHandle,
    ) -> Result<PublicationPointRunResult, String> {
        self.calls
            .lock()
            .unwrap()
            .push(ca.manifest_rsync_uri.clone());
        self.by_manifest
            .get(&ca.manifest_rsync_uri)
            .cloned()
            .ok_or_else(|| format!("no mock for {}", ca.manifest_rsync_uri))
    }
}

fn empty_snapshot(manifest_uri: &str, pp_uri: &str) -> PublicationPointSnapshot {
    PublicationPointSnapshot {
        format_version: 1,
        publication_point_rsync_uri: pp_uri.to_string(),
        manifest_rsync_uri: manifest_uri.to_string(),
        manifest_number_be: vec![1],
        this_update: PackTime {
            rfc3339_utc: "2026-01-01T00:00:00Z".to_string(),
        },
        next_update: PackTime {
            rfc3339_utc: "2026-12-31T00:00:00Z".to_string(),
        },
        verified_at: PackTime {
            rfc3339_utc: "2026-02-06T00:00:00Z".to_string(),
        },
        manifest_bytes: vec![1, 2, 3],
        files: vec![PackFile::from_bytes_compute_sha256(manifest_uri, vec![1])],
    }
}

fn ca_handle(manifest_uri: &str) -> CaInstanceHandle {
    CaInstanceHandle {
        depth: 0,
        tal_id: "test-tal".to_string(),
        parent_manifest_rsync_uri: None,
        ca_certificate: CaCertificateRef::inline_der(Vec::new()),
        ca_certificate_rsync_uri: None,
        effective_ip_resources: None,
        effective_as_resources: None,
        rsync_base_uri: "rsync://example.test/repo/".to_string(),
        manifest_rsync_uri: manifest_uri.to_string(),
        publication_point_rsync_uri: "rsync://example.test/repo/".to_string(),
        rrdp_notification_uri: None,
    }
}

fn discovered_child(
    parent_manifest_uri: &str,
    child_manifest_uri: &str,
) -> DiscoveredChildCaInstance {
    let name = child_manifest_uri
        .rsplit('/')
        .next()
        .unwrap_or("child.mft")
        .trim_end_matches(".mft");
    DiscoveredChildCaInstance {
        handle: ca_handle(child_manifest_uri),
        discovered_from: DiscoveredFrom {
            parent_manifest_rsync_uri: parent_manifest_uri.to_string(),
            child_ca_certificate_rsync_uri: format!("rsync://example.test/repo/{name}.cer"),
            child_ca_certificate_sha256_hex: "00".repeat(32),
        },
        child_entry_projection: None,
    }
}

#[test]
fn tree_enqueues_children_for_fresh_and_current_instance_vcir_results() {
    let root_manifest = "rsync://example.test/repo/root.mft";
    let child1_manifest = "rsync://example.test/repo/child1.mft";
    let child2_manifest = "rsync://example.test/repo/child2.mft";
    let grandchild_manifest = "rsync://example.test/repo/grandchild.mft";

    let root_children = vec![
        discovered_child(root_manifest, child1_manifest),
        discovered_child(root_manifest, child2_manifest),
    ];
    let child1_children = vec![discovered_child(child1_manifest, grandchild_manifest)];

    let runner = MockRunner::default()
        .with(
            root_manifest,
            PublicationPointRunResult {
                source: PublicationPointSource::Fresh,
                snapshot: Some(empty_snapshot(root_manifest, "rsync://example.test/repo/")),
                warnings: Vec::new(),
                objects: ObjectsOutput {
                    vrps: Vec::new(),
                    aspas: Vec::new(),
                    router_keys: Vec::new(),
                    local_outputs_cache: Vec::new(),
                    warnings: Vec::new(),
                    stats: ObjectsStats::default(),
                    audit: Vec::new(),
                    roa_cache_stats: Default::default(),
                    roa_cache_object_meta: Vec::new(),
                },
                audit: PublicationPointAudit::default(),
                cir_fresh_objects: Vec::new(),
                cir_cached_objects: Vec::new(),
                discovered_children: root_children,
            },
        )
        .with(
            child1_manifest,
            PublicationPointRunResult {
                source: PublicationPointSource::VcirCurrentInstance,
                snapshot: Some(empty_snapshot(
                    child1_manifest,
                    "rsync://example.test/repo/child1/",
                )),
                warnings: vec![Warning::new("child1 warning")],
                objects: ObjectsOutput {
                    vrps: Vec::new(),
                    aspas: Vec::new(),
                    router_keys: Vec::new(),
                    local_outputs_cache: Vec::new(),
                    warnings: Vec::new(),
                    stats: ObjectsStats::default(),
                    audit: Vec::new(),
                    roa_cache_stats: Default::default(),
                    roa_cache_object_meta: Vec::new(),
                },
                audit: PublicationPointAudit::default(),
                cir_fresh_objects: Vec::new(),
                cir_cached_objects: Vec::new(),
                discovered_children: child1_children,
            },
        )
        .with(
            child2_manifest,
            PublicationPointRunResult {
                source: PublicationPointSource::Fresh,
                snapshot: Some(empty_snapshot(
                    child2_manifest,
                    "rsync://example.test/repo/child2/",
                )),
                warnings: Vec::new(),
                objects: ObjectsOutput {
                    vrps: Vec::new(),
                    aspas: Vec::new(),
                    router_keys: Vec::new(),
                    local_outputs_cache: Vec::new(),
                    warnings: Vec::new(),
                    stats: ObjectsStats::default(),
                    audit: Vec::new(),
                    roa_cache_stats: Default::default(),
                    roa_cache_object_meta: Vec::new(),
                },
                audit: PublicationPointAudit::default(),
                cir_fresh_objects: Vec::new(),
                cir_cached_objects: Vec::new(),
                discovered_children: Vec::new(),
            },
        )
        .with(
            grandchild_manifest,
            PublicationPointRunResult {
                source: PublicationPointSource::Fresh,
                snapshot: Some(empty_snapshot(
                    grandchild_manifest,
                    "rsync://example.test/repo/grandchild/",
                )),
                warnings: Vec::new(),
                objects: ObjectsOutput {
                    vrps: Vec::new(),
                    aspas: Vec::new(),
                    router_keys: Vec::new(),
                    local_outputs_cache: Vec::new(),
                    warnings: Vec::new(),
                    stats: ObjectsStats::default(),
                    audit: Vec::new(),
                    roa_cache_stats: Default::default(),
                    roa_cache_object_meta: Vec::new(),
                },
                audit: PublicationPointAudit::default(),
                cir_fresh_objects: Vec::new(),
                cir_cached_objects: Vec::new(),
                discovered_children: Vec::new(),
            },
        );

    let out = run_tree_serial(ca_handle(root_manifest), &runner, &TreeRunConfig::default())
        .expect("run tree");

    // root + child1 + child2 + grandchild. child1 uses current-instance VCIR, but its child
    // entry is still restored and processed fresh-first later in the traversal.
    assert_eq!(out.instances_processed, 4);
    assert_eq!(out.instances_failed, 0);

    let called = runner.called();
    assert_eq!(
        called,
        vec![
            root_manifest,
            child1_manifest,
            child2_manifest,
            grandchild_manifest
        ]
    );

    assert!(
        out.warnings
            .iter()
            .any(|w| w.message.contains("child1 warning")),
        "expected child1 warning propagated"
    );
    assert!(
        out.warnings
            .iter()
            .all(|w| !w.message.contains("skipping child CA discovery")),
        "did not expect old skip-child warning"
    );
}

#[test]
fn tree_respects_max_depth_and_max_instances() {
    let root_manifest = "rsync://example.test/repo/root.mft";
    let child_manifest = "rsync://example.test/repo/child.mft";
    let grandchild_manifest = "rsync://example.test/repo/grandchild.mft";

    let runner = MockRunner::default()
        .with(
            root_manifest,
            PublicationPointRunResult {
                source: PublicationPointSource::Fresh,
                snapshot: Some(empty_snapshot(root_manifest, "rsync://example.test/repo/")),
                warnings: Vec::new(),
                objects: ObjectsOutput {
                    vrps: Vec::new(),
                    aspas: Vec::new(),
                    router_keys: Vec::new(),
                    local_outputs_cache: Vec::new(),
                    warnings: Vec::new(),
                    stats: ObjectsStats::default(),
                    audit: Vec::new(),
                    roa_cache_stats: Default::default(),
                    roa_cache_object_meta: Vec::new(),
                },
                audit: PublicationPointAudit::default(),
                cir_fresh_objects: Vec::new(),
                cir_cached_objects: Vec::new(),
                discovered_children: vec![discovered_child(root_manifest, child_manifest)],
            },
        )
        .with(
            child_manifest,
            PublicationPointRunResult {
                source: PublicationPointSource::Fresh,
                snapshot: Some(empty_snapshot(
                    child_manifest,
                    "rsync://example.test/repo/child/",
                )),
                warnings: Vec::new(),
                objects: ObjectsOutput {
                    vrps: Vec::new(),
                    aspas: Vec::new(),
                    router_keys: Vec::new(),
                    local_outputs_cache: Vec::new(),
                    warnings: Vec::new(),
                    stats: ObjectsStats::default(),
                    audit: Vec::new(),
                    roa_cache_stats: Default::default(),
                    roa_cache_object_meta: Vec::new(),
                },
                audit: PublicationPointAudit::default(),
                cir_fresh_objects: Vec::new(),
                cir_cached_objects: Vec::new(),
                discovered_children: vec![discovered_child(child_manifest, grandchild_manifest)],
            },
        )
        .with(
            grandchild_manifest,
            PublicationPointRunResult {
                source: PublicationPointSource::Fresh,
                snapshot: Some(empty_snapshot(
                    grandchild_manifest,
                    "rsync://example.test/repo/grandchild/",
                )),
                warnings: Vec::new(),
                objects: ObjectsOutput {
                    vrps: Vec::new(),
                    aspas: Vec::new(),
                    router_keys: Vec::new(),
                    local_outputs_cache: Vec::new(),
                    warnings: Vec::new(),
                    stats: ObjectsStats::default(),
                    audit: Vec::new(),
                    roa_cache_stats: Default::default(),
                    roa_cache_object_meta: Vec::new(),
                },
                audit: PublicationPointAudit::default(),
                cir_fresh_objects: Vec::new(),
                cir_cached_objects: Vec::new(),
                discovered_children: Vec::new(),
            },
        );

    let out = run_tree_serial(
        ca_handle(root_manifest),
        &runner,
        &TreeRunConfig {
            max_depth: Some(0),
            max_instances: None,
            compact_audit: false,
            persist_vcir: true,
            build_ccr_accumulator: true,
            enable_roa_validation_cache: false,
            enable_child_certificate_validation_cache: false,
            publication_point_cache_observe_only: false,
            enable_publication_point_validation_cache: false,
            enable_transport_request_prefetch: false,
        },
    )
    .expect("run tree depth-limited");
    assert_eq!(out.instances_processed, 1);
    assert_eq!(out.instances_failed, 0);
    assert_eq!(runner.called(), vec![root_manifest]);

    let out = run_tree_serial(
        ca_handle(root_manifest),
        &runner,
        &TreeRunConfig {
            max_depth: Some(1),
            max_instances: None,
            compact_audit: false,
            persist_vcir: true,
            build_ccr_accumulator: true,
            enable_roa_validation_cache: false,
            enable_child_certificate_validation_cache: false,
            publication_point_cache_observe_only: false,
            enable_publication_point_validation_cache: false,
            enable_transport_request_prefetch: false,
        },
    )
    .expect("run tree depth-one-limited");
    assert_eq!(out.instances_processed, 2);
    assert_eq!(out.instances_failed, 0);
    assert_eq!(
        runner.called(),
        vec![root_manifest, root_manifest, child_manifest]
    );

    let out = run_tree_serial(
        ca_handle(root_manifest),
        &runner,
        &TreeRunConfig {
            max_depth: Some(2),
            max_instances: None,
            compact_audit: false,
            persist_vcir: true,
            build_ccr_accumulator: true,
            enable_roa_validation_cache: false,
            enable_child_certificate_validation_cache: false,
            publication_point_cache_observe_only: false,
            enable_publication_point_validation_cache: false,
            enable_transport_request_prefetch: false,
        },
    )
    .expect("run tree depth-two-limited");
    assert_eq!(out.instances_processed, 3);
    assert_eq!(out.instances_failed, 0);
    assert_eq!(
        runner.called(),
        vec![
            root_manifest,
            root_manifest,
            child_manifest,
            root_manifest,
            child_manifest,
            grandchild_manifest,
        ]
    );

    let out = run_tree_serial(
        ca_handle(root_manifest),
        &runner,
        &TreeRunConfig {
            max_depth: None,
            max_instances: Some(1),
            compact_audit: false,
            persist_vcir: true,
            build_ccr_accumulator: true,
            enable_roa_validation_cache: false,
            enable_child_certificate_validation_cache: false,
            publication_point_cache_observe_only: false,
            enable_publication_point_validation_cache: false,
            enable_transport_request_prefetch: false,
        },
    )
    .expect("run tree instance-limited");
    assert_eq!(out.instances_processed, 1);
    assert_eq!(out.instances_failed, 0);
}

#[test]
fn tree_audit_includes_parent_and_discovered_from_for_non_root_nodes() {
    let root_manifest = "rsync://example.test/repo/root.mft";
    let child_manifest = "rsync://example.test/repo/child.mft";

    let runner = MockRunner::default()
        .with(
            root_manifest,
            PublicationPointRunResult {
                source: PublicationPointSource::Fresh,
                snapshot: Some(empty_snapshot(root_manifest, "rsync://example.test/repo/")),
                warnings: Vec::new(),
                objects: ObjectsOutput {
                    vrps: Vec::new(),
                    aspas: Vec::new(),
                    router_keys: Vec::new(),
                    local_outputs_cache: Vec::new(),
                    warnings: Vec::new(),
                    stats: ObjectsStats::default(),
                    audit: Vec::new(),
                    roa_cache_stats: Default::default(),
                    roa_cache_object_meta: Vec::new(),
                },
                audit: PublicationPointAudit::default(),
                cir_fresh_objects: Vec::new(),
                cir_cached_objects: Vec::new(),
                discovered_children: vec![discovered_child(root_manifest, child_manifest)],
            },
        )
        .with(
            child_manifest,
            PublicationPointRunResult {
                source: PublicationPointSource::Fresh,
                snapshot: Some(empty_snapshot(
                    child_manifest,
                    "rsync://example.test/repo/child/",
                )),
                warnings: Vec::new(),
                objects: ObjectsOutput {
                    vrps: Vec::new(),
                    aspas: Vec::new(),
                    router_keys: Vec::new(),
                    local_outputs_cache: Vec::new(),
                    warnings: Vec::new(),
                    stats: ObjectsStats::default(),
                    audit: Vec::new(),
                    roa_cache_stats: Default::default(),
                    roa_cache_object_meta: Vec::new(),
                },
                audit: PublicationPointAudit::default(),
                cir_fresh_objects: Vec::new(),
                cir_cached_objects: Vec::new(),
                discovered_children: Vec::new(),
            },
        );

    let out = run_tree_serial_audit(ca_handle(root_manifest), &runner, &TreeRunConfig::default())
        .expect("run tree audit");

    assert_eq!(out.tree.instances_processed, 2);
    assert_eq!(out.publication_points.len(), 2);

    let root_audit = &out.publication_points[0];
    assert_eq!(root_audit.node_id, Some(0));
    assert_eq!(root_audit.parent_node_id, None);
    assert!(root_audit.discovered_from.is_none());

    let child_audit = &out.publication_points[1];
    assert_eq!(child_audit.node_id, Some(1));
    assert_eq!(child_audit.parent_node_id, Some(0));
    let df = child_audit
        .discovered_from
        .as_ref()
        .expect("child discovered_from");
    assert_eq!(df.parent_manifest_rsync_uri, root_manifest);
}

#[test]
fn tree_aggregates_router_keys_from_publication_point_results() {
    let root_manifest = "rsync://example.test/repo/root.mft";

    let runner = MockRunner::default().with(
        root_manifest,
        PublicationPointRunResult {
            source: PublicationPointSource::Fresh,
            snapshot: Some(empty_snapshot(root_manifest, "rsync://example.test/repo/")),
            warnings: Vec::new(),
            objects: ObjectsOutput {
                vrps: Vec::new(),
                aspas: Vec::new(),
                router_keys: vec![
                    RouterKeyPayload {
                        as_id: 64496,
                        ski: vec![0x11; 20],
                        spki_der: vec![0x30, 0x00],
                        source_object_uri: "rsync://example.test/repo/router1.cer".to_string(),
                        source_object_hash: "11".repeat(32),
                        source_ee_cert_hash: "11".repeat(32),
                        item_effective_until: PackTime {
                            rfc3339_utc: "2026-12-31T00:00:00Z".to_string(),
                        },
                    },
                    RouterKeyPayload {
                        as_id: 64497,
                        ski: vec![0x22; 20],
                        spki_der: vec![0x30, 0x01],
                        source_object_uri: "rsync://example.test/repo/router2.cer".to_string(),
                        source_object_hash: "22".repeat(32),
                        source_ee_cert_hash: "22".repeat(32),
                        item_effective_until: PackTime {
                            rfc3339_utc: "2026-12-31T00:00:00Z".to_string(),
                        },
                    },
                ],
                local_outputs_cache: Vec::new(),
                warnings: Vec::new(),
                stats: ObjectsStats::default(),
                audit: Vec::new(),
                roa_cache_stats: Default::default(),
                roa_cache_object_meta: Vec::new(),
            },
            audit: PublicationPointAudit::default(),
            cir_fresh_objects: Vec::new(),
            cir_cached_objects: Vec::new(),
            discovered_children: Vec::new(),
        },
    );

    let out = run_tree_serial(ca_handle(root_manifest), &runner, &TreeRunConfig::default())
        .expect("run tree");

    assert_eq!(out.router_keys.len(), 2);
    assert_eq!(out.router_keys[0].as_id, 64496);
    assert_eq!(out.router_keys[1].as_id, 64497);
}

#[test]
fn tree_prefers_lexicographically_first_discovery_when_duplicate_manifest_is_queued() {
    let root_manifest = "rsync://example.test/repo/root.mft";
    let duplicate_manifest = "rsync://example.test/repo/child.mft";

    let mut first = discovered_child(root_manifest, duplicate_manifest);
    first.discovered_from.child_ca_certificate_rsync_uri =
        "rsync://example.test/repo/z-child.cer".to_string();
    let mut second = discovered_child(root_manifest, duplicate_manifest);
    second.discovered_from.child_ca_certificate_rsync_uri =
        "rsync://example.test/repo/a-child.cer".to_string();

    let runner = MockRunner::default()
        .with(
            root_manifest,
            PublicationPointRunResult {
                source: PublicationPointSource::Fresh,
                snapshot: Some(empty_snapshot(root_manifest, "rsync://example.test/repo/")),
                warnings: Vec::new(),
                objects: ObjectsOutput {
                    vrps: Vec::new(),
                    aspas: Vec::new(),
                    router_keys: Vec::new(),
                    local_outputs_cache: Vec::new(),
                    warnings: Vec::new(),
                    stats: ObjectsStats::default(),
                    audit: Vec::new(),
                    roa_cache_stats: Default::default(),
                    roa_cache_object_meta: Vec::new(),
                },
                audit: PublicationPointAudit::default(),
                cir_fresh_objects: Vec::new(),
                cir_cached_objects: Vec::new(),
                discovered_children: vec![first, second],
            },
        )
        .with(
            duplicate_manifest,
            PublicationPointRunResult {
                source: PublicationPointSource::Fresh,
                snapshot: Some(empty_snapshot(
                    duplicate_manifest,
                    "rsync://example.test/repo/child/",
                )),
                warnings: Vec::new(),
                objects: ObjectsOutput {
                    vrps: Vec::new(),
                    aspas: Vec::new(),
                    router_keys: Vec::new(),
                    local_outputs_cache: Vec::new(),
                    warnings: Vec::new(),
                    stats: ObjectsStats::default(),
                    audit: Vec::new(),
                    roa_cache_stats: Default::default(),
                    roa_cache_object_meta: Vec::new(),
                },
                audit: PublicationPointAudit::default(),
                cir_fresh_objects: Vec::new(),
                cir_cached_objects: Vec::new(),
                discovered_children: Vec::new(),
            },
        );

    let out = run_tree_serial_audit(ca_handle(root_manifest), &runner, &TreeRunConfig::default())
        .expect("run tree audit");

    assert_eq!(out.tree.instances_processed, 2);
    assert_eq!(
        runner.called(),
        vec![root_manifest.to_string(), duplicate_manifest.to_string()]
    );
    let child_audit = &out.publication_points[1];
    assert_eq!(
        child_audit
            .discovered_from
            .as_ref()
            .expect("child discovered_from")
            .child_ca_certificate_rsync_uri,
        "rsync://example.test/repo/a-child.cer"
    );
}
