///|
/// Dispatch emulation domain.
fn BidiProtocol::dispatch_emulation(
  self : BidiProtocol,
  request : BidiRequest,
  action : String,
) -> Result[Unit, String] {
  match action {
    "setUserAgentOverride" =>
      match
        self.resolve_emulation_set_user_agent_override(
          request.id,
          request.params,
        ) {
        Some(_) => {
          self.send_success(request.id, Some(make_object({})))
          Ok(())
        }
        None => Ok(())
      }
    "setLocaleOverride" =>
      match
        self.resolve_emulation_set_locale_override(request.id, request.params) {
        Some(_) => {
          self.send_success(request.id, Some(make_object({})))
          Ok(())
        }
        None => Ok(())
      }
    "setTimezoneOverride" =>
      match
        self.resolve_emulation_set_timezone_override(request.id, request.params) {
        Some(_) => {
          self.send_success(request.id, Some(make_object({})))
          Ok(())
        }
        None => Ok(())
      }
    "setGeolocationOverride" =>
      match
        self.resolve_emulation_set_geolocation_override(
          request.id,
          request.params,
        ) {
        Some(_) => {
          self.send_success(request.id, Some(make_object({})))
          Ok(())
        }
        None => Ok(())
      }
    "setNetworkConditions" =>
      match
        self.resolve_emulation_set_network_conditions(
          request.id,
          request.params,
        ) {
        Some(_) => {
          self.send_success(request.id, Some(make_object({})))
          Ok(())
        }
        None => Ok(())
      }
    "setScreenOrientationOverride" =>
      match
        self.resolve_emulation_set_screen_orientation_override(
          request.id,
          request.params,
        ) {
        Some(_) => {
          self.send_success(request.id, Some(make_object({})))
          Ok(())
        }
        None => Ok(())
      }
    "setScreenSettingsOverride" =>
      match
        self.resolve_emulation_set_screen_settings_override(
          request.id,
          request.params,
        ) {
        Some(_) => {
          self.send_success(request.id, Some(make_object({})))
          Ok(())
        }
        None => Ok(())
      }
    _ => {
      self.send_error(
        request.id,
        "unknown command",
        "Unknown method: " + request.method_name,
      )
      Ok(())
    }
  }
}

///|
extern "js" fn js_default_runtime_locale() -> String =
  #| () => {
  #|   try {
  #|     const locale = new Intl.DateTimeFormat().resolvedOptions().locale;
  #|     return typeof locale === "string" && locale ? locale : "en-US";
  #|   } catch (_error) {
  #|     return "en-US";
  #|   }
  #| }

///|
extern "js" fn js_normalize_runtime_locale(locale : String) -> String =
  #| (locale) => {
  #|   try {
  #|     const normalized = new Intl.DateTimeFormat(String(locale)).resolvedOptions().locale;
  #|     return typeof normalized === "string" && normalized ? normalized : "";
  #|   } catch (_error) {
  #|     return "";
  #|   }
  #| }

///|
fn default_runtime_locale() -> String {
  js_default_runtime_locale()
}

///|
extern "js" fn js_default_runtime_timezone() -> String =
  #| () => {
  #|   try {
  #|     const zone = new Intl.DateTimeFormat().resolvedOptions().timeZone;
  #|     return typeof zone === "string" && zone ? zone : "UTC";
  #|   } catch (_error) {
  #|     return "UTC";
  #|   }
  #| }

///|
extern "js" fn js_normalize_runtime_timezone(timezone : String) -> String =
  #| (timezone) => {
  #|   try {
  #|     const normalized = new Intl.DateTimeFormat("en-US", {
  #|       timeZone: String(timezone),
  #|     }).resolvedOptions().timeZone;
  #|     return typeof normalized === "string" && normalized ? normalized : "";
  #|   } catch (_error) {
  #|     return "";
  #|   }
  #| }

///|
fn default_runtime_timezone() -> String {
  js_default_runtime_timezone()
}

///|
fn BidiProtocol::resolve_effective_user_agent(
  self : BidiProtocol,
  ctx_id : String,
) -> String {
  match
    self.resolve_effective_scoped_override(
      ctx_id,
      self.emulation_state.emulation_user_agent_by_context,
      self.emulation_state.emulation_user_agent_by_user_context,
      self.emulation_state.emulation_user_agent_global,
    ) {
    Some(user_agent) => user_agent
    None => @browser_domain.default_runtime_user_agent()
  }
}

