mod common;

use std::io::{ErrorKind, Read, Write};
use std::net::TcpStream;
#[cfg(unix)]
use std::os::unix::fs::symlink;
use std::path::{Path, PathBuf};
use std::sync::Arc;
use std::{env, panic};

// #[serial] is used on all these tests to run them sequentially. If they're run in parallel,
// the global env var configuration in the env var test interferes with the others.
use serial_test::serial;

/// Check if connection to site works using native roots.
///
/// Yields an Err if and only if there is an issue connecting that
/// appears to be due to a certificate problem.
///
/// # Panics
///
/// Panics on errors unrelated to the TLS connection like errors during
/// certificate loading, or connecting via TCP.
fn check_site(domain: &str) -> Result<(), ()> {
    check_site_with_roots(domain, rustls_native_certs::load_native_certs().unwrap())
}

/// Check if connection to site works using the given roots.
///
/// Yields an Err if and only if there is an issue connecting that
/// appears to be due to a certificate problem.
///
/// # Panics
///
/// Panics on errors unrelated to the TLS connection like connecting via TCP.
fn check_site_with_roots(
    domain: &str,
    root_certs: Vec<pki_types::CertificateDer<'static>>,
) -> Result<(), ()> {
    let mut roots = rustls::RootCertStore::empty();
    roots.add_parsable_certificates(root_certs);

    let config = rustls::ClientConfig::builder()
        .with_root_certificates(roots)
        .with_no_client_auth();

    let mut conn = rustls::ClientConnection::new(
        Arc::new(config),
        pki_types::ServerName::try_from(domain)
            .unwrap()
            .to_owned(),
    )
    .unwrap();
    let mut sock = TcpStream::connect(format!("{domain}:443")).unwrap();
    let mut tls = rustls::Stream::new(&mut conn, &mut sock);
    let result = tls.write_all(
        format!(
            "GET / HTTP/1.1\r\n\
                       Host: {domain}\r\n\
                       Connection: close\r\n\
                       Accept-Encoding: identity\r\n\
                       \r\n"
        )
        .as_bytes(),
    );
    match result {
        Ok(()) => (),
        Err(e) if e.kind() == ErrorKind::InvalidData => return Err(()), // TLS error
        Err(e) => panic!("{e}"),
    }
    let mut plaintext = [0u8; 1024];
    let len = tls.read(&mut plaintext).unwrap();
    assert!(plaintext[..len].starts_with(b"HTTP/1.1 ")); // or whatever
    Ok(())
}

#[test]
#[serial]
#[ignore]
fn google() {
    unsafe {
        // SAFETY: safe because of #[serial]
        common::clear_env();
    }
    check_site("google.com").unwrap();
}

#[test]
#[serial]
#[ignore]
fn amazon() {
    unsafe {
        // SAFETY: safe because of #[serial]
        common::clear_env();
    }
    check_site("amazon.com").unwrap();
}

#[test]
#[serial]
#[ignore]
fn facebook() {
    unsafe {
        // SAFETY: safe because of #[serial]
        common::clear_env();
    }
    check_site("facebook.com").unwrap();
}

#[test]
#[serial]
#[ignore]
fn netflix() {
    unsafe {
        // SAFETY: safe because of #[serial]
        common::clear_env();
    }
    check_site("netflix.com").unwrap();
}

#[test]
#[serial]
#[ignore]
fn ebay() {
    unsafe {
        // SAFETY: safe because of #[serial]
        common::clear_env();
    }
    check_site("ebay.com").unwrap();
}

#[test]
#[serial]
#[ignore]
fn apple() {
    unsafe {
        // SAFETY: safe because of #[serial]
        common::clear_env();
    }
    check_site("apple.com").unwrap();
}

#[test]
#[serial]
#[ignore]
fn badssl_with_env() {
    unsafe {
        // SAFETY: safe because of #[serial]
        common::clear_env();
    }

    // Self-signed certs should never be trusted by default:
    assert!(check_site("self-signed.badssl.com").is_err());

    // But they should be trusted if SSL_CERT_FILE is set:
    env::set_var(
        "SSL_CERT_FILE",
        // The CA cert, downloaded directly from the site itself:
        PathBuf::from("./tests/badssl-com-chain.pem"),
    );
    check_site("self-signed.badssl.com").unwrap();
}

