///|
/// A server-side push promise captured by the `TestClient`: the pushed `path`
/// and the request headers the server would send for it (`http.response.push`).
pub(all) struct PushPromise {
  path : String
  headers : Array[(String, String)]
} derive(Eq)

///|
/// First push-request header matching `name`.
pub fn PushPromise::header(self : PushPromise, name : String) -> String? {
  for pair in self.headers {
    if pair.0 == name {
      return Some(pair.1)
    }
  }
  None
}

///|
/// A `http.response.zerocopysend` message captured by the `TestClient`: the open
/// file descriptor the app handed off, and the optional byte `offset` / `count`.
/// The sans-transport analog of the server performing the zero-copy send.
pub(all) struct ZeroCopySend {
  fd : Int
  offset : Int?
  count : Int?
} derive(Eq)

///|
/// The materialised result of driving an application in-process: the response
/// status and headers, the body reassembled from every `HttpResponseBody`
/// chunk, the trailing headers gathered from `HttpResponseTrailers`, any push
/// promises, the `pathsend` path if the app used the path-send extension, the
/// last `zerocopysend` handoff, every `debug` info payload, and the
/// `early_hints` (each a `103` message's `Link` values). The captured,
/// sans-transport analog of what a real client would observe.
pub(all) struct TestResponse {
  status : Int
  headers : Array[(String, String)]
  body : Bytes
  trailers : Array[(String, String)]
  pushes : Array[PushPromise]
  pathsend : String?
  zerocopysend : ZeroCopySend?
  debug : Array[Json]
  early_hints : Array[Array[String]]
} derive(Eq)

///|
/// First response header matching `name` (ASGI lowercased-name convention).
pub fn TestResponse::header(self : TestResponse, name : String) -> String? {
  for pair in self.headers {
    if pair.0 == name {
      return Some(pair.1)
    }
  }
  None
}

///|
/// First trailing header matching `name`.
pub fn TestResponse::trailer(self : TestResponse, name : String) -> String? {
  for pair in self.trailers {
    if pair.0 == name {
      return Some(pair.1)
    }
  }
  None
}

///|
/// The response body decoded as UTF-8 (lossy: invalid sequences become the
/// replacement character), for asserting on text responses.
pub fn TestResponse::text(self : TestResponse) -> String {
  @utf8.decode_lossy(self.body[:])
}

///|
/// The response body parsed as JSON. Raises `@json.ParseError` if the body is
/// not valid JSON, mirroring a real client's `.json()`.
pub fn TestResponse::json(self : TestResponse) -> Json raise {
  @json.parse(@utf8.decode_lossy(self.body[:]))
}

///|
/// Fold an outbound event stream into a `TestResponse`: `HttpResponseStart`
/// sets status/headers, every `HttpResponseBody` appends to the body, every
/// `HttpResponseTrailers` appends to the trailers, each `HttpResponsePush`
/// records a promise, `HttpResponsePathSend` records the served path, and every
/// `HttpResponseEarlyHint` records one `103` message's `Link` values. Other
/// events (websocket, lifespan) are ignored — the http response is what a client
/// observes.
fn reassemble(events : Array[Event]) -> TestResponse {
  let mut status = 0
  let mut headers : Array[(String, String)] = []
  let buf = Buffer()
  let trailers : Array[(String, String)] = []
  let pushes : Array[PushPromise] = []
  let mut pathsend : String? = None
  let mut zerocopysend : ZeroCopySend? = None
  let debug : Array[Json] = []
  let early_hints : Array[Array[String]] = []
  for ev in events {
    match ev {
      HttpResponseStart(status=s, headers=h, trailers=_) => {
        status = s
        headers = h
      }
      HttpResponseBody(body=b, more_body=_) => buf.write_bytes(b[:])
      HttpResponseTrailers(headers=h, more_trailers=_) =>
        for pair in h {
          trailers.push(pair)
        }
      HttpResponsePush(path=p, headers=h) =>
        pushes.push({ path: p, headers: h })
      HttpResponsePathSend(path=p) => pathsend = Some(p)
      HttpResponseZeroCopySend(fd~, offset~, count~, more_body=_) =>
        zerocopysend = Some({ fd, offset, count })
      HttpResponseDebug(info~) => debug.push(info)
      HttpResponseEarlyHint(links=l) => early_hints.push(l)
      _ => ()
    }
  }
  {
    status,
    headers,
    body: buf.to_bytes(),
    trailers,
    pushes,
    pathsend,
    zerocopysend,
    debug,
    early_hints,
  }
}