///|
fn BidiProtocol::resolve_effective_locale(
  self : BidiProtocol,
  ctx_id : String,
) -> String {
  match
    self.resolve_effective_scoped_override(
      ctx_id,
      self.emulation_state.emulation_locale_by_context,
      self.emulation_state.emulation_locale_by_user_context,
      self.emulation_state.emulation_locale_global,
    ) {
    Some(locale) => locale
    None => default_runtime_locale()
  }
}

///|
fn BidiProtocol::resolve_effective_timezone(
  self : BidiProtocol,
  ctx_id : String,
) -> String {
  match
    self.resolve_effective_scoped_override(
      ctx_id,
      self.emulation_state.emulation_timezone_by_context,
      self.emulation_state.emulation_timezone_by_user_context,
      self.emulation_state.emulation_timezone_global,
    ) {
    Some(timezone) => timezone
    None => default_runtime_timezone()
  }
}

///|
fn BidiProtocol::refresh_effective_user_agent_for_all_contexts(
  self : BidiProtocol,
) -> Unit {
  for ctx_id in self.manager.list_sessions() {
    self.apply_effective_viewport_to_runtime_context(ctx_id, ctx_id)
  }
}

///|
fn BidiProtocol::refresh_effective_locale_for_all_contexts(
  self : BidiProtocol,
) -> Unit {
  for ctx_id in self.manager.list_sessions() {
    self.apply_effective_viewport_to_runtime_context(ctx_id, ctx_id)
  }
}

///|
fn BidiProtocol::refresh_effective_timezone_for_all_contexts(
  self : BidiProtocol,
) -> Unit {
  for ctx_id in self.manager.list_sessions() {
    self.apply_effective_viewport_to_runtime_context(ctx_id, ctx_id)
  }
}

///|
fn BidiProtocol::try_handle_synthetic_user_agent_fetch_eval(
  self : BidiProtocol,
  request_id : Int,
  ctx_id : String,
  realm_id : String,
  expression : String,
  unwrap_result : Bool,
) -> Bool {
  let expr = expression.trim().to_owned()
  if !expr.has_prefix("fetch(") || !expr.contains(".text()") {
    return false
  }
  let request_url = match @browser_domain.extract_emulation_fetch_url(expr) {
    Some(request_url) => request_url
    None => return false
  }
  let resolved_url = self.resolve_navigation_target_url(ctx_id, request_url)
  if !is_wpt_headers_echo_url(resolved_url) {
    return false
  }
  let payload = self.build_network_headers_echo_payload(ctx_id, [])
  if unwrap_result {
    let parsed_payload = @json.parse(payload) catch { _ => make_object({}) }
    self.send_success(request_id, Some(parsed_payload))
  } else {
    self.send_script_remote_value_response(
      request_id,
      realm_id,
      make_object({
        "type": Json::string("string"),
        "value": Json::string(payload),
      }),
      false,
    )
  }
  true
}

///|
fn BidiProtocol::try_handle_synthetic_locale_eval(
  self : BidiProtocol,
  request_id : Int,
  ctx_id : String,
  realm_id : String,
  expression : String,
  unwrap_result : Bool,
) -> Bool {
  let expr = expression.trim().to_owned()
  if expr == "navigator.language" {
    self.send_script_remote_value_response(
      request_id,
      realm_id,
      make_object({
        "type": Json::string("string"),
        "value": Json::string(self.resolve_effective_locale(ctx_id)),
      }),
      unwrap_result,
    )
    return true
  }
  if expr == "navigator.languages" {
    let locale = self.resolve_effective_locale(ctx_id)
    self.send_script_remote_value_response(
      request_id,
      realm_id,
      make_object({
        "type": Json::string("array"),
        "value": Json::array([
          make_object({
            "type": Json::string("string"),
            "value": Json::string(locale),
          }),
        ]),
      }),
      unwrap_result,
    )
    return true
  }
  if expr == "new Intl.DateTimeFormat().resolvedOptions().locale" {
    self.send_script_remote_value_response(
      request_id,
      realm_id,
      make_object({
        "type": Json::string("string"),
        "value": Json::string(self.resolve_effective_locale(ctx_id)),
      }),
      unwrap_result,
    )
    return true
  }
  false
}

