use std::ascii::escape_default;
use std::fmt::Write;
use std::fs;

use pki_types::CertificateDer;
use ring::digest;
use webpki::anchor_from_trusted_cert;
use webpki_ccadb::fetch_ccadb_roots;
use x509_parser::prelude::AttributeTypeAndValue;
use x509_parser::x509::X509Name;

#[tokio::test]
async fn new_generated_code_is_fresh() {
    let tls_roots_map = fetch_ccadb_roots().await;
    let mut code = String::with_capacity(256 * 1_024);
    code.push_str(HEADER);
    code.push_str("pub const TLS_SERVER_ROOTS: &[TrustAnchor] = &[\n");
    let (mut subject, mut spki, mut name_constraints) =
        (String::new(), String::new(), String::new());

    for (_, root) in tls_roots_map {
        // Verify the DER FP matches the metadata FP.
        let der = root.der();
        let calculated_fp = digest::digest(&digest::SHA256, &der);
        let metadata_fp = hex::decode(&root.sha256_fingerprint).expect("malformed fingerprint");
        assert_eq!(calculated_fp.as_ref(), metadata_fp.as_slice());

        let ta_der = CertificateDer::from(der.as_ref());
        let ta = anchor_from_trusted_cert(&ta_der).expect("malformed trust anchor der");
        subject.clear();
        for &b in ta.subject.as_ref() {
            write!(&mut subject, "{}", escape_default(b)).unwrap();
        }

        spki.clear();
        for &b in ta.subject_public_key_info.as_ref() {
            write!(&mut spki, "{}", escape_default(b)).unwrap();
        }

        name_constraints.clear();
        if let Some(nc) = &root.mozilla_applied_constraints() {
            for &b in nc.iter() {
                write!(&mut name_constraints, "{}", escape_default(b)).unwrap();
            }
        }

        let (_, parsed_cert) =
            x509_parser::parse_x509_certificate(&der).expect("malformed x509 der");
        let issuer = name_to_string(parsed_cert.issuer());
        let subject_str = name_to_string(parsed_cert.subject());
        let label = root.common_name_or_certificate_name.clone();
        let serial = root.serial().to_string();
        let sha256_fp = root.sha256_fp();

        // Write comment
        code.push_str("  /*\n");
        code.push_str(&format!("   * Issuer: {}\n", issuer));
        code.push_str(&format!("   * Subject: {}\n", subject_str));
        code.push_str(&format!("   * Label: {:?}\n", label));
        code.push_str(&format!("   * Serial: {}\n", serial));
        code.push_str(&format!("   * SHA256 Fingerprint: {}\n", sha256_fp));
        for ln in root.pem().lines() {
            code.push_str("   * ");
            code.push_str(ln.trim());
            code.push('\n');
        }
        code.push_str("   */\n");

        // Write the code
        code.push_str("  TrustAnchor {\n");
        code.write_fmt(format_args!(
            "    subject: Der::from_slice(b\"{subject}\"),\n"
        ))
        .unwrap();
        code.write_fmt(format_args!(
            "    subject_public_key_info: Der::from_slice(b\"{spki}\"),\n"
        ))
        .unwrap();
        match name_constraints.is_empty() {
            false => code
                .write_fmt(format_args!(
                    "    name_constraints: Some(Der::from_slice(b\"{name_constraints}\"))\n"
                ))
                .unwrap(),
            true => code.push_str("    name_constraints: None\n"),
        }
        code.push_str("  },\n\n");
    }
    code.push_str("];\n");

    // Check that the generated code matches the checked-in code
    let old = fs::read_to_string("src/lib.rs").unwrap();
    if old != code {
        fs::write("src/lib.rs", code).unwrap();
        panic!("generated code changed");
    }
}

/// The built-in x509_parser::X509Name Display impl uses a different sort order than
/// the one historically used by mkcert.org^[0]. We re-create that sort order here to
/// avoid unnecessary churn in the generated code.
///
/// [0]: <https://github.com/Lukasa/mkcert/blob/6911a8f68681f4d6a795c1f6db7b063f75b03b5a/certs/convert_mozilla_certdata.go#L405-L428>
fn name_to_string(name: &X509Name) -> String {
    let mut ret = String::with_capacity(256);

    if let Some(cn) = name
        .iter_common_name()
        .next()
        .and_then(|cn| cn.as_str().ok())
    {
        write!(ret, "CN={}", cn).unwrap();
    }

    let mut append_attrs = |attrs: Vec<&AttributeTypeAndValue>, label| {
        let str_parts = attrs
            .iter()
            .filter_map(|attr| match attr.as_str() {
                Ok(s) => Some(s),
                Err(_) => None,
            })
            .collect::<Vec<_>>()
            .join("/");
        if !str_parts.is_empty() {
            if !ret.is_empty() {
                ret.push(' ');
            }
            write!(ret, "{}={}", label, str_parts).unwrap();
        }
    };

    append_attrs(name.iter_organization().collect(), "O");
    append_attrs(name.iter_organizational_unit().collect(), "OU");

    ret
}

const HEADER: &str = r#"//! A compiled-in copy of the root certificates trusted by Mozilla.
//!
//! To use this library with rustls 0.22:
//!
//! ```rust
//! let root_store = rustls::RootCertStore {
//!   roots: webpki_roots::TLS_SERVER_ROOTS.iter().cloned().collect(),
//! };
//! ```
//!
//! This library is suitable for use in applications that can always be recompiled and instantly deployed.
//! For applications that are deployed to end-users and cannot be recompiled, or which need certification
//! before deployment, consider a library that uses the platform native certificate verifier such as
//! [rustls-platform-verifier]. This has the additional benefit of supporting OS provided CA constraints
//! and revocation data.
//!
//! [rustls-platform-verifier]: https://docs.rs/rustls-platform-verifier
//
// This library is automatically generated from the Mozilla
// IncludedCACertificateReportPEMCSV report via ccadb.org. Don't edit it.
//
// The generation is done deterministically so you can verify it
// yourself by inspecting and re-running the generation process.

#![no_std]
#![forbid(unsafe_code, unstable_features)]
#![deny(
    trivial_casts,
    trivial_numeric_casts,
    unused_import_braces,
    unused_extern_crates,
    unused_qualifications
)]

use pki_types::{Der, TrustAnchor};

"#;
