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

#include <string>
#include <string_view>

#include "base/base64.h"
#include "base/check_deref.h"
#include "base/functional/bind.h"
#include "base/memory/memory_pressure_listener_registry.h"
#include "base/memory/raw_ptr.h"
#include "base/path_service.h"
#include "base/strings/string_split.h"
#include "base/strings/string_util.h"
#include "base/strings/stringprintf.h"
#include "base/strings/to_string.h"
#include "base/strings/utf_string_conversions.h"
#include "base/test/bind.h"
#include "base/test/scoped_feature_list.h"
#include "base/test/test_switches.h"
#include "base/test/values_test_util.h"
#include "base/threading/thread_restrictions.h"
#include "base/values.h"
#include "build/build_config.h"
#include "chrome/browser/apps/app_service/app_service_proxy.h"
#include "chrome/browser/apps/app_service/app_service_proxy_factory.h"
#include "chrome/browser/apps/app_service/browser_app_launcher.h"
#include "chrome/browser/custom_handlers/protocol_handler_registry_factory.h"
#include "chrome/browser/data_saver/data_saver.h"
#include "chrome/browser/devtools/devtools_window.h"
#include "chrome/browser/devtools/protocol/devtools_protocol_test_support.h"
#include "chrome/browser/preloading/preloading_prefs.h"
#include "chrome/browser/privacy_sandbox/privacy_sandbox_attestations/privacy_sandbox_attestations_mixin.h"
#include "chrome/browser/profiles/profile.h"
#include "chrome/browser/ssl/https_upgrades_util.h"
#include "chrome/browser/ui/browser_window/public/browser_window_interface.h"
#include "chrome/common/chrome_paths.h"
#include "chrome/common/pref_names.h"
#include "chrome/common/webui_url_constants.h"
#include "chrome/test/base/chrome_test_utils.h"
#include "components/content_settings/core/browser/cookie_settings.h"
#include "components/content_settings/core/common/pref_names.h"
#include "components/custom_handlers/protocol_handler_registry.h"
#include "components/infobars/content/content_infobar_manager.h"
#include "components/infobars/core/infobar.h"
#include "components/infobars/core/infobar_delegate.h"
#include "components/privacy_sandbox/privacy_sandbox_attestations/privacy_sandbox_attestations.h"
#include "components/services/app_service/public/cpp/app_launch_params.h"
#include "content/public/browser/btm_redirect.h"
#include "content/public/browser/btm_service.h"
#include "content/public/browser/cookie_access_details.h"
#include "content/public/browser/devtools_agent_host.h"
#include "content/public/browser/navigation_entry.h"
#include "content/public/browser/page_navigator.h"
#include "content/public/browser/ssl_status.h"
#include "content/public/browser/web_contents.h"
#include "content/public/common/content_features.h"
#include "content/public/common/referrer.h"
#include "content/public/test/browser_test.h"
#include "content/public/test/browser_test_utils.h"
#include "content/public/test/btm_service_test_utils.h"
#include "content/public/test/download_test_observer.h"
#include "content/public/test/preloading_test_util.h"
#include "content/public/test/prerender_test_util.h"
#include "net/base/ip_address.h"
#include "net/dns/mock_host_resolver.h"
#include "net/ssl/ssl_cipher_suite_names.h"
#include "net/ssl/ssl_config.h"
#include "net/ssl/ssl_connection_status_flags.h"
#include "net/ssl/ssl_server_config.h"
#include "printing/buildflags/buildflags.h"
#include "services/network/public/cpp/network_switches.h"
#include "testing/gmock/include/gmock/gmock.h"
#include "third_party/blink/public/common/features.h"
#include "third_party/boringssl/src/include/openssl/ssl.h"
#include "ui/base/page_transition_types.h"
#include "ui/base/window_open_disposition.h"
#include "ui/gfx/codec/png_codec.h"
#include "url/origin.h"

#if BUILDFLAG(IS_ANDROID)
#include "base/android/android_info.h"
#include "base/test/run_until.h"
#include "chrome/browser/android/tab_android.h"
#include "chrome/browser/devtools/protocol/browser_handler_android.h"
#include "chrome/browser/ui/android/tab_model/tab_model.h"
#include "chrome/browser/ui/android/tab_model/tab_model_list.h"
#include "chrome/browser/ui/android/tab_model/tab_model_test_helper.h"
#include "chrome/browser/ui/browser_window/public/browser_window_interface_iterator.h"
#endif

#if !BUILDFLAG(IS_ANDROID)
#include "base/task/sequenced_task_runner.h"
#include "chrome/browser/prefs/session_startup_pref.h"
#include "chrome/browser/profiles/keep_alive/profile_keep_alive_types.h"
#include "chrome/browser/profiles/keep_alive/scoped_profile_keep_alive.h"
#include "chrome/browser/profiles/profile_window.h"
#include "chrome/browser/resource_coordinator/tab_manager.h"
#include "chrome/browser/sessions/session_restore_test_helper.h"
#include "chrome/browser/sessions/session_service_test_helper.h"
#include "chrome/browser/ui/browser_commands.h"
#include "chrome/browser/ui/browser_window/public/global_browser_collection.h"
#include "chrome/browser/ui/web_applications/test/isolated_web_app_test_utils.h"
#include "chrome/browser/web_applications/isolated_web_apps/isolated_web_app_url_info.h"
#include "chrome/browser/web_applications/isolated_web_apps/test/isolated_web_app_builder.h"
#include "chrome/test/base/mixin_based_in_process_browser_test.h"
#include "chrome/test/base/ui_test_utils.h"
#include "components/guest_view/browser/guest_view_base.h"
#include "components/guest_view/browser/guest_view_manager_delegate.h"
#include "components/guest_view/browser/test_guest_view_manager.h"
#include "components/keep_alive_registry/keep_alive_types.h"
#include "components/keep_alive_registry/scoped_keep_alive.h"
#include "components/permissions/permission_request_manager.h"
#include "components/tab_groups/tab_group_id.h"
#include "content/public/browser/devtools_agent_host_client.h"
#include "content/public/test/browser_test_utils.h"
#endif  // !BUILDFLAG(IS_ANDROID)

#if BUILDFLAG(ENABLE_EXTENSIONS_CORE)
#include "chrome/browser/extensions/extension_util.h"
#include "chrome/browser/extensions/scoped_test_mv2_enabler.h"
#include "extensions/browser/api/extensions_api_client.h"
#include "extensions/browser/app_window/app_window.h"
#include "extensions/browser/app_window/app_window_registry.h"
#include "extensions/browser/extension_host.h"
#include "extensions/browser/extension_registrar.h"
#include "extensions/browser/extension_system.h"
#include "extensions/browser/process_manager.h"
#include "extensions/browser/test_extension_registry_observer.h"
#include "extensions/browser/unpacked_installer.h"
#include "extensions/common/manifest_handlers/background_info.h"
#include "extensions/test/extension_test_message_listener.h"
#endif  // BUILDFLAG(ENABLE_EXTENSIONS_CORE)

#if BUILDFLAG(IS_WIN)
#include "base/base_paths_win.h"
#include "base/test/scoped_path_override.h"
#endif

using DevToolsProtocolTest = DevToolsProtocolTestBase;
using testing::AllOf;
using testing::Contains;
using testing::Eq;
using testing::Not;

namespace {

#if !BUILDFLAG(IS_ANDROID)
IN_PROC_BROWSER_TEST_F(DevToolsProtocolTest,
                       VisibleSecurityStateChangedNeutralState) {
  ASSERT_TRUE(content::NavigateToURL(
      chrome_test_utils::GetActiveWebContents(this), GURL("about:blank")));
  EXPECT_TRUE(content::WaitForLoadStop(web_contents()));

  Attach();
  SendCommandAsync("Security.enable");
  base::DictValue params =
      WaitForNotification("Security.visibleSecurityStateChanged", true);

  std::string* security_state =
      params.FindStringByDottedPath("visibleSecurityState.securityState");
  ASSERT_TRUE(security_state);
  EXPECT_EQ(std::string("neutral"), *security_state);
  EXPECT_FALSE(params.FindStringByDottedPath(
      "visibleSecurityState.certificateSecurityState"));
  EXPECT_FALSE(
      params.FindStringByDottedPath("visibleSecurityState.safetyTipInfo"));
  const base::Value* security_state_issue_ids =
      params.FindByDottedPath("visibleSecurityState.securityStateIssueIds");
  EXPECT_TRUE(security_state_issue_ids->GetList().contains(
      "scheme-is-not-cryptographic"));
}

IN_PROC_BROWSER_TEST_F(DevToolsProtocolTest, CreateDeleteContext) {
  AttachToBrowserTarget();
  for (int i = 0; i < 2; i++) {
    const base::DictValue* result =
        SendCommandSync("Target.createBrowserContext");
    std::string context_id = *result->FindString("browserContextId");

    base::DictValue params;
    params.Set("url", "about:blank");
    params.Set("browserContextId", context_id);
    SendCommandSync("Target.createTarget", std::move(params));

    params = base::DictValue();
    params.Set("browserContextId", context_id);
    SendCommandSync("Target.disposeBrowserContext", std::move(params));
  }
}

IN_PROC_BROWSER_TEST_F(
    DevToolsProtocolTest,
    DownloadBehaviorOverridesRemainIndependentAcrossBrowserContexts) {
  ASSERT_TRUE(embedded_test_server()->Start());
  const GURL download_url =
      embedded_test_server()->GetURL("/download-test1.lib");

  AttachToBrowserTarget();
  const base::DictValue* result =
      SendCommandSync("Target.createBrowserContext");
  ASSERT_TRUE(result);
  const std::string* browser_context_id =
      result->FindString("browserContextId");
  ASSERT_TRUE(browser_context_id);
  const std::string context_id = *browser_context_id;

  content::TestDevToolsProtocolClient older_client;
  older_client.AttachToBrowserTarget();
  base::DictValue params;
  params.Set("behavior", "deny");
  ASSERT_TRUE(older_client.SendCommandSync("Browser.setDownloadBehavior",
                                           params.Clone()));
  // Replacing an override for the same context must not let the old handle
  // reset the replacement.
  ASSERT_TRUE(older_client.SendCommandSync("Browser.setDownloadBehavior",
                                           params.Clone()));
  params.Set("browserContextId", context_id);
  ASSERT_TRUE(older_client.SendCommandSync("Browser.setDownloadBehavior",
                                           std::move(params)));

  {
    content::DownloadTestObserverTerminal observer(
        browser()->GetProfile()->GetDownloadManager(), 1,
        content::DownloadTestObserver::ON_DANGEROUS_DOWNLOAD_FAIL);
    ASSERT_TRUE(ui_test_utils::NavigateToURL(browser(), download_url));
    observer.WaitForFinished();
    EXPECT_EQ(1u, observer.NumDownloadsSeenInState(
                      download::DownloadItem::CANCELLED));
  }

  content::TestDevToolsProtocolClient newer_client;
  newer_client.AttachToBrowserTarget();
  params = base::DictValue();
  params.Set("behavior", "deny");
  ASSERT_TRUE(newer_client.SendCommandSync("Browser.setDownloadBehavior",
                                           std::move(params)));

  params = base::DictValue();
  params.Set("browserContextId", context_id);
  ASSERT_TRUE(
      SendCommandSync("Target.disposeBrowserContext", std::move(params)));
  older_client.DetachProtocolClient();

  {
    content::DownloadTestObserverTerminal observer(
        browser()->GetProfile()->GetDownloadManager(), 1,
        content::DownloadTestObserver::ON_DANGEROUS_DOWNLOAD_FAIL);
    ASSERT_TRUE(ui_test_utils::NavigateToURL(browser(), download_url));
    observer.WaitForFinished();
    EXPECT_EQ(1u, observer.NumDownloadsSeenInState(
                      download::DownloadItem::CANCELLED));
  }

  newer_client.DetachProtocolClient();
}

IN_PROC_BROWSER_TEST_F(DevToolsProtocolTest,
                       NewTabPageInCreatedContextDoesNotCrash) {
  AttachToBrowserTarget();
  const base::DictValue* result =
      SendCommandSync("Target.createBrowserContext");
  std::string context_id = *result->FindString("browserContextId");

  base::DictValue params;
  params.Set("url", chrome::kChromeUINewTabURL);
  params.Set("browserContextId", context_id);
  const base::DictValue* create_target_result =
      SendCommandSync("Target.createTarget", std::move(params));
  std::string target_id = *create_target_result->FindString("targetId");
  scoped_refptr<content::DevToolsAgentHost> agent_host =
      content::DevToolsAgentHost::GetForId(target_id);
  ASSERT_TRUE(agent_host);
  content::WebContents* wc = agent_host->GetWebContents();
  ASSERT_TRUE(wc);
  ASSERT_TRUE(content::WaitForLoadStop(wc));
  EXPECT_EQ(chrome::ChromeUINewTabURLAsGURL(), wc->GetLastCommittedURL());

  // Should not crash by this point.
}

IN_PROC_BROWSER_TEST_F(DevToolsProtocolTest,
                       CreateBrowserContextAcceptsProxyServer) {
  ScopedAllowHttpForHostnamesForTesting allow_http(
      {"this-page-does-not-exist.com"},
      chrome_test_utils::GetProfile(this)->GetPrefs());

  AttachToBrowserTarget();
  embedded_test_server()->RegisterRequestHandler(base::BindLambdaForTesting(
      [&](const net::test_server::HttpRequest& request)
          -> std::unique_ptr<net::test_server::HttpResponse> {
        std::unique_ptr<net::test_server::BasicHttpResponse> http_response(
            std::make_unique<net::test_server::BasicHttpResponse>());
        http_response->set_code(net::HTTP_OK);
        http_response->set_content_type("text/html");
        http_response->set_content("<title>Hello from proxy server!</title>");
        return std::move(http_response);
      }));
  ASSERT_TRUE(embedded_test_server()->Start());

  base::DictValue params;
  params.Set("proxyServer",
             embedded_test_server()->host_port_pair().ToString());
  const base::DictValue* result =
      SendCommandSync("Target.createBrowserContext", std::move(params));
  std::string context_id = *result->FindString("browserContextId");

  params = base::DictValue();
  params.Set("url", "http://this-page-does-not-exist.com/site.html");
  params.Set("browserContextId", context_id);
  result = SendCommandSync("Target.createTarget", std::move(params));
  std::string target_id = *result->FindString("targetId");

  scoped_refptr<content::DevToolsAgentHost> agent_host =
      content::DevToolsAgentHost::GetForId(target_id);
  ASSERT_TRUE(agent_host);
  content::WebContents* wc = agent_host->GetWebContents();
  ASSERT_TRUE(wc);
  ASSERT_TRUE(content::WaitForLoadStop(wc));

  EXPECT_EQ(GURL("http://this-page-does-not-exist.com/site.html"),
            wc->GetURL());

  EXPECT_EQ("Hello from proxy server!", base::UTF16ToUTF8(wc->GetTitle()));
}

IN_PROC_BROWSER_TEST_F(DevToolsProtocolTest, CreateInDefaultContextById) {
  AttachToBrowserTarget();
  base::DictValue params;
  params.Set("filter",
             base::ListValue().Append(
                 base::DictValue().Set("type", "page").Set("exclude", false)));
  const base::DictValue* result =
      SendCommandSync("Target.getTargets", std::move(params));
  const base::ListValue* list = result->FindList("targetInfos");
  ASSERT_TRUE(list->size() == 1);
  const std::string context_id =
      *list->front().GetDict().FindString("browserContextId");

  params = base::DictValue();
  params.Set("url", "about:blank");
  params.Set("browserContextId", context_id);
  result = SendCommandSync("Target.createTarget", std::move(params));
  ASSERT_TRUE(result);
}

IN_PROC_BROWSER_TEST_F(DevToolsProtocolTest,
                       InputDispatchEventsToCorrectTarget) {
  Attach();

  std::string setup_logging = R"(
      window.logs = [];
      ['dragenter', 'keydown', 'mousedown', 'mouseenter', 'mouseleave',
       'mousemove', 'mouseout', 'mouseover', 'mouseup', 'click', 'touchcancel',
       'touchend', 'touchmove', 'touchstart',
      ].forEach((event) =>
        window.addEventListener(event, (e) => logs.push(e.type)));)";
  content::WebContents* target_web_contents =
      chrome_test_utils::GetActiveWebContents(this);

