///|
fn BidiProtocol::handle_input_record_synthetic_events(
  self : BidiProtocol,
  request_id : Int,
  params : Json?,
) -> Unit {
  let map = match params {
    Some(Object(map)) => map
    _ => {
      self.send_error(
        request_id, "invalid argument", "params must be an object",
      )
      return
    }
  }
  let ctx_id = match map.get("context") {
    Some(String(ctx_id)) => ctx_id
    _ => {
      self.send_error(
        request_id, "invalid argument", "context must be a string",
      )
      return
    }
  }
  if !self.manager.has_session(ctx_id) {
    self.send_error(request_id, "no such frame", "Unknown context: " + ctx_id)
    return
  }
  let events = match map.get("events") {
    Some(Array(events)) => events
    _ => {
      self.send_error(request_id, "invalid argument", "events must be an array")
      return
    }
  }
  let existing = self.input_synthetic_events_by_context
    .get(ctx_id)
    .unwrap_or([])
  for event in events {
    match event {
      Object(_) => existing.push(event)
      _ => {
        self.send_error(
          request_id, "invalid argument", "events entries must be objects",
        )
        return
      }
    }
  }
  self.input_synthetic_events_by_context[ctx_id] = existing
  self.send_success(request_id, Some(make_object({})))
}

///|
fn BidiProtocol::handle_input_set_files(
  self : BidiProtocol,
  request_id : Int,
  params : Json?,
) -> Unit {
  let map = match params {
    Some(Object(map)) => map
    _ => {
      self.send_error(
        request_id, "invalid argument", "params must be an object",
      )
      return
    }
  }
  let ctx_id = match self.validate_input_context(request_id, params) {
    Some(ctx_id) => ctx_id
    None => return
  }
  let element = match map.get("element") {
    Some(Object(element)) => element
    _ => {
      self.send_error(
        request_id, "invalid argument", "element must be an object",
      )
      return
    }
  }
  let source_paths = match self.input_set_files_source_paths(request_id, map) {
    Some(values) => values
    None => return
  }
  let display_names = match
    self.input_set_files_display_names(request_id, map, source_paths) {
    Some(values) => values
    None => return
  }
  if source_paths.length() != display_names.length() {
    self.send_error(
      request_id, "invalid argument", "sourcePaths and displayNames must have the same length",
    )
    return
  }
  let (element_id, allow_fallback) = match
    self.resolve_input_set_files_locator(request_id, element) {
    Some(locator) => locator
    None => return
  }
  let summary = match
    self.apply_input_set_files_summary(
      ctx_id, element_id, allow_fallback, source_paths, display_names,
    ) {
    Some(summary) => summary
    None => {
      self.send_error(request_id, "no such element", "No such element")
      return
    }
  }
  let found = input_set_files_summary_bool(summary, "found")
  let is_element = input_set_files_summary_bool(summary, "isElement")
  let disabled = input_set_files_summary_bool(summary, "disabled")
  let multiple = input_set_files_summary_bool(summary, "multiple")
  let tag_name = input_set_files_summary_string(summary, "tagName").unwrap_or(
    "",
  )
  let input_type = input_set_files_summary_string(summary, "inputType").unwrap_or(
    "",
  )

  if !found || !is_element {
    self.send_error(request_id, "no such element", "No such element")
    return
  }
  if tag_name != "input" ||
    input_type != "file" ||
    disabled ||
    (source_paths.length() > 1 && !multiple) {
    self.send_error(
      request_id, "unable to set file input", "Unable to set file input",
    )
    return
  }

  let event_files = match
    input_set_files_summary_string_list(summary, "eventFiles") {
    Some(values) => values
    None => display_names
  }
  match input_set_files_summary_string_list(summary, "eventTypes") {
    Some(event_types) =>
      if event_types.length() > 0 {
        self.append_input_synthetic_events(ctx_id, event_types, event_files)
      }
    None => ()
  }
  self.send_success(request_id, Some(make_object({})))
}

///|
fn BidiProtocol::append_input_synthetic_events(
  self : BidiProtocol,
  ctx_id : String,
  event_types : Array[String],
  event_files : Array[String],
) -> Unit {
  let existing = self.input_synthetic_events_by_context
    .get(ctx_id)
    .unwrap_or([])
  let files_json : Array[Json] = []
  for file_name in event_files {
    files_json.push(Json::string(file_name))
  }
  for event_type in event_types {
    existing.push(
      make_object({
        "type": Json::string(event_type),
        "files": Json::array(files_json),
      }),
    )
  }
  self.input_synthetic_events_by_context[ctx_id] = existing
}