#[test]
#[serial]
#[ignore]
fn badssl_with_dir_from_env() {
    unsafe {
        // SAFETY: safe because of #[serial]
        common::clear_env();
    }
    let temp_dir = tempfile::TempDir::new().unwrap();
    let original = Path::new("tests/badssl-com-chain.pem")
        .canonicalize()
        .unwrap();
    let link1 = temp_dir.path().join("5d30f3c5.3");
    #[cfg(unix)]
    let link2 = temp_dir.path().join("fd3003c5.0");

    env::set_var(
        "SSL_CERT_DIR",
        // The CA cert, downloaded directly from the site itself:
        temp_dir.path(),
    );
    assert!(check_site("self-signed.badssl.com").is_err());

    // OpenSSL uses symlinks too. So, use one for testing too, if possible.
    #[cfg(unix)]
    symlink(original, link1).unwrap();
    #[cfg(not(unix))]
    std::fs::copy(original, link1).unwrap();

    // Dangling symlink
    #[cfg(unix)]
    symlink("/a/path/which/does/not/exist/hopefully", link2).unwrap();

    check_site("self-signed.badssl.com").unwrap();
}

#[test]
#[serial]
#[ignore]
fn ssl_cert_dir_multiple_paths_are_respected() {
    unsafe {
        // SAFETY: safe because of #[serial]
        common::clear_env();
    }

    // Create 2 temporary directories
    let temp_dir1 = tempfile::TempDir::new().unwrap();
    let temp_dir2 = tempfile::TempDir::new().unwrap();

    // Copy the certificate to the 2nd dir, leaving the 1st one
    // empty.
    let original = Path::new("tests/badssl-com-chain.pem")
        .canonicalize()
        .unwrap();
    let cert = temp_dir2.path().join("5d30f3c5.3");
    std::fs::copy(original, cert).unwrap();

    let list_sep = if cfg!(windows) { ';' } else { ':' };
    let value = format!(
        "{}{}{}",
        temp_dir1.path().display(),
        list_sep,
        temp_dir2.path().display()
    );

    env::set_var("SSL_CERT_DIR", value);
    check_site("self-signed.badssl.com").unwrap();
}

#[test]
#[serial]
#[ignore]
fn ssl_cert_dir_non_hash_based_name() {
    unsafe {
        // SAFETY: safe because of #[serial]
        common::clear_env();
    }

    // Create temporary directory
    let temp_dir = tempfile::TempDir::new().unwrap();

    // Copy the certificate to the dir
    let original = Path::new("tests/badssl-com-chain.pem")
        .canonicalize()
        .unwrap();
    let cert = temp_dir.path().join("test.pem");
    std::fs::copy(original, cert).unwrap();

    env::set_var(
        "SSL_CERT_DIR",
        // The CA cert, downloaded directly from the site itself:
        temp_dir.path(),
    );

    check_site("self-signed.badssl.com").unwrap();
}

#[test]
#[serial]
#[ignore]
#[cfg(target_os = "linux")]
fn google_with_dir_but_broken_file() {
    unsafe {
        // SAFETY: safe because of #[serial]
        common::clear_env();
    }

    env::set_var("SSL_CERT_DIR", "/etc/ssl/certs");
    env::set_var("SSL_CERT_FILE", "not-exist");
    let res = rustls_native_certs::load_native_certs();

    let first_err = res.errors.first().unwrap().to_string();
    dbg!(&first_err);
    assert!(first_err.contains("from file"));
    assert!(first_err.contains("not-exist"));

    check_site_with_roots("google.com", res.certs).unwrap();
}

#[test]
#[serial]
#[ignore]
#[cfg(target_os = "linux")]
fn google_with_file_but_broken_dir() {
    unsafe {
        // SAFETY: safe because of #[serial]
        common::clear_env();
    }

    env::set_var("SSL_CERT_DIR", "/not-exist");
    env::set_var("SSL_CERT_FILE", "/etc/ssl/certs/ca-certificates.crt");
    let res = rustls_native_certs::load_native_certs();

    let first_err = res.errors.first().unwrap().to_string();
    dbg!(&first_err);
    assert!(first_err.contains("opening directory"));
    assert!(first_err.contains("/not-exist"));

    check_site_with_roots("google.com", res.certs).unwrap();
}

#[test]
#[serial]
#[ignore]
#[cfg(target_os = "linux")]
fn nothing_works_with_broken_file_and_dir() {
    unsafe {
        // SAFETY: safe because of #[serial]
        common::clear_env();
    }

    env::set_var("SSL_CERT_DIR", "/not-exist");
    env::set_var("SSL_CERT_FILE", "not-exist");
    let res = rustls_native_certs::load_native_certs();
    assert_eq!(res.errors.len(), 2);

    let first_err = res.errors.first().unwrap().to_string();
    dbg!(&first_err);
    assert!(first_err.contains("from file"));
    assert!(first_err.contains("not-exist"));

    let second_err = res.errors.get(1).unwrap().to_string();
    dbg!(&second_err);
    assert!(second_err.contains("opening directory"));
    assert!(second_err.contains("/not-exist"));
}
