use std::process::Command;

use rpki::data_model::rc::{ResourceCertKind, ResourceCertificate};
use rpki::data_model::router_cert::{
    BgpsecRouterCertificate, BgpsecRouterCertificateDecodeError, BgpsecRouterCertificatePathError,
    BgpsecRouterCertificateProfileError,
};

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

fn skip_heavy_crypto_test() -> bool {
    std::env::var_os("RPKI_SKIP_HEAVY_CRYPTO_TESTS").is_some()
}

fn run(cmd: &mut 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)
        );
    }
}

struct Generated {
    issuer_ca_der: Vec<u8>,
    router_der: Vec<u8>,
    issuer_crl_der: Vec<u8>,
    wrong_issuer_der: Vec<u8>,
}

fn generate_router_cert_with_variant(
    key_spec: &str,
    include_eku: bool,
    extra_ext: &str,
) -> Generated {
    assert!(openssl_available(), "openssl is required for this test");
    let td = tempfile::tempdir().expect("tempdir");
    let dir = td.path();

    std::fs::create_dir_all(dir.join("newcerts")).expect("newcerts");
    std::fs::write(dir.join("index.txt"), b"").expect("index");
    std::fs::write(dir.join("serial"), b"1000\n").expect("serial");
    std::fs::write(dir.join("crlnumber"), b"1000\n").expect("crlnumber");

    let cnf = format!(
        r#"
[ ca ]
default_ca = CA_default

[ CA_default ]
dir = {dir}
database = $dir/index.txt
new_certs_dir = $dir/newcerts
certificate = $dir/issuer.pem
private_key = $dir/issuer.key
serial = $dir/serial
crlnumber = $dir/crlnumber
default_md = sha256
default_days = 365
default_crl_days = 1
policy = policy_any
x509_extensions = v3_issuer_ca
crl_extensions = crl_ext
unique_subject = no
copy_extensions = none

[ policy_any ]
commonName = supplied

[ req ]
prompt = no
distinguished_name = dn

[ dn ]
CN = Test Issuer CA

[ v3_issuer_ca ]
basicConstraints = critical,CA:true
keyUsage = critical, keyCertSign, cRLSign
subjectKeyIdentifier = hash
authorityKeyIdentifier = keyid:always
certificatePolicies = critical, 1.3.6.1.5.5.7.14.2
subjectInfoAccess = caRepository;URI:rsync://example.test/repo/issuer/, rpkiManifest;URI:rsync://example.test/repo/issuer/issuer.mft, rpkiNotify;URI:https://example.test/notification.xml
sbgp-ipAddrBlock = critical, IPv4:10.0.0.0/8
sbgp-autonomousSysNum = critical, AS:64496-64511

[ v3_router ]
keyUsage = critical, digitalSignature
{eku_line}
authorityKeyIdentifier = keyid:always
crlDistributionPoints = URI:rsync://example.test/repo/issuer/issuer.crl
authorityInfoAccess = caIssuers;URI:rsync://example.test/repo/issuer/issuer.cer
certificatePolicies = critical, 1.3.6.1.5.5.7.14.2
sbgp-autonomousSysNum = critical, AS:64496
{eku_line}
{extra_ext}

[ crl_ext ]
authorityKeyIdentifier = keyid:always
"#,
        dir = dir.display(),
        eku_line = if include_eku {
            "extendedKeyUsage = 1.3.6.1.5.5.7.3.30"
        } else {
            ""
        },
        extra_ext = extra_ext
    );
    std::fs::write(dir.join("openssl.cnf"), cnf.as_bytes()).expect("write cnf");

    run(Command::new("openssl")
        .arg("genrsa")
        .arg("-out")
        .arg(dir.join("issuer.key"))
        .arg("2048"));
    run(Command::new("openssl")
        .arg("req")
        .arg("-new")
        .arg("-x509")
        .arg("-sha256")
        .arg("-days")
        .arg("365")
        .arg("-key")
        .arg(dir.join("issuer.key"))
        .arg("-config")
        .arg(dir.join("openssl.cnf"))
        .arg("-extensions")
        .arg("v3_issuer_ca")
        .arg("-out")
        .arg(dir.join("issuer.pem")));

    match key_spec {
        "ec-p256" => {
            run(Command::new("openssl")
                .arg("ecparam")
                .arg("-name")
                .arg("prime256v1")
                .arg("-genkey")
                .arg("-noout")
                .arg("-out")
                .arg(dir.join("router.key")));
        }
        "ec-p384" => {
            run(Command::new("openssl")
                .arg("ecparam")
                .arg("-name")
                .arg("secp384r1")
                .arg("-genkey")
                .arg("-noout")
                .arg("-out")
                .arg(dir.join("router.key")));
        }
        "rsa" => {
            run(Command::new("openssl")
                .arg("genrsa")
                .arg("-out")
                .arg(dir.join("router.key"))
                .arg("2048"));
        }
        other => panic!("unsupported key_spec {other}"),
    }

    run(Command::new("openssl")
        .arg("req")
        .arg("-new")
        .arg("-key")
        .arg(dir.join("router.key"))
        .arg("-subj")
        .arg("/CN=ROUTER-0000FC10/serialNumber=01020304")
        .arg("-out")
        .arg(dir.join("router.csr")));

    run(Command::new("openssl")
        .arg("ca")
        .arg("-batch")
        .arg("-config")
        .arg(dir.join("openssl.cnf"))
        .arg("-in")
        .arg(dir.join("router.csr"))
        .arg("-extensions")
        .arg("v3_router")
        .arg("-out")
        .arg(dir.join("router.pem")));

    run(Command::new("openssl")
        .arg("x509")
        .arg("-in")
        .arg(dir.join("issuer.pem"))
        .arg("-outform")
        .arg("DER")
        .arg("-out")
        .arg(dir.join("issuer.cer")));
    run(Command::new("openssl")
        .arg("x509")
        .arg("-in")
        .arg(dir.join("router.pem"))
        .arg("-outform")
        .arg("DER")
        .arg("-out")
        .arg(dir.join("router.cer")));

    run(Command::new("openssl")
        .arg("ca")
        .arg("-gencrl")
        .arg("-config")
        .arg(dir.join("openssl.cnf"))
        .arg("-out")
        .arg(dir.join("issuer.crl.pem")));
    run(Command::new("openssl")
        .arg("crl")
        .arg("-in")
        .arg(dir.join("issuer.crl.pem"))
        .arg("-outform")
        .arg("DER")
        .arg("-out")
        .arg(dir.join("issuer.crl")));

    // Wrong issuer for path failure.
    run(Command::new("openssl")
        .arg("genrsa")
        .arg("-out")
        .arg(dir.join("other.key"))
        .arg("2048"));
    run(Command::new("openssl")
        .arg("req")
        .arg("-new")
        .arg("-x509")
        .arg("-sha256")
        .arg("-days")
        .arg("365")
        .arg("-key")
        .arg(dir.join("other.key"))
        .arg("-subj")
        .arg("/CN=Other Issuer")
        .arg("-out")
        .arg(dir.join("other.pem")));
    run(Command::new("openssl")
        .arg("x509")
        .arg("-in")
        .arg(dir.join("other.pem"))
        .arg("-outform")
        .arg("DER")
        .arg("-out")
        .arg(dir.join("other.cer")));

    Generated {
        issuer_ca_der: std::fs::read(dir.join("issuer.cer")).expect("read issuer der"),
        router_der: std::fs::read(dir.join("router.cer")).expect("read router der"),
        issuer_crl_der: std::fs::read(dir.join("issuer.crl")).expect("read crl der"),
        wrong_issuer_der: std::fs::read(dir.join("other.cer")).expect("read other issuer der"),
    }
}