///|
fn BidiProtocol::handle_input_is_file_dialog_canceled_for_test(
  self : BidiProtocol,
  request_id : Int,
  params : Json?,
) -> Unit {
  let ctx_id = match self.validate_input_context(request_id, params) {
    Some(ctx_id) => ctx_id
    None => return
  }
  let handler = self.resolve_unhandled_prompt_handler(ctx_id, "file")
  self.send_success(request_id, Some(Json::boolean(handler != "ignore")))
}

///|
fn BidiProtocol::resolve_input_set_files_locator(
  self : BidiProtocol,
  request_id : Int,
  element : Map[String, Json],
) -> (String, Bool)? {
  match element.get("type") {
    Some(String(type_name)) if type_name == "null" => return Some(("", true))
    _ => ()
  }
  let value_map = match element.get("value") {
    Some(Object(value_map)) => Some(value_map)
    _ => None
  }
  let shared_id = match element.get("sharedId") {
    Some(String(shared_id)) => Some(shared_id)
    _ =>
      match value_map {
        Some(value_map) =>
          match value_map.get("sharedId") {
            Some(String(shared_id)) => Some(shared_id)
            _ => None
          }
        None => None
      }
  }
  let shared_id = match shared_id {
    Some(shared_id) => shared_id
    None => {
      self.send_error(
        request_id, "invalid argument", "element.sharedId must be a string",
      )
      return None
    }
  }
  match value_map {
    Some(value_map) =>
      match value_map.get("nodeType") {
        Some(Number(node_type, ..)) if node_type != 1.0 => {
          self.send_error(request_id, "no such element", "No such element")
          return None
        }
        _ => ()
      }
    None => ()
  }
  if !has_runtime_shared_node(shared_id) {
    self.send_error(
      request_id,
      "no such node",
      "Unknown sharedId: " + shared_id,
    )
    return None
  }
  if !runtime_shared_node_is_element(shared_id) {
    self.send_error(request_id, "no such element", "No such element")
    return None
  }
  let element_id = match value_map {
    Some(value_map) =>
      match value_map.get("attributes") {
        Some(Object(attributes)) =>
          match attributes.get("id") {
            Some(String(element_id)) => element_id
            _ => ""
          }
        _ => ""
      }
    None => ""
  }
  Some((element_id, false))
}

///|
fn BidiProtocol::apply_input_set_files_summary(
  self : BidiProtocol,
  ctx_id : String,
  element_id : String,
  allow_fallback : Bool,
  source_paths : Array[String],
  display_names : Array[String],
) -> Map[String, Json]? {
  set_runtime_context(ctx_id)
  self.apply_effective_viewport_to_runtime_context(ctx_id, ctx_id)
  let source_paths_json : Array[Json] = []
  for path in source_paths {
    source_paths_json.push(Json::string(path))
  }
  let display_names_json : Array[Json] = []
  for name in display_names {
    display_names_json.push(Json::string(name))
  }
  let payload = js_input_apply_file_selection(
    element_id,
    allow_fallback,
    Json::array(source_paths_json).stringify(),
    Json::array(display_names_json).stringify(),
  )
  let summary = @json.parse(payload) catch { _ => return None }
  match summary {
    Object(summary) => Some(summary)
    _ => None
  }
}

///|
fn BidiProtocol::input_set_files_string_array(
  self : BidiProtocol,
  request_id : Int,
  raw_value : Json?,
  field_name : String,
) -> Array[String]? {
  let items = match raw_value {
    Some(Array(items)) => items
    _ => {
      self.send_error(
        request_id,
        "invalid argument",
        field_name + " must be an array",
      )
      return None
    }
  }
  let values : Array[String] = []
  for item in items {
    match item {
      String(value) => values.push(value)
      _ => {
        self.send_error(
          request_id,
          "invalid argument",
          field_name + " entries must be strings",
        )
        return None
      }
    }
  }
  Some(values)
}

///|
fn BidiProtocol::input_set_files_source_paths(
  self : BidiProtocol,
  request_id : Int,
  map : Map[String, Json],
) -> Array[String]? {
  match map.get("files") {
    Some(raw_value) =>
      self.input_set_files_string_array(request_id, Some(raw_value), "files")
    None =>
      self.input_set_files_string_array(
        request_id,
        map.get("sourcePaths"),
        "sourcePaths",
      )
  }
}