  ui_test_utils::NavigateToURLWithDisposition(
      browser(), GURL("about:blank"), WindowOpenDisposition::NEW_FOREGROUND_TAB,
      ui_test_utils::BROWSER_TEST_WAIT_FOR_LOAD_STOP);
  content::WebContents* other_web_contents =
      chrome_test_utils::GetActiveWebContents(this);
  EXPECT_TRUE(content::EvalJs(target_web_contents, setup_logging).is_ok());
  EXPECT_TRUE(content::EvalJs(other_web_contents, setup_logging).is_ok());

  base::DictValue params;
  params.Set("button", "left");
  params.Set("clickCount", 1);
  params.Set("x", 100);
  params.Set("y", 250);
  params.Set("clickCount", 1);

  params.Set("type", "mousePressed");
  SendCommandSync("Input.dispatchMouseEvent", params.Clone());

  params.Set("type", "mouseMoved");
  params.Set("y", 270);
  SendCommandSync("Input.dispatchMouseEvent", params.Clone());

  params.Set("type", "mouseReleased");
  SendCommandSync("Input.dispatchMouseEvent", std::move(params));

  params = base::DictValue();
  params.Set("x", 100);
  params.Set("y", 250);
  params.Set("type", "dragEnter");
  params.SetByDottedPath("data.dragOperationsMask", 1);
  params.SetByDottedPath("data.items", base::ListValue());
  SendCommandSync("Input.dispatchDragEvent", std::move(params));

  params = base::DictValue();
  params.Set("x", 100);
  params.Set("y", 250);
  SendCommandSync("Input.synthesizeTapGesture", std::move(params));

  params = base::DictValue();
  params.Set("type", "keyDown");
  params.Set("key", "a");
  SendCommandSync("Input.dispatchKeyEvent", std::move(params));

  content::EvalJsResult main_target_events =
      content::EvalJs(target_web_contents, "logs.join(' ')");
  content::EvalJsResult other_target_events =
      content::EvalJs(other_web_contents, "logs.join(' ')");
  // mouse events might happen in the other_target if the real mouse pointer
  // happens to be over the browser window
  EXPECT_THAT(
      base::SplitString(main_target_events.ExtractString(), " ",
                        base::KEEP_WHITESPACE, base::SPLIT_WANT_ALL),
      AllOf(Contains("mouseover"), Contains("mousedown"), Contains("mousemove"),
            Contains("mouseup"), Contains("click"), Contains("dragenter"),
            Contains("keydown")));
  EXPECT_THAT(base::SplitString(other_target_events.ExtractString(), " ",
                                base::KEEP_WHITESPACE, base::SPLIT_WANT_ALL),
              AllOf(Not(Contains("click")), Not(Contains("dragenter")),
                    Not(Contains("keydown"))));
}

IN_PROC_BROWSER_TEST_F(DevToolsProtocolTest, CreateTargetWithFocus) {
  AttachToBrowserTarget();
  content::WebContents* const initial_tab =
      chrome_test_utils::GetActiveWebContents(this);
  ASSERT_TRUE(initial_tab);
  EXPECT_EQ(1, browser()->tab_strip_model()->count());

  // By default, creating a new target also focuses it.
  SendCommandSync("Target.createTarget",
                  base::DictValue().Set("url", "about:blank#1"));
  ASSERT_FALSE(error());
  content::WebContents* const focused_tab =
      chrome_test_utils::GetActiveWebContents(this);
  EXPECT_NE(initial_tab, focused_tab);
  EXPECT_EQ(2, browser()->tab_strip_model()->count());
  EXPECT_EQ(GURL("about:blank#1"), focused_tab->GetVisibleURL());

  // When focus=false, the new target is not focused.
  SendCommandSync(
      "Target.createTarget",
      base::DictValue().Set("url", "about:blank#2").Set("focus", false));
  ASSERT_FALSE(error());
  EXPECT_EQ(focused_tab, chrome_test_utils::GetActiveWebContents(this));
  EXPECT_EQ(3, browser()->tab_strip_model()->count());
  EXPECT_EQ(GURL("about:blank#2"),
            browser()->tab_strip_model()->GetWebContentsAt(2)->GetVisibleURL());

  // When background=true, the new target is not focused by default.
  SendCommandSync(
      "Target.createTarget",
      base::DictValue().Set("url", "about:blank#3").Set("background", true));
  ASSERT_FALSE(error());
  EXPECT_EQ(focused_tab, chrome_test_utils::GetActiveWebContents(this));
  EXPECT_EQ(4, browser()->tab_strip_model()->count());
  EXPECT_EQ(GURL("about:blank#3"),
            browser()->tab_strip_model()->GetWebContentsAt(3)->GetVisibleURL());

  // It's an error to use focus=true and background=true.
  SendCommandSync("Target.createTarget", base::DictValue()
                                             .Set("url", "about:blank#4")
                                             .Set("focus", true)
                                             .Set("background", true));
  ASSERT_TRUE(error());
  EXPECT_EQ(
      "Can't focus a target in the background. Use background=false instead.",
      *error()->FindString("message"));
  EXPECT_EQ(4, browser()->tab_strip_model()->count());
}

#if BUILDFLAG(IS_CHROMEOS) || BUILDFLAG(IS_MAC)
// On ChromeOS and MacOS, the tabs are not backgrounded and unloaded in the same way as
// on other platforms.
#define MAYBE_AutoAttachToUnloadedTab DISABLED_AutoAttachToUnloadedTab
#else
#define MAYBE_AutoAttachToUnloadedTab AutoAttachToUnloadedTab
#endif

IN_PROC_BROWSER_TEST_F(DevToolsProtocolTest, MAYBE_AutoAttachToUnloadedTab) {
  ASSERT_TRUE(embedded_test_server()->Start());
  GURL url1 = embedded_test_server()->GetURL("/title1.html");
  GURL url2 = embedded_test_server()->GetURL("/title2.html");

  // 1. Create two tabs.
  ASSERT_TRUE(ui_test_utils::NavigateToURL(browser(), url1));
  ui_test_utils::NavigateToURLWithDisposition(
      browser(), url2, WindowOpenDisposition::NEW_FOREGROUND_TAB,
      ui_test_utils::BROWSER_TEST_WAIT_FOR_LOAD_STOP);
  ASSERT_EQ(2, browser()->tab_strip_model()->count());
  ASSERT_EQ(1, browser()->tab_strip_model()->active_index());

  content::WebContents* background_tab =
      browser()->tab_strip_model()->GetWebContentsAt(0);
  content::WebContents* active_tab =
      browser()->tab_strip_model()->GetWebContentsAt(1);

  // 2. Restore session without loading background tabs.
  Profile* profile = browser()->GetProfile();
  SessionStartupPref::SetStartupPref(
      profile, SessionStartupPref(SessionStartupPref::LAST));
  SessionServiceTestHelper(profile).SetForceBrowserNotAliveWithNoWindows(true);

  auto keep_alive = std::make_unique<ScopedKeepAlive>(
      KeepAliveOrigin::SESSION_RESTORE, KeepAliveRestartOption::DISABLED);
  auto profile_keep_alive = std::make_unique<ScopedProfileKeepAlive>(
      profile, ProfileKeepAliveOrigin::kBrowserWindow);
  CloseBrowserSynchronously(browser());

  ui_test_utils::AllBrowserTabAddedWaiter tab_waiter;
  SessionRestoreTestHelper restore_observer;

  chrome::NewEmptyWindow(profile);

  BrowserWindowInterface* new_browser =
      GlobalBrowserCollection::GetInstance()->FindBrowserWithTab(
          tab_waiter.Wait());

  base::MemoryPressureListenerRegistry::SimulatePressureNotification(
      base::MEMORY_PRESSURE_LEVEL_CRITICAL);

  keep_alive.reset();
  profile_keep_alive.reset();

  // The new browser should have 2 tabs.
  ASSERT_EQ(2, new_browser->GetTabStripModel()->count());
  ASSERT_EQ(1, new_browser->GetTabStripModel()->active_index());

  background_tab = new_browser->GetTabStripModel()->GetWebContentsAt(0);
  active_tab = new_browser->GetTabStripModel()->GetWebContentsAt(1);

  // Background tab should not have a renderer.
  EXPECT_FALSE(background_tab->GetPrimaryMainFrame()->IsRenderFrameLive());
  ASSERT_TRUE(content::WaitForLoadStop(active_tab));
  EXPECT_TRUE(active_tab->GetPrimaryMainFrame()->IsRenderFrameLive());

  // Attach to the browser target.
  AttachToBrowserTarget();
  // Send Target.setAutoAttach with autoAttach=true, flatten=true.
  base::DictValue set_auto_attach_params;
  set_auto_attach_params.Set("autoAttach", true);
  set_auto_attach_params.Set("waitForDebuggerOnStart", false);
  set_auto_attach_params.Set("flatten", true);
  base::ListValue filter;
  // Include tab target.
  base::DictValue page_filter;
  page_filter.Set("type", "page");
  page_filter.Set("exclude", true);
  filter.Append(std::move(page_filter));
  filter.Append(base::DictValue());
  set_auto_attach_params.Set("filter", std::move(filter));
  SendCommandSync("Target.setAutoAttach", std::move(set_auto_attach_params));

  auto is_tab_target = [](const base::DictValue& params) {
    const std::string* type = params.FindStringByDottedPath("targetInfo.type");
    return type && *type == "tab";
  };

  // Verify that two attachedToTarget tab target events are received.
  base::DictValue attached_tab1_notif = WaitForMatchingNotification(
      "Target.attachedToTarget", base::BindRepeating(is_tab_target));
  const std::string& session_id1 =
      *attached_tab1_notif.FindStringByDottedPath("sessionId");
  ASSERT_EQ("tab",
            *attached_tab1_notif.FindStringByDottedPath("targetInfo.type"));

  base::DictValue set_auto_attach_params_for_tab;
  set_auto_attach_params_for_tab.Set("autoAttach", true);
  set_auto_attach_params_for_tab.Set("waitForDebuggerOnStart", false);
  set_auto_attach_params_for_tab.Set("flatten", true);
  SendSessionCommand("Target.setAutoAttach",
                     set_auto_attach_params_for_tab.Clone(), session_id1,
                     false);

  base::DictValue attached_tab2_notif = WaitForMatchingNotification(
      "Target.attachedToTarget", base::BindRepeating(is_tab_target));
  const std::string& session_id2 =
      *attached_tab2_notif.FindStringByDottedPath("sessionId");
  ASSERT_EQ("tab",
            *attached_tab2_notif.FindStringByDottedPath("targetInfo.type"));

  SendSessionCommand("Target.setAutoAttach",
                     std::move(set_auto_attach_params_for_tab), session_id2,
                     false);

  auto is_page_target = [](const base::DictValue& params) {
    const std::string* type = params.FindStringByDottedPath("targetInfo.type");
    return type && *type == "page";
  };

  // Verify two attachedToTarget events for page targets are received.
  base::DictValue attached_page1_notif = WaitForMatchingNotification(
      "Target.attachedToTarget", base::BindRepeating(is_page_target));
  base::DictValue attached_page2_notif = WaitForMatchingNotification(
      "Target.attachedToTarget", base::BindRepeating(is_page_target));

  // The notifications can arrive in any order. The active tab is eagerly
  // loaded, so it should have a URL. The background tab may not.
  if (const std::string* url =
          attached_page1_notif.FindStringByDottedPath("targetInfo.url");
      url && *url == url2.spec()) {
    std::swap(attached_page1_notif, attached_page2_notif);
  }
  // The background tab's URL may not be available yet.
  ASSERT_THAT(attached_page2_notif.FindStringByDottedPath("targetInfo.url"),
              testing::Pointee(url2.spec()));

  const std::string& page_session_id1 =
      *attached_page1_notif.FindStringByDottedPath("sessionId");
  ASSERT_EQ("page",
            *attached_page1_notif.FindStringByDottedPath("targetInfo.type"));

  const std::string& page_session_id2 =
      *attached_page2_notif.FindStringByDottedPath("sessionId");
  ASSERT_EQ("page",
            *attached_page2_notif.FindStringByDottedPath("targetInfo.type"));

  // Send Runtime.evaluate to both page targets and verify script result is
  // received for both.
  EXPECT_TRUE(background_tab->GetPrimaryMainFrame()->IsRenderFrameLive());
  EXPECT_TRUE(active_tab->GetPrimaryMainFrame()->IsRenderFrameLive());

  base::DictValue evaluate_params;
  evaluate_params.Set("expression", "1 + 2");
  const base::DictValue* evaluate_result1 = SendSessionCommand(
      "Runtime.evaluate", evaluate_params.Clone(), page_session_id1, true);
  ASSERT_EQ(3, evaluate_result1->FindIntByDottedPath("result.value"));

  const base::DictValue* evaluate_result2 = SendSessionCommand(
      "Runtime.evaluate", std::move(evaluate_params), page_session_id2, true);
  ASSERT_EQ(3, evaluate_result2->FindIntByDottedPath("result.value"));
}

#endif  // !BUILDFLAG(IS_ANDROID)

#if BUILDFLAG(IS_ANDROID)
IN_PROC_BROWSER_TEST_F(
    DevToolsProtocolTest,
    CreateWindowTargetRemainsAvailableWithoutMultiInstanceSupport) {
  if (base::android::android_info::sdk_int() >=
      base::android::android_info::SDK_VERSION_S) {
    GTEST_SKIP() << "This test covers the pre-Android S fallback";
  }

  AttachToBrowserTarget();
  const base::DictValue* create_result = SendCommandSync(
      "Target.createTarget",
      base::DictValue().Set("url", "about:blank").Set("newWindow", true));

  ASSERT_FALSE(error());
  ASSERT_TRUE(create_result);
  EXPECT_TRUE(create_result->FindString("targetId"));
}

IN_PROC_BROWSER_TEST_F(DevToolsProtocolTest,
                       AutoAttachedWindowTargetCanQueryPendingBrowserWindow) {
  if (base::android::android_info::sdk_int() <
      base::android::android_info::SDK_VERSION_S) {
    GTEST_SKIP() << "Pending browser windows require Android S+";
  }

  AttachToBrowserTarget();

  // A normal Android browser window requires an existing Activity to launch
  // from. Wait for the test's initial window to finish registering before
  // exercising creation of a second, pending window.
  ASSERT_TRUE(base::test::RunUntil(
      [] { return !GetAllBrowserWindowInterfaces().empty(); }));

  SendCommandSync("Target.setAutoAttach",
                  base::DictValue()
                      .Set("autoAttach", true)
                      .Set("waitForDebuggerOnStart", false)
                      .Set("flatten", true));
  ASSERT_TRUE(result());
  ClearNotifications();

  SendCommandAsync(
      "Target.createTarget",
      base::DictValue().Set("url", "about:blank").Set("newWindow", true));

  // The target is exposed during command dispatch, before Android can finish
  // creating and globally registering its Activity-backed browser window.
  auto is_page_target = [](const base::DictValue& params) {
    const std::string* type = params.FindStringByDottedPath("targetInfo.type");
    return type && *type == "page";
  };
  ASSERT_TRUE(HasExistingNotificationMatching(
      [&is_page_target](const base::DictValue& notification) {
        const std::string* method = notification.FindString("method");
        const base::DictValue* params = notification.FindDict("params");
        return method && *method == "Target.attachedToTarget" && params &&
               is_page_target(*params);
      }));
  const base::DictValue attached = WaitForMatchingNotification(
      "Target.attachedToTarget", base::BindRepeating(is_page_target));
  const std::string* target_id =
      attached.FindStringByDottedPath("targetInfo.targetId");
  ASSERT_TRUE(target_id);

  const base::DictValue* window_result =
      SendCommandSync("Browser.getWindowForTarget",
                      base::DictValue().Set("targetId", *target_id));
  ASSERT_TRUE(window_result);
  const std::optional<int> window_id = window_result->FindInt("windowId");
  ASSERT_TRUE(window_id.has_value());
  const base::DictValue* bounds = window_result->FindDict("bounds");
  ASSERT_TRUE(bounds);
  EXPECT_TRUE(bounds->FindInt("left").has_value());
  EXPECT_TRUE(bounds->FindInt("top").has_value());
  EXPECT_TRUE(bounds->FindInt("width").has_value());
  EXPECT_TRUE(bounds->FindInt("height").has_value());
  EXPECT_TRUE(bounds->FindString("windowState"));

  EXPECT_EQ(nullptr,
            BrowserHandlerAndroid::FindBrowserWindowById(window_id.value()));
}

