use std::fs;
use std::io::BufReader;
use std::sync::Arc;
use std::sync::atomic::{AtomicU64, Ordering};
use std::time::{SystemTime, UNIX_EPOCH};

use arc_swap::ArcSwap;
use rustls::ServerConfig;
use rustls::pki_types::{CertificateDer, PrivateKeyDer};
use rustls::server::WebPkiClientVerifier;
use tokio::net::TcpStream;
use tokio_rustls::TlsAcceptor;
use tokio_rustls::server::TlsStream;
use x509_parser::prelude::*;

use crate::config::TlsConfig;
use crate::error::Error;

/// Extracted client certificate metadata for logging and audit.
pub struct ClientCertInfo {
    pub common_name: Option<String>,
    pub sans: Vec<String>,
    pub fingerprint_sha256: String,
    pub serial: String,
    pub not_after: u64,
}

/// TLS provider for both TCP and HTTP listeners.
///
/// Wraps a `TlsAcceptor` that can be hot-swapped via `arc_swap` for
/// certificate reloads without restarting the server.
impl std::fmt::Debug for TlsProvider {
    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
        f.debug_struct("TlsProvider")
            .field("client_auth", &self.client_auth)
            .field("config.cert_file", &self.config.cert_file)
            .field("config.key_file", &self.config.key_file)
            .finish()
    }
}

/// A `TlsProvider` with a listener label for metrics and admin API responses.
#[derive(Clone)]
pub struct LabeledTlsProvider {
    pub label: &'static str,
    pub provider: Arc<TlsProvider>,
}

/// Newtype for `Vec<LabeledTlsProvider>` so it can be injected as an axum Extension.
#[derive(Clone, Default)]
pub struct TlsProviders(pub Vec<LabeledTlsProvider>);

pub struct TlsProvider {
    acceptor: ArcSwap<TlsAcceptor>,
    server_config: ArcSwap<Arc<ServerConfig>>,
    client_auth: bool,
    config: TlsConfig,
    cert_mtime: AtomicU64,
    key_mtime: AtomicU64,
    /// Cached certificate expiry as seconds since Unix epoch.
    /// Updated at construction and on each successful reload.
    cert_expiry_epoch: AtomicU64,
    /// Cached SHA-256 fingerprint of the server leaf cert (colon-separated hex).
    /// Updated at construction and on each successful reload.
    cert_fingerprint: std::sync::RwLock<String>,
}

impl TlsProvider {
    /// Build from config. Returns `Ok(None)` if TLS is disabled.
    ///
    /// Validates cert/key readability and builds a `rustls::ServerConfig`
    /// at construction time. Fails fast with a descriptive error if files
    /// are missing, unreadable, or the cert is expired.
    pub fn from_config(config: &TlsConfig) -> Result<Option<Self>, Error> {
        if !config.enabled {
            return Ok(None);
        }

        let (certs, key) = load_cert_and_key(&config.cert_file, &config.key_file)?;

        // Parse certificate expiry and validate
        let expiry_epoch = cert_expiry_epoch(&certs)?;
        let now_secs = now_unix_secs();

        if expiry_epoch <= now_secs {
            return Err(Error::Config(
                "TLS certificate is already expired".to_owned(),
            ));
        }

        let ttl = expiry_epoch - now_secs;
        let days_remaining = ttl / 86400;
        if days_remaining < 7 {
            tracing::warn!(
                days_remaining,
                "TLS certificate expires in less than 7 days"
            );
        }

        let client_auth = !config.client_ca_file.is_empty();

        let server_config = build_server_config(config, &certs, key.clone_key(), client_auth)?;
        let server_config = Arc::new(server_config);
        let acceptor = TlsAcceptor::from(Arc::clone(&server_config));

        let cert_mtime = file_mtime_secs(&config.cert_file).unwrap_or(0);
        let key_mtime = file_mtime_secs(&config.key_file).unwrap_or(0);
        let fingerprint = cert_fingerprint_sha256(&certs);

        Ok(Some(Self {
            acceptor: ArcSwap::from_pointee(acceptor),
            server_config: ArcSwap::from_pointee(server_config),
            client_auth,
            config: config.clone(),
            cert_mtime: AtomicU64::new(cert_mtime),
            key_mtime: AtomicU64::new(key_mtime),
            cert_expiry_epoch: AtomicU64::new(expiry_epoch),
            cert_fingerprint: std::sync::RwLock::new(fingerprint),
        }))
    }