///|
fn BidiProtocol::input_set_files_display_names(
  self : BidiProtocol,
  request_id : Int,
  map : Map[String, Json],
  source_paths : Array[String],
) -> Array[String]? {
  match map.get("displayNames") {
    Some(raw_value) =>
      self.input_set_files_string_array(
        request_id,
        Some(raw_value),
        "displayNames",
      )
    None => Some(input_set_files_default_display_names(source_paths))
  }
}

///|
fn input_set_files_default_display_names(
  source_paths : Array[String],
) -> Array[String] {
  let display_names : Array[String] = []
  for source_path in source_paths {
    display_names.push(input_set_files_display_name(source_path))
  }
  display_names
}

///|
fn input_set_files_display_name(source_path : String) -> String {
  let mut last_separator = -1
  let mut search_from = 0
  while search_from < source_path.length() {
    match find_substring(source_path, "/", search_from) {
      Some(idx) => {
        last_separator = idx
        search_from = idx + 1
      }
      None => break
    }
  }
  search_from = 0
  while search_from < source_path.length() {
    match find_substring(source_path, "\\", search_from) {
      Some(idx) => {
        if idx > last_separator {
          last_separator = idx
        }
        search_from = idx + 1
      }
      None => break
    }
  }
  if last_separator < 0 {
    return source_path
  }
  let base_name = source_path.unsafe_substring(
    start=last_separator + 1,
    end=source_path.length(),
  )
  if base_name.length() == 0 {
    return source_path
  }
  base_name
}

///|
fn input_set_files_summary_bool(
  summary : Map[String, Json],
  key : String,
) -> Bool {
  match summary.get(key) {
    Some(True) => true
    Some(False) => false
    _ => false
  }
}

///|
fn input_set_files_summary_string(
  summary : Map[String, Json],
  key : String,
) -> String? {
  match summary.get(key) {
    Some(String(value)) => Some(value)
    _ => None
  }
}

///|
fn input_set_files_summary_string_list(
  summary : Map[String, Json],
  key : String,
) -> Array[String]? {
  let items = match summary.get(key) {
    Some(Array(items)) => items
    _ => return None
  }
  let values : Array[String] = []
  for item in items {
    match item {
      String(value) => values.push(value)
      _ => ()
    }
  }
  Some(values)
}

///|
fn evaluate_current_file_input_remote_value() -> Json? {
  let eval_result_json = evaluate_js_with_console(
    "(() => { const input = document.getElementById(\"input\"); return input ?? null; })()",
    false, false, false, "{}",
  )
  let eval_result = @json.parse(eval_result_json) catch { _ => return None }
  match get_string_field(eval_result, "type") {
    Some("success") => get_field(eval_result, "result")
    _ => None
  }
}

///|
fn evaluate_current_file_input_multiple() -> Bool {
  let eval_result_json = evaluate_js_with_console(
    "(() => { const input = document.getElementById(\"input\"); return Boolean(input && (input.multiple || (typeof input.hasAttribute === \"function\" && input.hasAttribute(\"multiple\")))); })()",
    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
  }
}

