// Copyright 2013 The Chromium Authors
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.

#include "chrome/test/chromedriver/session_commands.h"

#include <algorithm>
#include <list>
#include <memory>
#include <thread>
#include <utility>

#include "base/files/file_util.h"
#include "base/functional/bind.h"
#include "base/functional/callback.h"
#include "base/json/json_reader.h"
#include "base/location.h"
#include "base/logging.h"  // For CHECK macros.
#include "base/strings/string_number_conversions.h"
#include "base/strings/string_util.h"
#include "base/strings/stringprintf.h"
#include "base/synchronization/waitable_event.h"
#include "base/system/sys_info.h"
#include "base/task/bind_post_task.h"
#include "base/task/single_thread_task_runner.h"
#include "base/time/time.h"
#include "base/types/expected.h"
#include "base/values.h"
#include "chrome/test/chromedriver/basic_types.h"
#include "chrome/test/chromedriver/bidimapper/bidimapper.h"
#include "chrome/test/chromedriver/capabilities.h"
#include "chrome/test/chromedriver/chrome/bidi_tracker.h"
#include "chrome/test/chromedriver/chrome/browser_info.h"
#include "chrome/test/chromedriver/chrome/chrome.h"
#include "chrome/test/chromedriver/chrome/chrome_desktop_impl.h"
#include "chrome/test/chromedriver/chrome/chrome_impl.h"
#include "chrome/test/chromedriver/chrome/device_manager.h"
#include "chrome/test/chromedriver/chrome/devtools_client_impl.h"
#include "chrome/test/chromedriver/chrome/devtools_event_listener.h"
#include "chrome/test/chromedriver/chrome/geoposition.h"
#include "chrome/test/chromedriver/chrome/status.h"
#include "chrome/test/chromedriver/chrome/web_view.h"
#include "chrome/test/chromedriver/chrome_launcher.h"
#include "chrome/test/chromedriver/command_listener.h"
#include "chrome/test/chromedriver/constants/version.h"
#include "chrome/test/chromedriver/logging.h"
#include "chrome/test/chromedriver/net/sync_websocket.h"
#include "chrome/test/chromedriver/net/sync_websocket_factory.h"
#include "chrome/test/chromedriver/session.h"
#include "chrome/test/chromedriver/util.h"
#include "services/device/public/cpp/generic_sensor/orientation_util.h"
#include "services/network/public/mojom/url_loader_factory.mojom.h"

namespace {

const int kWifiMask = 0x2;
const int k4GMask = 0x8;
const int k3GMask = 0x10;
const int k2GMask = 0x20;

const int kAirplaneModeLatency = 0;
const int kAirplaneModeThroughput = 0;
const int kWifiLatency = 2;
const int kWifiThroughput = 30720 * 1024;
const int k4GLatency = 20;
const int k4GThroughput = 4096 * 1024;
const int k3GLatency = 100;
const int k3GThroughput = 750 * 1024;
const int k2GLatency = 300;
const int k2GThroughput = 250 * 1024;

Status EvaluateScriptAndIgnoreResult(Session* session,
                                     std::string expression,
                                     const bool await_promise = false) {
  WebView* web_view = nullptr;
  Status status = session->GetTargetWindow(&web_view);
  if (status.IsError())
    return status;
  if (!web_view->IsServiceWorker() && web_view->IsDialogOpen()) {
    std::string alert_text;
    status = web_view->GetDialogMessage(alert_text);
    if (status.IsError())
      return Status(kUnexpectedAlertOpen);
    return Status(kUnexpectedAlertOpen, "{Alert text : " + alert_text + "}");
  }
  std::string frame_id = session->GetCurrentFrameId();
  std::unique_ptr<base::Value> result;
  return web_view->EvaluateScript(frame_id, expression, await_promise, &result);
}

void SetSerializedTimeout(base::DictValue& timeouts,
                          const std::string& key,
                          base::TimeDelta timeout) {
  if (timeout == base::TimeDelta::Max()) {
    timeouts.Set(key, base::Value());
  } else {
    SetSafeInt(timeouts, key, timeout.InMilliseconds());
  }
}

}  // namespace

InitSessionParams::InitSessionParams(
    network::mojom::URLLoaderFactory* factory,
    const SyncWebSocketFactory& socket_factory,
    DeviceManager* device_manager,
    const scoped_refptr<base::SingleThreadTaskRunner> cmd_task_runner,
    TerminateSessionCallback terminate_on_cmd)
    : url_loader_factory(factory),
      socket_factory(socket_factory),
      device_manager(device_manager),
      cmd_task_runner(cmd_task_runner),
      terminate_on_cmd(terminate_on_cmd) {}

InitSessionParams::InitSessionParams(const InitSessionParams& other) = default;

InitSessionParams::~InitSessionParams() = default;

// Look for W3C mode setting in InitSession command parameters.
bool GetW3CSetting(const base::DictValue& params) {
  const base::DictValue* options_dict = nullptr;

  const base::DictValue* caps_dict =
      params.FindDictByDottedPath("capabilities.alwaysMatch");
  if (caps_dict && GetChromeOptionsDictionary(*caps_dict, &options_dict)) {
    std::optional<bool> w3c = options_dict->FindBool("w3c");
    if (w3c.has_value())
      return *w3c;
  }

  const base::ListValue* list =
      params.FindListByDottedPath("capabilities.firstMatch");
  if (list && list->size()) {
    const base::Value& caps_dict_ref = (*list)[0];
    if (caps_dict_ref.is_dict() &&
        GetChromeOptionsDictionary(caps_dict_ref.GetDict(), &options_dict)) {
      std::optional<bool> w3c = options_dict->FindBool("w3c");
      if (w3c.has_value())
        return *w3c;
    }
  }

  caps_dict = params.FindDict("desiredCapabilities");
  if (caps_dict && GetChromeOptionsDictionary(*caps_dict, &options_dict)) {
    std::optional<bool> w3c = options_dict->FindBool("w3c");
    if (w3c.has_value())
      return *w3c;
  }

  if (!params.contains("capabilities") &&
      params.contains("desiredCapabilities")) {
    return false;
  }

  return kW3CDefault;
}

