// Copyright 2016 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 "base/command_line.h"
#include "base/run_loop.h"
#include "base/strings/stringprintf.h"
#include "base/test/metrics/histogram_tester.h"
#include "base/test/with_feature_override.h"
#include "chrome/browser/devtools/devtools_window.h"
#include "chrome/browser/extensions/extension_action_dispatcher.h"
#include "chrome/browser/extensions/extension_apitest.h"
#include "chrome/browser/extensions/extension_util.h"
#include "chrome/browser/renderer_context_menu/render_view_context_menu_test_util.h"
#include "chrome/browser/ui/browser_window/public/browser_window_interface.h"
#include "chrome/browser/ui/tabs/tab_strip_model.h"
#include "chrome/test/base/ui_test_utils.h"
#include "components/sessions/content/session_tab_helper.h"
#include "content/public/test/browser_test.h"
#include "content/public/test/browser_test_utils.h"
#include "extensions/browser/api/file_system/file_system_api.h"
#include "extensions/browser/background_script_executor.h"
#include "extensions/browser/event_router.h"
#include "extensions/browser/extension_action.h"
#include "extensions/browser/extension_action_manager.h"
#include "extensions/browser/extension_function_histogram_value.h"
#include "extensions/browser/extension_host_test_helper.h"
#include "extensions/browser/extension_util.h"
#include "extensions/browser/process_manager.h"
#include "extensions/browser/script_result_queue.h"
#include "extensions/common/extension.h"
#include "extensions/common/extension_features.h"
#include "extensions/common/mojom/view_type.mojom.h"
#include "extensions/common/switches.h"
#include "extensions/test/extension_test_message_listener.h"
#include "extensions/test/result_catcher.h"
#include "extensions/test/test_extension_dir.h"
#include "net/dns/mock_host_resolver.h"
#include "ui/base/window_open_disposition.h"

namespace extensions {

namespace {

// A script that can verify whether a developer-mode-restricted API is
// available. Note that we use separate verify methods here (as opposed to
// a boolean "is API available") so we can better verify expected errors and
// give more meaningful messages in the case of failure.
constexpr char kCheckApiAvailability[] =
    R"(
       async function verifyApiIsAvailable() {
         let message;
         try {
           const tabs = await chrome.tabs.query({});
           chrome.test.assertEq(1, tabs.length);
           const debuggee = {tabId: tabs[0].id};
           await chrome.debugger.attach(debuggee, '1.3');
           await chrome.debugger.detach(debuggee);
           message = 'success';
         } catch (e) {
           message = 'Unexpected error: ' + e.toString();
         }
         chrome.test.sendScriptResult(message);
       }

       async function verifyApiIsNotAvailable() {
         let message;
         try {
           // Note: we try to call a method on the API (and not just test
           // accessing it) since, if it was previously instantiated when the
           // API was available, it would still be present.
           await chrome.debugger.getTargets();
           message = 'API unexpectedly available.';
         } catch(e) {
           const expectedError =
               `Error: Failed to read the 'debugger' property from ` +
               `'Object': The 'debugger' API is only available for users ` +
               'in developer mode.';
           message = e.toString() == expectedError
               ? 'success'
               : 'Unexpected error: ' + e.toString();
         }
         chrome.test.sendScriptResult(message);
       })";



bool ApiExists(content::WebContents* web_contents,
               const std::string& api_name) {
  return content::EvalJs(web_contents,
                         base::StringPrintf("!!%s;", api_name.c_str()))
      .ExtractBool();
}

bool ObjectIsDefined(content::WebContents* web_contents,
                     const std::string& object_name) {
  return content::EvalJs(web_contents,
                         base::StringPrintf("self.hasOwnProperty('%s');",
                                            object_name.c_str()))
      .ExtractBool();
}

}  // namespace

// And end-to-end test for extension APIs using native bindings.
class NativeBindingsApiTest : public ExtensionApiTest {
 public:
  NativeBindingsApiTest() = default;

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

  ~NativeBindingsApiTest() override = default;

