use std::collections::HashMap;

use rpki::data_model::tal::Tal;
use rpki::fetch::rsync::{RsyncFetchError, RsyncFetcher};
use rpki::policy::{Policy, SyncPreference};
use rpki::storage::RocksStore;
use rpki::sync::rrdp::Fetcher;
use rpki::validation::from_tal::{
    FromTalError, discover_root_ca_instance_from_tal,
    discover_root_ca_instance_from_tal_and_ta_der, discover_root_ca_instance_from_tal_url,
    run_root_from_tal_url_once,
};
use rpki::validation::manifest::PublicationPointSource;
use url::Url;

struct MapFetcher {
    by_uri: HashMap<String, Vec<u8>>,
}

impl MapFetcher {
    fn new(by_uri: HashMap<String, Vec<u8>>) -> Self {
        Self { by_uri }
    }
}

impl Fetcher for MapFetcher {
    fn fetch(&self, uri: &str) -> Result<Vec<u8>, String> {
        self.by_uri
            .get(uri)
            .cloned()
            .ok_or_else(|| format!("missing mapping for {uri}"))
    }
}

struct EmptyRsync;

impl RsyncFetcher for EmptyRsync {
    fn fetch_objects(
        &self,
        _rsync_base_uri: &str,
    ) -> Result<Vec<(String, Vec<u8>)>, RsyncFetchError> {
        Ok(Vec::new())
    }
}

fn openssl_available() -> bool {
    std::process::Command::new("openssl")
        .arg("version")
        .output()
        .map(|o| o.status.success())
        .unwrap_or(false)
}

fn run(cmd: &mut std::process::Command) {
    let out = cmd.output().expect("run command");
    if !out.status.success() {
        panic!(
            "command failed: {:?}\nstdout={}\nstderr={}",
            cmd,
            String::from_utf8_lossy(&out.stdout),
            String::from_utf8_lossy(&out.stderr)
        );
    }
}

fn apnic_tal_bytes() -> Vec<u8> {
    std::fs::read("tests/fixtures/tal/apnic-rfc7730-https.tal").expect("read apnic TAL fixture")
}

fn apnic_ta_der() -> Vec<u8> {
    std::fs::read("tests/fixtures/ta/apnic-ta.cer").expect("read apnic TA fixture")
}

#[test]
fn offline_discovery_from_apnic_tal_and_ta_der_fixture_works() {
    let tal_bytes = apnic_tal_bytes();
    let ta_der = apnic_ta_der();

    let d = discover_root_ca_instance_from_tal_and_ta_der(&tal_bytes, &ta_der, None)
        .expect("discover root from fixtures");

    assert!(d.ca_instance.rsync_base_uri.starts_with("rsync://"));
    assert!(d.ca_instance.rsync_base_uri.ends_with('/'));
    assert!(d.ca_instance.manifest_rsync_uri.starts_with("rsync://"));
    assert!(d.ca_instance.manifest_rsync_uri.ends_with(".mft"));
    if let Some(n) = &d.ca_instance.rrdp_notification_uri {
        assert!(n.starts_with("https://"));
    }
}

#[test]
fn offline_discovery_rejects_tampered_ta_self_signature() {
    let tal_bytes = apnic_tal_bytes();
    let mut ta_der = apnic_ta_der();
    let last = ta_der.last_mut().expect("TA fixture must not be empty");
    *last ^= 0x01;

    let err = discover_root_ca_instance_from_tal_and_ta_der(&tal_bytes, &ta_der, None)
        .expect_err("tampered TA signature must not form a root CA instance");
    assert!(err.to_string().contains("self-signature"), "{err}");
}

#[test]
fn discover_root_from_tal_url_works_with_mock_fetcher() {
    let tal_bytes = apnic_tal_bytes();
    let tal = Tal::decode_bytes(&tal_bytes).expect("decode TAL");
    let ta_uri = tal.ta_uris[0].as_str().to_string();

    let mut map = HashMap::new();
    map.insert("https://example.test/apnic.tal".to_string(), tal_bytes);
    map.insert(ta_uri, apnic_ta_der());

    let fetcher = MapFetcher::new(map);

    let d = discover_root_ca_instance_from_tal_url(&fetcher, "https://example.test/apnic.tal")
        .expect("discover");
    assert!(d.ca_instance.rsync_base_uri.starts_with("rsync://"));
}

#[test]
fn discover_root_tries_multiple_ta_uris_until_one_succeeds() {
    let tal_bytes = apnic_tal_bytes();
    let mut tal = Tal::decode_bytes(&tal_bytes).expect("decode TAL");
    let good_uri = tal.ta_uris[0].clone();
    tal.ta_uris
        .insert(0, Url::parse("https://example.invalid/bad.cer").unwrap());

    let mut map = HashMap::new();
    map.insert(good_uri.as_str().to_string(), apnic_ta_der());
    let fetcher = MapFetcher::new(map);

    let d = discover_root_ca_instance_from_tal(&fetcher, tal, None).expect("discover");
    assert_eq!(d.trust_anchor.resolved_ta_uri.as_ref(), Some(&good_uri));
}

#[test]
fn discover_root_errors_when_no_ta_uris_present() {
    let tal_bytes = apnic_tal_bytes();
    let mut tal = Tal::decode_bytes(&tal_bytes).expect("decode TAL");
    tal.ta_uris.clear();

    let fetcher = MapFetcher::new(HashMap::new());
    let err = discover_root_ca_instance_from_tal(&fetcher, tal, None).unwrap_err();
    assert!(matches!(err, FromTalError::NoTaUris));
}

