// Integration-test exemptions (same posture as the tidaldb integration tests):
// unwrap on known-good fixtures and short-lived read guards are idiomatic here.
#![allow(clippy::unwrap_used, clippy::significant_drop_tightening)]
//! Tests for mutual TLS configuration.

use std::{collections::HashMap, net::SocketAddr, thread, time::Duration};

use tidal_net::{
    GrpcTransport,
    config::{GrpcTransportConfig, TlsConfig},
};
use tidaldb::replication::{
    WalSegmentId,
    shard::{RegionId, ShardId},
    transport::{Transport, WalSegmentPayload},
};

fn free_addr() -> SocketAddr {
    let listener = std::net::TcpListener::bind("127.0.0.1:0").unwrap();
    listener.local_addr().unwrap()
}

/// Generate a self-signed CA and server/client certificates using rcgen.
fn generate_certs(dir: &std::path::Path) -> TlsConfig {
    use rcgen::{CertificateParams, KeyPair};

    // Generate CA.
    let ca_key = KeyPair::generate().unwrap();
    let mut ca_params = CertificateParams::new(vec!["tidaldb-ca".to_string()]).unwrap();
    ca_params.is_ca = rcgen::IsCa::Ca(rcgen::BasicConstraints::Unconstrained);
    let ca_cert = ca_params.self_signed(&ca_key).unwrap();

    // Generate server cert signed by CA.
    let server_key = KeyPair::generate().unwrap();
    let server_params =
        CertificateParams::new(vec!["localhost".to_string(), "127.0.0.1".to_string()]).unwrap();
    let server_cert = server_params
        .signed_by(&server_key, &ca_cert, &ca_key)
        .unwrap();

    // Generate client cert signed by CA.
    let client_key = KeyPair::generate().unwrap();
    let client_params = CertificateParams::new(vec!["tidaldb-client".to_string()]).unwrap();
    let client_cert = client_params
        .signed_by(&client_key, &ca_cert, &ca_key)
        .unwrap();

    // Write to files.
    let ca_cert_path = dir.join("ca.pem");
    let server_cert_path = dir.join("server.pem");
    let server_key_path = dir.join("server-key.pem");
    let client_cert_path = dir.join("client.pem");
    let client_key_path = dir.join("client-key.pem");

    std::fs::write(&ca_cert_path, ca_cert.pem()).unwrap();
    std::fs::write(&server_cert_path, server_cert.pem()).unwrap();
    std::fs::write(&server_key_path, server_key.serialize_pem()).unwrap();
    std::fs::write(&client_cert_path, client_cert.pem()).unwrap();
    std::fs::write(&client_key_path, client_key.serialize_pem()).unwrap();

    TlsConfig {
        ca_cert: ca_cert_path,
        server_cert: server_cert_path,
        server_key: server_key_path,
        client_cert: Some(client_cert_path),
        client_key: Some(client_key_path),
    }
}

/// Generate a SECOND, independent CA and a client cert/key signed by it,
/// written alongside the legit material. The returned `TlsConfig` carries the
/// legit server-trusted CA (so the client still trusts the server) but a client
/// identity the server's `client_ca_root` does NOT trust — i.e. an untrusted
/// client certificate. Used to prove the mTLS boundary actually rejects
/// foreign client certs.
fn generate_untrusted_client(dir: &std::path::Path, legit: &TlsConfig) -> TlsConfig {
    use rcgen::{CertificateParams, KeyPair};

    // A rogue CA the server has never heard of.
    let rogue_ca_key = KeyPair::generate().unwrap();
    let mut rogue_ca_params = CertificateParams::new(vec!["rogue-ca".to_string()]).unwrap();
    rogue_ca_params.is_ca = rcgen::IsCa::Ca(rcgen::BasicConstraints::Unconstrained);
    let rogue_ca_cert = rogue_ca_params.self_signed(&rogue_ca_key).unwrap();

    // A client cert signed by the rogue CA (NOT the server-trusted CA).
    let rogue_client_key = KeyPair::generate().unwrap();
    let rogue_client_params = CertificateParams::new(vec!["rogue-client".to_string()]).unwrap();
    let rogue_client_cert = rogue_client_params
        .signed_by(&rogue_client_key, &rogue_ca_cert, &rogue_ca_key)
        .unwrap();

    let rogue_client_cert_path = dir.join("rogue-client.pem");
    let rogue_client_key_path = dir.join("rogue-client-key.pem");
    std::fs::write(&rogue_client_cert_path, rogue_client_cert.pem()).unwrap();
    std::fs::write(&rogue_client_key_path, rogue_client_key.serialize_pem()).unwrap();

    TlsConfig {
        // Keep the legit CA so the CLIENT still trusts the server's cert; only
        // the client's OWN identity is untrusted by the server.
        ca_cert: legit.ca_cert.clone(),
        server_cert: legit.server_cert.clone(),
        server_key: legit.server_key.clone(),
        client_cert: Some(rogue_client_cert_path),
        client_key: Some(rogue_client_key_path),
    }
}

/// A server config with the legit (trusted) CA and short timeouts so a rejected
/// handshake fails fast in tests.
fn server_config(shard: ShardId, listen: SocketAddr, tls: TlsConfig) -> GrpcTransportConfig {
    GrpcTransportConfig {
        local_shard: shard,
        listen_addr: listen,
        peers: HashMap::new(),
        tls: Some(tls),
        insecure: false,
        connect_timeout: Duration::from_millis(500),
        request_timeout: Duration::from_millis(500),
        ..Default::default()
    }
}