  void SetUpCommandLine(base::CommandLine* command_line) override {
    ExtensionApiTest::SetUpCommandLine(command_line);
    // We allowlist the extension so that it can use the cast.streaming.* APIs,
    // which are the only APIs that are prefixed twice.
    command_line->AppendSwitchASCII(switches::kAllowlistedExtensionID,
                                    "ddchlicdkolnonkihahngkmmmjnjlkkf");
  }

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

IN_PROC_BROWSER_TEST_F(NativeBindingsApiTest, SimpleEndToEndTest) {
  embedded_test_server()->ServeFilesFromDirectory(test_data_dir_);
  ASSERT_TRUE(StartEmbeddedTestServer());
  ASSERT_TRUE(RunExtensionTest("native_bindings/extension")) << message_;
}

// The following test is executed as Chrome App, which is only supported on
// ChromeOS.
#if BUILDFLAG(IS_CHROMEOS)
// A simplistic app test for app-specific APIs.
IN_PROC_BROWSER_TEST_F(NativeBindingsApiTest, SimpleAppTest) {
  ExtensionTestMessageListener ready_listener("ready",
                                              ReplyBehavior::kWillReply);
  ASSERT_TRUE(RunExtensionTest("native_bindings/platform_app",
                               {.launch_as_platform_app = true}))
      << message_;
  ASSERT_TRUE(ready_listener.WaitUntilSatisfied());

  // On reply, the extension will try to close the app window and send a
  // message.
  ExtensionTestMessageListener close_listener;
  ready_listener.Reply(std::string());
  ASSERT_TRUE(close_listener.WaitUntilSatisfied());
  EXPECT_EQ("success", close_listener.message());
}
#endif  // BUILDFLAG(IS_CHROMEOS)

// Tests the declarativeContent API and declarative events.
IN_PROC_BROWSER_TEST_F(NativeBindingsApiTest, DeclarativeEvents) {
  embedded_test_server()->ServeFilesFromDirectory(test_data_dir_);
  ASSERT_TRUE(StartEmbeddedTestServer());
  // Load an extension. On load, this extension will a) run a few simple tests
  // using chrome.test.runTests() and b) set up rules for declarative events for
  // a browser-driven test. Wait for both the tests to finish and the extension
  // to be ready.
  ExtensionTestMessageListener listener("ready");
  ResultCatcher catcher;
  const Extension* extension = LoadExtension(
      test_data_dir_.AppendASCII("native_bindings/declarative_content"));
  ASSERT_TRUE(catcher.GetNextResult()) << catcher.message();
  ASSERT_TRUE(extension);
  ASSERT_TRUE(listener.WaitUntilSatisfied());

  // The extension's page action should currently be hidden.
  ExtensionAction* action =
      ExtensionActionManager::Get(profile())->GetExtensionAction(*extension);
  content::WebContents* web_contents = GetActiveWebContents();
  int tab_id = sessions::SessionTabHelper::IdForTab(web_contents).id();
  EXPECT_FALSE(action->GetIsVisible(tab_id));
  EXPECT_TRUE(action->GetDeclarativeIcon(tab_id).IsEmpty());

  // Navigating to example.com should show the page action.
  ASSERT_TRUE(NavigateToURL(
      web_contents, embedded_test_server()->GetURL(
                        "example.com", "/native_bindings/simple.html")));
  base::RunLoop().RunUntilIdle();
  EXPECT_TRUE(action->GetIsVisible(tab_id));
  EXPECT_FALSE(action->GetDeclarativeIcon(tab_id).IsEmpty());

  // And the extension should be notified of the click.
  ExtensionTestMessageListener clicked_listener("clicked and removed");
  ExtensionActionDispatcher::Get(profile())->DispatchExtensionActionClicked(
      *action, web_contents, extension);
  ASSERT_TRUE(clicked_listener.WaitUntilSatisfied());
}

IN_PROC_BROWSER_TEST_F(NativeBindingsApiTest, LazyListeners) {
  ProcessManager::SetEventPageIdleTimeForTesting(1);
  ProcessManager::SetEventPageSuspendingTimeForTesting(1);

  ExtensionHostTestHelper background_page_done(profile());
  background_page_done.RestrictToType(
      mojom::ViewType::kExtensionBackgroundPage);
  const Extension* extension = LoadExtension(
      test_data_dir_.AppendASCII("native_bindings/lazy_listeners"));
  ASSERT_TRUE(extension);
  // Wait for the event page to cycle.
  background_page_done.WaitForDocumentElementAvailable();
  background_page_done.WaitForHostDestroyed();

  EventRouter* event_router = EventRouter::Get(profile());
  EXPECT_TRUE(event_router->ExtensionHasEventListener(extension->id(),
                                                      "tabs.onCreated"));
}

// End-to-end test for the fileSystem API, which includes parameters with
// instance-of requirements and a post-validation argument updater that violates
// the schema.
IN_PROC_BROWSER_TEST_F(NativeBindingsApiTest, FileSystemApiGetDisplayPath) {
  base::FilePath test_dir = test_data_dir_.AppendASCII("native_bindings");
  FileSystemChooseEntryFunction::RegisterTempExternalFileSystemForTest(
      "test_root", test_dir);
  base::FilePath test_file = test_dir.AppendASCII("text.txt");
  const FileSystemChooseEntryFunction::TestOptions test_options{
      .path_to_be_picked = &test_file};
  auto reset_options =
      FileSystemChooseEntryFunction::SetOptionsForTesting(test_options);
  ASSERT_TRUE(RunExtensionTest("native_bindings/instance_of",
                               {.launch_as_platform_app = true}))
      << message_;
}

// Tests the webRequest API, which requires IO thread requests and custom
// events.
IN_PROC_BROWSER_TEST_F(NativeBindingsApiTest, WebRequest) {
  embedded_test_server()->ServeFilesFromDirectory(test_data_dir_);
  ASSERT_TRUE(StartEmbeddedTestServer());
  // Load an extension and wait for it to be ready.
  ResultCatcher catcher;
  const Extension* extension =
      LoadExtension(test_data_dir_.AppendASCII("native_bindings/web_request"));
  ASSERT_TRUE(extension);
  ASSERT_TRUE(catcher.GetNextResult()) << catcher.message();

  auto* web_contents = GetActiveWebContents();
  ASSERT_TRUE(NavigateToURL(
      web_contents, embedded_test_server()->GetURL(
                        "example.com", "/native_bindings/simple.html")));

  GURL expected_url = embedded_test_server()->GetURL(
      "example.com", "/native_bindings/simple2.html");
  EXPECT_EQ(expected_url, web_contents->GetLastCommittedURL());
}

// Tests the context menu API, which includes calling sendRequest with an
// different signature than specified and using functions as properties on an
// object.
IN_PROC_BROWSER_TEST_F(NativeBindingsApiTest, ContextMenusTest) {
  TestExtensionDir test_dir;
  test_dir.WriteManifest(
      R"({
           "name": "Context menus",
           "manifest_version": 2,
           "version": "0.1",
           "permissions": ["contextMenus"],
           "background": {
             "scripts": ["background.js"]
           }
         })");
  test_dir.WriteFile(
      FILE_PATH_LITERAL("background.js"),
      R"(chrome.contextMenus.create(
           {
             title: 'Context Menu Item',
             onclick: () => { chrome.test.sendMessage('clicked'); },
           }, () => { chrome.test.sendMessage('registered'); });)");

  const Extension* extension = nullptr;
  {
    ExtensionTestMessageListener listener("registered");
    extension = LoadExtension(test_dir.UnpackedPath());
    ASSERT_TRUE(extension);
    EXPECT_TRUE(listener.WaitUntilSatisfied());
  }

  content::WebContents* web_contents = GetActiveWebContents();
  std::unique_ptr<TestRenderViewContextMenu> menu(
      TestRenderViewContextMenu::Create(web_contents,
                                        GURL("https://www.example.com")));

  ExtensionTestMessageListener listener("clicked");
  int command_id = ContextMenuMatcher::ConvertToExtensionsCustomCommandId(0);
  EXPECT_TRUE(menu->IsCommandIdEnabled(command_id));
  menu->ExecuteCommand(command_id, 0);
  EXPECT_TRUE(listener.WaitUntilSatisfied());
}

// Tests that unchecked errors don't impede future calls.
IN_PROC_BROWSER_TEST_F(NativeBindingsApiTest, ErrorsInCallbackTest) {
  embedded_test_server()->ServeFilesFromDirectory(test_data_dir_);
  ASSERT_TRUE(StartEmbeddedTestServer());

  TestExtensionDir test_dir;
  test_dir.WriteManifest(
      R"({
           "name": "Errors In Callback",
           "manifest_version": 2,
           "version": "0.1",
           "permissions": ["contextMenus"],
           "background": {
             "scripts": ["background.js"]
           }
         })");
  test_dir.WriteFile(
      FILE_PATH_LITERAL("background.js"),
      R"(chrome.tabs.query({}, function(tabs) {
           chrome.tabs.executeScript(tabs[0].id, {code: 'x'}, function() {
             // There's an error here (we don't have permission to access the
             // host), but we don't check it so that it gets surfaced as an
             // unchecked runtime.lastError.
             // We should still be able to invoke other APIs and get correct
             // callbacks.
             chrome.tabs.query({}, function(tabs) {
               chrome.tabs.query({}, function(tabs) {
                 chrome.test.sendMessage('callback');
               });
             });
           });
         });)");

  ASSERT_TRUE(
      NavigateToURL(GetActiveWebContents(),
                    embedded_test_server()->GetURL(
                        "example.com", "/native_bindings/simple.html")));

  ExtensionTestMessageListener listener("callback");
  ASSERT_TRUE(LoadExtension(test_dir.UnpackedPath()));
  EXPECT_TRUE(listener.WaitUntilSatisfied());
}

// Tests that bindings are available in WebUI pages.
IN_PROC_BROWSER_TEST_F(NativeBindingsApiTest, WebUIBindings) {
  auto* web_contents = GetActiveWebContents();
  ASSERT_TRUE(NavigateToURL(web_contents, GURL("chrome://extensions")));

  EXPECT_TRUE(ApiExists(web_contents, "chrome.developerPrivate"));
  EXPECT_TRUE(ApiExists(web_contents,
                        "chrome.developerPrivate.getProfileConfiguration"));
  EXPECT_TRUE(ApiExists(web_contents, "chrome.management"));
  EXPECT_TRUE(ApiExists(web_contents, "chrome.management.setEnabled"));
  EXPECT_FALSE(ApiExists(web_contents, "chrome.networkingPrivate"));
  EXPECT_FALSE(ApiExists(web_contents, "chrome.sockets"));
  EXPECT_FALSE(ApiExists(web_contents, "chrome.browserAction"));
}

