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

#include <memory>
#include <string_view>

#include "base/feature_list.h"
#include "base/functional/callback_helpers.h"
#include "base/json/json_reader.h"
#include "base/memory/raw_ptr.h"
#include "base/run_loop.h"
#include "base/strings/stringprintf.h"
#include "base/test/bind.h"
#include "base/test/scoped_feature_list.h"
#include "base/test/simple_test_tick_clock.h"
#include "base/test/values_test_util.h"
#include "base/time/time.h"
#include "build/build_config.h"
#include "chrome/browser/devtools/devtools_window_testing.h"
#include "chrome/browser/extensions/api/runtime/chrome_runtime_api_delegate.h"
#include "chrome/browser/extensions/extension_apitest.h"
#include "chrome/browser/extensions/extension_service.h"
#include "chrome/browser/extensions/extension_tab_util.h"
#include "chrome/browser/profiles/profile.h"
#include "chrome/browser/ui/extensions/extension_action_test_helper.h"
#include "chrome/common/url_constants.h"
#include "content/public/browser/web_contents.h"
#include "content/public/test/browser_test.h"
#include "content/public/test/browser_test_utils.h"
#include "content/public/test/test_navigation_observer.h"
#include "extensions/browser/api/offscreen/offscreen_document_manager.h"
#include "extensions/browser/api/runtime/runtime_api.h"
#include "extensions/browser/api_test_utils.h"
#include "extensions/browser/background_script_executor.h"
#include "extensions/browser/blocklist_extension_prefs.h"
#include "extensions/browser/blocklist_state.h"
#include "extensions/browser/extension_api_frame_id_map.h"
#include "extensions/browser/extension_dialog_auto_confirm.h"
#include "extensions/browser/extension_function.h"
#include "extensions/browser/extension_host.h"
#include "extensions/browser/extension_host_registry.h"
#include "extensions/browser/extension_prefs.h"
#include "extensions/browser/extension_registry.h"
#include "extensions/browser/offscreen_document_host.h"
#include "extensions/browser/process_manager.h"
#include "extensions/browser/script_executor.h"
#include "extensions/browser/test_extension_registry_observer.h"
#include "extensions/buildflags/buildflags.h"
#include "extensions/common/extension_features.h"
#include "extensions/common/extension_id.h"
#include "extensions/test/extension_test_message_listener.h"
#include "extensions/test/result_catcher.h"
#include "extensions/test/test_extension_dir.h"
#include "net/test/embedded_test_server/embedded_test_server.h"
#include "partition_alloc/buildflags.h"
#include "url/url_constants.h"

#if !BUILDFLAG(IS_ANDROID)
#include "chrome/browser/ui/browser_window.h"
#include "chrome/browser/ui/browser_window/public/browser_window_interface.h"
#include "chrome/test/base/ui_test_utils.h"
#endif

#if BUILDFLAG(ENABLE_PLATFORM_APPS)
#include "chrome/browser/apps/platform_apps/app_browsertest_util.h"
#endif

// Includes used only by the dangling-pointer regression test below.
#if !BUILDFLAG(IS_ANDROID) && PA_BUILDFLAG(ENABLE_DANGLING_RAW_PTR_CHECKS) && \
    !BUILDFLAG(IS_CHROMEOS)
#include "base/allocator/partition_alloc_features.h"
#include "base/functional/callback_helpers.h"
#include "base/scoped_observation.h"
#include "base/task/single_thread_task_runner.h"
#include "chrome/browser/browser_process.h"
#include "chrome/browser/profiles/delete_profile_helper.h"
#include "chrome/browser/profiles/profile_manager.h"
#include "chrome/browser/profiles/profile_metrics.h"
#include "chrome/browser/profiles/profile_observer.h"
#include "chrome/browser/profiles/profile_test_util.h"
#include "chrome/test/base/profile_destruction_waiter.h"
#include "extensions/common/extension_builder.h"
#endif

static_assert(BUILDFLAG(ENABLE_EXTENSIONS_CORE));

namespace extensions {

class RuntimeApiTest : public ExtensionApiTest {
 public:
  RuntimeApiTest() = default;
  ~RuntimeApiTest() override = default;
  RuntimeApiTest(const RuntimeApiTest&) = delete;
  RuntimeApiTest& operator=(const RuntimeApiTest&) = delete;

  std::string GetActiveUrl() {
    return GetActiveWebContents()->GetLastCommittedURL().spec();
  }
};

// Tests the privileged components of chrome.runtime.
IN_PROC_BROWSER_TEST_F(RuntimeApiTest, ChromeRuntimePrivileged) {
  ASSERT_TRUE(RunExtensionTest("runtime/privileged")) << message_;
}

// Tests the unprivileged components of chrome.runtime.
IN_PROC_BROWSER_TEST_F(RuntimeApiTest, ChromeRuntimeUnprivileged) {
  ASSERT_TRUE(StartEmbeddedTestServer());
  ASSERT_TRUE(
      LoadExtension(test_data_dir_.AppendASCII("runtime/content_script")));

  // The content script runs on this page.
  ResultCatcher catcher;
  ASSERT_TRUE(content::NavigateToURL(
      GetActiveWebContents(), embedded_test_server()->GetURL("/title1.html")));
  EXPECT_TRUE(catcher.GetNextResult()) << message_;
}

IN_PROC_BROWSER_TEST_F(RuntimeApiTest, ChromeRuntimeUninstallURL) {
  // Auto-confirm the uninstall dialog.
  ScopedTestDialogAutoConfirm auto_confirm(ScopedTestDialogAutoConfirm::ACCEPT);
  ExtensionTestMessageListener ready_listener("ready");
  ASSERT_TRUE(LoadExtension(test_data_dir_.AppendASCII("runtime")
                                .AppendASCII("uninstall_url")
                                .AppendASCII("sets_uninstall_url")));
  EXPECT_TRUE(ready_listener.WaitUntilSatisfied());
  ASSERT_TRUE(RunExtensionTest("runtime/uninstall_url")) << message_;
}

class RuntimeGetPlatformInfoTest : public ExtensionApiTest,
                                   public testing::WithParamInterface<bool> {
 public:
  RuntimeGetPlatformInfoTest() {
    if (GetParam()) {
      feature_list_.InitAndEnableFeature(
          extensions_features::kApiRuntimeGetPlatformInfoNaClArch);
    } else {
      feature_list_.InitAndDisableFeature(
          extensions_features::kApiRuntimeGetPlatformInfoNaClArch);
    }
  }
  ~RuntimeGetPlatformInfoTest() override = default;
  RuntimeGetPlatformInfoTest(const RuntimeGetPlatformInfoTest&) = delete;
  RuntimeGetPlatformInfoTest& operator=(const RuntimeGetPlatformInfoTest&) =
      delete;

 private:
  base::test::ScopedFeatureList feature_list_;
};

IN_PROC_BROWSER_TEST_P(RuntimeGetPlatformInfoTest, GetPlatformInfo) {
  {
    if (!GetParam()) {
      SetCustomArg("NaCl Arch unavailable");
    }
    ASSERT_TRUE(RunExtensionTest("runtime/get_platform_info")) << message_;
  }
}

INSTANTIATE_TEST_SUITE_P(All, RuntimeGetPlatformInfoTest, ::testing::Bool());

namespace {

#if BUILDFLAG(ENABLE_EXTENSIONS)
const char kUninstallUrl[] = "https://www.google.com/";
#endif

class RuntimeAPIUpdateTest : public ExtensionApiTest {
 public:
  RuntimeAPIUpdateTest() = default;

  RuntimeAPIUpdateTest(const RuntimeAPIUpdateTest&) = delete;
  RuntimeAPIUpdateTest& operator=(const RuntimeAPIUpdateTest&) = delete;

 protected:
  void SetUpOnMainThread() override {
    ExtensionApiTest::SetUpOnMainThread();
    EXPECT_TRUE(scoped_temp_dir_.CreateUniqueTempDir());
  }

  struct ExtensionCRXData {
    std::string unpacked_relative_path;
    base::FilePath crx_path;
    explicit ExtensionCRXData(const std::string& unpacked_relative_path)
        : unpacked_relative_path(unpacked_relative_path) {}
  };

  void SetUpCRX(const std::string& root_dir,
                const std::string& pem_filename,
                std::vector<ExtensionCRXData>* crx_data_list) {
    const base::FilePath test_dir = test_data_dir_.AppendASCII(root_dir);
    const base::FilePath pem_path = test_dir.AppendASCII(pem_filename);
    for (ExtensionCRXData& crx_data : *crx_data_list) {
      crx_data.crx_path = PackExtensionWithOptions(
          test_dir.AppendASCII(crx_data.unpacked_relative_path),
          scoped_temp_dir_.GetPath().AppendASCII(
              crx_data.unpacked_relative_path + ".crx"),
          pem_path, base::FilePath());
    }
  }

  bool CrashEnabledExtension(const ExtensionId& extension_id) {
    ExtensionHost* background_host =
        ProcessManager::Get(profile())->GetBackgroundHostForExtension(
            extension_id);
    if (!background_host) {
      return false;
    }
    content::CrashTab(background_host->host_contents());
    return true;
  }

