//! End-to-end: a real TLS request against the real host, compared with the
//! Python oracle byte-for-byte.
//!
//! The adapter's own suite already proves the response bytes. This proves the
//! whole transport: OpenSSL handshake, HTTP framing, and the same bytes coming
//! back over the wire — the redirector equivalent of the Blaze sidecar's
//! live-transport suite.

use std::io::{Read, Write};
use std::net::TcpStream;

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

/// Start the real host on an ephemeral port.
fn start(advertise: &str) -> String {
    let mut cfg = RedirectorConfig::for_test(advertise);
    cfg.listen_port = 0;
    let server = bind(cfg).expect("host binds");
    let addr = server.local_addr.to_string();
    std::thread::spawn(move || {
        let _ = server.run();
    });
    addr
}

/// A request body, because a retail client POSTs one.
///
/// The first version of this suite sent `Content-Length: 0`. That is why it
/// passed against a host that never drained the body — the blind spot was in
/// the measurement, not the assertion.
const REQUEST_BODY: &[u8] = b"<?xml version=\"1.0\" encoding=\"UTF-8\"?>\
<serverinstancerequest><name>fifa-2017-pc</name></serverinstancerequest>";

/// A client that behaves like the captured FIFA 17 ClientHello.
fn fifa_like_request(addr: &str) -> (String, Vec<u8>) {
    let mut b = SslConnector::builder(SslMethod::tls()).expect("connector");
    b.set_cipher_list(tls::OBSERVED_CLIENT_SUITES)
        .expect("client cipher list");
    // The retail client's cert checks are patched out and the oracle cert is
    // self-signed; the variable under test is cipher/version negotiation.
    b.set_verify(SslVerifyMode::NONE);
    let connector = b.build();

    let sock = TcpStream::connect(addr).expect("connect");
    let ssl = connector
        .configure()
        .expect("configure")
        .verify_hostname(false)
        .into_ssl("winter15.gosredirector.ea.com")
        .expect("ssl");
    let mut stream = openssl::ssl::SslStream::new(ssl, sock).expect("stream");
    stream.connect().expect("TLS handshake");

    let negotiated = format!(
        "{} / {}",
        stream.ssl().version_str(),
        stream
            .ssl()
            .current_cipher()
            .map(|c| c.name())
            .unwrap_or("?")
    );

    let mut req = format!(
        "POST /redirector/getServerInstance HTTP/1.1\r\n\
         Host: winter15.gosredirector.ea.com\r\n\
         Content-Type: application/xml\r\n\
         Content-Length: {}\r\n\r\n",
        REQUEST_BODY.len()
    )
    .into_bytes();
    req.extend_from_slice(REQUEST_BODY);
    stream.write_all(&req).expect("write request");
    stream.flush().ok();

    let mut out = Vec::new();
    let mut chunk = [0u8; 4096];
    loop {
        match stream.read(&mut chunk) {
            Ok(0) | Err(_) => break,
            Ok(n) => out.extend_from_slice(&chunk[..n]),
        }
    }
    (negotiated, out)
}

/// Gate 4: a FIFA-like client completes TLS and gets an HTTP response.
#[test]
fn a_fifa_like_client_gets_a_response_over_tls() {
    let addr = start("198.51.100.7");
    let (negotiated, body) = fifa_like_request(&addr);

    assert_eq!(
        negotiated, "TLSv1.2 / AES256-GCM-SHA384",
        "negotiated the wrong thing"
    );
    let text = String::from_utf8_lossy(&body);
    assert!(text.starts_with("HTTP/1.1 200 OK"), "{text}");
    assert!(text.contains("<serverinstanceinfo>"), "{text}");
}

/// Gate 5: what comes back over TLS is byte-identical to the Python oracle's
/// recorded response for the same advertised address.
#[test]
fn the_response_over_tls_is_byte_identical_to_the_oracle() {
    let path = format!(
        "{}/../openfut-adapter-fifa17/fixtures/redirector.json",
        env!("CARGO_MANIFEST_DIR")
    );
    let text = std::fs::read_to_string(&path).expect("oracle fixtures");

    // Minimal extraction: the fixture is a flat {address: hex} object written
    // by our own generator, so a full JSON dependency would be overkill here.
    let advertise = "198.51.100.7";
    let key = format!("\"{advertise}\"");
    let start_idx = text.find(&key).expect("fixture for this address");
    let rest = &text[start_idx + key.len()..];
    let open = rest.find('"').expect("hex opens");
    let close = rest[open + 1..].find('"').expect("hex closes");
    let want_hex = &rest[open + 1..open + 1 + close];
    let want: Vec<u8> = (0..want_hex.len())
        .step_by(2)
        .map(|i| u8::from_str_radix(&want_hex[i..i + 2], 16).expect("hex"))
        .collect();

    let addr = start(advertise);
    let (_, got) = fifa_like_request(&addr);

    assert_eq!(
        String::from_utf8_lossy(&got),
        String::from_utf8_lossy(&want),
        "the response over TLS differs from the Python oracle"
    );
}