namespace {

std::string PlatformNameToW3C(const std::string& platform_name) {
  std::string result = base::ToLowerASCII(platform_name);
  if (base::StartsWith(result, "mac")) {
    result = "mac";
  }
  return result;
}

// Creates a JSON object (represented by base::DictValue) that contains
// the capabilities, for returning to the client app as the result of New
// Session command.
base::DictValue CreateCapabilities(Session* session,
                                   const Capabilities& capabilities,
                                   const base::DictValue& desired_caps) {
  base::DictValue caps;

  // Capabilities defined by W3C. Some of these capabilities have different
  // names in legacy mode.
  caps.Set("browserName", session->chrome->GetBrowserInfo()->is_headless_shell
                              ? kHeadlessShellCapabilityName
                              : kBrowserCapabilityName);
  caps.Set(session->w3c_compliant ? "browserVersion" : "version",
           session->chrome->GetBrowserInfo()->browser_version);
  std::string os_name = session->chrome->GetOperatingSystemName();
  if (os_name.find("Windows") != std::string::npos)
    os_name = "Windows";
  if (session->w3c_compliant) {
    caps.Set("platformName", PlatformNameToW3C(os_name));
  } else {
    caps.Set("platform", os_name);
  }
  caps.Set("pageLoadStrategy", session->chrome->page_load_strategy());
  caps.Set("acceptInsecureCerts", capabilities.accept_insecure_certs);

  const base::Value* proxy = desired_caps.Find("proxy");
  if (proxy == nullptr || proxy->is_none())
    caps.Set("proxy", base::DictValue());
  else
    caps.Set("proxy", proxy->Clone());

  // add setWindowRect based on whether we are desktop/android/remote
  if (capabilities.IsAndroid() || capabilities.IsRemoteBrowser()) {
    caps.Set("setWindowRect", false);
  } else {
    caps.Set("setWindowRect", true);
  }
  base::DictValue timeouts;
  SetSerializedTimeout(timeouts, "script", session->script_timeout);
  SetSerializedTimeout(timeouts, "pageLoad", session->page_load_timeout);
  SetSerializedTimeout(timeouts, "implicit", session->implicit_wait);
  caps.Set("timeouts", std::move(timeouts));
  caps.Set("strictFileInteractability", session->strict_file_interactability);
  caps.Set(session->w3c_compliant ? "unhandledPromptBehavior"
                                  : "unexpectedAlertBehaviour",
           session->unhandled_prompt_behavior.CapabilityView());

  // Extensions defined by the W3C.
  // See https://w3c.github.io/webauthn/#sctn-automation-webdriver-capability
  caps.Set("webauthn:virtualAuthenticators", !capabilities.IsAndroid());
  caps.Set("webauthn:extension:largeBlob", !capabilities.IsAndroid());
  caps.Set("webauthn:extension:minPinLength", !capabilities.IsAndroid());
  caps.Set("webauthn:extension:credBlob", !capabilities.IsAndroid());
  caps.Set("webauthn:extension:prf", !capabilities.IsAndroid());
  caps.Set("webauthn:extension:cmtgKey", !capabilities.IsAndroid());

  // See https://github.com/fedidcg/FedCM/pull/478
  caps.Set("fedcm:accounts", true);

  // Chrome-specific extensions.
  const std::string chrome_driver_version_key = base::StringPrintf(
      "%s.%sVersion", base::ToLowerASCII(kBrowserShortName).c_str(),
      base::ToLowerASCII(kChromeDriverProductShortName).c_str());
  caps.SetByDottedPath(chrome_driver_version_key, kChromeDriverVersion);
  if (session->chrome->GetBrowserInfo()->debugger_endpoint.IsValid()) {
    const std::string debugger_address_key = base::StringPrintf(
        "%s.debuggerAddress", kChromeDriverOptionsKeyPrefixed);
    caps.SetByDottedPath(debugger_address_key, session->chrome->GetBrowserInfo()
                                                   ->debugger_endpoint.Address()
                                                   .ToString());
  }

  // If the request to initialize a browser process came from localhost, and
  // not an intermediary node, send the main browser PID back in the custom
  // capabilities. The process_id should only be set in BrowserInfo in this
  // scenario.
  // This is done specifically for the purpose of testing accessibility APIs
  // after starting and interacting with a browser using chromedriver --
  // accessibility APIs are found via the browser PID. If we are starting a
  // process remotely, we cannot access the accessibility API and the PID is
  // not useful.
  if (!capabilities.IsRemoteBrowser()) {
    int desktop_process_id = session->chrome->GetBrowserInfo()->process_id;
    if (desktop_process_id) {
      caps.Set("goog:processID", desktop_process_id);
    }
  }

  ChromeDesktopImpl* desktop = nullptr;
  Status status = session->chrome->GetAsDesktop(&desktop);
  if (status.IsOk()) {
    const std::string user_data_key = base::StringPrintf(
        "%s.userDataDir", base::ToLowerASCII(kBrowserShortName).c_str());
    caps.SetByDottedPath(
        user_data_key,
        desktop->command().GetSwitchValuePath("user-data-dir").AsUTF8Unsafe());
    caps.Set("networkConnectionEnabled", desktop->IsNetworkConnectionEnabled());
  }

  // Legacy capabilities.
  if (!session->w3c_compliant) {
    caps.Set("javascriptEnabled", true);
    caps.Set("takesScreenshot", true);
    caps.Set("takesHeapSnapshot", true);
    caps.Set("handlesAlerts", true);
    caps.Set("databaseEnabled", false);
    caps.Set("locationContextEnabled", true);
    caps.Set("mobileEmulationEnabled",
             session->chrome->IsMobileEmulationEnabled());
    caps.Set("browserConnectionEnabled", false);
    caps.Set("cssSelectorsEnabled", true);
    caps.Set("webStorageEnabled", true);
    caps.Set("rotatable", false);
    caps.Set("acceptSslCerts", capabilities.accept_insecure_certs);
    caps.Set("nativeEvents", true);
    caps.Set("hasTouchScreen", session->chrome->HasTouchScreen());
  }

  if (session->web_socket_url) {
    caps.Set("webSocketUrl",
             "ws://" + session->host + "/session/" + session->id);
  }

  return caps;
}

Status InitSessionHelper(const InitSessionParams& bound_params,
                         Session* session,
                         const base::DictValue& params,
                         std::unique_ptr<base::Value>* value) {
  session->cmd_task_runner = bound_params.cmd_task_runner;
  session->terminate_on_cmd = base::BindPostTask(
      session->cmd_task_runner,
      base::BindOnce(bound_params.terminate_on_cmd, session->id), FROM_HERE);
  if (!bound_params.device_manager) {
    return Status{kSessionNotCreated, "device manager cannot be null"};
  }

  const base::DictValue* desired_caps;
  base::DictValue merged_caps;

  Capabilities capabilities;
  Status status = internal::ConfigureSession(session, params, desired_caps,
                                             merged_caps, &capabilities);
  if (status.IsError())
    return status;

  // Create Log's and DevToolsEventListener's for ones that are DevTools-based.
  // Session will own the Log's, Chrome will own the listeners.
  // Also create |CommandListener|s for the appropriate logs.
  std::vector<std::unique_ptr<DevToolsEventListener>> devtools_event_listeners;
  std::vector<std::unique_ptr<CommandListener>> command_listeners;
  status = CreateLogs(capabilities,
                      session,
                      &session->devtools_logs,
                      &devtools_event_listeners,
                      &command_listeners);
  if (status.IsError())
    return status;

  // |session| will own the |CommandListener|s.
  session->command_listeners.swap(command_listeners);

  if (session->web_socket_url) {
    // Suffixes used with the client channels.
    BidiTracker* bidi_tracker = new BidiTracker();
    bidi_tracker->SetChannelSuffix(std::move(Session::kChannelSuffix));
    bidi_tracker->SetBidiCallback(base::BindRepeating(
        &Session::OnBidiResponse, base::Unretained(session)));
    devtools_event_listeners.emplace_back(bidi_tracker);
  }

  status = LaunchChrome(
      bound_params.url_loader_factory, bound_params.socket_factory,
      *bound_params.device_manager, capabilities,
      std::move(devtools_event_listeners),
      base::BindRepeating(&Session::HandleMessagesAndTerminateIfNecessary),
      session->w3c_compliant, session->chrome);

  if (status.IsError())
    return status;

  if (capabilities.accept_insecure_certs) {
    status = session->chrome->SetAcceptInsecureCerts();
    if (status.IsError())
      return status;
  }

  status = session->chrome->GetWebViewIdForFirstTab(&session->window,
                                                    session->w3c_compliant);
  if (status.IsError())
    return status;
  session->detach = capabilities.detach;
  session->capabilities = std::make_unique<base::DictValue>(
      CreateCapabilities(session, capabilities, *desired_caps));

  status = internal::ConfigureHeadlessSession(session, capabilities);
  if (status.IsError())
    return status;

  if (session->w3c_compliant) {
    base::DictValue body;
    body.Set("capabilities", session->capabilities->Clone());
    body.Set("sessionId", session->id);
    *value = std::make_unique<base::Value>(std::move(body));
  } else {
    *value = std::make_unique<base::Value>(session->capabilities->Clone());
  }

  if (session->web_socket_url) {
    // The webview that will be visible to the user as the initial one.
    WebView* initial_web_view = nullptr;
    // The webview that will be used for running BiDi mapper. Can be either
    // a hidden target or a visible tab.
    WebView* bidi_mapper_web_view = nullptr;

    // Get the currently open web view.
    status = session->GetTargetWindow(&initial_web_view);
    if (status.IsError()) {
      return status;
    }

    // Navigate the initial page to `about:blank` to align with the html spec:
    // https://html.spec.whatwg.org/multipage/document-sequences.html#creating-a-new-browsing-context
    Timeout timeout(session->page_load_timeout);
    status = initial_web_view->Load("about:blank", &timeout);
    if (status.IsError()) {
      return status;
    }
    // Wait until the currently open web view's navigation is over.
    status = initial_web_view->WaitForPendingNavigations(
        session->GetCurrentFrameId(), Timeout(session->page_load_timeout),
        true);
    if (status.IsError()) {
      return status;
    }

    base::CommandLine* cmd_line = base::CommandLine::ForCurrentProcess();
    // Create a hidden target for running  BiDi-CDP mapper in it.
    status = session->chrome->NewHiddenTarget(
        session->window, session->w3c_compliant,
        &session->bidi_mapper_web_view_id);
    if (status.IsError()) {
      return status;
    }

    // Set the BiDi mapper web view to the new target.
    session->chrome->GetWebViewById(session->bidi_mapper_web_view_id,
                                    &bidi_mapper_web_view);

    // Wait until the initial navigation is over to prevent the mapper from
    // being evicted by the navigation.
    status = bidi_mapper_web_view->WaitForPendingNavigations(
        session->GetCurrentFrameId(), Timeout(session->page_load_timeout),
        true);
    if (status.IsError()) {
      return status;
    }

    // Start the mapper.
    base::FilePath bidi_mapper_path =
        cmd_line->GetSwitchValuePath("bidi-mapper-path");

    std::string mapper_script = kMapperScript;

    if (!bidi_mapper_path.empty()) {
      VLOG(0) << "Custom BiDi mapper path specified: " << bidi_mapper_path;

      if (!base::ReadFileToString(bidi_mapper_path, &mapper_script)) {
        return Status(StatusCode::kUnknownError,
                      "Failed to read the specified BiDi mapper path: " +
                          bidi_mapper_path.AsUTF8Unsafe());
      }
    }

    bool enable_unsafe_extension_debugging =
        capabilities.switches.HasSwitch("enable-unsafe-extension-debugging");
    status = bidi_mapper_web_view->StartBidiServer(
        mapper_script, enable_unsafe_extension_debugging);
    if (status.IsError()) {
      return status;
    }

    // Execute session.new for the newly-created mapper instance.
    base::DictValue bidi_cmd;
    bidi_cmd.Set("goog:channel", "/init-bidi-session");
    bidi_cmd.Set("id", 1);
    bidi_cmd.Set("params", params.Clone());
    bidi_cmd.Set("method", "session.new");
    base::DictValue bidi_response;
    status = bidi_mapper_web_view->SendBidiCommand(
        std::move(bidi_cmd), Timeout(base::Seconds(20)), bidi_response);
    if (status.IsError()) {
      return status;
    }
  }  // if (session->web_socket_url)

  return status;
}

}  // namespace

