//! npro's h1 head parser against the heads of C's transcripts.
//!
//! The `h1-uri-*`, `h1-reqline-*` and `h1-header-past-limit` connections
//! are each one request to C's `sansio-uri` vhost, whose context limits a
//! GET's target to 33 bytes and a User-Agent to 16.  C either refuses the
//! request with a status, or its app answers 200 with the path lws decoded,
//! a newline and the urlargs.  So each says what C's parser made of its
//! request, and npro's must make the same of it: the same refusal, or the
//! same path and urlargs.
//!
//! The client connections' first rx is the response head.  The ones whose
//! framing C refuses (`h1-client-cl-junk` and the like) are refused after
//! the head, by what the framing headers say; the head itself parses, and
//! `npro_h1::fields` must read them as C does.

#![expect(
    unused_crate_dependencies,
    reason = "an integration test sees all of its crate's dependencies; this one uses npro-test and npro-h1"
)]

// held to clippy's rules for tests
#[cfg(test)]
mod h1_heads {
    use core::num::NonZeroU16;

    use npro_h1::fields::{content_length, transfer_encoding_is_chunked};
    use npro_h1::head::{Config, Head, Progress, Side};
    use npro_h1::table::DEFAULT_CAPACITY;
    use npro_h1::token::Token;
    use npro_test::{StepKind, Transcript, vendored};

    /// The `sansio-uri` context's limits.
    fn uri_config() -> Config {
        Config::new()
            .with_limit(Token::GetUri, NonZeroU16::new(33).unwrap())
            .with_limit(Token::UserAgent, NonZeroU16::new(16).unwrap())
    }

    fn first_rx(t: &Transcript) -> &[u8] {
        t.steps
            .iter()
            .find_map(|s| match &s.kind {
                StepKind::Rx(b) => Some(b.as_slice()),
                StepKind::Tx(_) | StepKind::AppRx(_) | StepKind::Close => None,
            })
            .unwrap()
    }

    fn first_tx(t: &Transcript) -> &[u8] {
        t.steps
            .iter()
            .find_map(|s| match &s.kind {
                StepKind::Tx(b) => Some(b.as_slice()),
                StepKind::Rx(_) | StepKind::AppRx(_) | StepKind::Close => None,
            })
            .unwrap()
    }

    /// What the request's answer says: its status, and for a 200, its body.
    fn answered(tx: &[u8]) -> (u16, Vec<u8>) {
        let status = core::str::from_utf8(&tx[9..12]).unwrap().parse().unwrap();
        let body = tx
            .windows(4)
            .position(|w| w == b"\r\n\r\n")
            .map(|n| tx[n..].strip_prefix(b"\r\n\r\n").unwrap().to_vec())
            .unwrap();
        (status, body)
    }

    /// What C's `sansio-uri` app answers a request with: the path, a newline
    /// and the urlargs, joined as `lws_hdr_copy()` joins them.
    fn uri_app_body<S: AsRef<[u8]> + AsMut<[u8]>>(h: &Head<S>) -> Vec<u8> {
        let t = h.table();
        let path = Token::METHODS
            .iter()
            .find_map(|m| t.first(*m))
            .unwrap()
            .to_vec();
        let mut args = vec![0u8; t.total_len(Token::UriArgs)];
        t.copy(Token::UriArgs, &mut args).unwrap();
        [path, b"\n".to_vec(), args].concat()
    }

    #[test]
    #[cfg_attr(miri, ignore = "reads the transcripts: native runs keep it")]
    fn requests_are_parsed_and_refused_as_c_does() {
        let mut seen = 0;
        for t in vendored().unwrap() {
            let c = t.case.as_str();
            if !(c.starts_with("h1-uri-")
                || c.starts_with("h1-reqline-")
                || c == "h1-header-past-limit")
            {
                continue;
            }
            seen += 1;
            let (status, body) = answered(first_tx(&t));
            let mut h = Head::new(vec![0u8; DEFAULT_CAPACITY], Side::Server, uri_config()).unwrap();
            match h.rx(first_rx(&t)) {
                Ok(Progress::Complete { .. }) => {
                    assert_eq!(status, 200, "{c}: C refused what npro took");
                    assert_eq!(
                        uri_app_body(&h).escape_ascii().to_string(),
                        body.escape_ascii().to_string(),
                        "{c}"
                    );
                }
                Ok(p) => panic!("{c}: {p:?}"),
                Err(r) => {
                    let a = r.answer().unwrap_or_else(|| panic!("{c}: {r} unanswered"));
                    assert_eq!(a.code(), status, "{c}: {r}");
                }
            }
        }
        assert_eq!(seen, 16, "the uri and request line cases");
    }

    /// A request split anywhere is the same request.
    #[test]
    #[cfg_attr(miri, ignore = "reads the transcripts: native runs keep it")]
    fn requests_split_anywhere_parse_the_same() {
        for t in vendored().unwrap() {
            if t.side != npro_test::Side::Server {
                continue;
            }
            let rx = first_rx(&t);
            let mut whole =
                Head::new(vec![0u8; DEFAULT_CAPACITY], Side::Server, uri_config()).unwrap();
            let want = whole.rx(rx);
            for at in 0..=rx.len() {
                let mut h =
                    Head::new(vec![0u8; DEFAULT_CAPACITY], Side::Server, uri_config()).unwrap();
                let (a, b) = rx.split_at(at);
                let got = match h.rx(a) {
                    Ok(Progress::More) => h.rx(b).map(|p| match p {
                        Progress::Complete { consumed } => Progress::Complete {
                            consumed: consumed + at,
                        },
                        p @ (Progress::More | Progress::Fallback) => p,
                    }),
                    other => other,
                };
                assert_eq!(got, want, "{} split at {at}", t.case);
                if want.is_ok() {
                    assert_eq!(h.table().used(), whole.table().used(), "{}", t.case);
                }
            }
        }
    }

    #[test]
    #[cfg_attr(miri, ignore = "reads the transcripts: native runs keep it")]
    fn response_heads_and_their_framing_as_c_reads_them() {
        let mut seen = 0;
        for t in vendored().unwrap() {
            let c = t.case.as_str();
            if !c.starts_with("h1-client-") || c.ends_with("digest-retry") {
                continue;
            }
            seen += 1;
            let mut h =
                Head::new(vec![0u8; DEFAULT_CAPACITY], Side::Client, Config::new()).unwrap();
            let rx = first_rx(&t);
            let Ok(Progress::Complete { consumed }) = h.rx(rx) else {
                panic!("{c}: the head did not parse");
            };
            let tb = h.table();
            let status = tb.first(Token::Http).unwrap();
            let cl: Vec<&[u8]> = tb.fragments(Token::ContentLength).collect();
            match c {
                "h1-client-get" => {
                    assert_eq!(status, b"200 OK");
                    assert_eq!(
                        content_length(cl[0]),
                        Ok(u64::try_from(rx.len().checked_sub(consumed).unwrap()).unwrap())
                    );
                }
                "h1-client-cl-junk" => assert!(content_length(cl[0]).is_err()),
                "h1-client-cl-twice" => assert_eq!(cl.len(), 2),
                "h1-client-te-list" => assert!(!transfer_encoding_is_chunked(tb)),
                "h1-client-head-chunked" => assert!(transfer_encoding_is_chunked(tb)),
                "h1-client-head-cl" => assert_eq!(content_length(cl[0]), Ok(10)),
                "h1-client-304-cl" => assert_eq!(status, b"304 Not Modified"),
                _ => panic!("{c}: a new client case: say what its head is"),
            }
        }
        assert_eq!(seen, 7, "the h1 client cases");
    }
}