//! Integration tests for `peel::http` against the mock server.
//!
//! Every test starts a fresh mock server on `127.0.0.1:<auto>`, points
//! a fresh `Client` at it, drives one or more requests, and asserts on
//! the parsed response. The mock server is dropped (and torn down) at
//! the end of each test.

use std::io::Read;
use std::time::Duration;

use peel::http::range::{parse_content_range, ContentRange};
use peel::http::Url;
use peel::http::{Client, ClientConfig, ClientError};
use peel::types::{ByteOffset, ByteRange};

#[path = "support/mod.rs"]
mod support;

use support::mock_server::{MockRequest, MockResponse, MockServer};

fn build_client() -> Client {
    let cfg = ClientConfig {
        timeout: Duration::from_secs(5),
        ..ClientConfig::default()
    };
    Client::with_config(cfg).expect("client constructs")
}

fn url(server: &MockServer, path: &str) -> Url {
    let s = format!("{}{path}", server.base_url());
    Url::parse(&s).expect("url parses")
}

// ---- HEAD --------------------------------------------------------------

#[test]
fn head_returns_status_and_headers() {
    let server = MockServer::start(|_req: &MockRequest, _n| MockResponse::Reply {
        status: 200,
        reason: "OK",
        headers: vec![
            ("Content-Length".into(), "1234".into()),
            ("ETag".into(), "\"v1\"".into()),
            ("Accept-Ranges".into(), "bytes".into()),
        ],
        body: Vec::new(),
    });

    let client = build_client();
    let r = client.head(&url(&server, "/foo")).expect("head ok");
    assert_eq!(r.status.code, 200);
    assert_eq!(r.headers.get("Content-Length"), Some("1234"));
    assert_eq!(r.headers.get("ETag"), Some("\"v1\""));
    assert_eq!(r.headers.get("Accept-Ranges"), Some("bytes"));
    assert_eq!(r.final_url.path(), "/foo");
}

#[test]
fn head_request_method_is_head_on_wire() {
    let server = MockServer::start(|_req, _n| MockResponse::ok(""));
    let client = build_client();
    let _ = client.head(&url(&server, "/")).expect("head ok");
    let reqs = server.snapshot_requests();
    assert_eq!(reqs.len(), 1);
    assert_eq!(reqs[0].method, "HEAD");
}

// ---- 200 GET -----------------------------------------------------------

#[test]
fn get_full_returns_body() {
    let payload = b"hello world".to_vec();
    let payload_for_handler = payload.clone();
    let server = MockServer::start(move |_req, _n| MockResponse::ok(payload_for_handler.clone()));

    let client = build_client();
    let mut resp = client.get_full(&url(&server, "/data")).expect("get ok");
    assert_eq!(resp.status.code, 200);
    let mut body = Vec::new();
    resp.body.read_to_end(&mut body).expect("read body");
    assert_eq!(body, payload);
}

#[test]
fn get_full_handles_empty_body() {
    let server = MockServer::start(|_req, _n| MockResponse::ok(""));
    let client = build_client();
    let mut resp = client.get_full(&url(&server, "/")).expect("get ok");
    let mut body = Vec::new();
    resp.body.read_to_end(&mut body).expect("read body");
    assert!(body.is_empty());
}

// ---- 206 Range ---------------------------------------------------------

#[test]
fn get_range_returns_206_and_correct_slice() {
    let full: Vec<u8> = (0u8..=255).cycle().take(10_000).collect();
    let full_for_handler = full.clone();
    let server = MockServer::start(move |req, _n| {
        let range_hdr = req.header("range").unwrap_or("");
        let after = range_hdr.strip_prefix("bytes=").unwrap_or(range_hdr);
        let (a, b) = after.split_once('-').unwrap_or((after, "0"));
        let a: usize = a.parse().unwrap_or(0);
        let b: usize = b.parse().unwrap_or(0);
        let slice = full_for_handler[a..=b].to_vec();
        MockResponse::Reply {
            status: 206,
            reason: "Partial Content",
            headers: vec![(
                "Content-Range".into(),
                format!("bytes {a}-{b}/{}", full_for_handler.len()),
            )],
            body: slice,
        }
    });

    let client = build_client();
    let r = ByteRange::new(ByteOffset::new(100), ByteOffset::new(300)).unwrap();
    let mut resp = client
        .get_range(&url(&server, "/d"), r)
        .expect("get_range ok");
    assert_eq!(resp.status.code, 206);

    let cr = parse_content_range(resp.headers.get("Content-Range").expect("CR")).expect("parse");
    assert_eq!(cr.first_byte(), 100);
    assert_eq!(cr.last_byte(), 299);
    assert_eq!(cr.total(), Some(10_000));

    let mut body = Vec::new();
    resp.body.read_to_end(&mut body).expect("read body");
    assert_eq!(body, full[100..=299]);
}