#[test]
fn decode_bgpsec_router_certificate_fixture_smoke() {
    if skip_heavy_crypto_test() {
        return;
    }
    let g = generate_router_cert_with_variant("ec-p256", true, "");
    let cert = BgpsecRouterCertificate::decode_der(&g.router_der).expect("decode router cert");
    assert_eq!(cert.resource_cert.kind, ResourceCertKind::Ee);
    assert_eq!(cert.asns, vec![64496]);
    assert_eq!(cert.subject_key_identifier.len(), 20);
    assert_eq!(cert.spki_der[0], 0x30);
}

#[test]
fn router_certificate_profile_rejects_missing_eku() {
    if skip_heavy_crypto_test() {
        return;
    }
    let g = generate_router_cert_with_variant("ec-p256", false, "");
    let err = BgpsecRouterCertificate::decode_der(&g.router_der).unwrap_err();
    assert!(
        matches!(
            err,
            BgpsecRouterCertificateDecodeError::Validate(
                BgpsecRouterCertificateProfileError::MissingBgpsecRouterEku
                    | BgpsecRouterCertificateProfileError::MissingExtendedKeyUsage
            )
        ),
        "{err}"
    );
}

#[test]
fn router_certificate_profile_rejects_sia_and_ip_resources_and_ranges() {
    if skip_heavy_crypto_test() {
        return;
    }
    let g = generate_router_cert_with_variant(
        "ec-p256",
        true,
        "subjectInfoAccess = caRepository;URI:rsync://example.test/repo/router/\n",
    );
    let err = BgpsecRouterCertificate::decode_der(&g.router_der).unwrap_err();
    assert!(
        matches!(
            err,
            BgpsecRouterCertificateDecodeError::Validate(
                BgpsecRouterCertificateProfileError::SubjectInfoAccessPresent
            )
        ),
        "{err}"
    );

    let g = generate_router_cert_with_variant(
        "ec-p256",
        true,
        "sbgp-ipAddrBlock = critical, IPv4:10.0.0.0/8\n",
    );
    let err = BgpsecRouterCertificate::decode_der(&g.router_der).unwrap_err();
    assert!(
        matches!(
            err,
            BgpsecRouterCertificateDecodeError::Validate(
                BgpsecRouterCertificateProfileError::IpResourcesPresent
            )
        ),
        "{err}"
    );

    let g = generate_router_cert_with_variant(
        "ec-p256",
        true,
        "sbgp-autonomousSysNum = critical, AS:64496-64500\n",
    );
    let err = BgpsecRouterCertificate::decode_der(&g.router_der).unwrap_err();
    assert!(
        matches!(
            err,
            BgpsecRouterCertificateDecodeError::Validate(
                BgpsecRouterCertificateProfileError::AsResourcesRangeNotAllowed
            )
        ),
        "{err}"
    );
}