/// The advertised address must come from configuration, over the wire, not
/// just in a unit test.
#[test]
fn the_advertised_address_reaches_the_wire() {
    let addr = start("203.0.113.42");
    let (_, body) = fifa_like_request(&addr);
    let text = String::from_utf8_lossy(&body);
    assert!(text.contains("<hostname>203.0.113.42</hostname>"), "{text}");
    assert!(text.contains("<ip>3405803818</ip>"), "{text}");
    assert!(!text.contains("198.51.100.7"));
}

/// The body must be DRAINED, not left in the receive queue.
///
/// A server that answers and closes with unread data makes the kernel send RST
/// instead of FIN. Sending the body in a second write, after a pause, is what
/// makes this observable: a host that stops at the header terminator has
/// already responded by the time the body arrives.
#[test]
fn the_request_body_is_drained_before_closing() {
    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 outcomes = server.outcomes();
    std::thread::spawn(move || {
        let _ = server.run();
    });

    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 stream = openssl::ssl::SslStream::new(ssl, sock).expect("stream");
    stream.connect().expect("handshake");

    stream
        .write_all(
            format!(
                "POST /redirector/getServerInstance HTTP/1.1\r\n\
                 Content-Length: {}\r\n\r\n",
                REQUEST_BODY.len()
            )
            .as_bytes(),
        )
        .expect("write head");
    stream.flush().ok();
    std::thread::sleep(std::time::Duration::from_millis(150));

    // If the host had already answered and closed, this write — or the read
    // that follows it — is where the reset surfaces.
    stream
        .write_all(REQUEST_BODY)
        .expect("body must be accepted");
    stream.flush().ok();

    let mut out = Vec::new();
    let mut chunk = [0u8; 4096];
    loop {
        match stream.read(&mut chunk) {
            Ok(0) => break,
            Ok(n) => out.extend_from_slice(&chunk[..n]),
            Err(e) => panic!("connection broke instead of closing cleanly: {e}"),
        }
    }
    assert!(
        String::from_utf8_lossy(&out).contains("<serverinstanceinfo>"),
        "expected the full response after a split-write body, got {} bytes",
        out.len()
    );

    // The assertion that carries the weight. Checking only that the client got
    // its response does NOT detect an undrained body — verified by mutation:
    // with the drain removed, the client still reads the buffered response and
    // sees close_notify before any reset. Only the host's own record shows
    // whether it consumed what the client sent.
    let rec = outcomes.lock().expect("outcomes");
    let last = rec.last().expect("one connection was recorded");
    assert_eq!(
        last.body,
        BodyRead::Complete(REQUEST_BODY.len()),
        "the host did not drain the request body: {last:?}"
    );
}

/// The oracle holds the connection open after responding; so must this host.
///
/// Asserted as a floor rather than an exact figure — this measures scheduling,
/// so the guarantee is "at least the oracle's dwell", not "exactly 300ms".
#[test]
fn the_connection_is_held_open_for_the_oracle_dwell() {
    use openfut_redirector_host::config::ORACLE_CLOSE_DWELL;

    let addr = start("198.51.100.7");
    let t0 = std::time::Instant::now();
    let (_, body) = fifa_like_request(&addr);
    let held = t0.elapsed();

    assert!(!body.is_empty(), "no response");
    assert!(
        held >= ORACLE_CLOSE_DWELL,
        "closed after {held:?}, before the oracle's {ORACLE_CLOSE_DWELL:?} dwell"
    );
}

/// The dwell is configurable so the causal experiment can be run, and a bad
/// value is refused rather than silently ignored.
#[test]
fn the_dwell_is_overridable_for_the_causal_experiment() {
    let mut cfg = RedirectorConfig::for_test("198.51.100.7");
    cfg.listen_port = 0;
    cfg.close_dwell = std::time::Duration::ZERO;
    let server = bind(cfg).expect("binds");
    let addr = server.local_addr.to_string();
    std::thread::spawn(move || {
        let _ = server.run();
    });

    let t0 = std::time::Instant::now();
    let (_, body) = fifa_like_request(&addr);
    assert!(!body.is_empty(), "no response");
    assert!(
        t0.elapsed() < std::time::Duration::from_millis(250),
        "a zero dwell should close promptly, took {:?}",
        t0.elapsed()
    );
}

/// A client that offers only forward-secret suites — i.e. anything modern —
/// must fail, confirming this listener really is the legacy island it claims
/// to be and has not silently acquired a modern policy.
#[test]
fn a_modern_client_cannot_connect_to_this_listener() {
    let addr = start("198.51.100.7");
    let mut b = SslConnector::builder(SslMethod::tls()).expect("connector");
    b.set_cipher_list("ECDHE-RSA-AES256-GCM-SHA384")
        .expect("list");
    b.set_verify(SslVerifyMode::NONE);
    let connector = b.build();

    let sock = TcpStream::connect(&addr).expect("connect");
    let ssl = connector
        .configure()
        .expect("configure")
        .verify_hostname(false)
        .into_ssl("winter15.gosredirector.ea.com")
        .expect("ssl");
    let mut stream = openssl::ssl::SslStream::new(ssl, sock).expect("stream");
    assert!(
        stream.connect().is_err(),
        "an ECDHE-only client should not negotiate with this listener"
    );
}