// Tests creating an API from a context that hasn't been initialized yet
// by doing so in a parent frame. Regression test for
// https://crbug.com/41375376.
IN_PROC_BROWSER_TEST_F(NativeBindingsApiTest, APICreationFromNewContext) {
  embedded_test_server()->ServeFilesFromDirectory(test_data_dir_);
  ASSERT_TRUE(StartEmbeddedTestServer());
  ASSERT_TRUE(RunExtensionTest("native_bindings/context_initialization"))
      << message_;
}

// End-to-end test for promise support on bindings for MV3 extensions, using a
// few tabs APIs. Also ensures callbacks still work for the API as expected.
IN_PROC_BROWSER_TEST_F(NativeBindingsApiTest, PromiseBasedAPI) {
  base::HistogramTester histogram_tester;
  ASSERT_TRUE(StartEmbeddedTestServer());

  TestExtensionDir test_dir;
  test_dir.WriteManifest(
      R"({
           "name": "Promises",
           "manifest_version": 3,
           "version": "0.1",
           "background": {
             "service_worker": "background.js"
           },
           "permissions": ["tabs", "storage", "contentSettings", "privacy"]
         })");
  constexpr char kBackgroundJs[] =
      R"(let tabIdExample;
         let tabIdGoogle;

         chrome.test.getConfig((config) => {
           let exampleUrl = `https://example.com:${config.testServer.port}/`;
           let googleUrl = `https://google.com:${config.testServer.port}/`

           chrome.test.runTests([
             function createNewTabPromise() {
               let promise = chrome.tabs.create({url: exampleUrl});
               chrome.test.assertNoLastError();
               chrome.test.assertTrue(promise instanceof Promise);
               promise.then((tab) => {
                 let url = tab.pendingUrl;
                 chrome.test.assertEq(exampleUrl, url);
                 tabIdExample = tab.id;
                 chrome.test.assertNoLastError();
                 chrome.test.succeed();
               });
             },
             function queryTabPromise() {
               let promise = chrome.tabs.query({url: exampleUrl});
               chrome.test.assertNoLastError();
               chrome.test.assertTrue(promise instanceof Promise);
               promise.then((tabs) => {
                 chrome.test.assertTrue(tabs instanceof Array);
                 chrome.test.assertEq(1, tabs.length);
                 chrome.test.assertEq(tabIdExample, tabs[0].id);
                 chrome.test.assertNoLastError();
                 chrome.test.succeed();
               });
             },
             async function storageAreaCustomTypeWithPromises() {
               await chrome.storage.local.set({foo: 'bar', alpha: 'beta'});
               {
                 const {foo} = await chrome.storage.local.get('foo');
                 chrome.test.assertEq('bar', foo);
               }
               await chrome.storage.local.remove('foo');
               {
                 const {foo} = await chrome.storage.local.get('foo');
                 chrome.test.assertEq(undefined, foo);
               }
               let allValues = await chrome.storage.local.get(null);
               chrome.test.assertEq({alpha: 'beta'}, allValues);
               await chrome.storage.local.clear();
               allValues = await chrome.storage.local.get(null);
               chrome.test.assertEq({}, allValues);
               chrome.test.succeed();
             },
             async function contentSettingsCustomTypesWithPromises() {
               await chrome.contentSettings.cookies.set({
                   primaryPattern: '<all_urls>', setting: 'block'});
               {
                 const {setting} = await chrome.contentSettings.cookies.get({
                     primaryUrl: exampleUrl});
                 chrome.test.assertEq('block', setting);
               }
               await chrome.contentSettings.cookies.clear({});
               {
                 const {setting} = await chrome.contentSettings.cookies.get({
                     primaryUrl: exampleUrl});
                 // 'allow' is the default value for the setting.
                 chrome.test.assertEq('allow', setting);
               }
               chrome.test.succeed();
             },
             async function chromeSettingCustomTypesWithPromises() {
               // Short alias for ease of calling.
               let doNotTrack = chrome.privacy.websites.doNotTrackEnabled;
               await doNotTrack.set({value: true});
               {
                 const {value} = await doNotTrack.get({});
                 chrome.test.assertEq(true, value);
               }
               await doNotTrack.clear({});
               {
                 const {value} = await doNotTrack.get({});
                 // false is the default value for the setting.
                 chrome.test.assertEq(false, value);
               }
               chrome.test.succeed();
             },


             function createNewTabCallback() {
               chrome.tabs.create({url: googleUrl}, (tab) => {
                 let url = tab.pendingUrl;
                 chrome.test.assertEq(googleUrl, url);
                 tabIdGoogle = tab.id;
                 chrome.test.assertNoLastError();
                 chrome.test.succeed();
               });
             },
             function queryTabCallback() {
               chrome.tabs.query({url: googleUrl}, (tabs) => {
                 chrome.test.assertTrue(tabs instanceof Array);
                 chrome.test.assertEq(1, tabs.length);
                 chrome.test.assertEq(tabIdGoogle, tabs[0].id);
                 chrome.test.assertNoLastError();
                 chrome.test.succeed();
               });
             },
             function storageAreaCustomTypeWithCallbacks() {
               // Lots of stuff would probably fail if the callback version of
               // storage failed, so this is mostly just a rough sanity check.
               chrome.storage.local.set({gamma: 'delta'}, () => {
                 chrome.storage.local.get('gamma', ({gamma}) => {
                   chrome.test.assertEq('delta', gamma);
                   chrome.storage.local.clear(() => {
                     chrome.storage.local.get(null, (allValues) => {
                       chrome.test.assertEq({}, allValues);
                       chrome.test.succeed();
                     });
                   });
                 });
               });
             },
           ]);
         });)";
  test_dir.WriteFile(FILE_PATH_LITERAL("background.js"), kBackgroundJs);
  ResultCatcher catcher;
  ASSERT_TRUE(LoadExtension(test_dir.UnpackedPath()));
  ASSERT_TRUE(catcher.GetNextResult()) << catcher.message();

  // The above test makes 2 calls to chrome.tabs.create, so check that those
  // have been logged in the histograms we expect them to be.
  EXPECT_EQ(2, histogram_tester.GetBucketCount(
                   "Extensions.Functions.ExtensionCalls",
                   functions::HistogramValue::TABS_CREATE));
  EXPECT_EQ(2, histogram_tester.GetBucketCount(
                   "Extensions.Functions.ExtensionServiceWorkerCalls",
                   functions::HistogramValue::TABS_CREATE));
  EXPECT_EQ(2, histogram_tester.GetBucketCount(
                   "Extensions.Functions.ExtensionMV3Calls",
                   functions::HistogramValue::TABS_CREATE));
}

class NativeBindingsBrowserNamespaceTest : public NativeBindingsApiTest {
 public:
  NativeBindingsBrowserNamespaceTest() = default;

  NativeBindingsBrowserNamespaceTest(
      const NativeBindingsBrowserNamespaceTest&) = delete;
  const NativeBindingsBrowserNamespaceTest& operator=(
      const NativeBindingsBrowserNamespaceTest&) = delete;
  ~NativeBindingsBrowserNamespaceTest() override = default;
};

