///|
/// Emit browsingContext.contextCreated events for an existing context tree.
fn BidiProtocol::emit_existing_context_created_tree(
  self : BidiProtocol,
  ctx_id : String,
) -> Unit {
  let url = match self.manager.get_session(ctx_id) {
    Some(session) => session.get_url()
    None => "about:blank"
  }
  let parent_ctx_id = self.context_parent.get(ctx_id)
  let original_opener = self.context_original_opener.get(ctx_id)
  let ctx_type = if self.context_parent.contains(ctx_id) {
    "tab"
  } else {
    "window"
  }
  self.emit_context_created(
    ctx_id, ctx_type, url, original_opener, parent_ctx_id,
  )
  for child_ctx_id in self.context_children.get(ctx_id).unwrap_or([]) {
    self.emit_existing_context_created_tree(child_ctx_id)
  }
}

///|
/// Rebuild child contexts from iframe elements under a parent context.
fn BidiProtocol::refresh_child_contexts_from_iframes(
  self : BidiProtocol,
  parent_ctx_id : String,
) -> Unit {
  self.refresh_child_contexts_from_iframes_with_depth(parent_ctx_id, 0)
}

///|
/// Register iframe elements that were inserted by an already-completed
/// script.evaluate call. This intentionally avoids
/// apply_effective_viewport_to_runtime_context: evaluate applies it at the
/// start of the command, and applying it again here would drain deferred
/// runtime bridges (notably userPromptOpened) too early.
///
/// This path also intentionally skips the navigate / load_data_url_content
/// steps that the explicit-creation paths use. The BiDi runtime serves all
/// contexts from a single shared globalThis.document, so calling
/// load_data_url_content for an iframe whose src is "about:blank" routes
/// through sync_runtime_html("") -> __loadHTML("") -> resetContainer(body),
/// which would clobber the parent's just-populated DOM (the very DOM that
/// produced this iframe in the first place). The iframe element survives in
/// the parent's DOM tree, and BiDi only needs the child-context bookkeeping
/// so frame-tree queries observe it; explicit navigation paths still
/// re-enter the heavy variant when the iframe is actually targeted.
fn BidiProtocol::refresh_child_contexts_from_runtime_iframes_after_eval(
  self : BidiProtocol,
  parent_ctx_id : String,
) -> Unit {
  set_runtime_context(parent_ctx_id)
  let frame_sources = get_runtime_iframe_sources()
  if frame_sources.length() == 0 {
    return
  }
  self.register_child_contexts_for_iframe_sources(parent_ctx_id, frame_sources)
}

///|
/// Lightweight child-context registration: create+emit only.
/// See refresh_child_contexts_from_runtime_iframes_after_eval for the
/// rationale on why this path skips navigate/load_data_url_content.
fn BidiProtocol::register_child_contexts_for_iframe_sources(
  self : BidiProtocol,
  parent_ctx_id : String,
  frame_sources : Array[String],
) -> Unit {
  let existing_children = self.context_children.get(parent_ctx_id).unwrap_or([])
  let user_ctx = self.context_user_context
    .get(parent_ctx_id)
    .unwrap_or("default")
  let mut frame_index = 0
  for _src in frame_sources {
    if frame_index < existing_children.length() {
      frame_index += 1
      continue
    }
    let child_ctx_id = self.create_child_context(parent_ctx_id, user_ctx)
    self.emit_context_created(
      child_ctx_id,
      "tab",
      "about:blank",
      None,
      Some(parent_ctx_id),
    )
    frame_index += 1
  }
}

///|
/// Rebuild child contexts from iframe elements with recursion guard.
fn BidiProtocol::refresh_child_contexts_from_iframes_with_depth(
  self : BidiProtocol,
  parent_ctx_id : String,
  depth : Int,
) -> Unit {
  if depth >= 8 {
    return
  }
  let frame_sources = self.collect_iframe_sources(parent_ctx_id)
  self.refresh_child_contexts_from_iframe_sources_with_depth(
    parent_ctx_id, frame_sources, depth,
  )
}

///|
fn BidiProtocol::refresh_child_contexts_from_iframe_sources_with_depth(
  self : BidiProtocol,
  parent_ctx_id : String,
  frame_sources : Array[String],
  depth : Int,
) -> Unit {
  let existing_children = self.context_children.get(parent_ctx_id).unwrap_or([])
  let user_ctx = self.context_user_context
    .get(parent_ctx_id)
    .unwrap_or("default")
  let mut frame_index = 0
  for src in frame_sources {
    if frame_index < existing_children.length() {
      frame_index += 1
      continue
    }
    let child_ctx_id = self.create_child_context(parent_ctx_id, user_ctx)
    self.emit_context_created(
      child_ctx_id,
      "tab",
      "about:blank",
      None,
      Some(parent_ctx_id),
    )
    let child_nav_id = self.next_navigation_id.to_string()
    self.next_navigation_id += 1
    if self.is_iframe_navigation_blocked(child_ctx_id, src) {
      self.emit_default_realm(child_ctx_id)
      self.emit_navigation_started_event(child_ctx_id, src, child_nav_id)
      self.emit_navigation_failed_event(child_ctx_id, src, child_nav_id)
      continue
    }
    match self.manager.get_session(child_ctx_id) {
      Some(child_session) => {
        let _ = child_session.navigate_to(src)
      }
      None => ()
    }
    self.load_data_url_content(child_ctx_id, src)
    let window_realm_id = self.assign_new_realm(child_ctx_id)
    self.emit_default_realm(child_ctx_id)
    self.emit_worker_realms_from_scripts(child_ctx_id, window_realm_id)
    self.apply_preload_scripts_for_context(child_ctx_id)
    self.execute_inline_scripts_for_context(child_ctx_id)
    self.emit_navigation_events(child_ctx_id, src, child_nav_id)
    // about:blank fallback frames can mirror parent runtime and recurse infinitely.
    // Avoid deep expansion until frame documents diverge via explicit navigation/src.
    if src != "about:blank" {
      self.refresh_child_contexts_from_iframes_with_depth(
        child_ctx_id,
        depth + 1,
      )
    }
    frame_index += 1
  }
}