 private:
  base::ScopedTempDir scoped_temp_dir_;
};
}  // namespace

IN_PROC_BROWSER_TEST_F(ExtensionApiTest, ChromeRuntimeOpenOptionsPage) {
  ASSERT_TRUE(RunExtensionTest("runtime/open_options_page"));
}

IN_PROC_BROWSER_TEST_F(ExtensionApiTest, ChromeRuntimeOpenOptionsPageError) {
  ASSERT_TRUE(RunExtensionTest("runtime/open_options_page_error"));
}

IN_PROC_BROWSER_TEST_P(RuntimeGetPlatformInfoTest,
                       ChromeRuntimeGetPlatformInfo) {
  base::DictValue dict =
      api_test_utils::ToDict(api_test_utils::RunFunctionAndReturnSingleResult(
          new RuntimeGetPlatformInfoFunction(), "[]", profile()));
  EXPECT_TRUE(dict.contains("os"));
  EXPECT_TRUE(dict.contains("arch"));
#if defined(ARCH_CPU_RISCV64) || BUILDFLAG(IS_ANDROID)
  // Native Client had never supported RISC-V ISA or Android OS.
  EXPECT_FALSE(dict.contains("nacl_arch"));
#else
  EXPECT_EQ(dict.contains("nacl_arch"), GetParam());
#endif
}

#if BUILDFLAG(ENABLE_PLATFORM_APPS)
// Tests chrome.runtime.getPackageDirectory with an app.
IN_PROC_BROWSER_TEST_F(PlatformAppBrowserTest,
                       ChromeRuntimeGetPackageDirectoryEntryApp) {
  ASSERT_TRUE(RunExtensionTest("api_test/runtime/get_package_directory/app",
                               {.launch_as_platform_app = true}))
      << message_;
}
#endif  // BUILDFLAG(ENABLE_PLATFORM_APPS)

// Tests chrome.runtime.getPackageDirectory with an extension. Note: we use
// an html page in this test as getPackageDirectory isn't exposed on service
// workers.
IN_PROC_BROWSER_TEST_F(ExtensionApiTest,
                       ChromeRuntimeGetPackageDirectoryEntryExtension) {
  SetCustomArg("run_promise_test");
  ASSERT_TRUE(RunExtensionTest("runtime/get_package_directory/extension",
                               {.extension_url = "test/test.html"}))
      << message_;
}

// Tests that an extension calling chrome.runtime.reload() repeatedly
// will eventually be terminated.
IN_PROC_BROWSER_TEST_F(ExtensionApiTest, ExtensionTerminatedForRapidReloads) {
  // This tests an extension repeatedly calling reload and assumes that it will
  // trigger a "rapid reload" threshold. Use a test clock for deterministic
  // timing. Otherwise, it's possible that a reload legitimately takes long
  // enough that it resets the count and the threshold.
  base::SimpleTestTickClock clock;
  ChromeRuntimeAPIDelegate::set_tick_clock_for_tests(&clock);
  // Clean up test clock pointer when the test exits.
  base::ScopedClosureRunner cleanup_clock(base::BindOnce(
      []() { ChromeRuntimeAPIDelegate::set_tick_clock_for_tests(nullptr); }));

  ExtensionRegistry* registry = ExtensionRegistry::Get(profile());
  static constexpr char kManifest[] = R"(
      {
        "name": "reload",
        "version": "1.0",
        "background": {
          "service_worker": "background.js"
        },
        "manifest_version": 3
      })";

  TestExtensionDir dir;
  dir.WriteManifest(kManifest);
  dir.WriteFile(FILE_PATH_LITERAL("background.js"),
                "chrome.test.sendMessage('ready');");

  // Use a packed extension, since this is the scenario we are interested in
  // testing. Unpacked extensions are allowed more reloads within the allotted
  // time, to avoid interfering with the developer work flow.
  const Extension* extension = LoadExtension(dir.Pack());
  ASSERT_TRUE(extension);
  const ExtensionId extension_id = extension->id();

  // The current limit for fast reload is 5, so the loop limit of 10
  // be enough to trigger termination. If the extension manages to
  // reload itself that often without being terminated, the test fails
  // anyway.
  for (int i = 0; i < RuntimeAPI::kFastReloadCount + 1; i++) {
    clock.Advance(base::Milliseconds(10));
    ExtensionTestMessageListener ready_listener_reload("ready");
    TestExtensionRegistryObserver unload_observer(registry, extension_id);
    BackgroundScriptExecutor::ExecuteScriptAsync(profile(), extension_id,
                                                 "chrome.runtime.reload();");
    unload_observer.WaitForExtensionUnloaded();
    base::RunLoop().RunUntilIdle();

    if (registry->terminated_extensions().GetByID(extension_id)) {
      break;
    } else {
      EXPECT_TRUE(ready_listener_reload.WaitUntilSatisfied());
    }
  }
  ASSERT_TRUE(registry->terminated_extensions().GetByID(extension_id));
}

// Tests chrome.runtime.reload
IN_PROC_BROWSER_TEST_F(ExtensionApiTest, ChromeRuntimeReload) {
  static constexpr char kManifest[] = R"(
      {
        "name": "reload",
        "version": "1.0",
        "background": {
          "service_worker": "background.js"
        },
        "manifest_version": 3
      })";

  static constexpr char kScript[] = R"(
    chrome.test.sendMessage('ready', function(response) {
      if (response == 'reload') {
        chrome.runtime.reload();
      } else if (response == 'done') {
        chrome.test.notifyPass();
      }
    });
  )";

  TestExtensionDir dir;
  dir.WriteManifest(kManifest);
  dir.WriteFile(FILE_PATH_LITERAL("background.js"), kScript);

  // This listener will respond to the initial load of the extension
  // and tell the script to do the reload.
  ExtensionTestMessageListener ready_listener_reload("ready",
                                                     ReplyBehavior::kWillReply);
  const Extension* extension = LoadExtension(dir.UnpackedPath());
  ASSERT_TRUE(extension);
  const ExtensionId extension_id = extension->id();
  EXPECT_TRUE(ready_listener_reload.WaitUntilSatisfied());

  // This listener will respond to the ready message from the
  // reloaded extension and tell the script to finish the test.
  ExtensionTestMessageListener ready_listener_done("ready",
                                                   ReplyBehavior::kWillReply);
  ResultCatcher reload_catcher;
  ready_listener_reload.Reply("reload");
  EXPECT_TRUE(ready_listener_done.WaitUntilSatisfied());
  ready_listener_done.Reply("done");
  EXPECT_TRUE(reload_catcher.GetNextResult());
}

#if BUILDFLAG(ENABLE_PLATFORM_APPS)
IN_PROC_BROWSER_TEST_F(RuntimeApiTest, ChromeRuntimeReloadApp) {
  scoped_refptr<const Extension> extension;

  // Load and launch the app and wait for it to create a window.
  extension = base::WrapRefCounted(
      LoadAndLaunchApp(test_data_dir_.AppendASCII("runtime/platform_app")));

  const ExtensionId extension_id = extension->id();
  ExtensionRegistry* registry = ExtensionRegistry::Get(profile());

  // Reload the extension and wait for a pair of
  // ExtensionRegistry::OnExtensionUnloaded()/Loaded() calls.
  TestExtensionRegistryObserver registry_observer(registry, extension_id);
  ASSERT_TRUE(ExecuteScriptInBackgroundPageNoWait(extension_id,
                                                  "chrome.runtime.reload();"));
  ASSERT_EQ(extension, registry_observer.WaitForExtensionUnloaded());
  EXPECT_TRUE(registry->disabled_extensions().Contains(extension_id));
  ASSERT_TRUE(extension = registry_observer.WaitForExtensionLoaded());
  ASSERT_EQ(extension->id(), extension_id);
  EXPECT_TRUE(registry->enabled_extensions().Contains(extension_id));

  // Reloading the app should launch it again automatically.
  // Wait for the app to create a new window.
  ResultCatcher catcher;
  ASSERT_TRUE(catcher.GetNextResult());
}
#endif  // BUILDFLAG(ENABLE_PLATFORM_APPS)

// Tests sending messages from a webpage in the extension using
// chrome.runtime.sendMessage and responding to those from the extension's
// service worker in a chrome.runtime.onMessage listener.
IN_PROC_BROWSER_TEST_F(ExtensionApiTest, ChromeRuntimeSendMessage) {
  ASSERT_TRUE(
      RunExtensionTest("runtime/send_message", {.extension_url = "test.html"}));
}

// Simple test for chrome.runtime.getBackgroundPage with a persistent background
// page.
// Note: Uses an MV2 extension because chrome.runtime.getBackgroundPage is
// unavailable in MV3.
IN_PROC_BROWSER_TEST_F(ExtensionApiTest, ChromeGetBackgroundPage) {
  static constexpr char kManifest[] = R"(
      {
        "name": "getBackgroundPage",
        "version": "1.0",
        "background": {
          "scripts": ["background.js"]
        },
        "manifest_version": 2
      })";

  static constexpr char kBackground[] = "window.backgroundExists = true;";
  static constexpr char kTestPage[] = R"(<script src="test.js"></script>)";
  static constexpr char kTestJS[] = R"(
    chrome.test.runTests([
      function getBackgroundPage() {
        chrome.runtime.getBackgroundPage((page) => {
          chrome.test.assertTrue(page.backgroundExists);
          chrome.test.succeed();
        });
      }
    ]);
  )";

  TestExtensionDir dir;
  dir.WriteManifest(kManifest);
  dir.WriteFile(FILE_PATH_LITERAL("background.js"), kBackground);
  dir.WriteFile(FILE_PATH_LITERAL("test.html"), kTestPage);
  dir.WriteFile(FILE_PATH_LITERAL("test.js"), kTestJS);

  ASSERT_TRUE(RunExtensionTest(dir.UnpackedPath(),
                               {.extension_url = "test.html"},
                               /*load_options=*/{}));
}

