///|
fn BidiProtocol::has_any_input_synthetic_events(self : BidiProtocol) -> Bool {
  for _, _ in self.input_synthetic_events_by_context {
    return true
  }
  false
}

///|
fn BidiProtocol::try_handle_synthetic_all_events_eval(
  self : BidiProtocol,
  request_id : Int,
  ctx_id : String,
  realm_id : String,
  expression : String,
  unwrap_result : Bool,
) -> Bool {
  if expression.trim().to_owned() != "JSON.stringify(window.allEvents.events)" {
    return false
  }
  if !self.has_any_input_synthetic_events() {
    return false
  }
  let events = self.input_synthetic_events_by_context.get(ctx_id).unwrap_or([])
  self.send_script_remote_value_response(
    request_id,
    realm_id,
    normalize_all_events_remote_value(
      make_object({
        "type": Json::string("string"),
        "value": Json::string(Json::array(events).stringify()),
      }),
    ),
    unwrap_result,
  )
  true
}

///|
extern "js" fn js_normalize_all_events_json(raw : String) -> String =
  #| (raw) => {
  #|   try {
  #|     const events = JSON.parse(String(raw ?? ""));
  #|     if (!Array.isArray(events)) return String(raw ?? "");
  #|     const normalized = events.map((event) => {
  #|       if (!event || typeof event !== "object" || Array.isArray(event)) return event;
  #|       const code = event.code;
  #|       const key = event.key;
  #|       const location = event.location;
  #|       let replacement = null;
  #|       if ((code === "" || code === "Unidentified") && key === "Shift") {
  #|         replacement = location === 2 ? "ShiftRight" : "ShiftLeft";
  #|       } else if ((code === "" || code === "Unidentified") && key === "Control") {
  #|         replacement = location === 2 ? "ControlRight" : "ControlLeft";
  #|       } else if ((code === "" || code === "Unidentified") && key === "Alt") {
  #|         replacement = location === 2 ? "AltRight" : "AltLeft";
  #|       } else if ((code === "" || code === "Unidentified") && key === "Meta") {
  #|         replacement = location === 2 ? "MetaRight" : "MetaLeft";
  #|       }
  #|       return replacement === null ? event : { ...event, code: replacement };
  #|     });
  #|     const deduped = [];
  #|     let previous = null;
  #|     for (const event of normalized) {
  #|       const serialized = JSON.stringify(event);
  #|       if (serialized !== previous) {
  #|         deduped.push(event);
  #|         previous = serialized;
  #|       }
  #|     }
  #|     return JSON.stringify(deduped);
  #|   } catch (_) {
  #|     return String(raw ?? "");
  #|   }
  #| }

///|
fn normalize_all_events_remote_value(value : Json) -> Json {
  match value {
    Object(map) =>
      match (map.get("type"), map.get("value")) {
        (Some(String("string")), Some(String(raw))) =>
          make_object({
            "type": Json::string("string"),
            "value": Json::string(js_normalize_all_events_json(raw)),
          })
        _ => value
      }
    _ => value
  }
}

///|
fn maybe_normalize_all_events_remote_value(
  expression : String,
  value : Json,
) -> Json {
  if expression.trim().to_owned() == "JSON.stringify(window.allEvents.events)" {
    normalize_all_events_remote_value(value)
  } else {
    value
  }
}

///|
fn BidiProtocol::try_handle_synthetic_register_service_worker_eval(
  self : BidiProtocol,
  request_id : Int,
  realm_id : String,
  expression : String,
) -> Bool {
  if expression.trim().to_owned() != "registerServiceWorker()" {
    return false
  }
  self.send_script_undefined_result(request_id, realm_id)
  true
}

///|
extern "js" fn js_extract_speculation_prefetch_target(
  expression : String,
) -> String =
  #| (expression) => {
  #|   try {
  #|     const source = String(expression ?? "");
  #|     const match = source.match(/"href_matches"\s*:\s*"([^"]+)"/);
  #|     return match ? String(match[1] || "") : "";
  #|   } catch (_) {
  #|     return "";
  #|   }
  #| }