/// SECURITY BOUNDARY: a client whose certificate is signed by a CA the server
/// does not trust must be REJECTED — the mTLS handshake fails and no segment is
/// ever accepted. This is the negative counterpart to `mtls_send_and_receive`;
/// without it the happy path alone could pass even if the server accepted ANY
/// client cert (or none).
#[test]
fn untrusted_client_cert_is_rejected() {
    let tmp = tempfile::tempdir().unwrap();
    let legit = generate_certs(tmp.path());
    let rogue = generate_untrusted_client(tmp.path(), &legit);

    let addr_server = free_addr();
    let addr_client = free_addr();

    // Server (shard 1) trusts only the legit CA for client auth.
    let server = GrpcTransport::new(server_config(ShardId(1), addr_server, legit))
        .expect("server with trusted CA");

    // Client (shard 0) presents a cert signed by a rogue CA.
    let client_cfg = GrpcTransportConfig {
        peers: HashMap::from([(ShardId(1), addr_server)]),
        ..server_config(ShardId(0), addr_client, rogue)
    };
    let client = GrpcTransport::new(client_cfg).expect("client with rogue cert");

    thread::sleep(Duration::from_millis(200));

    // The mTLS handshake must fail, so the send must error. Retry a couple of
    // times to defeat any single-shot connect race; every attempt must fail.
    let mut last_ok = false;
    for seq in 0..3u64 {
        let payload = WalSegmentPayload {
            id: WalSegmentId::new(RegionId::SINGLE, ShardId(0), seq),
            bytes: vec![0x11; 32],
            event_count: 1,
            leader_last_seq: seq,
        };
        if client.send_segment(ShardId(1), payload).is_ok() {
            last_ok = true;
            break;
        }
        thread::sleep(Duration::from_millis(100));
    }
    assert!(
        !last_ok,
        "server must REJECT a client cert signed by an untrusted CA"
    );

    // Keep the server alive until assertions run.
    drop(server);
}

/// SECURITY BOUNDARY: a client that presents NO certificate (absent client
/// identity) must be REJECTED by a server that requires client auth
/// (`client_ca_root`). Verifies the server demands mutual auth rather than
/// accepting one-way TLS.
#[test]
fn absent_client_cert_is_rejected() {
    let tmp = tempfile::tempdir().unwrap();
    let legit = generate_certs(tmp.path());

    let addr_server = free_addr();
    let addr_client = free_addr();

    let server = GrpcTransport::new(server_config(ShardId(1), addr_server, legit.clone()))
        .expect("server requiring client auth");

    // Client trusts the server's CA but presents NO client identity.
    let no_client_id = TlsConfig {
        ca_cert: legit.ca_cert.clone(),
        server_cert: legit.server_cert.clone(),
        server_key: legit.server_key,
        client_cert: None,
        client_key: None,
    };
    let client_cfg = GrpcTransportConfig {
        peers: HashMap::from([(ShardId(1), addr_server)]),
        ..server_config(ShardId(0), addr_client, no_client_id)
    };
    let client = GrpcTransport::new(client_cfg).expect("client without identity");

    thread::sleep(Duration::from_millis(200));

    let mut last_ok = false;
    for seq in 0..3u64 {
        let payload = WalSegmentPayload {
            id: WalSegmentId::new(RegionId::SINGLE, ShardId(0), seq),
            bytes: vec![0x22; 32],
            event_count: 1,
            leader_last_seq: seq,
        };
        if client.send_segment(ShardId(1), payload).is_ok() {
            last_ok = true;
            break;
        }
        thread::sleep(Duration::from_millis(100));
    }
    assert!(
        !last_ok,
        "server requiring client_ca_root must REJECT a client with no certificate"
    );

    // Keep the server alive until assertions run.
    drop(server);
}

#[test]
fn mtls_send_and_receive() {
    let tmp = tempfile::tempdir().unwrap();
    let tls = generate_certs(tmp.path());

    let addr0 = free_addr();
    let addr1 = free_addr();

    let config0 = GrpcTransportConfig {
        local_shard: ShardId(0),
        listen_addr: addr0,
        peers: HashMap::from([(ShardId(1), addr1)]),
        tls: Some(tls.clone()),
        insecure: false,
        ..Default::default()
    };

    let config1 = GrpcTransportConfig {
        local_shard: ShardId(1),
        listen_addr: addr1,
        peers: HashMap::from([(ShardId(0), addr0)]),
        tls: Some(tls),
        insecure: false,
        ..Default::default()
    };

    let t0 = GrpcTransport::new(config0).expect("transport 0 with TLS");
    let t1 = GrpcTransport::new(config1).expect("transport 1 with TLS");
    thread::sleep(Duration::from_millis(200));

    let payload = WalSegmentPayload {
        id: WalSegmentId::new(RegionId::SINGLE, ShardId(0), 99),
        bytes: vec![0xEF; 64],
        event_count: 2,
        leader_last_seq: 99,
    };
    t0.send_segment(ShardId(1), payload).unwrap();

    let received = t1.recv_segment().unwrap();
    assert_eq!(received.id.seqno, 99);
    assert_eq!(received.event_count, 2);
}

#[test]
fn plaintext_rejected_when_not_insecure() {
    let addr = free_addr();
    let config = GrpcTransportConfig {
        local_shard: ShardId(0),
        listen_addr: addr,
        peers: HashMap::new(),
        tls: None,
        insecure: false, // Should reject — no TLS and not insecure.
        ..Default::default()
    };

    let result = GrpcTransport::new(config);
    assert!(result.is_err(), "should reject plaintext when not insecure");
}
