// 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 "chrome/browser/extensions/extension_tab_util.h"

#include "base/run_loop.h"
#include "base/test/bind.h"
#include "base/test/metrics/histogram_tester.h"
#include "base/test/scoped_feature_list.h"
#include "base/test/test_future.h"
#include "build/build_config.h"
#include "chrome/browser/extensions/chrome_extensions_browser_client.h"
#include "chrome/browser/extensions/extension_browsertest.h"
#include "chrome/browser/extensions/extension_util.h"
#include "chrome/browser/profiles/profile.h"
#include "chrome/browser/tab_group_sync/tab_group_sync_service_factory.h"
#include "chrome/browser/tab_list/tab_list_interface.h"
#include "chrome/browser/ui/browser_window/public/browser_window_interface.h"
#include "chrome/common/webui_url_constants.h"
#include "chrome/test/base/browser_created_waiter.h"
#include "components/data_sharing/public/features.h"
#include "components/saved_tab_groups/public/features.h"
#include "components/saved_tab_groups/public/tab_group_sync_service.h"
#include "components/sessions/core/session_id.h"
#include "components/tab_groups/tab_group_id.h"
#include "components/tabs/public/tab_interface.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/runtime/runtime_api.h"
#include "extensions/browser/extension_registry.h"
#include "extensions/browser/extensions_browser_client.h"
#include "extensions/browser/test_extension_registry_observer.h"
#include "extensions/buildflags/buildflags.h"
#include "extensions/common/manifest_handlers/options_page_info.h"

#if BUILDFLAG(ENABLE_EXTENSIONS)
#include "chrome/browser/ui/browser_window/public/browser_window_interface.h"
#include "chrome/browser/ui/browser_window/public/browser_window_interface_iterator.h"
#include "chrome/browser/ui/browser_window/public/global_browser_collection.h"
#include "chrome/browser/ui/tabs/tab_group_model.h"
#include "chrome/browser/ui/tabs/tab_strip_model.h"
#include "chrome/test/base/ui_test_utils.h"
#include "components/tabs/public/tab_group.h"
#endif

static_assert(BUILDFLAG(ENABLE_EXTENSIONS_CORE));

