//! The FUT roster-update response — "is there a squad update to download?".
//!
//! ```text
//!   FIFA 17 ──HTTPS GET /fifa17/fut/rosterupdate.xml──> <rosterupdate version="0"/>
//! ```
//!
//! # Why this matters more than its size suggests
//!
//! `checkFUTRostersFlow` downloads this before entering FUT. On success it
//! advances to the hub; on failure it aborts with *"An error occurred
//! downloading the FUT Squad Update"*. It is the last gate before the hub, and
//! because it is a separate TLS connection from the redirector it is also where
//! a certificate mismatch elsewhere in the stack first becomes visible. Three
//! redirector gates were lost to exactly that.
//!
//! # What the oracle actually puts on the wire
//!
//! `roster_server.py` uses `http.server.BaseHTTPRequestHandler`, which shapes
//! the response in ways the handler code does not show:
//!
//! * the status line is **HTTP/1.0**, because `protocol_version` is left at its
//!   default — not HTTP/1.1, despite the client asking for 1.1
//! * `send_response` emits `Server:` and `Date:` *before* any header the
//!   handler sets, so header order is Server, Date, Content-Type,
//!   Content-Length, Connection
//! * **POST answers with headers only.** The handler writes the body `if method
//!   == "GET"`, so a POST advertises `Content-Length: 67` and then sends
//!   nothing. That is preserved here rather than corrected: it is the behaviour
//!   the retail client was proven against, and "obviously a bug" is exactly the
//!   kind of judgement that has been wrong before in this port.
//!
//! Any path gets the same answer — the oracle logs `self.path` and never routes
//! on it. A reimplementation that started 404ing unknown paths would be a
//! behaviour change, not a fix.

/// The path the client requests. Recorded for diagnostics; **not** used for
/// routing, because the oracle does not route.
pub const REQUEST_PATH: &str = "/fifa17/fut/rosterupdate.xml";

/// The "no update available" body, byte-for-byte from the oracle.
pub const ROSTER_XML: &[u8] =
    b"<?xml version=\"1.0\" encoding=\"utf-8\"?>\n<rosterupdate version=\"0\"/>\n";

/// The `Server:` string the oracle emits.
///
/// Environment-derived, not protocol-derived: it is Python's version string,
/// so it changes when the container's Python does. Kept as a default rather
/// than hardcoded into the response builder so a host can match whatever the
/// deployed oracle actually sends — the same reasoning that makes the
/// advertised address configuration rather than a constant.
pub const ORACLE_SERVER: &str = "BaseHTTP/0.6 Python/3.12.13";

/// Which of the oracle's three handlers a request lands in.
///
/// `Post` is distinct from `Head` in the oracle's *code* (it drains the request
/// body first) but identical in its *response*, so the distinction is kept here
/// for the host's benefit rather than the response builder's.
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum Method {
    Get,
    Head,
    Post,
}

impl Method {
    /// Parse the request line's method. Unknown methods are `None` — the oracle
    /// only defines `do_GET`/`do_HEAD`/`do_POST`, and `BaseHTTPRequestHandler`
    /// answers anything else with its own 501, which is a different response
    /// this module deliberately does not claim to reproduce.
    pub fn parse(line0: &str) -> Option<Method> {
        match line0.split_whitespace().next()? {
            "GET" => Some(Method::Get),
            "HEAD" => Some(Method::Head),
            "POST" => Some(Method::Post),
            _ => None,
        }
    }

    /// Only GET carries the body, per the oracle.
    pub fn carries_body(self) -> bool {
        matches!(self, Method::Get)
    }
}

/// Build the response exactly as the oracle would.
///
/// `date` is supplied by the caller rather than read from the clock here, so
/// this stays a pure function and the fixtures can pin it. Format is the one
/// `BaseHTTPRequestHandler.date_time_string` produces: RFC 7231 IMF-fixdate,
/// always GMT.
pub fn roster_response(method: Method, server: &str, date: &str) -> Vec<u8> {
    let mut out = Vec::with_capacity(256);
    out.extend_from_slice(b"HTTP/1.0 200 OK\r\n");
    out.extend_from_slice(format!("Server: {server}\r\n").as_bytes());
    out.extend_from_slice(format!("Date: {date}\r\n").as_bytes());
    out.extend_from_slice(b"Content-Type: application/xml\r\n");
    // Always the body's length, even when no body follows. See the module note.
    out.extend_from_slice(format!("Content-Length: {}\r\n", ROSTER_XML.len()).as_bytes());
    out.extend_from_slice(b"Connection: close\r\n");
    out.extend_from_slice(b"\r\n");
    if method.carries_body() {
        out.extend_from_slice(ROSTER_XML);
    }
    out
}