namespace internal {

Status ConfigureSession(Session* session,
                        const base::DictValue& params,
                        const base::DictValue*& desired_caps,
                        base::DictValue& merged_caps,
                        Capabilities* capabilities) {
  session->driver_log =
      std::make_unique<WebDriverLog>(WebDriverLog::kDriverType, Log::kAll);

  session->w3c_compliant = GetW3CSetting(params);
  if (session->w3c_compliant) {
    Status status = ProcessCapabilities(params, merged_caps);
    if (status.IsError())
      return status;
    desired_caps = &merged_caps;
  } else {
    const base::DictValue* caps = params.FindDict("desiredCapabilities");
    if (!caps)
      return Status(kSessionNotCreated, "Missing or invalid capabilities");
    desired_caps = caps;
  }

  Status status = capabilities->Parse(*desired_caps, session->w3c_compliant);
  if (status.IsError())
    return status;

  if (capabilities->unhandled_prompt_behavior) {
    session->unhandled_prompt_behavior =
        std::move(capabilities->unhandled_prompt_behavior).value();
  } else {
    session->unhandled_prompt_behavior = PromptBehavior(session->w3c_compliant);
  }

  session->implicit_wait = capabilities->implicit_wait_timeout;
  session->page_load_timeout = capabilities->page_load_timeout;
  session->script_timeout = capabilities->script_timeout;
  session->strict_file_interactability =
      capabilities->strict_file_interactability;
  session->web_socket_url = capabilities->web_socket_url;
  Log::Level driver_level = Log::kWarning;
  if (capabilities->logging_prefs.count(WebDriverLog::kDriverType))
    driver_level = capabilities->logging_prefs[WebDriverLog::kDriverType];
  session->driver_log->set_min_level(driver_level);

  return Status(kOk);
}

Status ConfigureHeadlessSession(Session* session,
                                const Capabilities& capabilities) {
  if (!session->chrome->GetBrowserInfo()->is_headless_shell) {
    return Status(kOk);
  }

  const std::string* download_directory = nullptr;
  if (capabilities.prefs) {
    download_directory = capabilities.prefs->FindStringByDottedPath(
        "download.default_directory");
    if (!download_directory) {
      download_directory =
          capabilities.prefs->FindString("download.default_directory");
    }
  }
  session->headless_download_directory = std::make_unique<std::string>(
      download_directory ? *download_directory : ".");

  WebView* first_view;
  session->chrome->GetWebViewById(session->window, &first_view);
  return first_view->OverrideDownloadDirectoryIfNeeded(
      *session->headless_download_directory);
}

}  // namespace internal

bool MergeCapabilities(const base::DictValue& always_match,
                       const base::DictValue& first_match,
                       base::DictValue& merged) {
  merged.clear();

  for (auto kv : first_match) {
    if (always_match.Find(kv.first)) {
      // `first_match` cannot have the same `keys` as `always_match`.
      return false;
    }
  }

  // merge the capabilities together since guaranteed no key collisions
  merged = always_match.Clone();
  merged.Merge(first_match.Clone());
  return true;
}

// Implementation of "matching capabilities", as defined in W3C spec at
// https://www.w3.org/TR/webdriver/#dfn-matching-capabilities.
// It checks some requested capabilities and make sure they are supported.
// Currently, we only check "browserName", "platformName", "fedcm:accounts"
// and webauthn capabilities but more can be added as necessary.
bool MatchCapabilities(const base::DictValue& capabilities) {
  const base::Value* name = capabilities.Find("browserName");
  if (name && !name->is_none()) {
    if (!name->is_string()) {
      return false;
    }
    if (name->GetString() != kBrowserCapabilityName &&
        name->GetString() != kHeadlessShellCapabilityName) {
      return false;
    }
  }

  const base::DictValue* chrome_options;
  const bool has_chrome_options =
      GetChromeOptionsDictionary(capabilities, &chrome_options);

  bool is_android = has_chrome_options &&
                    chrome_options->FindString("androidPackage") != nullptr;

  const base::Value* platform_name_value = capabilities.Find("platformName");
  if (platform_name_value && !platform_name_value->is_none()) {
    if (!platform_name_value->is_string())
      return false;

    std::string requested_platform_name = platform_name_value->GetString();
    std::string requested_first_token =
        requested_platform_name.substr(0, requested_platform_name.find(' '));

    std::string actual_platform_name =
        base::ToLowerASCII(base::SysInfo::OperatingSystemName());
    std::string actual_first_token =
        actual_platform_name.substr(0, actual_platform_name.find(' '));

    bool is_remote = has_chrome_options &&
                     chrome_options->FindString("debuggerAddress") != nullptr;
    if (requested_platform_name == "any" || is_remote ||
        (is_android && requested_platform_name == "android")) {
      // "any" can be used as a wild card for platformName.
      // if |is_remote| there is no easy way to know
      // target platform. Android check also occurs here.
      // If any of the above cases pass, we return true.
    } else if (is_android && requested_platform_name != "android") {
      return false;
    } else if (requested_first_token == "mac" ||
               requested_first_token == "windows" ||
               requested_first_token == "linux") {
      if (actual_first_token != requested_first_token)
        return false;
    } else if (requested_platform_name != actual_platform_name) {
      return false;
    }
  }

  const base::Value* virtual_authenticators_value =
      capabilities.Find("webauthn:virtualAuthenticators");
  if (virtual_authenticators_value) {
    if (!virtual_authenticators_value->is_bool() ||
        (virtual_authenticators_value->GetBool() && is_android)) {
      return false;
    }
  }

  const base::Value* large_blob_value =
      capabilities.Find("webauthn:extension:largeBlob");
  if (large_blob_value) {
    if (!large_blob_value->is_bool() ||
        (large_blob_value->GetBool() && is_android)) {
      return false;
    }
  }

  const base::Value* fedcm_accounts_value = capabilities.Find("fedcm:accounts");
  if (fedcm_accounts_value) {
    if (!fedcm_accounts_value->is_bool() || !fedcm_accounts_value->GetBool()) {
      return false;
    }
  }

  return true;
}