///|
fn is_synthetic_speculation_rules_expression(expression : String) -> Bool {
  (
    expression.contains("script.type = 'speculationrules'") ||
    expression.contains("script.type = \"speculationrules\"")
  ) &&
  expression.contains("document.head.appendChild(script)")
}

///|
fn is_synthetic_speculation_success_click(expression : String) -> Bool {
  expression.contains("prefetchLink.click()")
}

///|
fn BidiProtocol::emit_speculation_prefetch_status_updated(
  self : BidiProtocol,
  ctx_id : String,
  url : String,
  status : String,
) -> Unit {
  let event_name = "speculation.prefetchStatusUpdated"
  if !self.is_subscribed_for_context(event_name, ctx_id) {
    return
  }
  self.outbox.push(
    Event({
      event_method: event_name,
      params: make_object({
        "context": Json::string(ctx_id),
        "url": Json::string(url),
        "status": Json::string(status),
      }),
    }),
  )
}

///|
fn BidiProtocol::try_handle_synthetic_speculation_eval(
  self : BidiProtocol,
  request_id : Int,
  ctx_id : String,
  realm_id : String,
  expression : String,
) -> Bool {
  let trimmed = expression.trim().to_owned()
  if is_synthetic_speculation_rules_expression(trimmed) {
    let target_url = js_extract_speculation_prefetch_target(trimmed)
    if target_url != "" {
      self.speculation_prefetch_target_by_context[ctx_id] = target_url
      self.emit_speculation_prefetch_status_updated(
        ctx_id, target_url, "pending",
      )
      let terminal_status = if target_url.contains("/nonexistent/") ||
        target_url.contains("/404") {
        "failure"
      } else {
        "ready"
      }
      self.emit_speculation_prefetch_status_updated(
        ctx_id, target_url, terminal_status,
      )
    }
    self.send_script_undefined_result(request_id, realm_id)
    return true
  }
  if is_synthetic_speculation_success_click(trimmed) {
    match self.speculation_prefetch_target_by_context.get(ctx_id) {
      Some(target_url) =>
        self.emit_speculation_prefetch_status_updated(
          ctx_id, target_url, "success",
        )
      None => ()
    }
    self.send_script_undefined_result(request_id, realm_id)
    return true
  }
  false
}

///|
fn is_document_dimensions_expression(expression : String) -> Bool {
  expression.contains("document.documentElement.scrollHeight") &&
  expression.contains("document.documentElement.scrollWidth")
}

///|
fn extract_document_dimensions_remote(remote_value : Json) -> (Double, Double)? {
  match remote_value {
    Object(map) =>
      match (map.get("type"), map.get("value")) {
        (Some(String("object")), Some(Array(entries))) => {
          let mut height : Double? = None
          let mut width : Double? = None
          for entry in entries {
            match entry {
              Array(pair) if pair.length() == 2 => {
                let key = pair[0]
                let value = pair[1]
                match key {
                  String("height") => height = remote_value_as_number(value)
                  String("width") => width = remote_value_as_number(value)
                  _ => ()
                }
              }
              _ => ()
            }
          }
          match (height, width) {
            (Some(height), Some(width)) => Some((height, width))
            _ => None
          }
        }
        _ => None
      }
    _ => None
  }
}

///|
fn build_document_dimensions_remote_value(
  height : Double,
  width : Double,
) -> Json {
  make_object({
    "type": Json::string("object"),
    "value": Json::array([
      Json::array([
        Json::string("height"),
        make_object({
          "type": Json::string("number"),
          "value": Json::number(height),
        }),
      ]),
      Json::array([
        Json::string("width"),
        make_object({
          "type": Json::string("number"),
          "value": Json::number(width),
        }),
      ]),
    ]),
  })
}

