///|
/// Complete navigation state update and optionally defer success response by wait mode.
fn BidiProtocol::complete_navigation_request_with_wait(
  self : BidiProtocol,
  request_id : Int,
  ctx_id : String,
  url : String,
  wait_mode : String,
) -> Unit {
  self.complete_navigation_request_with_wait_and_navigation(
    request_id,
    ctx_id,
    url,
    wait_mode,
    None,
  )
}

///|
/// Apply committed navigation state and emit navigation lifecycle events.
fn BidiProtocol::apply_navigation_commit_state(
  self : BidiProtocol,
  ctx_id : String,
  url : String,
  navigation_id : String,
) -> Unit {
  self.apply_navigation_commit_state_internal(ctx_id, url, navigation_id, false)
}

///|
/// Apply committed navigation state. When `defer_load` is true, the runtime
/// document load (sync_runtime_page / sync_runtime_html) is skipped — the
/// caller is responsible for invoking the load and sending the BiDi response
/// once it resolves. Used for HTTP wait=complete navigations that need the
/// JS async bridge so script.evaluate sees the loaded document.
fn BidiProtocol::apply_navigation_commit_state_internal(
  self : BidiProtocol,
  ctx_id : String,
  url : String,
  navigation_id : String,
  defer_load : Bool,
) -> Unit {
  let previous_children = self.context_children.get(ctx_id).unwrap_or([])
  reset_runtime_event_buffers(ctx_id)
  if !defer_load {
    self.load_data_url_content(ctx_id, url)
  }
  let defer_parent_lifecycle = self.collect_iframe_sources(ctx_id).length() > 0
  for child_ctx_id in previous_children {
    let original_opener = self.context_original_opener.get(child_ctx_id)
    self.emit_context_destroyed(child_ctx_id, original_opener)
  }
  self.drop_child_contexts(ctx_id, false)
  // Navigation replaces all realms owned by the context.
  self.remove_all_realms_for_context(ctx_id)
  let window_realm_id = self.assign_new_realm(ctx_id)
  self.emit_default_realm(ctx_id)
  self.emit_worker_realms_from_scripts(ctx_id, window_realm_id)
  self.apply_preload_scripts_for_context(ctx_id)
  self.execute_inline_scripts_for_context(ctx_id)
  if url.contains("/webdriver/tests/support/html/beforeunload.html") {
    self.context_has_beforeunload[ctx_id] = true
  } else {
    self.context_has_beforeunload.remove(ctx_id)
  }
  if navigation_blocks_cross_origin_iframe_navigation(url) {
    self.context_blocks_cross_origin_iframe_navigation[ctx_id] = true
  } else {
    self.context_blocks_cross_origin_iframe_navigation.remove(ctx_id)
  }
  self.emit_navigation_events_internal(
    ctx_id,
    url,
    navigation_id,
    !defer_parent_lifecycle,
  )
  self.refresh_child_contexts_from_iframes(ctx_id)
  if defer_parent_lifecycle {
    self.emit_navigation_lifecycle_events(ctx_id, url, navigation_id)
  }
  self.register_navigation_prompt_from_url(ctx_id, url)
}

///|
/// Complete navigation update using an optional preallocated navigation id.
fn BidiProtocol::complete_navigation_request_with_wait_and_navigation(
  self : BidiProtocol,
  request_id : Int,
  ctx_id : String,
  url : String,
  wait_mode : String,
  navigation_id : String?,
) -> Unit {
  let completion = self.build_navigation_completion(
    request_id, ctx_id, url, wait_mode, navigation_id,
  )
  if !completion.deferred_response {
    self.send_success(request_id, Some(completion.result))
  }
}

///|
/// Compute navigation state and return the nav id WITHOUT sending the
/// BiDi response. Caller MUST send the response via socket asynchronously
/// (currently: js_navigate_and_send_async).
///
/// PRECONDITION: should_block_navigation_response(wait_mode, url) must be
/// false for this URL — otherwise build_navigation_completion would set
/// deferred_response=true expecting register_in_flight_navigation to
/// later fire send_success, and the async bridge would double-respond.
/// Currently safe because should_navigate_via_async_bridge excludes
/// trickle URLs (the only ones where block_response would be true for
/// HTTP).
fn BidiProtocol::complete_navigation_request_with_wait_deferred(
  self : BidiProtocol,
  request_id : Int,
  ctx_id : String,
  url : String,
  wait_mode : String,
  navigation_id : String?,
) -> String {
  let completion = self.build_navigation_completion_internal(
    request_id, ctx_id, url, wait_mode, navigation_id, true,
  )
  completion.navigation_id
}