namespace extensions {

namespace {

#if BUILDFLAG(ENABLE_EXTENSIONS)

const GURL& GetActiveUrl(BrowserWindowInterface* browser) {
  return browser->GetTabStripModel()
      ->GetActiveWebContents()
      ->GetLastCommittedURL();
}

// Emulates opening the options page from the extension calling the API
// function (chrome.runtime.openOptionsPage()).
bool OpenOptionsPageFromAPI(const Extension* extension,
                            content::BrowserContext* browser_context) {
  RuntimeAPI* api = RuntimeAPI::GetFactoryInstance()->Get(browser_context);
  base::test::TestFuture<bool> future;
  api->OpenOptionsPage(extension, browser_context, future.GetCallback());
  return future.Get();
}

#endif  // BUILDFLAG(ENABLE_EXTENSIONS)

}  // namespace

using ExtensionTabUtilBrowserTest = ExtensionBrowserTest;

IN_PROC_BROWSER_TEST_F(ExtensionTabUtilBrowserTest, ForEachTab) {
  // Browser tests start with 1 tab.
  EXPECT_EQ(GetTabCount(), 1);
  // ForEachTab should always supply a non-null WebContents.
  int count = 0;
  ExtensionTabUtil::ForEachTab(
      base::BindLambdaForTesting([&count](content::WebContents* contents) {
        EXPECT_TRUE(contents) << count;
        ++count;
      }));
  EXPECT_EQ(count, 1);
}

// Regression test for a crash on Android in ClearBackForwardCache caused by an
// extension that uses redirects. crbug.com/419143076
IN_PROC_BROWSER_TEST_F(ExtensionTabUtilBrowserTest,
                       ClearBackForwardCache_NoCrash) {
  base::RunLoop run_loop;
  ChromeExtensionsBrowserClient* client =
      static_cast<ChromeExtensionsBrowserClient*>(
          ExtensionsBrowserClient::Get());
  client->set_on_clear_back_forward_cache_for_test(run_loop.QuitClosure());
  ASSERT_TRUE(InstallExtensionFromWebstore(
      test_data_dir_.AppendASCII("crash_on_clear_back_forward_cache"),
      std::nullopt));
  run_loop.Run();
}

IN_PROC_BROWSER_TEST_F(ExtensionTabUtilBrowserTest, GetTabById) {
  // Browser tests start with 1 tab open.
  content::WebContents* active_contents = GetActiveWebContents();
  ASSERT_TRUE(active_contents);

  // Get the ID for the active tab.
  int tab_id = ExtensionTabUtil::GetTabId(active_contents);
  ASSERT_NE(tab_id, SessionID::InvalidValue().id());

  // Look up the web contents by ID. It should match the active contents.
  content::WebContents* found_contents = nullptr;
  EXPECT_TRUE(ExtensionTabUtil::GetTabById(
      tab_id, profile(), /*include_incognito=*/true, &found_contents));
  EXPECT_EQ(found_contents, active_contents);
}

IN_PROC_BROWSER_TEST_F(ExtensionTabUtilBrowserTest,
                       OpenOptionsPageFromWebContents) {
  // Load an extension with an options page that opens in a tab.
  const Extension* options_in_tab =
      LoadExtension(test_data_dir_.AppendASCII("options_page"));
  ASSERT_TRUE(options_in_tab);
  ASSERT_TRUE(OptionsPageInfo::HasOptionsPage(options_in_tab));

  content::WebContents* active_contents = GetActiveWebContents();
  ASSERT_TRUE(active_contents);

  EXPECT_TRUE(ExtensionTabUtil::OpenOptionsPageFromWebContents(
      options_in_tab, active_contents));

  EXPECT_EQ(GetActiveWebContents()->GetURL(),
            OptionsPageInfo::GetOptionsPage(options_in_tab));
}

IN_PROC_BROWSER_TEST_F(ExtensionTabUtilBrowserTest,
                       OpenOptionsPageFromWebContentsInView) {
  // Load an extension with an options page that opens in the
  // chrome://extensions page in a view.
  const Extension* options_in_view =
      LoadExtension(test_data_dir_.AppendASCII("options_page_in_view"));
  ASSERT_TRUE(options_in_view);
  ASSERT_TRUE(OptionsPageInfo::HasOptionsPage(options_in_view));

  content::WebContents* active_contents = GetActiveWebContents();
  ASSERT_TRUE(active_contents);

  EXPECT_TRUE(ExtensionTabUtil::OpenOptionsPageFromWebContents(
      options_in_view, active_contents));

  GURL expected_url;
#if BUILDFLAG(IS_ANDROID)
  expected_url = OptionsPageInfo::GetOptionsPage(options_in_view);
#else
  expected_url = GURL("chrome://extensions?options=" + options_in_view->id());
#endif

  EXPECT_EQ(GetActiveWebContents()->GetURL(), expected_url);
}

// Verifies that `ExtensionTabUtil::NavigateToURL` successfully navigates
// the current tab and new tabs to the specified URLs.
IN_PROC_BROWSER_TEST_F(ExtensionTabUtilBrowserTest, NavigateToURLNormal) {
  content::WebContents* web_contents = GetActiveWebContents();
  int initial_tab_count = GetTabCount();

  content::TestNavigationObserver version_page_observer(
      GURL("chrome://version"));
  version_page_observer.WatchWebContents(web_contents);
  ExtensionTabUtil::NavigateToURL(WindowOpenDisposition::CURRENT_TAB,
                                  web_contents, GURL("chrome://version"),
                                  base::DoNothing());
  version_page_observer.Wait();

  EXPECT_EQ(initial_tab_count, GetTabCount());
  auto url1 = GetActiveWebContents()->GetURL();
  EXPECT_THAT(url1, GURL("chrome://version"));

  content::TestNavigationObserver history_page_observer(
      GURL("chrome://history"));
  history_page_observer.StartWatchingNewWebContents();
  ExtensionTabUtil::NavigateToURL(WindowOpenDisposition::NEW_FOREGROUND_TAB,
                                  web_contents, GURL("chrome://history"),
                                  base::DoNothing());
  history_page_observer.Wait();

  EXPECT_EQ(initial_tab_count + 1, GetTabCount());
  auto url2 = GetActiveWebContents()->GetURL();
  EXPECT_THAT(url2, GURL("chrome://history"));
}

IN_PROC_BROWSER_TEST_F(ExtensionTabUtilBrowserTest, NavigateToURLWindow) {
  content::WebContents* web_contents = GetActiveWebContents();

  BrowserCreatedWaiter browser_created_waiter;

  base::RunLoop loop;
  ExtensionTabUtil::NavigateToURL(WindowOpenDisposition::NEW_WINDOW,
                                  web_contents, GURL("chrome://version"),
                                  loop.QuitClosure());
  loop.Run();

  // Wait for the new browser to be created and get its pointer.
  BrowserWindowInterface* const new_browser = browser_created_waiter.Wait();
  ASSERT_TRUE(new_browser);

  // Ensure it's not the same as the original browser.
  ASSERT_NE(browser_window_interface(), new_browser);
  TabListInterface* tab_list = TabListInterface::From(new_browser);
  ASSERT_TRUE(tab_list);
  content::WebContents* new_web_contents =
      tab_list->GetActiveTab()->GetContents();
  content::WaitForLoadStop(new_web_contents);
  ASSERT_TRUE(new_web_contents);
  EXPECT_THAT(new_web_contents->GetURL(), GURL("chrome://version"));
}

IN_PROC_BROWSER_TEST_F(ExtensionTabUtilBrowserTest, SupportsTabGroups) {
  // Tests default to a normal browser window.
  ASSERT_EQ(BrowserWindowInterface::TYPE_NORMAL,
            browser_window_interface()->GetType());

  // Normal browsers support tab groups.
  EXPECT_TRUE(ExtensionTabUtil::SupportsTabGroups(browser_window_interface()));
}

IN_PROC_BROWSER_TEST_F(ExtensionTabUtilBrowserTest, DoesNotSupportTabGroups) {
#if BUILDFLAG(IS_ANDROID)
  // Android doesn't support Chrome Apps, so we test with popups.
  const auto window_type = BrowserWindowInterface::Type::TYPE_POPUP;
#else
  // Test other platforms with apps, because they are a more typical use case.
  const auto window_type = BrowserWindowInterface::Type::TYPE_APP;
#endif  // BUILDFLAG(IS_ANDROID)

  BrowserWindowInterface* browser = CreateBrowserWindowWithType(window_type);

  // The window does not support tab groups.
  EXPECT_FALSE(ExtensionTabUtil::SupportsTabGroups(browser));
}

#if BUILDFLAG(ENABLE_EXTENSIONS)
IN_PROC_BROWSER_TEST_F(ExtensionTabUtilBrowserTest, OpenExtensionsOptionsPage) {
  // Load an extension with an options page that opens in a tab and one that
  // opens in the chrome://extensions page in a view.
  const Extension* options_in_tab =
      LoadExtension(test_data_dir_.AppendASCII("options_page"));
  const Extension* options_in_view =
      LoadExtension(test_data_dir_.AppendASCII("options_page_in_view"));
  ASSERT_TRUE(options_in_tab);
  ASSERT_TRUE(options_in_view);
  ASSERT_TRUE(OptionsPageInfo::HasOptionsPage(options_in_tab));
  ASSERT_TRUE(OptionsPageInfo::HasOptionsPage(options_in_view));

  // Start at the new tab page, and then open the extension options page.
  ASSERT_TRUE(ui_test_utils::NavigateToURL(browser(),
                                           chrome::ChromeUINewTabURLAsGURL()));
  EXPECT_EQ(1, browser()->tab_strip_model()->count());
  GURL options_url = OptionsPageInfo::GetOptionsPage(options_in_tab);
  EXPECT_TRUE(ExtensionTabUtil::OpenOptionsPage(options_in_tab, browser()));

  // Opening the options page should take the new tab and use it, so we should
  // have only one tab, and it should be open to the options page.
  EXPECT_EQ(1, browser()->tab_strip_model()->count());
  EXPECT_TRUE(content::WaitForLoadStop(
      browser()->tab_strip_model()->GetActiveWebContents()));
  EXPECT_EQ(options_url, GetActiveUrl(browser()));

  // Calling OpenOptionsPage again shouldn't result in any new tabs, since we
  // re-use the existing options page.
  EXPECT_TRUE(ExtensionTabUtil::OpenOptionsPage(options_in_tab, browser()));
  EXPECT_EQ(1, browser()->tab_strip_model()->count());
  EXPECT_TRUE(content::WaitForLoadStop(
      browser()->tab_strip_model()->GetActiveWebContents()));
  EXPECT_EQ(options_url, GetActiveUrl(browser()));

  // Navigate to chrome://version (something non-newtab, non-options). Calling
  // OpenOptionsPage() should create a new tab and navigate it to the options
  // page. So we should have two total tabs, with the active tab pointing to
  // options.
  ASSERT_TRUE(
      ui_test_utils::NavigateToURL(browser(), GURL("chrome://version")));
  EXPECT_TRUE(ExtensionTabUtil::OpenOptionsPage(options_in_tab, browser()));
  EXPECT_EQ(2, browser()->tab_strip_model()->count());
  EXPECT_TRUE(content::WaitForLoadStop(
      browser()->tab_strip_model()->GetActiveWebContents()));
  EXPECT_EQ(options_url, GetActiveUrl(browser()));

  // Navigate the tab to a different extension URL, and call OpenOptionsPage().
  // We should not reuse the current tab since it's opened to a page that isn't
  // the options page, and we don't want to arbitrarily close extension content.
  // Regression test for crbug.com/41239902.
  ASSERT_TRUE(ui_test_utils::NavigateToURL(
      browser(), options_in_tab->GetResourceURL("other.html")));
  EXPECT_TRUE(ExtensionTabUtil::OpenOptionsPage(options_in_tab, browser()));
  EXPECT_EQ(3, browser()->tab_strip_model()->count());
  EXPECT_TRUE(content::WaitForLoadStop(
      browser()->tab_strip_model()->GetActiveWebContents()));
  EXPECT_EQ(options_url, GetActiveUrl(browser()));

  // If the user navigates to the options page e.g. by typing in the url, it
  // should not override the currently-open tab.
  NavigateToURLInNewTab(options_url);
  EXPECT_EQ(4, browser()->tab_strip_model()->count());
  EXPECT_EQ(options_url, GetActiveUrl(browser()));

  // Test the extension that has the options page open in a view inside
  // chrome://extensions.
  // Triggering OpenOptionsPage() should create a new tab, since there are none
  // to override.
  options_url = GURL(std::string(chrome::kChromeUIExtensionsURL) +
                     "?options=" + options_in_view->id());
  EXPECT_TRUE(ExtensionTabUtil::OpenOptionsPage(options_in_view, browser()));
  EXPECT_EQ(5, browser()->tab_strip_model()->count());
  EXPECT_TRUE(content::WaitForLoadStop(
      browser()->tab_strip_model()->GetActiveWebContents()));
  EXPECT_EQ(options_url, GetActiveUrl(browser()));

  // Calling it a second time should not create a new tab, since one already
  // exists with that options page open.
  EXPECT_TRUE(ExtensionTabUtil::OpenOptionsPage(options_in_view, browser()));
  EXPECT_EQ(5, browser()->tab_strip_model()->count());
  EXPECT_TRUE(content::WaitForLoadStop(
      browser()->tab_strip_model()->GetActiveWebContents()));
  EXPECT_EQ(options_url, GetActiveUrl(browser()));

  // Navigate to chrome://extensions (no options). Calling OpenOptionsPage()
  // should override that tab rather than opening a new tab. crbug.com/41244380.
  ASSERT_TRUE(ui_test_utils::NavigateToURL(
      browser(), GURL(chrome::kChromeUIExtensionsURL)));
  EXPECT_TRUE(ExtensionTabUtil::OpenOptionsPage(options_in_view, browser()));
  EXPECT_EQ(5, browser()->tab_strip_model()->count());
  EXPECT_TRUE(content::WaitForLoadStop(
      browser()->tab_strip_model()->GetActiveWebContents()));
  EXPECT_EQ(options_url, GetActiveUrl(browser()));
}

IN_PROC_BROWSER_TEST_F(ExtensionTabUtilBrowserTest,
                       OpenSplitModeExtensionOptionsPageIncognito) {
  const Extension* options_split_extension =
      LoadExtension(test_data_dir_.AppendASCII("options_page_split_incognito"),
                    {.allow_in_incognito = true});
  ASSERT_TRUE(options_split_extension);
  ASSERT_TRUE(OptionsPageInfo::HasOptionsPage(options_split_extension));
  GURL options_url = OptionsPageInfo::GetOptionsPage(options_split_extension);

  BrowserWindowInterface* incognito = CreateIncognitoBrowser();

  // There should be two browser windows open, regular and incognito.
  EXPECT_EQ(2u, GlobalBrowserCollection::GetInstance()->GetSize());

  // In the regular browser window, start at the new tab page, and then open the
  // extension options page.
  ASSERT_TRUE(ui_test_utils::NavigateToURL(browser(),
                                           chrome::ChromeUINewTabURLAsGURL()));
  EXPECT_EQ(1, browser()->tab_strip_model()->count());
  EXPECT_TRUE(
      ExtensionTabUtil::OpenOptionsPage(options_split_extension, browser()));

  // Opening the options page should take the new tab and use it, so we should
  // have only one tab, and it should be open to the options page.
  EXPECT_EQ(1, browser()->tab_strip_model()->count());
  EXPECT_TRUE(content::WaitForLoadStop(
      browser()->tab_strip_model()->GetActiveWebContents()));
  EXPECT_EQ(options_url, GetActiveUrl(browser()));

  // If the options page is already opened from a regular window, calling
  // OpenOptionsPage() from an incognito window should not refocus to the
  // options page in the regular window, but instead open the options page in
  // the incognito window.
  ASSERT_TRUE(ui_test_utils::NavigateToURL(incognito,
                                           chrome::ChromeUINewTabURLAsGURL()));
  EXPECT_EQ(1, incognito->GetTabStripModel()->count());

  EXPECT_TRUE(
      OpenOptionsPageFromAPI(options_split_extension, incognito->GetProfile()));
  EXPECT_EQ(1, incognito->GetTabStripModel()->count());
  EXPECT_TRUE(content::WaitForLoadStop(
      incognito->GetTabStripModel()->GetActiveWebContents()));
  EXPECT_EQ(options_url, GetActiveUrl(incognito));

  // Both regular and incognito windows should have one tab each.
  EXPECT_EQ(1, browser()->tab_strip_model()->count());
  EXPECT_EQ(1, incognito->GetTabStripModel()->count());

  // Reset the incognito browser.
  CloseBrowserSynchronously(incognito);
  EXPECT_EQ(1u, GlobalBrowserCollection::GetInstance()->GetSize());
  incognito = CreateIncognitoBrowser();

  // Close the regular browser.
  CloseBrowserSynchronously(browser());
  EXPECT_EQ(1u, GlobalBrowserCollection::GetInstance()->GetSize());

  // In the incognito browser, start at the new tab page, and then open the
  // extension options page.
  ASSERT_TRUE(ui_test_utils::NavigateToURL(incognito,
                                           chrome::ChromeUINewTabURLAsGURL()));
  EXPECT_EQ(1, incognito->GetTabStripModel()->count());
  EXPECT_TRUE(
      OpenOptionsPageFromAPI(options_split_extension, incognito->GetProfile()));

  // Opening the options page should take the new tab and use it, so we should
  // have only one tab, and it should be open to the options page.
  EXPECT_EQ(1, incognito->GetTabStripModel()->count());
  EXPECT_TRUE(content::WaitForLoadStop(
      incognito->GetTabStripModel()->GetActiveWebContents()));
  EXPECT_EQ(options_url, GetActiveUrl(incognito));

  // Calling OpenOptionsPage again shouldn't result in any new tabs, since we
  // re-use the existing options page.
  EXPECT_TRUE(
      OpenOptionsPageFromAPI(options_split_extension, incognito->GetProfile()));
  EXPECT_EQ(1, incognito->GetTabStripModel()->count());
  EXPECT_TRUE(content::WaitForLoadStop(
      incognito->GetTabStripModel()->GetActiveWebContents()));
  EXPECT_EQ(options_url, GetActiveUrl(incognito));

  // Navigate to google.com (something non-newtab, non-options). Calling
  // OpenOptionsPage() should create a new tab and navigate it to the options
  // page. So we should have two total tabs, with the active tab pointing to
  // options.
  ASSERT_TRUE(
      ui_test_utils::NavigateToURL(incognito, GURL("http://www.google.com/")));
  EXPECT_TRUE(
      OpenOptionsPageFromAPI(options_split_extension, incognito->GetProfile()));
  EXPECT_EQ(2, incognito->GetTabStripModel()->count());
  EXPECT_TRUE(content::WaitForLoadStop(
      incognito->GetTabStripModel()->GetActiveWebContents()));
  EXPECT_EQ(options_url, GetActiveUrl(incognito));
}

IN_PROC_BROWSER_TEST_F(ExtensionTabUtilBrowserTest,
                       OpenSpanningModeExtensionOptionsPageIncognito) {
  const Extension* options_spanning_extension = LoadExtension(
      test_data_dir_.AppendASCII("options_page_spanning_incognito"),
      {.allow_in_incognito = true});
  ASSERT_TRUE(options_spanning_extension);
  ASSERT_TRUE(OptionsPageInfo::HasOptionsPage(options_spanning_extension));
  GURL options_url =
      OptionsPageInfo::GetOptionsPage(options_spanning_extension);

  // Start a regular browser window with two tabs, one that is non-options,
  // non-newtab and the other that is the options page.
  ASSERT_TRUE(
      ui_test_utils::NavigateToURL(browser(), GURL("https://www.google.com/")));
  EXPECT_TRUE(
      ExtensionTabUtil::OpenOptionsPage(options_spanning_extension, browser()));
  EXPECT_EQ(2, browser()->tab_strip_model()->count());
  EXPECT_TRUE(content::WaitForLoadStop(
      browser()->tab_strip_model()->GetActiveWebContents()));
  EXPECT_EQ(options_url, GetActiveUrl(browser()));
  // Switch to tab containing google.com such that it is the active tab.
  browser()->tab_strip_model()->SelectPreviousTab();
  EXPECT_EQ(GURL("https://www.google.com/"), GetActiveUrl(browser()));

  // Spanning mode extensions can never open pages in incognito so a regular
  // (non-OTR) profile must be used. If the options page is already opened from
  // a regular window, calling OpenOptionsPage() from an incognito window should
  // refocus to the options page in the regular window.
  BrowserWindowInterface* incognito = CreateIncognitoBrowser();
  ASSERT_TRUE(ui_test_utils::NavigateToURL(incognito,
                                           chrome::ChromeUINewTabURLAsGURL()));
  EXPECT_EQ(1, incognito->GetTabStripModel()->count());
  EXPECT_TRUE(OpenOptionsPageFromAPI(options_spanning_extension, profile()));
  // There should be two browser windows open, regular and incognito.
  EXPECT_EQ(2u, GlobalBrowserCollection::GetInstance()->GetSize());
  // Ensure that the regular browser is the foreground browser.
  EXPECT_EQ(browser(), GetLastActiveBrowserWindowInterfaceWithAnyProfile());
  // The options page in the regular window should be in focus instead of
  // the tab pointing to www.google.com.
  EXPECT_TRUE(content::WaitForLoadStop(
      browser()->tab_strip_model()->GetActiveWebContents()));
  EXPECT_EQ(options_url, GetActiveUrl(browser()));

  // Only the incognito browser should be left.
  CloseBrowserSynchronously(browser());
  EXPECT_EQ(1u, GlobalBrowserCollection::GetInstance()->GetSize());

  // Start at the new tab page in incognito and open the extension options page.
  auto browser_created_observer =
      std::make_optional<ui_test_utils::BrowserCreatedObserver>();
  ASSERT_TRUE(ui_test_utils::NavigateToURL(incognito,
                                           chrome::ChromeUINewTabURLAsGURL()));
  EXPECT_EQ(1, incognito->GetTabStripModel()->count());
  EXPECT_TRUE(OpenOptionsPageFromAPI(options_spanning_extension, profile()));
  BrowserWindowInterface* regular = browser_created_observer->Wait();

  // Opening the options page from an incognito window should open a new regular
  // profile window, which should have one tab open to the options page.
  ASSERT_EQ(2u, GlobalBrowserCollection::GetInstance()->GetSize());
  EXPECT_EQ(1, regular->GetTabStripModel()->count());
  EXPECT_TRUE(content::WaitForLoadStop(
      regular->GetTabStripModel()->GetActiveWebContents()));
  EXPECT_EQ(options_url, GetActiveUrl(regular));

  // Leave only incognito browser open.
  CloseBrowserSynchronously(regular);
  EXPECT_EQ(1u, GlobalBrowserCollection::GetInstance()->GetSize());

  // Right-clicking on an extension action icon in the toolbar and selecting
  // options should open the options page in a regular window. In this case, the
  // profile is an OTR profile instead of a non-OTR profile, as described above.
  browser_created_observer.emplace();
  ASSERT_TRUE(ui_test_utils::NavigateToURL(incognito,
                                           chrome::ChromeUINewTabURLAsGURL()));
  EXPECT_EQ(1, incognito->GetTabStripModel()->count());
  // Because the OpenOptionsPage() call originates from an OTR window via, e.g.
  // the action menu, instead of initiated by the extension, the
  // OpenOptionsPage() version that takes a BrowserWindowInterface* is used.
  EXPECT_TRUE(
      ExtensionTabUtil::OpenOptionsPage(options_spanning_extension, incognito));
  regular = browser_created_observer->Wait();
  // There should be two browser windows open, regular and incognito.
  EXPECT_EQ(2u, GlobalBrowserCollection::GetInstance()->GetSize());

  // Ensure that the regular browser is the foreground browser.
  ui_test_utils::WaitForBrowserSetLastActive(regular);
  EXPECT_EQ(2u, GlobalBrowserCollection::GetInstance()->GetSize());
  EXPECT_EQ(regular, GetLastActiveBrowserWindowInterfaceWithAnyProfile());

  EXPECT_EQ(1, regular->GetTabStripModel()->count());
  EXPECT_TRUE(content::WaitForLoadStop(
      regular->GetTabStripModel()->GetActiveWebContents()));
  EXPECT_EQ(options_url, GetActiveUrl(regular));
}

IN_PROC_BROWSER_TEST_F(ExtensionTabUtilBrowserTest, RecordNavigationScheme) {
  const std::string kHttpUrl("http://example.com");
  const std::string kHttpsUrl("https://example.com");
  const std::string kChromeUrl("chrome://settings");
  const std::string kFileUrl("file:///etc/passwd");
  const std::string kOtherUrl("data:,test");
  struct {
    std::string url;
    int expected_bucket;
  } test_cases[] = {
      {kHttpUrl, 0}, {kHttpsUrl, 0}, {kChromeUrl, 1},
      {kFileUrl, 2}, {kOtherUrl, 4},
  };
  std::string error;
  GURL url;

  const Extension* extension =
      LoadExtension(test_data_dir_.AppendASCII("simple_with_file"));
  ExtensionId id = extension->id();
  TestExtensionRegistryObserver observer(ExtensionRegistry::Get(profile()), id);
  // Allow file access. This will reload the extension, so we need to reset the
  // extension pointer.
  util::SetAllowFileAccess(id, profile(), true);
  extension = observer.WaitForExtensionLoaded().get();

  for (const auto& test_case : test_cases) {
    base::HistogramTester histogram_tester;
    auto result_url = ExtensionTabUtil::PrepareURLForNavigation(
        test_case.url, extension, profile());
    histogram_tester.ExpectBucketCount("Extensions.Navigation.Scheme",
                                       test_case.expected_bucket, 1);
  }
}

// TODO(405219902): Port test to desktop Android when we have support for
// creating a tab group from native code.
IN_PROC_BROWSER_TEST_F(ExtensionTabUtilBrowserTest, GetGroupById) {
  TabStripModel* tab_strip_model = browser()->tab_strip_model();
  ASSERT_EQ(1, tab_strip_model->count());
  ASSERT_TRUE(NavigateToURLInNewTab(GURL("about:blank")));
  ASSERT_EQ(2, tab_strip_model->count());

  tab_groups::TabGroupId group_id = tab_strip_model->AddToNewGroup({0, 1});

  const tab_groups::TabGroupVisualData new_data(
      u"Test", tab_groups::TabGroupColorId::kCyan);
  tab_strip_model->group_model()->GetTabGroup(group_id)->SetVisualData(
      new_data);

  int raw_group_id = ExtensionTabUtil::GetGroupId(group_id);

  WindowController* window = nullptr;
  tab_groups::TabGroupId found_id = tab_groups::TabGroupId::CreateEmpty();
  tab_groups::TabGroupVisualData visual_data;
  std::string error;
  bool found = ExtensionTabUtil::GetGroupById(
      raw_group_id, profile(),
      /*include_incognito=*/true, &window, &found_id, &visual_data, &error);

  EXPECT_TRUE(found);
  EXPECT_TRUE(window);
  EXPECT_EQ(group_id, found_id);
  EXPECT_EQ(visual_data.title(), u"Test");
  EXPECT_EQ(visual_data.color(), tab_groups::TabGroupColorId::kCyan);
  EXPECT_TRUE(error.empty());
}

class SharedTabGroupExtensionsTabUtilTest : public ExtensionTabUtilBrowserTest {
 public:
  SharedTabGroupExtensionsTabUtilTest() {
    feature_list_.InitWithFeatures(
        {
            data_sharing::features::kDataSharingFeature,
        },
        {});
  }

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