// Simple test for chrome.runtime.getBackgroundPage with an MV3 service worker
// extension, which should return an error due to there being no background
// page.
IN_PROC_BROWSER_TEST_F(ExtensionApiTest, ChromeGetBackgroundPageMV3) {
  static constexpr char kManifest[] = R"(
      {
        "name": "getBackgroundPage",
        "version": "1.0",
        "background": {
          "service_worker": "worker.js"
        },
        "manifest_version": 3
      })";

  static constexpr char kWorker[] = "// We're just expecting an error";
  static constexpr char kTestPage[] = R"(<script src="test.js"></script>)";
  static constexpr char kTestJS[] = R"(
    chrome.test.runTests([
      function getBackgroundPage() {
        chrome.runtime.getBackgroundPage((page) => {
          chrome.test.assertEq(undefined, page);
          chrome.test.assertLastError('You do not have a background page.');
          chrome.test.succeed();
        });
      },

      async function getBackGroundPagePromise() {
        await chrome.test.assertPromiseRejects(
            chrome.runtime.getBackgroundPage(),
            'Error: You do not have a background page.');
        chrome.test.succeed();
      }
    ]);
  )";

  TestExtensionDir dir;
  dir.WriteManifest(kManifest);
  dir.WriteFile(FILE_PATH_LITERAL("worker.js"), kWorker);
  dir.WriteFile(FILE_PATH_LITERAL("test.html"), kTestPage);
  dir.WriteFile(FILE_PATH_LITERAL("test.js"), kTestJS);

  ASSERT_TRUE(RunExtensionTest(
      dir.UnpackedPath(), {.extension_url = "test.html"}, /*load_options=*/{}));
}

// Simple test for chrome.runtime.requestUpdateCheck using promises and
// callbacks. The actual behaviors and responses are more thoroughly tested in
// chrome_runtime_api_delegate_unittest.cc
IN_PROC_BROWSER_TEST_F(ExtensionApiTest, RuntimeRequestUpdateCheck) {
  static constexpr char kManifest[] = R"(
      {
        "name": "requestUpdateCheck",
        "version": "1.0",
        "background": {
          "service_worker": "worker.js"
        },
        "manifest_version": 3
      })";

  static constexpr char kWorker[] = R"(
    chrome.test.runTests([
      // Note: when called with a callback, the callback will receive two
      // parameters, but when called with a promise they will come back as
      // parameters on a single object.
      function noUpdateCallback() {
        chrome.runtime.requestUpdateCheck((status, details) => {
          chrome.test.assertNoLastError();
          chrome.test.assertEq('no_update', status);
          chrome.test.assertEq({version: ''}, details);

          // Another call soon after will be throttled.
          chrome.runtime.requestUpdateCheck((status, details) => {
            chrome.test.assertNoLastError();
            chrome.test.assertEq('throttled', status);
            chrome.test.assertEq({version: ''}, details);
            chrome.test.succeed();
          });
        });
      },

      async function noUpdate() {
        // Advance the throttle clock so the requests in the previous test don't
        // result in this getting a throttled response.
        await chrome.test.sendMessage('Advance');
        let result = await chrome.runtime.requestUpdateCheck();
        chrome.test.assertEq({status:'no_update', version: ''}, result);

        result = await chrome.runtime.requestUpdateCheck();
        chrome.test.assertEq({status:'throttled', version: ''}, result);
        chrome.test.succeed();
      }
    ]);
  )";
  base::SimpleTestTickClock clock;
  ChromeRuntimeAPIDelegate::set_tick_clock_for_tests(&clock);

  ExtensionTestMessageListener message_listener("Advance",
                                                ReplyBehavior::kWillReply);

  TestExtensionDir dir;
  dir.WriteManifest(kManifest);
  dir.WriteFile(FILE_PATH_LITERAL("worker.js"), kWorker);
  // In the test environment we need this to be a packed extension.
  base::FilePath crx_path = PackExtension(dir.UnpackedPath());
  ASSERT_FALSE(crx_path.empty());

  auto OnMessage = [&](const std::string& message) {
    // Advance the clock past the point it will be throttled.
    clock.Advance(base::Days(1));
    message_listener.Reply("");
  };
  message_listener.SetOnSatisfied(base::BindLambdaForTesting(OnMessage));

  ASSERT_TRUE(RunExtensionTest(crx_path, {}, {}));
}

// Tests that updating a terminated extension sends runtime.onInstalled event
// with correct previousVersion.
// Regression test for https://crbug.com/40521729.
// TODO(https://crbug.com/491516661): This uses an MV2 extension because it
// currently relies on crashing the extension's background page. We can update
// that.
IN_PROC_BROWSER_TEST_F(RuntimeAPIUpdateTest,
                       TerminatedExtensionUpdateHasCorrectPreviousVersion) {
  std::vector<ExtensionCRXData> data;
  data.emplace_back("v1");
  data.emplace_back("v2");
  SetUpCRX("runtime/update_terminated_extension", "pem.pem", &data);

  ExtensionId extension_id;
  {
    // Install version 1 of the extension.
    ResultCatcher catcher;
    const int expected_change = 1;
    const Extension* extension_v1 =
        InstallExtension(data[0].crx_path, expected_change);
    extension_id = extension_v1->id();
    ASSERT_TRUE(extension_v1);
    EXPECT_TRUE(catcher.GetNextResult());
  }

  ASSERT_TRUE(CrashEnabledExtension(extension_id));

  // The process-terminated notification may be received immediately before
  // the task that will actually update the active-extensions count, so spin
  // the message loop to ensure we are up-to-date.
  base::RunLoop().RunUntilIdle();

  {
    // Update to version 2, expect runtime.onInstalled with
    // previousVersion = '1'.
    ResultCatcher catcher;
    const int expected_change = 1;
    const Extension* extension_v2 =
        UpdateExtension(extension_id, data[1].crx_path, expected_change);
    ASSERT_TRUE(extension_v2);
    EXPECT_TRUE(catcher.GetNextResult());
  }
}

// The bug this test reproduces can only be detected when dangling-pointer
// checks are compiled in, so the test is built only in that case.
// Also exclude from ChromeOS because multiple profiles aren't supported there,
// so the test can't be set up properly.
#if !BUILDFLAG(IS_ANDROID) && PA_BUILDFLAG(ENABLE_DANGLING_RAW_PTR_CHECKS) && \
    !BUILDFLAG(IS_CHROMEOS)
namespace {

// Test helper. When the observed profile is about to be destroyed, it fires the
// extension install+load notifications. RuntimeAPI observes those and responds
// by posting a `DispatchOnInstalledEvent` task that captures the profile's
// BrowserContext -- leaving that task queued just as the profile is torn down.
class OnInstalledDuringShutdownPoster : public ProfileObserver {
 public:
  OnInstalledDuringShutdownPoster(Profile* profile,
                                  scoped_refptr<const Extension> extension)
      : extension_(std::move(extension)) {
    observation_.Observe(profile);
  }

  bool posted() const { return posted_; }

  // ProfileObserver:
  void OnProfileWillBeDestroyed(Profile* profile) override {
    observation_.Reset();
    // This fires before the profile's keyed services are destroyed, so
    // RuntimeAPI and EventRouter are still alive and will post the onInstalled
    // task that captures the now-doomed BrowserContext.
    ExtensionRegistry* registry = ExtensionRegistry::Get(profile);
    registry->TriggerOnWillBeInstalled(extension_.get(), /*is_update=*/false,
                                       /*old_name=*/std::string());
    registry->AddEnabled(extension_);
    registry->TriggerOnLoaded(extension_.get());
    posted_ = true;
  }

 private:
  scoped_refptr<const Extension> extension_;
  bool posted_ = false;
  base::ScopedObservation<Profile, ProfileObserver> observation_{this};
};

}  // namespace

// Enables crash mode for the unretained dangling-pointer check, so a dangling
// pointer reaching the posted task fails the test instead of passing silently.
class RuntimeOnInstalledShutdownTest : public RuntimeApiTest {
 public:
  RuntimeOnInstalledShutdownTest() {
    feature_list_.InitAndEnableFeatureWithParameters(
        base::features::kPartitionAllocUnretainedDanglingPtr,
        {{"mode", "crash"}});
  }

 private:
  base::test::ScopedFeatureList feature_list_;
};