/// `Date:` in the oracle's format, from a Unix timestamp.
///
/// Implemented rather than pulled from a date crate to keep this crate
/// dependency-free, matching the protocol layer's constraint. Civil-date
/// conversion is the standard days-from-epoch algorithm.
pub fn http_date(unix_secs: i64) -> String {
    const DAYS: [&str; 7] = ["Mon", "Tue", "Wed", "Thu", "Fri", "Sat", "Sun"];
    const MONTHS: [&str; 12] = [
        "Jan", "Feb", "Mar", "Apr", "May", "Jun", "Jul", "Aug", "Sep", "Oct", "Nov", "Dec",
    ];
    let days = unix_secs.div_euclid(86_400);
    let secs = unix_secs.rem_euclid(86_400);
    // 1970-01-01 was a Thursday (index 3).
    let dow = DAYS[(days + 3).rem_euclid(7) as usize];

    // days-from-civil, inverted (Howard Hinnant's algorithm).
    let z = days + 719_468;
    let era = z.div_euclid(146_097);
    let doe = z.rem_euclid(146_097);
    let yoe = (doe - doe / 1460 + doe / 36_524 - doe / 146_096) / 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 };

    format!(
        "{dow}, {d:02} {mon} {y} {h:02}:{mi:02}:{s:02} GMT",
        mon = MONTHS[(m - 1) as usize],
        h = secs / 3600,
        mi = (secs % 3600) / 60,
        s = secs % 60
    )
}

#[cfg(test)]
mod tests {
    use super::*;

    #[test]
    fn the_body_is_the_oracles_67_bytes() {
        assert_eq!(ROSTER_XML.len(), 67);
        assert!(ROSTER_XML.starts_with(b"<?xml version=\"1.0\" encoding=\"utf-8\"?>"));
        assert!(ROSTER_XML.ends_with(b"<rosterupdate version=\"0\"/>\n"));
    }

    #[test]
    fn only_get_carries_the_body() {
        let d = "Tue, 11 Aug 2026 05:15:13 GMT";
        let get = roster_response(Method::Get, ORACLE_SERVER, d);
        let head = roster_response(Method::Head, ORACLE_SERVER, d);
        let post = roster_response(Method::Post, ORACLE_SERVER, d);
        assert_eq!(get.len(), head.len() + ROSTER_XML.len());
        assert_eq!(head, post, "the oracle answers POST exactly as HEAD");
    }

    /// The quirk, asserted so a future "cleanup" has to argue with a test.
    #[test]
    fn a_bodiless_response_still_advertises_the_body_length() {
        let r = roster_response(Method::Head, ORACLE_SERVER, "Tue, 11 Aug 2026 05:15:13 GMT");
        let text = String::from_utf8_lossy(&r);
        assert!(text.contains("Content-Length: 67"), "{text}");
        assert!(text.ends_with("\r\n\r\n"), "no body may follow");
    }

    #[test]
    fn header_order_matches_basehttprequesthandler() {
        let r = roster_response(Method::Get, ORACLE_SERVER, "Tue, 11 Aug 2026 05:15:13 GMT");
        let text = String::from_utf8_lossy(&r);
        let order: Vec<&str> = [
            "Server:",
            "Date:",
            "Content-Type:",
            "Content-Length:",
            "Connection:",
        ]
        .iter()
        .map(|h| text.find(h).map(|_| *h).unwrap_or("MISSING"))
        .collect();
        assert!(!order.contains(&"MISSING"), "{text}");
        let positions: Vec<usize> = order.iter().map(|h| text.find(h).unwrap()).collect();
        let mut sorted = positions.clone();
        sorted.sort_unstable();
        assert_eq!(
            positions, sorted,
            "headers out of the oracle's order:\n{text}"
        );
        assert!(
            text.starts_with("HTTP/1.0 200 OK\r\n"),
            "must be HTTP/1.0: {text}"
        );
    }

    #[test]
    fn methods_parse_and_unknown_ones_are_refused() {
        assert_eq!(Method::parse("GET /x HTTP/1.1"), Some(Method::Get));
        assert_eq!(Method::parse("HEAD /x HTTP/1.1"), Some(Method::Head));
        assert_eq!(Method::parse("POST /x HTTP/1.1"), Some(Method::Post));
        // Not reproduced on purpose: the oracle answers these with its own 501.
        assert_eq!(Method::parse("PUT /x HTTP/1.1"), None);
        assert_eq!(Method::parse(""), None);
    }

    #[test]
    fn http_date_matches_the_oracles_format() {
        // 1786425313 == Tue, 11 Aug 2026 05:15:13 GMT, the captured fixture time.
        assert_eq!(http_date(1_786_425_313), "Tue, 11 Aug 2026 05:15:13 GMT");
        assert_eq!(http_date(0), "Thu, 01 Jan 1970 00:00:00 GMT");
        // A leap day, because the civil-date maths is the only real logic here.
        assert_eq!(http_date(1_709_164_800), "Thu, 29 Feb 2024 00:00:00 GMT");
    }
}