///|
fn BidiProtocol::try_handle_synthetic_timezone_eval(
  self : BidiProtocol,
  request_id : Int,
  ctx_id : String,
  realm_id : String,
  expression : String,
  unwrap_result : Bool,
) -> Bool {
  let expr = expression.trim().to_owned()
  if expr == "Intl.DateTimeFormat().resolvedOptions().timeZone" {
    self.send_script_remote_value_response(
      request_id,
      realm_id,
      make_object({
        "type": Json::string("string"),
        "value": Json::string(self.resolve_effective_timezone(ctx_id)),
      }),
      unwrap_result,
    )
    return true
  }
  match self.extract_emulation_timezone_offset(ctx_id, expr) {
    Some(offset_minutes) => {
      self.send_script_remote_value_response(
        request_id,
        realm_id,
        make_object({
          "type": Json::string("number"),
          "value": Json::number(offset_minutes.to_double()),
        }),
        unwrap_result,
      )
      true
    }
    None => false
  }
}

///|
fn BidiProtocol::extract_emulation_timezone_offset(
  self : BidiProtocol,
  ctx_id : String,
  expression : String,
) -> Int? {
  let prefix = "(new Date("
  let suffix = ")).getTimezoneOffset()"
  if !expression.has_prefix(prefix) || !expression.has_suffix(suffix) {
    return None
  }
  let timezone = self.resolve_effective_timezone(ctx_id)
  @browser_domain.emulation_timezone_offset_minutes(timezone)
}

///|
fn BidiProtocol::resolve_emulation_set_user_agent_override(
  self : BidiProtocol,
  request_id : Int,
  params : Json?,
) -> Bool? {
  let map = match params {
    Some(Object(map)) => map
    _ => {
      self.send_error(
        request_id, "invalid argument", "params must be an object",
      )
      return None
    }
  }

  let user_agent : String? = match
    get_map_field_with_alias(map, "userAgent", "user_agent") {
    Some(Null) => None
    Some(String(user_agent)) => Some(user_agent)
    Some(_) => {
      self.send_error(
        request_id, "invalid argument", "userAgent must be a string or null",
      )
      return None
    }
    None => {
      self.send_error(
        request_id, "invalid argument", "userAgent must be provided",
      )
      return None
    }
  }

  let target_contexts : Array[String] = []
  match map.get("contexts") {
    Some(Array(contexts)) =>
      if contexts.length() == 0 {
        self.send_error(
          request_id, "invalid argument", "contexts must not be an empty array",
        )
        return None
      } else {
        for context in contexts {
          match context {
            String(ctx_id) =>
              if !self.manager.has_session(ctx_id) {
                self.send_error(
                  request_id,
                  "no such frame",
                  "Unknown context: " + ctx_id,
                )
                return None
              } else if self.context_parent.contains(ctx_id) {
                self.send_error(
                  request_id, "invalid argument", "contexts must reference top-level browsing contexts",
                )
                return None
              } else if !array_contains(target_contexts, ctx_id) {
                target_contexts.push(ctx_id)
              }
            _ => {
              self.send_error(
                request_id, "invalid argument", "contexts entries must be strings",
              )
              return None
            }
          }
        }
      }
    Some(_) => {
      self.send_error(
        request_id, "invalid argument", "contexts must be an array",
      )
      return None
    }
    None => ()
  }

  let target_user_contexts : Array[String] = []
  match get_map_field_with_alias(map, "userContexts", "user_contexts") {
    Some(Array(user_contexts)) =>
      if user_contexts.length() == 0 {
        self.send_error(
          request_id, "invalid argument", "userContexts must not be an empty array",
        )
        return None
      } else {
        for raw_user_context in user_contexts {
          match raw_user_context {
            String(user_context_id) =>
              if user_context_id == "" ||
                !self.user_contexts.contains(user_context_id) {
                self.send_error(
                  request_id,
                  "no such user context",
                  "Unknown user context: " + user_context_id,
                )
                return None
              } else if !array_contains(target_user_contexts, user_context_id) {
                target_user_contexts.push(user_context_id)
              }
            _ => {
              self.send_error(
                request_id, "invalid argument", "userContexts entries must be strings",
              )
              return None
            }
          }
        }
      }
    Some(_) => {
      self.send_error(
        request_id, "invalid argument", "userContexts must be an array",
      )
      return None
    }
    None => ()
  }

  if target_contexts.length() > 0 && target_user_contexts.length() > 0 {
    self.send_error(
      request_id, "invalid argument", "contexts and userContexts are mutually exclusive",
    )
    return None
  }

  if target_contexts.length() > 0 {
    for ctx_id in target_contexts {
      match user_agent {
        Some(user_agent) =>
          self.emulation_state.emulation_user_agent_by_context[ctx_id] = user_agent
        None =>
          self.emulation_state.emulation_user_agent_by_context.remove(ctx_id)
      }
    }
    self.refresh_effective_user_agent_for_all_contexts()
    return Some(true)
  }

  if target_user_contexts.length() > 0 {
    for user_context_id in target_user_contexts {
      match user_agent {
        Some(user_agent) =>
          self.emulation_state.emulation_user_agent_by_user_context[user_context_id] = user_agent
        None =>
          self.emulation_state.emulation_user_agent_by_user_context.remove(
            user_context_id,
          )
      }
    }
    self.refresh_effective_user_agent_for_all_contexts()
    return Some(true)
  }

  self.emulation_state.emulation_user_agent_global = user_agent
  self.refresh_effective_user_agent_for_all_contexts()
  Some(true)
}