///|
/// An in-process application driver — the ASGI `TestClient` — built on the
/// synchronous `run_http_app` core, so it needs no socket and runs on every
/// backend. It builds a synthetic http scope, feeds the request body in as
/// `HttpRequest` events, runs the application, and reassembles the outbound
/// stream into a `TestResponse`. `app` is the event-emitting application; the
/// `new` / `from_stream` constructors adapt a `Handler` / `StreamHandler`.
pub(all) struct TestClient {
  app : (Request) -> Array[Event]
  root_path : String
  base_headers : Array[(String, String)]
  client : (String, Int)?
  server : (String, Int)?
  extensions : Extensions
}

///|
/// Build a `TestClient` from a raw event-emitting application (the shape
/// `run_http_app` drives). Defaults mirror a typical test harness: empty
/// `root_path`, no base headers, a `("testclient", 50000)` client peer, a
/// `("testserver", 80)` server, and no advertised extensions.
pub fn TestClient::from_app(
  app : (Request) -> Array[Event],
  root_path? : String = "",
  base_headers? : Array[(String, String)] = [],
  client? : (String, Int)? = Some(("testclient", 50000)),
  server? : (String, Int)? = Some(("testserver", 80)),
  extensions? : Extensions = Extensions::none(),
) -> TestClient {
  { app, root_path, base_headers, client, server, extensions }
}

///|
/// Build a `TestClient` from a unary `Handler`.
pub fn TestClient::new(
  handler : Handler,
  root_path? : String = "",
  base_headers? : Array[(String, String)] = [],
  client? : (String, Int)? = Some(("testclient", 50000)),
  server? : (String, Int)? = Some(("testserver", 80)),
  extensions? : Extensions = Extensions::none(),
) -> TestClient {
  TestClient::from_app(
    fn(req) { handler(req).events() },
    root_path~,
    base_headers~,
    client~,
    server~,
    extensions~,
  )
}

///|
/// Build a `TestClient` from a streaming `StreamHandler` (multi-chunk body,
/// optional trailers).
pub fn TestClient::from_stream(
  handler : StreamHandler,
  root_path? : String = "",
  base_headers? : Array[(String, String)] = [],
  client? : (String, Int)? = Some(("testclient", 50000)),
  server? : (String, Int)? = Some(("testserver", 80)),
  extensions? : Extensions = Extensions::none(),
) -> TestClient {
  TestClient::from_app(
    fn(req) { handler(req).events() },
    root_path~,
    base_headers~,
    client~,
    server~,
    extensions~,
  )
}