// Tests that extension background script contexts have access to
// `chrome.<extension_api>` and `browser.<extension_api>` objects.
IN_PROC_BROWSER_TEST_F(NativeBindingsBrowserNamespaceTest,
                       ChromeAndBrowserObjects_ExtensionBackground) {
  ASSERT_TRUE(RunExtensionTest("browser_object/background_context"))
      << message_;
}

// Tests that extension foreground script contexts (e.g. content script,
// extension page) have access to `chrome.<extension_api>` and
// `browser.<extension_api>` objects.
IN_PROC_BROWSER_TEST_F(NativeBindingsBrowserNamespaceTest,
                       ChromeAndBrowserObjects_ExtensionForeground) {
  ASSERT_TRUE(StartEmbeddedTestServer());
  const GURL& test_website =
      embedded_test_server()->GetURL("a.com", "/title1.html");

  const Extension* extension = LoadExtension(
      test_data_dir_.AppendASCII("browser_object/foreground_context"));
  ASSERT_TRUE(extension);

  // Content script.
  ResultCatcher catcher;
  auto* web_contents = GetActiveWebContents();
  ASSERT_TRUE(NavigateToURL(web_contents, test_website));
  ASSERT_TRUE(catcher.GetNextResult()) << catcher.message();

  // Extension page.
  ResultCatcher extension_resource_catcher;
  ASSERT_TRUE(NavigateToURL(
      web_contents,
      GURL(extension->GetResourceURL("extension_resource_page.html"))));
  ASSERT_TRUE(catcher.GetNextResult()) << catcher.message();
}

// Tests that an externally connectable webpage has access to
// `chrome.<extension_api>` and `browser.<extension_api>` objects. Additionally
// it tests that `chrome.app` is bound, but `browser.app` is not.
IN_PROC_BROWSER_TEST_F(NativeBindingsBrowserNamespaceTest,
                       ChromeAndBrowserObjects_ExternallyConnectableWebpage) {
  ASSERT_TRUE(StartEmbeddedTestServer());
  const GURL& test_website =
      embedded_test_server()->GetURL("a.com", "/title1.html");

  TestExtensionDir test_dir;
  test_dir.WriteManifest(
      R"({
          "name": "Externally connectable test extension",
          "version": "0.1",
          "manifest_version": 3,
          "externally_connectable": {
            "matches": ["*://a.com/*"]
          }
        })");
  test_dir.WriteFile(FILE_PATH_LITERAL("background.js"), "");
  ASSERT_TRUE(LoadExtension(test_dir.UnpackedPath()));

  auto* web_contents = GetActiveWebContents();
  ASSERT_TRUE(NavigateToURL(web_contents, test_website));

  EXPECT_TRUE(ApiExists(web_contents, "chrome.runtime"));
  EXPECT_TRUE(ApiExists(web_contents, "browser.runtime"));
  EXPECT_TRUE(ApiExists(web_contents, "chrome.app"));
  EXPECT_FALSE(ApiExists(web_contents, "browser.app"));
}

// Tests that the `browser` namespace is not available in WebUI.
IN_PROC_BROWSER_TEST_F(NativeBindingsBrowserNamespaceTest, WebUIBindings) {
  ASSERT_TRUE(StartEmbeddedTestServer());
  auto* web_contents = GetActiveWebContents();
  ASSERT_TRUE(NavigateToURL(web_contents, GURL("chrome://extensions")));

  EXPECT_TRUE(ObjectIsDefined(web_contents, "chrome"));
  EXPECT_FALSE(ObjectIsDefined(web_contents, "browser"));
}

// Tests that an arbitrary web page with no extension API access has access to
// the `chrome` namespace, but not the browser namespace.
IN_PROC_BROWSER_TEST_F(NativeBindingsBrowserNamespaceTest,
                       TestNonExtensionChromeAndBrowserObjects) {
  ASSERT_TRUE(StartEmbeddedTestServer());
  const GURL& test_website =
      embedded_test_server()->GetURL("a.com", "/title1.html");
  auto* web_contents = GetActiveWebContents();
  ASSERT_TRUE(NavigateToURL(web_contents, test_website));

  EXPECT_TRUE(ObjectIsDefined(web_contents, "chrome"));
  EXPECT_FALSE(ObjectIsDefined(web_contents, "browser"));
}

// Tests that standard APIs like `runtime` are distinct objects in the `chrome`
// and `browser` namespaces, even if they point to the same underlying API.
IN_PROC_BROWSER_TEST_F(NativeBindingsBrowserNamespaceTest,
                       ChromeAndBrowserObjects_ApiAliasing) {
  TestExtensionDir test_dir;
  test_dir.WriteManifest(
      R"({
          "name": "Api Aliasing test",
          "version": "0.1",
          "manifest_version": 3,
          "background": {"service_worker": "background.js"}
        })");
  test_dir.WriteFile(FILE_PATH_LITERAL("background.js"),
                     R"(chrome.test.runTests([
                        function checkApiAliasing() {
                          // Standard APIs like runtime are independently
                          // created for both chrome and browser namespaces.
                          // They are not aliases of each other identity-wise,
                          // but they provide the same functionality.

                          // Try to modify chrome.runtime as representative of
                          // most APIs since they use the same bindings
                          // accessor. In a non-devtools-page context like this,
                          // the API root on chrome is typically writable.
                          let originalRuntimeApi = chrome.runtime;
                          chrome.runtime = 'bar';
                          chrome.test.assertEq('bar', chrome.runtime);

                          // Verify that browser.runtime was not affected by
                          // the change to chrome.runtime, confirming it's an
                          // independent object instance.
                          chrome.test.assertEq(originalRuntimeApi,
                                               browser.runtime);

                          // Revert modification for the following tests.
                          chrome.runtime = originalRuntimeApi;
                          chrome.test.assertEq(originalRuntimeApi,
                                               chrome.runtime);

                          // Modify a member of chrome.runtime and confirm
                          // browser.runtime reflects that change, because
                          // both independent binding objects point to the
                          // same underlying API implementation.
                          chrome.runtime.sendMessage = 'bar';
                          chrome.test.assertEq('bar',
                                               chrome.runtime.sendMessage);
                          chrome.test.assertEq(chrome.runtime.sendMessage,
                                               browser.runtime.sendMessage);

                          chrome.test.succeed();
                        }
                      ]);)");

  ResultCatcher catcher;
  const Extension* extension = LoadExtension(test_dir.UnpackedPath());
  ASSERT_TRUE(extension);
  ASSERT_TRUE(catcher.GetNextResult()) << catcher.message();
}