#[test]
fn get_range_rejects_200_when_range_was_sent() {
    let server = MockServer::start(|_req, _n| MockResponse::ok("ignored"));
    let client = build_client();
    let r = ByteRange::new(ByteOffset::new(0), ByteOffset::new(10)).unwrap();
    let err = client
        .get_range(&url(&server, "/"), r)
        .expect_err("must error on 200");
    match err {
        ClientError::UnexpectedStatus { status, .. } => assert_eq!(status, 200),
        other => panic!("unexpected {other:?}"),
    }
}

// ---- 301/302 redirects -------------------------------------------------

#[test]
fn follows_relative_redirect() {
    let server = MockServer::start(|req, _n| {
        if req.path == "/old" {
            MockResponse::Reply {
                status: 301,
                reason: "Moved Permanently",
                headers: vec![("Location".into(), "/new".into())],
                body: Vec::new(),
            }
        } else {
            MockResponse::ok("redirected")
        }
    });

    let client = build_client();
    let mut resp = client.get_full(&url(&server, "/old")).expect("get ok");
    assert_eq!(resp.status.code, 200);
    let mut body = Vec::new();
    resp.body.read_to_end(&mut body).expect("read");
    assert_eq!(body, b"redirected");

    let reqs = server.snapshot_requests();
    assert_eq!(reqs.len(), 2);
    assert_eq!(reqs[0].path, "/old");
    assert_eq!(reqs[1].path, "/new");
}

#[test]
fn redirect_loop_aborts_with_too_many_redirects() {
    let server = MockServer::start(|_req, _n| MockResponse::Reply {
        status: 302,
        reason: "Found",
        headers: vec![("Location".into(), "/loop".into())],
        body: Vec::new(),
    });

    let cfg = ClientConfig {
        max_redirects: 3,
        timeout: Duration::from_secs(5),
        ..ClientConfig::default()
    };
    let client = Client::with_config(cfg).expect("client");
    let err = client
        .get_full(&url(&server, "/loop"))
        .expect_err("must error");
    match err {
        ClientError::TooManyRedirects { limit } => assert_eq!(limit, 3),
        other => panic!("unexpected {other:?}"),
    }
}

#[test]
fn redirect_without_location_errors_cleanly() {
    let server = MockServer::start(|_req, _n| MockResponse::Reply {
        status: 301,
        reason: "Moved",
        headers: vec![],
        body: Vec::new(),
    });
    let client = build_client();
    let err = client.head(&url(&server, "/x")).expect_err("must error");
    match err {
        ClientError::MissingLocation { status } => assert_eq!(status, 301),
        other => panic!("unexpected {other:?}"),
    }
}

// ---- 404 / 503 ---------------------------------------------------------

#[test]
fn passes_through_404() {
    let server = MockServer::start(|_req, _n| MockResponse::Reply {
        status: 404,
        reason: "Not Found",
        headers: vec![],
        body: b"missing".to_vec(),
    });

    let client = build_client();
    let mut resp = client.get_full(&url(&server, "/nope")).expect("get ok");
    assert_eq!(resp.status.code, 404);
    let mut body = Vec::new();
    resp.body.read_to_end(&mut body).expect("read");
    assert_eq!(body, b"missing");
}

#[test]
fn passes_through_503() {
    let server = MockServer::start(|_req, _n| MockResponse::Reply {
        status: 503,
        reason: "Service Unavailable",
        headers: vec![("Retry-After".into(), "1".into())],
        body: Vec::new(),
    });

    let client = build_client();
    let resp = client.head(&url(&server, "/")).expect("head ok");
    assert_eq!(resp.status.code, 503);
    assert_eq!(resp.headers.get("Retry-After"), Some("1"));
}

