use rpki::analysis::timing::{TimingHandle, TimingMeta};
use rpki::validation::from_tal::discover_root_ca_instance_from_tal_and_ta_der;
use rpki::validation::run_tree_from_tal::root_handle_from_trust_anchor;
use rpki::validation::run_tree_from_tal::{
    run_tree_from_tal_and_ta_der_serial, run_tree_from_tal_and_ta_der_serial_audit,
    run_tree_from_tal_and_ta_der_serial_audit_with_timing, run_tree_from_tal_url_serial,
    run_tree_from_tal_url_serial_audit, run_tree_from_tal_url_serial_audit_with_timing,
};
use rpki::validation::tree::{CaCertificateRef, TreeRunConfig};

use std::collections::HashMap;

struct MapHttpFetcher {
    map: HashMap<String, Vec<u8>>,
}

impl rpki::sync::rrdp::Fetcher for MapHttpFetcher {
    fn fetch(&self, uri: &str) -> Result<Vec<u8>, String> {
        self.map
            .get(uri)
            .cloned()
            .ok_or_else(|| format!("no fixture mapped for {uri}"))
    }
}

struct EmptyRsyncFetcher;
impl rpki::fetch::rsync::RsyncFetcher for EmptyRsyncFetcher {
    fn fetch_objects(
        &self,
        _rsync_base_uri: &str,
    ) -> Result<Vec<(String, Vec<u8>)>, rpki::fetch::rsync::RsyncFetchError> {
        Ok(Vec::new())
    }
}

fn test_timing_handle(temp: &tempfile::TempDir) -> TimingHandle {
    TimingHandle::new(TimingMeta {
        recorded_at_utc_rfc3339: "2026-03-11T00:00:00Z".to_string(),
        validation_time_utc_rfc3339: "2026-03-11T00:00:00Z".to_string(),
        tal_url: Some("mock:apnic.tal".to_string()),
        db_path: Some(temp.path().display().to_string()),
    })
}

fn read_timing_json(temp: &tempfile::TempDir, timing: &TimingHandle) -> serde_json::Value {
    let path = temp.path().join("timing.json");
    timing.write_json(&path, 20).expect("write timing json");
    serde_json::from_slice(&std::fs::read(&path).expect("read timing json"))
        .expect("parse timing json")
}

#[test]
fn root_handle_is_constructible_from_fixture_tal_and_ta() {
    let tal_bytes = std::fs::read("tests/fixtures/tal/apnic-rfc7730-https.tal")
        .expect("read apnic tal fixture");
    let ta_der = std::fs::read("tests/fixtures/ta/apnic-ta.cer").expect("read apnic ta fixture");

    let discovery =
        discover_root_ca_instance_from_tal_and_ta_der(&tal_bytes, &ta_der, None).expect("discover");

    let root = root_handle_from_trust_anchor(
        &discovery.trust_anchor,
        "test-tal".to_string(),
        None,
        &discovery.ca_instance,
    );

    assert_eq!(root.depth, 0);
    assert_eq!(
        root.manifest_rsync_uri,
        discovery.ca_instance.manifest_rsync_uri
    );
    assert_eq!(root.rsync_base_uri, discovery.ca_instance.rsync_base_uri);
    match &root.ca_certificate {
        CaCertificateRef::InlineDer(bytes) => {
            assert!(bytes.len() > 100, "TA der should be non-empty");
        }
        other => panic!("TA root certificate should be inline DER, got {other:?}"),
    }
}