// Implementation of "process capabilities", as defined in W3C spec at
// https://www.w3.org/TR/webdriver/#processing-capabilities. Step numbers in
// the comments correspond to the step numbers in the spec.
Status ProcessCapabilities(const base::DictValue& params,
                           base::DictValue& result_capabilities) {
  // 1. Get the property "capabilities" from parameters.
  const base::DictValue* capabilities_request = params.FindDict("capabilities");
  if (!capabilities_request)
    return Status(kInvalidArgument, "'capabilities' must be a JSON object");

  // 2. Get the property "alwaysMatch" from capabilities request.
  const base::DictValue empty_object;
  const base::DictValue* required_capabilities;
  const base::Value* required_capabilities_value =
      capabilities_request->Find("alwaysMatch");
  if (required_capabilities_value == nullptr) {
    required_capabilities = &empty_object;
  } else if (required_capabilities_value->is_dict()) {
    required_capabilities = &required_capabilities_value->GetDict();
    Capabilities cap;
    Status status = cap.Parse(*required_capabilities);
    if (status.IsError())
      return status;
  } else {
    return Status(kInvalidArgument, "'alwaysMatch' must be a JSON object");
  }

  // 3. Get the property "firstMatch" from capabilities request.
  base::ListValue default_list;
  const base::ListValue* all_first_match_capabilities;
  const base::Value* all_first_match_capabilities_value =
      capabilities_request->Find("firstMatch");
  if (all_first_match_capabilities_value == nullptr) {
    default_list.Append(base::DictValue());
    all_first_match_capabilities = &default_list;
  } else if (all_first_match_capabilities_value->is_list()) {
    all_first_match_capabilities =
        &all_first_match_capabilities_value->GetList();
    if (all_first_match_capabilities->size() < 1) {
      return Status(kInvalidArgument,
                    "'firstMatch' must contain at least one entry");
    }
  } else {
    return Status(kInvalidArgument, "'firstMatch' must be a JSON list");
  }

  // 4. Let validated first match capabilities be an empty JSON List.
  std::vector<const base::DictValue*> validated_first_match_capabilities;

  // 5. Validate all first match capabilities.
  for (size_t i = 0; i < all_first_match_capabilities->size(); ++i) {
    const base::Value& first_match = (*all_first_match_capabilities)[i];
    if (!first_match.is_dict()) {
      return Status(kInvalidArgument,
                    base::StringPrintf(
                        "entry %zu of 'firstMatch' must be a JSON object", i));
    }
    Capabilities cap;
    Status status = cap.Parse(first_match.GetDict());
    if (status.IsError())
      return Status(
          kInvalidArgument,
          base::StringPrintf("entry %zu of 'firstMatch' is invalid", i),
          status);
    validated_first_match_capabilities.push_back(&first_match.GetDict());
  }

  // 6. Let merged capabilities be an empty List.
  std::vector<base::DictValue> merged_capabilities;

  // 7. Merge capabilities.
  for (size_t i = 0; i < validated_first_match_capabilities.size(); ++i) {
    const base::DictValue* first_match_capabilities =
        validated_first_match_capabilities[i];
    base::DictValue merged;
    if (!MergeCapabilities(*required_capabilities, *first_match_capabilities,
                           merged)) {
      return Status(
          kInvalidArgument,
          base::StringPrintf(
              "unable to merge 'alwaysMatch' with entry %zu of 'firstMatch'",
              i));
    }
    merged_capabilities.emplace_back(std::move(merged));
  }

  // 8. Match capabilities.
  for (auto& capabilities : merged_capabilities) {
    if (MatchCapabilities(capabilities)) {
      result_capabilities = std::move(capabilities);
      return Status(kOk);
    }
  }

  // 9. The spec says "return success with data null", but then the caller is
  // instructed to return error when the data is null. Since we don't have a
  // convenient way to return data null, we will take a shortcut and return an
  // error directly.
  return Status(kSessionNotCreated, "No matching capabilities found");
}

Status ExecuteInitSession(const InitSessionParams& bound_params,
                          Session* session,
                          const base::DictValue& params,
                          std::unique_ptr<base::Value>* value) {
  Status status = InitSessionHelper(bound_params, session, params, value);
  if (status.IsError()) {
    session->quit = true;
    if (session->chrome != nullptr)
      session->chrome->Quit();
    return status;
  }

  return status;
}

Status ExecuteQuit(bool allow_detach,
                   Session* session,
                   const base::DictValue& params,
                   std::unique_ptr<base::Value>* value) {
  session->quit = true;
  if (allow_detach && session->detach)
    return Status(kOk);
  return session->chrome->Quit();
}

// Quits a session.
Status ExecuteBidiSessionEnd(Session* session,
                             const base::DictValue& params,
                             std::unique_ptr<base::Value>* value) {
  Status status{kOk};
  WebView* web_view = nullptr;
  status = session->chrome->GetWebViewById(session->bidi_mapper_web_view_id,
                                           &web_view);
  if (status.IsOk()) {
    // Attempting to forward any pending BiDi responses / events.
    status = web_view->HandleReceivedEvents();
  }

  if (status.IsError()) {
    VLOG(0) << "Ignoring the error while shutting down a BiDi session: "
            << status.message();
  }

  session->quit = true;
  status = session->chrome->Quit();
  if (status.IsOk()) {
    *value = std::make_unique<base::Value>(base::Value::Type::DICT);
  }
  return status;
}

Status ExecuteGetSessionCapabilities(Session* session,
                                     const base::DictValue& params,
                                     std::unique_ptr<base::Value>* value) {
  *value = std::make_unique<base::Value>(session->capabilities->Clone());
  return Status(kOk);
}

Status ExecuteGetCurrentWindowHandle(Session* session,
                                     const base::DictValue& params,
                                     std::unique_ptr<base::Value>* value) {
  WebView* web_view = nullptr;
  Status status = session->GetTargetWindow(&web_view);
  if (status.IsError())
    return status;
  *value = std::make_unique<base::Value>(web_view->GetId());
  return Status(kOk);
}

Status ExecuteClose(Session* session,
                    const base::DictValue& params,
                    std::unique_ptr<base::Value>* value) {
  std::list<std::string> web_view_ids;
  Status status = session->chrome->GetTopLevelWebViewIds(
      &web_view_ids, session->w3c_compliant);
  if (status.IsError())
    return status;
  bool is_last_web_view = web_view_ids.size() == 1u;
  if (session->web_socket_url) {
    is_last_web_view = web_view_ids.size() <= 2u;
  }
  web_view_ids.clear();

  WebView* web_view = nullptr;
  status = session->GetTargetWindow(&web_view);
  if (status.IsError())
    return status;

  status = web_view->HandleReceivedEvents();
  if (status.IsError())
    return status;

  if (web_view->IsDialogOpen()) {
    std::string alert_text;
    status = web_view->GetDialogMessage(alert_text);
    if (status.IsError())
      return status;

    std::string dialog_type;
    status = web_view->GetTypeOfDialog(dialog_type);
    if (status.IsError()) {
      return status;
    }

    PromptHandlerConfiguration prompt_handler_configuration;
    status = session->unhandled_prompt_behavior.GetConfiguration(
        dialog_type, prompt_handler_configuration);
    if (status.IsError()) {
      return status;
    }

    if (prompt_handler_configuration.type == PromptHandlerType::kAccept ||
        prompt_handler_configuration.type == PromptHandlerType::kDismiss) {
      status = web_view->HandleDialog(
          prompt_handler_configuration.type == PromptHandlerType::kAccept,
          session->prompt_text);
      if (status.IsError()) {
        return status;
      }
    }

    if (prompt_handler_configuration.notify) {
      return Status(kUnexpectedAlertOpen, "{Alert text : " + alert_text + "}");
    }
  }

  status = session->chrome->CloseWebView(web_view->GetTabId());
  if (status.IsError())
    return status;

  if (is_last_web_view) {
    // If there is only one window left, call quit as well.
    session->quit = true;
    status = session->chrome->Quit();
    if (status.IsOk())
      *value = std::make_unique<base::Value>(base::Value::Type::LIST);
  } else {
    status = ExecuteGetWindowHandles(session, base::DictValue(), value);
    if (status.IsError())
      return status;
  }

  return status;
}