// Regression test for crbug.com/360903464.
//
// During profile shutdown, RuntimeAPI's posted `DispatchOnInstalledEvent` task
// can run after its BrowserContext has been freed. The context is therefore
// passed as `MayBeDangling<void>` (`base::UnsafeDangling`) and re-checked with
// `IsValidContext()` before any dereference.
//
// The test queues that task, destroys the profile, then runs the task; it must
// not crash.
IN_PROC_BROWSER_TEST_F(RuntimeOnInstalledShutdownTest,
                       DispatchOnInstalledSurvivesProfileShutdown) {
  ProfileManager* profile_manager = g_browser_process->profile_manager();
  const base::FilePath profile_path =
      profile_manager->GenerateNextProfileDirectoryPath();
  Profile& secondary_profile =
      profiles::testing::CreateProfileSync(profile_manager, profile_path);

  // Create RuntimeAPI so it starts observing this profile's ExtensionRegistry.
  ASSERT_TRUE(RuntimeAPI::GetFactoryInstance()->Get(&secondary_profile));

  scoped_refptr<const Extension> extension = ExtensionBuilder("Test").Build();
  OnInstalledDuringShutdownPoster poster(&secondary_profile, extension);

  // Destroying the profile makes `poster` queue the onInstalled task during
  // teardown; the task then runs below, after the profile is gone.
  ProfileDestructionWaiter destruction_waiter(&secondary_profile);
  profile_manager->GetDeleteProfileHelper().MaybeScheduleProfileForDeletion(
      profile_path, base::DoNothing(),
      ProfileMetrics::DELETE_PROFILE_USER_MANAGER);
  destruction_waiter.Wait();
  EXPECT_TRUE(poster.posted());

  // Run the queued task; it must not crash.
  base::RunLoop().RunUntilIdle();
}
#endif  // !BUILDFLAG(IS_ANDROID) &&
        // PA_BUILDFLAG(ENABLE_DANGLING_RAW_PTR_CHECKS) &&
        // !BUILDFLAG(IS_CHROMEOS)

#if BUILDFLAG(ENABLE_EXTENSIONS)
// TODO(crbug.com/423725749): Port to desktop Android when a cross-platform
// browser abstraction is available.
// Tests that when the last active tab in the window belongs to the extension
// with an uninstall URL, uninstalling the extension does not close the current
// browser. Regression test for crbug.com/362452856
//
// TODO(crbug.com/415617543): Test is flaky on Linux ASan.
#if BUILDFLAG(IS_LINUX) && defined(ADDRESS_SANITIZER)
#define MAYBE_OpenUninstallUrlWhenExtensionPageIsTheOnlyActiveTab \
  DISABLED_OpenUninstallUrlWhenExtensionPageIsTheOnlyActiveTab
#else
#define MAYBE_OpenUninstallUrlWhenExtensionPageIsTheOnlyActiveTab \
  OpenUninstallUrlWhenExtensionPageIsTheOnlyActiveTab
#endif
IN_PROC_BROWSER_TEST_F(
    RuntimeApiTest,
    MAYBE_OpenUninstallUrlWhenExtensionPageIsTheOnlyActiveTab) {
  ExtensionTestMessageListener ready_listener("ready");
  // Load an extension that has set an uninstall url.
  scoped_refptr<const Extension> extension =
      LoadExtension(test_data_dir_.AppendASCII("runtime")
                        .AppendASCII("uninstall_url")
                        .AppendASCII("sets_uninstall_url"));
  EXPECT_TRUE(ready_listener.WaitUntilSatisfied());
  ASSERT_TRUE(extension.get());
  extension_registrar()->AddExtension(extension.get());
  ASSERT_TRUE(extension_registrar()->IsExtensionEnabled(extension->id()));
  TabStripModel* tabs = browser()->GetTabStripModel();

  ASSERT_EQ(1, tabs->count());
  ASSERT_EQ("about:blank", GetActiveUrl());

  // Navigate to an extension page.
  const GURL extension_page_url = extension->GetResourceURL("page.html");
  content::RenderFrameHost* new_host =
      ui_test_utils::NavigateToURL(browser(), extension_page_url);
  ASSERT_TRUE(new_host);

  EXPECT_EQ(1, tabs->count());
  EXPECT_EQ(extension_page_url.spec(), GetActiveUrl());
  // Uninstall the extension and expect its uninstall url to open in a new tab.
  extension_registrar()->UninstallExtension(
      extension->id(), UNINSTALL_REASON_USER_INITIATED, nullptr);
  content::WaitForLoadStop(tabs->GetActiveWebContents());
  EXPECT_EQ(2, tabs->count());

  // The current tab should be pointing to the uninstall url of the extension.
  EXPECT_EQ(kUninstallUrl, GetActiveUrl());

  // The tab at index 0 should now be overwritten with the default NTP.
  EXPECT_EQ(chrome::kChromeUINewTabURL,
            tabs->GetWebContentsAt(0)->GetLastCommittedURL().spec());
}

// Tests that when a blocklisted extension with a set uninstall url is
// uninstalled, its uninstall url does not open.
IN_PROC_BROWSER_TEST_F(RuntimeApiTest,
                       DoNotOpenUninstallUrlForBlocklistedExtensions) {
  ExtensionTestMessageListener ready_listener("ready");
  // Load an extension that has set an uninstall url.
  scoped_refptr<const Extension> extension =
      LoadExtension(test_data_dir_.AppendASCII("runtime")
                        .AppendASCII("uninstall_url")
                        .AppendASCII("sets_uninstall_url"));
  EXPECT_TRUE(ready_listener.WaitUntilSatisfied());
  ASSERT_TRUE(extension.get());
  extension_registrar()->AddExtension(extension.get());
  ASSERT_TRUE(extension_registrar()->IsExtensionEnabled(extension->id()));

  // Uninstall the extension and expect its uninstall url to open.
  extension_registrar()->UninstallExtension(
      extension->id(), UNINSTALL_REASON_USER_INITIATED, nullptr);
  TabStripModel* tabs = browser()->GetTabStripModel();

  EXPECT_EQ(2, tabs->count());
  content::WaitForLoadStop(tabs->GetActiveWebContents());
  // Verify the uninstall url
  EXPECT_EQ(kUninstallUrl, GetActiveUrl());

  // Close the tab pointing to the uninstall url.
  tabs->CloseWebContentsAt(tabs->active_index(), 0);
  EXPECT_EQ(1, tabs->count());
  EXPECT_EQ("about:blank", GetActiveUrl());

  // Load the same extension again, except blocklist it after installation.
  ExtensionTestMessageListener ready_listener_reload("ready");
  extension = LoadExtension(test_data_dir_.AppendASCII("runtime")
                                .AppendASCII("uninstall_url")
                                .AppendASCII("sets_uninstall_url"));
  EXPECT_TRUE(ready_listener_reload.WaitUntilSatisfied());
  extension_registrar()->AddExtension(extension.get());
  ASSERT_TRUE(extension_registrar()->IsExtensionEnabled(extension->id()));

  // Blocklist extension.
  blocklist_prefs::SetSafeBrowsingExtensionBlocklistState(
      extension->id(), BitMapBlocklistState::BLOCKLISTED_MALWARE,
      ExtensionPrefs::Get(profile()));

  // Uninstalling a blocklisted extension should not open its uninstall url.
  TestExtensionRegistryObserver observer(ExtensionRegistry::Get(profile()),
                                         extension->id());
  extension_registrar()->UninstallExtension(
      extension->id(), UNINSTALL_REASON_USER_INITIATED, nullptr);
  observer.WaitForExtensionUninstalled();

  EXPECT_EQ(1, tabs->count());
  EXPECT_TRUE(content::WaitForLoadStop(tabs->GetActiveWebContents()));
  EXPECT_EQ(url::kAboutBlankURL, GetActiveUrl());
}

// TODO(crbug.com/510816360): Remove this histogram test around M155,
// once we've gathered enough data to analyze usage.
class ChromeRuntimeUninstallURLHistogramTest : public ExtensionApiTest {
 public:
  void SetUp() override {
    histogram_tester_ = std::make_unique<base::HistogramTester>();
    ExtensionApiTest::SetUp();
  }

  void TearDown() override {
    histogram_tester_ = nullptr;
    ExtensionApiTest::TearDown();
  }

 protected:
  std::string GetUninstallURL() {
    const Extension* extension = GetSingleLoadedExtension();
    EXPECT_TRUE(extension) << "Exactly one extension should be loaded.";
    EXPECT_EQ("Uninstall URL Histogram", extension->name());
    ExtensionPrefs* extension_prefs = ExtensionPrefs::Get(profile());
    std::string url_string;
    extension_prefs->ReadPrefAsString(extension->id(), "uninstall_url",
                                      &url_string);
    return url_string;
  }

  std::unique_ptr<base::HistogramTester> histogram_tester_;
};

// Test that the histogram for counting secure and insecure uninstall URLs
// counts URLs on extension load.
IN_PROC_BROWSER_TEST_F(ChromeRuntimeUninstallURLHistogramTest,
                       PRE_UninstallURL) {
  // Write the uninstall URL into persistent storage to be read in the main
  // part of the test.
  EXPECT_TRUE(RunExtensionTest("runtime/uninstall_url_histogram")) << message_;

  // The last URL is used.
  EXPECT_EQ("https://example.com", GetUninstallURL());

  // Nothing is counted yet.
  histogram_tester_->ExpectTotalCount("Extensions.RuntimeUninstallURL.Host",
                                      /*expected_count=*/0);
}

// Confirm the uninstall URL is loaded from and histogram is emitted on
// extension load.
IN_PROC_BROWSER_TEST_F(ChromeRuntimeUninstallURLHistogramTest, UninstallURL) {
  // The last URL is used.
  EXPECT_EQ("https://example.com", GetUninstallURL());

  // After the extension is loaded, the histogram counts should have
  // been incremented. Only the last set uninstall URL is counted.
  histogram_tester_->ExpectUniqueSample("Extensions.RuntimeUninstallURL.Host",
                                        /*sample=kHTTPS*/ 0,
                                        /*expected_bucket_count=*/1);
}

#endif  // BUILDFLAG(ENABLE_EXTENSIONS)