IN_PROC_BROWSER_TEST_F(DevToolsProtocolTest,
                       CreateTargetUsesDefaultBrowserContext) {
  AttachToBrowserTarget();

  content::WebContents* initial_web_contents =
      chrome_test_utils::GetActiveWebContents(this);
  ASSERT_TRUE(initial_web_contents);

  base::DictValue params;
  params.Set("url", "about:blank");
  params.Set("newWindow", false);
  const base::DictValue* result =
      SendCommandSync("Target.createTarget", std::move(params));
  ASSERT_TRUE(result);
  const std::string* target_id = result->FindString("targetId");
  ASSERT_TRUE(target_id);
  const std::string created_target_id = *target_id;

  scoped_refptr<content::DevToolsAgentHost> agent_host =
      content::DevToolsAgentHost::GetForId(created_target_id);
  ASSERT_TRUE(agent_host);
  content::WebContents* web_contents = agent_host->GetWebContents();
  ASSERT_TRUE(web_contents);
  EXPECT_EQ(initial_web_contents->GetBrowserContext(),
            web_contents->GetBrowserContext());

  TabAndroid* tab = TabAndroid::FromWebContents(web_contents);
  ASSERT_TRUE(tab);
  EXPECT_EQ(web_contents->GetBrowserContext(), tab->profile());

  params = base::DictValue();
  params.Set("targetId", created_target_id);
  ASSERT_TRUE(SendCommandSync("Target.closeTarget", std::move(params)));
}

IN_PROC_BROWSER_TEST_F(DevToolsProtocolTest,
                       CreateTargetUsesRequestedBrowserContext) {
  AttachToBrowserTarget();

  const base::DictValue* result =
      SendCommandSync("Target.createBrowserContext");
  ASSERT_TRUE(result);
  const std::string* browser_context_id =
      result->FindString("browserContextId");
  ASSERT_TRUE(browser_context_id);
  const std::string context_id = *browser_context_id;

  base::DictValue params;
  params.Set("url", "about:blank");
  params.Set("newWindow", true);
  params.Set("browserContextId", context_id);
  result = SendCommandSync("Target.createTarget", std::move(params));
  ASSERT_TRUE(result);
  const std::string* target_id_value = result->FindString("targetId");
  ASSERT_TRUE(target_id_value);
  const std::string target_id = *target_id_value;

  scoped_refptr<content::DevToolsAgentHost> agent_host =
      content::DevToolsAgentHost::GetForId(target_id);
  ASSERT_TRUE(agent_host);
  content::WebContents* web_contents = agent_host->GetWebContents();
  ASSERT_TRUE(web_contents);
  EXPECT_EQ(context_id, web_contents->GetBrowserContext()->UniqueId());
  TabAndroid* tab = TabAndroid::FromWebContents(web_contents);
  ASSERT_TRUE(tab);
  EXPECT_EQ(web_contents->GetBrowserContext(), tab->profile());

  result = SendCommandSync("Target.getTargets");
  ASSERT_TRUE(result);
  const base::ListValue* target_infos = result->FindList("targetInfos");
  ASSERT_TRUE(target_infos);

  const base::Value* created_target_info = nullptr;
  for (const auto& target : *target_infos) {
    const std::string* listed_target_id =
        target.GetDict().FindString("targetId");
    if (listed_target_id && *listed_target_id == target_id) {
      created_target_info = &target;
      break;
    }
  }

  ASSERT_TRUE(created_target_info);
  const std::string* listed_browser_context_id =
      created_target_info->GetDict().FindString("browserContextId");
  ASSERT_TRUE(listed_browser_context_id);
  EXPECT_EQ(context_id, *listed_browser_context_id);

  params = base::DictValue();
  params.Set("browserContextId", context_id);
  ASSERT_TRUE(
      SendCommandSync("Target.disposeBrowserContext", std::move(params)));
  EXPECT_FALSE(agent_host->GetWebContents());
}

IN_PROC_BROWSER_TEST_F(
    DevToolsProtocolTest,
    DisposeBrowserContextClosesFrozenTabsAcrossTabModelRemoval) {
  AttachToBrowserTarget();

  const base::DictValue* result =
      SendCommandSync("Target.createBrowserContext");
  ASSERT_TRUE(result);
  const std::string* browser_context_id =
      result->FindString("browserContextId");
  ASSERT_TRUE(browser_context_id);
  const std::string context_id = *browser_context_id;

  base::DictValue params;
  params.Set("url", "about:blank");
  params.Set("newWindow", true);
  params.Set("browserContextId", context_id);
  result = SendCommandSync("Target.createTarget", std::move(params));
  ASSERT_TRUE(result);
  const std::string* target_id_value = result->FindString("targetId");
  ASSERT_TRUE(target_id_value);
  const std::string target_id = *target_id_value;

  scoped_refptr<content::DevToolsAgentHost> agent_host =
      content::DevToolsAgentHost::GetForId(target_id);
  ASSERT_TRUE(agent_host);
  content::WebContents* web_contents = agent_host->GetWebContents();
  ASSERT_TRUE(web_contents);
  TabAndroid* context_tab = TabAndroid::FromWebContents(web_contents);
  ASSERT_TRUE(context_tab);
  Profile* context_profile = context_tab->profile();
  ASSERT_TRUE(context_profile);

  TabModel* context_model = TabModelList::GetTabModelForTabAndroid(context_tab);
  ASSERT_TRUE(context_model);
  ASSERT_EQ(1, context_model->GetTabCount());
  ASSERT_EQ(context_model, TabModelList::models().back());
  const size_t model_count = TabModelList::models().size();

  OwningTestTabModel later_model_a(context_profile);
  OwningTestTabModel later_model_b(context_profile);

  ASSERT_TRUE(later_model_a.AddEmptyTab(0, /*select=*/true));
  TabAndroid* frozen_tab_a = later_model_a.AddEmptyTab(1, /*select=*/false);
  ASSERT_TRUE(frozen_tab_a);
  ASSERT_TRUE(later_model_b.AddEmptyTab(0, /*select=*/true));
  TabAndroid* frozen_tab_b = later_model_b.AddEmptyTab(1, /*select=*/false);
  ASSERT_TRUE(frozen_tab_b);

  ASSERT_EQ(context_profile, frozen_tab_a->profile());
  ASSERT_EQ(context_profile, frozen_tab_b->profile());
  ASSERT_TRUE(frozen_tab_a->web_contents());
  ASSERT_TRUE(frozen_tab_b->web_contents());
  frozen_tab_a->DestroyWebContents();
  frozen_tab_b->DestroyWebContents();
  ASSERT_FALSE(frozen_tab_a->web_contents());
  ASSERT_FALSE(frozen_tab_b->web_contents());
  ASSERT_EQ(context_profile, frozen_tab_a->profile());
  ASSERT_EQ(context_profile, frozen_tab_b->profile());
  ASSERT_EQ(2, later_model_a.GetTabCount());
  ASSERT_EQ(2, later_model_b.GetTabCount());

  params = base::DictValue();
  params.Set("browserContextId", context_id);
  ASSERT_TRUE(
      SendCommandSync("Target.disposeBrowserContext", std::move(params)));

  EXPECT_EQ(0, later_model_a.GetTabCount());
  EXPECT_EQ(0, later_model_b.GetTabCount());
  EXPECT_FALSE(agent_host->GetWebContents());
  EXPECT_EQ(model_count + 1, TabModelList::models().size());

  result = SendCommandSync("Target.getBrowserContexts");
  ASSERT_TRUE(result);
  const base::ListValue* browser_context_ids =
      result->FindList("browserContextIds");
  ASSERT_TRUE(browser_context_ids);
  EXPECT_FALSE(browser_context_ids->contains(context_id));
}

IN_PROC_BROWSER_TEST_F(DevToolsProtocolTest,
                       CreateTargetRejectsUnknownBrowserContext) {
  AttachToBrowserTarget();

  base::DictValue params;
  params.Set("url", "about:blank");
  params.Set("browserContextId", "unknown");
  SendCommandSync("Target.createTarget", std::move(params));

  ASSERT_TRUE(error());
  EXPECT_EQ("Failed to find browser context with id unknown",
            *error()->FindString("message"));
}

IN_PROC_BROWSER_TEST_F(DevToolsProtocolTest, CreateListDisposeBrowserContext) {
  AttachToBrowserTarget();

  const base::DictValue* result =
      SendCommandSync("Target.createBrowserContext");
  ASSERT_TRUE(result);
  const std::string* browser_context_id =
      result->FindString("browserContextId");
  ASSERT_TRUE(browser_context_id);
  const std::string first_context_id = *browser_context_id;

  result = SendCommandSync("Target.createBrowserContext");
  ASSERT_TRUE(result);
  browser_context_id = result->FindString("browserContextId");
  ASSERT_TRUE(browser_context_id);
  const std::string second_context_id = *browser_context_id;
  EXPECT_NE(first_context_id, second_context_id);

  result = SendCommandSync("Target.getBrowserContexts");
  ASSERT_TRUE(result);
  const base::ListValue* browser_context_ids =
      result->FindList("browserContextIds");
  ASSERT_TRUE(browser_context_ids);
  EXPECT_TRUE(browser_context_ids->contains(first_context_id));
  EXPECT_TRUE(browser_context_ids->contains(second_context_id));
  const std::string* default_context_id =
      result->FindString("defaultBrowserContextId");
  ASSERT_TRUE(default_context_id);
  EXPECT_NE(first_context_id, *default_context_id);
  EXPECT_NE(second_context_id, *default_context_id);

  base::DictValue params;
  params.Set("browserContextId", first_context_id);
  ASSERT_TRUE(
      SendCommandSync("Target.disposeBrowserContext", std::move(params)));

  result = SendCommandSync("Target.getBrowserContexts");
  ASSERT_TRUE(result);
  browser_context_ids = result->FindList("browserContextIds");
  ASSERT_TRUE(browser_context_ids);
  EXPECT_FALSE(browser_context_ids->contains(first_context_id));
  EXPECT_TRUE(browser_context_ids->contains(second_context_id));

  params = base::DictValue();
  params.Set("browserContextId", second_context_id);
  ASSERT_TRUE(
      SendCommandSync("Target.disposeBrowserContext", std::move(params)));

  result = SendCommandSync("Target.getBrowserContexts");
  ASSERT_TRUE(result);
  browser_context_ids = result->FindList("browserContextIds");
  ASSERT_TRUE(browser_context_ids);
  EXPECT_FALSE(browser_context_ids->contains(first_context_id));
  EXPECT_FALSE(browser_context_ids->contains(second_context_id));
}

#endif  // BUILDFLAG(IS_ANDROID)

IN_PROC_BROWSER_TEST_F(DevToolsProtocolTest,
                       NoInputEventsSentToBrowserWhenDisallowed) {
  SetIsTrusted(false);
  Attach();

  base::DictValue params;
  params.Set("type", "rawKeyDown");
  params.Set("key", "F12");
  params.Set("windowsVirtualKeyCode", 123);
  params.Set("nativeVirtualKeyCode", 123);
  SendCommandSync("Input.dispatchKeyEvent", std::move(params));

  EXPECT_EQ(nullptr, DevToolsWindow::FindDevToolsWindow(agent_host_.get()));
}

IN_PROC_BROWSER_TEST_F(DevToolsProtocolTest,
                       PreloadEnabledStateUpdatedDefault) {
  Attach();

  SendCommandAsync("Preload.enable");
  const base::DictValue result =
      WaitForNotification("Preload.preloadEnabledStateUpdated", true);

  EXPECT_THAT(result.FindBool("disabledByPreference"), false);
  EXPECT_THAT(result.FindBool("disabledByHoldbackPrefetchSpeculationRules"),
              false);
  EXPECT_THAT(result.FindBool("disabledByHoldbackPrerenderSpeculationRules"),
              false);
}

IN_PROC_BROWSER_TEST_F(DevToolsProtocolTest,
                       PreloadEnabledStateUpdatedDisabledByPreference) {
  Attach();

  prefetch::SetPreloadPagesState(
      chrome_test_utils::GetProfile(this)->GetPrefs(),
      prefetch::PreloadPagesState::kNoPreloading);

  SendCommandAsync("Preload.enable");
  const base::DictValue result =
      WaitForNotification("Preload.preloadEnabledStateUpdated", true);

  EXPECT_THAT(result.FindBool("disabledByPreference"), true);
}

class DevToolsProtocolTest_PreloadEnabledStateUpdatedDisabledByHoldback
    : public DevToolsProtocolTest {
 protected:
  void SetUp() override {
    // This holds back speculation rules prefetch and prerender. Note that
    // directly using enums (instead of strings) in the call to SetHoldback is
    // usually preferred, but this is not possible here because
    // content::content_preloading_predictor::kSpeculationRules, which is not
    // exposed outside of content.
    preloading_config_override_.SetHoldback("Prefetch", "SpeculationRules",
                                            true);
    preloading_config_override_.SetHoldback("Prerender", "SpeculationRules",
                                            true);

    DevToolsProtocolTest::SetUp();
  }

 private:
  content::test::PreloadingConfigOverride preloading_config_override_;
};

IN_PROC_BROWSER_TEST_F(
    DevToolsProtocolTest_PreloadEnabledStateUpdatedDisabledByHoldback,
    PreloadEnabledStateUpdatedDisabledByHoldback) {
  Attach();

  SendCommandAsync("Preload.enable");
  const base::DictValue result =
      WaitForNotification("Preload.preloadEnabledStateUpdated", true);

  EXPECT_THAT(result.FindBool("disabledByHoldbackPrefetchSpeculationRules"),
              true);
  EXPECT_THAT(result.FindBool("disabledByHoldbackPrerenderSpeculationRules"),
              true);
}

class DevToolsProtocolTest_PrefetchHoldbackDisabledIfCDPClientConnected
    : public DevToolsProtocolTest {
 protected:
  void SetUp() override {
    preloading_config_override_.SetHoldback("Prefetch", "SpeculationRules",
                                            true);

    DevToolsProtocolTest::SetUp();
  }

 private:
  content::test::PreloadingConfigOverride preloading_config_override_;
};