///|
fn BidiProtocol::resolve_emulation_set_locale_override(
  self : BidiProtocol,
  request_id : Int,
  params : Json?,
) -> Bool? {
  let map = match params {
    Some(Object(map)) => map
    _ => {
      self.send_error(
        request_id, "invalid argument", "params must be an object",
      )
      return None
    }
  }

  let locale : String? = match
    get_map_field_with_alias(map, "locale", "locale") {
    Some(Null) => None
    Some(String(locale)) => {
      let normalized = js_normalize_runtime_locale(locale)
      if normalized == "" {
        self.send_error(request_id, "invalid argument", "locale is invalid")
        return None
      }
      Some(normalized)
    }
    Some(_) => {
      self.send_error(
        request_id, "invalid argument", "locale must be a string or null",
      )
      return None
    }
    None => {
      self.send_error(request_id, "invalid argument", "locale must be provided")
      return None
    }
  }

  let target_contexts : Array[String] = []
  match map.get("contexts") {
    Some(Array(contexts)) =>
      if contexts.length() == 0 {
        self.send_error(
          request_id, "invalid argument", "contexts must not be an empty array",
        )
        return None
      } else {
        for context in contexts {
          match context {
            String(ctx_id) =>
              if !self.manager.has_session(ctx_id) {
                self.send_error(
                  request_id,
                  "no such frame",
                  "Unknown context: " + ctx_id,
                )
                return None
              } else if self.context_parent.contains(ctx_id) {
                self.send_error(
                  request_id, "invalid argument", "contexts must reference top-level browsing contexts",
                )
                return None
              } else if !array_contains(target_contexts, ctx_id) {
                target_contexts.push(ctx_id)
              }
            _ => {
              self.send_error(
                request_id, "invalid argument", "contexts entries must be strings",
              )
              return None
            }
          }
        }
      }
    Some(_) => {
      self.send_error(
        request_id, "invalid argument", "contexts must be an array",
      )
      return None
    }
    None => ()
  }

  let target_user_contexts : Array[String] = []
  match get_map_field_with_alias(map, "userContexts", "user_contexts") {
    Some(Array(user_contexts)) =>
      if user_contexts.length() == 0 {
        self.send_error(
          request_id, "invalid argument", "userContexts must not be an empty array",
        )
        return None
      } else {
        for raw_user_context in user_contexts {
          match raw_user_context {
            String(user_context_id) =>
              if user_context_id == "" ||
                !self.user_contexts.contains(user_context_id) {
                self.send_error(
                  request_id,
                  "no such user context",
                  "Unknown user context: " + user_context_id,
                )
                return None
              } else if !array_contains(target_user_contexts, user_context_id) {
                target_user_contexts.push(user_context_id)
              }
            _ => {
              self.send_error(
                request_id, "invalid argument", "userContexts entries must be strings",
              )
              return None
            }
          }
        }
      }
    Some(_) => {
      self.send_error(
        request_id, "invalid argument", "userContexts must be an array",
      )
      return None
    }
    None => ()
  }

  if target_contexts.length() > 0 && target_user_contexts.length() > 0 {
    self.send_error(
      request_id, "invalid argument", "contexts and userContexts are mutually exclusive",
    )
    return None
  }

  if target_contexts.length() > 0 {
    for ctx_id in target_contexts {
      match locale {
        Some(locale) =>
          self.emulation_state.emulation_locale_by_context[ctx_id] = locale
        None => self.emulation_state.emulation_locale_by_context.remove(ctx_id)
      }
    }
    self.refresh_effective_locale_for_all_contexts()
    return Some(true)
  }

  if target_user_contexts.length() > 0 {
    for user_context_id in target_user_contexts {
      match locale {
        Some(locale) =>
          self.emulation_state.emulation_locale_by_user_context[user_context_id] = locale
        None =>
          self.emulation_state.emulation_locale_by_user_context.remove(
            user_context_id,
          )
      }
    }
    self.refresh_effective_locale_for_all_contexts()
    return Some(true)
  }

  self.emulation_state.emulation_locale_global = locale
  self.refresh_effective_locale_for_all_contexts()
  Some(true)
}