// Tests where the `devtools` API should and should not be defined for the
// `chrome` and `browser` namespaces across different extension contexts. Note:
// Although the `devtools` API is not restricted in any special way beyond its
// permission and the script context, it is bespoke and dynamically injected by
// the DevTools frontend rather than the standard extension bindings system.
// Because of this we explicitly test the API's visibility behaviors across
// contexts to prevent regressions.
IN_PROC_BROWSER_TEST_F(NativeBindingsBrowserNamespaceTest,
                       ChromeAndBrowserObjects_DevToolsVisibility) {
  ASSERT_TRUE(StartEmbeddedTestServer());
  TestExtensionDir test_dir;
  test_dir.WriteManifest(
      R"({
          "name": "DevTools Visibility test",
          "version": "0.1",
          "manifest_version": 3,
          "background": {"service_worker": "background.js"},
          "devtools_page": "devtools.html",
          "content_scripts": [{
            "matches": ["*://example.com/*"],
            "js": ["content_script.js"]
          }]
        })");
  test_dir.WriteFile(FILE_PATH_LITERAL("devtools.html"),
                     "<script src='devtools.js'></script>");
  test_dir.WriteFile(FILE_PATH_LITERAL("devtools.js"),
                     R"(chrome.test.runTests([
                          function checkDevTools() {
                            chrome.test.assertTrue(
                              chrome.hasOwnProperty('devtools'));
                            chrome.test.assertNe(undefined, chrome.devtools);
                            chrome.test.assertNe(undefined, browser);
                            chrome.test.assertTrue('devtools' in browser);
                            chrome.test.assertNe(undefined, browser.devtools);
                            chrome.test.succeed();
                          }
                        ]);)");
  constexpr char kCheckNoDevTools[] =
      R"(chrome.test.runTests([
           function checkNoDevTools() {
             chrome.test.assertNe(undefined, chrome);
             chrome.test.assertFalse('devtools' in chrome);
             chrome.test.assertEq(undefined, chrome.devtools);
             chrome.test.assertNe(undefined, browser);
             chrome.test.assertFalse('devtools' in browser);
             chrome.test.assertEq(undefined, browser.devtools);
             chrome.test.succeed();
           }
         ]);)";
  test_dir.WriteFile(FILE_PATH_LITERAL("background.js"), kCheckNoDevTools);
  test_dir.WriteFile(FILE_PATH_LITERAL("page.html"),
                     "<script src='page.js'></script>");
  test_dir.WriteFile(FILE_PATH_LITERAL("page.js"), kCheckNoDevTools);
  test_dir.WriteFile(FILE_PATH_LITERAL("content_script.js"), kCheckNoDevTools);

  // Confirm the background page does not have chrome/browser.devtools defined.
  ResultCatcher background_catcher;
  const Extension* extension = LoadExtension(test_dir.UnpackedPath());
  ASSERT_TRUE(extension);
  ASSERT_TRUE(background_catcher.GetNextResult())
      << background_catcher.message();

  // Confirm that an extension page context does not have
  // chrome/browser.devtools defined.
  ResultCatcher extension_page_catcher;
  // Navigate to the extension page to run devtools tests.
  ASSERT_TRUE(NavigateToURL(GetActiveWebContents(),
                            extension->GetResourceURL("page.html")));
  ASSERT_TRUE(extension_page_catcher.GetNextResult())
      << extension_page_catcher.message();

  // Confirm that a content script context does not have chrome/browser.devtools
  // defined.
  ResultCatcher content_script_catcher;
  ASSERT_TRUE(NavigateToURL(
      GetActiveWebContents(),
      embedded_test_server()->GetURL("example.com", "/title1.html")));
  ASSERT_TRUE(content_script_catcher.GetNextResult())
      << content_script_catcher.message();

  // Confirm that the main world of the web page does not have devtools defined.
  EXPECT_EQ(false, content::EvalJs(GetActiveWebContents(),
                                   "chrome.hasOwnProperty('devtools')"));
  EXPECT_EQ(true, content::EvalJs(GetActiveWebContents(),
                                  "typeof browser === 'undefined'"));

  // Confirm that the devtools page *does* have chrome/browser.devtools defined.
  ResultCatcher devtools_page_catcher;
  DevToolsWindow::OpenDevToolsWindow(GetActiveWebContents(),
                                     DevToolsToggleAction::Show(),
                                     DevToolsOpenedByAction::kUnknown);

  ASSERT_TRUE(devtools_page_catcher.GetNextResult())
      << devtools_page_catcher.message();
}

// Tests the edge case where the DevTools page is loaded outside of the DevTools
// frontend (e.g. manually navigating to it in a regular tab).
// `browser.devtools` and `chrome.devtools` should not be defined because they
// are only injected by the DevTools frontend when hosted within it, but the
// `browser` namespace itself should still be defined. We explicitly test this
// to prevent regressions in the bespoke DevTools frontend injection and
// aliasing checks.
IN_PROC_BROWSER_TEST_F(NativeBindingsBrowserNamespaceTest,
                       ChromeAndBrowserObjects_DevToolsVisibility_External) {
  ASSERT_TRUE(StartEmbeddedTestServer());
  TestExtensionDir test_dir;
  test_dir.WriteManifest(
      R"({
          "name": "DevTools External Visibility test",
          "version": "0.1",
          "manifest_version": 3,
          "devtools_page": "devtools.html"
        })");
  test_dir.WriteFile(FILE_PATH_LITERAL("devtools.html"),
                     "<script src='devtools.js'></script>");
  test_dir.WriteFile(FILE_PATH_LITERAL("devtools.js"),
                     R"(chrome.test.runTests([
                          function checkNoDevTools() {
                            chrome.test.assertFalse(chrome.hasOwnProperty(
                              'devtools'));
                            chrome.test.assertNe(undefined, chrome);
                            chrome.test.assertFalse('devtools' in chrome);
                            chrome.test.assertNe(undefined, browser);
                            chrome.test.assertFalse('devtools' in browser);
                            chrome.test.succeed();
                          }
                        ]);)");

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

  // Manually navigate to the devtools page. In a manual navigation the devtools
  // frontend isn't available to inject chrome.devtools so we shouldn't alias
  // browser.devtools either.
  ASSERT_TRUE(NavigateToURL(GetActiveWebContents(),
                            extension->GetResourceURL("devtools.html")));
  ASSERT_TRUE(catcher.GetNextResult()) << catcher.message();
}

// Tests the visibility and aliasing of the `devtools` API for the `chrome` and
// `browser` namespaces inside iframes nested within a DevTools page. Since the
// `devtools` API is bespoke and dynamically injected by the DevTools frontend
// rather than through the standard extension bindings system, we explicitly
// test nested frames to ensure the custom injection and aliasing logic properly
// propagates across frame boundaries without regression.
IN_PROC_BROWSER_TEST_F(
    NativeBindingsBrowserNamespaceTest,
    ChromeAndBrowserObjects_DevToolsVisibility_NestedIframe) {
  ASSERT_TRUE(StartEmbeddedTestServer());
  TestExtensionDir test_dir;
  test_dir.WriteManifest(
      R"({
          "name": "DevTools Nested Iframe test",
          "version": "0.1",
          "manifest_version": 3,
          "devtools_page": "devtools.html"
        })");
  test_dir.WriteFile(FILE_PATH_LITERAL("devtools.html"),
                     R"(<iframe src="child.html"></iframe>)");
  test_dir.WriteFile(FILE_PATH_LITERAL("child.html"),
                     "<script src='child.js'></script>");
  test_dir.WriteFile(FILE_PATH_LITERAL("child.js"),
                     R"(chrome.test.runTests([
                          // It takes a bit of time for the devtools frontend to
                          // inject the API since this is a iframe. So to avoid
                          // test flakiness we wait for chrome.devtools to be
                          // defined before proceeding with the test.
                          async function waitForDevTools() {
                            const start = Date.now();
                            // 2 second timeout
                            while (Date.now() - start < 2000) {
                              if (chrome.devtools) {
                                chrome.test.succeed();
                                return;
                              }
                              await new Promise(r => setTimeout(r, 50));
                            }
                            chrome.test.fail('Timed out waiting for ' +
                              'devtools frontend to define chrome.devtools.');
                          },
                          async function checkNestedFrameHasDevTools() {
                            chrome.test.assertNe(undefined, chrome);
                            chrome.test.assertTrue('devtools' in chrome);
                            chrome.test.assertNe(undefined, chrome.devtools);
                            chrome.test.assertNe(undefined, browser);
                            chrome.test.assertTrue('devtools' in browser);
                            chrome.test.assertNe(undefined, browser.devtools);
                            chrome.test.succeed();
                          }
                        ]);)");

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

  DevToolsWindow::OpenDevToolsWindow(GetActiveWebContents(),
                                     DevToolsToggleAction::Show(),
                                     DevToolsOpenedByAction::kUnknown);

  ASSERT_TRUE(catcher.GetNextResult()) << catcher.message();
}