#[test]
fn discover_root_errors_when_all_ta_fetches_fail() {
    let tal_bytes = apnic_tal_bytes();
    let tal = Tal::decode_bytes(&tal_bytes).expect("decode TAL");
    let fetcher = MapFetcher::new(HashMap::new());
    let err = discover_root_ca_instance_from_tal(&fetcher, tal, None).unwrap_err();
    assert!(matches!(err, FromTalError::TaFetch(_)));
}

#[test]
fn discover_root_errors_when_ta_does_not_bind_to_tal_spki() {
    let tal_bytes = apnic_tal_bytes();
    let tal = Tal::decode_bytes(&tal_bytes).expect("decode TAL");

    // Use a different TA cert fixture to trigger SPKI mismatch.
    let wrong_ta = std::fs::read("tests/fixtures/ta/arin-ta.cer").expect("read arin ta");

    let mut map = HashMap::new();
    map.insert(tal.ta_uris[0].as_str().to_string(), wrong_ta);
    let fetcher = MapFetcher::new(map);

    let err = discover_root_ca_instance_from_tal(&fetcher, tal, None).unwrap_err();
    assert!(matches!(err, FromTalError::TaFetch(_)));
}

#[test]
fn discover_root_from_tal_url_errors_when_tal_fetch_fails() {
    let fetcher = MapFetcher::new(HashMap::new());
    let err = discover_root_ca_instance_from_tal_url(&fetcher, "https://example.test/missing.tal")
        .unwrap_err();
    assert!(matches!(err, FromTalError::TalFetch(_)));
}

#[test]
fn run_root_from_tal_url_once_returns_failed_fetch_no_cache_when_repo_is_empty() {
    let tal_bytes = apnic_tal_bytes();
    let tal = Tal::decode_bytes(&tal_bytes).expect("decode TAL");
    let ta_uri = tal.ta_uris[0].as_str().to_string();

    let mut map = HashMap::new();
    map.insert("https://example.test/apnic.tal".to_string(), tal_bytes);
    map.insert(ta_uri, apnic_ta_der());
    let fetcher = MapFetcher::new(map);

    let temp = tempfile::tempdir().expect("tempdir");
    let store = RocksStore::open(temp.path()).expect("open rocksdb");

    let mut policy = Policy::default();
    policy.sync_preference = SyncPreference::RsyncOnly;

    let out = run_root_from_tal_url_once(
        &store,
        &policy,
        "https://example.test/apnic.tal",
        &fetcher,
        &EmptyRsync,
        time::OffsetDateTime::now_utc(),
    )
    .expect("run should return failed-fetch-no-cache output");

    assert_eq!(
        out.run.publication_point_source,
        PublicationPointSource::FailedFetchNoCache
    );
    assert!(out.run.objects.vrps.is_empty());
    assert!(out.run.objects.aspas.is_empty());
    assert!(
        out.run
            .publication_point_warnings
            .iter()
            .any(|warning| warning.message.contains("no latest validated result"))
    );
}

#[test]
fn discover_root_records_ca_instance_discovery_failure_when_ta_lacks_sia() {
    use base64::Engine;

    assert!(openssl_available(), "openssl is required for this test");

    let temp = tempfile::tempdir().expect("tempdir");
    let dir = temp.path();
    std::fs::write(
        dir.join("openssl.cnf"),
        r#"
[ req ]
prompt = no
distinguished_name = dn
x509_extensions = v3_ta

[ dn ]
CN = Test TA Without SIA

[ v3_ta ]
basicConstraints = critical,CA:true
keyUsage = critical, keyCertSign, cRLSign
subjectKeyIdentifier = hash
certificatePolicies = critical, 1.3.6.1.5.5.7.14.2
sbgp-ipAddrBlock = critical, IPv4:10.0.0.0/8
sbgp-autonomousSysNum = critical, AS:64496-64511
"#,
    )
    .expect("write openssl cnf");

    run(std::process::Command::new("openssl")
        .arg("genrsa")
        .arg("-out")
        .arg(dir.join("ta.key"))
        .arg("2048"));
    run(std::process::Command::new("openssl")
        .arg("req")
        .arg("-new")
        .arg("-x509")
        .arg("-sha256")
        .arg("-days")
        .arg("365")
        .arg("-key")
        .arg(dir.join("ta.key"))
        .arg("-config")
        .arg(dir.join("openssl.cnf"))
        .arg("-extensions")
        .arg("v3_ta")
        .arg("-out")
        .arg(dir.join("ta.pem")));
    run(std::process::Command::new("openssl")
        .arg("x509")
        .arg("-in")
        .arg(dir.join("ta.pem"))
        .arg("-outform")
        .arg("DER")
        .arg("-out")
        .arg(dir.join("ta.cer")));
    run(std::process::Command::new("sh").arg("-c").arg(format!(
        "openssl x509 -in {} -pubkey -noout | openssl pkey -pubin -outform DER > {}",
        dir.join("ta.pem").display(),
        dir.join("spki.der").display(),
    )));

    let ta_uri = "https://example.test/no-sia-ta.cer";
    let tal_text = format!(
        "{ta_uri}\n\n{}\n",
        base64::engine::general_purpose::STANDARD
            .encode(std::fs::read(dir.join("spki.der")).expect("read spki der"))
    );
    let tal = Tal::decode_bytes(tal_text.as_bytes()).expect("decode generated tal");

    let mut map = HashMap::new();
    map.insert(
        ta_uri.to_string(),
        std::fs::read(dir.join("ta.cer")).expect("read ta der"),
    );
    let fetcher = MapFetcher::new(map);

    let err = discover_root_ca_instance_from_tal(&fetcher, tal, None).unwrap_err();
    assert!(matches!(err, FromTalError::TaFetch(_)), "{err}");
    assert!(
        err.to_string().contains("CA instance discovery failed"),
        "{err}"
    );
}