///|
/// Split a request target into its path and raw query string on the first `?`;
/// `"/a/b?x=1&y=2"` → `("/a/b", "x=1&y=2")`, `"/a/b"` → `("/a/b", "")`.
fn split_target(target : String) -> (String, String) {
  for i in 0.. Array[Event] {
  match chunks {
    Some(cs) =>
      if cs.length() == 0 {
        [HttpRequest(body=b"", more_body=false)]
      } else {
        let out : Array[Event] = []
        for i = 0; i < cs.length(); i = i + 1 {
          out.push(HttpRequest(body=cs[i], more_body=i != cs.length() - 1))
        }
        out
      }
    None => [HttpRequest(body~, more_body=false)]
  }
}

///|
/// Drive one request through the application and capture the response. Splits
/// `path` into path + query string, merges the client's `base_headers` before
/// the per-request `headers`, builds an http scope (carrying the client's
/// `root_path` / peers / advertised `extensions`), feeds the body in, runs the
/// app, and reassembles the outbound stream. Pass `chunks` to stream the request
/// body as several `HttpRequest` events.
pub fn TestClient::request(
  self : TestClient,
  http_method~ : String,
  path~ : String,
  headers? : Array[(String, String)] = [],
  body? : Bytes = b"",
  http_version? : String = "1.1",
  chunks? : Array[Bytes],
) -> TestResponse {
  let (p, q) = split_target(path)
  let scope = HttpScope::new(
    http_method~,
    path=p,
    http_version~,
    query_string=@utf8.encode(q),
    root_path=self.root_path,
    headers=[..self.base_headers, ..headers],
    client=self.client,
    server=self.server,
    extensions=self.extensions,
  )
  reassemble(run_http_app(self.app, Http(scope), body_events(body, chunks)))
}

///|
/// `GET path`.
pub fn TestClient::get(
  self : TestClient,
  path : String,
  headers? : Array[(String, String)] = [],
) -> TestResponse {
  self.request(http_method="GET", path~, headers~)
}

///|
/// `HEAD path`.
pub fn TestClient::head(
  self : TestClient,
  path : String,
  headers? : Array[(String, String)] = [],
) -> TestResponse {
  self.request(http_method="HEAD", path~, headers~)
}

///|
/// `DELETE path`.
pub fn TestClient::delete(
  self : TestClient,
  path : String,
  headers? : Array[(String, String)] = [],
) -> TestResponse {
  self.request(http_method="DELETE", path~, headers~)
}

///|
/// `POST path` with `body`.
pub fn TestClient::post(
  self : TestClient,
  path : String,
  body? : Bytes = b"",
  headers? : Array[(String, String)] = [],
) -> TestResponse {
  self.request(http_method="POST", path~, headers~, body~)
}

///|
/// `PUT path` with `body`.
pub fn TestClient::put(
  self : TestClient,
  path : String,
  body? : Bytes = b"",
  headers? : Array[(String, String)] = [],
) -> TestResponse {
  self.request(http_method="PUT", path~, headers~, body~)
}

///|
/// `PATCH path` with `body`.
pub fn TestClient::patch(
  self : TestClient,
  path : String,
  body? : Bytes = b"",
  headers? : Array[(String, String)] = [],
) -> TestResponse {
  self.request(http_method="PATCH", path~, headers~, body~)
}

///|
/// The materialised result of driving a WebSocket connection in-process: whether
/// the handshake was `accepted`, the negotiated `subprotocol` and `accept_headers`,
/// every server→client message (`messages`), whether the server `closed` and with
/// what `close_code` / `close_reason`, and — when the handshake was denied with
/// the `websocket.http.response` extension — the captured HTTP `denial` response.
/// The WebSocket analog of `TestResponse`.
pub(all) struct WsTestSession {
  accepted : Bool
  subprotocol : String?
  accept_headers : Array[(String, String)]
  messages : Array[WsMessage]
  closed : Bool
  close_code : Int?
  close_reason : String?
  denial : TestResponse?
} derive(Eq)

///|
/// First accept-response header matching `name`.
pub fn WsTestSession::header(self : WsTestSession, name : String) -> String? {
  for pair in self.accept_headers {
    if pair.0 == name {
      return Some(pair.1)
    }
  }
  None
}

///|
/// The server→client messages that were text frames, decoded to their strings
/// (binary frames are skipped). The ergonomic assertion target for an echo/chat
/// exchange.
pub fn WsTestSession::texts(self : WsTestSession) -> Array[String] {
  let out : Array[String] = []
  for m in self.messages {
    match m {
      Text(t) => out.push(t)
      Binary(_) => ()
    }
  }
  out
}

///|
/// Fold an outbound WebSocket event stream into a `WsTestSession`:
/// `WebSocketAccept` records the handshake and negotiated subprotocol/headers,
/// every `WebSocketSendText` / `WebSocketSendBytes` appends a message,
/// `WebSocketClose` records the close, and a `WebSocketHttpResponseStart` +
/// `WebSocketHttpResponseBody` pair reconstructs the denial HTTP response. Other
/// events are ignored.
fn reassemble_ws(events : Array[Event]) -> WsTestSession {
  let mut accepted = false
  let mut subprotocol : String? = None
  let mut accept_headers : Array[(String, String)] = []
  let messages : Array[WsMessage] = []
  let mut closed = false
  let mut close_code : Int? = None
  let mut close_reason : String? = None
  let mut had_denial = false
  let mut denial_status = 0
  let mut denial_headers : Array[(String, String)] = []
  let denial_body = Buffer()
  for ev in events {
    match ev {
      WebSocketAccept(subprotocol=sp, headers=h) => {
        accepted = true
        subprotocol = sp
        accept_headers = h
      }
      WebSocketSendText(t) => messages.push(Text(t))
      WebSocketSendBytes(b) => messages.push(Binary(b))
      WebSocketClose(code~, reason~) => {
        closed = true
        close_code = Some(code)
        close_reason = Some(reason)
      }
      WebSocketHttpResponseStart(status~, headers~) => {
        had_denial = true
        denial_status = status
        denial_headers = headers
      }
      WebSocketHttpResponseBody(body~, more_body=_) =>
        denial_body.write_bytes(body[:])
      _ => ()
    }
  }
  let denial : TestResponse? = if had_denial {
    Some({
      status: denial_status,
      headers: denial_headers,
      body: denial_body.to_bytes(),
      trailers: [],
      pushes: [],
      pathsend: None,
      zerocopysend: None,
      debug: [],
      early_hints: [],
    })
  } else {
    None
  }
  {
    accepted,
    subprotocol,
    accept_headers,
    messages,
    closed,
    close_code,
    close_reason,
    denial,
  }
}

///|
/// Materialise client→server messages into the inbound event stream a server
/// hands a WebSocket app: a leading `WebSocketConnect`, one `WebSocketReceive`
/// per message (text or binary), and a terminating `WebSocketDisconnect` when a
/// `disconnect` code is given.
fn ws_inbound(messages : Array[WsMessage], disconnect : Int?) -> Array[Event] {
  let out : Array[Event] = [WebSocketConnect]
  for m in messages {
    match m {
      Text(t) => out.push(WebSocketReceive(text=Some(t), bytes=None))
      Binary(b) => out.push(WebSocketReceive(text=None, bytes=Some(b)))
    }
  }
  match disconnect {
    Some(c) => out.push(WebSocketDisconnect(code=c, reason=None))
    None => ()
  }
  out
}

///|
/// Build the synthetic `WebSocketScope` the WebSocket driver runs against: the
/// path/query split off `path`, the client's `base_headers` merged before the
/// per-connection `headers`, the offered `subprotocols`, the websocket `asgi`
/// handshake (sub-spec `2.5`), and the client's `root_path` / peers / advertised
/// `extensions`.
fn TestClient::ws_scope(
  self : TestClient,
  path : String,
  headers : Array[(String, String)],
  subprotocols : Array[String],
) -> WebSocketScope {
  let (p, q) = split_target(path)
  WebSocketScope::new(
    path=p,
    query_string=@utf8.encode(q),
    root_path=self.root_path,
    headers=[..self.base_headers, ..headers],
    client=self.client,
    server=self.server,
    subprotocols~,
    asgi=AsgiVersion::websocket(),
    extensions=self.extensions,
  )
}

///|
/// Drive a `WebSocketHandler` through a full connection in-process and capture
/// the result. Opens a `WebSocketConnect`, feeds each `send` message in as a
/// `WebSocketReceive`, then (unless `disconnect` is `None`) a
/// `WebSocketDisconnect` with the given close code, runs the handler through the
/// synchronous `ws_run` core, and reassembles the server's outbound frames into a
/// `WsTestSession`. No socket — testable on every backend, the WebSocket
/// counterpart of `request`.
pub fn TestClient::websocket(
  self : TestClient,
  path~ : String,
  handler~ : WebSocketHandler,
  send? : Array[WsMessage] = [],
  headers? : Array[(String, String)] = [],
  subprotocols? : Array[String] = [],
  disconnect? : Int? = Some(1000),
) -> WsTestSession {
  let scope = self.ws_scope(path, headers, subprotocols)
  reassemble_ws(ws_run(handler, WebSocket(scope), ws_inbound(send, disconnect)))
}

///|
/// Drive a general `(WebSocketScope, Array[Event]) -> Array[Event]` application
/// through a full connection in-process. Like `websocket`, but for an app that
/// consumes the whole inbound stream directly (the `ws_run_app` escape hatch)
/// rather than the connect/receive/disconnect fold.
pub fn TestClient::websocket_app(
  self : TestClient,
  path~ : String,
  app~ : (WebSocketScope, Array[Event]) -> Array[Event],
  send? : Array[WsMessage] = [],
  headers? : Array[(String, String)] = [],
  subprotocols? : Array[String] = [],
  disconnect? : Int? = Some(1000),
) -> WsTestSession {
  let scope = self.ws_scope(path, headers, subprotocols)
  reassemble_ws(ws_run_app(app, WebSocket(scope), ws_inbound(send, disconnect)))
}