Status ExecuteGetWindowHandles(Session* session,
                               const base::DictValue& params,
                               std::unique_ptr<base::Value>* value) {
  std::list<std::string> tab_view_ids;
  Status status = session->chrome->GetTopLevelWebViewIds(
      &tab_view_ids, session->w3c_compliant);
  if (status.IsError()) {
    return status;
  }

  base::ListValue window_ids;
  for (std::list<std::string>::const_iterator it = tab_view_ids.begin();
       it != tab_view_ids.end(); ++it) {
    WebView* page = nullptr;
    status = session->chrome->GetActivePageByWebViewId(*it, &page,
                                                       /*wait_for_page=*/false);
    if (status.IsError()) {
      if (status.code() == kNoActivePage) {
        continue;
      }
      return status;
    }
    CHECK(page != nullptr);
    window_ids.Append(page->GetId());
  }

  if (session->web_socket_url) {
    const std::string& (base::Value::*get_string)() const =
        &base::Value::GetString;
    auto it = std::ranges::find(window_ids, session->bidi_mapper_web_view_id,
                                get_string);
    if (it != window_ids.end()) {
      window_ids.erase(it);
    }
  }

  *value = std::make_unique<base::Value>(std::move(window_ids));
  return Status(kOk);
}

Status ExecuteSwitchToWindow(Session* session,
                             const base::DictValue& params,
                             std::unique_ptr<base::Value>* value) {
  const std::string* name;
  if (session->w3c_compliant) {
    name = params.FindString("handle");
    if (!name)
      return Status(kInvalidArgument, "'handle' must be a string");
  } else {
    name = params.FindString("name");
    if (!name)
      return Status(kInvalidArgument, "'name' must be a string");
  }

  std::list<std::string> tab_view_ids;
  Status status = session->chrome->GetTopLevelWebViewIds(
      &tab_view_ids, session->w3c_compliant);
  if (status.IsError())
    return status;

  // Find active web page view for each tab.
  std::unordered_map<std::string, WebView*> tab_to_active_webview;
  std::vector<std::unique_ptr<WebViewHolder>> web_view_locks;
  for (auto& tab_id : tab_view_ids) {
    WebView* active_page = nullptr;
    status = session->chrome->GetActivePageByWebViewId(tab_id, &active_page,
                                                       /*wait_for_page=*/false);
    if (status.IsError()) {
      if (status.code() == kNoActivePage) {
        continue;
      }
      return status;
    }
    web_view_locks.push_back(active_page->GetHolder());
    tab_to_active_webview[tab_id] = active_page;
  }

  WebView* web_view = nullptr;
  std::string web_view_id;
  bool found = false;
  // Check if any tab_view or web_view matches |name|.
  for (auto& it : tab_to_active_webview) {
    if (it.first == *name || it.second->GetId() == *name) {
      web_view = it.second;
      web_view_id = it.first;
      found = true;
      break;
    }
  }
  if (!found) {
    // Check if any of the tab window names match |name|.
    const char* kGetWindowNameScript = "function() { return window.name; }";
    base::ListValue args;
    for (auto& it : tab_to_active_webview) {
      std::unique_ptr<base::Value> result;
      status = it.second->CallFunction(std::string(), kGetWindowNameScript,
                                       args, &result);
      if (status.IsError())
        return status;
      if (!result->is_string())
        return Status(kUnknownError, "failed to get window name");
      if (result->GetString() == *name) {
        web_view = it.second;
        web_view_id = it.first;
        found = true;
        break;
      }
    }
  }

  if (!found)
    return Status(kNoSuchWindow);

  if (session->overridden_geoposition ||
      session->overridden_network_conditions ||
      session->headless_download_directory ||
      session->chrome->IsMobileEmulationEnabled()) {
    // apply type specific configurations:
    if (session->overridden_geoposition) {
      status = web_view->OverrideGeolocation(*session->overridden_geoposition);
      if (status.IsError())
        return status;
    }
    if (session->overridden_network_conditions) {
      status = web_view->OverrideNetworkConditions(
          *session->overridden_network_conditions);
      if (status.IsError())
        return status;
    }
    if (session->headless_download_directory) {
      status = web_view->OverrideDownloadDirectoryIfNeeded(
          *session->headless_download_directory);
      if (status.IsError())
        return status;
    }
  }

  status = session->chrome->ActivateWebView(web_view_id);
  if (status.IsError())
    return status;
  session->window = web_view_id;
  session->SwitchToTopFrame();
  session->mouse_position = WebPoint(0, 0);
  return Status(kOk);
}

// Handles legacy format SetTimeout command.
// TODO(crbug.com/chromedriver/2596): Remove when we stop supporting legacy
// protocol.
Status ExecuteSetTimeoutLegacy(Session* session,
                               const base::DictValue& params,
                               std::unique_ptr<base::Value>* value) {
  std::optional<double> maybe_ms = params.FindDouble("ms");
  if (!maybe_ms.has_value())
    return Status(kInvalidArgument, "'ms' must be a double");

  const std::string* type = params.FindString("type");
  if (!type)
    return Status(kInvalidArgument, "'type' must be a string");

  base::TimeDelta timeout =
      base::Milliseconds(static_cast<int>(maybe_ms.value()));
  if (*type == "implicit") {
    session->implicit_wait = timeout;
  } else if (*type == "script") {
    session->script_timeout = timeout;
  } else if (*type == "page load") {
    session->page_load_timeout =
        ((timeout.is_negative()) ? Session::kDefaultPageLoadTimeout : timeout);
  } else {
    return Status(kInvalidArgument, "unknown type of timeout:" + *type);
  }
  return Status(kOk);
}

Status ExecuteSetTimeoutsW3C(Session* session,
                             const base::DictValue& params,
                             std::unique_ptr<base::Value>* value) {
  for (auto setting : params) {
    int64_t timeout_ms_int64 = -1;
    base::TimeDelta timeout;
    const std::string& type = setting.first;
    if (setting.second.is_none()) {
      timeout = base::TimeDelta::Max();
    } else {
      if (!GetOptionalSafeInt(params, setting.first, &timeout_ms_int64) ||
          timeout_ms_int64 < 0) {
        return Status(kInvalidArgument, "value must be a non-negative integer");
      }
      timeout = base::Milliseconds(timeout_ms_int64);
    }
    if (type == "script") {
      session->script_timeout = timeout;
    } else if (type == "pageLoad") {
      session->page_load_timeout = timeout;
    } else if (type == "implicit") {
      session->implicit_wait = timeout;
    }
  }
  return Status(kOk);
}

Status ExecuteSetTimeouts(Session* session,
                          const base::DictValue& params,
                          std::unique_ptr<base::Value>* value) {
  // TODO(crbug.com/chromedriver/2596): Remove legacy version support when we
  // stop supporting non-W3C protocol. At that time, we can delete the legacy
  // function and merge the W3C function into this function.
  if (params.contains("ms"))
    return ExecuteSetTimeoutLegacy(session, params, value);
  return ExecuteSetTimeoutsW3C(session, params, value);
}

Status ExecuteGetTimeouts(Session* session,
                          const base::DictValue& params,
                          std::unique_ptr<base::Value>* value) {
  base::DictValue timeouts;
  SetSerializedTimeout(timeouts, "script", session->script_timeout);
  SetSerializedTimeout(timeouts, "pageLoad", session->page_load_timeout);
  SetSerializedTimeout(timeouts, "implicit", session->implicit_wait);

  *value = base::Value::ToUniquePtrValue(base::Value(std::move(timeouts)));
  return Status(kOk);
}

Status ExecuteSetScriptTimeout(Session* session,
                               const base::DictValue& params,
                               std::unique_ptr<base::Value>* value) {
  std::optional<double> maybe_ms = params.FindDouble("ms");
  if (!maybe_ms.has_value() || maybe_ms.value() < 0)
    return Status(kInvalidArgument, "'ms' must be a non-negative number");
  session->script_timeout =
      base::Milliseconds(static_cast<int>(maybe_ms.value()));
  return Status(kOk);
}

Status ExecuteImplicitlyWait(Session* session,
                             const base::DictValue& params,
                             std::unique_ptr<base::Value>* value) {
  std::optional<double> maybe_ms = params.FindDouble("ms");
  if (!maybe_ms.has_value() || maybe_ms.value() < 0)
    return Status(kInvalidArgument, "'ms' must be a non-negative number");
  session->implicit_wait =
      base::Milliseconds(static_cast<int>(maybe_ms.value()));
  return Status(kOk);
}