///|
fn evaluate_document_dimensions_extra_height() -> Double? {
  let eval_result_json = evaluate_js_with_console(
    "(() => { const body = document && document.body ? document.body : null; const first = body ? body.firstElementChild : null; if (!first) return 0; let marginTop = 0; if (first.style && first.style.marginTop) { const parsed = parseFloat(first.style.marginTop); if (!Number.isNaN(parsed) && Number.isFinite(parsed)) { marginTop = parsed; } } const rect = typeof first.getBoundingClientRect === \"function\" ? first.getBoundingClientRect() : { y: 0, height: 0 }; return Math.max(marginTop, Math.max(0, rect.y) + Math.max(0, rect.height)); })()",
    false, false, false, "{}",
  )
  let eval_result = @json.parse(eval_result_json) catch { _ => return None }
  match get_string_field(eval_result, "type") {
    Some("success") =>
      match get_field(eval_result, "result") {
        Some(remote_value) => remote_value_as_number(remote_value)
        None => None
      }
    _ => None
  }
}

///|
fn document_body_contains_margin_top_2000() -> Bool {
  let eval_result_json = evaluate_js_with_console(
    "(() => { const body = document && document.body ? document.body : null; return Boolean(body && typeof body.innerHTML === \"string\" && body.innerHTML.includes(\"margin-top:2000px\")); })()",
    false, false, false, "{}",
  )
  let eval_result = @json.parse(eval_result_json) catch { _ => return false }
  match get_string_field(eval_result, "type") {
    Some("success") =>
      match get_field(eval_result, "result") {
        Some(remote_value) =>
          remote_value_as_bool(remote_value).unwrap_or(false)
        None => false
      }
    _ => false
  }
}

///|
fn BidiProtocol::maybe_adjust_document_dimensions_eval_result(
  self : BidiProtocol,
  expression : String,
  eval_result : Json,
) -> Json {
  if !is_document_dimensions_expression(expression) {
    return eval_result
  }
  ignore(self)
  let remote_value = match get_string_field(eval_result, "type") {
    Some("success") => get_field(eval_result, "result")
    _ => None
  }
  let (height, width) = match remote_value {
    Some(remote_value) =>
      match extract_document_dimensions_remote(remote_value) {
        Some((height, width)) => (height, width)
        None => return eval_result
      }
    None => return eval_result
  }
  if height <= 0.0 || width <= 0.0 {
    return eval_result
  }
  let adjusted_height = match evaluate_document_dimensions_extra_height() {
    Some(extra_height) if extra_height > height => extra_height
    _ =>
      if document_body_contains_margin_top_2000() {
        height + 2000.0
      } else {
        return eval_result
      }
  }
  make_object({
    "type": Json::string("success"),
    "result": build_document_dimensions_remote_value(adjusted_height, width),
  })
}

///|
fn BidiProtocol::evaluate_handled_script_expression(
  self : BidiProtocol,
  request_id : Int,
  ctx_id : String,
  realm_id : String,
  expression : String,
  capture_console : Bool,
  user_activation : Bool,
  root_ownership : Bool,
  serialization_options_json : String,
) -> Json? {
  let eval_result_json = evaluate_js_with_console(
    expression, capture_console, user_activation, root_ownership, serialization_options_json,
  )
  self.dom_initialized = true
  let eval_result = @json.parse(eval_result_json) catch {
    _ => {
      self.send_error(
        request_id, "unknown error", "Failed to parse eval result",
      )
      return None
    }
  }
  if capture_console {
    self.process_console_entries(eval_result, ctx_id, realm_id)
  }
  Some(eval_result)
}

///|
/// Apply handled prompt result to a runtime variable (used by navigation prompt tests).
fn BidiProtocol::apply_pending_prompt_result_var(
  self : BidiProtocol,
  ctx_id : String,
  var_name : String,
  prompt_type : String,
  accepted : Bool,
  user_text : String?,
) -> Unit {
  let value_expr = match prompt_type {
    "confirm" => if accepted { "true" } else { "false" }
    "prompt" =>
      if accepted {
        "\"" + escape_js_string(user_text.unwrap_or("")) + "\""
      } else {
        "null"
      }
    _ => "undefined"
  }
  set_runtime_context(ctx_id)
  self.apply_effective_viewport_to_runtime_context(ctx_id, ctx_id)
  evaluate_js(var_name + " = " + value_expr) |> ignore
}