// Check that prefetch is enabled if DevToolsAgentHost exists even if it is
// disabled by PreloadingConfig.
IN_PROC_BROWSER_TEST_F(
    DevToolsProtocolTest_PrefetchHoldbackDisabledIfCDPClientConnected,
    PrefetchHoldbackDisabledIfCDPClientConnected) {
  Attach();

  {
    SendCommandAsync("Preload.enable");
    const base::DictValue result =
        WaitForNotification("Preload.preloadEnabledStateUpdated", true);

    EXPECT_THAT(result.FindBool("disabledByHoldbackPrefetchSpeculationRules"),
                true);
  }

  ASSERT_TRUE(embedded_test_server()->Start());
  const GURL url(embedded_test_server()->GetURL("/empty.html"));
  ASSERT_TRUE(content::NavigateToURL(
      chrome_test_utils::GetActiveWebContents(this), url));

  const std::string add_specrules = R"(
    const specrules = document.createElement("script");
    specrules.type = "speculationrules";
    specrules.text = `
      {
        "prefetch":[
          {
            "source": "list",
            "urls": ["title1.html"]
          }
        ]
      }`;
    document.body.appendChild(specrules);
  )";

  EXPECT_TRUE(content::EvalJs(web_contents(), add_specrules).is_ok());

  {
    base::DictValue result;
    while (true) {
      result = WaitForNotification("Preload.prefetchStatusUpdated", true);
      if (*result.FindString("status") == "Ready") {
        break;
      }
    }
  }
}

#if !BUILDFLAG(IS_ANDROID)
IN_PROC_BROWSER_TEST_F(
    DevToolsProtocolTest,
    NoPendingUrlShownWhenAttachedToBrowserInitiatedFailedNavigation) {
  GURL url("invalid.scheme:for-sure");
  ui_test_utils::AllBrowserTabAddedWaiter tab_added_waiter;

  content::WebContents* web_contents = browser()->OpenURL(
      content::OpenURLParams(url, content::Referrer(),
                             WindowOpenDisposition::NEW_FOREGROUND_TAB,
                             ui::PAGE_TRANSITION_TYPED, false),
      /*navigation_handle_callback=*/{});
  tab_added_waiter.Wait();
  ASSERT_TRUE(WaitForLoadStop(web_contents));

  content::NavigationController& navigation_controller =
      web_contents->GetController();
  content::NavigationEntry* pending_entry =
      navigation_controller.GetPendingEntry();
  ASSERT_NE(nullptr, pending_entry);
  EXPECT_EQ(url, pending_entry->GetURL());

  EXPECT_EQ(pending_entry, navigation_controller.GetVisibleEntry());
  agent_host_ = content::DevToolsAgentHost::GetOrCreateFor(web_contents);
  agent_host_->AttachClient(this);
  SendCommandSync("Page.enable");

  // Ensure that a failed pending entry is cleared when the DevTools protocol
  // attaches, so that any modified page content is not attributed to the failed
  // URL. (crbug.com/40055319)
  EXPECT_EQ(nullptr, navigation_controller.GetPendingEntry());
  EXPECT_EQ(GURL(""), navigation_controller.GetVisibleEntry()->GetURL());
}

IN_PROC_BROWSER_TEST_F(DevToolsProtocolTest,
                       NoPendingUrlShownForPageNavigateFromChromeExtension) {
  GURL url("https://example.com");
  // DevTools protocol use cases that have an initiator origin (e.g., for
  // extensions) should use renderer-initiated navigations and be subject to URL
  // spoof defenses.
  SetNavigationInitiatorOrigin(
      url::Origin::Create(GURL("chrome-extension://abc123/")));

  // Attach DevTools and start a navigation but don't wait for it to finish.
  Attach();
  SendCommandSync("Page.enable");
  base::DictValue params;
  params.Set("url", url.spec());
  SendCommandAsync("Page.navigate", std::move(params));
  content::NavigationController& navigation_controller =
      web_contents()->GetController();
  content::NavigationEntry* pending_entry =
      navigation_controller.GetPendingEntry();
  ASSERT_NE(nullptr, pending_entry);
  EXPECT_EQ(url, pending_entry->GetURL());

  // Attaching the DevTools protocol to the initial empty document of a new tab
  // should prevent the pending URL from being visible, since the protocol
  // allows modifying the initial empty document in a way that could be useful
  // for URL spoofs.
  EXPECT_NE(pending_entry, navigation_controller.GetVisibleEntry());
  EXPECT_NE(nullptr, navigation_controller.GetPendingEntry());
  EXPECT_EQ(GURL("about:blank"),
            navigation_controller.GetVisibleEntry()->GetURL());
}

IN_PROC_BROWSER_TEST_F(DevToolsProtocolTest, SetRPHRegistrationMode) {
  ASSERT_TRUE(embedded_test_server()->Start());
  const GURL url(embedded_test_server()->GetURL("/empty.html"));
  ASSERT_TRUE(content::NavigateToURL(
      chrome_test_utils::GetActiveWebContents(this), url));
  Attach();

  // Initial value
  custom_handlers::ProtocolHandlerRegistry* registry =
      ProtocolHandlerRegistryFactory::GetForBrowserContext(
          chrome_test_utils::GetProfile(this));
  EXPECT_EQ(custom_handlers::RphRegistrationMode::kNone,
            registry->registration_mode());

  // Set a invalid value
  base::DictValue params_invalid;
  params_invalid.Set("mode", "accept");
  SendCommand("Page.setRPHRegistrationMode", std::move(params_invalid));
  EXPECT_EQ(custom_handlers::RphRegistrationMode::kNone,
            registry->registration_mode());

  // Set a valid value
  base::DictValue params;
  params.Set("mode", "autoAccept");
  SendCommand("Page.setRPHRegistrationMode", std::move(params));
  EXPECT_EQ(custom_handlers::RphRegistrationMode::kAutoAccept,
            registry->registration_mode());
}

class URLCookieAccessObserver : public content::WebContentsObserver {
 public:
  URLCookieAccessObserver(content::WebContents* web_contents,
                          const GURL& url,
                          content::CookieAccessDetails::Type access_type)
      : WebContentsObserver(web_contents),
        url_(url),
        access_type_(access_type) {}

  void Wait() { run_loop_.Run(); }

 private:
  // WebContentsObserver overrides
  void OnCookiesAccessed(content::RenderFrameHost* render_frame_host,
                         const content::CookieAccessDetails& details) override {
    if (details.type == access_type_ && details.url == url_) {
      run_loop_.Quit();
    }
  }

  void OnCookiesAccessed(content::NavigationHandle* navigation_handle,
                         const content::CookieAccessDetails& details) override {
    if (details.type == access_type_ && details.url == url_) {
      run_loop_.Quit();
    }
  }

  GURL url_;
  content::CookieAccessDetails::Type access_type_;
  base::RunLoop run_loop_;
};

class DevToolsProtocolTest_BounceTrackingMitigations
    : public DevToolsProtocolTest {
 protected:
  void SetUp() override {
    scoped_feature_list_.InitWithFeaturesAndParameters(
        /*enabled_features=*/{{features::kBtm,
                               {{"triggering_action", "stateful_bounce"}}}},
        /*disabled_features=*/{});

    DevToolsProtocolTest::SetUp();
  }

  void SetUpOnMainThread() override {
    host_resolver()->AddRule("*", "127.0.0.1");
    DevToolsProtocolTest::SetUpOnMainThread();
  }

  void SetBlockThirdPartyCookies(bool value) {
    chrome_test_utils::GetProfile(this)->GetPrefs()->SetInteger(
        prefs::kCookieControlsMode,
        static_cast<int>(
            value ? content_settings::CookieControlsMode::kBlockThirdParty
                  : content_settings::CookieControlsMode::kOff));
  }

 private:
  base::test::ScopedFeatureList scoped_feature_list_;
};

testing::AssertionResult SimulateBtmBounce(content::WebContents* web_contents,
                                           const GURL& initial_url,
                                           const GURL& bounce_url,
                                           const GURL& final_url) {
  web_contents = web_contents->OpenURL(
      content::OpenURLParams(initial_url, content::Referrer(),
                             WindowOpenDisposition::NEW_FOREGROUND_TAB,
                             ui::PageTransition::PAGE_TRANSITION_TYPED,
                             /*is_renderer_initiated=*/false),
      {});
  if (!web_contents) {
    return testing::AssertionFailure() << "OpenURL() returned nullptr";
  }

  if (!content::WaitForLoadStop(web_contents)) {
    return testing::AssertionFailure() << "Failed to wait for loading to stop";
  }

  content::BtmService* btm_service =
      content::BtmService::Get(web_contents->GetBrowserContext());
  if (!content::NavigateToURLFromRenderer(web_contents, bounce_url)) {
    return testing::AssertionFailure()
           << "Failed to navigate to " << bounce_url;
  }

  URLCookieAccessObserver cookie_observer(
      web_contents, bounce_url, content::CookieAccessDetails::Type::kChange);
  testing::AssertionResult js_result =
      content::ExecJs(web_contents, "document.cookie = 'bounce=stateful';",
                      content::EXECUTE_SCRIPT_NO_USER_GESTURE);
  if (!js_result) {
    return js_result;
  }
  cookie_observer.Wait();

  content::BtmRedirectChainObserver final_observer(btm_service, final_url);
  if (!content::NavigateToURLFromRendererWithoutUserGesture(web_contents,
                                                            final_url)) {
    return testing::AssertionFailure() << "Failed to navigate to " << final_url;
  }

  // End redirect chain by closing the tab.
  web_contents->Close();
  final_observer.Wait();

  if (testing::Test::HasFailure()) {
    return testing::AssertionFailure() << "Failure generated while waiting for "
                                          "the redirect chain to be reported";
  }

  if (final_observer.redirects()->size() != 1) {
    return testing::AssertionFailure() << "Expected 1 redirect; found "
                                       << final_observer.redirects()->size();
  }

  const content::BtmRedirect& redirect = *final_observer.redirects()->at(0);
  if (redirect.redirector_url != bounce_url) {
    return testing::AssertionFailure() << "Expected redirect at " << bounce_url
                                       << "; found " << redirect.redirector_url;
  }

  if (redirect.access_type != content::BtmDataAccessType::kWrite &&
      redirect.access_type != content::BtmDataAccessType::kReadWrite) {
    return testing::AssertionFailure()
           << "No write access recorded for redirect";
  }

  return testing::AssertionSuccess();
}

IN_PROC_BROWSER_TEST_F(DevToolsProtocolTest_BounceTrackingMitigations,
                       RunBounceTrackingMitigations) {
  SetBlockThirdPartyCookies(true);
  ASSERT_TRUE(embedded_test_server()->Start());
  const GURL url(embedded_test_server()->GetURL("/empty.html"));
  ASSERT_TRUE(content::NavigateToURL(
      chrome_test_utils::GetActiveWebContents(this), url));
  Attach();

  const GURL bouncer(
      embedded_test_server()->GetURL("example.test", "/title1.html"));

  // Record a stateful bounce for `bouncer`.
  ASSERT_TRUE(SimulateBtmBounce(
      web_contents(), embedded_test_server()->GetURL("a.test", "/empty.html"),
      bouncer, embedded_test_server()->GetURL("b.test", "/empty.html")));

  SendCommandSync("Storage.runBounceTrackingMitigations");

  const base::ListValue* deleted_sites_list =
      result()->FindList("deletedSites");
  ASSERT_TRUE(deleted_sites_list);

  std::vector<std::string> deleted_sites;
  for (const auto& site : *deleted_sites_list) {
    deleted_sites.push_back(site.GetString());
  }

  EXPECT_THAT(deleted_sites, testing::ElementsAre("example.test"));
}

class BtmStatusDevToolsProtocolTest
    : public DevToolsProtocolTest,
      public testing::WithParamInterface<std::tuple<bool, std::string>> {
  // The fields of `GetParam()` indicate/control the following:
  //   `std::get<0>(GetParam())` => `features::kBtm`
  //   `std::get<1>(GetParam())` => `features::kBtmTriggeringAction`
  //
  // In order for Bounce Tracking Mitigations to take effect, `features::kBtm`
  // must be enabled, and `features::kTriggeringAction` must NOT be none.
  //
  // Note: Bounce Tracking Mitigations issues only report sites that would
  // be affected when `features::kTriggeringAction` is set to stateful_bounce.

 protected:
  void SetUp() override {
    if (std::get<0>(GetParam())) {
      scoped_feature_list_.InitAndEnableFeatureWithParameters(
          features::kBtm, {{"triggering_action", std::get<1>(GetParam())}});
    } else {
      scoped_feature_list_.InitAndDisableFeature(features::kBtm);
    }

    DevToolsProtocolTest::SetUp();
  }

  bool ShouldBeEnabled() {
    return (std::get<0>(GetParam()) && (std::get<1>(GetParam()) != "none"));
  }

 private:
  base::test::ScopedFeatureList scoped_feature_list_;
};

IN_PROC_BROWSER_TEST_P(BtmStatusDevToolsProtocolTest,
                       TrueWhenEnabledAndDeleting) {
  AttachToBrowserTarget();

  base::DictValue btm_params;
  btm_params.Set("featureState", "DIPS");

  SendCommand("SystemInfo.getFeatureState", std::move(btm_params));
  EXPECT_EQ(result()->FindBool("featureEnabled"), ShouldBeEnabled());
}

INSTANTIATE_TEST_SUITE_P(
    All,
    BtmStatusDevToolsProtocolTest,
    ::testing::Combine(
        ::testing::Bool(),
        ::testing::Values("none", "storage", "bounce", "stateful_bounce")));

using DevToolsProtocolTest_AppId = DevToolsProtocolTest;

IN_PROC_BROWSER_TEST_F(DevToolsProtocolTest_AppId, ReturnsManifestAppId) {
  ASSERT_TRUE(embedded_test_server()->Start());
  const GURL url(embedded_test_server()->GetURL(
      "/banners/manifest_test_page.html?manifest=manifest_with_id.json"));
  ASSERT_TRUE(content::NavigateToURL(
      chrome_test_utils::GetActiveWebContents(this), url));
  Attach();

  const base::DictValue* result = SendCommandSync("Page.getAppId");
  EXPECT_EQ(*result->FindString("appId"),
            embedded_test_server()->GetURL("/some_id"));
}

IN_PROC_BROWSER_TEST_F(DevToolsProtocolTest_AppId,
                       ReturnsStartUrlAsManifestAppIdIfNotSet) {
  ASSERT_TRUE(embedded_test_server()->Start());
  const GURL url(
      embedded_test_server()->GetURL("/web_apps/no_service_worker.html"));
  ASSERT_TRUE(content::NavigateToURL(
      chrome_test_utils::GetActiveWebContents(this), url));
  Attach();

  const base::DictValue* result = SendCommandSync("Page.getAppId");
  EXPECT_EQ(*result->FindString("appId"),
            embedded_test_server()->GetURL("/web_apps/no_service_worker.html"));
  EXPECT_EQ(*result->FindString("recommendedId"),
            "/web_apps/no_service_worker.html");
}

IN_PROC_BROWSER_TEST_F(DevToolsProtocolTest_AppId, ReturnsNoAppIdIfNoManifest) {
  ASSERT_TRUE(embedded_test_server()->Start());
  const GURL url(embedded_test_server()->GetURL("/empty.html"));
  ASSERT_TRUE(content::NavigateToURL(
      chrome_test_utils::GetActiveWebContents(this), url));
  Attach();

  const base::DictValue* result = SendCommandSync("Page.getAppId");
  EXPECT_FALSE(result->Find("appId"));
  EXPECT_FALSE(result->Find("recommendedId"));
}