///|
extern "js" fn js_input_apply_file_selection(
  element_id : String,
  allow_fallback : Bool,
  source_paths_json : String,
  display_names_json : String,
) -> String =
  #| (elementId, allowFallback, sourcePathsJson, displayNamesJson) => {
  #|   const sourcePaths = (() => {
  #|     try {
  #|       const value = JSON.parse(String(sourcePathsJson ?? "[]"));
  #|       return Array.isArray(value) ? value.map(path => String(path)) : [];
  #|     } catch (_) {
  #|       return [];
  #|     }
  #|   })();
  #|   const displayNames = (() => {
  #|     try {
  #|       const value = JSON.parse(String(displayNamesJson ?? "[]"));
  #|       return Array.isArray(value) ? value.map(name => String(name)) : [];
  #|     } catch (_) {
  #|       return [];
  #|     }
  #|   })();
  #|   let input = null;
  #|   if (typeof document !== "undefined" && typeof elementId === "string" && elementId !== "") {
  #|     input = document.getElementById(elementId);
  #|   }
  #|   if (!input && allowFallback && typeof document !== "undefined") {
  #|     input = document.querySelector("input[type=file], #input");
  #|   }
  #|   if (!input && allowFallback && typeof document !== "undefined" && document.body && typeof document.createElement === "function") {
  #|     input = document.createElement("input");
  #|     input.type = "file";
  #|     input.id = "input";
  #|     document.body.appendChild(input);
  #|   }
  #|   if (!input) {
  #|     return JSON.stringify({
  #|       found: false,
  #|       isElement: false,
  #|       tagName: "",
  #|       inputType: "",
  #|       disabled: false,
  #|       multiple: false,
  #|       eventTypes: [],
  #|       eventFiles: [],
  #|     });
  #|   }
  #|   const isElement = Number(input?.nodeType || 0) === 1;
  #|   const tagName = isElement ? String(input.localName || input.tagName || "").toLowerCase() : "";
  #|   const getAttr = (name) => typeof input?.getAttribute === "function" ? input.getAttribute(name) : null;
  #|   const inputType = isElement && tagName === "input" ? String(input.type || getAttr("type") || "").toLowerCase() : "";
  #|   const disabled = isElement && tagName === "input" ? (Boolean(input.disabled) || getAttr("disabled") !== null) : false;
  #|   const multiple = isElement && tagName === "input" ? (Boolean(input.multiple) || getAttr("multiple") !== null) : false;
  #|   const summary = {
  #|     found: true,
  #|     isElement,
  #|     tagName,
  #|     inputType,
  #|     disabled,
  #|     multiple,
  #|     eventTypes: [],
  #|     eventFiles: [],
  #|   };
  #|   if (!isElement || tagName !== "input" || inputType !== "file" || disabled || (!multiple && sourcePaths.length > 1)) {
  #|     return JSON.stringify(summary);
  #|   }
  #|   summary.eventFiles = displayNames.slice();
  #|   const previousSourcePaths = Array.isArray(input.__craterSyntheticSourcePaths) ? input.__craterSyntheticSourcePaths.slice() : [];
  #|   const isSameSelection = previousSourcePaths.length === sourcePaths.length && previousSourcePaths.every((path, index) => path === sourcePaths[index]);
  #|   const syntheticFiles = displayNames.map(name => ({ name }));
  #|   try {
  #|     Object.defineProperty(input, "files", {
  #|       configurable: true,
  #|       get: () => syntheticFiles,
  #|     });
  #|   } catch (_) {
  #|     input.files = syntheticFiles;
  #|   }
  #|   input.__craterSyntheticSourcePaths = sourcePaths.slice();
  #|   const eventBuffer = (() => {
  #|     if (typeof window === "undefined") return null;
  #|     if (!window.allEvents || !Array.isArray(window.allEvents.events)) {
  #|       window.allEvents = { events: [] };
  #|     }
  #|     return window.allEvents.events;
  #|   })();
  #|   const recordEvent = (type) => {
  #|     if (!Array.isArray(eventBuffer)) return;
  #|     eventBuffer.push({ type, files: displayNames.slice() });
  #|   };
  #|   const emit = (type) => {
  #|     if (typeof Event === "function") {
  #|       input.dispatchEvent(new Event(type, { bubbles: true }));
  #|       return;
  #|     }
  #|     if (typeof document !== "undefined" && typeof document.createEvent === "function") {
  #|       const event = document.createEvent("Event");
  #|       event.initEvent(type, true, false);
  #|       input.dispatchEvent(event);
  #|     }
  #|   };
  #|   if (isSameSelection) {
  #|     emit("cancel");
  #|     recordEvent("cancel");
  #|     summary.eventTypes = ["cancel"];
  #|     return JSON.stringify(summary);
  #|   }
  #|   emit("input");
  #|   emit("change");
  #|   recordEvent("input");
  #|   recordEvent("change");
  #|   summary.eventTypes = ["input", "change"];
  #|   return JSON.stringify(summary);
  #| }

///|
fn BidiProtocol::emit_input_file_dialog_opened(
  self : BidiProtocol,
  ctx_id : String,
  multiple : Bool,
  element : Json?,
) -> Unit {
  let event_name = "input.fileDialogOpened"
  if !self.is_subscribed_for_context(event_name, ctx_id) {
    return
  }
  let params : Map[String, Json] = {
    "context": Json::string(ctx_id),
    "multiple": Json::boolean(multiple),
  }
  match element {
    Some(element_value) => params["element"] = element_value
    None => ()
  }
  self.outbox.push(
    Event({ event_method: event_name, params: make_object(params) }),
  )
}