#[test]
fn router_certificate_profile_rejects_wrong_spki_algorithm_or_curve() {
    if skip_heavy_crypto_test() {
        return;
    }
    let g = generate_router_cert_with_variant("rsa", true, "");
    let err = BgpsecRouterCertificate::decode_der(&g.router_der).unwrap_err();
    assert!(
        matches!(
            err,
            BgpsecRouterCertificateDecodeError::Validate(
                BgpsecRouterCertificateProfileError::SpkiAlgorithmNotEcPublicKey
            )
        ),
        "{err}"
    );

    let g = generate_router_cert_with_variant("ec-p384", true, "");
    let err = BgpsecRouterCertificate::decode_der(&g.router_der).unwrap_err();
    assert!(
        matches!(
            err,
            BgpsecRouterCertificateDecodeError::Validate(
                BgpsecRouterCertificateProfileError::SpkiWrongCurve
                    | BgpsecRouterCertificateProfileError::SpkiEcPointNotUncompressedP256
            )
        ),
        "{err}"
    );
}

#[test]
fn router_certificate_path_validation_accepts_valid_and_rejects_wrong_issuer() {
    if skip_heavy_crypto_test() {
        return;
    }
    use rpki::data_model::common::BigUnsigned;
    use rpki::data_model::crl::RpkixCrl;
    use std::collections::HashSet;
    use x509_parser::prelude::FromDer;
    use x509_parser::x509::SubjectPublicKeyInfo;

    let g = generate_router_cert_with_variant("ec-p256", true, "");
    let issuer = ResourceCertificate::decode_der(&g.issuer_ca_der).expect("decode issuer");
    let wrong_issuer =
        ResourceCertificate::decode_der(&g.wrong_issuer_der).expect("decode wrong issuer");
    let issuer_crl = RpkixCrl::decode_der(&g.issuer_crl_der).expect("decode crl");
    let (rem, issuer_spki) =
        SubjectPublicKeyInfo::from_der(&issuer.tbs.subject_public_key_info).expect("issuer spki");
    assert!(rem.is_empty());
    let (rem, wrong_spki) =
        SubjectPublicKeyInfo::from_der(&wrong_issuer.tbs.subject_public_key_info)
            .expect("wrong issuer spki");
    assert!(rem.is_empty());
    let now = time::OffsetDateTime::now_utc();

    let cert = BgpsecRouterCertificate::validate_path_with_prevalidated_issuer(
        &g.router_der,
        &issuer,
        &issuer_spki,
        &issuer_crl,
        &HashSet::new(),
        Some("rsync://example.test/repo/issuer/issuer.cer"),
        Some("rsync://example.test/repo/issuer/issuer.crl"),
        now,
    )
    .expect("router path valid");
    assert_eq!(cert.asns, vec![64496]);

    let err = BgpsecRouterCertificate::validate_path_with_prevalidated_issuer(
        &g.router_der,
        &wrong_issuer,
        &wrong_spki,
        &issuer_crl,
        &HashSet::new(),
        None,
        None,
        now,
    )
    .unwrap_err();
    assert!(
        matches!(err, BgpsecRouterCertificatePathError::CertPath(_)),
        "{err}"
    );

    let rc = ResourceCertificate::decode_der(&g.router_der).expect("decode router rc");
    let mut revoked = HashSet::new();
    revoked.insert(BigUnsigned::from_biguint(&rc.tbs.serial_number).bytes_be);
    let err = BgpsecRouterCertificate::validate_path_with_prevalidated_issuer(
        &g.router_der,
        &issuer,
        &issuer_spki,
        &issuer_crl,
        &revoked,
        Some("rsync://example.test/repo/issuer/issuer.cer"),
        Some("rsync://example.test/repo/issuer/issuer.crl"),
        now,
    )
    .unwrap_err();
    assert!(
        matches!(err, BgpsecRouterCertificatePathError::CertPath(_)),
        "{err}"
    );
}