// ---- Chunked transfer encoding ----------------------------------------

#[test]
fn parses_chunked_response() {
    // Build a chunked-encoded body the client must reassemble.
    let body =
        b"HTTP/1.1 200 OK\r\nTransfer-Encoding: chunked\r\n\r\n5\r\nhello\r\n7\r\n, world\r\n0\r\n\r\n";
    let server = MockServer::start(move |_req, _n| MockResponse::RawBytesThenClose(body.to_vec()));

    let client = build_client();
    let mut resp = client.get_full(&url(&server, "/")).expect("get ok");
    assert_eq!(resp.status.code, 200);
    let mut decoded = Vec::new();
    resp.body.read_to_end(&mut decoded).expect("read");
    assert_eq!(decoded, b"hello, world");
}

// ---- Connection drops --------------------------------------------------

#[test]
fn early_disconnect_returns_response_error() {
    let server = MockServer::start(|_req, _n| MockResponse::DropConnection);
    let client = build_client();
    let err = client
        .get_full(&url(&server, "/"))
        .expect_err("dropped conn must error");
    // hyper sees the connection drop before any frame and surfaces it
    // as a transport error.
    matches!(err, ClientError::Transport { .. });
}

#[test]
fn truncated_body_during_read_errors() {
    // Server promises 100 bytes but only sends 5 then closes.
    let raw = b"HTTP/1.1 200 OK\r\nContent-Length: 100\r\n\r\nhello";
    let server = MockServer::start(move |_req, _n| MockResponse::RawBytesThenClose(raw.to_vec()));
    let client = build_client();
    let mut resp = client.get_full(&url(&server, "/")).expect("headers ok");
    let mut body = Vec::new();
    // hyper raises a transport error mid-stream once it observes the
    // declared Content-Length cannot be satisfied. The exact
    // `ErrorKind` surfaced depends on the hyper version (today it's
    // wrapped via `io::Error::other`, so it is `Other`); previously
    // the hand-rolled parser emitted `UnexpectedEof`. We assert only
    // that the truncation surfaced as an error and that the partial
    // bytes received are present in `body`.
    let _err = resp.body.read_to_end(&mut body).expect_err("must error");
    assert!(body.starts_with(b"hello"));
}

// ---- Body-idle timeout ------------------------------------------------

#[test]
fn body_idle_timeout_surfaces_as_io_error() {
    // Server sends headers + 5 bytes of body, then sleeps for several
    // seconds without sending the rest. With a sub-second
    // `body_idle_timeout` the client must raise a body-IO error
    // instead of waiting for the stalled stream.
    let server = MockServer::start(|_req, _n| MockResponse::StallAfterPartialBody {
        status: 200,
        reason: "OK",
        headers: vec![],
        partial_body: b"hello".to_vec(),
        remaining: 95,
        stall: Duration::from_secs(5),
    });
    let cfg = ClientConfig {
        body_idle_timeout: Duration::from_millis(200),
        timeout: Duration::from_secs(5),
        ..ClientConfig::default()
    };
    let client = Client::with_config(cfg).expect("client");
    let started = std::time::Instant::now();
    let mut resp = client.get_full(&url(&server, "/")).expect("headers ok");
    let mut body = Vec::new();
    // The first read returns the partial body (5 bytes); subsequent
    // reads block on `body.frame()` until the idle deadline trips and
    // surfaces as `io::Error`. `read_to_end` propagates the error.
    let err = resp
        .body
        .read_to_end(&mut body)
        .expect_err("stalled body must error");
    let elapsed = started.elapsed();
    assert!(body.starts_with(b"hello"), "partial body lost: {body:?}");
    assert!(
        elapsed < Duration::from_secs(2),
        "client waited {elapsed:?}, did not honor idle deadline",
    );
    // The synthesized error is plain io; just sanity-check it surfaced.
    let msg = err.to_string();
    assert!(
        msg.contains("stalled") || msg.contains("idle") || msg.contains("frame"),
        "expected stall-flavored message, got {msg:?}",
    );
}