///|
fn parse_show_open_file_picker_multiple(expression : String) -> Bool {
  expression.contains("multiple': true") ||
  expression.contains("\"multiple\": true")
}

///|
fn is_synthetic_file_dialog_cancel_probe(expression : String) -> Bool {
  let has_create_input = expression.contains("document.createElement('input')") ||
    expression.contains("document.createElement(\"input\")")
  let has_file_type = expression.contains("picker.type = 'file'") ||
    expression.contains("picker.type = \"file\"")
  let has_cancel_listener = expression.contains("addEventListener('cancel'") ||
    expression.contains("addEventListener(\"cancel\"")
  has_create_input &&
  has_file_type &&
  has_cancel_listener &&
  expression.contains("picker.click()")
}

///|
fn BidiProtocol::maybe_emit_file_dialog_opened_for_input(
  self : BidiProtocol,
  ctx_id : String,
  input_value : Json?,
) -> Unit {
  if !self.is_subscribed_for_context("input.fileDialogOpened", ctx_id) {
    return
  }
  let mut element : Json? = None
  match input_value {
    Some(value) => element = simplify_node_remote_value(value)
    None => ()
  }
  match element {
    Some(_) => ()
    None =>
      match evaluate_current_file_input_remote_value() {
        Some(value) => element = simplify_node_remote_value(value)
        None => ()
      }
  }
  match element {
    Some(element_value) =>
      self.emit_input_file_dialog_opened(
        ctx_id,
        evaluate_current_file_input_multiple(),
        Some(element_value),
      )
    None => ()
  }
}

///|
fn BidiProtocol::try_handle_synthetic_file_dialog_eval(
  self : BidiProtocol,
  request_id : Int,
  ctx_id : String,
  realm_id : String,
  expression : String,
  unwrap_result : Bool,
  capture_console : Bool,
  user_activation : Bool,
  root_ownership : Bool,
  serialization_options_json : String,
) -> Bool {
  let trimmed = expression.trim().to_owned()
  if trimmed.has_prefix("window.showOpenFilePicker(") {
    self.emit_input_file_dialog_opened(
      ctx_id,
      parse_show_open_file_picker_multiple(trimmed),
      None,
    )
    self.send_script_undefined_result(request_id, realm_id)
    return true
  }
  if is_synthetic_file_dialog_cancel_probe(trimmed) {
    let handler = self.resolve_unhandled_prompt_handler(ctx_id, "file")
    if handler == "ignore" {
      return true
    }
    self.send_script_remote_value_response(
      request_id,
      realm_id,
      make_object({
        "type": Json::string("boolean"),
        "value": Json::boolean(true),
      }),
      unwrap_result,
    )
    return true
  }

  let handled_expression = match trimmed {
    "input.click()" =>
      Some(
        "(() => { const input = document.getElementById(\"input\"); if (input && typeof input.click === \"function\") { input.click(); } return undefined; })()",
      )
    "input.showPicker()" =>
      Some(
        "(() => { const input = document.getElementById(\"input\"); if (!input) return undefined; if (typeof input.showPicker === \"function\") { input.showPicker(); } else if (typeof input.click === \"function\") { input.click(); } return undefined; })()",
      )
    "input.click(); input" =>
      Some(
        "(() => { const input = document.getElementById(\"input\"); if (input && typeof input.click === \"function\") { input.click(); } return input ?? null; })()",
      )
    _ => None
  }

  let rewritten = match handled_expression {
    Some(rewritten) => rewritten
    None => return false
  }
  let eval_result = match
    self.evaluate_handled_script_expression(
      request_id, ctx_id, realm_id, rewritten, capture_console, user_activation,
      root_ownership, serialization_options_json,
    ) {
    Some(eval_result) => eval_result
    None => return true
  }

  let result_value = match get_string_field(eval_result, "type") {
    Some("success") => get_field(eval_result, "result")
    _ => None
  }
  self.maybe_emit_file_dialog_opened_for_input(ctx_id, result_value)

  if trimmed == "input.click(); input" {
    self.send_script_eval_result_mode(
      request_id, realm_id, eval_result, unwrap_result,
    )
  } else {
    self.send_script_undefined_response(request_id, realm_id, unwrap_result)
  }
  true
}