Status ExecuteIsLoading(Session* session,
                        const base::DictValue& params,
                        std::unique_ptr<base::Value>* value) {
  WebView* web_view = nullptr;
  Status status = session->GetTargetWindow(&web_view);
  if (status.IsError())
    return status;

  bool is_pending;
  status = web_view->IsPendingNavigation(nullptr, &is_pending);
  if (status.IsError())
    return status;
  *value = std::make_unique<base::Value>(is_pending);
  return Status(kOk);
}

Status ExecuteCreateVirtualSensor(Session* session,
                                  const base::DictValue& params,
                                  std::unique_ptr<base::Value>* value) {
  WebView* web_view = nullptr;
  Status status = session->GetTargetWindow(&web_view);
  if (status.IsError()) {
    return status;
  }

  const std::string* type = params.FindString("type");
  if (!type) {
    return Status(kInvalidArgument, "'type' must be a string");
  }

  base::DictValue args;
  args.Set("enabled", true);
  args.Set("type", *type);

  base::DictValue metadata;
  metadata.Set("available", params.FindBool("connected").value_or(true));
  if (auto minimum_sampling_frequency =
          params.FindDouble("minSamplingFrequency");
      minimum_sampling_frequency) {
    metadata.Set("minimumFrequency", minimum_sampling_frequency.value());
  }
  if (auto maximum_sampling_frequency =
          params.FindDouble("maxSamplingFrequency");
      maximum_sampling_frequency) {
    metadata.Set("maximumFrequency", maximum_sampling_frequency.value());
  }
  args.Set("metadata", std::move(metadata));

  return web_view->SendCommand("Emulation.setSensorOverrideEnabled", args);
}

namespace {

bool ParseSingleValue(const std::string& key_name,
                      const base::DictValue& params,
                      base::DictValue* out_params) {
  std::optional<double> value = params.FindDouble(key_name);
  if (!value.has_value()) {
    return false;
  }
  // Construct a dict that looks like this:
  // {
  //   single: {
  //     value: VAL
  //   }
  // }
  out_params->Set("single", base::DictValue().Set("value", *value));
  return true;
}

bool ParseXYZValue(const base::DictValue& params, base::DictValue* out_params) {
  std::optional<double> x = params.FindDouble("x");
  if (!x.has_value()) {
    return false;
  }
  std::optional<double> y = params.FindDouble("y");
  if (!y.has_value()) {
    return false;
  }
  std::optional<double> z = params.FindDouble("z");
  if (!z.has_value()) {
    return false;
  }
  // Construct a dict that looks like this:
  // {
  //   xyz: {
  //     x: VAL1,
  //     y: VAL2,
  //     z: VAL3
  //   }
  // }
  out_params->Set("xyz",
                  base::DictValue().Set("x", *x).Set("y", *y).Set("z", *z));
  return true;
}

bool ParseOrientationEuler(const base::DictValue& params,
                           base::DictValue* out_params) {
  if (!params.contains("alpha") || !params.contains("beta") ||
      !params.contains("gamma")) {
    return false;
  }

  std::optional<double> alpha = params.FindDouble("alpha");
  if (!alpha.has_value()) {
    return false;
  }
  std::optional<double> beta = params.FindDouble("beta");
  if (!beta.has_value()) {
    return false;
  }
  std::optional<double> gamma = params.FindDouble("gamma");
  if (!gamma.has_value()) {
    return false;
  }
  device::SensorReading quaternion_readings;
  if (!device::ComputeQuaternionFromEulerAngles(*alpha, *beta, *gamma,
                                                &quaternion_readings)) {
    return false;
  }

  // Construct a dict that looks like this:
  // {
  //   quaternion: {
  //     x: VAL1,
  //     y: VAL2,
  //     z: VAL3,
  //     w: VAL4
  //   }
  // }
  const double x = quaternion_readings.orientation_quat.x;
  const double y = quaternion_readings.orientation_quat.y;
  const double z = quaternion_readings.orientation_quat.z;
  const double w = quaternion_readings.orientation_quat.w;
  out_params->Set(
      "quaternion",
      base::DictValue().Set("x", x).Set("y", y).Set("z", z).Set("w", w));
  return true;
}

base::expected<base::DictValue, Status> ParseSensorUpdateParams(
    const base::DictValue& params) {
  base::DictValue cdp_params;

  const std::string* type = params.FindString("type");
  if (!type) {
    return base::unexpected(
        Status(kInvalidArgument, "'type' must be a string"));
  }
  cdp_params.Set("type", *type);

  const base::DictValue* reading_dict = params.FindDict("reading");
  if (!reading_dict) {
    return base::unexpected(
        Status(kInvalidArgument, "Missing 'reading' field"));
  }

  base::DictValue reading;
  if (*type == "ambient-light") {
    if (!ParseSingleValue("illuminance", *reading_dict, &reading)) {
      return base::unexpected(
          Status(kInvalidArgument, "Could not parse illuminance"));
    }
  } else if (*type == "accelerometer" || *type == "gravity" ||
             *type == "gyroscope" || *type == "linear-acceleration" ||
             *type == "magnetometer") {
    if (!ParseXYZValue(*reading_dict, &reading)) {
      return base::unexpected(
          Status(kInvalidArgument, "Could not parse XYZ fields"));
    }
  } else if (*type == "absolute-orientation" ||
             *type == "relative-orientation") {
    if (!ParseOrientationEuler(*reading_dict, &reading)) {
      return base::unexpected(Status(
          kInvalidArgument, "Could not parse " + *type +
                                " readings. Invalid alpha/beta/gamma values"));
    }
  } else {
    return base::unexpected(Status(
        kInvalidArgument, "Unexpected type " + *type + " in 'type' field"));
  }
  cdp_params.Set("reading", std::move(reading));

  return cdp_params;
}

}  // namespace

Status ExecuteUpdateVirtualSensor(Session* session,
                                  const base::DictValue& params,
                                  std::unique_ptr<base::Value>*) {
  WebView* web_view = nullptr;
  Status status = session->GetTargetWindow(&web_view);
  if (status.IsError()) {
    return status;
  }

  auto cdp_params = ParseSensorUpdateParams(params);
  if (!cdp_params.has_value()) {
    return cdp_params.error();
  }

  return web_view->SendCommand("Emulation.setSensorOverrideReadings",
                               cdp_params.value());
}

Status ExecuteRemoveVirtualSensor(Session* session,
                                  const base::DictValue& params,
                                  std::unique_ptr<base::Value>* value) {
  WebView* web_view = nullptr;
  Status status = session->GetTargetWindow(&web_view);
  if (status.IsError()) {
    return status;
  }

  const std::string* type = params.FindString("type");

  if (!type) {
    return Status(kInvalidArgument, "'type' must be a string");
  }

  base::DictValue args;
  args.Set("enabled", false);
  args.Set("type", *type);

  return web_view->SendCommand("Emulation.setSensorOverrideEnabled", args);
}

Status ExecuteGetVirtualSensorInformation(Session* session,
                                          const base::DictValue& params,
                                          std::unique_ptr<base::Value>* value) {
  WebView* web_view = nullptr;
  Status status = session->GetTargetWindow(&web_view);
  if (status.IsError()) {
    return status;
  }

  const std::string* type = params.FindString("type");
  if (!type) {
    return Status(kInvalidArgument, "'type' must be a string");
  }

  base::DictValue args;
  args.Set("type", *type);

  return web_view->SendCommandAndGetResult(
      "Emulation.getOverriddenSensorInformation", args, value);
}

Status ExecuteGetLocation(Session* session,
                          const base::DictValue& params,
                          std::unique_ptr<base::Value>* value) {
  if (!session->overridden_geoposition) {
    return Status(kUnknownError,
                  "Location must be set before it can be retrieved");
  }
  base::DictValue location;
  location.Set("latitude", session->overridden_geoposition->latitude);
  location.Set("longitude", session->overridden_geoposition->longitude);
  location.Set("accuracy", session->overridden_geoposition->accuracy);
  // Set a dummy altitude to make WebDriver clients happy.
  // https://code.google.com/p/chromedriver/issues/detail?id=281
  location.Set("altitude", 0);
  *value = base::Value::ToUniquePtrValue(base::Value(std::move(location)));
  return Status(kOk);
}