#[test]
fn body_idle_timeout_zero_disables_deadline() {
    // Setting `body_idle_timeout` to zero opts back into the
    // pre-fix behaviour: the read does *not* time out client-side and
    // unblocks only when the connection drops or the stall completes.
    // The mock's stall is short so the test still finishes promptly.
    let server = MockServer::start(|_req, _n| MockResponse::StallAfterPartialBody {
        status: 200,
        reason: "OK",
        headers: vec![],
        partial_body: b"hi".to_vec(),
        remaining: 10,
        stall: Duration::from_millis(300),
    });
    let cfg = ClientConfig {
        body_idle_timeout: Duration::ZERO,
        timeout: Duration::from_secs(5),
        ..ClientConfig::default()
    };
    let client = Client::with_config(cfg).expect("client");
    let started = std::time::Instant::now();
    let mut resp = client.get_full(&url(&server, "/")).expect("headers ok");
    let mut body = Vec::new();
    // Eventually errors when the mock drops the connection, but
    // *only* after the stall elapsed.
    let _ = resp.body.read_to_end(&mut body);
    let elapsed = started.elapsed();
    assert!(body.starts_with(b"hi"));
    assert!(
        elapsed >= Duration::from_millis(250),
        "with timeout disabled the client should have waited for the mock stall, got {elapsed:?}",
    );
}

// ---- Body-throughput watchdog -----------------------------------------

#[test]
fn body_throughput_watchdog_aborts_trickle_streams() {
    // Server drips out 8 KiB total in 64-byte chunks, one chunk every
    // 50 ms — sustained ~1.3 KiB/s. The per-frame idle deadline stays
    // happy (frames arrive every 50 ms), but the throughput watchdog
    // configured at 32 KiB/s over a 500 ms window should fire and
    // return an error before the body completes naturally.
    let body = vec![0xABu8; 8 * 1024];
    let server = MockServer::start(move |_req, _n| MockResponse::DripBody {
        status: 200,
        reason: "OK",
        headers: vec![],
        body: body.clone(),
        bytes_per_chunk: 64,
        interval: Duration::from_millis(50),
    });
    let cfg = ClientConfig {
        body_idle_timeout: Duration::from_secs(5),
        body_min_throughput: 32 * 1024,
        body_throughput_window: Duration::from_millis(500),
        body_throughput_grace: Duration::from_millis(200),
        timeout: Duration::from_secs(5),
        ..ClientConfig::default()
    };
    let client = Client::with_config(cfg).expect("client");
    let started = std::time::Instant::now();
    let mut resp = client.get_full(&url(&server, "/")).expect("headers ok");
    let mut buf = Vec::new();
    let err = resp
        .body
        .read_to_end(&mut buf)
        .expect_err("trickle must error");
    let elapsed = started.elapsed();
    // The watchdog should fire well before the natural body deadline
    // (8 KiB / 64 B per 50 ms ≈ 6.4 s of dripping). Allow generous
    // slack but ensure we did not block for the full natural runtime.
    assert!(
        elapsed < Duration::from_secs(3),
        "client waited {elapsed:?} — watchdog did not fire promptly",
    );
    let msg = err.to_string();
    assert!(
        msg.contains("throughput") || msg.contains("below floor") || msg.contains("B/s"),
        "expected throughput-flavored message, got {msg:?}",
    );
}

#[test]
fn body_throughput_watchdog_zero_disables() {
    // Same drip pattern; with the watchdog disabled (min_throughput
    // = 0) the request must complete naturally with the full body
    // and no error.
    let body = vec![0xCDu8; 1024];
    let server = MockServer::start(move |_req, _n| MockResponse::DripBody {
        status: 200,
        reason: "OK",
        headers: vec![],
        body: body.clone(),
        bytes_per_chunk: 128,
        interval: Duration::from_millis(20),
    });
    let cfg = ClientConfig {
        body_idle_timeout: Duration::from_secs(5),
        body_min_throughput: 0,
        body_throughput_window: Duration::from_millis(50),
        body_throughput_grace: Duration::from_millis(0),
        timeout: Duration::from_secs(5),
        ..ClientConfig::default()
    };
    let client = Client::with_config(cfg).expect("client");
    let mut resp = client.get_full(&url(&server, "/")).expect("headers ok");
    let mut received = Vec::new();
    resp.body.read_to_end(&mut received).expect("body ok");
    assert_eq!(received.len(), 1024);
    assert!(received.iter().all(|b| *b == 0xCD));
}