IN_PROC_BROWSER_TEST_F(DevToolsProtocolTest, VisibleSecurityStateSecureState) {
  net::EmbeddedTestServer https_server(net::EmbeddedTestServer::TYPE_HTTPS);
  https_server.ServeFilesFromSourceDirectory(GetChromeTestDataDir());
  ASSERT_TRUE(https_server.Start());

  ui_test_utils::NavigateToURLBlockUntilNavigationsComplete(
      browser(), https_server.GetURL("/title1.html"), 1);
  content::NavigationEntry* entry =
      web_contents()->GetController().GetLastCommittedEntry();
  ASSERT_TRUE(entry);

  // Extract SSL status data from the navigation entry.
  scoped_refptr<net::X509Certificate> page_cert = entry->GetSSL().certificate;
  ASSERT_TRUE(page_cert);

  int ssl_version =
      net::SSLConnectionStatusToVersion(entry->GetSSL().connection_status);
  const char* page_protocol;
  net::SSLVersionToString(&page_protocol, ssl_version);

  const char* page_key_exchange_str;
  const char* page_cipher;
  const char* page_mac;
  bool is_aead;
  bool is_tls13;
  uint16_t page_cipher_suite =
      net::SSLConnectionStatusToCipherSuite(entry->GetSSL().connection_status);
  net::SSLCipherSuiteToStrings(&page_key_exchange_str, &page_cipher, &page_mac,
                               &is_aead, &is_tls13, page_cipher_suite);
  std::string page_key_exchange;
  if (page_key_exchange_str)
    page_key_exchange = page_key_exchange_str;

  const char* page_key_exchange_group =
      SSL_get_curve_name(entry->GetSSL().key_exchange_group);

  std::string page_subject_name;
  std::string page_issuer_name;
  double page_valid_from = 0.0;
  double page_valid_to = 0.0;
  if (entry->GetSSL().certificate) {
    page_subject_name = entry->GetSSL().certificate->subject().common_name;
    page_issuer_name = entry->GetSSL().certificate->issuer().common_name;
    page_valid_from =
        entry->GetSSL().certificate->valid_start().InSecondsFSinceUnixEpoch();
    page_valid_to =
        entry->GetSSL().certificate->valid_expiry().InSecondsFSinceUnixEpoch();
  }

  std::string page_certificate_network_error;
  if (net::IsCertStatusError(entry->GetSSL().cert_status)) {
    page_certificate_network_error = net::ErrorToString(
        net::MapCertStatusToNetError(entry->GetSSL().cert_status));
  }

  bool page_certificate_has_weak_signature =
      (entry->GetSSL().cert_status & net::CERT_STATUS_WEAK_SIGNATURE_ALGORITHM);

  bool page_certificate_has_sha1_signature_present =
      (entry->GetSSL().cert_status & net::CERT_STATUS_SHA1_SIGNATURE_PRESENT);

  int status = net::ObsoleteSSLStatus(entry->GetSSL().connection_status,
                                      entry->GetSSL().peer_signature_algorithm);
  bool page_modern_ssl = status == net::OBSOLETE_SSL_NONE;
  bool page_obsolete_ssl_protocol = status & net::OBSOLETE_SSL_MASK_PROTOCOL;
  bool page_obsolete_ssl_key_exchange =
      status & net::OBSOLETE_SSL_MASK_KEY_EXCHANGE;
  bool page_obsolete_ssl_cipher = status & net::OBSOLETE_SSL_MASK_CIPHER;
  bool page_obsolete_ssl_signature = status & net::OBSOLETE_SSL_MASK_SIGNATURE;

  Attach();
  SendCommandAsync("Security.enable");
  auto has_certificate = [](const base::DictValue& params) {
    return params.FindListByDottedPath(
               "visibleSecurityState.certificateSecurityState.certificate") !=
           nullptr;
  };
  base::DictValue params =
      WaitForMatchingNotification("Security.visibleSecurityStateChanged",
                                  base::BindRepeating(has_certificate));

  // Verify that the visibleSecurityState payload matches the SSL status data.
  std::string* security_state =
      params.FindStringByDottedPath("visibleSecurityState.securityState");
  ASSERT_TRUE(security_state);
  ASSERT_EQ(std::string("secure"), *security_state);

  base::Value* certificate_security_state =
      params.FindByDottedPath("visibleSecurityState.certificateSecurityState");
  ASSERT_TRUE(certificate_security_state);
  base::DictValue& dict = certificate_security_state->GetDict();

  std::string* protocol = dict.FindString("protocol");
  ASSERT_TRUE(protocol);
  ASSERT_EQ(*protocol, page_protocol);

  std::string* key_exchange = dict.FindString("keyExchange");
  ASSERT_TRUE(key_exchange);
  ASSERT_EQ(*key_exchange, page_key_exchange);

  std::string* key_exchange_group = dict.FindString("keyExchangeGroup");
  if (key_exchange_group) {
    ASSERT_EQ(*key_exchange_group, page_key_exchange_group);
  }

  std::string* mac = dict.FindString("mac");
  if (mac) {
    ASSERT_EQ(*mac, page_mac);
  }

  std::string* cipher = dict.FindString("cipher");
  ASSERT_TRUE(cipher);
  ASSERT_EQ(*cipher, page_cipher);

  std::string* subject_name = dict.FindString("subjectName");
  ASSERT_TRUE(subject_name);
  ASSERT_EQ(*subject_name, page_subject_name);

  std::string* issuer = dict.FindString("issuer");
  ASSERT_TRUE(issuer);
  ASSERT_EQ(*issuer, page_issuer_name);

  auto valid_from = dict.FindDouble("validFrom");
  ASSERT_TRUE(valid_from);
  ASSERT_EQ(*valid_from, page_valid_from);

  auto valid_to = dict.FindDouble("validTo");
  ASSERT_TRUE(valid_to);
  ASSERT_EQ(*valid_to, page_valid_to);

  std::string* certificate_network_error =
      dict.FindString("certificateNetworkError");
  if (certificate_network_error) {
    ASSERT_EQ(*certificate_network_error, page_certificate_network_error);
  }

  auto certificate_has_weak_signature =
      dict.FindBool("certificateHasWeakSignature");
  ASSERT_TRUE(certificate_has_weak_signature);
  ASSERT_EQ(*certificate_has_weak_signature,
            page_certificate_has_weak_signature);

  auto certificate_has_sha1_signature_present =
      dict.FindBool("certificateHasSha1Signature");
  ASSERT_TRUE(certificate_has_sha1_signature_present);
  ASSERT_EQ(*certificate_has_sha1_signature_present,
            page_certificate_has_sha1_signature_present);

  auto modern_ssl = dict.FindBool("modernSSL");
  ASSERT_TRUE(modern_ssl);
  ASSERT_EQ(*modern_ssl, page_modern_ssl);

  auto obsolete_ssl_protocol = dict.FindBool("obsoleteSslProtocol");
  ASSERT_TRUE(obsolete_ssl_protocol);
  ASSERT_EQ(*obsolete_ssl_protocol, page_obsolete_ssl_protocol);

  auto obsolete_ssl_key_exchange = dict.FindBool("obsoleteSslKeyExchange");
  ASSERT_TRUE(obsolete_ssl_key_exchange);
  ASSERT_EQ(*obsolete_ssl_key_exchange, page_obsolete_ssl_key_exchange);

  auto obsolete_ssl_cipher = dict.FindBool("obsoleteSslCipher");
  ASSERT_TRUE(obsolete_ssl_cipher);
  ASSERT_EQ(*obsolete_ssl_cipher, page_obsolete_ssl_cipher);

  auto obsolete_ssl_signature = dict.FindBool("obsoleteSslSignature");
  ASSERT_TRUE(obsolete_ssl_signature);
  ASSERT_EQ(*obsolete_ssl_signature, page_obsolete_ssl_signature);

  const base::ListValue* certificate_value = dict.FindList("certificate");
  ASSERT_TRUE(certificate_value);
  std::vector<std::string> der_certs;
  for (const auto& cert : *certificate_value) {
    std::string decoded;
    ASSERT_TRUE(base::Base64Decode(cert.GetString(), &decoded));
    der_certs.push_back(decoded);
  }
  std::vector<std::string_view> cert_string_piece;
  for (const auto& str : der_certs) {
    cert_string_piece.push_back(str);
  }

  // Check that the certificateSecurityState.certificate matches.
  net::SHA256HashValue page_cert_chain_fingerprint =
      page_cert->CalculateChainFingerprint256();
  scoped_refptr<net::X509Certificate> certificate =
      net::X509Certificate::CreateFromDERCertChain(cert_string_piece);
  ASSERT_TRUE(certificate);
  EXPECT_EQ(page_cert_chain_fingerprint,
            certificate->CalculateChainFingerprint256());
  const base::Value* security_state_issue_ids =
      params.FindByDottedPath("visibleSecurityState.securityStateIssueIds");
  ASSERT_TRUE(security_state_issue_ids->is_list());
  EXPECT_EQ(security_state_issue_ids->GetList().size(), 0u);

  EXPECT_FALSE(params.FindByDottedPath("visibleSecurityState.safetyTipInfo"));
}

IN_PROC_BROWSER_TEST_F(DevToolsProtocolTest,
                       AutomationOverrideShowsAndRemovesInfoBar) {
  Attach();
  auto* manager = infobars::ContentInfoBarManager::FromWebContents(
      chrome_test_utils::GetActiveWebContents(this));
  {
    base::DictValue params;
    params.Set("enabled", true);
    SendCommandSync("Emulation.setAutomationOverride", std::move(params));
  }
  EXPECT_EQ(manager->infobars().size(), 1u);
  {
    base::DictValue params;
    params.Set("enabled", false);
    SendCommandSync("Emulation.setAutomationOverride", std::move(params));
  }
  EXPECT_EQ(manager->infobars().size(), 0u);
}

IN_PROC_BROWSER_TEST_F(DevToolsProtocolTest,
                       AutomationOverrideAddsOneInfoBarOnly) {
  Attach();
  auto* manager = infobars::ContentInfoBarManager::FromWebContents(
      chrome_test_utils::GetActiveWebContents(this));
  {
    base::DictValue params;
    params.Set("enabled", true);
    SendCommandSync("Emulation.setAutomationOverride", std::move(params));
  }
  EXPECT_EQ(manager->infobars().size(), 1u);
  {
    base::DictValue params;
    params.Set("enabled", true);
    SendCommandSync("Emulation.setAutomationOverride", std::move(params));
  }
  EXPECT_EQ(manager->infobars().size(), 1u);
}
#endif  // !BUILDFLAG(IS_ANDROID)

IN_PROC_BROWSER_TEST_F(DevToolsProtocolTest, UntrustedClient) {
  SetIsTrusted(false);
  Attach();
  EXPECT_FALSE(SendCommandSync("HeapProfiler.enable"));  // Implemented in V8
  EXPECT_FALSE(SendCommandSync("LayerTree.enable"));     // Implemented in blink
  EXPECT_FALSE(SendCommandSync(
      "Memory.prepareForLeakDetection"));        // Implemented in content
  EXPECT_FALSE(SendCommandSync("Cast.enable"));  // Implemented in content
  EXPECT_TRUE(SendCommandSync("Accessibility.enable"));
}

#if !BUILDFLAG(IS_ANDROID)
class DevToolsProtocolScreenshotTest : public DevToolsProtocolTest {
 protected:
  void SetUp() override {
    EnablePixelOutput();
    DevToolsProtocolTest::SetUp();
  }

  SkBitmap CaptureScreenshot() {
    SendCommandSync("Page.captureScreenshot");
    CHECK(!error());
    const std::string* base64_data = result()->FindString("data");
    CHECK(base64_data);
    std::optional<std::vector<uint8_t>> png_data =
        base::Base64Decode(*base64_data);
    SkBitmap bitmap = gfx::PNGCodec::Decode(png_data.value());
    CHECK(!bitmap.isNull());
    return bitmap;
  }
};

IN_PROC_BROWSER_TEST_F(DevToolsProtocolScreenshotTest, ScreenshotInactiveTab) {
  static constexpr char kBluePageURL[] =
      R"(data:text/html,<body style="background-color: blue"></body>)";
  static constexpr char kRedPageURL[] =
      R"(data:text/html,<body style="background-color: red"></body>)";
  ASSERT_TRUE(content::NavigateToURL(web_contents(), GURL(kBluePageURL)));
  EXPECT_TRUE(WaitForLoadStop(web_contents()));
  Attach();
  constexpr int kIndex = 1;
  ASSERT_TRUE(AddTabAtIndex(kIndex, GURL(kRedPageURL),
                            ui::PageTransition::PAGE_TRANSITION_TYPED));

  SkBitmap bitmap = CaptureScreenshot();
  SkColor pixel_color = bitmap.getColor(100, 100);
  EXPECT_EQ(SK_ColorBLUE, pixel_color);
}
#endif  // !BUILDFLAG(IS_ANDROID)

#if BUILDFLAG(ENABLE_EXTENSIONS_CORE)
class ExtensionProtocolTest : public DevToolsProtocolTest {
 protected:
  void SetUpOnMainThread() override {
    DevToolsProtocolTest::SetUpOnMainThread();
    Profile* profile = chrome_test_utils::GetProfile(this);
    extension_registrar_ = extensions::ExtensionRegistrar::Get(profile);
    extension_registry_ = extensions::ExtensionRegistry::Get(profile);
  }

  content::WebContents* web_contents() override {
    return background_web_contents_;
  }

  const extensions::Extension* LoadExtensionOrApp(
      const base::FilePath& extension_path) {
    extensions::TestExtensionRegistryObserver observer(extension_registry_);
    extensions::UnpackedInstaller::Create(chrome_test_utils::GetProfile(this))
        ->Load(extension_path);
    observer.WaitForExtensionLoaded();
    const extensions::Extension* extension = nullptr;
    for (const auto& enabled_extension :
         extension_registry_->enabled_extensions()) {
      if (enabled_extension->path() == extension_path) {
        extension = enabled_extension.get();
        break;
      }
    }
    CHECK(extension) << "Failed to find loaded extension " << extension_path;
    return extension;
  }

  const extensions::Extension* LoadExtension(
      const base::FilePath& extension_path) {
    ExtensionTestMessageListener activated_listener("WORKER_ACTIVATED");
    const extensions::Extension* extension = LoadExtensionOrApp(extension_path);
    auto* process_manager =
        extensions::ProcessManager::Get(chrome_test_utils::GetProfile(this));
    if (extensions::BackgroundInfo::IsServiceWorkerBased(extension)) {
      EXPECT_TRUE(activated_listener.WaitUntilSatisfied());
      auto worker_ids =
          process_manager->GetServiceWorkersForExtension(extension->id());
      CHECK_EQ(1lu, worker_ids.size());
    } else {
      extensions::ExtensionHost* host =
          process_manager->GetBackgroundHostForExtension(extension->id());
      background_web_contents_ = host->host_contents();
    }

    return extension;
  }

#if !BUILDFLAG(IS_ANDROID)
  // Android does not support Chrome Apps.
  void LaunchApp(const std::string& app_id) {
    apps::AppLaunchParams params(
        app_id, apps::LaunchContainer::kLaunchContainerNone,
        WindowOpenDisposition::NEW_WINDOW, apps::LaunchSource::kFromTest);
    apps::AppServiceProxyFactory::GetForProfile(
        chrome_test_utils::GetProfile(this))
        ->BrowserAppLauncher()
        ->LaunchAppWithParamsForTesting(std::move(params));
  }
#endif

  void ReloadExtension(const std::string& extension_id) {
    extensions::TestExtensionRegistryObserver observer(extension_registry_);
    extension_registrar_->ReloadExtension(extension_id);
    observer.WaitForExtensionLoaded();
  }

 private:
  raw_ptr<extensions::ExtensionRegistrar, DanglingUntriaged>
      extension_registrar_;
  raw_ptr<extensions::ExtensionRegistry, DanglingUntriaged> extension_registry_;
  raw_ptr<content::WebContents, DanglingUntriaged> background_web_contents_;
#if BUILDFLAG(IS_WIN)
  // This is needed to stop ExtensionProtocolTestsfrom creating a
  // shortcut in the Windows start menu. The override needs to last until the
  // test is destroyed, because Windows shortcut tasks which create the shortcut
  // can run after the test body returns.
  base::ScopedPathOverride override_start_dir{base::DIR_START_MENU};
#endif  // BUILDFLAG(IS_WIN

  // TODO(https://crbug.com/40804030): Remove this when updated to use MV3.
  extensions::ScopedTestMV2Enabler mv2_enabler_;
};