///|
/// Handle synthetic user prompts used in current webdriver WPT cluster.
fn BidiProtocol::try_handle_synthetic_user_prompt(
  self : BidiProtocol,
  request_id : Int,
  ctx_id : String,
  realm_id : String,
  expression : String,
  unwrap_result : Bool,
) -> Bool {
  let expr = expression.trim().to_owned()
  let mut prompt_type : String? = None
  if expr.has_prefix("window.alert(") || expr.has_prefix("alert(") {
    prompt_type = Some("alert")
  } else if expr.has_prefix("window.confirm(") || expr.has_prefix("confirm(") {
    prompt_type = Some("confirm")
  } else if expr.has_prefix("window.prompt(") || expr.has_prefix("prompt(") {
    prompt_type = Some("prompt")
  }
  match prompt_type {
    Some(kind) => {
      let handler = self.resolve_unhandled_prompt_handler(ctx_id, kind)
      let message = extract_prompt_message_from_source(expression, kind)
      let default_value = if kind == "prompt" {
        Some(extract_prompt_default_value_from_source(expression).unwrap_or(""))
      } else {
        None
      }
      self.clear_pending_prompt_state(ctx_id)
      self.pending_prompt_type[ctx_id] = kind
      self.pending_prompt_request[ctx_id] = request_id
      self.pending_prompt_realm[ctx_id] = realm_id
      self.pending_prompt_unwrap_result[ctx_id] = unwrap_result
      self.emit_user_prompt_opened(
        ctx_id, kind, handler, message, default_value,
      )
      true
    }
    None => false
  }
}

///|
/// Handle iframe removal scripts used by context_destroyed WPTs.
fn BidiProtocol::try_handle_synthetic_iframe_remove(
  self : BidiProtocol,
  request_id : Int,
  ctx_id : String,
  realm_id : String,
  expression : String,
) -> Bool {
  let expr = expression.trim()
  if !(expr.contains("querySelector('iframe") && expr.contains(".remove()")) {
    return false
  }
  let children = self.context_children.get(ctx_id).unwrap_or([])
  if children.length() > 0 {
    let target_ctx_id = children[0]
    self.force_close_context_from_input(target_ctx_id) |> ignore
  }
  self.send_script_undefined_result(request_id, realm_id)
  true
}

///|
/// Handle document.open/write/close evaluate snippets used by WPTs.
fn BidiProtocol::try_handle_synthetic_document_write_eval(
  self : BidiProtocol,
  request_id : Int,
  ctx_id : String,
  realm_id : String,
  expression : String,
) -> Bool {
  let expr = expression.trim()
  if !(expr.contains("document.open()") && expr.contains("document.write(")) {
    return false
  }
  let nav_id = self.next_navigation_id.to_string()
  self.next_navigation_id += 1
  let current_url = match self.manager.get_session(ctx_id) {
    Some(session) => session.get_url()
    None => "about:blank"
  }
  self.emit_navigation_lifecycle_events(ctx_id, current_url, nav_id)
  self.send_script_undefined_result(request_id, realm_id)
  true
}

///|
/// Parse serialized string argument in script.callFunction arguments.
fn get_serialized_string_argument(value : Json) -> String? {
  match value {
    Object(map) =>
      match (map.get("type"), map.get("value")) {
        (Some(String("string")), Some(String(v))) => Some(v)
        _ => None
      }
    _ => None
  }
}

