use std::collections::HashMap;

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

fn empty_snapshot(manifest_uri: &str, pp_uri: &str) -> PublicationPointSnapshot {
    PublicationPointSnapshot {
        format_version: PublicationPointSnapshot::FORMAT_VERSION_V1,
        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::new(),
    }
}

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,
    }
}

#[derive(Default)]
struct ResultRunner {
    by_manifest: HashMap<String, Result<PublicationPointRunResult, String>>,
}

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

    fn with_err(mut self, manifest: &str, err: &str) -> Self {
        self.by_manifest
            .insert(manifest.to_string(), Err(err.to_string()));
        self
    }
}

impl PublicationPointRunner for ResultRunner {
    fn run_publication_point(
        &self,
        ca: &CaInstanceHandle,
    ) -> Result<PublicationPointRunResult, String> {
        self.by_manifest
            .get(&ca.manifest_rsync_uri)
            .cloned()
            .unwrap_or_else(|| Err(format!("no mock for {}", ca.manifest_rsync_uri)))
    }
}

#[test]
fn tree_continues_when_a_publication_point_fails() {
    let root_manifest = "rsync://example.test/repo/root.mft";
    let bad_child_manifest = "rsync://example.test/repo/bad-child.mft";
    let ok_child_manifest = "rsync://example.test/repo/ok-child.mft";

    let runner = ResultRunner::default()
        .with_ok(
            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, bad_child_manifest),
                    discovered_child(root_manifest, ok_child_manifest),
                ],
            },
        )
        .with_err(bad_child_manifest, "synthetic failure")
        .with_ok(
            ok_child_manifest,
            PublicationPointRunResult {
                source: PublicationPointSource::Fresh,
                snapshot: Some(empty_snapshot(
                    ok_child_manifest,
                    "rsync://example.test/repo/ok-child/",
                )),
                warnings: vec![Warning::new("ok child 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: Vec::new(),
            },
        );

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

    assert_eq!(out.instances_processed, 2);
    assert_eq!(out.instances_failed, 1);
    assert!(
        out.warnings
            .iter()
            .any(|w| w.message.contains("publication point failed")),
        "expected failure warning"
    );
    assert!(
        out.warnings
            .iter()
            .any(|w| w.message.contains("ok child warning")),
        "expected ok child warning propagated"
    );
}