IN_PROC_BROWSER_TEST_F(ExtensionProtocolTest, ReloadTracedExtension) {
  base::FilePath extension_path =
      base::PathService::CheckedGet(chrome::DIR_TEST_DATA)
          .AppendASCII("devtools")
          .AppendASCII("extensions")
          .AppendASCII("simple_background_page");
  auto* extension = LoadExtension(extension_path);
  ASSERT_TRUE(extension);
  Attach();
  ReloadExtension(extension->id());
  base::DictValue params;
  params.Set("categories", "-*");
  SendCommandSync("Tracing.start", std::move(params));
  SendCommandAsync("Tracing.end");
  WaitForNotification("Tracing.tracingComplete", true);
}

IN_PROC_BROWSER_TEST_F(ExtensionProtocolTest,
                       DISABLED_ReloadServiceWorkerExtension) {
  base::FilePath extension_path =
      base::PathService::CheckedGet(chrome::DIR_TEST_DATA)
          .AppendASCII("devtools")
          .AppendASCII("extensions")
          .AppendASCII("service_worker");
  std::string extension_id;
  {
    // `extension` is stale after reload.
    auto* extension = LoadExtension(extension_path);
    ASSERT_THAT(extension, testing::NotNull());
    extension_id = extension->id();
  }
  AttachToBrowserTarget();
  const base::DictValue* result = SendCommandSync("Target.getTargets");

  base::DictValue ext_target;
  for (const auto& target : *result->FindList("targetInfos")) {
    if (*target.GetDict().FindString("type") == "service_worker") {
      ext_target = target.Clone().TakeDict();
      break;
    }
  }
  {
    base::DictValue params;
    params.Set("targetId", CHECK_DEREF(ext_target.FindString("targetId")));
    params.Set("waitForDebuggerOnStart", false);
    SendCommandSync("Target.autoAttachRelated", std::move(params));
  }
  ReloadExtension(extension_id);
  base::DictValue attached =
      WaitForNotification("Target.attachedToTarget", true);
  base::Value* targetInfo = attached.Find("targetInfo");
  ASSERT_THAT(targetInfo, testing::NotNull());
  EXPECT_THAT(*targetInfo, base::test::DictionaryHasValue(
                               "type", base::Value("service_worker")));
  EXPECT_THAT(*targetInfo, base::test::DictionaryHasValue(
                               "url", CHECK_DEREF(ext_target.Find("url"))));
  EXPECT_THAT(attached.FindBool("waitingForDebugger"),
              testing::Optional(false));

  {
    base::DictValue params;
    params.Set("targetId", *targetInfo->GetDict().FindString("targetId"));
    params.Set("waitForDebuggerOnStart", false);
    SendCommandSync("Target.autoAttachRelated", std::move(params));
  }
  auto detached = WaitForNotification("Target.detachedFromTarget", true);
  EXPECT_THAT(*detached.FindString("sessionId"), Eq("sessionId"));
}

// TODO(https://crbug.com/501442926): The following tests use guest view in a
// Chrome App, which is only supported on ChromeOS.
#if BUILDFLAG(IS_CHROMEOS)

// Accepts a list of URL predicates and allows awaiting for all matching
// WebContents to load.
class WebContentsBarrier {
 public:
  using Predicate = base::FunctionRef<bool(const GURL& url)>;

  WebContentsBarrier(std::initializer_list<Predicate> predicates)
      : predicates_(predicates) {}

  std::vector<raw_ptr<content::WebContents, VectorExperimental>> Await() {
    if (!IsReady()) {
      base::RunLoop run_loop;
      ready_callback_ = run_loop.QuitClosure();
      run_loop.Run();
      CHECK(IsReady());
    }
    return std::move(ready_web_contents_);
  }

 private:
  class LoadObserver : public content::WebContentsObserver {
   public:
    LoadObserver(content::WebContents& wc, WebContentsBarrier& owner)
        : WebContentsObserver(&wc), owner_(owner) {}

   private:
    void DidFinishLoad(content::RenderFrameHost* host,
                       const GURL& url) override {
      if (host != web_contents()->GetPrimaryMainFrame()) {
        return;
      }
      owner_->OnWebContentsLoaded(web_contents(), url);
    }
    const raw_ref<WebContentsBarrier> owner_;
  };

  bool IsReady() const { return !pending_contents_count_; }

  void OnWebContentsCreated(content::WebContents* wc) {
    observers_.push_back(std::make_unique<LoadObserver>(*wc, *this));
  }

  void OnWebContentsLoaded(content::WebContents* wc, const GURL& url) {
    CHECK(!IsReady());
    for (size_t i = 0; i < predicates_.size(); ++i) {
      if (!predicates_[i](url)) {
        continue;
      }
      CHECK(!ready_web_contents_[i])
          << " predicate #" << i << " matches "
          << ready_web_contents_[i]->GetLastCommittedURL() << " and " << url;
      ready_web_contents_[i] = wc;
      --pending_contents_count_;
    }
    if (IsReady() && ready_callback_) {
      std::move(ready_callback_).Run();
    }
  }

  const std::vector<Predicate> predicates_;

  std::vector<raw_ptr<content::WebContents, VectorExperimental>>
      ready_web_contents_{predicates_.size(), nullptr};
  size_t pending_contents_count_{predicates_.size()};
  base::CallbackListSubscription creation_subscription_{
      content::RegisterWebContentsCreationCallback(
          base::BindRepeating(&WebContentsBarrier::OnWebContentsCreated,
                              base::Unretained(this)))};
  std::vector<std::unique_ptr<LoadObserver>> observers_;
  base::OnceClosure ready_callback_;
};

// TODO(crbug.com/40202416): Remove this when we remove the inner WebContents
// implementation for guests.
class ExtensionProtocolTestWithGuestViewInnerWebContents
    : public ExtensionProtocolTest {
 public:
  ExtensionProtocolTestWithGuestViewInnerWebContents() {
    scoped_feature_list_.InitAndDisableFeature(features::kGuestViewMPArch);
  }
  ~ExtensionProtocolTestWithGuestViewInnerWebContents() override = default;

 private:
  base::test::ScopedFeatureList scoped_feature_list_;
  guest_view::TestGuestViewManagerFactory guest_view_manager_factory_;
};

IN_PROC_BROWSER_TEST_F(ExtensionProtocolTestWithGuestViewInnerWebContents,
                       TabTargetWithGuestView) {
  ASSERT_FALSE(base::FeatureList::IsEnabled(features::kGuestViewMPArch));
  base::FilePath extension_path =
      base::PathService::CheckedGet(chrome::DIR_TEST_DATA)
          .AppendASCII("devtools")
          .AppendASCII("extensions")
          .AppendASCII("app_with_webview");
  auto* extension = LoadExtensionOrApp(extension_path);
  ASSERT_THAT(extension, testing::NotNull());

  WebContentsBarrier barrier(
      {[](const GURL& url) -> bool {
         return base::EndsWith(url.GetPath(), "host.html");
       },
       [](const GURL& url) -> bool { return url.SchemeIs(url::kDataScheme); }});

  LaunchApp(extension->id());

  std::vector<raw_ptr<content::WebContents, VectorExperimental>> wcs =
      barrier.Await();
  ASSERT_THAT(wcs, testing::SizeIs(2));
  EXPECT_NE(wcs[0], wcs[1]);
  // Assure host and view have different DevTools hosts.
  EXPECT_NE(content::DevToolsAgentHost::GetOrCreateForTab(wcs[0]),
            content::DevToolsAgentHost::GetOrCreateForTab(wcs[1]));
  // Assure host does not auto-attach view.
  AttachToTabTarget(wcs[0]);
  base::DictValue command_params;
  command_params = base::DictValue();
  command_params.Set("autoAttach", true);
  command_params.Set("waitForDebuggerOnStart", false);
  command_params.Set("flatten", true);
  SendCommandSync("Target.setAutoAttach", std::move(command_params));
  EXPECT_FALSE(
      HasExistingNotificationMatching([](const base::DictValue& notification) {
        if (*notification.FindString("method") != "Target.attachedToTarget") {
          return false;
        }
        const std::string* url =
            notification.FindStringByDottedPath("params.targetInfo.url");
        return url && base::StartsWith(*url, "data:");
      }));
}

class ExtensionProtocolTestWithGuestViewMPArch : public ExtensionProtocolTest {
 public:
  ExtensionProtocolTestWithGuestViewMPArch() {
    scoped_feature_list_.InitAndEnableFeature(features::kGuestViewMPArch);
  }
  ~ExtensionProtocolTestWithGuestViewMPArch() override = default;

  guest_view::TestGuestViewManager* GetGuestViewManager() {
    return guest_view_manager_factory_.GetOrCreateTestGuestViewManager(
        chrome_test_utils::GetProfile(this),
        extensions::ExtensionsAPIClient::Get()
            ->CreateGuestViewManagerDelegate());
  }

 private:
  base::test::ScopedFeatureList scoped_feature_list_;
  guest_view::TestGuestViewManagerFactory guest_view_manager_factory_;
};

IN_PROC_BROWSER_TEST_F(ExtensionProtocolTestWithGuestViewMPArch,
                       TabTargetDoesNotAutoAttachGuestView) {
  ASSERT_TRUE(base::FeatureList::IsEnabled(features::kGuestViewMPArch));
  base::FilePath extension_path =
      base::PathService::CheckedGet(chrome::DIR_TEST_DATA)
          .AppendASCII("devtools")
          .AppendASCII("extensions")
          .AppendASCII("app_with_webview");
  auto* extension = LoadExtensionOrApp(extension_path);
  ASSERT_THAT(extension, testing::NotNull());

  WebContentsBarrier barrier({[](const GURL& url) -> bool {
    return base::EndsWith(url.GetPath(), "host.html");
  }});
  LaunchApp(extension->id());
  std::vector<raw_ptr<content::WebContents, VectorExperimental>> wcs =
      barrier.Await();
  ASSERT_THAT(wcs, testing::SizeIs(1));

  auto* guest_view = GetGuestViewManager()->WaitForSingleGuestViewCreated();
  ASSERT_TRUE(guest_view);
  GetGuestViewManager()->WaitUntilAttached(guest_view);

  // Assure tab-target does not auto-attach view.
  AttachToTabTarget(wcs[0]);
  auto command_params = base::DictValue()
                            .Set("autoAttach", true)
                            .Set("waitForDebuggerOnStart", false)
                            .Set("flatten", true);
  SendCommandSync("Target.setAutoAttach", std::move(command_params));
  EXPECT_FALSE(
      HasExistingNotificationMatching([](const base::DictValue& notification) {
        if (*notification.FindString("method") != "Target.attachedToTarget") {
          return false;
        }
        const std::string* url =
            notification.FindStringByDottedPath("params.targetInfo.url");
        return url && base::StartsWith(*url, "data:");
      }));
}

IN_PROC_BROWSER_TEST_F(ExtensionProtocolTestWithGuestViewMPArch,
                       PrimaryMainFrameTargetAutoAttachesGuestView) {
  ASSERT_TRUE(base::FeatureList::IsEnabled(features::kGuestViewMPArch));
  base::FilePath extension_path =
      base::PathService::CheckedGet(chrome::DIR_TEST_DATA)
          .AppendASCII("devtools")
          .AppendASCII("extensions")
          .AppendASCII("app_with_webview");
  auto* extension = LoadExtensionOrApp(extension_path);
  ASSERT_THAT(extension, testing::NotNull());

  WebContentsBarrier barrier({[](const GURL& url) -> bool {
    return base::EndsWith(url.GetPath(), "host.html");
  }});
  LaunchApp(extension->id());
  std::vector<raw_ptr<content::WebContents, VectorExperimental>> wcs =
      barrier.Await();
  ASSERT_EQ(wcs.size(), 1u);

  auto* guest_view = GetGuestViewManager()->WaitForSingleGuestViewCreated();
  const std::string devtools_frame_token =
      guest_view->GetGuestMainFrame()->GetDevToolsFrameToken().ToString();
  ASSERT_TRUE(guest_view);
  GetGuestViewManager()->WaitUntilAttached(guest_view);

  AttachToWebContents(wcs[0]);
  auto command_params = base::DictValue()
                            .Set("autoAttach", true)
                            .Set("waitForDebuggerOnStart", false)
                            .Set("flatten", true);
  SendCommand("Target.setAutoAttach", std::move(command_params));
  base::DictValue params =
      WaitForNotification("Target.attachedToTarget", /*allow_existing=*/true);

  EXPECT_EQ("webview", *params.FindStringByDottedPath("targetInfo.type"));
  EXPECT_EQ(devtools_frame_token,
            *params.FindStringByDottedPath("targetInfo.targetId"));
  EXPECT_EQ(wcs[0]->GetPrimaryMainFrame()->GetDevToolsFrameToken().ToString(),
            content::DevToolsAgentHost::GetForId(devtools_frame_token)
                ->GetParentId());
}

IN_PROC_BROWSER_TEST_F(ExtensionProtocolTestWithGuestViewMPArch,
                       GuestViewIframeContentFrameUpdatedAfterAttach) {
  ASSERT_TRUE(base::FeatureList::IsEnabled(features::kGuestViewMPArch));
  base::FilePath extension_path =
      base::PathService::CheckedGet(chrome::DIR_TEST_DATA)
          .AppendASCII("devtools")
          .AppendASCII("extensions")
          .AppendASCII("app_with_webview");
  auto* extension = LoadExtensionOrApp(extension_path);
  ASSERT_THAT(extension, testing::NotNull());

  WebContentsBarrier barrier({[](const GURL& url) -> bool {
    return base::EndsWith(url.GetPath(), "host.html");
  }});
  LaunchApp(extension->id());
  std::vector<raw_ptr<content::WebContents, VectorExperimental>> wcs =
      barrier.Await();
  ASSERT_EQ(wcs.size(), 1u);

  auto* guest_view = GetGuestViewManager()->WaitForSingleGuestViewCreated();
  ASSERT_TRUE(guest_view);
  GetGuestViewManager()->WaitUntilAttached(guest_view);

  AttachToWebContents(wcs[0]);

  // Get the document's NodeId.
  const base::DictValue* result = SendCommandSync("DOM.getDocument");
  ASSERT_TRUE(result);
  int document_node_id = result->FindIntByDottedPath("root.nodeId").value();

  // Get the <webview>'s nodeId (by searching for it using querySelector).
  auto params = base::DictValue()
                    .Set("nodeId", document_node_id)
                    .Set("selector", "webview");
  result = SendCommandSync("DOM.querySelector", std::move(params));
  ASSERT_TRUE(result);
  int web_view_node_id = result->FindInt("nodeId").value();

  // Get the <webview> shadow tree (using its nodeId), and retrieve its
  // placeholder <iframe>'s frameId. The result from "DOM.describeNode" will
  // look something like:
  // {"node": {
  //    ...,
  //    "shadowRoots": [{
  //      ...,
  //      "children": [{
  //        ...,
  //        "frameId": "...."
  //       }]
  //    }]
  // }
  params = base::DictValue()
               .Set("nodeId", web_view_node_id)
               .Set("depth", 2)
               .Set("pierce", true);
  result = SendCommandSync("DOM.describeNode", std::move(params));
  ASSERT_TRUE(result);
  auto* frame_id = result->FindListByDottedPath("node.shadowRoots")
                       ->front()
                       .GetDict()
                       .FindList("children")
                       ->front()
                       .GetDict()
                       .FindString("frameId");
  ASSERT_TRUE(frame_id);
  // The frameId (i.e. the placeholder RemoteFrame's devtools_frame_token)
  // should match the devtools_frame_token of the guest's main frame.
  EXPECT_EQ(
      *frame_id,
      guest_view->GetGuestMainFrame()->GetDevToolsFrameToken().ToString());
}
#endif  // BUILDFLAG(IS_CHROMEOS)

#endif  // BUILDFLAG(ENABLE_EXTENSIONS_CORE)

