//! A bare port probe must not be reported as a TLS fault.
//!
//! `TLS HANDSHAKE FAILED: ... unexpected EOF` is the exact signature the
//! certificate mismatch produced — the defect that cost three live gate
//! attempts and was invisible everywhere else. Anything that can forge that
//! line degrades the one channel this project gates on.
//!
//! A reachability probe forges it trivially: `TcpStream::connect` followed by a
//! drop opens the connection and closes it without sending a byte, and the
//! acceptor reports that as `unexpected EOF`. The launcher's preflight makes
//! two such probes per run, and on 2026-08-12 they produced ten of these lines
//! and sent this session chasing a client-side fault that did not exist.

use std::io::Write;
use std::net::TcpStream;
use std::sync::atomic::Ordering;
use std::time::Duration;

use openfut_adapter_fifa17::tls;
use openfut_redirector_host::{bind, classify_opening, PeerOpening, RedirectorConfig};
use openssl::ssl::{SslConnector, SslMethod, SslVerifyMode};

/// Start a host on an ephemeral port; hand back its address and its counters.
fn start() -> (
    String,
    openfut_redirector_host::ProbeCount,
    openfut_redirector_host::Outcomes,
) {
    let mut cfg = RedirectorConfig::for_test("198.51.100.7");
    cfg.listen_port = 0;
    let server = bind(cfg).expect("host binds");
    let addr = server.local_addr.to_string();
    let (probes, outcomes) = (server.probes(), server.outcomes());
    std::thread::spawn(move || {
        let _ = server.run();
    });
    (addr, probes, outcomes)
}

fn tls_request(addr: &str) -> Vec<u8> {
    let mut b = SslConnector::builder(SslMethod::tls()).expect("connector");
    b.set_cipher_list(tls::OBSERVED_CLIENT_SUITES)
        .expect("list");
    b.set_verify(SslVerifyMode::NONE);
    let sock = TcpStream::connect(addr).expect("connect");
    let ssl = b
        .build()
        .configure()
        .expect("configure")
        .verify_hostname(false)
        .into_ssl("winter15.gosredirector.ea.com")
        .expect("ssl");
    let mut s = openssl::ssl::SslStream::new(ssl, sock).expect("stream");
    s.connect().expect("handshake");
    s.write_all(b"POST /redirector/getServerInstance HTTP/1.1\r\nContent-Length: 0\r\n\r\n")
        .expect("write");
    s.flush().ok();
    let mut out = Vec::new();
    let _ = std::io::Read::read_to_end(&mut s, &mut out);
    out
}

/// The whole point: connect, send nothing, close — and be classified as a
/// probe rather than reaching the acceptor.
///
/// Asserting on the counter rather than on client-visible behaviour is
/// deliberate. A probe produces no response either way, so a test written
/// against what the client sees would pass with the classification deleted —
/// exactly the mutation that must not survive.
#[test]
fn a_bare_connect_and_close_is_classified_as_a_probe() {
    let (addr, probes, outcomes) = start();

    drop(TcpStream::connect(&addr).expect("probe connects"));
    std::thread::sleep(Duration::from_millis(200));

    assert_eq!(
        probes.load(Ordering::Relaxed),
        1,
        "a connect-and-close was not classified as a probe"
    );
    assert!(
        outcomes.lock().expect("lock").is_empty(),
        "a probe must not be recorded as a served connection"
    );
}

/// A probe must not disturb the host: the next real client still gets served.
#[test]
fn a_probe_does_not_break_the_connection_that_follows_it() {
    let (addr, probes, _outcomes) = start();

    drop(TcpStream::connect(&addr).expect("probe connects"));
    std::thread::sleep(Duration::from_millis(100));

    let response = tls_request(&addr);
    assert!(
        response.starts_with(b"HTTP/1.1 200 OK"),
        "real client after a probe got: {:?}",
        String::from_utf8_lossy(response.get(..64).unwrap_or(&response))
    );
    assert_eq!(
        probes.load(Ordering::Relaxed),
        1,
        "the real client was miscounted as a probe"
    );
}

/// The dangerous direction: a client that DOES speak and then fails must still
/// reach the acceptor and be reported as a fault. Classifying it as a probe
/// would hide precisely what the distinction exists to protect.
#[test]
fn a_client_that_speaks_then_fails_is_not_a_probe() {
    let (addr, probes, _outcomes) = start();

    let mut sock = TcpStream::connect(&addr).expect("connect");
    // One byte of nonsense: enough that the peer has spoken, not enough to be
    // a ClientHello, so the handshake genuinely fails.
    sock.write_all(&[0x16]).expect("write");
    sock.flush().ok();
    drop(sock);
    std::thread::sleep(Duration::from_millis(200));

    assert_eq!(
        probes.load(Ordering::Relaxed),
        0,
        "a failing handshake was silently reclassified as a benign probe"
    );
}

#[test]
fn the_classification_policy_is_explicit_about_each_case() {
    assert_eq!(
        classify_opening(&Ok(0)),
        PeerOpening::ClosedWithoutSpeaking,
        "EOF before any byte is a probe"
    );
    assert_eq!(classify_opening(&Ok(1)), PeerOpening::Spoke);
    // A timeout must NOT be a probe: a slow or broken client still deserves a
    // real diagnostic from the acceptor.
    assert_eq!(
        classify_opening(&Err(std::io::Error::from(std::io::ErrorKind::WouldBlock))),
        PeerOpening::Undetermined
    );
}