#if !BUILDFLAG(IS_ANDROID)
// Regression test for https://crbug.com/40822787 - whether a tab opened
// from the background page (via `window.open(...)`) will be correctly
// marked as `mojom::ViewType::kTabContents`.
//
// This test runs with an MV2 extension because service workers can call
// neither 1) window.open nor 2) chrome.extension.getViews.
IN_PROC_BROWSER_TEST_F(RuntimeApiTest,
                       GetViewsOfWindowOpenedFromBackgroundPage) {
  static constexpr char kManifest[] = R"(
      {
        "name": "test",
        "version": "1.0",
        "background": {"scripts": ["background.js"]},
        "manifest_version": 2
      })";
  TestExtensionDir dir;
  dir.WriteManifest(kManifest);
  dir.WriteFile(FILE_PATH_LITERAL("background.js"), "");
  dir.WriteFile(FILE_PATH_LITERAL("index.htm"), "");

  const Extension* extension = LoadExtension(dir.UnpackedPath());
  ASSERT_TRUE(extension);

  GURL new_tab_url = extension->GetResourceURL("index.htm");
  {
    content::TestNavigationObserver nav_observer(new_tab_url);
    nav_observer.StartWatchingNewWebContents();
    ASSERT_TRUE(ExecuteScriptInBackgroundPageNoWait(
        extension->id(), R"(window.open('/index.htm', '');)"));
    nav_observer.Wait();
  }

  {
    ExtensionHost* host =
        ProcessManager::Get(profile())->GetBackgroundHostForExtension(
            extension->id());
    ASSERT_TRUE(host);
    content::DOMMessageQueue message_queue(host->host_contents());

    static constexpr char kScript[] = R"(
        const foundWindows = chrome.extension.getViews({type: 'tab'});
        domAutomationController.send(foundWindows.length);
        domAutomationController.send(foundWindows[0].location.href);
    )";
    ASSERT_TRUE(ExecuteScriptInBackgroundPageNoWait(extension->id(), kScript));

    std::string json;
    ASSERT_TRUE(message_queue.WaitForMessage(&json));
    ASSERT_EQ("1", json);

    ASSERT_TRUE(message_queue.WaitForMessage(&json));
    std::optional<base::Value> url =
        base::JSONReader::Read(json, base::JSON_ALLOW_TRAILING_COMMAS);
    ASSERT_TRUE(url->is_string());
    ASSERT_EQ(new_tab_url.spec(), url->GetString());
  }
}
#endif  // !BUILDFLAG(IS_ANDROID)

class RuntimeGetContextsApiTest : public ExtensionApiTest {
 public:
  RuntimeGetContextsApiTest() = default;
  RuntimeGetContextsApiTest(const RuntimeGetContextsApiTest&) = delete;
  RuntimeGetContextsApiTest& operator=(const RuntimeGetContextsApiTest&) =
      delete;
  ~RuntimeGetContextsApiTest() override = default;

  void SetUpOnMainThread() override {
    ExtensionApiTest::SetUpOnMainThread();
    SetUpExtension();
  }

  void TearDownOnMainThread() override {
    extension_ = nullptr;
    ExtensionApiTest::TearDownOnMainThread();
  }

  // Don't create a side panel context because desktop Android doesn't support
  // that. It's tested separately below.
  virtual void SetUpExtension() {
    static constexpr char kManifest[] =
        R"({
             "name": "Get Contexts",
             "version": "0.1",
             "manifest_version": 3,
             "permissions": ["offscreen"],
             "devtools_page": "devtools.html",
             "background": {
               "service_worker": "background.js"
             }
           })";
    test_dir_.WriteManifest(kManifest);
    test_dir_.WriteFile(FILE_PATH_LITERAL("background.js"),
                        "// Intentionally blank");
    test_dir_.WriteFile(FILE_PATH_LITERAL("page.html"),
                        "<html>Hello, world!</html>");
    test_dir_.WriteFile(FILE_PATH_LITERAL("offscreen.html"),
                        "<html>Hello, offscreen world!</html>");
    test_dir_.WriteFile(FILE_PATH_LITERAL("devtools.html"),
                        R"(<html>
                             Hello, developer tools!
                             <script src="devtools.js"></script>
                           </html>)");
    test_dir_.WriteFile(FILE_PATH_LITERAL("devtools.js"),
                        "chrome.test.sendMessage('devtools page opened');");
    extension_ = LoadExtension(test_dir_.UnpackedPath());
    ASSERT_TRUE(extension_);
  }

  // Runs `chrome.runtime.getContexts()` and returns the result as a
  // base::Value.
  base::Value GetContexts(std::string_view filter) {
    static constexpr char kScriptTemplate[] =
        R"((async () => {
             chrome.test.sendScriptResult(
                 await chrome.runtime.getContexts(%s));
           })();)";
    std::string script = base::StringPrintf(kScriptTemplate, filter.data());
    return BackgroundScriptExecutor::ExecuteScript(
        profile(), extension_->id(), script,
        BackgroundScriptExecutor::ResultCapture::kSendScriptResult);
  }

  // Runs `chrome.runtime.getContexts()` and returns the result as a vector
  // of strongly-typed `ExtensionContext`s. Expects the getContexts() call to
  // return a valid value (i.e., not throw an error).
  std::vector<api::runtime::ExtensionContext> GetContextStructs(
      std::string_view filter) {
    base::Value value = GetContexts(filter);
    return ContextValueToContextStructs(value);
  }

  // Converts the given `value` into a vector of strongly-typed
  // `ExtensionContext`s. Expects the value to properly convert.
  std::vector<api::runtime::ExtensionContext> ContextValueToContextStructs(
      const base::Value& value) {
    if (!value.is_list()) {
      ADD_FAILURE() << "Invalid return value: " << value;
      return {};
    }

    std::vector<api::runtime::ExtensionContext> result;
    result.reserve(value.GetList().size());
    for (const auto& entry : value.GetList()) {
      if (!entry.is_dict()) {
        ADD_FAILURE() << "Invalid return value: " << value;
        return {};
      }
      auto context = api::runtime::ExtensionContext::FromValue(entry.GetDict());
      if (!context) {
        ADD_FAILURE() << "Invalid return value: " << value;
        return {};
      }

      result.push_back(std::move(*context));
    }

    return result;
  }

  // Returns a matcher that expects an ExtensionContext to be a valid
  // background context (without testing details of the entry).
  auto GetBackgroundMatcher() {
    return testing::AllOf(
        testing::Field(&api::runtime::ExtensionContext::context_type,
                       testing::Eq(api::runtime::ContextType::kBackground)),
        testing::Field(&api::runtime::ExtensionContext::tab_id,
                       testing::Eq(-1)),
        testing::Field(&api::runtime::ExtensionContext::window_id,
                       testing::Eq(-1)),
        testing::Field(&api::runtime::ExtensionContext::frame_id,
                       testing::Eq(-1)));
  }

  // Returns a matcher that expects an ExtensionContext to correspond to a
  // frame-based context type (without testing the details of the entry).
  auto GetFrameMatcher(api::runtime::ContextType context_type,
                       const GURL& url) {
    return testing::AllOf(
        testing::Field(&api::runtime::ExtensionContext::context_type,
                       testing::Eq(context_type)),
        testing::Field(&api::runtime::ExtensionContext::document_url,
                       testing::Eq(url.spec())));
  }

  const Extension& extension() const { return *extension_; }

 protected:
  raw_ptr<const Extension> extension_ = nullptr;
  TestExtensionDir test_dir_;
};

// Tests retrieving the background service worker context using
// `chrome.runtime.getContexts()`.

// TODO(crbug.com/40901108): failed on "chromium/ci/Mac12 Tests"
#if BUILDFLAG(IS_MAC)
#define MAYBE_GetServiceWorkerContext DISABLED_GetServiceWorkerContext
#else
#define MAYBE_GetServiceWorkerContext GetServiceWorkerContext
#endif
IN_PROC_BROWSER_TEST_F(RuntimeGetContextsApiTest,
                       MAYBE_GetServiceWorkerContext) {
  // An empty dictionary filter should match all contexts (of which there is
  // only one).
  base::Value contexts = GetContexts("{}");

  ProcessManager* process_manager = ProcessManager::Get(profile());
  std::vector<WorkerId> workers =
      process_manager->GetServiceWorkersForExtension(extension().id());
  ASSERT_EQ(1u, workers.size());
  base::Uuid expected_context_id =
      ProcessManager::Get(profile())->GetContextIdForWorker(workers[0]);
  EXPECT_TRUE(expected_context_id.is_valid());

  // Note: fields of `documentId`, `documentUrl`, and `documentOrigin` are
  // undefined (service worker contexts don't have an associated document).
  // `tabId`, `frameId`, and `windowId` are -1 for consistency with other
  // APIs.
  static constexpr char kExpected[] =
      R"([{
            "contextType": "BACKGROUND",
            "contextId": "%s",
            "tabId": -1,
            "windowId": -1,
            "frameId": -1,
            "incognito": false
         }])";
  std::string expected_contents = base::StringPrintf(
      kExpected, expected_context_id.AsLowercaseString().c_str());
  EXPECT_THAT(contexts, base::test::IsJson(expected_contents));

  // Now, wait for the extension worker to terminate and verify that no
  // no contexts are retrieved.
  browsertest_util::StopServiceWorkerForExtensionGlobalScope(profile(),
                                                             extension().id());
  // In order to be able to call the API, we need to open a new tab to an
  // extension resource.
  auto* web_contents = GetActiveWebContents();
  const GURL extension_page_url = extension().GetResourceURL("page.html");
  ASSERT_TRUE(NavigateToURL(web_contents, extension_page_url));
  content::RenderFrameHost* new_host = web_contents->GetPrimaryMainFrame();
  ASSERT_TRUE(new_host);

  static constexpr char kScript[] =
      R"((async () => {
           let contexts =
               await chrome.runtime.getContexts({contextTypes: ['BACKGROUND']});
           return JSON.stringify(contexts);
         })();)";
  // No contexts should have been returned.
  EXPECT_EQ("[]", content::EvalJs(new_host, kScript));
}