class PrerenderDataSaverProtocolTest : public DevToolsProtocolTest {
 public:
  PrerenderDataSaverProtocolTest()
      : prerender_helper_(base::BindRepeating(
            &PrerenderDataSaverProtocolTest::GetActiveWebContents,
            base::Unretained(this))) {}

 protected:
  void SetUp() override {
    prerender_helper_.RegisterServerRequestMonitor(embedded_test_server());
    data_saver::OverrideIsDataSaverEnabledForTesting(true);
    DevToolsProtocolTest::SetUp();
  }

  content::test::PrerenderTestHelper* prerender_helper() {
    return &prerender_helper_;
  }

  void TearDown() override {
    data_saver::ResetIsDataSaverEnabledForTesting();
    DevToolsProtocolTest::TearDown();
  }

  content::WebContents* GetActiveWebContents() {
    return chrome_test_utils::GetActiveWebContents(this);
  }

 private:
  content::test::PrerenderTestHelper prerender_helper_;
};

IN_PROC_BROWSER_TEST_F(PrerenderDataSaverProtocolTest,
                       CheckReportedDisabledByDataSaverPreloadingState) {
  base::HistogramTester histogram_tester;
  ASSERT_TRUE(embedded_test_server()->Start());

  Attach();
  SendCommandSync("Runtime.enable");

  const GURL initial_url = embedded_test_server()->GetURL("/empty.html");
  const GURL prerendering_url =
      embedded_test_server()->GetURL("/empty.html?prerender");

  content::test::PrerenderHostRegistryObserver observer(
      *GetActiveWebContents());
  ASSERT_TRUE(content::NavigateToURL(
      chrome_test_utils::GetActiveWebContents(this), initial_url));
  prerender_helper()->AddPrerenderAsync(prerendering_url);
  observer.WaitForTrigger(prerendering_url);

  content::PrerenderHostId host_id =
      prerender_helper()->GetHostForUrl(prerendering_url);
  EXPECT_TRUE(host_id.is_null());

  SendCommandAsync("Preload.enable");
  const base::DictValue result =
      WaitForNotification("Preload.preloadEnabledStateUpdated", true);

  EXPECT_THAT(result.FindBool("disabledByDataSaver"), true);
  histogram_tester.ExpectUniqueSample(
      "Prerender.Experimental.PrerenderHostFinalStatus.SpeculationRule",
      /*PrerenderFinalStatus::kDataSaverEnabled=*/38, 1);
}

#if !BUILDFLAG(IS_ANDROID)
class PrivacySandboxAttestationsOverrideTest
    : public InProcessBrowserTestMixinHostSupport<DevToolsProtocolTest> {
 public:
  PrivacySandboxAttestationsOverrideTest() = default;

 private:
  privacy_sandbox::PrivacySandboxAttestationsMixin
      privacy_sandbox_attestations_mixin_{&mixin_host_};
};

IN_PROC_BROWSER_TEST_F(PrivacySandboxAttestationsOverrideTest,
                       PrivacySandboxEnrollmentOverride) {
  Attach();

  base::DictValue btm_params;
  const std::string attestation_url = "https://google.com";
  btm_params.Set("url", attestation_url);

  SendCommand("Browser.addPrivacySandboxEnrollmentOverride",
              std::move(btm_params));

  EXPECT_TRUE(
      privacy_sandbox::PrivacySandboxAttestations::GetInstance()->IsOverridden(
          net::SchemefulSite(GURL(attestation_url))));
}

IN_PROC_BROWSER_TEST_F(PrivacySandboxAttestationsOverrideTest,
                       PrivacySandboxEnrollmentOverrideInvalidUrl) {
  Attach();

  base::DictValue btm_params;
  const std::string attestation_url = "this is a bad url";
  btm_params.Set("url", attestation_url);

  SendCommand("Browser.addPrivacySandboxEnrollmentOverride",
              std::move(btm_params));

  EXPECT_TRUE(error());
  EXPECT_FALSE(
      privacy_sandbox::PrivacySandboxAttestations::GetInstance()->IsOverridden(
          net::SchemefulSite(GURL(attestation_url))));
}

class DevToolsProtocolTest_RelatedWebsiteSets : public DevToolsProtocolTest {
 protected:
  const char* kPrimarySite = "https://a.test";
  const char* kAssociatedSite = "https://b.test";
  const char* kServiceSite = "https://c.test";
  const char* kPrimaryCcTLD = "https://a.cctld";

  void SetUpCommandLine(base::CommandLine* command_line) override {
    DevToolsProtocolTest::SetUpCommandLine(command_line);
    command_line->AppendSwitchASCII(
        network::switches::kUseRelatedWebsiteSet,
        base::StringPrintf(R"({"primary": "%s",)"
                           R"("associatedSites": ["%s"],)"
                           R"("serviceSites": ["%s"],)"
                           R"("ccTLDs": {"%s": ["%s"]}})",
                           kPrimarySite, kAssociatedSite, kServiceSite,
                           kPrimarySite, kPrimaryCcTLD));
  }
};

IN_PROC_BROWSER_TEST_F(DevToolsProtocolTest_RelatedWebsiteSets,
                       GetRelatedWebsiteSets) {
  ASSERT_TRUE(embedded_test_server()->Start());
  const GURL url(embedded_test_server()->GetURL("/empty.html"));
  ASSERT_TRUE(content::NavigateToURL(
      chrome_test_utils::GetActiveWebContents(this), url));
  Attach();

  SendCommandSync("Storage.getRelatedWebsiteSets");

  if (result()) {
    const base::ListValue* set_list = result()->FindList("sets");
    ASSERT_TRUE(set_list);

    base::ListValue expected =
        base::ListValue()  //
            .Append(base::DictValue()
                        .Set("associatedSites",
                             base::ListValue().Append(kAssociatedSite))
                        .Set("primarySites", base::ListValue()
                                                 .Append(kPrimaryCcTLD)
                                                 .Append(kPrimarySite))
                        .Set("serviceSites",
                             base::ListValue().Append(kServiceSite)));

    EXPECT_EQ(*set_list, expected);
  } else if (error()) {
    EXPECT_EQ(*error()->FindString("message"),
              "Failed fetching RelatedWebsiteSets");
  }
}

IN_PROC_BROWSER_TEST_F(DevToolsProtocolTest,
                       TargetCreateToFileUrlRequiresFileAccess) {
  AttachToBrowserTarget();

  base::DictValue params;
  params.Set("url", "file:///some/path");
  SendCommandSync("Target.createTarget", params.Clone());
  EXPECT_TRUE(result());

  DetachProtocolClient();
  SetMayReadLocalFiles(false);

  AttachToBrowserTarget();

  SendCommandSync("Target.createTarget", params.Clone());
  ASSERT_TRUE(error());
  EXPECT_THAT(error()->FindInt("code"),
              testing::Optional(-32000));  // SERVER_ERROR
}

IN_PROC_BROWSER_TEST_F(DevToolsProtocolTest,
                       TargetCreateToViewSourceFileUrlRequiresFileAccess) {
  AttachToBrowserTarget();

  base::DictValue params;
  params.Set("url", "view-source:file:///some/path");
  SendCommandSync("Target.createTarget", params.Clone());
  EXPECT_TRUE(result());

  DetachProtocolClient();
  SetMayReadLocalFiles(false);

  AttachToBrowserTarget();

  SendCommandSync("Target.createTarget", params.Clone());
  ASSERT_TRUE(error());
  EXPECT_THAT(error()->FindInt("code"),
              testing::Optional(-32000));  // SERVER_ERROR
}

IN_PROC_BROWSER_TEST_F(DevToolsProtocolTest,
                       HiddenTargetIsNotVisibleInTabStrip) {
  AttachToBrowserTarget();

  ASSERT_EQ(browser()->tab_strip_model()->count(), 1);

  SendCommandSync("Target.getTargets");
  const size_t initial_count = result()->FindList("targetInfos")->size();
  ASSERT_GT(initial_count, 0u);

  base::DictValue params;
  params.Set("url", "about:blank");
  params.Set("hidden", true);
  SendCommandSync("Target.createTarget", std::move(params));

  // The tab strip should not contain the new tab.
  EXPECT_EQ(browser()->tab_strip_model()->count(), 1);

  // CDP `Target.getTargets` result should contain the new target.
  SendCommandSync("Target.getTargets");
  EXPECT_EQ(initial_count + 1, result()->FindList("targetInfos")->size());
}

IN_PROC_BROWSER_TEST_F(DevToolsProtocolTest,
                       HiddenTargetClosesWhenSessionClosed) {
  AttachToBrowserTarget();

  ASSERT_EQ(browser()->tab_strip_model()->count(), 1);

  SendCommandSync("Target.getTargets");
  const size_t initial_count = result()->FindList("targetInfos")->size();
  ASSERT_GT(initial_count, 0u);

  base::DictValue params;
  params.Set("url", "about:blank");
  params.Set("hidden", true);
  SendCommandSync("Target.createTarget", std::move(params));

  // The tab strip should not contain the new tab.
  EXPECT_EQ(browser()->tab_strip_model()->count(), 1);

  // CDP `Target.getTargets` result should contain the new target.
  SendCommandSync("Target.getTargets");
  EXPECT_EQ(initial_count + 1, result()->FindList("targetInfos")->size());

  // Disconnect and connect to session.
  agent_host_->DetachClient(this);
  AttachToBrowserTarget();

  // The hidden target should be closed.
  SendCommandSync("Target.getTargets");
  EXPECT_EQ(initial_count, result()->FindList("targetInfos")->size());
}

IN_PROC_BROWSER_TEST_F(DevToolsProtocolTest, HiddenTargetCanBeClosed) {
  AttachToBrowserTarget();

  ASSERT_EQ(browser()->tab_strip_model()->count(), 1);

  SendCommandSync("Target.getTargets");
  const size_t initial_count = result()->FindList("targetInfos")->size();
  ASSERT_GT(initial_count, 0u);

  SendCommand("Target.setAutoAttach", base::DictValue()
                                          .Set("autoAttach", true)
                                          .Set("waitForDebuggerOnStart", false)
                                          .Set("flatten", true));

  SendCommandSync(
      "Target.createTarget",
      base::DictValue().Set("url", "about:blank").Set("hidden", true));

  const std::string targetId(*result()->FindStringByDottedPath("targetId"));

  // CDP `Target.getTargets` result should contain the new target.
  SendCommandSync("Target.getTargets");
  EXPECT_EQ(initial_count + 1, result()->FindList("targetInfos")->size());

  SendCommandSync("Target.closeTarget",
                  base::DictValue().Set("targetId", targetId));

  WaitForNotification("Target.detachedFromTarget", true);

  SendCommandSync("Target.getTargets");
  EXPECT_EQ(initial_count, result()->FindList("targetInfos")->size());
}

IN_PROC_BROWSER_TEST_F(DevToolsProtocolTest, HiddenTargetIsTheLastOne) {
  AttachToBrowserTarget();

  ASSERT_EQ(browser()->tab_strip_model()->count(), 1);

  base::DictValue filter_params;
  filter_params.Set(
      "filter",
      base::ListValue().Append(
          base::DictValue().Set("type", "page").Set("exclude", false)));

  SendCommandSync("Target.getTargets", filter_params.Clone());
  const base::ListValue* list = result()->FindList("targetInfos");
  ASSERT_TRUE(list && list->size() == 1);
  const std::string targetId(*list->front().GetDict().FindString("targetId"));

  SendCommandSync(
      "Target.createTarget",
      base::DictValue().Set("url", "about:blank").Set("hidden", true));

  SendCommandSync("Target.getTargets", std::move(filter_params));
  EXPECT_EQ(1u, result()->FindList("targetInfos")->size());

  ui_test_utils::BrowserDestroyedObserver observer;
  SendCommandSync("Target.closeTarget",
                  base::DictValue().Set("targetId", targetId));

  observer.Wait();
}

IN_PROC_BROWSER_TEST_F(DevToolsProtocolTest,
                       NotHiddenTargetIsVisibleInTabStrip) {
  AttachToBrowserTarget();

  ASSERT_EQ(browser()->tab_strip_model()->count(), 1);

  SendCommandSync("Target.getTargets");
  const size_t initial_count = result()->FindList("targetInfos")->size();
  ASSERT_GT(initial_count, 0u);

  base::DictValue params;
  params.Set("url", "about:blank");
  params.Set("hidden", false);
  SendCommandSync("Target.createTarget", std::move(params));

  // The tab strip should contain the new tab.
  EXPECT_EQ(browser()->tab_strip_model()->count(), 2);

  // CDP `Target.getTargets` result should contain the new target.
  SendCommandSync("Target.getTargets");
  EXPECT_EQ(initial_count + 1, result()->FindList("targetInfos")->size());
}

IN_PROC_BROWSER_TEST_F(DevToolsProtocolTest,
                       TargetGetTargetsIncludesTabEmbedderData) {
  AttachToBrowserTarget();

  ASSERT_EQ(browser()->tab_strip_model()->count(), 1);
  browser()->tab_strip_model()->SetTabPinned(0, true);

  // Open a background tab to cover active and inactive tab metadata.
  SendCommandSync("Target.createTarget", base::DictValue()
                                             .Set("url", "about:blank")
                                             .Set("forTab", true)
                                             .Set("background", true));
  ASSERT_FALSE(error());
  EXPECT_EQ(2, browser()->tab_strip_model()->count());

  const tab_groups::TabGroupId group_id =
      browser()->tab_strip_model()->AddToNewGroup({1});

  base::DictValue tab_filter;
  tab_filter.Set("type", "tab");
  tab_filter.Set("exclude", false);
  base::ListValue target_filter =
      base::ListValue().Append(std::move(tab_filter));
  base::DictValue get_targets_params;
  get_targets_params.Set("filter", std::move(target_filter));
  SendCommandSync("Target.getTargets", std::move(get_targets_params));
  ASSERT_TRUE(result());

  const base::ListValue* target_infos = result()->FindList("targetInfos");
  ASSERT_TRUE(target_infos);
  ASSERT_EQ(2u, target_infos->size());

  const base::DictValue* first_tab_data = nullptr;
  const base::DictValue* second_tab_data = nullptr;
  std::string first_tab_target_id;
  std::string second_tab_target_id;
  for (const auto& target_info : *target_infos) {
    const base::DictValue& target_info_dict = target_info.GetDict();
    EXPECT_EQ("tab", *target_info_dict.FindString("type"));
    const std::string* browser_context_id =
        target_info_dict.FindString("browserContextId");
    ASSERT_TRUE(browser_context_id);
    EXPECT_EQ(browser()->GetProfile()->UniqueId(), *browser_context_id);
    const std::string* target_id = target_info_dict.FindString("targetId");
    ASSERT_TRUE(target_id);
    const base::DictValue* embedder_data =
        target_info_dict.FindDict("embedderData");
    ASSERT_TRUE(embedder_data);
    std::optional<int> tab_strip_index =
        embedder_data->FindInt("tabStripIndex");
    ASSERT_TRUE(tab_strip_index);
    if (*tab_strip_index == 0) {
      first_tab_data = embedder_data;
      first_tab_target_id = *target_id;
    } else if (*tab_strip_index == 1) {
      second_tab_data = embedder_data;
      second_tab_target_id = *target_id;
    }
  }

  ASSERT_TRUE(first_tab_data);
  EXPECT_EQ(true, *first_tab_data->FindBool("tabActive"));
  EXPECT_EQ(true, *first_tab_data->FindBool("tabPinned"));

  ASSERT_TRUE(second_tab_data);
  EXPECT_EQ(false, *second_tab_data->FindBool("tabActive"));
  EXPECT_EQ(false, *second_tab_data->FindBool("tabPinned"));
  EXPECT_EQ(group_id.ToString(), *second_tab_data->FindString("tabGroupId"));

  ASSERT_FALSE(first_tab_target_id.empty());
  SendCommandSync("Target.getTargetInfo",
                  base::DictValue().Set("targetId", first_tab_target_id));
  ASSERT_TRUE(result());
  const base::Value* first_target_info_embedder_data =
      result()->FindByDottedPath("targetInfo.embedderData");
  ASSERT_TRUE(first_target_info_embedder_data);
  const base::DictValue& first_get_target_info_data =
      first_target_info_embedder_data->GetDict();
  EXPECT_EQ(0, *first_get_target_info_data.FindInt("tabStripIndex"));
  EXPECT_EQ(true, *first_get_target_info_data.FindBool("tabActive"));
  EXPECT_EQ(true, *first_get_target_info_data.FindBool("tabPinned"));

  ASSERT_FALSE(second_tab_target_id.empty());
  SendCommandSync("Target.getTargetInfo",
                  base::DictValue().Set("targetId", second_tab_target_id));
  ASSERT_TRUE(result());
  const base::Value* second_target_info_embedder_data =
      result()->FindByDottedPath("targetInfo.embedderData");
  ASSERT_TRUE(second_target_info_embedder_data);
  const base::DictValue& second_get_target_info_data =
      second_target_info_embedder_data->GetDict();
  EXPECT_EQ(1, *second_get_target_info_data.FindInt("tabStripIndex"));
  EXPECT_EQ(false, *second_get_target_info_data.FindBool("tabActive"));
  EXPECT_EQ(false, *second_get_target_info_data.FindBool("tabPinned"));
  EXPECT_EQ(group_id.ToString(),
            *second_get_target_info_data.FindString("tabGroupId"));
}
#endif  // !BUILDFLAG(IS_ANDROID)