#[test]
fn run_tree_from_tal_url_entry_executes_and_records_failure_when_repo_empty() {
    let tal_url = "mock:apnic.tal";
    let tal_bytes = std::fs::read("tests/fixtures/tal/apnic-rfc7730-https.tal")
        .expect("read apnic tal fixture");
    let ta_der = std::fs::read("tests/fixtures/ta/apnic-ta.cer").expect("read apnic ta fixture");

    let mut map = HashMap::new();
    map.insert(tal_url.to_string(), tal_bytes);
    map.insert(
        "https://rpki.apnic.net/repository/apnic-rpki-root-iana-origin.cer".to_string(),
        ta_der.clone(),
    );
    map.insert(
        "rsync://rpki.apnic.net/repository/apnic-rpki-root-iana-origin.cer".to_string(),
        ta_der.clone(),
    );
    let http = MapHttpFetcher { map };

    let rsync = EmptyRsyncFetcher;
    let temp = tempfile::tempdir().expect("tempdir");
    let store = rpki::storage::RocksStore::open(temp.path()).expect("open rocksdb");
    let policy = rpki::policy::Policy {
        sync_preference: rpki::policy::SyncPreference::RsyncOnly,
        ..rpki::policy::Policy::default()
    };

    let out = run_tree_from_tal_url_serial(
        &store,
        &policy,
        tal_url,
        &http,
        &rsync,
        time::OffsetDateTime::now_utc(),
        &TreeRunConfig {
            max_depth: Some(0),
            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");
    assert_eq!(out.tree.instances_processed, 1);
    assert_eq!(out.tree.instances_failed, 0);
    assert!(
        out.tree
            .warnings
            .iter()
            .any(|w| w.message.contains("manifest failed fetch")
                || w.message.contains("no latest validated result")),
        "expected failed-fetch warning"
    );
}

#[test]
fn run_tree_from_tal_and_ta_der_entry_executes_and_records_failure_when_repo_empty() {
    let tal_bytes = std::fs::read("tests/fixtures/tal/apnic-rfc7730-https.tal")
        .expect("read apnic tal fixture");
    let ta_der = std::fs::read("tests/fixtures/ta/apnic-ta.cer").expect("read apnic ta fixture");

    let http = MapHttpFetcher {
        map: HashMap::new(),
    };
    let rsync = EmptyRsyncFetcher;
    let temp = tempfile::tempdir().expect("tempdir");
    let store = rpki::storage::RocksStore::open(temp.path()).expect("open rocksdb");
    let policy = rpki::policy::Policy {
        sync_preference: rpki::policy::SyncPreference::RsyncOnly,
        ..rpki::policy::Policy::default()
    };

    let out = run_tree_from_tal_and_ta_der_serial(
        &store,
        &policy,
        &tal_bytes,
        &ta_der,
        None,
        &http,
        &rsync,
        time::OffsetDateTime::now_utc(),
        &TreeRunConfig {
            max_depth: Some(0),
            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");
    assert_eq!(out.tree.instances_processed, 1);
    assert_eq!(out.tree.instances_failed, 0);
    assert!(
        out.tree
            .warnings
            .iter()
            .any(|w| w.message.contains("manifest failed fetch")
                || w.message.contains("no latest validated result")),
        "expected failed-fetch warning"
    );
}

#[test]
fn run_tree_from_tal_url_audit_entry_collects_no_publication_points_when_repo_empty() {
    let tal_url = "mock:apnic.tal";
    let tal_bytes = std::fs::read("tests/fixtures/tal/apnic-rfc7730-https.tal")
        .expect("read apnic tal fixture");
    let ta_der = std::fs::read("tests/fixtures/ta/apnic-ta.cer").expect("read apnic ta fixture");

    let mut map = HashMap::new();
    map.insert(tal_url.to_string(), tal_bytes);
    map.insert(
        "https://rpki.apnic.net/repository/apnic-rpki-root-iana-origin.cer".to_string(),
        ta_der.clone(),
    );
    map.insert(
        "rsync://rpki.apnic.net/repository/apnic-rpki-root-iana-origin.cer".to_string(),
        ta_der.clone(),
    );
    let http = MapHttpFetcher { map };
    let rsync = EmptyRsyncFetcher;

    let temp = tempfile::tempdir().expect("tempdir");
    let store = rpki::storage::RocksStore::open(temp.path()).expect("open rocksdb");
    let policy = rpki::policy::Policy {
        sync_preference: rpki::policy::SyncPreference::RsyncOnly,
        ..rpki::policy::Policy::default()
    };

    let out = run_tree_from_tal_url_serial_audit(
        &store,
        &policy,
        tal_url,
        &http,
        &rsync,
        time::OffsetDateTime::now_utc(),
        &TreeRunConfig {
            max_depth: Some(0),
            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 audit");

    assert_eq!(out.tree.instances_processed, 1);
    assert_eq!(out.tree.instances_failed, 0);
    assert_eq!(out.publication_points.len(), 1);
    assert_eq!(out.publication_points[0].source, "failed_fetch_no_cache");
}

#[test]
fn run_tree_from_tal_and_ta_der_audit_entry_collects_no_publication_points_when_repo_empty() {
    let tal_bytes = std::fs::read("tests/fixtures/tal/apnic-rfc7730-https.tal")
        .expect("read apnic tal fixture");
    let ta_der = std::fs::read("tests/fixtures/ta/apnic-ta.cer").expect("read apnic ta fixture");

    let http = MapHttpFetcher {
        map: HashMap::new(),
    };
    let rsync = EmptyRsyncFetcher;

    let temp = tempfile::tempdir().expect("tempdir");
    let store = rpki::storage::RocksStore::open(temp.path()).expect("open rocksdb");
    let policy = rpki::policy::Policy {
        sync_preference: rpki::policy::SyncPreference::RsyncOnly,
        ..rpki::policy::Policy::default()
    };

    let out = run_tree_from_tal_and_ta_der_serial_audit(
        &store,
        &policy,
        &tal_bytes,
        &ta_der,
        None,
        &http,
        &rsync,
        time::OffsetDateTime::now_utc(),
        &TreeRunConfig {
            max_depth: Some(0),
            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 audit");

    assert_eq!(out.tree.instances_processed, 1);
    assert_eq!(out.tree.instances_failed, 0);
    assert_eq!(out.publication_points.len(), 1);
    assert_eq!(out.publication_points[0].source, "failed_fetch_no_cache");
}

#[test]
fn run_tree_from_tal_url_audit_with_timing_records_phases_when_repo_empty() {
    let tal_url = "mock:apnic.tal";
    let tal_bytes = std::fs::read("tests/fixtures/tal/apnic-rfc7730-https.tal")
        .expect("read apnic tal fixture");
    let ta_der = std::fs::read("tests/fixtures/ta/apnic-ta.cer").expect("read apnic ta fixture");

    let mut map = HashMap::new();
    map.insert(tal_url.to_string(), tal_bytes);
    map.insert(
        "https://rpki.apnic.net/repository/apnic-rpki-root-iana-origin.cer".to_string(),
        ta_der.clone(),
    );
    map.insert(
        "rsync://rpki.apnic.net/repository/apnic-rpki-root-iana-origin.cer".to_string(),
        ta_der.clone(),
    );
    let http = MapHttpFetcher { map };
    let rsync = EmptyRsyncFetcher;

    let temp = tempfile::tempdir().expect("tempdir");
    let store = rpki::storage::RocksStore::open(temp.path()).expect("open rocksdb");
    let policy = rpki::policy::Policy {
        sync_preference: rpki::policy::SyncPreference::RsyncOnly,
        ..rpki::policy::Policy::default()
    };
    let timing = test_timing_handle(&temp);

    let out = run_tree_from_tal_url_serial_audit_with_timing(
        &store,
        &policy,
        tal_url,
        &http,
        &rsync,
        time::OffsetDateTime::now_utc(),
        &TreeRunConfig {
            max_depth: Some(0),
            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,
        },
        &timing,
    )
    .expect("run tree audit with timing");

    assert_eq!(out.tree.instances_processed, 1);
    assert_eq!(out.tree.instances_failed, 0);

    let report = read_timing_json(&temp, &timing);
    assert_eq!(report["phases"]["tal_bootstrap"]["count"].as_u64(), Some(1));
    assert_eq!(
        report["phases"]["tree_run_total"]["count"].as_u64(),
        Some(1)
    );
}

#[test]
fn run_tree_from_tal_and_ta_der_audit_with_timing_records_phases_when_repo_empty() {
    let tal_bytes = std::fs::read("tests/fixtures/tal/apnic-rfc7730-https.tal")
        .expect("read apnic tal fixture");
    let ta_der = std::fs::read("tests/fixtures/ta/apnic-ta.cer").expect("read apnic ta fixture");

    let http = MapHttpFetcher {
        map: HashMap::new(),
    };
    let rsync = EmptyRsyncFetcher;

    let temp = tempfile::tempdir().expect("tempdir");
    let store = rpki::storage::RocksStore::open(temp.path()).expect("open rocksdb");
    let policy = rpki::policy::Policy {
        sync_preference: rpki::policy::SyncPreference::RsyncOnly,
        ..rpki::policy::Policy::default()
    };
    let timing = test_timing_handle(&temp);

    let out = run_tree_from_tal_and_ta_der_serial_audit_with_timing(
        &store,
        &policy,
        &tal_bytes,
        &ta_der,
        None,
        &http,
        &rsync,
        time::OffsetDateTime::now_utc(),
        &TreeRunConfig {
            max_depth: Some(0),
            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,
        },
        &timing,
    )
    .expect("run tree audit with timing");

    assert_eq!(out.tree.instances_processed, 1);
    assert_eq!(out.tree.instances_failed, 0);

    let report = read_timing_json(&temp, &timing);
    assert_eq!(report["phases"]["tal_bootstrap"]["count"].as_u64(), Some(1));
    assert_eq!(
        report["phases"]["tree_run_total"]["count"].as_u64(),
        Some(1)
    );
}
