///|
/// Async fetcher signature used by the CORS gate. The actual network
/// roundtrip is driven by the caller-supplied closure, which may
/// either dispatch a real fetch (in the live path) or return a canned
/// response (in wbtests).
type ResponseFetcher = async (String, @http.FetchOptions) -> @http.HttpResponse raise @http.HttpError

///|
/// Sync OPTIONS preflight fetcher. The PreflightCache invokes this on
/// cache miss / expiry, never on a fresh hit. The cache contract is
/// sync (see `@http_cors.PreflightCache::get_or_validate`), so the
/// live wiring captures a `now`-bounded sync surface: a real network
/// preflight runs as an `async` call that the closure awaits before
/// returning the synthetic response.
type PreflightFetcher = (@http_cors.PreflightRequest) -> @http.HttpResponse

///|
/// Run a subresource fetch through the CORS gate:
///
/// 1. `@http_cors.classify_request` → Allow / PreflightRequired / Blocked.
/// 2. If `Blocked`, raise `CorsBlocked` without touching the network.
/// 3. If `PreflightRequired`, ask `cache.get_or_validate` to either
///    reuse a fresh entry or invoke `preflight_fetcher` once. A
///    preflight validation failure raises `PreflightFailed`.
/// 4. Dispatch the actual request via `fetcher`.
/// 5. Run `@http_cors.validate_actual_response` on the response and
///    raise `CorsBlocked` on Err. Same-origin responses skip the check
///    (the validator returns Ok early in that case).
///
/// Tests drive this seam directly by passing closures that return
/// canned `HttpResponse` values — no real network I/O required.
async fn script_fetch_with_cors(
  url~ : String,
  options~ : @http.FetchOptions,
  preflight_cache~ : @http_cors.PreflightCache,
  now~ : () -> Double,
  preflight_fetcher~ : PreflightFetcher,
  fetcher~ : ResponseFetcher,
) -> @http.HttpResponse raise @http.HttpError {
  let decision = @http_cors.classify_request(
    url~,
    origin=options.origin,
    mode=options.mode,
    http_method=options.http_method,
    headers=options.headers,
  )
  match decision {
    Blocked(reason) => raise @http.HttpError::CorsBlocked(reason)
    PreflightRequired(preflight_req) =>
      match
        preflight_cache.get_or_validate(preflight_req, now, preflight_fetcher) {
        Err(msg) => raise @http.HttpError::PreflightFailed(msg)
        Ok(_) => ()
      }
    Allow => ()
  }
  let response = fetcher(url, options)
  // NoCors responses are opaque per Fetch spec: the script tag can
  // still execute the body, but the page can't read it. We skip
  // validate_actual_response in that mode — the validator assumes a
  // CORS-mode request and would otherwise block every cross-origin
  //