// Tests the filter matching behavior of `runtime.getContexts()`.
IN_PROC_BROWSER_TEST_F(RuntimeGetContextsApiTest, FilterMatching) {
  // Currently, there is only one context: the background service worker. Also
  // open a tab-based context.
  auto* web_contents = GetActiveWebContents();
  const GURL extension_page_url = extension().GetResourceURL("page.html");
  ASSERT_TRUE(NavigateToURL(web_contents, extension_page_url));
  content::RenderFrameHost* new_host = web_contents->GetPrimaryMainFrame();
  ASSERT_TRUE(new_host);
  int tab_id = ExtensionTabUtil::GetTabId(web_contents);

  {
    // Pass a filter matching everything. Both the tab context and worker
    // context should be returned.
    std::vector<api::runtime::ExtensionContext> contexts =
        GetContextStructs(R"({})");
    EXPECT_THAT(contexts, testing::UnorderedElementsAre(
                              GetBackgroundMatcher(),
                              GetFrameMatcher(api::runtime::ContextType::kTab,
                                              extension_page_url)));
  }
  {
    // Passing a filter to match background contexts should match the worker.
    std::vector<api::runtime::ExtensionContext> contexts =
        GetContextStructs(R"({"contextTypes": ["BACKGROUND"]})");
    EXPECT_THAT(contexts, testing::ElementsAre(GetBackgroundMatcher()));
  }
  {
    // Passing a filter to match a tab ID should match the corresponding
    // page context.
    std::string filter = base::StringPrintf(R"({"tabIds": [%d]})", tab_id);
    std::vector<api::runtime::ExtensionContext> contexts =
        GetContextStructs(filter);
    EXPECT_THAT(contexts,
                testing::ElementsAre(GetFrameMatcher(
                    api::runtime::ContextType::kTab, extension_page_url)));
  }
  {
    // Try passing a filter for a context type with no corresponding matches. No
    // contexts should be returned.
    std::vector<api::runtime::ExtensionContext> contexts =
        GetContextStructs(R"({"contextTypes": ["POPUP"]})");
    EXPECT_THAT(contexts, testing::IsEmpty());
  }
  {
    // Filter properties support an array of options; if the context matches an
    // entry in the array, it matches the filter for that property. Thus,
    // passing both "BACKGROUND" and "POPUP" should match the service worker
    // context.
    std::vector<api::runtime::ExtensionContext> contexts =
        GetContextStructs(R"({"contextTypes": ["BACKGROUND", "POPUP"]})");
    EXPECT_THAT(contexts, testing::ElementsAre(GetBackgroundMatcher()));
  }
  {
    // All specified filter properties must match. Thus, if we look for a
    // background context and also specify a tab ID, nothing should match
    // (since the background context doesn't have an associated tab).
    static constexpr char kFilter[] =
        R"({
             "contextTypes": ["BACKGROUND"],
             "tabIds": [2]
           })";
    std::vector<api::runtime::ExtensionContext> contexts =
        GetContextStructs(kFilter);
    EXPECT_THAT(contexts, testing::IsEmpty());
  }
}

// Tests retrieving tab contexts using `chrome.runtime.getContexts()`.
IN_PROC_BROWSER_TEST_F(RuntimeGetContextsApiTest, GetTabContext) {
  // Open a new extension tab.
  auto* web_contents = GetActiveWebContents();
  const GURL frame_url = extension().GetResourceURL("page.html");
  ASSERT_TRUE(NavigateToURL(web_contents, frame_url));
  content::RenderFrameHost* new_host = web_contents->GetPrimaryMainFrame();
  ASSERT_TRUE(new_host);

  int expected_tab_id = ExtensionTabUtil::GetTabId(web_contents);
  int expected_window_id = ExtensionTabUtil::GetWindowIdOfTab(web_contents);
  int expected_frame_id = ExtensionApiFrameIdMap::GetFrameId(new_host);
  std::string expected_context_id =
      ExtensionApiFrameIdMap::GetContextId(new_host).AsLowercaseString();
  std::string expected_document_id =
      ExtensionApiFrameIdMap::GetDocumentId(new_host).ToString();
  std::string expected_frame_url = frame_url.spec();
  std::string expected_origin = extension().origin().Serialize();

  // Query for tab-based contexts. There should only be one.
  base::Value background_contexts = GetContexts(R"({"contextTypes": ["TAB"]})");

  // Verify the properties of the returned context.
  static constexpr char kExpectedTemplate[] =
      R"([{
            "contextType": "TAB",
            "contextId": "%s",
            "tabId": %d,
            "windowId": %d,
            "frameId": %d,
            "documentId": "%s",
            "documentUrl": "%s",
            "documentOrigin": "%s",
            "incognito": false
         }])";
  std::string expected = base::StringPrintf(
      kExpectedTemplate, expected_context_id.c_str(), expected_tab_id,
      expected_window_id, expected_frame_id, expected_document_id.c_str(),
      expected_frame_url.c_str(), expected_origin.c_str());
  EXPECT_THAT(background_contexts, base::test::IsJson(expected));
}

// Tests retrieving offscreen documents with `runtime.getContexts()`.
IN_PROC_BROWSER_TEST_F(RuntimeGetContextsApiTest, GetOffscreenDocumentContext) {
  // Open a new offscreen document.
  static constexpr char kOpenOffscreenDocumentScript[] =
      R"((async () => {
           await chrome.offscreen.createDocument(
               {
                   url: 'offscreen.html',
                   reasons: ['DOM_PARSER'],
                   justification: 'testing'
               });
           chrome.test.sendScriptResult('done');
         })();)";
  base::Value script_result = BackgroundScriptExecutor::ExecuteScript(
      profile(), extension().id(), kOpenOffscreenDocumentScript,
      BackgroundScriptExecutor::ResultCapture::kSendScriptResult);
  EXPECT_EQ("done", script_result);

  OffscreenDocumentManager* offscreen_manager =
      OffscreenDocumentManager::Get(profile());
  const OffscreenDocumentHost* offscreen_document =
      offscreen_manager->GetOffscreenDocumentForExtension(extension());
  ASSERT_TRUE(offscreen_document);

  content::RenderFrameHost* offscreen_frame_host =
      offscreen_document->web_contents()->GetPrimaryMainFrame();
  int expected_frame_id =
      ExtensionApiFrameIdMap::GetFrameId(offscreen_frame_host);
  std::string expected_context_id =
      ExtensionApiFrameIdMap::GetContextId(offscreen_frame_host)
          .AsLowercaseString();
  std::string expected_document_id =
      ExtensionApiFrameIdMap::GetDocumentId(offscreen_frame_host).ToString();
  std::string expected_frame_url =
      extension().GetResourceURL("offscreen.html").spec();
  std::string expected_origin = extension().origin().Serialize();

  // Query for offscreen document contexts. There should only be one.
  base::Value background_contexts =
      GetContexts(R"({"contextTypes": ["OFFSCREEN_DOCUMENT"]})");

  // Verify the properties of the returned context.
  static constexpr char kExpectedTemplate[] =
      R"([{
            "contextType": "OFFSCREEN_DOCUMENT",
            "contextId": "%s",
            "tabId": -1,
            "windowId": -1,
            "frameId": %d,
            "documentId": "%s",
            "documentUrl": "%s",
            "documentOrigin": "%s",
            "incognito": false
         }])";
  std::string expected =
      base::StringPrintf(kExpectedTemplate, expected_context_id.c_str(),
                         expected_frame_id, expected_document_id.c_str(),
                         expected_frame_url.c_str(), expected_origin.c_str());
  EXPECT_THAT(background_contexts, base::test::IsJson(expected));
}

#if !BUILDFLAG(IS_ANDROID)
// TODO(crbug.com/405218955): Support side panel on desktop Android.
class RuntimeGetContextsSidePanelTest : public RuntimeGetContextsApiTest {
 public:
  void SetUpExtension() override {
    static constexpr char kManifest[] =
        R"({
             "name": "Get Contexts",
             "version": "0.1",
             "manifest_version": 3,
             "permissions": ["sidePanel"],
             "side_panel": {
               "default_path": "side_panel.html"
             },
             "action": {},
             "background": {
               "service_worker": "background.js"
             }
           })";
    test_dir_.WriteManifest(kManifest);
    test_dir_.WriteFile(FILE_PATH_LITERAL("background.js"),
                        "// Intentionally blank");
    test_dir_.WriteFile(FILE_PATH_LITERAL("side_panel.html"),
                        R"(<html>
                             Hello, side panel!
                             <script src="side_panel.js"></script>
                           </html>)");
    test_dir_.WriteFile(FILE_PATH_LITERAL("side_panel.js"),
                        "chrome.test.sendMessage('panel opened');");
    extension_ = LoadExtension(test_dir_.UnpackedPath());
    ASSERT_TRUE(extension_);
  }
};