  // Adds tab navigated to |url| in the given |browser|.
  tabs::TabInterface* AddTab(const GURL& url) {
    return browser()->tab_strip_model()->GetTabForWebContents(
        content::WebContents::FromRenderFrameHost(NavigateToURLInNewTab(url)));
  }

  tab_groups::TabGroupId CreateTabGroup() {
    auto* tab_1 = AddTab(GURL("https://www.site1.com"));
    auto* tab_2 = AddTab(GURL("https://www.site2.com"));

    auto* tsm = browser()->tab_strip_model();

    return tsm->AddToNewGroup(
        {tsm->GetIndexOfTab(tab_1), tsm->GetIndexOfTab(tab_2)});
  }

  void ShareTabGroup(const tab_groups::TabGroupId& group_id,
                     const syncer::CollaborationId& collaboration_id) {
    tab_groups::TabGroupSyncService* service =
        static_cast<tab_groups::TabGroupSyncService*>(
            tab_groups::TabGroupSyncServiceFactory::GetForProfile(profile()));
    service->MakeTabGroupSharedForTesting(group_id, collaboration_id);
  }

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

IN_PROC_BROWSER_TEST_F(SharedTabGroupExtensionsTabUtilTest,
                       GetSharedGroupState) {
  auto group_id = CreateTabGroup();

  EXPECT_FALSE(ExtensionTabUtil::GetSharedStateOfGroup(group_id));
  EXPECT_FALSE(ExtensionTabUtil::CreateTabGroupObject(group_id)->shared);

  ShareTabGroup(group_id, syncer::CollaborationId("share_id"));

  EXPECT_TRUE(ExtensionTabUtil::GetSharedStateOfGroup(group_id));
  EXPECT_TRUE(ExtensionTabUtil::CreateTabGroupObject(group_id)->shared);
}
#endif  // BUILDFLAG(ENABLE_EXTENSIONS)

}  // namespace extensions