    /// Reload certificate and key files from disk.
    ///
    /// Returns `Ok(true)` if certs were reloaded, `Ok(false)` if unchanged.
    /// On failure, the old cert continues serving.
    pub fn reload(&self) -> Result<bool, Error> {
        let new_cert_mtime = file_mtime_secs(&self.config.cert_file).unwrap_or(0);
        let new_key_mtime = file_mtime_secs(&self.config.key_file).unwrap_or(0);

        let old_cert_mtime = self.cert_mtime.load(Ordering::Relaxed);
        let old_key_mtime = self.key_mtime.load(Ordering::Relaxed);

        if new_cert_mtime == old_cert_mtime && new_key_mtime == old_key_mtime {
            return Ok(false);
        }

        let (certs, key) = load_cert_and_key(&self.config.cert_file, &self.config.key_file)?;

        let expiry_epoch = cert_expiry_epoch(&certs)?;

        let server_config =
            build_server_config(&self.config, &certs, key.clone_key(), self.client_auth)?;
        let server_config = Arc::new(server_config);
        let acceptor = TlsAcceptor::from(Arc::clone(&server_config));

        self.acceptor.store(Arc::new(acceptor));
        self.server_config.store(Arc::new(server_config));
        self.cert_mtime.store(new_cert_mtime, Ordering::Relaxed);
        self.key_mtime.store(new_key_mtime, Ordering::Relaxed);
        self.cert_expiry_epoch
            .store(expiry_epoch, Ordering::Relaxed);
        if let Ok(mut fp) = self.cert_fingerprint.write() {
            *fp = cert_fingerprint_sha256(&certs);
        }

        tracing::info!("TLS certificates reloaded successfully");
        Ok(true)
    }

    /// Wrap a raw `TcpStream` in TLS. Returns the negotiated stream.
    pub async fn accept(&self, stream: TcpStream) -> Result<TlsStream<TcpStream>, Error> {
        let acceptor = self.acceptor.load();
        acceptor
            .accept(stream)
            .await
            .map_err(|e| Error::Tls(format!("TLS handshake failed: {e}")))
    }

    /// Return a clone of the current `ServerConfig` for axum-server.
    pub fn server_config(&self) -> Arc<ServerConfig> {
        let guard = self.server_config.load();
        Arc::clone(&*guard)
    }

    /// Extract client certificate info from a completed TLS connection.
    ///
    /// Returns `None` if the client presented no certificate (standard TLS
    /// without mTLS, or mTLS where the client didn't provide one — though
    /// `WebPkiClientVerifier` rejects that during handshake).
    pub fn client_info(conn: &TlsStream<TcpStream>) -> Option<ClientCertInfo> {
        let (_, server_conn) = conn.get_ref();
        let certs = server_conn.peer_certificates()?;
        let leaf = certs.first()?;
        parse_client_cert_info(leaf.as_ref())
    }

    /// Seconds until the current server cert expires. 0 if already expired or
    /// if the expiry could not be determined.
    ///
    /// Uses cached expiry from construction/reload — no disk I/O.
    pub fn cert_ttl_secs(&self) -> u64 {
        let expiry = self.cert_expiry_epoch.load(Ordering::Relaxed);
        let now = now_unix_secs();
        expiry.saturating_sub(now)
    }

    /// Certificate expiry as seconds since Unix epoch.
    pub fn cert_expiry_epoch(&self) -> u64 {
        self.cert_expiry_epoch.load(Ordering::Relaxed)
    }

    /// Whether mTLS client authentication is enabled.
    pub fn client_auth(&self) -> bool {
        self.client_auth
    }

    /// Configured TLS handshake timeout.
    pub fn handshake_timeout(&self) -> std::time::Duration {
        std::time::Duration::from_secs(self.config.handshake_timeout_secs)
    }

    /// SHA-256 fingerprint of the server leaf certificate (colon-separated hex).
    pub fn cert_fingerprint(&self) -> String {
        self.cert_fingerprint
            .read()
            .map(|fp| fp.clone())
            .unwrap_or_default()
    }