///|
fn BidiProtocol::resolve_emulation_set_timezone_override(
  self : BidiProtocol,
  request_id : Int,
  params : Json?,
) -> Bool? {
  let map = match params {
    Some(Object(map)) => map
    _ => {
      self.send_error(
        request_id, "invalid argument", "params must be an object",
      )
      return None
    }
  }

  let timezone : String? = match
    get_map_field_with_alias(map, "timezone", "timezone") {
    Some(Null) => None
    Some(String(timezone)) => {
      let normalized = js_normalize_runtime_timezone(timezone)
      if normalized == "" {
        self.send_error(request_id, "invalid argument", "timezone is invalid")
        return None
      }
      Some(normalized)
    }
    Some(_) => {
      self.send_error(
        request_id, "invalid argument", "timezone must be a string or null",
      )
      return None
    }
    None => {
      self.send_error(
        request_id, "invalid argument", "timezone must be provided",
      )
      return None
    }
  }

  let target_contexts : Array[String] = []
  match map.get("contexts") {
    Some(Array(contexts)) =>
      if contexts.length() == 0 {
        self.send_error(
          request_id, "invalid argument", "contexts must not be an empty array",
        )
        return None
      } else {
        for context in contexts {
          match context {
            String(ctx_id) =>
              if !self.manager.has_session(ctx_id) {
                self.send_error(
                  request_id,
                  "no such frame",
                  "Unknown context: " + ctx_id,
                )
                return None
              } else if self.context_parent.contains(ctx_id) {
                self.send_error(
                  request_id, "invalid argument", "contexts must reference top-level browsing contexts",
                )
                return None
              } else if !array_contains(target_contexts, ctx_id) {
                target_contexts.push(ctx_id)
              }
            _ => {
              self.send_error(
                request_id, "invalid argument", "contexts entries must be strings",
              )
              return None
            }
          }
        }
      }
    Some(_) => {
      self.send_error(
        request_id, "invalid argument", "contexts must be an array",
      )
      return None
    }
    None => ()
  }

  let target_user_contexts : Array[String] = []
  match get_map_field_with_alias(map, "userContexts", "user_contexts") {
    Some(Array(user_contexts)) =>
      if user_contexts.length() == 0 {
        self.send_error(
          request_id, "invalid argument", "userContexts must not be an empty array",
        )
        return None
      } else {
        for raw_user_context in user_contexts {
          match raw_user_context {
            String(user_context_id) =>
              if user_context_id == "" ||
                !self.user_contexts.contains(user_context_id) {
                self.send_error(
                  request_id,
                  "no such user context",
                  "Unknown user context: " + user_context_id,
                )
                return None
              } else if !array_contains(target_user_contexts, user_context_id) {
                target_user_contexts.push(user_context_id)
              }
            _ => {
              self.send_error(
                request_id, "invalid argument", "userContexts entries must be strings",
              )
              return None
            }
          }
        }
      }
    Some(_) => {
      self.send_error(
        request_id, "invalid argument", "userContexts must be an array",
      )
      return None
    }
    None => ()
  }

  if target_contexts.length() > 0 && target_user_contexts.length() > 0 {
    self.send_error(
      request_id, "invalid argument", "contexts and userContexts are mutually exclusive",
    )
    return None
  }

  if target_contexts.length() > 0 {
    for ctx_id in target_contexts {
      match timezone {
        Some(timezone) =>
          self.emulation_state.emulation_timezone_by_context[ctx_id] = timezone
        None =>
          self.emulation_state.emulation_timezone_by_context.remove(ctx_id)
      }
    }
    self.refresh_effective_timezone_for_all_contexts()
    return Some(true)
  }

  if target_user_contexts.length() > 0 {
    for user_context_id in target_user_contexts {
      match timezone {
        Some(timezone) =>
          self.emulation_state.emulation_timezone_by_user_context[user_context_id] = timezone
        None =>
          self.emulation_state.emulation_timezone_by_user_context.remove(
            user_context_id,
          )
      }
    }
    self.refresh_effective_timezone_for_all_contexts()
    return Some(true)
  }

  self.emulation_state.emulation_timezone_global = timezone
  self.refresh_effective_timezone_for_all_contexts()
  Some(true)
}