// Tests the `browser.devtools` dynamic aliasing behavior to `chrome.devtools`
// in a DevTools page. Note: The `devtools` API is bespoke and dynamically
// injected onto `chrome` by the DevTools frontend rather than through the
// standard extension bindings system. We explicitly test this aliasing to
// prevent regressions in the custom frontend-injected API.
IN_PROC_BROWSER_TEST_F(NativeBindingsBrowserNamespaceTest,
                       ChromeAndBrowserObjects_DevToolsApiAliasing) {
  TestExtensionDir test_dir;
  test_dir.WriteManifest(
      R"({
          "name": "DevTools Aliasing test",
          "version": "0.1",
          "manifest_version": 3,
          "devtools_page": "devtools.html"
        })");
  test_dir.WriteFile(FILE_PATH_LITERAL("devtools.html"),
                     "<script src='devtools.js'></script>");
  test_dir.WriteFile(FILE_PATH_LITERAL("devtools.js"),
                     R"(chrome.test.runTests([
                        function checkDevtoolsApiAliasing() {
                          // Unlike other APIs, browser.devtools is a dynamic
                          // alias (via a getter) to chrome.devtools. This is
                          // necessary because devtools is injected by the
                          // devtools frontend.

                          // Attempts to overwrite the root chrome.devtools
                          // object do not succeed though. In this context
                          // (devtools page), it is non-writable/configurable.
                          let originalDevtoolsApi = chrome.devtools;
                          chrome.devtools = 'bar';
                          chrome.test.assertEq(originalDevtoolsApi,
                                               chrome.devtools);

                          // Since browser.devtools is a getter that looks up
                          // chrome.devtools, it still matches whatever is on
                          // chrome.
                          chrome.test.assertEq(chrome.devtools,
                                               browser.devtools);

                          // Modify a member of chrome.devtools and confirm
                          // browser.devtools reflects that change, because
                          // it is a direct dynamic alias to the same
                          // underlying object.
                          chrome.devtools.panels = 'bar';
                          chrome.test.assertEq('bar', browser.devtools.panels);

                          chrome.test.succeed();
                        }
                      ]);)");

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

  DevToolsWindow::OpenDevToolsWindow(GetActiveWebContents(),
                                     DevToolsToggleAction::Show(),
                                     DevToolsOpenedByAction::kUnknown);

  ASSERT_TRUE(catcher.GetNextResult()) << catcher.message();
}

class NativeBindingsBrowserNamespaceOnWebPagesTest
    : public base::test::WithFeatureOverride,
      public NativeBindingsApiTest {
 public:
  NativeBindingsBrowserNamespaceOnWebPagesTest()
      : base::test::WithFeatureOverride(
            extensions_features::kExtensionBrowserNamespaceOnWebPages) {}

  void SetUpCommandLine(base::CommandLine* command_line) override {
    NativeBindingsApiTest::SetUpCommandLine(command_line);
    command_line->AppendSwitch(switches::kExtensionTestApiOnWebPages);
  }
};

// Tests that the `browser` namespace is defined on web pages if and only if
// the `extensions_features::kExtensionBrowserNamespaceOnWebPages` feature
// is enabled. Also confirms that when enabled, attributes on `chrome` and
// `browser` (like `test` API) point to the exact same object.
IN_PROC_BROWSER_TEST_P(NativeBindingsBrowserNamespaceOnWebPagesTest,
                       BrowserObjectOnWebPages) {
  // Start the test server and navigate to a non-extension web page `a.com`.
  ASSERT_TRUE(StartEmbeddedTestServer());
  const GURL& test_website =
      embedded_test_server()->GetURL("a.com", "/title1.html");
  auto* web_contents = GetActiveWebContents();
  ASSERT_TRUE(NavigateToURL(web_contents, test_website));

  // The `chrome` namespace should always be defined on web pages.
  EXPECT_TRUE(ObjectIsDefined(web_contents, "chrome"));

  // The `browser` namespace should be defined if the feature is enabled,
  // and undefined otherwise.
  if (IsParamFeatureEnabled()) {
    EXPECT_TRUE(ObjectIsDefined(web_contents, "browser"));
    // Confirm that the `test` API on both namespaces points to the exact same
    // object.
    EXPECT_TRUE(ApiExists(web_contents, "chrome.test"));
    EXPECT_TRUE(ApiExists(web_contents, "browser.test"));
    EXPECT_TRUE(content::EvalJs(web_contents, "chrome.test === browser.test")
                    .ExtractBool());
  } else {
    EXPECT_FALSE(ObjectIsDefined(web_contents, "browser"));
  }
}

INSTANTIATE_FEATURE_OVERRIDE_TEST_SUITE(
    NativeBindingsBrowserNamespaceOnWebPagesTest);

class NativeBindingsBrowserNamespaceOnWebPagesNoTestApiTest
    : public base::test::WithFeatureOverride,
      public NativeBindingsApiTest {
 public:
  NativeBindingsBrowserNamespaceOnWebPagesNoTestApiTest()
      : base::test::WithFeatureOverride(
            extensions_features::kExtensionBrowserNamespaceOnWebPages) {}
};

// Tests that the `browser` namespace is defined on web pages if and only if
// the `extensions_features::kExtensionBrowserNamespaceOnWebPages` feature
// is enabled, even if there are no features available to the page.
IN_PROC_BROWSER_TEST_P(NativeBindingsBrowserNamespaceOnWebPagesNoTestApiTest,
                       BrowserObjectOnWebPagesWithoutAPIs) {
  ASSERT_TRUE(StartEmbeddedTestServer());
  const GURL& test_website =
      embedded_test_server()->GetURL("a.com", "/title1.html");
  auto* web_contents = GetActiveWebContents();
  ASSERT_TRUE(NavigateToURL(web_contents, test_website));

  EXPECT_TRUE(ObjectIsDefined(web_contents, "chrome"));

  if (IsParamFeatureEnabled()) {
    EXPECT_TRUE(ObjectIsDefined(web_contents, "browser"));
    // Note: We cannot test that `browser` has the same attributes as `chrome`
    // here because the only attributes present on `chrome` for a regular
    // web page without any extension APIs are legacy properties like
    // `chrome.loadTimes` and `chrome.csi` (and `chrome.app` which is explicitly
    // skipped for `browser` in `NativeExtensionBindingsSystem`).
    // `loadTimes` and `csi` are injected via a separate V8 extension script
    // (v8/LoadTimes) which explicitly only populates the `chrome` object and
    // does not alias to `browser`. `browser` is not a generic proxy of
    // `chrome`, but rather a separate object where extension APIs are
    // explicitly mirrored.
  } else {
    EXPECT_FALSE(ObjectIsDefined(web_contents, "browser"));
  }
}