Status ExecuteGetNetworkConnection(Session* session,
                                   const base::DictValue& params,
                                   std::unique_ptr<base::Value>* value) {
  ChromeDesktopImpl* desktop = nullptr;
  Status status = session->chrome->GetAsDesktop(&desktop);
  if (status.IsError())
    return status;
  if (!desktop->IsNetworkConnectionEnabled())
    return Status(kUnknownError, "network connection must be enabled");

  int connection_type = 0;
  connection_type = desktop->GetNetworkConnection();

  *value = std::make_unique<base::Value>(connection_type);
  return Status(kOk);
}

Status ExecuteGetNetworkConditions(Session* session,
                                   const base::DictValue& params,
                                   std::unique_ptr<base::Value>* value) {
  if (!session->overridden_network_conditions) {
    return Status(kUnknownError,
                  "network conditions must be set before it can be retrieved");
  }
  base::DictValue conditions =
      base::DictValue()
          .Set("offline", session->overridden_network_conditions->offline)
          .Set("latency", session->overridden_network_conditions->latency)
          .Set("download_throughput",
               session->overridden_network_conditions->download_throughput)
          .Set("upload_throughput",
               session->overridden_network_conditions->upload_throughput);
  *value = base::Value::ToUniquePtrValue(base::Value(std::move(conditions)));
  return Status(kOk);
}

Status ExecuteSetNetworkConnection(Session* session,
                                   const base::DictValue& params,
                                   std::unique_ptr<base::Value>* value) {
  ChromeDesktopImpl* desktop = nullptr;
  Status status = session->chrome->GetAsDesktop(&desktop);
  if (status.IsError())
    return status;
  if (!desktop->IsNetworkConnectionEnabled())
    return Status(kUnknownError, "network connection must be enabled");

  std::optional<int> connection_type =
      params.FindIntByDottedPath("parameters.type");
  if (!connection_type)
    return Status(kInvalidArgument, "invalid connection_type");

  desktop->SetNetworkConnection(*connection_type);

  std::unique_ptr<NetworkConditions> network_conditions(
      new NetworkConditions());

  if (*connection_type & kWifiMask) {
    network_conditions->latency = kWifiLatency;
    network_conditions->upload_throughput = kWifiThroughput;
    network_conditions->download_throughput = kWifiThroughput;
    network_conditions->offline = false;
  } else if (*connection_type & k4GMask) {
    network_conditions->latency = k4GLatency;
    network_conditions->upload_throughput = k4GThroughput;
    network_conditions->download_throughput = k4GThroughput;
    network_conditions->offline = false;
  } else if (*connection_type & k3GMask) {
    network_conditions->latency = k3GLatency;
    network_conditions->upload_throughput = k3GThroughput;
    network_conditions->download_throughput = k3GThroughput;
    network_conditions->offline = false;
  } else if (*connection_type & k2GMask) {
    network_conditions->latency = k2GLatency;
    network_conditions->upload_throughput = k2GThroughput;
    network_conditions->download_throughput = k2GThroughput;
    network_conditions->offline = false;
  } else {
    network_conditions->latency = kAirplaneModeLatency;
    network_conditions->upload_throughput = kAirplaneModeThroughput;
    network_conditions->download_throughput = kAirplaneModeThroughput;
    network_conditions->offline = true;
  }

  session->overridden_network_conditions.reset(
      network_conditions.release());

  // Applies overridden network connection to all WebViews of the session
  // to ensure that network emulation is applied on a per-session basis
  // rather than the just to the current WebView.
  std::list<std::string> tab_view_ids;
  status = session->chrome->GetTopLevelWebViewIds(&tab_view_ids,
                                                  session->w3c_compliant);
  if (status.IsError())
    return status;

  for (std::string tab_view_id : tab_view_ids) {
    WebView* web_view;
    status = session->chrome->GetActivePageByWebViewId(tab_view_id, &web_view,
                                                       /*wait_for_page=*/false);
    if (status.IsError()) {
      if (status.code() == kNoActivePage) {
        continue;
      }
      return status;
    }
    std::unique_ptr<WebViewHolder> scoped_web_view_lock = web_view->GetHolder();
    web_view->OverrideNetworkConditions(
        *session->overridden_network_conditions);
  }

  *value = std::make_unique<base::Value>(*connection_type);
  return Status(kOk);
}

Status ExecuteGetWindowPosition(Session* session,
                                const base::DictValue& params,
                                std::unique_ptr<base::Value>* value) {
  Chrome::WindowRect window_rect;
  Status status = session->chrome->GetWindowRect(session->window, &window_rect);

  if (status.IsError())
    return status;

  base::DictValue position =
      base::DictValue().Set("x", window_rect.x).Set("y", window_rect.y);
  *value = base::Value::ToUniquePtrValue(base::Value(std::move(position)));
  return Status(kOk);
}

Status ExecuteSetWindowPosition(Session* session,
                                const base::DictValue& params,
                                std::unique_ptr<base::Value>* value) {
  std::optional<double> maybe_x = params.FindDouble("x");
  std::optional<double> maybe_y = params.FindDouble("y");

  if (!maybe_x.has_value() || !maybe_y.has_value())
    return Status(kInvalidArgument, "missing or invalid 'x' or 'y'");

  base::DictValue rect_params;
  rect_params.Set("x", static_cast<int>(maybe_x.value()));
  rect_params.Set("y", static_cast<int>(maybe_y.value()));
  return session->chrome->SetWindowRect(session->window,
                                        std::move(rect_params));
}

Status ExecuteGetWindowSize(Session* session,
                            const base::DictValue& params,
                            std::unique_ptr<base::Value>* value) {
  Chrome::WindowRect window_rect;
  Status status = session->chrome->GetWindowRect(session->window, &window_rect);

  if (status.IsError())
    return status;

  base::DictValue size = base::DictValue()
                             .Set("width", window_rect.width)
                             .Set("height", window_rect.height);
  *value = base::Value::ToUniquePtrValue(base::Value(std::move(size)));
  return Status(kOk);
}

Status ExecuteSetWindowSize(Session* session,
                            const base::DictValue& params,
                            std::unique_ptr<base::Value>* value) {
  std::optional<double> maybe_width = params.FindDouble("width");
  std::optional<double> maybe_height = params.FindDouble("height");

  if (!maybe_width.has_value() || !maybe_height.has_value())
    return Status(kInvalidArgument, "missing or invalid 'width' or 'height'");

  base::DictValue rect_params;
  rect_params.Set("width", static_cast<int>(maybe_width.value()));
  rect_params.Set("height", static_cast<int>(maybe_height.value()));
  return session->chrome->SetWindowRect(session->window,
                                        std::move(rect_params));
}

Status ExecuteGetAvailableLogTypes(Session* session,
                                   const base::DictValue& params,
                                   std::unique_ptr<base::Value>* value) {
  std::unique_ptr<base::ListValue> types(new base::ListValue());
  std::vector<WebDriverLog*> logs = session->GetAllLogs();
  for (std::vector<WebDriverLog*>::const_iterator log = logs.begin();
       log != logs.end();
       ++log) {
    types->Append((*log)->type());
  }
  *value = std::make_unique<base::Value>(base::Value(std::move(*types)));
  return Status(kOk);
}

Status ExecuteGetLog(Session* session,
                     const base::DictValue& params,
                     std::unique_ptr<base::Value>* value) {
  const std::string* log_type = params.FindString("type");
  if (!log_type) {
    return Status(kInvalidArgument, "missing or invalid 'type'");
  }

  // Evaluate a JavaScript in the renderer process for the current tab, to flush
  // out any pending logging-related events.
  Status status = EvaluateScriptAndIgnoreResult(session, "1");
  if (status.IsError()) {
    // Sometimes a WebDriver client fetches logs to diagnose an error that has
    // occurred. It's possible that in the case of an error, the renderer is no
    // be longer available, but we should return the logs anyway. So log (but
    // don't fail on) any error that we get while evaluating the script.
    LOG(WARNING) << "Unable to evaluate script: " << status.message();
  }

  std::vector<WebDriverLog*> logs = session->GetAllLogs();
  for (std::vector<WebDriverLog*>::const_iterator log = logs.begin();
       log != logs.end();
       ++log) {
    if (*log_type == (*log)->type()) {
      *value = base::Value::ToUniquePtrValue(
          base::Value((*log)->GetAndClearEntries()));
      return Status(kOk);
    }
  }
  return Status(kInvalidArgument, "log type '" + *log_type + "' not found");
}