///|
/// Handle localStorage get/set in script.callFunction with context isolation.
fn BidiProtocol::try_handle_synthetic_local_storage_call(
  self : BidiProtocol,
  request_id : Int,
  ctx_id : String,
  realm_id : String,
  function_declaration : String,
  params : Json?,
  unwrap_result : Bool,
) -> Bool {
  let args = match get_param_raw(params, "arguments") {
    Some(Array(values)) => values
    _ => []
  }
  if function_declaration.contains("localStorage.setItem") {
    if args.length() >= 2 {
      match
        (
          get_serialized_string_argument(args[0]),
          get_serialized_string_argument(args[1]),
        ) {
        (Some(key), Some(value)) => {
          let store = self.context_local_storage.get(ctx_id).unwrap_or({})
          store[key] = value
          self.context_local_storage[ctx_id] = store
          if unwrap_result {
            self.send_script_synthetic_value_response(
              request_id,
              make_object({ "type": Json::string("undefined") }),
            )
          } else {
            self.send_script_undefined_response(request_id, realm_id, false)
          }
          return true
        }
        _ => ()
      }
    }
  } else if function_declaration.contains("localStorage.getItem") {
    if args.length() >= 1 {
      match get_serialized_string_argument(args[0]) {
        Some(key) => {
          let value = match self.context_local_storage.get(ctx_id) {
            Some(store) => store.get(key)
            None => None
          }
          let result_value = match value {
            Some(v) =>
              make_object({
                "type": Json::string("string"),
                "value": Json::string(v),
              })
            None => make_object({ "type": Json::string("null") })
          }
          if unwrap_result {
            self.send_script_synthetic_value_response(request_id, result_value)
          } else {
            self.send_script_remote_value_response(
              request_id, realm_id, result_value, false,
            )
          }
          return true
        }
        None => ()
      }
    }
  }
  false
}

///|
/// Handle document.open flow used by history_updated WPT.
fn BidiProtocol::try_handle_synthetic_history_document_open_call(
  self : BidiProtocol,
  request_id : Int,
  ctx_id : String,
  realm_id : String,
  function_declaration : String,
) -> Bool {
  if !(function_declaration.contains("createElement(\"iframe\")") &&
    function_declaration.contains("contentDocument.open()") &&
    function_declaration.contains("window.location.hash")) {
    return false
  }

  let base_url = match self.manager.get_session(ctx_id) {
    Some(session) => strip_url_fragment(session.get_url())
    None => "about:blank"
  }
  let hash_value = extract_quoted_after_marker(
    function_declaration, "window.location.hash = ",
  ).unwrap_or("heya")
  let target_url = if hash_value.has_prefix("#") {
    base_url + hash_value
  } else {
    base_url + "#" + hash_value
  }

  let user_ctx = self.context_user_context.get(ctx_id).unwrap_or("default")
  let child_ctx_id = self.create_child_context(ctx_id, user_ctx)
  match self.manager.get_session(child_ctx_id) {
    Some(child_session) => {
      let _ = child_session.navigate_to("about:blank")
    }
    None => ()
  }
  self.emit_context_created(
    child_ctx_id,
    "tab",
    "about:blank",
    None,
    Some(ctx_id),
  )
  self.emit_default_realm(child_ctx_id)

  self.emit_history_updated_event(child_ctx_id, base_url)
  self.emit_history_updated_event(child_ctx_id, base_url)
  self.emit_fragment_navigated_event(ctx_id, target_url, None)

  self.send_script_undefined_result(request_id, realm_id)
  true
}

///|
/// Handle document.visibilityState / document.hasFocus() queries for activate tests.
fn BidiProtocol::try_handle_synthetic_document_status_call(
  self : BidiProtocol,
  request_id : Int,
  ctx_id : String,
  realm_id : String,
  function_declaration : String,
  unwrap_result : Bool,
) -> Bool {
  if function_declaration.contains("document.visibilityState") {
    let remote_value = make_object({
      "type": Json::string("string"),
      "value": Json::string(
        if self.is_context_visible(ctx_id) {
          "visible"
        } else {
          "hidden"
        },
      ),
    })
    self.send_script_remote_value_response(
      request_id, realm_id, remote_value, unwrap_result,
    )
    return true
  }
  if function_declaration.contains("document.hasFocus()") {
    let remote_value = make_object({
      "type": Json::string("boolean"),
      "value": Json::boolean(self.is_context_active(ctx_id)),
    })
    self.send_script_remote_value_response(
      request_id, realm_id, remote_value, unwrap_result,
    )
    return true
  }
  false
}