//! Request-body handling shared by OpenFUT transport hosts.
//!
//! # Why this is its own crate
//!
//! Draining the request body is not a detail. Answering a client while it is
//! still sending leaves unread data in the receive queue, and Linux turns the
//! subsequent close into an **RST rather than a FIN**. That divergence is
//! invisible to any byte-comparison of the response, and it cost the redirector
//! two live gate attempts before it was found.
//!
//! Once a second host needed the same behaviour, the choice was to share the
//! code or copy it. The same reasoning that produced `openfut-tls` applies:
//! behaviour that must be identical across hosts should have one implementation.
//!
//! # Scope — deliberately narrow
//!
//! Only the part that was **byte-identical** in both hosts is here. The two
//! head-reading loops were *not* identical and are deliberately left where they
//! are:
//!
//! | | redirector | roster |
//! |---|---|---|
//! | head cap | 65536 | 16384 |
//! | read chunk | 4096 | 1024 |
//! | on read error | abort the connection | proceed if any bytes arrived |
//!
//! Those differences may well be accidental, but each host is gate-proven with
//! the values it has. Unifying them would be a behaviour change wearing a
//! refactor's clothes, and this project has already paid for that mistake —
//! the fix is evidence, not tidiness. If they should converge, that is a
//! separate change with its own gate.
//!
//! This crate has no dependencies, knows nothing about TLS, and knows nothing
//! about any game.

use std::io::Read;

/// What happened when the request body was drained.
///
/// Reported per connection because "the client sent a body we never read" is
/// exactly the class of divergence a response-bytes comparison cannot see.
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum BodyRead {
    /// No `Content-Length` header — nothing to drain.
    None,
    Complete(usize),
    Short {
        got: usize,
        want: usize,
    },
}

impl BodyRead {
    /// One-line description for a log or a test failure message.
    pub fn describe(self) -> String {
        match self {
            BodyRead::None => "none".to_string(),
            BodyRead::Complete(n) => format!("{n}B complete"),
            BodyRead::Short { got, want } => format!("{got}B of {want}B SHORT"),
        }
    }
}

/// Parse `Content-Length` from the head already in `buf` and read the rest of
/// the body, appending it to `buf`.
///
/// `buf` must already contain everything read so far, including the `\r\n\r\n`
/// terminator and any body bytes that arrived with it.
///
/// Mirrors the Python oracle: header lookup is case-insensitive, and a client
/// that stops early ends the read rather than hanging until the socket timeout.
pub fn drain_body<S: Read>(stream: &mut S, buf: &mut Vec<u8>) -> BodyRead {
    let Some(sep) = buf.windows(4).position(|w| w == b"\r\n\r\n") else {
        return BodyRead::None;
    };
    let head = String::from_utf8_lossy(&buf[..sep]).to_string();
    let Some(want) = head
        .lines()
        .find(|l| l.to_ascii_lowercase().starts_with("content-length"))
        .and_then(|l| l.split(':').nth(1))
        .and_then(|v| v.trim().parse::<usize>().ok())
    else {
        return BodyRead::None;
    };

    let mut got = buf.len() - (sep + 4);
    let mut chunk = [0u8; 4096];
    while got < want {
        match stream.read(&mut chunk) {
            Ok(0) | Err(_) => return BodyRead::Short { got, want },
            Ok(n) => {
                buf.extend_from_slice(&chunk[..n]);
                got += n;
            }
        }
    }
    BodyRead::Complete(got)
}

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

    #[test]
    fn no_content_length_means_nothing_to_drain() {
        let mut buf = b"GET / HTTP/1.1\r\nHost: x\r\n\r\n".to_vec();
        let mut rest: &[u8] = b"";
        assert_eq!(drain_body(&mut rest, &mut buf), BodyRead::None);
    }

    #[test]
    fn an_unterminated_head_is_not_treated_as_a_body() {
        // No \r\n\r\n yet: there is no way to know where a body would start.
        let mut buf = b"POST / HTTP/1.1\r\nContent-Length: 4\r\n".to_vec();
        let mut rest: &[u8] = b"abcd";
        assert_eq!(drain_body(&mut rest, &mut buf), BodyRead::None);
    }

    #[test]
    fn a_body_already_fully_buffered_needs_no_further_read() {
        let mut buf = b"POST / HTTP/1.1\r\nContent-Length: 4\r\n\r\nabcd".to_vec();
        // An empty stream proves nothing more was read.
        let mut rest: &[u8] = b"";
        assert_eq!(drain_body(&mut rest, &mut buf), BodyRead::Complete(4));
    }

    #[test]
    fn a_body_split_across_reads_is_completed() {
        let mut buf = b"POST / HTTP/1.1\r\nContent-Length: 8\r\n\r\nab".to_vec();
        let mut rest: &[u8] = b"cdefgh";
        assert_eq!(drain_body(&mut rest, &mut buf), BodyRead::Complete(8));
        assert!(buf.ends_with(b"abcdefgh"));
    }

    /// The failure mode that matters: a client that stops early must end the
    /// read rather than block. Reported as Short so the host can log it.
    #[test]
    fn a_client_that_stops_early_is_reported_short() {
        let mut buf = b"POST / HTTP/1.1\r\nContent-Length: 10\r\n\r\nabc".to_vec();
        let mut rest: &[u8] = b"";
        assert_eq!(
            drain_body(&mut rest, &mut buf),
            BodyRead::Short { got: 3, want: 10 }
        );
    }

    #[test]
    fn content_length_is_matched_case_insensitively() {
        let mut buf = b"POST / HTTP/1.1\r\ncOnTeNt-LeNgTh: 4\r\n\r\n".to_vec();
        let mut rest: &[u8] = b"abcd";
        assert_eq!(drain_body(&mut rest, &mut buf), BodyRead::Complete(4));
    }

    #[test]
    fn a_malformed_content_length_is_ignored_rather_than_guessed() {
        let mut buf = b"POST / HTTP/1.1\r\nContent-Length: banana\r\n\r\n".to_vec();
        let mut rest: &[u8] = b"abcd";
        assert_eq!(drain_body(&mut rest, &mut buf), BodyRead::None);
    }

    /// A header whose NAME merely contains "content-length" must not be read as
    /// the real one. `starts_with` is what makes this safe; a `contains` would
    /// match `X-Original-Content-Length` and drain the wrong number of bytes.
    #[test]
    fn a_lookalike_header_is_not_mistaken_for_content_length() {
        let mut buf = b"POST / HTTP/1.1\r\nX-Original-Content-Length: 99\r\n\r\n".to_vec();
        let mut rest: &[u8] = b"";
        assert_eq!(drain_body(&mut rest, &mut buf), BodyRead::None);
    }

    #[test]
    fn describe_is_stable_for_logs() {
        assert_eq!(BodyRead::None.describe(), "none");
        assert_eq!(BodyRead::Complete(665).describe(), "665B complete");
        assert_eq!(
            BodyRead::Short { got: 3, want: 10 }.describe(),
            "3B of 10B SHORT"
        );
    }
}
