///|
/// The outcome of the ASGI 3.0 conformance harness: how many round-trip checks
/// ran, how many `passed`, and the names of any that `failures`. A clean run has
/// an empty `failures`. Reusable beyond the test suite — a server or framework
/// built on the seam can call `run_conformance` to self-verify the wiring.
pub(all) struct ConformanceReport {
  total : Int
  passed : Int
  failures : Array[String]
} derive(Eq)

///|
/// Whether every conformance check passed (no failures recorded).
pub fn ConformanceReport::ok(self : ConformanceReport) -> Bool {
  self.failures.length() == 0
}

///|
/// One of each `Event` variant, distinct payloads — the table the harness folds
/// over to prove `Event`'s derived equality discriminates every message in the
/// http / websocket / lifespan sets in both directions (no two variants collapse).
fn all_event_variants() -> Array[Event] {
  [
    HttpRequest(body=b"body", more_body=true),
    HttpDisconnect,
    HttpResponseStart(status=200, headers=[("a", "b")], trailers=false),
    HttpResponseBody(body=b"x", more_body=false),
    HttpResponseTrailers(headers=[("t", "v")], more_trailers=false),
    HttpResponsePush(path="/p", headers=[]),
    HttpResponsePathSend(path="/f"),
    HttpResponseZeroCopySend(
      fd=7,
      offset=Some(0),
      count=Some(16),
      more_body=false,
    ),
    HttpResponseDebug(info={ "trace": "abc" }.to_json()),
    HttpResponseEarlyHint(links=["; rel=preload"]),
    WebSocketConnect,
    WebSocketReceive(text=Some("hi"), bytes=None),
    WebSocketDisconnect(code=1000, reason=Some("bye")),
    WebSocketAccept(subprotocol=Some("chat"), headers=[]),
    WebSocketSendText("hi"),
    WebSocketSendBytes(b"hi"),
    WebSocketClose(code=1001, reason="bye"),
    WebSocketHttpResponseStart(status=403, headers=[]),
    WebSocketHttpResponseBody(body=b"no", more_body=false),
    LifespanStartup,
    LifespanShutdown,
    LifespanStartupComplete,
    LifespanStartupFailed(message="boom"),
    LifespanShutdownComplete,
    LifespanShutdownFailed(message="boom"),
  ]
}