///|
/// Fallback path used when an async bridge call cannot be made (e.g. the
/// wbtest harness never attaches a socket). Performs the deferred runtime
/// document load synchronously and emits the BiDi success response into the
/// outbox so test code observes the same result shape as the production
/// async path.
fn BidiProtocol::complete_navigation_request_with_wait_after_deferred(
  self : BidiProtocol,
  request_id : Int,
  ctx_id : String,
  url : String,
  navigation_id : String,
) -> Unit {
  self.load_data_url_content(ctx_id, url)
  let result = make_object({
    "navigation": Json::string(navigation_id),
    "url": Json::string(url),
  })
  self.send_success(request_id, Some(result))
}

///|
/// Decide whether navigate should hand off to js_navigate_and_send_async
/// instead of the broken sync sync_runtime_page() path. Only HTTP(S) URLs
/// with wait=complete need the async bridge — data:/about:/trickle URLs
/// already work via the existing sync paths.
fn BidiProtocol::should_navigate_via_async_bridge(
  self : BidiProtocol,
  url : String,
  wait_mode : String,
) -> Bool {
  ignore(self)
  if wait_mode != "complete" {
    return false
  }
  if !(url.has_prefix("http://") || url.has_prefix("https://")) {
    return false
  }
  // Trickle-marker URLs are WPT test fixtures handled by the existing
  // sync block-and-respond path; keep them on that path so the in-flight
  // navigation accounting (register_in_flight_navigation /
  // apply_navigation_wait_delay) still applies.
  if is_trickle_navigation_url(url) {
    return false
  }
  true
}

///|
fn BidiProtocol::build_navigation_completion(
  self : BidiProtocol,
  request_id : Int,
  ctx_id : String,
  url : String,
  wait_mode : String,
  navigation_id : String?,
) -> NavigationCompletion {
  self.build_navigation_completion_internal(
    request_id, ctx_id, url, wait_mode, navigation_id, false,
  )
}

///|
fn BidiProtocol::build_navigation_completion_internal(
  self : BidiProtocol,
  request_id : Int,
  ctx_id : String,
  url : String,
  wait_mode : String,
  navigation_id : String?,
  defer_load : Bool,
) -> NavigationCompletion {
  let nav_id = match navigation_id {
    Some(id) => id
    None => {
      let generated_id = self.next_navigation_id.to_string()
      self.next_navigation_id += 1
      generated_id
    }
  }
  self.apply_navigation_commit_state_internal(ctx_id, url, nav_id, defer_load)
  let has_inline_redirect = match resolve_inline_script_redirect_url(url) {
    Some(_) => true
    None => false
  }
  let block_response = should_block_navigation_response(wait_mode, url)
  let mark_in_flight = is_interruptible_navigation_candidate(url) &&
    !has_inline_redirect
  let deferred_response = block_response && mark_in_flight
  if mark_in_flight {
    if block_response {
      self.register_in_flight_navigation(ctx_id, url, nav_id, Some(request_id))
    } else {
      self.register_in_flight_navigation(ctx_id, url, nav_id, None)
    }
  } else {
    self.clear_pending_navigation_state(ctx_id)
    if block_response {
      apply_navigation_wait_delay(wait_mode, url)
    }
  }
  let result = make_object({
    "navigation": Json::string(nav_id),
    "url": Json::string(url),
  })
  { result, navigation_id: nav_id, deferred_response }
}

///|
/// Complete same-document fragment navigation.
fn BidiProtocol::complete_fragment_navigation_request(
  self : BidiProtocol,
  request_id : Int,
  ctx_id : String,
  url : String,
) -> Unit {
  self.clear_pending_navigation_state(ctx_id)
  let nav_id = self.next_navigation_id.to_string()
  self.next_navigation_id += 1
  self.emit_fragment_navigated_event(ctx_id, url, Some(nav_id))
  self.emit_history_updated_event(ctx_id, url)
  let result = make_object({
    "navigation": Json::string(nav_id),
    "url": Json::string(url),
  })
  self.send_success(request_id, Some(result))
}