    /// Path to the configured cert file.
    pub fn cert_file(&self) -> &str {
        &self.config.cert_file
    }

    /// Minimum TLS version configured.
    pub fn min_version(&self) -> &str {
        &self.config.min_version
    }
}

/// Parse a DER-encoded X.509 certificate into `ClientCertInfo`.
///
/// Extracted from `TlsProvider::client_info()` so it can be unit-tested
/// without constructing a full `TlsStream`.
fn parse_client_cert_info(der: &[u8]) -> Option<ClientCertInfo> {
    let (_, x509) = X509Certificate::from_der(der).ok()?;

    // Common Name from subject
    let common_name = x509
        .subject()
        .iter_common_name()
        .next()
        .and_then(|cn| cn.as_str().ok().map(String::from));

    // Subject Alternative Names
    let sans = x509
        .subject_alternative_name()
        .ok()
        .flatten()
        .map(|ext| {
            ext.value
                .general_names
                .iter()
                .filter_map(|name| match name {
                    x509_parser::extensions::GeneralName::DNSName(s) => Some((*s).to_owned()),
                    x509_parser::extensions::GeneralName::IPAddress(bytes) => {
                        if bytes.len() == 4 {
                            Some(format!(
                                "{}.{}.{}.{}",
                                bytes[0], bytes[1], bytes[2], bytes[3]
                            ))
                        } else {
                            None
                        }
                    }
                    _ => None,
                })
                .collect::<Vec<_>>()
        })
        .unwrap_or_default();

    // SHA-256 fingerprint using ring (already linked via rustls)
    let digest = ring::digest::digest(&ring::digest::SHA256, der);
    let fingerprint_sha256 = digest
        .as_ref()
        .iter()
        .map(|b| format!("{b:02x}"))
        .collect::<Vec<_>>()
        .join(":");

    // Serial number (hex-encoded)
    let serial = x509
        .raw_serial()
        .iter()
        .map(|b| format!("{b:02x}"))
        .collect::<Vec<_>>()
        .join(":");

    // Expiry as unix millis
    let not_after = x509.validity().not_after.timestamp() as u64 * 1000;

    Some(ClientCertInfo {
        common_name,
        sans,
        fingerprint_sha256,
        serial,
        not_after,
    })
}

/// Load PEM certificate chain and private key from files.
fn load_cert_and_key(
    cert_path: &str,
    key_path: &str,
) -> Result<(Vec<CertificateDer<'static>>, PrivateKeyDer<'static>), Error> {
    let cert_file = fs::File::open(cert_path)
        .map_err(|e| Error::Config(format!("failed to open cert_file {cert_path:?}: {e}")))?;
    let mut cert_reader = BufReader::new(cert_file);
    let certs: Vec<CertificateDer<'static>> = rustls_pemfile::certs(&mut cert_reader)
        .collect::<Result<Vec<_>, _>>()
        .map_err(|e| Error::Config(format!("failed to parse cert_file {cert_path:?}: {e}")))?;
    if certs.is_empty() {
        return Err(Error::Config(format!(
            "cert_file {cert_path:?} contains no certificates"
        )));
    }

    let key_file = fs::File::open(key_path)
        .map_err(|e| Error::Config(format!("failed to open key_file {key_path:?}: {e}")))?;
    let mut key_reader = BufReader::new(key_file);
    let key = rustls_pemfile::private_key(&mut key_reader)
        .map_err(|e| Error::Config(format!("failed to parse key_file {key_path:?}: {e}")))?
        .ok_or_else(|| Error::Config(format!("key_file {key_path:?} contains no private key")))?;

    Ok((certs, key))
}