INSTANTIATE_FEATURE_OVERRIDE_TEST_SUITE(
    NativeBindingsBrowserNamespaceOnWebPagesNoTestApiTest);

// TODO(crbug.com/401226626): Test that the browser object also has dev mode
// restricted APIs set on correctly as well.

class DeveloperModeNativeBindingsApiTest : public NativeBindingsApiTest {
 public:
  DeveloperModeNativeBindingsApiTest() {
    // Ensure chrome.debugger is controlled by Developer Mode.
    scoped_feature_list_.InitAndEnableFeature(
        extensions_features::kDebuggerAPIRestrictedToDevMode);
  }

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

// TODO(crbug.com/390138269): Revert the user scripts specific testing once the
// extensions::kUserScriptUserExtensionToggle feature is launched.
IN_PROC_BROWSER_TEST_F(
    DeveloperModeNativeBindingsApiTest,
    DeveloperModeOnlyWithAPIPermissionUserIsNotInDeveloperMode) {
  // Developer mode-only APIs should not be available if the user is not in
  // developer mode.
  SetCustomArg("not_in_developer_mode");
  util::SetDeveloperModeForProfile(profile(), false);
  ASSERT_TRUE(RunExtensionTest(
      "native_bindings/developer_mode_only_with_api_permission"))
      << message_;
}

IN_PROC_BROWSER_TEST_F(
    DeveloperModeNativeBindingsApiTest,
    DeveloperModeOnlyWithAPIPermissionUserIsInDeveloperMode) {
  // Developer mode-only APIs should be available if the user is in developer
  // mode.
  SetCustomArg("in_developer_mode");
  util::SetDeveloperModeForProfile(profile(), true);
  ASSERT_TRUE(RunExtensionTest(
      "native_bindings/developer_mode_only_with_api_permission"))
      << message_;
}

IN_PROC_BROWSER_TEST_F(
    DeveloperModeNativeBindingsApiTest,
    DeveloperModeOnlyWithoutAPIPermissionUserIsNotInDeveloperMode) {
  util::SetDeveloperModeForProfile(profile(), false);
  ASSERT_TRUE(RunExtensionTest(
      "native_bindings/developer_mode_only_without_api_permission"))
      << message_;
}

IN_PROC_BROWSER_TEST_F(
    DeveloperModeNativeBindingsApiTest,
    DeveloperModeOnlyWithoutAPIPermissionUserIsInDeveloperMode) {
  util::SetDeveloperModeForProfile(profile(), true);
  ASSERT_TRUE(RunExtensionTest(
      "native_bindings/developer_mode_only_without_api_permission"))
      << message_;
}

// Tests that changing the developer mode setting affects existing renderers
// for page-based contexts (i.e., the main renderer thread).
IN_PROC_BROWSER_TEST_F(DeveloperModeNativeBindingsApiTest,
                       SwitchingDeveloperModeAffectsExistingRenderers_Pages) {
  static constexpr char kManifest[] =
      R"({
           "name": "Test",
           "manifest_version": 3,
           "version": "0.1",
           "permissions": ["%s"]
         })";
  static constexpr char kPageHtml[] =
      R"(<!doctype html>
         <html>
           <script src="page.js"></script>
         </html>)";

  TestExtensionDir test_dir;
  test_dir.WriteManifest(base::StringPrintf(kManifest, "debugger"));
  test_dir.WriteFile(FILE_PATH_LITERAL("page.html"), kPageHtml);
  test_dir.WriteFile(FILE_PATH_LITERAL("page.js"), kCheckApiAvailability);
  test_dir.WriteFile(FILE_PATH_LITERAL("script.js"), "// blank");

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

  const GURL extension_url = extension->GetResourceURL("page.html");

  // Navigate to the extension page.
  auto* existing_tab = GetActiveWebContents();
  ASSERT_TRUE(NavigateToURL(existing_tab, extension_url));
  ASSERT_EQ(extension_url, existing_tab->GetLastCommittedURL());

  ScriptResultQueue result_queue;

  // By default, the API is unavailable.
  ASSERT_TRUE(content::ExecJs(existing_tab, "verifyApiIsNotAvailable();"));
  EXPECT_EQ("success", result_queue.GetNextResult());

  // Next, set the user in developer mode. Now the API should be available.
  util::SetDeveloperModeForProfile(profile(), true);
  ASSERT_TRUE(content::ExecJs(existing_tab, "verifyApiIsAvailable();"));
  EXPECT_EQ("success", result_queue.GetNextResult());

  // Toggle back to not in developer mode. The API should be unavailable again.
  util::SetDeveloperModeForProfile(profile(), false);
  ASSERT_TRUE(content::ExecJs(existing_tab, "verifyApiIsNotAvailable();"));
  EXPECT_EQ("success", result_queue.GetNextResult());
}

// Tests that incognito windows use the developer mode setting from the
// original, on-the-record profile (since incognito windows can't separately
// set developer mode).
IN_PROC_BROWSER_TEST_F(DeveloperModeNativeBindingsApiTest,
                       IncognitoRenderersUseOriginalProfilesDevModeSetting) {
  static constexpr char kManifest[] =
      R"({
           "name": "Test",
           "manifest_version": 3,
           "version": "0.1",
           "incognito": "split",
           "permissions": ["%s"]
         })";
  static constexpr char kPageHtml[] =
      R"(<!doctype html>
         <html>
           <script src="page.js"></script>
         </html>)";

  TestExtensionDir test_dir;
  test_dir.WriteManifest(base::StringPrintf(kManifest, "debugger"));
  test_dir.WriteFile(FILE_PATH_LITERAL("page.html"), kPageHtml);
  test_dir.WriteFile(FILE_PATH_LITERAL("page.js"), kCheckApiAvailability);
  test_dir.WriteFile(FILE_PATH_LITERAL("script.js"), "// blank");

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

  const GURL extension_url = extension->GetResourceURL("page.html");

  BrowserWindowInterface* incognito_browser =
      OpenURLOffTheRecord(profile(), extension_url);
  content::WebContents* incognito_tab =
      incognito_browser->GetTabStripModel()->GetActiveWebContents();
  content::WaitForLoadStop(incognito_tab);

  ScriptResultQueue result_queue;

  // By default, the API is unavailable.
  ASSERT_TRUE(content::ExecJs(incognito_tab, "verifyApiIsNotAvailable();"));
  EXPECT_EQ("success", result_queue.GetNextResult());

  // Next, set the user in developer mode. Now the API should be available.
  util::SetDeveloperModeForProfile(profile(), true);
  ASSERT_TRUE(content::ExecJs(incognito_tab, "verifyApiIsAvailable();"));
  EXPECT_EQ("success", result_queue.GetNextResult());

  // Toggle back to not in developer mode. The API should be unavailable again.
  util::SetDeveloperModeForProfile(profile(), false);
  ASSERT_TRUE(content::ExecJs(incognito_tab, "verifyApiIsNotAvailable();"));
  EXPECT_EQ("success", result_queue.GetNextResult());
}