// Tests retrieving a side panel context from the `runtime.getContexts()` API.
IN_PROC_BROWSER_TEST_F(RuntimeGetContextsSidePanelTest, GetSidePanelContext) {
  // Set the side panel to open on toolbar action click. This makes it easier
  // to trigger.
  static constexpr char kSetUpSidePanelScript[] =
      R"((async () => {
           await chrome.sidePanel.setPanelBehavior(
               {openPanelOnActionClick: true});
           chrome.test.sendScriptResult('done');
         })();)";

  base::Value script_result = BackgroundScriptExecutor::ExecuteScript(
      profile(), extension().id(), kSetUpSidePanelScript,
      BackgroundScriptExecutor::ResultCapture::kSendScriptResult);
  EXPECT_EQ("done", script_result);

  // Click on the toolbar action and wait for the panel context to open.
  ExtensionTestMessageListener panel_listener("panel opened");
  ExtensionActionTestHelper::Create(browser())->Press(extension().id());
  ASSERT_TRUE(panel_listener.WaitUntilSatisfied());

  // Fetch the side panel host.
  ExtensionHostRegistry* host_registry = ExtensionHostRegistry::Get(profile());
  std::vector<ExtensionHost*> hosts =
      host_registry->GetHostsForExtension(extension().id());
  ASSERT_EQ(1u, hosts.size());
  ExtensionHost* panel_host = hosts[0];
  EXPECT_EQ(mojom::ViewType::kExtensionSidePanel,
            panel_host->extension_host_type());
  content::RenderFrameHost* panel_frame_host =
      panel_host->web_contents()->GetPrimaryMainFrame();

  // Verify the `runtime.getContexts()` API can retrieve the context and that
  // the proper values are returned.
  int expected_frame_id = ExtensionApiFrameIdMap::GetFrameId(panel_frame_host);
  std::string expected_context_id =
      ExtensionApiFrameIdMap::GetContextId(panel_frame_host)
          .AsLowercaseString();
  std::string expected_document_id =
      ExtensionApiFrameIdMap::GetDocumentId(panel_frame_host).ToString();
  std::string expected_frame_url =
      extension().GetResourceURL("side_panel.html").spec();
  std::string expected_origin = extension().origin().Serialize();

  base::Value side_panel_contexts =
      GetContexts(R"({"contextTypes": ["SIDE_PANEL"]})");

  // Verify the properties of the returned context.
  static constexpr char kExpectedTemplate[] =
      R"([{
            "contextType": "SIDE_PANEL",
            "contextId": "%s",
            "tabId": -1,
            "windowId": -1,
            "frameId": %d,
            "documentId": "%s",
            "documentUrl": "%s",
            "documentOrigin": "%s",
            "incognito": false
         }])";
  std::string expected =
      base::StringPrintf(kExpectedTemplate, expected_context_id.c_str(),
                         expected_frame_id, expected_document_id.c_str(),
                         expected_frame_url.c_str(), expected_origin.c_str());
  EXPECT_THAT(side_panel_contexts, base::test::IsJson(expected));
}

// Tests that the side panel extension context is fully loaded and registered
// before the chrome.sidePanel.onOpened event is dispatched. This ensures
// chrome.runtime.getContexts() correctly returns the side panel context when
// called immediately inside the onOpened listener.
IN_PROC_BROWSER_TEST_F(RuntimeGetContextsSidePanelTest,
                       GetContextsFromOnOpenedEvent) {
  TestExtensionDir test_dir;

  test_dir.WriteManifest(R"({
    "name": "Side Panel Context Test",
    "version": "1",
    "manifest_version": 3,
    "permissions": ["sidePanel"],
    "background": {"service_worker": "background.js"},
    "action": {},
    "side_panel": {"default_path": "sidepanel.html"}
  })");

  test_dir.WriteFile(FILE_PATH_LITERAL("sidepanel.html"), R"(
    <!DOCTYPE html>
    <html>
    <body>Side Panel</body>
    </html>
  )");

  test_dir.WriteFile(FILE_PATH_LITERAL("background.js"), R"(
    // Explicitly open the side panel when the extension action is clicked
    chrome.action.onClicked.addListener(async (tab) => {
      await chrome.sidePanel.open({ windowId: tab.windowId });
    });

    chrome.sidePanel.onOpened.addListener(async () => {
      const contexts = await chrome.runtime.getContexts({
        contextTypes: [chrome.runtime.ContextType.SIDE_PANEL]
      });

      // Using succeed/fail prevents the C++ test from hanging if the count is
      // wrong.
      if (contexts.length === 1) {
        chrome.test.succeed();
      } else {
        chrome.test.fail(`Expected 1 context, but got ${contexts.length}`);
      }
    });
  )");

  // ResultCatcher listens for chrome.test.succeed() or chrome.test.fail().
  extensions::ResultCatcher catcher;

  const Extension* extension = LoadExtension(test_dir.UnpackedPath());
  ASSERT_TRUE(extension);

  auto helper = ExtensionActionTestHelper::Create(browser());
  ASSERT_TRUE(helper);

  // Trigger the action.onClicked listener.
  helper->Press(extension->id());

  // Wait for the result. If it fails (e.g. the bug is present and returns 0),
  // GetNextResult() returns false and prints the JS error message.
  EXPECT_TRUE(catcher.GetNextResult()) << catcher.message();
}

#endif  // !BUILDFLAG(IS_ANDROID)

// Tests the behavior of `runtime.getContexts()` with a split-mode incognito
// extension. In split mode, the extension should only be able to access data
// about its own process's contexts.
IN_PROC_BROWSER_TEST_F(RuntimeGetContextsApiTest,
                       RetrievingIncognitoContexts_SplitMode) {
  // Load up a split-mode extension.
  static constexpr char kManifest[] =
      R"({
           "name": "Split mode extension",
           "version": "0.1",
           "manifest_version": 3,
           "incognito": "split",
           "background": {"service_worker": "background.js"}
         })";

  // Since we need to wait for the incognito profile's separate service worker
  // to start, our bootstrapping code in LoadExtension() doesn't automatically
  // handle it for us. Include a separate "ready" message.
  TestExtensionDir test_dir;
  test_dir.WriteManifest(kManifest);
  test_dir.WriteFile(FILE_PATH_LITERAL("background.js"),
                     "chrome.test.sendMessage('ready');");
  test_dir.WriteFile(FILE_PATH_LITERAL("regular.html"), "<html>Regular</html>");
  test_dir.WriteFile(FILE_PATH_LITERAL("incognito.html"),
                     "<html>Incognito</html>");

  ExtensionTestMessageListener ready_listener("ready");
  const Extension* extension =
      LoadExtension(test_dir.UnpackedPath(), {.allow_in_incognito = true});
  ASSERT_TRUE(extension);
  ASSERT_TRUE(ready_listener.WaitUntilSatisfied());

  // Open a tab on-the-record to one of the extension's pages.
  auto* web_contents = GetActiveWebContents();
  GURL regular_url = extension->GetResourceURL("regular.html");
  ASSERT_TRUE(NavigateToURL(web_contents, regular_url));
  ASSERT_TRUE(web_contents->GetPrimaryMainFrame());

  // Open up an incognito tab to another extension page, and wait for the
  // incognito version of the extension to start up.
  ready_listener.Reset();
  GURL incognito_url = extension->GetResourceURL("incognito.html");
  content::WebContents* incognito_web_contents =
      PlatformOpenURLOffTheRecord(profile(), incognito_url);
  ASSERT_TRUE(incognito_web_contents->GetPrimaryMainFrame());
  ASSERT_TRUE(ready_listener.WaitUntilSatisfied());

  // A helper method to retrieve the contexts for the given `browser_context`.
  auto run_get_contexts =
      [extension](content::BrowserContext* browser_context) {
        static constexpr char kScript[] =
            R"((async () => {
             chrome.test.sendScriptResult(
                 await chrome.runtime.getContexts({}));
           })();)";
        return BackgroundScriptExecutor::ExecuteScript(
            browser_context, extension->id(), kScript,
            BackgroundScriptExecutor::ResultCapture::kSendScriptResult);
      };

  {
    // Verify the on-the-record contexts. There should be a single background
    // context and the on-the-record tab.
    base::Value regular_results = run_get_contexts(profile());
    std::vector<api::runtime::ExtensionContext> contexts =
        ContextValueToContextStructs(regular_results);
    EXPECT_THAT(contexts, testing::UnorderedElementsAre(
                              GetBackgroundMatcher(),
                              GetFrameMatcher(api::runtime::ContextType::kTab,
                                              regular_url)));
  }
  {
    // Now verify the incognito contexts. Here, too, there should be a single
    // background context and tab, but it should be the incognito tab.
    base::Value incognito_results =
        run_get_contexts(incognito_web_contents->GetBrowserContext());
    std::vector<api::runtime::ExtensionContext> contexts =
        ContextValueToContextStructs(incognito_results);
    EXPECT_THAT(contexts, testing::UnorderedElementsAre(
                              GetBackgroundMatcher(),
                              GetFrameMatcher(api::runtime::ContextType::kTab,
                                              incognito_url)));
  }
}