#if !BUILDFLAG(IS_ANDROID)
class DevToolsProtocolTest_OpensDevTools : public DevToolsProtocolTest {
 public:
  std::string GetCurrentPageTargetId() {
    base::DictValue params;
    params.Set(
        "filter",
        base::ListValue().Append(
            base::DictValue().Set("type", "page").Set("exclude", false)));
    const base::DictValue* result =
        SendCommandSync("Target.getTargets", std::move(params));
    const base::ListValue* list = result->FindList("targetInfos");
    EXPECT_EQ(1u, list->size());
    return *list->front().GetDict().FindString("targetId");
  }

  const base::DictValue* OpenDevToolsForCurrentPageTarget(
      std::optional<std::string> panel_id = std::nullopt) {
    const std::string target_id = GetCurrentPageTargetId();
    base::DictValue params;
    params.Set("targetId", target_id);
    if (panel_id.has_value()) {
      params.Set("panelId", *panel_id);
    }
    return SendCommandSync("Target.openDevTools", std::move(params));
  }

  base::DictValue FindDevToolsTarget() {
    // CDP `Target.getTargets` result should contain the new DevTools target.
    const base::DictValue* result = SendCommandSync("Target.getTargets");

    base::DictValue devtools_target;
    for (const auto& target : *result->FindList("targetInfos")) {
      if (*target.GetDict().FindString("type") == "other") {
        devtools_target = target.Clone().TakeDict();
        break;
      }
    }
    EXPECT_FALSE(devtools_target.empty());

    return devtools_target;
  }
};

IN_PROC_BROWSER_TEST_F(DevToolsProtocolTest_OpensDevTools,
                       FindsDevToolsTarget) {
  AttachToBrowserTarget();

  const std::string target_id = GetCurrentPageTargetId();

  // 1. Check there is no devtools target initially.
  base::DictValue params;
  params.Set("targetId", target_id);
  const base::DictValue* result =
      SendCommandSync("Target.getDevToolsTarget", params.Clone());
  EXPECT_FALSE(error());
  EXPECT_FALSE(result->Find("targetId"));

  // 2. Open DevTools.
  OpenDevToolsForCurrentPageTarget();
  EXPECT_FALSE(error());

  // 3. Check that devtools target is found.
  const base::DictValue* devtools_target_result =
      SendCommandSync("Target.getDevToolsTarget", std::move(params));
  EXPECT_FALSE(error());
  const std::string* devtools_target_id =
      devtools_target_result->FindString("targetId");
  EXPECT_TRUE(devtools_target_id);
  EXPECT_FALSE(devtools_target_id->empty());
}

IN_PROC_BROWSER_TEST_F(DevToolsProtocolTest_OpensDevTools,
                       OpensDevTools_FailsForNonBrowserTargetSession) {
  AttachToTabTarget(web_contents());

  OpenDevToolsForCurrentPageTarget();

  EXPECT_EQ(*error()->FindString("message"), "Not allowed");
}

IN_PROC_BROWSER_TEST_F(DevToolsProtocolTest,
                       OpensDevTools_FailsForNonExistentTarget) {
  AttachToBrowserTarget();

  base::DictValue params;
  params.Set("targetId", "<NonExistentTargetId>");
  SendCommandSync("Target.openDevTools", std::move(params));

  EXPECT_EQ(*error()->FindString("message"), "No target with given id found");
}

IN_PROC_BROWSER_TEST_F(DevToolsProtocolTest_OpensDevTools,
                       OpensDevTools_OpensForPageTarget) {
  AttachToBrowserTarget();

  const base::DictValue* result = OpenDevToolsForCurrentPageTarget();

  const std::string devtools_target_id(*result->FindString("targetId"));

  const base::DictValue devtools_target = FindDevToolsTarget();
  EXPECT_EQ(devtools_target_id, *devtools_target.FindString("targetId"));

  const std::string url = *devtools_target.FindString("url");
  EXPECT_TRUE(url.find("panel") == std::string::npos);
}

IN_PROC_BROWSER_TEST_F(DevToolsProtocolTest, OpensDevTools_OpensForTabTarget) {
  AttachToBrowserTarget();

  base::DictValue tabFilter = base::DictValue();
  tabFilter.Set("type", "tab");
  tabFilter.Set("exclude", false);
  base::ListValue targetFilter = base::ListValue().Append(std::move(tabFilter));
  base::DictValue getTargetParams = base::DictValue();
  getTargetParams.Set("filter", std::move(targetFilter));

  const base::DictValue* result =
      SendCommandSync("Target.getTargets", std::move(getTargetParams));
  base::DictValue tab_target;
  for (const auto& target : *result->FindList("targetInfos")) {
    if (*target.GetDict().FindString("type") == "tab") {
      tab_target = target.Clone().TakeDict();
      break;
    }
  }

  base::DictValue params;
  params.Set("targetId", *tab_target.FindString("targetId"));
  result = SendCommandSync("Target.openDevTools", std::move(params));

  const std::string devtools_target_id(*result->FindString("targetId"));

  // CDP `Target.getTargets` result should contain the new DevTools target.
  result = SendCommandSync("Target.getTargets");

  base::DictValue devtools_target;
  for (const auto& target : *result->FindList("targetInfos")) {
    if (*target.GetDict().FindString("type") == "other") {
      devtools_target = target.Clone().TakeDict();
      break;
    }
  }

  EXPECT_FALSE(devtools_target.empty());
  EXPECT_EQ(devtools_target_id, *devtools_target.FindString("targetId"));
}

IN_PROC_BROWSER_TEST_F(DevToolsProtocolTest, OpensDevTools_OpensUndocked) {
  set_agent_host_can_close();

  base::DictValue prefs;
  prefs.Set("isUnderTest", "true");
  prefs.Set("currentDockState", "undocked");
  browser()->GetProfile()->GetPrefs()->SetDict(prefs::kDevToolsPreferences,
                                               std::move(prefs));
  AttachToBrowserTarget();

  base::DictValue tabFilter = base::DictValue();
  tabFilter.Set("type", "tab");
  tabFilter.Set("exclude", false);
  base::ListValue targetFilter = base::ListValue().Append(std::move(tabFilter));
  base::DictValue getTargetParams = base::DictValue();
  getTargetParams.Set("filter", std::move(targetFilter));

  const base::DictValue* result =
      SendCommandSync("Target.getTargets", std::move(getTargetParams));
  base::DictValue tab_target;
  for (const auto& target : *result->FindList("targetInfos")) {
    if (*target.GetDict().FindString("type") == "tab") {
      tab_target = target.Clone().TakeDict();
      break;
    }
  }

  base::DictValue params;
  params.Set("targetId", *tab_target.FindString("targetId"));
  result = SendCommandSync("Target.openDevTools", std::move(params));

  const std::string devtools_target_id(*result->FindString("targetId"));

  // CDP `Target.getTargets` result should contain the new DevTools target.
  result = SendCommandSync("Target.getTargets");

  base::DictValue devtools_target;
  for (const auto& target : *result->FindList("targetInfos")) {
    if (*target.GetDict().FindString("type") == "other") {
      devtools_target = target.Clone().TakeDict();
      break;
    }
  }

  EXPECT_FALSE(devtools_target.empty());
  EXPECT_EQ(devtools_target_id, *devtools_target.FindString("targetId"));
}

IN_PROC_BROWSER_TEST_F(DevToolsProtocolTest_OpensDevTools,
                       OpensDevTools_OpensInResourcesPanel) {
  AttachToBrowserTarget();

  const base::DictValue* result = OpenDevToolsForCurrentPageTarget("resources");
  const std::string devtools_target_id(*result->FindString("targetId"));

  const base::DictValue devtools_target = FindDevToolsTarget();
  EXPECT_EQ(devtools_target_id, *devtools_target.FindString("targetId"));

  const std::string url = *devtools_target.FindString("url");
  EXPECT_TRUE(url.find("panel=resources") != std::string::npos);
}

IN_PROC_BROWSER_TEST_F(DevToolsProtocolTest_OpensDevTools,
                       OpensDevTools_OpensInUnsuportedPanel) {
  AttachToBrowserTarget();

  const base::DictValue* result =
      OpenDevToolsForCurrentPageTarget("unsupported");
  const std::string devtools_target_id(*result->FindString("targetId"));

  const base::DictValue devtools_target = FindDevToolsTarget();
  EXPECT_EQ(devtools_target_id, *devtools_target.FindString("targetId"));

  const std::string url = *devtools_target.FindString("url");
  EXPECT_TRUE(url.find("panel") == std::string::npos);
}

class IsolatedWebMulticastSocketsTest
    : public web_app::IsolatedWebAppBrowserTestHarness,
      public content::DevToolsAgentHostClient {
 public:
  content::RenderFrameHost* InstallAndOpenIsolatedWebApp() {
    using PermissionsPolicyFeature = network::mojom::PermissionsPolicyFeature;

    auto manifest_builder =
        web_app::ManifestBuilder()
            .AddPermissionsPolicyWildcard(
                PermissionsPolicyFeature::kDirectSockets)
            .AddPermissionsPolicyWildcard(
                PermissionsPolicyFeature::kLocalNetwork)
            .AddPermissionsPolicyWildcard(
                PermissionsPolicyFeature::kLoopbackNetwork)
            .AddPermissionsPolicyWildcard(
                PermissionsPolicyFeature::kMulticastInDirectSockets);
    auto app = web_app::IsolatedWebAppBuilder(std::move(manifest_builder))
                   .BuildBundle();
    web_app::IsolatedWebAppUrlInfo url_info = app->Install(profile()).value();
    return OpenApp(url_info.app_id());
  }

  void DispatchProtocolMessage(content::DevToolsAgentHost* agent_host,
                               base::span<const uint8_t> message) override {
    std::string_view message_sv(reinterpret_cast<const char*>(message.data()),
                                message.size());
    std::optional<base::Value> parsed =
        base::JSONReader::Read(message_sv, base::JSON_ALLOW_TRAILING_COMMAS);
    if (!parsed || !parsed->is_dict()) {
      return;
    }
    base::DictValue command = std::move(parsed->GetDict());
    const std::string* method = command.FindString("method");
    if (!method) {
      return;
    }
    notifications_[*method].push_back(std::move(command));
    if (wait_for_notification_run_loop_ && method &&
        *method == wait_for_method_) {
      wait_for_notification_run_loop_->Quit();
    }
  }

  void AgentHostClosed(content::DevToolsAgentHost* agent_host) override {
    agent_host_ = nullptr;
  }

 protected:
  void Detach() {
    if (agent_host_) {
      agent_host_->DetachClient(this);
      agent_host_ = nullptr;
    }
  }

  void AttachToFrame(content::RenderFrameHost* frame_host) {
    Detach();
    agent_host_ = content::DevToolsAgentHost::GetOrCreateFor(
        content::WebContents::FromRenderFrameHost(frame_host));
    agent_host_->AttachClient(this);
  }

  void sendCommand(const std::string& method, base::DictValue params) {
    base::DictValue command;
    command.Set("id", ++last_id_);
    command.Set("method", method);
    if (!params.empty()) {
      command.Set("params", std::move(params));
    }
    std::string json_command;
    base::JSONWriter::Write(command, &json_command);
    agent_host_->DispatchProtocolMessage(
        this, std::vector<uint8_t>(json_command.begin(), json_command.end()));
  }

  base::DictValue WaitForNotification(const std::string& method,
                                      bool allow_existing = false) {
    if (allow_existing) {
      if (auto it = notifications_.find(method);
          it != notifications_.end() && !it->second.empty()) {
        base::DictValue notification = std::move(it->second.front());
        it->second.erase(it->second.begin());
        return notification;
      }
    }

    wait_for_notification_run_loop_ = std::make_unique<base::RunLoop>();
    wait_for_method_ = method;
    wait_for_notification_run_loop_->Run();
    wait_for_notification_run_loop_.reset();
    wait_for_method_ = "";
    auto it = notifications_.find(method);
    EXPECT_TRUE(it != notifications_.end());
    EXPECT_TRUE(!it->second.empty());
    base::DictValue notification = std::move(it->second.front());
    it->second.erase(it->second.begin());
    return notification;
  }

  scoped_refptr<content::DevToolsAgentHost> agent_host_;

 private:
  std::map<std::string, std::vector<base::DictValue>> notifications_;
  std::unique_ptr<base::RunLoop> wait_for_notification_run_loop_;
  std::string wait_for_method_;
  int last_id_ = 0;
};

IN_PROC_BROWSER_TEST_F(IsolatedWebMulticastSocketsTest,
                       DirectSocketsUDPJoinLeaveMulticastEvents) {
  content::RenderFrameHost* iwa_frame = InstallAndOpenIsolatedWebApp();

  auto* web_contents = content::WebContents::FromRenderFrameHost(iwa_frame);
  auto* manager =
      permissions::PermissionRequestManager::FromWebContents(web_contents);
  manager->set_auto_response_for_test(
      permissions::PermissionRequestManager::ACCEPT_ALL);

  ASSERT_TRUE(iwa_frame);

  AttachToFrame(iwa_frame);
  ASSERT_TRUE(agent_host_);

  sendCommand("Network.enable", base::DictValue());

  const std::string multicast_script =
      R"JS(
  (async () => {
    const socket = new UDPSocket({ localAddress: "0.0.0.0" });
    const { multicastController } = await socket.opened;
    if (!multicastController) {
      throw new Error("No multicastController");
    }
    await multicastController.joinGroup("237.132.100.17");
    await multicastController.leaveGroup("237.132.100.17");
    await socket.close();
  })()
  )JS";

  content::EvalJsResult result = content::EvalJs(iwa_frame, multicast_script);
  ASSERT_TRUE(result.is_ok())
      << "Connect script failed: " << result.ExtractError();

  // Check for Join event
  base::DictValue joined_params =
      WaitForNotification("Network.directUDPSocketJoinedMulticastGroup", true);
  EXPECT_EQ(*joined_params.FindStringByDottedPath("params.IPAddress"),
            "237.132.100.17");

  // Check for Leave event
  base::DictValue left_params =
      WaitForNotification("Network.directUDPSocketLeftMulticastGroup", true);
  EXPECT_EQ(*left_params.FindStringByDottedPath("params.IPAddress"),
            "237.132.100.17");

  Detach();
  agent_host_ = nullptr;
}

#endif  // !BUILDFLAG(IS_ANDROID)

}  // namespace