Status ExecuteUploadFile(Session* session,
                         const base::DictValue& params,
                         std::unique_ptr<base::Value>* value) {
  const std::string* base64_zip_data = params.FindString("file");
  if (!base64_zip_data)
    return Status(kInvalidArgument, "missing or invalid 'file'");
  std::string zip_data;
  if (!Base64Decode(*base64_zip_data, &zip_data))
    return Status(kUnknownError, "unable to decode 'file'");

  if (!session->temp_dir.IsValid()) {
    if (!session->temp_dir.CreateUniqueTempDir())
      return Status(kUnknownError, "unable to create temp dir");
  }
  base::FilePath upload_dir;
  if (!base::CreateTemporaryDirInDir(session->temp_dir.GetPath(),
                                     FILE_PATH_LITERAL("upload"),
                                     &upload_dir)) {
    return Status(kUnknownError, "unable to create temp dir");
  }
  std::string error_msg;
  base::FilePath upload;
  Status status = UnzipSoleFile(upload_dir, zip_data, &upload);
  if (status.IsError())
    return Status(kUnknownError, "unable to unzip 'file'", status);

  *value = std::make_unique<base::Value>(upload.AsUTF8Unsafe());
  return Status(kOk);
}

Status ExecuteSetSPCTransactionMode(Session* session,
                                    const base::DictValue& params,
                                    std::unique_ptr<base::Value>* value) {
  WebView* web_view = nullptr;
  Status status = session->GetTargetWindow(&web_view);
  if (status.IsError())
    return status;

  const std::string* mode = params.FindString("mode");
  if (!mode)
    return Status(kInvalidArgument, "missing parameter 'mode'");

  base::DictValue body;
  body.Set("mode", *mode);

  return web_view->SendCommandAndGetResult("Page.setSPCTransactionMode", body,
                                           value);
}

Status ExecuteGenerateTestReport(Session* session,
                                 const base::DictValue& params,
                                 std::unique_ptr<base::Value>* value) {
  WebView* web_view = nullptr;
  Status status = session->GetTargetWindow(&web_view);
  if (status.IsError())
    return status;

  const std::string* message = params.FindString("message");
  if (!message)
    return Status(kInvalidArgument, "missing parameter 'message'");
  const std::string* group = params.FindString("group");

  base::DictValue body;
  body.Set("message", *message);
  body.Set("group", group ? *group : "default");

  web_view->SendCommandAndGetResult("Page.generateTestReport", body, value);
  return Status(kOk);
}

Status ExecuteSetTimeZone(Session* session,
                          const base::DictValue& params,
                          std::unique_ptr<base::Value>* value) {
  WebView* web_view = nullptr;
  Status status = session->GetTargetWindow(&web_view);
  if (status.IsError())
    return status;

  const std::string* time_zone = params.FindString("time_zone");
  if (!time_zone)
    return Status(kInvalidArgument, "missing parameter 'time_zone'");

  base::DictValue body;
  body.Set("timezoneId", *time_zone);

  web_view->SendCommandAndGetResult("Emulation.setTimezoneOverride", body,
                                    value);
  return Status(kOk);
}

Status ExecuteCreateVirtualPressureSource(Session* session,
                                          const base::DictValue& params,
                                          std::unique_ptr<base::Value>* value) {
  WebView* web_view = nullptr;
  Status status = session->GetTargetWindow(&web_view);
  if (status.IsError()) {
    return status;
  }

  const std::string* type = params.FindString("type");
  if (!type) {
    return Status(kInvalidArgument, "'type' must be a string");
  }

  base::DictValue body;
  body.Set("enabled", true);
  body.Set("source", *type);

  base::DictValue metadata;
  metadata.Set("available", true);
  if (params.contains("supported")) {
    auto supported = params.FindBool("supported");
    if (!supported.has_value()) {
      return Status(kInvalidArgument, "'supported' must be a boolean");
    }
    metadata.Set("available", *supported);
  }
  body.Set("metadata", std::move(metadata));

  return web_view->SendCommand("Emulation.setPressureSourceOverrideEnabled",
                               body);
}

Status ExecuteUpdateVirtualPressureSource(Session* session,
                                          const base::DictValue& params,
                                          std::unique_ptr<base::Value>* value) {
  WebView* web_view = nullptr;
  Status status = session->GetTargetWindow(&web_view);
  if (status.IsError()) {
    return status;
  }

  const std::string* type = params.FindString("type");
  if (!type) {
    return Status(kInvalidArgument, "'type' must be a string");
  }

  const std::string* sample = params.FindString("sample");
  if (!sample) {
    return Status(kInvalidArgument, "'sample' must be a string");
  }

  base::DictValue body;
  body.Set("source", *type);
  body.Set("state", *sample);
  return web_view->SendCommand("Emulation.setPressureStateOverride", body);
}

Status ExecuteRemoveVirtualPressureSource(Session* session,
                                          const base::DictValue& params,
                                          std::unique_ptr<base::Value>* value) {
  WebView* web_view = nullptr;
  Status status = session->GetTargetWindow(&web_view);
  if (status.IsError()) {
    return status;
  }

  const std::string* type = params.FindString("type");
  if (!type) {
    return Status(kInvalidArgument, "'type' must be a string");
  }

  base::DictValue body;
  body.Set("enabled", false);
  body.Set("source", *type);
  return web_view->SendCommand("Emulation.setPressureSourceOverrideEnabled",
                               body);
}

Status ExecuteSetProtectedAudienceKAnonymity(
    Session* session,
    const base::DictValue& params,
    std::unique_ptr<base::Value>* value) {
  WebView* web_view = nullptr;
  Status status = session->GetTargetWindow(&web_view);
  if (status.IsError()) {
    return status;
  }

  const std::string* owner = params.FindString("owner");
  if (!owner) {
    return Status(kInvalidArgument, "missing parameter 'owner'");
  }
  const std::string* name = params.FindString("name");
  if (!name) {
    return Status(kInvalidArgument, "missing parameter 'name'");
  }
  const base::ListValue* hashes = params.FindList("hashes");
  if (!hashes) {
    return Status(kInvalidArgument, "missing parameter 'hashes'");
  }
  for (const auto& hash : *hashes) {
    if (!hash.is_string()) {
      return Status(kInvalidArgument, "hashes should be base64 strings");
    }
  }

  base::DictValue body;
  body.Set("owner", *owner);
  body.Set("name", *name);
  body.Set("hashes", base::Value(hashes->Clone()));

  return web_view->SendCommand("Storage.setProtectedAudienceKAnonymity", body);
}

// Run a BiDi command
Status ForwardBidiCommand(Session* session,
                          const base::DictValue& params,
                          std::unique_ptr<base::Value>* value) {
  // session == nullptr is a valid case: ExecuteQuit has already been handled
  // in the session thread but the following
  // TerminateSessionThreadOnCommandThread has not yet been executed (the later
  // destroys the session thread) The connection has already been accepted by
  // the CMD thread but soon it will be closed. We don't need to do anything.
  if (session == nullptr) {
    return Status{kInvalidArgument, "session not found"};
  }
  const base::DictValue* data = params.FindDict("bidiCommand");
  if (!data) {
    return Status{kUnknownError, "bidiCommand is missing in params"};
  }

  std::optional<int> connection_id = params.FindInt("connectionId");
  if (!connection_id) {
    return Status{kUnknownCommand, "connectionId is missing in params"};
  }

  WebView* web_view = nullptr;
  Status status = session->chrome->GetActivePageByWebViewId(
      session->bidi_mapper_web_view_id, &web_view, /*wait_for_page=*/false);
  if (status.IsError()) {
    return status;
  }

  base::DictValue bidi_cmd = data->Clone();

  std::string* user_channel = bidi_cmd.FindString("goog:channel");
  std::string channel = (user_channel ? *user_channel : "") + "/" +
                        base::NumberToString(*connection_id) +
                        Session::kChannelSuffix;

  bidi_cmd.Set("goog:channel", std::move(channel));
  status = web_view->PostBidiCommand(std::move(bidi_cmd));

  return status;
}