// Tests that changing the developer mode setting affects existing renderers
// for service worker contexts (which run off the main thread in the renderer).
// TODO(crbug.com/40946312): Test flaky on multiple platforms
IN_PROC_BROWSER_TEST_F(
    DeveloperModeNativeBindingsApiTest,
    DISABLED_SwitchingDeveloperModeAffectsExistingRenderers_ServiceWorkers) {
  static constexpr char kManifest[] =
      R"({
           "name": "Test",
           "manifest_version": 3,
           "version": "0.1",
           "permissions": ["%s"],
           "background": {"service_worker": "background.js"}
         })";

  TestExtensionDir test_dir;
  test_dir.WriteManifest(base::StringPrintf(kManifest, "debugger"));
  test_dir.WriteFile(FILE_PATH_LITERAL("background.js"), kCheckApiAvailability);
  test_dir.WriteFile(FILE_PATH_LITERAL("script.js"), "// blank");

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

  auto call_in_service_worker = [this, extension](const std::string& script) {
    return BackgroundScriptExecutor::ExecuteScript(
        profile(), extension->id(), script,
        BackgroundScriptExecutor::ResultCapture::kSendScriptResult);
  };

  auto renderer_round_trip = [this, extension]() {
    EXPECT_EQ("success",
              BackgroundScriptExecutor::ExecuteScript(
                  profile(), extension->id(),
                  "chrome.test.sendScriptResult('success');",
                  BackgroundScriptExecutor::ResultCapture::kSendScriptResult));
  };

  // By default, the API is unavailable.
  EXPECT_EQ("success", call_in_service_worker("verifyApiIsNotAvailable();"));

  // Next, set the user in developer mode. Now the API should be available.
  util::SetDeveloperModeForProfile(profile(), true);
  // We need to give the renderer time to do a few thread hops since there are
  // multiple IPC channels at play (unlike the test above). Do a round-trip to
  // the renderer to allow it to process.
  renderer_round_trip();
  EXPECT_EQ("success", call_in_service_worker("verifyApiIsAvailable();"));

  // Toggle back to not in developer mode. The API should be unavailable again.
  util::SetDeveloperModeForProfile(profile(), false);
  renderer_round_trip();
  EXPECT_EQ("success", call_in_service_worker("verifyApiIsNotAvailable();"));
}

// Tests that when an event listener throws an exception whose `stack` getter
// removes the `iframe` (destroying its context), synchronous event dispatch
// safely breaks out without a heap-use-after-free on the freed
// `extensions::JSRunner`, while ensuring a one-time message sender receives
// the response sent by a previous listener before context teardown. This is a
// regression test for `crbug.com/536676756` when run on AddressSanitizer
// builders. It is not limited to run just on AddressSanitizer builders so we
// can get code coverage too.
IN_PROC_BROWSER_TEST_F(NativeBindingsApiTest,
                       ListenersDestroyingFrameContextInErrorStackGetter) {
  static constexpr char kManifest[] =
      R"({
           "name": "Events UAF Reproducer",
           "version": "0.1",
           "manifest_version": 3,
           "permissions": ["storage"]
         })";

  static constexpr char kPageHtml[] =
      R"(<!DOCTYPE html>
         <html>
         <head>
           <script src="page.js"></script>
         </head>
         <body>
           <iframe id="test_frame" src="frame.html"></iframe>
         </body>
         </html>)";

  static constexpr char kPageJs[] =
      R"('use strict';

         window.onload = function() {
           // Send a one-time message to trigger `chrome.runtime.onMessage`.
           chrome.runtime.sendMessage('test_message', function(response) {
             chrome.test.sendMessage('got_response: ' + response);
           });
         };)";

  static constexpr char kFrameHtml[] =
      R"(<!DOCTYPE html>
         <html>
         <head>
           <script src="frame.js"></script>
         </head>
         <body></body>
         </html>)";

  static constexpr char kFrameJs[] =
      R"('use strict';

         // Add the first listener, which sends a reply to the sender via
         // `sendResponse()` and also returns a value.
         chrome.runtime.onMessage.addListener(
             function firstListener(message, sender, sendResponse) {
               sendResponse('first_listener_reply');
               return 'first_listener_return_value';
             });

         // Add the second listener, which calls `sendResponse()` and then
         // throws an error object with a custom `stack` property getter.
         // When `extensions::ExceptionHandler::HandleException()` inspects the
         // exception stack trace via `v8::TryCatch::StackTrace()`, the getter
         // executes reentrantly and calls `frameElement.remove()`. That
         // detaches the `iframe` from the DOM, destroys its `v8::Context`,
         // and frees the per-context `extensions::JSRunner` data on the heap.
         chrome.runtime.onMessage.addListener(
             function secondListener(message, sender, sendResponse) {
               sendResponse('second_listener_reply');
               const exception = {};
               Object.defineProperty(exception, 'stack', {
                 get() {
                   frameElement.remove();
                 },
               });
               throw exception;
             });

         // Add a third listener. Previously,
         // `extensions::EventEmitter::DispatchSync()` failed to check
         // `extensions::binding::IsContextValid()` after exception handling
         // and proceeded to execute this third listener using the dangling
         // `extensions::JSRunner` pointer, triggering a heap-use-after-free
         // read. `extensions::EventEmitter::DispatchSync()` now breaks the
         // loop without calling this third listener.
         chrome.runtime.onMessage.addListener(
             function thirdListener(message, sender, sendResponse) {
               sendResponse('third_listener_reply');
             });)";

  // Write the extension manifest, html, and js test files to the test
  // directory.
  TestExtensionDir test_dir;
  test_dir.WriteManifest(kManifest);
  test_dir.WriteFile(FILE_PATH_LITERAL("page.html"), kPageHtml);
  test_dir.WriteFile(FILE_PATH_LITERAL("page.js"), kPageJs);
  test_dir.WriteFile(FILE_PATH_LITERAL("frame.html"), kFrameHtml);
  test_dir.WriteFile(FILE_PATH_LITERAL("frame.js"), kFrameJs);

  // Set up a message listener to wait for the expected response from the
  // first event listener.
  ExtensionTestMessageListener response_listener(
      /*expected_message=*/"got_response: first_listener_reply");

  // Load the unpacked extension and assert that it loaded successfully.
  const Extension* extension = LoadExtension(test_dir.UnpackedPath());
  ASSERT_TRUE(extension);

  // Navigate to `page.html`, which loads the iframe and triggers one-time
  // messaging via `ui_test_utils::NavigateToURLWithDisposition()`.
  ASSERT_TRUE(ui_test_utils::NavigateToURLWithDisposition(
      browser(), extension->GetResourceURL("page.html"),
      WindowOpenDisposition::NEW_FOREGROUND_TAB,
      ui_test_utils::BROWSER_TEST_WAIT_FOR_LOAD_STOP));

  // Verify that the one-time message sender received the reply from
  // `firstListener` even though `secondListener` destroyed the iframe context.
  {
    SCOPED_TRACE(
        "Waiting for the one-time message sender to receive the reply from "
        "firstListener");
    EXPECT_TRUE(response_listener.WaitUntilSatisfied());
  }
}

}  // namespace extensions