///|
/// Run the ASGI 3.0 conformance harness: a table-driven battery that drives every
/// `Event` variant and every `Scope` field through `run_http`, `ws_run`, and the
/// `TestClient`, asserting round-trip fidelity — a value put in comes back
/// unchanged, and no two distinct events or scope shapes are confused — and a
/// negative table that asserts `validate_events` rejects every malformed ordering
/// (a body before the response starts, a websocket frame before the handshake, a
/// lifespan shutdown reply before startup, and the rest) with the exact
/// violation. Returns a `ConformanceReport` naming any check that failed, so the
/// suite (and any downstream server) can assert `ok()`.
pub fn run_conformance() -> ConformanceReport {
  let results : Array[(String, Bool)] = []
  let check = fn(name : String, cond : Bool) -> Unit {
    results.push((name, cond))
  }

  // A. every Event variant is self-equal and pairwise-distinct: derived equality
  //    discriminates all 25 messages, so none collapses into another.
  let evs = all_event_variants()
  check("event/count", evs.length() == 25)
  for i in 0.. resp.status == 401 && resp.body == b"denied"
    None => false
  }
  check("ws/deny-http", !s_deny.accepted && deny_ok)
  let s_close = echo.websocket(
    path="/ws",
    handler=WebSocketHandler::new(on_receive=fn(_m) {
      [SendText("bye"), Close(code=1000, reason="done")]
    }),
    send=[Text("first"), Text("second-should-be-ignored")],
    disconnect=None,
  )
  check("ws/app-close", s_close.messages == [Text("bye")] && s_close.closed)
  // ws_run_app escape hatch sees the full scope (subprotocols) and inbound stream.
  let s_app = echo.websocket_app(
    path="/ws",
    app=fn(scope : WebSocketScope, inbound : Array[Event]) {
      [
        WebSocketAccept(subprotocol=scope.subprotocols.get(0), headers=[
          ("x-frames", inbound.length().to_string()),
        ]),
      ]
    },
    subprotocols=["v1"],
    send=[Text("a"), Text("b")],
    disconnect=None,
  )
  check(
    "ws/run_app",
    s_app.accepted &&
    s_app.subprotocol == Some("v1") &&
    s_app.header("x-frames") == Some("3"),
  )

  // F. extension capability flags round-trip onto the scope; early-hint events
  //    are captured by the client.
  let ext = Extensions::none()
    .enable_push()
    .enable_trailers()
    .enable_pathsend()
    .enable_zerocopysend()
    .enable_early_hint()
    .enable_debug()
    .enable_websocket_http_response()
  check(
    "ext/flags",
    ext.http_response_push &&
    ext.http_response_trailers &&
    ext.http_response_pathsend &&
    ext.http_response_zerocopysend &&
    ext.http_response_early_hint &&
    ext.http_response_debug &&
    ext.websocket_http_response,
  )
  // Each enable_* advertises exactly its own extension and leaves the rest off —
  // the seven official ASGI extension keys, each independently toggled.
  check(
    "ext/zerocopysend-isolated",
    {
      let e = Extensions::none().enable_zerocopysend()
      e.http_response_zerocopysend &&
      !e.http_response_push &&
      !e.http_response_debug
    },
  )
  check(
    "ext/debug-isolated",
    {
      let e = Extensions::none().enable_debug()
      e.http_response_debug &&
      !e.http_response_zerocopysend &&
      !e.http_response_pathsend
    },
  )
  let eh_client = TestClient::from_app(fn(_req) {
    [
      HttpResponseEarlyHint(links=["; rel=preload"]),
      HttpResponseEarlyHint(links=[
        "; rel=preload", "; rel=preload",
      ]),
      HttpResponseStart(status=200, headers=[], trailers=false),
      HttpResponseBody(body=b"", more_body=false),
    ]
  })
  let eh = eh_client.get("/")
  check(
    "ext/early-hint",
    eh.early_hints.length() == 2 &&
    eh.early_hints[0] == ["; rel=preload"] &&
    eh.early_hints[1] == ["; rel=preload", "; rel=preload"],
  )

  // G. streaming response round-trip: early hints, chunked body, and trailers.
  let stream = StreamingResponse::new(
    status=200,
    chunks=[b"one ", b"two"],
    trailers=[("digest", "abc")],
    early_hints=[["; rel=preload"]],
  )
  let sclient = TestClient::from_stream(fn(_r) { stream })
  let sr = sclient.get("/")
  check(
    "stream/roundtrip",
    sr.body == b"one two" &&
    sr.trailer("digest") == Some("abc") &&
    sr.early_hints == [["; rel=preload"]],
  )

  // H. spec_version negotiation compares numerically across the dotted version.
  let ver_cases : Array[(String, Int, Int, Bool)] = [
    ("2.5", 2, 0, true),
    ("2.5", 2, 5, true),
    ("2.5", 2, 6, false),
    ("2.5", 1, 9, true),
    ("2.5", 3, 0, false),
    ("2.10", 2, 9, true),
    ("2.0", 2, 0, true),
    ("2.0", 2, 1, false),
  ]
  for vc in ver_cases {
    let (sv, maj, minr, want) = vc
    let v : AsgiVersion = { version: "3.0", spec_version: sv }
    check(
      "spec_version/" + sv + ">=" + maj.to_string() + "." + minr.to_string(),
      v.at_least(major=maj, minor=minr) == want,
    )
  }
  // I. event-ordering validation. A well-formed outbound stream passes
  //    `validate_events`; a malformed one is rejected with the exact violation.
  //    This is the negative half of the harness — the ordering rules ASGI pins.
  let hscope = Http(HttpScope::new(http_method="GET", path="/"))
  let wscope = WebSocket(WebSocketScope::new(path="/ws"))
  let lscope = Lifespan(LifespanScope::new())
  let ok_start = HttpResponseStart(status=200, headers=[], trailers=false)
  let ok_start_tr = HttpResponseStart(status=200, headers=[], trailers=true)
  let ok_body = HttpResponseBody(body=b"x", more_body=false)
  let ok_trailers = HttpResponseTrailers(
    headers=[("t", "v")],
    more_trailers=false,
  )
  let valid_http : Array[(String, Array[Event])] = [
    ("start+body", [ok_start, ok_body]),
    (
      "hint+start+body",
      [HttpResponseEarlyHint(links=[""]), ok_start, ok_body],
    ),
    ("start+body+trailers", [ok_start_tr, ok_body, ok_trailers]),
    (
      "start+zerocopy",
      [
        ok_start,
        HttpResponseZeroCopySend(fd=3, offset=None, count=None, more_body=false),
      ],
    ),
    ("start+pathsend", [ok_start, HttpResponsePathSend(path="/f")]),
    (
      "push+start+body",
      [HttpResponsePush(path="/p", headers=[]), ok_start, ok_body],
    ),
    (
      "start+debug+body",
      [ok_start, HttpResponseDebug(info={ "k": "v" }.to_json()), ok_body],
    ),
    // http.response.debug is out-of-band and valid at any point, including
    // before the response starts.
    (
      "debug-before-start",
      [HttpResponseDebug(info={ "k": "v" }.to_json()), ok_start, ok_body],
    ),
  ]
  for vc in valid_http {
    check("order/http-valid/" + vc.0, validate_events(hscope, vc.1) == None)
  }
  let bad_http : Array[(String, Array[Event], EventOrderError)] = [
    ("body-before-start", [ok_body], BodyBeforeStart),
    (
      "zerocopy-before-start",
      [HttpResponseZeroCopySend(fd=1, offset=None, count=None, more_body=false)],
      BodyBeforeStart,
    ),
    ("dup-start", [ok_start, ok_start], DuplicateResponseStart),
    (
      "hint-after-start",
      [ok_start, HttpResponseEarlyHint(links=[])],
      EarlyHintAfterStart,
    ),
    ("body-after-complete", [ok_start, ok_body, ok_body], BodyAfterComplete),
    (
      "unexpected-trailers",
      [ok_start, ok_body, ok_trailers],
      UnexpectedTrailers,
    ),
    ("trailers-before-body", [ok_start_tr, ok_trailers], TrailersBeforeBody),
    ("missing-start", [], MissingResponseStart),
    (
      "incomplete-body",
      [ok_start, HttpResponseBody(body=b"x", more_body=true)],
      IncompleteBody,
    ),
    ("missing-trailers", [ok_start_tr, ok_body], MissingTrailers),
    (
      "event-after-complete",
      [ok_start, ok_body, HttpResponsePush(path="/p", headers=[])],
      EventAfterComplete,
    ),
    ("non-response-event", [WebSocketConnect], NonResponseEvent),
  ]
  for bc in bad_http {
    check("order/http-bad/" + bc.0, validate_events(hscope, bc.1) == Some(bc.2))
  }
  let ws_accept = WebSocketAccept(subprotocol=None, headers=[])
  let ws_deny_start = WebSocketHttpResponseStart(status=403, headers=[])
  let valid_ws : Array[(String, Array[Event])] = [
    ("accept", [ws_accept]),
    (
      "accept+send+close",
      [ws_accept, WebSocketSendText("hi"), WebSocketClose(code=1000, reason="")],
    ),
    ("close", [WebSocketClose(code=1003, reason="no")]),
    (
      "deny",
      [ws_deny_start, WebSocketHttpResponseBody(body=b"no", more_body=false)],
    ),
  ]
  for vc in valid_ws {
    check("order/ws-valid/" + vc.0, validate_events(wscope, vc.1) == None)
  }
  let bad_ws : Array[(String, Array[Event], EventOrderError)] = [
    ("frame-before-accept", [WebSocketSendText("x")], FrameBeforeAccept),
    ("dup-accept", [ws_accept, ws_accept], DuplicateAccept),
    (
      "event-after-close",
      [WebSocketClose(code=1000, reason=""), WebSocketSendText("x")],
      EventAfterClose,
    ),
    (
      "denial-body-before-start",
      [WebSocketHttpResponseBody(body=b"", more_body=false)],
      DenialBodyBeforeStart,
    ),
    ("deny-after-accept", [ws_accept, ws_deny_start], DenialAfterAccept),
    ("accept-after-deny", [ws_deny_start, ws_accept], AcceptAfterDenial),
    ("incomplete-denial", [ws_deny_start], IncompleteDenial),
    ("missing-reply", [], MissingHandshakeReply),
  ]
  for bc in bad_ws {
    check("order/ws-bad/" + bc.0, validate_events(wscope, bc.1) == Some(bc.2))
  }
  let valid_ls : Array[(String, Array[Event])] = [
    ("startup+shutdown", [LifespanStartupComplete, LifespanShutdownComplete]),
    ("startup-failed", [LifespanStartupFailed(message="boom")]),
    (
      "shutdown-failed",
      [LifespanStartupComplete, LifespanShutdownFailed(message="x")],
    ),
  ]
  for vc in valid_ls {
    check("order/ls-valid/" + vc.0, validate_events(lscope, vc.1) == None)
  }
  let bad_ls : Array[(String, Array[Event], EventOrderError)] = [
    (
      "shutdown-before-startup",
      [LifespanShutdownComplete],
      ShutdownBeforeStartup,
    ),
    (
      "dup-startup",
      [LifespanStartupComplete, LifespanStartupComplete],
      DuplicateLifespanReply,
    ),
    (
      "reply-after-cycle",
      [
        LifespanStartupComplete,
        LifespanShutdownComplete,
        LifespanShutdownComplete,
      ],
      DuplicateLifespanReply,
    ),
    ("non-response-event", [HttpDisconnect], NonResponseEvent),
  ]
  for bc in bad_ls {
    check("order/ls-bad/" + bc.0, validate_events(lscope, bc.1) == Some(bc.2))
  }
  // J. lifespan driver: startup/shutdown replies, in-place state seeding, the
  //    full cycle's ordering, and the failed-startup short-circuit ASGI pins.
  let boot_scope = LifespanScope::new()
  let boot = LifespanHandler::new(on_startup=fn(s) {
    s.state["ready"] = true.to_json()
    Complete
  })
  let ls_driven = Lifespan(boot_scope)
  let up = run_lifespan(boot, ls_driven, [LifespanStartup])
  check("lifespan/startup-complete", up == [LifespanStartupComplete])
  check("lifespan/state-seeded", boot_scope.state.get("ready") is Some(_))
  let cyc_scope = Lifespan(LifespanScope::new())
  let cyc = run_lifespan(boot, cyc_scope, [LifespanStartup, LifespanShutdown])
  check(
    "lifespan/full-cycle",
    cyc == [LifespanStartupComplete, LifespanShutdownComplete],
  )
  check("lifespan/cycle-valid", validate_events(cyc_scope, cyc) == None)
  let sfail = LifespanHandler::new(on_shutdown=fn(_s) {
    Failed(message="drain")
  })
  check(
    "lifespan/shutdown-failed",
    run_lifespan(sfail, Lifespan(LifespanScope::new()), [LifespanShutdown]) ==
    [LifespanShutdownFailed(message="drain")],
  )
  let ufail = LifespanHandler::new(on_startup=fn(_s) { Failed(message="boom") })
  check(
    "lifespan/startup-failed-short-circuits",
    run_lifespan(ufail, Lifespan(LifespanScope::new()), [
      LifespanStartup,
      LifespanShutdown,
    ]) ==
    [LifespanStartupFailed(message="boom")],
  )
  check(
    "lifespan/non-lifespan-scope",
    run_lifespan(boot, hscope, [LifespanStartup]).length() == 0,
  )

  // K. scope-aware http core: run_http_scoped hands the app the full HttpScope
  //    (state, root_path) the ergonomic Request drops, and drains the body the
  //    same way run_http_app does.
  let seen_state : Array[Bool] = []
  let seen_root : Array[String] = []
  let scoped = fn(sh : HttpScope, sbody : Bytes) -> Array[Event] {
    seen_state.push(sh.state.get("k") is Some(_))
    seen_root.push(sh.root_path)
    [
      HttpResponseStart(status=200, headers=[], trailers=false),
      HttpResponseBody(body=sbody, more_body=false),
    ]
  }
  let scoped_scope = Http(
    HttpScope::new(
      http_method="POST",
      path="/x",
      root_path="/mnt",
      state=Map([("k", Json::string("v"))]),
    ),
  )
  let scoped_out = run_http_scoped(scoped, scoped_scope, [
    HttpRequest(body=b"ab", more_body=true),
    HttpRequest(body=b"c", more_body=false),
  ])
  check("scoped/state-visible", seen_state == [true])
  check("scoped/root-visible", seen_root == ["/mnt"])
  check("scoped/body-drained", reassemble(scoped_out).body == b"abc")
  check(
    "scoped/non-http-scope",
    run_http_scoped(scoped, lscope, [LifespanStartup]).length() == 0,
  )

  // L. HTTP/2 pseudo-header lowering: a HEADERS block (pseudo-headers + ordinary
  //    fields in wire order) maps onto an HttpScope the way a conforming h2/h2c
  //    server consumes a frame — request line from the pseudo-headers, host
  //    synthesised from :authority, `:`-prefixed names stripped from the header
  //    list — and every malformed set is rejected with the exact violation.
  let h2 = HttpScope::from_h2_headers([
    (":method", "POST"),
    (":scheme", "https"),
    (":authority", "example.test"),
    (":path", "/items?page=2&q=x"),
    ("content-type", "application/json"),
    ("accept", "*/*"),
  ])
  check(
    "h2/lower",
    match h2 {
      Ok(s) =>
        s.http_method == "POST" &&
        s.scheme == "https" &&
        s.http_version == "2" &&
        s.path == "/items" &&
        s.raw_path == b"/items" &&
        s.query_string == b"page=2&q=x" &&
        s.asgi.spec_version == "2.5" &&
        // :authority becomes the leading host header; pseudo-headers are gone.
        s.headers ==
        [
          ("host", "example.test"),
          ("content-type", "application/json"),
          ("accept", "*/*"),
        ]
      Err(_) => false
    },
  )
  // :authority replaces a host header the peer also sent (RFC 7540 §8.1.2.3).
  let h2_host = HttpScope::from_h2_headers([
    (":method", "GET"),
    (":scheme", "http"),
    (":authority", "authority.test"),
    (":path", "/"),
    ("host", "stale.test"),
    ("x-trace", "1"),
  ])
  check(
    "h2/authority-wins",
    match h2_host {
      Ok(s) => s.headers == [("host", "authority.test"), ("x-trace", "1")]
      Err(_) => false
    },
  )
  // No :authority: ordinary headers (including any host) pass through untouched,
  // and http_version="3" is honoured for the shared HTTP/3 pseudo-header path.
  let h3 = HttpScope::from_h2_headers(
    [(":method", "GET"), (":scheme", "https"), (":path", "/p"), ("host", "h")],
    http_version="3",
  )
  check(
    "h2/no-authority",
    match h3 {
      Ok(s) => s.http_version == "3" && s.headers == [("host", "h")]
      Err(_) => false
    },
  )
  let bad_h2 : Array[(String, Array[(String, String)], Http2HeaderError)] = [
    ("missing-method", [(":scheme", "https"), (":path", "/")], MissingMethod),
    ("missing-scheme", [(":method", "GET"), (":path", "/")], MissingScheme),
    ("missing-path", [(":method", "GET"), (":scheme", "https")], MissingPath),
    (
      "empty-path",
      [(":method", "GET"), (":scheme", "https"), (":path", "")],
      EmptyPath,
    ),
    (
      "dup-method",
      [
        (":method", "GET"),
        (":method", "POST"),
        (":scheme", "h"),
        (":path", "/"),
      ],
      DuplicatePseudoHeader(":method"),
    ),
    (
      "unknown-pseudo",
      [(":method", "GET"), (":scheme", "h"), (":path", "/"), (":x", "y")],
      UnknownPseudoHeader(":x"),
    ),
    (
      "pseudo-after-regular",
      [(":method", "GET"), ("a", "b"), (":scheme", "h"), (":path", "/")],
      PseudoHeaderAfterRegular(":scheme"),
    ),
  ]
  for bc in bad_h2 {
    check(
      "h2/bad/" + bc.0,
      match HttpScope::from_h2_headers(bc.1) {
        Err(e) => e == bc.2
        Ok(_) => false
      },
    )
  }
  // The SEAM is transport-version agnostic: an http_version="2" request drives
  // the exact same synchronous core and yields the same response as "1.1".
  let ver_seen : Array[String] = []
  let ver_client = TestClient::new(fn(req) {
    ver_seen.push(req.path)
    Response::text("v2")
  })
  let v2 = ver_client.request(http_method="GET", path="/h2", http_version="2")
  check(
    "h2/seam-agnostic",
    v2.status == 200 && v2.text() == "v2" && ver_seen == ["/h2"],
  )
  // M. header wire codec: the seam types headers as latin-1 Strings, but the ASGI
  //    spec types them as byte strings. The boundary codec must carry every byte
  //    0x00..0xFF losslessly, so the black-box header I/O conforms to the spec for
  //    any header a server puts on or takes off the wire.
  let all_bytes = Buffer()
  for b = 0; b < 256; b = b + 1 {
    all_bytes.write_byte(b.to_byte())
  }
  let wire_in : Array[(Bytes, Bytes)] = [
    (b"x-raw", all_bytes.to_bytes()),
    (b"content-type", b"application/json"),
  ]
  let decoded = headers_from_wire(wire_in)
  check("header/latin1-roundtrip", headers_to_wire(decoded) == wire_in)
  check("header/latin1-name", decoded[1].0 == "content-type")
  // Encoding a seam String header and decoding it back is the identity too.
  check(
    "header/latin1-seam-roundtrip",
    headers_from_wire(headers_to_wire([("x-a", "1"), ("x-b", "café")])) ==
    [("x-a", "1"), ("x-b", "café")],
  )
  // N. legacy 2.0 double-callable convention: run_legacy — the sync core of the
  //    two-call shape double_to_single_callable lifts — yields exactly what a
  //    single-callable app yields, so a 3.0 server stays backward-compatible with a
  //    legacy 2.0 app.
  let legacy_scope = Http(HttpScope::new(http_method="POST", path="/l"))
  let legacy_inbound : Array[Event] = [HttpRequest(body=b"z", more_body=false)]
  let legacy_handler : Handler = fn(_r) { Response::text("ok") }
  let legacy_out = run_legacy(
    fn(sc) { fn(inb) { run_http(legacy_handler, sc, inb) } },
    legacy_scope,
    legacy_inbound,
  )
  check(
    "legacy/matches-single",
    legacy_out == run_http(legacy_handler, legacy_scope, legacy_inbound),
  )
  let failures : Array[String] = []
  for r in results {
    if !r.1 {
      failures.push(r.0)
    }
  }
  {
    total: results.length(),
    passed: results.length() - failures.length(),
    failures,
  }
}