#[test]
fn body_throughput_watchdog_passes_fast_streams() {
    // Sanity: a body that easily exceeds the floor must complete
    // without the watchdog erroring out, even with the watchdog on.
    let body = vec![0xEFu8; 32 * 1024];
    let server = MockServer::start(move |_req, _n| MockResponse::DripBody {
        status: 200,
        reason: "OK",
        headers: vec![],
        body: body.clone(),
        bytes_per_chunk: 4 * 1024,
        interval: Duration::from_millis(10),
    });
    let cfg = ClientConfig {
        body_idle_timeout: Duration::from_secs(5),
        body_min_throughput: 32 * 1024,
        body_throughput_window: Duration::from_millis(100),
        body_throughput_grace: Duration::from_millis(0),
        timeout: Duration::from_secs(5),
        ..ClientConfig::default()
    };
    let client = Client::with_config(cfg).expect("client");
    let mut resp = client.get_full(&url(&server, "/")).expect("headers ok");
    let mut received = Vec::new();
    resp.body
        .read_to_end(&mut received)
        .expect("fast body must succeed");
    assert_eq!(received.len(), 32 * 1024);
}

// ---- Connection pool ---------------------------------------------------

#[test]
fn head_responses_populate_idle_pool() {
    // The previous hand-rolled client exposed an internal idle-pool
    // size that this test asserted on. With the move to hyper-util,
    // pool inspection is no longer available — the legacy client
    // owns its own pool. We can still verify the *behavioral*
    // guarantee: two HEAD requests to the same host both succeed.
    // Connection reuse is exercised end-to-end by the keep-alive
    // and HTTP/2 multiplexing tests; this case just confirms the
    // pool does not break repeat requests.
    let server = MockServer::start(|_req, _n| MockResponse::ok(""));
    let client = build_client();
    let _ = client.head(&url(&server, "/a")).expect("head ok");
    let _ = client.head(&url(&server, "/b")).expect("head ok");
    assert_eq!(server.request_count(), 2);
}

#[test]
fn connection_close_keeps_pool_empty() {
    let server = MockServer::start(|_req, _n| MockResponse::Reply {
        status: 200,
        reason: "OK",
        headers: vec![("Connection".into(), "close".into())],
        body: Vec::new(),
    });
    let client = build_client();
    // The hyper-util pool is opaque; we can only verify the request
    // succeeded.
    let _ = client.head(&url(&server, "/")).expect("head ok");
}

// ---- ContentRange round-trip via full client path ----------------------

#[test]
fn content_range_round_trips_through_real_request() {
    let total = 4096u64;
    let server = MockServer::start(move |req, _n| {
        let r = req.header("range").unwrap_or("bytes=0-0");
        let after = r.strip_prefix("bytes=").unwrap_or(r);
        let (a, b) = after.split_once('-').unwrap_or((after, "0"));
        let a: u64 = a.parse().unwrap_or(0);
        let b: u64 = b.parse().unwrap_or(0);
        let body: Vec<u8> = (a..=b).map(|x| (x & 0xFF) as u8).collect();
        MockResponse::Reply {
            status: 206,
            reason: "Partial Content",
            headers: vec![("Content-Range".into(), format!("bytes {a}-{b}/{total}"))],
            body,
        }
    });
    let client = build_client();
    let r = ByteRange::new(ByteOffset::new(1024), ByteOffset::new(2048)).unwrap();
    let resp = client
        .get_range(&url(&server, "/"), r)
        .expect("get_range ok");
    let cr = parse_content_range(resp.headers.get("Content-Range").unwrap()).expect("parse");
    let parsed: ContentRange = cr;
    assert_eq!(parsed.first_byte(), 1024);
    assert_eq!(parsed.last_byte(), 2047);
    assert_eq!(parsed.total(), Some(total));
    assert_eq!(parsed.as_byte_range(), r);
}