/// Build a `rustls::ServerConfig` from loaded cert/key.
fn build_server_config(
    config: &TlsConfig,
    certs: &[CertificateDer<'static>],
    key: PrivateKeyDer<'static>,
    client_auth: bool,
) -> Result<ServerConfig, Error> {
    let provider = rustls::crypto::ring::default_provider();

    let builder = ServerConfig::builder_with_provider(Arc::new(provider));

    let builder = match config.min_version.as_str() {
        "1.3" => builder.with_protocol_versions(&[&rustls::version::TLS13]),
        _ => builder.with_protocol_versions(rustls::ALL_VERSIONS),
    }
    .map_err(|e| Error::Config(format!("failed to configure TLS versions: {e}")))?;

    let builder = if client_auth {
        let ca_file = fs::File::open(&config.client_ca_file).map_err(|e| {
            Error::Config(format!(
                "failed to open client_ca_file {:?}: {e}",
                config.client_ca_file
            ))
        })?;
        let mut ca_reader = BufReader::new(ca_file);
        let mut root_store = rustls::RootCertStore::empty();
        let ca_certs: Vec<CertificateDer<'static>> = rustls_pemfile::certs(&mut ca_reader)
            .collect::<Result<Vec<_>, _>>()
            .map_err(|e| {
                Error::Config(format!(
                    "failed to parse client_ca_file {:?}: {e}",
                    config.client_ca_file
                ))
            })?;
        if ca_certs.is_empty() {
            return Err(Error::Config(format!(
                "client_ca_file {:?} contains no certificates",
                config.client_ca_file
            )));
        }
        for cert in ca_certs {
            root_store
                .add(cert)
                .map_err(|e| Error::Config(format!("failed to add CA cert to root store: {e}")))?;
        }
        let verifier = WebPkiClientVerifier::builder_with_provider(
            Arc::new(root_store),
            Arc::new(rustls::crypto::ring::default_provider()),
        )
        .build()
        .map_err(|e| Error::Config(format!("failed to build client verifier: {e}")))?;
        builder.with_client_cert_verifier(verifier)
    } else {
        builder.with_no_client_auth()
    };

    builder
        .with_single_cert(certs.to_vec(), key)
        .map_err(|e| Error::Config(format!("cert/key mismatch or invalid: {e}")))
}

/// Get file modification time as seconds since Unix epoch.
fn file_mtime_secs(path: &str) -> Option<u64> {
    let meta = fs::metadata(path).ok()?;
    let mtime = meta.modified().ok()?;
    mtime.duration_since(UNIX_EPOCH).ok().map(|d| d.as_secs())
}

/// Extract the `notAfter` timestamp from the leaf certificate as seconds
/// since Unix epoch. Uses `x509-parser` for robust ASN.1/X.509 parsing.
fn cert_expiry_epoch(certs: &[CertificateDer<'_>]) -> Result<u64, Error> {
    let leaf = certs
        .first()
        .ok_or_else(|| Error::Config("no certificates in chain".to_owned()))?;
    let (_, x509) = X509Certificate::from_der(leaf.as_ref())
        .map_err(|e| Error::Config(format!("failed to parse X.509 certificate: {e}")))?;
    let not_after = x509.validity().not_after;
    let ts = not_after.timestamp();
    Ok(ts as u64)
}

/// Compute SHA-256 fingerprint of the leaf certificate (colon-separated hex).
fn cert_fingerprint_sha256(certs: &[CertificateDer<'_>]) -> String {
    match certs.first() {
        Some(leaf) => {
            let digest = ring::digest::digest(&ring::digest::SHA256, leaf.as_ref());
            digest
                .as_ref()
                .iter()
                .map(|b| format!("{b:02x}"))
                .collect::<Vec<_>>()
                .join(":")
        }
        None => String::new(),
    }
}

/// Current time as seconds since Unix epoch.
fn now_unix_secs() -> u64 {
    SystemTime::now()
        .duration_since(UNIX_EPOCH)
        .unwrap_or_default()
        .as_secs()
}

#[cfg(test)]
mod tests {
    use super::*;
    use std::io::Write;
    use tempfile::NamedTempFile;

    /// Generate a self-signed cert+key pair with the given validity duration.
    fn generate_test_cert(valid_days: i64) -> (NamedTempFile, NamedTempFile) {
        let mut params = rcgen::CertificateParams::new(vec!["localhost".to_owned()]).unwrap();
        params.not_before = rcgen::date_time_ymd(2020, 1, 1);
        if valid_days > 0 {
            let now = SystemTime::now()
                .duration_since(UNIX_EPOCH)
                .unwrap()
                .as_secs();
            let future = now + valid_days as u64 * 86400;
            let days_since_epoch = future / 86400;
            let (y, m, d) = civil_from_days(days_since_epoch);
            params.not_after = rcgen::date_time_ymd(y as i32, m as u8, d as u8);
        } else {
            // Expired: set not_after in the past.
            params.not_after = rcgen::date_time_ymd(2020, 1, 2);
        }

        let key_pair = rcgen::KeyPair::generate().unwrap();
        let cert = params.self_signed(&key_pair).unwrap();

        let mut cert_file = NamedTempFile::new().unwrap();
        cert_file.write_all(cert.pem().as_bytes()).unwrap();

        let mut key_file = NamedTempFile::new().unwrap();
        key_file
            .write_all(key_pair.serialize_pem().as_bytes())
            .unwrap();

        (cert_file, key_file)
    }

    /// Inverse of days_from_civil for test date generation (Hinnant algorithm).
    fn civil_from_days(days: u64) -> (u64, u64, u64) {
        let z = days as i64 + 719468;
        let era = z / 146097;
        let doe = z - era * 146097;
        let yoe = (doe - doe / 1460 + doe / 36524 - doe / 146096) / 365;
        let y = yoe + era * 400;
        let doy = doe - (365 * yoe + yoe / 4 - yoe / 100);
        let mp = (5 * doy + 2) / 153;
        let d = doy - (153 * mp + 2) / 5 + 1;
        let m = if mp < 10 { mp + 3 } else { mp - 9 };
        let y = if m <= 2 { y + 1 } else { y };
        (y as u64, m as u64, d as u64)
    }

    fn make_tls_config(cert_path: &str, key_path: &str) -> TlsConfig {
        TlsConfig {
            enabled: true,
            cert_file: cert_path.to_owned(),
            key_file: key_path.to_owned(),
            client_ca_file: String::new(),
            min_version: "1.2".to_owned(),
            reload_interval_secs: 0,
            ciphersuite_allowlist: Vec::new(),
            handshake_timeout_secs: 10,
        }
    }

    #[test]
    fn disabled_returns_none() {
        let config = TlsConfig::default();
        assert!(!config.enabled);
        let provider = TlsProvider::from_config(&config).unwrap();
        assert!(provider.is_none());
    }

    #[test]
    fn valid_cert_constructs_provider() {
        let (cert_file, key_file) = generate_test_cert(365);
        let config = make_tls_config(
            cert_file.path().to_str().unwrap(),
            key_file.path().to_str().unwrap(),
        );
        let provider = TlsProvider::from_config(&config).unwrap();
        assert!(provider.is_some());
        let provider = provider.unwrap();
        assert!(!provider.client_auth());
    }

    #[test]
    fn missing_cert_file_fails() {
        let (_, key_file) = generate_test_cert(365);
        let config = make_tls_config("/nonexistent/cert.pem", key_file.path().to_str().unwrap());
        let result = TlsProvider::from_config(&config);
        assert!(result.is_err());
        let err = result.unwrap_err().to_string();
        assert!(err.contains("cert_file"), "error was: {err}");
    }

    #[test]
    fn missing_key_file_fails() {
        let (cert_file, _) = generate_test_cert(365);
        let config = make_tls_config(cert_file.path().to_str().unwrap(), "/nonexistent/key.pem");
        let result = TlsProvider::from_config(&config);
        assert!(result.is_err());
        let err = result.unwrap_err().to_string();
        assert!(err.contains("key_file"), "error was: {err}");
    }

    #[test]
    fn cert_key_mismatch_fails() {
        let (cert_file, _key1) = generate_test_cert(365);
        let (_cert2, key_file2) = generate_test_cert(365);
        let config = make_tls_config(
            cert_file.path().to_str().unwrap(),
            key_file2.path().to_str().unwrap(),
        );
        let result = TlsProvider::from_config(&config);
        assert!(result.is_err());
        let err = result.unwrap_err().to_string();
        assert!(
            err.contains("mismatch") || err.contains("invalid"),
            "error was: {err}"
        );
    }

    #[test]
    fn expired_cert_fails() {
        let (cert_file, key_file) = generate_test_cert(-1);
        let config = make_tls_config(
            cert_file.path().to_str().unwrap(),
            key_file.path().to_str().unwrap(),
        );
        let result = TlsProvider::from_config(&config);
        assert!(result.is_err());
        let err = result.unwrap_err().to_string();
        assert!(err.contains("expired"), "error was: {err}");
    }

    #[test]
    fn cert_ttl_returns_positive_for_valid_cert() {
        let (cert_file, key_file) = generate_test_cert(365);
        let config = make_tls_config(
            cert_file.path().to_str().unwrap(),
            key_file.path().to_str().unwrap(),
        );
        let provider = TlsProvider::from_config(&config).unwrap().unwrap();
        let ttl = provider.cert_ttl_secs();
        // Should be roughly 365 days in seconds, allow some slack
        assert!(ttl > 300 * 86400, "ttl was {ttl}");
        assert!(ttl < 400 * 86400, "ttl was {ttl}");
    }

    #[test]
    fn cert_ttl_uses_cached_value() {
        // Verify cert_ttl_secs doesn't re-read from disk by deleting the cert
        // file after construction. If it were reading from disk, it would return 0.
        let (cert_file, key_file) = generate_test_cert(365);
        let config = make_tls_config(
            cert_file.path().to_str().unwrap(),
            key_file.path().to_str().unwrap(),
        );
        let provider = TlsProvider::from_config(&config).unwrap().unwrap();

        // Delete cert file
        drop(cert_file);

        // Should still return a valid TTL from cache
        let ttl = provider.cert_ttl_secs();
        assert!(ttl > 300 * 86400, "ttl was {ttl} — expected cached value");
    }

    #[test]
    fn server_config_returns_arc() {
        let (cert_file, key_file) = generate_test_cert(365);
        let config = make_tls_config(
            cert_file.path().to_str().unwrap(),
            key_file.path().to_str().unwrap(),
        );
        let provider = TlsProvider::from_config(&config).unwrap().unwrap();
        let sc = provider.server_config();
        // Should be a valid ServerConfig — just verify we can call it.
        assert!(sc.alpn_protocols.is_empty());
    }

    #[test]
    fn reload_returns_false_when_unchanged() {
        let (cert_file, key_file) = generate_test_cert(365);
        let config = make_tls_config(
            cert_file.path().to_str().unwrap(),
            key_file.path().to_str().unwrap(),
        );
        let provider = TlsProvider::from_config(&config).unwrap().unwrap();
        let changed = provider.reload().unwrap();
        assert!(!changed);
    }

    #[test]
    fn reload_detects_changed_cert() {
        let (cert_file, key_file) = generate_test_cert(365);
        let config = make_tls_config(
            cert_file.path().to_str().unwrap(),
            key_file.path().to_str().unwrap(),
        );
        let provider = TlsProvider::from_config(&config).unwrap().unwrap();
        let old_expiry = provider.cert_expiry_epoch();

        // Generate a new cert with different expiry and overwrite the file.
        // Need to wait at least 1 second so mtime changes (filesystem resolution).
        std::thread::sleep(std::time::Duration::from_secs(1));

        let new_params = rcgen::CertificateParams::new(vec!["localhost".to_owned()]).unwrap();
        let new_kp = rcgen::KeyPair::generate().unwrap();
        let new_cert = new_params.self_signed(&new_kp).unwrap();

        // Overwrite cert + key files in place
        fs::write(cert_file.path(), new_cert.pem()).unwrap();
        fs::write(key_file.path(), new_kp.serialize_pem()).unwrap();

        let changed = provider.reload().unwrap();
        assert!(changed, "reload should detect changed files");

        let new_expiry = provider.cert_expiry_epoch();
        // New cert has default rcgen expiry (~4096 days) which differs from
        // the original 365-day cert.
        assert_ne!(old_expiry, new_expiry, "expiry should be updated on reload");
    }

    #[test]
    fn reload_fails_gracefully_with_bad_cert() {
        let (cert_file, key_file) = generate_test_cert(365);
        let config = make_tls_config(
            cert_file.path().to_str().unwrap(),
            key_file.path().to_str().unwrap(),
        );
        let provider = TlsProvider::from_config(&config).unwrap().unwrap();
        let old_expiry = provider.cert_expiry_epoch();

        // Corrupt the cert file
        std::thread::sleep(std::time::Duration::from_secs(1));
        fs::write(cert_file.path(), b"not a cert").unwrap();

        let result = provider.reload();
        assert!(result.is_err());

        // Old expiry should be preserved (old cert still serving)
        assert_eq!(provider.cert_expiry_epoch(), old_expiry);
    }

    #[test]
    fn accept_returns_tls_error() {
        // Verify that accept() returns Error::Tls, not Error::Config
        // We can't easily test a full handshake here, but we verify the error
        // type mapping exists by checking the function compiles and the error
        // variant is used in the source.
        let err = Error::Tls("test".to_owned());
        assert!(err.to_string().contains("TLS error"));
    }

    #[test]
    fn config_invalid_min_version() {
        use crate::config::ServerConfig as SC;
        let mut config: SC = toml::from_str(
            r#"
[tls]
min_version = "1.1"

[namespaces.default]
api_key = "abcdefghijklmnopqrstuvwx"
permissions = ["Subscribe"]
"#,
        )
        .unwrap();
        config.tls.enabled = false; // Don't require cert files
        let errors = config.validate();
        assert!(
            errors
                .iter()
                .any(|e| matches!(e, crate::config::ConfigError::InvalidTlsConfig { reason } if reason.contains("min_version"))),
            "expected min_version error, got: {errors:?}"
        );
    }

    #[test]
    fn tls13_only_config() {
        let (cert_file, key_file) = generate_test_cert(365);
        let mut config = make_tls_config(
            cert_file.path().to_str().unwrap(),
            key_file.path().to_str().unwrap(),
        );
        config.min_version = "1.3".to_owned();
        let provider = TlsProvider::from_config(&config).unwrap();
        assert!(provider.is_some());
    }

    #[test]
    fn resolved_for_listener_uses_shared_defaults() {
        let shared = TlsConfig {
            enabled: true,
            cert_file: "/shared/cert.pem".to_owned(),
            key_file: "/shared/key.pem".to_owned(),
            client_ca_file: "/shared/ca.pem".to_owned(),
            ..TlsConfig::default()
        };
        let resolved = shared.resolved_for_listener(None, None, None);
        assert_eq!(resolved.cert_file, "/shared/cert.pem");
        assert_eq!(resolved.key_file, "/shared/key.pem");
        assert_eq!(resolved.client_ca_file, "/shared/ca.pem");
    }

    #[test]
    fn resolved_for_listener_overrides_all() {
        let shared = TlsConfig {
            enabled: true,
            cert_file: "/shared/cert.pem".to_owned(),
            key_file: "/shared/key.pem".to_owned(),
            client_ca_file: "/shared/ca.pem".to_owned(),
            ..TlsConfig::default()
        };
        let resolved = shared.resolved_for_listener(
            Some(&"/tcp/cert.pem".to_owned()),
            Some(&"/tcp/key.pem".to_owned()),
            Some(&"/tcp/ca.pem".to_owned()),
        );
        assert_eq!(resolved.cert_file, "/tcp/cert.pem");
        assert_eq!(resolved.key_file, "/tcp/key.pem");
        assert_eq!(resolved.client_ca_file, "/tcp/ca.pem");
    }

    #[test]
    fn resolved_for_listener_partial_override() {
        let shared = TlsConfig {
            enabled: true,
            cert_file: "/shared/cert.pem".to_owned(),
            key_file: "/shared/key.pem".to_owned(),
            client_ca_file: "/shared/ca.pem".to_owned(),
            ..TlsConfig::default()
        };
        let resolved = shared.resolved_for_listener(Some(&"/tcp/cert.pem".to_owned()), None, None);
        assert_eq!(resolved.cert_file, "/tcp/cert.pem");
        assert_eq!(resolved.key_file, "/shared/key.pem");
        assert_eq!(resolved.client_ca_file, "/shared/ca.pem");
    }

    // -- parse_client_cert_info unit tests --

    /// Generate a DER cert with known CN and SANs for testing parse_client_cert_info.
    fn generate_test_der_cert(
        cn: &str,
        dns_sans: &[&str],
        ip_sans: &[std::net::Ipv4Addr],
    ) -> Vec<u8> {
        let mut params = rcgen::CertificateParams::new(
            dns_sans.iter().map(|s| (*s).to_owned()).collect::<Vec<_>>(),
        )
        .unwrap();
        params
            .distinguished_name
            .push(rcgen::DnType::CommonName, cn);
        for ip in ip_sans {
            params
                .subject_alt_names
                .push(rcgen::SanType::IpAddress(std::net::IpAddr::V4(*ip)));
        }
        let key_pair = rcgen::KeyPair::generate().unwrap();
        let cert = params.self_signed(&key_pair).unwrap();
        cert.der().to_vec()
    }

    #[test]
    fn parse_cert_info_extracts_cn() {
        let der = generate_test_der_cert("my-service", &["localhost"], &[]);
        let info = super::parse_client_cert_info(&der).unwrap();
        assert_eq!(info.common_name.as_deref(), Some("my-service"));
    }

    #[test]
    fn parse_cert_info_extracts_dns_sans() {
        let der = generate_test_der_cert("svc", &["foo.example.com", "bar.example.com"], &[]);
        let info = super::parse_client_cert_info(&der).unwrap();
        assert!(info.sans.contains(&"foo.example.com".to_owned()));
        assert!(info.sans.contains(&"bar.example.com".to_owned()));
    }

    #[test]
    fn parse_cert_info_extracts_ip_sans() {
        let der = generate_test_der_cert("svc", &["localhost"], &[std::net::Ipv4Addr::LOCALHOST]);
        let info = super::parse_client_cert_info(&der).unwrap();
        assert!(
            info.sans.contains(&"127.0.0.1".to_owned()),
            "expected 127.0.0.1 in SANs, got: {:?}",
            info.sans
        );
    }

    #[test]
    fn parse_cert_info_fingerprint_is_sha256_hex() {
        let der = generate_test_der_cert("svc", &["localhost"], &[]);
        let info = super::parse_client_cert_info(&der).unwrap();
        // SHA-256 fingerprint should be 32 bytes = 64 hex chars + 31 colons = 95 chars.
        assert_eq!(
            info.fingerprint_sha256.len(),
            95,
            "fingerprint: {}",
            info.fingerprint_sha256
        );
        // Verify it's colon-separated hex pairs.
        for part in info.fingerprint_sha256.split(':') {
            assert_eq!(part.len(), 2, "each fingerprint part should be 2 hex chars");
            assert!(u8::from_str_radix(part, 16).is_ok(), "invalid hex: {part}");
        }
    }

    #[test]
    fn parse_cert_info_serial_is_hex() {
        let der = generate_test_der_cert("svc", &["localhost"], &[]);
        let info = super::parse_client_cert_info(&der).unwrap();
        assert!(!info.serial.is_empty(), "serial should not be empty");
        // Serial is colon-separated hex.
        for part in info.serial.split(':') {
            assert_eq!(part.len(), 2);
            assert!(u8::from_str_radix(part, 16).is_ok(), "invalid hex: {part}");
        }
    }

    #[test]
    fn parse_cert_info_not_after_is_future() {
        let der = generate_test_der_cert("svc", &["localhost"], &[]);
        let info = super::parse_client_cert_info(&der).unwrap();
        let now_millis = SystemTime::now()
            .duration_since(UNIX_EPOCH)
            .unwrap()
            .as_millis() as u64;
        assert!(
            info.not_after > now_millis,
            "not_after {} should be in the future (now {})",
            info.not_after,
            now_millis
        );
    }

    #[test]
    fn parse_cert_info_no_cn() {
        // Generate a cert without a CN — only SANs.
        let mut params = rcgen::CertificateParams::new(vec!["localhost".to_owned()]).unwrap();
        // Don't push a CN — rcgen's default DN is empty.
        params.distinguished_name = rcgen::DistinguishedName::new();
        let key_pair = rcgen::KeyPair::generate().unwrap();
        let cert = params.self_signed(&key_pair).unwrap();
        let der = cert.der().to_vec();

        let info = super::parse_client_cert_info(&der).unwrap();
        assert_eq!(info.common_name, None, "CN should be None when not set");
        assert!(!info.sans.is_empty(), "SANs should still be present");
    }

    #[test]
    fn parse_cert_info_returns_none_for_garbage() {
        let result = super::parse_client_cert_info(b"not a cert");
        assert!(result.is_none(), "garbage input should return None");
    }
}