// Tests the behavior of `runtime.getContexts()` with a spanning-mode incognito
// extension.
IN_PROC_BROWSER_TEST_F(RuntimeGetContextsApiTest,
                       RetrievingIncognitoContexts_SpanningMode) {
  ASSERT_TRUE(StartEmbeddedTestServer());

  // Load up a spanning mode extension. See comment below for why we have
  // a web accessible resource.
  static constexpr char kManifest[] =
      R"({
           "name": "Split mode extension",
           "version": "0.1",
           "manifest_version": 3,
           "incognito": "spanning",
           "web_accessible_resources": [{
             "resources": ["incognito.html"],
             "matches": ["*://example.com/*"]
           }],
           "background": {"service_worker": "background.js"}
         })";

  TestExtensionDir test_dir;
  test_dir.WriteManifest(kManifest);
  test_dir.WriteFile(FILE_PATH_LITERAL("background.js"),
                     "// Intentionally blank");
  test_dir.WriteFile(FILE_PATH_LITERAL("regular.html"), "<html>Regular</html>");
  test_dir.WriteFile(FILE_PATH_LITERAL("incognito.html"),
                     "<html>Incognito</html>");

  const Extension* extension =
      LoadExtension(test_dir.UnpackedPath(), {.allow_in_incognito = true});
  ASSERT_TRUE(extension);

  // Open an on-the-record tab to an extension page.
  auto* web_contents = GetActiveWebContents();
  GURL regular_url = extension->GetResourceURL("regular.html");
  ASSERT_TRUE(NavigateToURL(web_contents, regular_url));

  // Now, the tricky part. Spanning mode extensions aren't, typically, allowed
  // to open contexts in an incognito profile (which means all contexts just
  // open in the same profile). There's one exception to this: an embedded web-
  // accessible iframe in an incognito tab. Make it so.
  GURL incognito_url = extension->GetResourceURL("incognito.html");
  auto* incognito_web_contents = PlatformOpenURLOffTheRecord(
      profile(), embedded_test_server()->GetURL("example.com", "/simple.html"));
  // Inject a script to add an iframe and navigate it to the extension's
  // web-accessible resource.
  content::RenderFrameHost* incognito_main_frame =
      incognito_web_contents->GetPrimaryMainFrame();
  static constexpr char kNavigateTemplate[] =
      R"(let frame = document.createElement('iframe');
         frame.src = '%s';
         new Promise(resolve => {
           frame.onload = () => { resolve('success'); };
           frame.onerror = (e) => {
             resolve('failure: ' + e.toString());
           };
           document.body.appendChild(frame);
         });
         )";

  EXPECT_EQ("success",
            content::EvalJs(incognito_main_frame,
                            base::StringPrintf(kNavigateTemplate,
                                               incognito_url.spec().c_str())));

  // Verify the frame loaded properly by checking both the URL and the content.
  content::RenderFrameHost* incognito_extension_frame =
      content::ChildFrameAt(incognito_main_frame, 0);
  ASSERT_TRUE(incognito_extension_frame);
  EXPECT_EQ(incognito_url, incognito_extension_frame->GetLastCommittedURL());
  EXPECT_EQ("Incognito", content::EvalJs(incognito_extension_frame,
                                         "document.body.textContent;"));

  // A helper method to retrieve the contexts for the given `profile`.
  auto run_get_contexts_in_profile = [extension](Profile* profile) {
    static constexpr char kScript[] =
        R"((async () => {
             chrome.test.sendScriptResult(
                 await chrome.runtime.getContexts({}));
           })();)";
    return BackgroundScriptExecutor::ExecuteScript(
        profile, extension->id(), kScript,
        BackgroundScriptExecutor::ResultCapture::kSendScriptResult);
  };

  {
    // Verify the results for the on-the-record profile. Since the extension
    // is in spanning mode, this is effectively the only instance of the
    // extension. It should see the background context and the on-the-record
    // tab, but not the embedded frame.
    base::Value regular_results = run_get_contexts_in_profile(profile());
    std::vector<api::runtime::ExtensionContext> contexts =
        ContextValueToContextStructs(regular_results);
    EXPECT_THAT(contexts, testing::UnorderedElementsAre(
                              GetBackgroundMatcher(),
                              GetFrameMatcher(api::runtime::ContextType::kTab,
                                              regular_url)));
  }
}

#if !BUILDFLAG(IS_ANDROID)
// This is a manifest V2 test meant to ensure test coverage for
// chrome.extension.getURL, which is deprecated and unavailable
// in MV3.
IN_PROC_BROWSER_TEST_F(ExtensionApiTest, GetExtensionURL) {
  static constexpr char kManifest[] = R"(
      {
        "name": "chrome.extension.getURL",
        "version": "1.0",
        "background": {
          "scripts": ["background.js"]
        },
        "manifest_version": 2
      })";

  static constexpr char kScript[] = R"(
    chrome.test.assertEq(
        chrome.extension.getURL('foo.html'),
        chrome.runtime.getURL('foo.html'));
    chrome.test.notifyPass();
  )";

  ResultCatcher catcher;
  TestExtensionDir dir;
  dir.WriteManifest(kManifest);
  dir.WriteFile(FILE_PATH_LITERAL("background.js"), kScript);

  const Extension* extension = LoadExtension(dir.UnpackedPath());
  ASSERT_TRUE(extension);
  EXPECT_TRUE(catcher.GetNextResult());
}

// Tests retrieving contexts when developer tools are opened.
// TODO(crbug.com/402538127): Improve devtools support on desktop Android.
class GetContextsWithDeveloperToolsOpened
    : public RuntimeGetContextsApiTest,
      public testing::WithParamInterface<bool> {
 public:
  GetContextsWithDeveloperToolsOpened() = default;
  ~GetContextsWithDeveloperToolsOpened() override = default;

  GetContextsWithDeveloperToolsOpened(
      const GetContextsWithDeveloperToolsOpened&) = delete;
  GetContextsWithDeveloperToolsOpened& operator=(
      const GetContextsWithDeveloperToolsOpened&) = delete;
};

// TODO(crbug.com/357845909): flaky on ChromeOS and Linux MSAN.
#if defined(MEMORY_SANITIZER) && (BUILDFLAG(IS_LINUX) || BUILDFLAG(IS_CHROMEOS))
#define MAYBE_ReturnsDevToolsContext DISABLED_ReturnsDevToolsContext
#else
#define MAYBE_ReturnsDevToolsContext ReturnsDevToolsContext
#endif
IN_PROC_BROWSER_TEST_P(GetContextsWithDeveloperToolsOpened,
                       MAYBE_ReturnsDevToolsContext) {
  const bool open_docked = GetParam();

  // Open the developer tools and wait for the extension page to be loaded.
  ExtensionTestMessageListener listener("devtools page opened");
  content::WebContents* inspected_web_contents = GetActiveWebContents();
  DevToolsWindow* devtools_window =
      DevToolsWindowTesting::OpenDevToolsWindowSync(inspected_web_contents,
                                                    open_docked);
  ASSERT_TRUE(listener.WaitUntilSatisfied());

  // Assert the docked state of developer tools.
  content::WebContents* devtools_web_contents =
      DevToolsWindowTesting::Get(devtools_window)->main_web_contents();
  bool is_docked = devtools_web_contents->GetTopLevelNativeWindow() ==
                   browser()->GetWindow()->GetNativeWindow();
  ASSERT_EQ(open_docked, is_docked);

  // Extract the extension host from the devtools web contents.
  GURL expected_frame_url = extension().GetResourceURL("devtools.html");
  auto is_extension_frame =
      [expected_frame_url](content::RenderFrameHost* rfh) {
        return rfh->GetLastCommittedURL() == expected_frame_url;
      };
  content::RenderFrameHost* extension_host = content::FrameMatchingPredicate(
      devtools_web_contents->GetPrimaryPage(),
      base::BindLambdaForTesting(is_extension_frame));

  // Setup the expected values for the context. Only one tab-based context
  // should be returned by chrome.runtime.getContexts().
  int expected_tab_id = -1;
  int expected_window_id = ExtensionTabUtil::GetWindowIdOfTab(
      is_docked ? inspected_web_contents : devtools_web_contents);
  int expected_frame_id = -1;
  std::string expected_context_id =
      ExtensionApiFrameIdMap::GetContextId(extension_host).AsLowercaseString();
  std::string expected_document_id =
      ExtensionApiFrameIdMap::GetDocumentId(extension_host).ToString();
  std::string expected_origin = extension().origin().Serialize();
  static constexpr char kExpectedTemplate[] =
      R"([{
            "contextType": "DEVELOPER_TOOLS",
            "contextId": "%s",
            "tabId": %d,
            "windowId": %d,
            "frameId": %d,
            "documentId": "%s",
            "documentUrl": "%s",
            "documentOrigin": "%s",
            "incognito": false
         }])";
  std::string expected_contexts = base::StringPrintf(
      kExpectedTemplate, expected_context_id.c_str(), expected_tab_id,
      expected_window_id, expected_frame_id, expected_document_id.c_str(),
      expected_frame_url.spec().c_str(), expected_origin.c_str());

  // Verify the result of chrome.runtime.getContexts().
  base::Value contexts =
      GetContexts(R"({"contextTypes": ["DEVELOPER_TOOLS"]})");
  EXPECT_THAT(contexts, base::test::IsJson(expected_contexts));
}

// Test for undocked developer tools.
INSTANTIATE_TEST_SUITE_P(UndockedDevTools,
                         GetContextsWithDeveloperToolsOpened,
                         ::testing::Values(false) /* open_docked */);

// Test for docked developer tools. This is also a regression test for
// crbug.com/355625882.
INSTANTIATE_TEST_SUITE_P(DockedDevTools,
                         GetContextsWithDeveloperToolsOpened,
                         ::testing::Values(true) /* open_docked */);
#endif  // !BUILDFLAG(IS_ANDROID)

}  // namespace extensions
