// Copyright 2015 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/ui/extensions/extension_action_view_model.h"

#include <stddef.h>

#include <array>
#include <memory>

#include "base/command_line.h"
#include "base/functional/callback_helpers.h"
#include "base/memory/raw_ptr.h"
#include "base/run_loop.h"
#include "base/strings/string_util.h"
#include "base/strings/stringprintf.h"
#include "base/strings/utf_string_conversions.h"
#include "chrome/browser/extensions/chrome_test_extension_loader.h"
#include "chrome/browser/extensions/extension_action_dispatcher.h"
#include "chrome/browser/extensions/extension_action_runner.h"
#include "chrome/browser/extensions/extension_action_test_util.h"
#include "chrome/browser/extensions/extension_browsertest.h"
#include "chrome/browser/extensions/extension_util.h"
#include "chrome/browser/extensions/extension_view_host.h"
#include "chrome/browser/extensions/scoped_test_mv2_enabler.h"
#include "chrome/browser/extensions/test_extension_system.h"
#include "chrome/browser/extensions/user_script_listener.h"
#include "chrome/browser/profiles/profile.h"
#include "chrome/browser/ui/extensions/extension_action_delegate.h"
#include "chrome/browser/ui/extensions/extensions_container.h"
#include "chrome/browser/ui/extensions/icon_with_badge_image_source.h"
#include "chrome/browser/ui/toolbar/toolbar_actions_model.h"
#include "chrome/grit/generated_resources.h"
#include "components/sessions/content/session_tab_helper.h"
#include "content/public/test/browser_test.h"
#include "extensions/browser/extension_action.h"
#include "extensions/browser/extension_action_manager.h"
#include "extensions/browser/extension_registrar.h"
#include "extensions/browser/extension_registry.h"
#include "extensions/browser/load_error_reporter.h"
#include "extensions/browser/permissions/permissions_updater.h"
#include "extensions/browser/permissions/scripting_permissions_modifier.h"
#include "extensions/browser/permissions/site_permissions_helper.h"
#include "extensions/browser/permissions_manager.h"
#include "extensions/browser/test_extension_registry_observer.h"
#include "extensions/common/api/extension_action/action_info.h"
#include "extensions/common/extension_builder.h"
#include "extensions/common/extension_features.h"
#include "extensions/common/mojom/run_location.mojom-shared.h"
#include "extensions/test/permissions_manager_waiter.h"
#include "extensions/test/test_extension_dir.h"
#include "net/dns/mock_host_resolver.h"
#include "testing/gtest/include/gtest/gtest.h"
#include "ui/base/l10n/l10n_util.h"
#include "ui/base/models/image_model.h"
#include "ui/gfx/image/image_skia_rep.h"
#include "ui/native_theme/native_theme.h"

#if !BUILDFLAG(IS_ANDROID)
#include "chrome/browser/ui/views/extensions/extensions_toolbar_desktop.h"
#include "chrome/browser/ui/views/frame/browser_view.h"
#include "chrome/browser/ui/views/toolbar/toolbar_view.h"
#endif  // !BUILDFLAG(IS_ANDROID)

#if BUILDFLAG(ENABLE_EXTENSIONS)
#include "chrome/browser/extensions/api/side_panel/side_panel_service.h"
#include "chrome/common/extensions/api/side_panel.h"
#endif  // BUILDFLAG(ENABLE_EXTENSIONS)

using extensions::ExtensionBrowserTest;
using extensions::mojom::ManifestLocation;
using SiteInteraction = extensions::SitePermissionsHelper::SiteInteraction;
using UserSiteSetting = extensions::PermissionsManager::UserSiteSetting;
using HoverCardState = ToolbarActionViewModel::HoverCardState;

// The size to render an action icon for verification.
static constexpr gfx::Size kTestIconSize(64, 64);

class ExtensionActionViewModelBrowserTest : public ExtensionBrowserTest {
 public:
  ExtensionActionViewModelBrowserTest() = default;
  ExtensionActionViewModelBrowserTest(
      const ExtensionActionViewModelBrowserTest& other) = delete;
  ExtensionActionViewModelBrowserTest& operator=(
      const ExtensionActionViewModelBrowserTest& other) = delete;

  ~ExtensionActionViewModelBrowserTest() override = default;

  void Init() { AddTab(GURL(u"https://example.com")); }

  void SetUpOnMainThread() override {
    host_resolver()->AddRule("*", "127.0.0.1");
    ASSERT_TRUE(embedded_https_test_server().Start());
    ExtensionBrowserTest::SetUpOnMainThread();
  }

  void AddTab(GURL gurl) { NavigateToURLInNewTab(gurl); }

  void NavigateAndCommitActiveTab(const GURL& gurl) {
    // TODO: Do not ignore the return value. Somehow it returns false in some
    // cases, but tests pass.
    void(NavigateToURL(GetActiveWebContents(), gurl));
  }

  // Sets whether the given |action| wants to run on the |web_contents|.
  void SetActionWantsToRunOnTab(const std::string& action_id,
                                content::WebContents* web_contents,
                                bool wants_to_run) {
    auto* registry = extensions::ExtensionRegistry::Get(profile());
    scoped_refptr<const extensions::Extension> extension =
        registry->enabled_extensions().GetByID(action_id);
    CHECK(extension);
    extensions::ExtensionAction* action =
        extensions::ExtensionActionManager::Get(profile())->GetExtensionAction(
            *extension);
    CHECK(action);

    action->SetIsVisible(
        sessions::SessionTabHelper::IdForTab(web_contents).id(), wants_to_run);
    extensions::ExtensionActionDispatcher::Get(profile())->NotifyChange(
        action, web_contents, profile());
  }

  ExtensionActionViewModel* GetViewModelForId(const std::string& action_id) {
    // It's safe to static cast here, because these tests only deal with
    // extensions.
    return static_cast<ExtensionActionViewModel*>(
        ExtensionsContainer::From(*browser_window_interface())
            ->GetActionForId(action_id));
  }

  scoped_refptr<const extensions::Extension> CreateAndAddExtension(
      const std::string& name,
      extensions::ActionInfo::Type action_type) {
    return CreateAndAddExtensionWithGrantedHostPermissions(name, action_type,
                                                           {});
  }

  void GrantActivePermissions(const extensions::Extension* extension) {
    extensions::PermissionsUpdater(profile()).GrantActivePermissions(extension);
  }

  scoped_refptr<const extensions::Extension>
  CreateAndAddExtensionWithGrantedHostPermissions(
      const std::string& name,
      extensions::ActionInfo::Type action_type,
      const std::vector<std::string>& permissions) {
    scoped_refptr<const extensions::Extension> extension =
        extensions::ExtensionBuilder(name)
            .SetAction(action_type)
            .SetLocation(ManifestLocation::kInternal)
            .AddHostPermissions(permissions)
            .Build();

    if (!permissions.empty()) {
      GrantActivePermissions(extension.get());
    }

    extension_registrar()->AddExtension(extension.get());
    return extension;
  }

  extensions::ExtensionRegistrar* extension_registrar() {
    return extensions::ExtensionRegistrar::Get(profile());
  }

  ToolbarActionsModel* toolbar_model() {
    return ToolbarActionsModel::Get(profile());
  }

#if BUILDFLAG(ENABLE_EXTENSIONS)
  extensions::SidePanelService* side_panel_service() {
    return extensions::SidePanelService::Get(profile());
  }
#endif  // BUILDFLAG(ENABLE_EXTENSIONS)
};

// Temporary test class to test functionality while kExtensionsMenuAccessControl
// feature is being rolled out.
// TODO(crbug.com/40857680): Remove once feature is fully enabled.
class ExtensionActionViewModelFeatureRolloutBrowserTest
    : public ExtensionActionViewModelBrowserTest,
      public testing::WithParamInterface<bool> {
 public:
  ExtensionActionViewModelFeatureRolloutBrowserTest() {
    if (GetParam()) {
      feature_list_.InitAndEnableFeature(
          extensions_features::kExtensionsMenuAccessControl);
    } else {
      feature_list_.InitAndDisableFeature(
          extensions_features::kExtensionsMenuAccessControl);
    }
  }

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

INSTANTIATE_TEST_SUITE_P(,
                         ExtensionActionViewModelFeatureRolloutBrowserTest,
                         testing::Bool(),
                         [](const testing::TestParamInfo<bool>& info) {
                           return info.param ? "FeatureEnabled"
                                             : "FeatureDisabled";
                         });

// Tests the icon appearance of extension actions in the toolbar.
// Extensions that don't want to run should have their icons grayscaled.
IN_PROC_BROWSER_TEST_P(ExtensionActionViewModelFeatureRolloutBrowserTest,
                       ExtensionActionWantsToRunAppearance) {
  Init();
  const std::string id =
      CreateAndAddExtension("extension", extensions::ActionInfo::Type::kPage)
          ->id();

  content::WebContents* web_contents = GetActiveWebContents();
  ExtensionActionViewModel* const model = GetViewModelForId(id);
  ASSERT_TRUE(model);
  std::unique_ptr<IconWithBadgeImageSource> image_source =
      model->GetIconImageSourceForTesting(web_contents, kTestIconSize);
  EXPECT_TRUE(image_source->grayscale());
  EXPECT_FALSE(image_source->paint_blocked_actions_decoration());

  SetActionWantsToRunOnTab(model->GetId(), web_contents, true);
  image_source =
      model->GetIconImageSourceForTesting(web_contents, kTestIconSize);
  EXPECT_FALSE(image_source->grayscale());
  EXPECT_FALSE(image_source->paint_blocked_actions_decoration());
}

// Tests the appearance of browser actions with blocked script actions.
IN_PROC_BROWSER_TEST_P(ExtensionActionViewModelFeatureRolloutBrowserTest,
                       BrowserActionBlockedActions) {
  Init();
  auto extension = CreateAndAddExtensionWithGrantedHostPermissions(
      "browser_action", extensions::ActionInfo::Type::kBrowser,
      {"https://www.google.com/*"});

  extensions::ScriptingPermissionsModifier permissions_modifier(profile(),
                                                                extension);
  permissions_modifier.SetWithholdHostPermissions(true);

  AddTab(GURL("https://www.google.com/"));

  ExtensionActionViewModel* const model = GetViewModelForId(extension->id());
  ASSERT_TRUE(model);

  content::WebContents* web_contents = GetActiveWebContents();
  ASSERT_TRUE(web_contents);
  std::unique_ptr<IconWithBadgeImageSource> image_source =
      model->GetIconImageSourceForTesting(web_contents, kTestIconSize);
  EXPECT_FALSE(image_source->grayscale());
  EXPECT_FALSE(image_source->paint_blocked_actions_decoration());

  // Request a script injection, so it can be marked as blocked.
  extensions::ExtensionActionRunner* action_runner =
      extensions::ExtensionActionRunner::GetForWebContents(web_contents);
  ASSERT_TRUE(action_runner);
  action_runner->RequestScriptInjectionForTesting(
      extension.get(), extensions::mojom::RunLocation::kDocumentIdle,
      base::DoNothing());
  image_source =
      model->GetIconImageSourceForTesting(web_contents, kTestIconSize);
  EXPECT_FALSE(image_source->grayscale());
  // Browser action has a decoration only when the feature is disabled.
  bool is_feature_enabled = GetParam();
  EXPECT_EQ(image_source->paint_blocked_actions_decoration(),
            !is_feature_enabled);

  // Verify running the action removes the decoration, if existent.
  {
    extensions::PermissionsManagerWaiter waiter(
        extensions::PermissionsManager::Get(profile()));
    action_runner->RunForTesting(extension.get());
    waiter.WaitForActiveTabPermissionGranted(extension->id());
  }
  image_source =
      model->GetIconImageSourceForTesting(web_contents, kTestIconSize);
  EXPECT_FALSE(image_source->grayscale());
  EXPECT_FALSE(image_source->paint_blocked_actions_decoration());
}

// Tests the appearance of page actions with blocked script actions.
IN_PROC_BROWSER_TEST_P(ExtensionActionViewModelFeatureRolloutBrowserTest,
                       PageActionBlockedActions) {
  Init();
  auto extension = CreateAndAddExtensionWithGrantedHostPermissions(
      "page_action", extensions::ActionInfo::Type::kPage,
      {"https://www.google.com/*"});

  extensions::ScriptingPermissionsModifier permissions_modifier(profile(),
                                                                extension);
  permissions_modifier.SetWithholdHostPermissions(true);
  AddTab(GURL("https://www.google.com/"));

  ExtensionActionViewModel* const model = GetViewModelForId(extension->id());
  ASSERT_TRUE(model);

  content::WebContents* web_contents = GetActiveWebContents();
  std::unique_ptr<IconWithBadgeImageSource> image_source =
      model->GetIconImageSourceForTesting(web_contents, kTestIconSize);
  EXPECT_FALSE(image_source->grayscale());
  EXPECT_FALSE(image_source->paint_blocked_actions_decoration());

  extensions::ExtensionActionRunner* action_runner =
      extensions::ExtensionActionRunner::GetForWebContents(web_contents);
  action_runner->RequestScriptInjectionForTesting(
      extension.get(), extensions::mojom::RunLocation::kDocumentIdle,
      base::DoNothing());
  image_source =
      model->GetIconImageSourceForTesting(web_contents, kTestIconSize);
  EXPECT_FALSE(image_source->grayscale());
  // Page action has a decoration only when the feature is disabled.
  bool is_feature_enabled = GetParam();
  EXPECT_EQ(image_source->paint_blocked_actions_decoration(),
            !is_feature_enabled);

  // Simulate NativeTheme update after `image_source` is created.
  // `image_source` should paint fine without hitting use-after-free in such
  // case.  See http://crbug.com/40833823
  ui::NativeTheme::GetInstanceForNativeUi()->NotifyOnNativeThemeUpdated();
  image_source->GetImageForScale(1.0f);
}

// Tests the appearance of extension actions for extensions without a browser or
// page action defined in their manifest, but with host permissions on a page.
IN_PROC_BROWSER_TEST_P(ExtensionActionViewModelFeatureRolloutBrowserTest,
                       OnlyHostPermissionsAppearance) {
  Init();
  bool is_feature_enabled = GetParam();
  const std::u16string kWantsAccessTooltip =
      is_feature_enabled ? u"just hosts\nRequesting to read & change this site"
                         : u"just hosts\nWants access to this site";
  const std::u16string kHasAccessTooltip =
      is_feature_enabled ? u"just hosts\nAllowed to read & change this site"
                         : u"just hosts\nHas access to this site";
  const std::u16string kNoAccessTooltip = u"just hosts";

  scoped_refptr<const extensions::Extension> extension =
      extensions::ExtensionBuilder("just hosts")
          .SetLocation(ManifestLocation::kInternal)
          .AddHostPermission("https://www.google.com/*")
          .Build();

  GrantActivePermissions(extension.get());
  extension_registrar()->AddExtension(extension.get());
  extensions::ScriptingPermissionsModifier permissions_modifier(profile(),
                                                                extension);
  permissions_modifier.SetWithholdHostPermissions(true);

  ExtensionActionViewModel* const model = GetViewModelForId(extension->id());
  ASSERT_TRUE(model);

  // Initially load on a site that the extension doesn't have permissions to.
  AddTab(GURL("https://www.chromium.org/"));
  content::WebContents* web_contents = GetActiveWebContents();

  std::unique_ptr<IconWithBadgeImageSource> image_source =
      model->GetIconImageSourceForTesting(web_contents, kTestIconSize);
  EXPECT_TRUE(image_source->grayscale());
  EXPECT_FALSE(model->IsEnabled(web_contents));
  EXPECT_FALSE(image_source->paint_blocked_actions_decoration());
  EXPECT_EQ(kNoAccessTooltip, model->GetTooltip(web_contents));

  // Navigate to a url the extension does have permissions to. The extension is
  // set to run on click and has the current URL withheld, so it should not be
  // grayscaled and should be clickable.
  NavigateAndCommitActiveTab(GURL("https://www.google.com/"));
  image_source =
      model->GetIconImageSourceForTesting(web_contents, kTestIconSize);
  EXPECT_FALSE(image_source->grayscale());
  EXPECT_TRUE(model->IsEnabled(web_contents));
  EXPECT_FALSE(image_source->paint_blocked_actions_decoration());
  EXPECT_EQ(kWantsAccessTooltip, model->GetTooltip(web_contents));

  // After triggering the action it should have access, which is reflected in
  // the tooltip.
  {
    extensions::PermissionsManagerWaiter waiter(
        extensions::PermissionsManager::Get(profile()));
    model->ExecuteUserAction(
        ToolbarActionViewModel::InvocationSource::kToolbarButton);
    waiter.WaitForActiveTabPermissionGranted(extension->id());
  }
  image_source =
      model->GetIconImageSourceForTesting(web_contents, kTestIconSize);
  EXPECT_FALSE(image_source->grayscale());
  EXPECT_FALSE(model->IsEnabled(web_contents));
  EXPECT_FALSE(image_source->paint_blocked_actions_decoration());
  EXPECT_EQ(kHasAccessTooltip, model->GetTooltip(web_contents));
}

IN_PROC_BROWSER_TEST_P(ExtensionActionViewModelFeatureRolloutBrowserTest,
                       ExtensionActionContextMenuVisibility) {
  Init();
  bool is_feature_enabled = GetParam();
  const std::u16string pin_label = l10n_util::GetStringUTF16(
      is_feature_enabled ? IDS_EXTENSIONS_CONTEXT_MENU_PIN_TO_TOOLBAR
                         : IDS_EXTENSIONS_PIN_TO_TOOLBAR);
  const std::u16string unpin_label = l10n_util::GetStringUTF16(
      is_feature_enabled ? IDS_EXTENSIONS_CONTEXT_MENU_UNPIN_FROM_TOOLBAR
                         : IDS_EXTENSIONS_UNPIN_FROM_TOOLBAR);

  std::string id =
      CreateAndAddExtension("extension", extensions::ActionInfo::Type::kBrowser)
          ->id();

  // Check that the context menu has the proper string for the action's pinned
  // state.
  auto check_visibility_string = [](ToolbarActionViewModel* action,
                                    std::u16string expected_label) {
    ui::SimpleMenuModel* context_menu = static_cast<ui::SimpleMenuModel*>(
        action->GetContextMenu(extensions::ExtensionContextMenuModel::
                                   ContextMenuSource::kToolbarAction));
    std::optional<size_t> visibility_index = context_menu->GetIndexOfCommandId(
        extensions::ExtensionContextMenuModel::TOGGLE_VISIBILITY);
    ASSERT_TRUE(visibility_index.has_value());
    std::u16string actual_label =
        context_menu->GetLabelAt(visibility_index.value());
    EXPECT_EQ(expected_label, actual_label);
  };

  ExtensionActionViewModel* const model = GetViewModelForId(id);
  ASSERT_TRUE(model);

  // Default state: unpinned.
  check_visibility_string(model, pin_label);

  // Pin the extension; re-check.
  toolbar_model()->SetActionVisibility(id, true);
  check_visibility_string(model, unpin_label);

#if !BUILDFLAG(IS_ANDROID)
  // Unpin the extension and ephemerally pop it out.
  // We skip this test on Android because popping out an extension via the
  // direct container access is not supported.
  ExtensionsToolbarDesktop* toolbar =
      BrowserView::GetBrowserViewForBrowser(browser())
          ->toolbar()
          ->extensions_container();
  toolbar_model()->SetActionVisibility(id, false);
  EXPECT_FALSE(toolbar->IsActionVisibleOnToolbar(id));
  base::RunLoop run_loop;
  toolbar->PopOutAction(id, run_loop.QuitClosure());
  EXPECT_TRUE(toolbar->IsActionVisibleOnToolbar(id));
  // The string should still just be "pin".
  check_visibility_string(model, pin_label);
#endif  // !BUILDFLAG(IS_ANDROID)
}

enum class PermissionType {
  kScriptableHost,
  kExplicitHost,
};

// TODO(crbug.com/40857680): This test used to be parameterized for
// PermissionType. However, parent class needs to parameterize the test for
// feature kExtensionsMenuAccessControl. Once feature is fully rolled out, we
// can go back to testing::WithParamInterface<PermissionType>.
class ExtensionActionViewModelGrayscaleTest
    : public ExtensionActionViewModelFeatureRolloutBrowserTest {
 public:
  enum class ActionState {
    kEnabled,
    kDisabled,
  };

  enum class PageAccessStatus {
    // The extension has been granted permission to the host.
    kGranted,
    // The extension had the host withheld and it has not tried to access the
    // page.
    kWithheld,
    // The extension had the host withheld and it has been blocked when trying
    // to access the page.
    kBlocked,
    // The extension has not been granted permissions to the host, nor was it
    // withheld.
    kNone,
  };

  enum class Coloring {
    // The extension action color is grayscale.
    kGrayscale,
    // The extension action has full color.
    kFull,
  };

  enum class BlockedDecoration {
    // The extension is blocked, thus its action has painted decoration.
    kPainted,
    // The extension is not blocked, thus its action has no painted decoration.
    kNotPainted,
  };

  ExtensionActionViewModelGrayscaleTest() = default;

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

  ~ExtensionActionViewModelGrayscaleTest() override = default;

  scoped_refptr<const extensions::Extension> CreateExtension(
      PermissionType permission_type,
      const std::string& host_permission);

  extensions::PermissionsData::PageAccess GetPageAccess(
      content::WebContents* web_contents,
      scoped_refptr<const extensions::Extension> extensions,
      PermissionType permission_type);
};

scoped_refptr<const extensions::Extension>
ExtensionActionViewModelGrayscaleTest::CreateExtension(
    PermissionType permission_type,
    const std::string& host_permission) {
  extensions::ExtensionBuilder builder("extension");
  builder.SetAction(extensions::ActionInfo::Type::kBrowser)
      .SetLocation(ManifestLocation::kInternal);
  switch (permission_type) {
    case PermissionType::kScriptableHost:
      builder.AddContentScript("script.js", {host_permission});
      break;
    case PermissionType::kExplicitHost:
      builder.AddHostPermission(host_permission);
      break;
  }

  return builder.Build();
}

extensions::PermissionsData::PageAccess
ExtensionActionViewModelGrayscaleTest::GetPageAccess(
    content::WebContents* web_contents,
    scoped_refptr<const extensions::Extension> extension,
    PermissionType permission_type) {
  int tab_id = sessions::SessionTabHelper::IdForTab(web_contents).id();
  GURL url = web_contents->GetLastCommittedURL();
  switch (permission_type) {
    case PermissionType::kExplicitHost:
      return extension->permissions_data()->GetPageAccess(url, tab_id,
                                                          /*error=*/nullptr);
    case PermissionType::kScriptableHost:
      return extension->permissions_data()->GetContentScriptAccess(
          url, tab_id, /*error=*/nullptr);
  }
}

INSTANTIATE_TEST_SUITE_P(,
                         ExtensionActionViewModelGrayscaleTest,
                         testing::Bool(),
                         [](const testing::TestParamInfo<bool>& info) {
                           return info.param ? "FeatureEnabled"
                                             : "FeatureDisabled";
                         });

// Tests the behavior for icon grayscaling.
IN_PROC_BROWSER_TEST_P(ExtensionActionViewModelGrayscaleTest, GrayscaleIcon) {
  Init();

  bool is_feature_enabled = GetParam();
  static constexpr PermissionType permission_types[] = {
      PermissionType::kScriptableHost, PermissionType::kExplicitHost};

  for (auto permission_type : permission_types) {
    // Create an extension with google.com as either an explicit or scriptable
    // host permission.
    std::string host_permission = "https://www.google.com/*";
    scoped_refptr<const extensions::Extension> extension =
        CreateExtension(permission_type, host_permission);
    GrantActivePermissions(extension.get());
    extension_registrar()->AddExtension(extension.get());

    extensions::ScriptingPermissionsModifier permissions_modifier(profile(),
                                                                  extension);
    permissions_modifier.SetWithholdHostPermissions(true);
    const GURL kHasPermissionUrl("https://www.google.com/");
    const GURL kNoPermissionsUrl("https://www.chromium.org/");

    // Make sure UserScriptListener doesn't hold up the navigation.
    extensions::ExtensionsBrowserClient::Get()
        ->GetUserScriptListener()
        ->TriggerUserScriptsReadyForTesting(profile());

    // Load up a page that we will navigate for the different test cases.
    AddTab(GURL("about:blank"));

    // Note: Action can only have a blocked decoration if the feature is
    // disabled.
    struct TestCases {
      ActionState action_state;
      PageAccessStatus page_access;
      Coloring expected_coloring;
      BlockedDecoration expected_blocked_decoration;
    };
    static auto test_cases = std::to_array<TestCases>({
        {ActionState::kEnabled, PageAccessStatus::kNone, Coloring::kFull,
         BlockedDecoration::kNotPainted},
        {ActionState::kEnabled, PageAccessStatus::kWithheld, Coloring::kFull,
         BlockedDecoration::kNotPainted},
        {ActionState::kEnabled, PageAccessStatus::kBlocked, Coloring::kFull,
         is_feature_enabled ? BlockedDecoration::kNotPainted
                            : BlockedDecoration::kPainted},
        {ActionState::kEnabled, PageAccessStatus::kGranted, Coloring::kFull,
         BlockedDecoration::kNotPainted},

        {ActionState::kDisabled, PageAccessStatus::kNone, Coloring::kGrayscale,
         BlockedDecoration::kNotPainted},
        {ActionState::kDisabled, PageAccessStatus::kWithheld, Coloring::kFull,
         BlockedDecoration::kNotPainted},
        {ActionState::kDisabled, PageAccessStatus::kBlocked, Coloring::kFull,
         is_feature_enabled ? BlockedDecoration::kNotPainted
                            : BlockedDecoration::kPainted},
        {ActionState::kDisabled, PageAccessStatus::kGranted, Coloring::kFull,
         BlockedDecoration::kNotPainted},
    });

    ExtensionActionViewModel* const model = GetViewModelForId(extension->id());
    ASSERT_TRUE(model);
    content::WebContents* web_contents = GetActiveWebContents();
    extensions::ExtensionAction* extension_action =
        extensions::ExtensionActionManager::Get(profile())->GetExtensionAction(
            *extension);
    extensions::ExtensionActionRunner* action_runner =
        extensions::ExtensionActionRunner::GetForWebContents(web_contents);
    int tab_id = sessions::SessionTabHelper::IdForTab(web_contents).id();

    for (size_t i = 0; i < std::size(test_cases); ++i) {
      SCOPED_TRACE(
          base::StringPrintf("Running test case %d", static_cast<int>(i)));
      const auto& test_case = test_cases[i];

      // Set up the proper state for the test case.
      switch (test_case.page_access) {
        case PageAccessStatus::kNone: {
          NavigateAndCommitActiveTab(kNoPermissionsUrl);
          // Page access should be denied; verify.
          extensions::PermissionsData::PageAccess page_access =
              GetPageAccess(web_contents, extension, permission_type);
          EXPECT_EQ(extensions::PermissionsData::PageAccess::kDenied,
                    page_access);
          break;
        }
        case PageAccessStatus::kWithheld: {
          NavigateAndCommitActiveTab(kHasPermissionUrl);
          // Page access should already be withheld; verify.
          extensions::PermissionsData::PageAccess page_access =
              GetPageAccess(web_contents, extension, permission_type);
          EXPECT_EQ(extensions::PermissionsData::PageAccess::kWithheld,
                    page_access);
          break;
        }
        case PageAccessStatus::kBlocked:
          // Navigate to a page where the permission is currently withheld and
          // try to inject a script.
          NavigateAndCommitActiveTab(kHasPermissionUrl);
          action_runner->RequestScriptInjectionForTesting(
              extension.get(), extensions::mojom::RunLocation::kDocumentIdle,
              base::DoNothing());
          break;
        case PageAccessStatus::kGranted:
          // Grant the withheld requested permission and navigate there.
          permissions_modifier.GrantHostPermission(kHasPermissionUrl);
          NavigateAndCommitActiveTab(kHasPermissionUrl);
          break;
      }

      // Enable or disable the action based on the test case.
      extension_action->SetIsVisible(
          tab_id, test_case.action_state == ActionState::kEnabled);

      std::unique_ptr<IconWithBadgeImageSource> image_source =
          model->GetIconImageSourceForTesting(web_contents, kTestIconSize);
      EXPECT_EQ(test_case.expected_coloring == Coloring::kGrayscale,
                image_source->grayscale());
      EXPECT_EQ(
          test_case.expected_blocked_decoration == BlockedDecoration::kPainted,
          image_source->paint_blocked_actions_decoration());

      // Clean up permissions state.
      if (test_case.page_access == PageAccessStatus::kGranted) {
        permissions_modifier.RemoveGrantedHostPermission(kHasPermissionUrl);
      }
      action_runner->ClearInjectionsForTesting(*extension);
    }
  }
}

IN_PROC_BROWSER_TEST_P(ExtensionActionViewModelFeatureRolloutBrowserTest,
                       RuntimeHostsTooltip) {
  Init();
  bool is_feature_enabled = GetParam();
  const std::u16string kWantsAccessTooltip =
      is_feature_enabled
          ? u"extension name\nRequesting to read & change this site"
          : u"extension name\nWants access to this site";
  const std::u16string kHasAccessTooltip =
      is_feature_enabled ? u"extension name\nAllowed to read & change this site"
                         : u"extension name\nHas access to this site";

  auto extension = CreateAndAddExtensionWithGrantedHostPermissions(
      "extension name", extensions::ActionInfo::Type::kBrowser,
      {"https://www.google.com/*"});

  extensions::ScriptingPermissionsModifier permissions_modifier(profile(),
                                                                extension);
  permissions_modifier.SetWithholdHostPermissions(true);
  const GURL kUrl("https://www.google.com/");
  AddTab(kUrl);

  ExtensionActionViewModel* const model = GetViewModelForId(extension->id());
  ASSERT_TRUE(model);
  content::WebContents* web_contents = GetActiveWebContents();
  int tab_id = sessions::SessionTabHelper::IdForTab(web_contents).id();

  // Page access should already be withheld.
  EXPECT_EQ(extensions::PermissionsData::PageAccess::kWithheld,
            extension->permissions_data()->GetPageAccess(kUrl, tab_id,
                                                         /*error=*/nullptr));
  EXPECT_EQ(kWantsAccessTooltip, model->GetTooltip(web_contents));

  // Request access.
  extensions::ExtensionActionRunner* action_runner =
      extensions::ExtensionActionRunner::GetForWebContents(web_contents);
  action_runner->RequestScriptInjectionForTesting(
      extension.get(), extensions::mojom::RunLocation::kDocumentIdle,
      base::DoNothing());
  EXPECT_EQ(kWantsAccessTooltip, model->GetTooltip(web_contents));

  // Grant access.
  action_runner->ClearInjectionsForTesting(*extension);
  permissions_modifier.GrantHostPermission(kUrl);
  EXPECT_EQ(kHasAccessTooltip, model->GetTooltip(web_contents));
}

// Tests the appearance of extension actions for an extension with the activeTab
// permission and no browser or page action defined in their manifest.
IN_PROC_BROWSER_TEST_P(ExtensionActionViewModelFeatureRolloutBrowserTest,
                       ActiveTabIconAppearance) {
  Init();
  bool is_feature_enabled = GetParam();
  const std::u16string kWantsAccessTooltip =
      is_feature_enabled ? u"active tab\nRequesting to read & change this site"
                         : u"active tab\nWants access to this site";
  const std::u16string kHasAccessTooltip =
      is_feature_enabled ? u"active tab\nAllowed to read & change this site"
                         : u"active tab\nHas access to this site";
  const std::u16string kNoAccessTooltip = u"active tab";

  const GURL kUnlistedHost("https://www.example.com");
  const GURL kGrantedHost("https://www.google.com");
  const GURL kRestrictedHost("chrome://extensions");
  scoped_refptr<const extensions::Extension> extension =
      extensions::ExtensionBuilder("active tab")
          .AddAPIPermission("activeTab")
          .AddHostPermission(kGrantedHost.spec())
          .Build();
  GrantActivePermissions(extension.get());
  extension_registrar()->AddExtension(extension.get());

  // Navigate the browser to a site the extension doesn't have explicit access
  // to and verify the expected appearance.
  AddTab(kUnlistedHost);
  ExtensionActionViewModel* const model = GetViewModelForId(extension->id());
  ASSERT_TRUE(model);
  content::WebContents* web_contents = GetActiveWebContents();

  {
    EXPECT_EQ(SiteInteraction::kActiveTab,
              model->GetSiteInteraction(web_contents));
    EXPECT_TRUE(model->IsEnabled(web_contents));
    std::unique_ptr<IconWithBadgeImageSource> image_source =
        model->GetIconImageSourceForTesting(web_contents, kTestIconSize);
    EXPECT_FALSE(image_source->grayscale());
    EXPECT_FALSE(image_source->paint_blocked_actions_decoration());
    EXPECT_EQ(kWantsAccessTooltip, model->GetTooltip(web_contents));
  }

  // Navigate to a site which the extension does have explicit host access to
  // and verify the expected appearance.
  NavigateAndCommitActiveTab(kGrantedHost);
  {
    EXPECT_EQ(SiteInteraction::kGranted,
              model->GetSiteInteraction(web_contents));
    // This is a little unintuitive, but if an extension is using a page action
    // and has not specified any declarative rules or manually changed it's
    // enabled state, it can have access to a page but be in the disabled state.
    // The icon will still be colored however.
    EXPECT_FALSE(model->IsEnabled(web_contents));
    std::unique_ptr<IconWithBadgeImageSource> image_source =
        model->GetIconImageSourceForTesting(web_contents, kTestIconSize);
    EXPECT_FALSE(image_source->grayscale());
    EXPECT_FALSE(image_source->paint_blocked_actions_decoration());
    EXPECT_EQ(kHasAccessTooltip, model->GetTooltip(web_contents));
  }

  // Navigate to a restricted URL and verify the expected appearance.
  NavigateAndCommitActiveTab(kRestrictedHost);
  {
    EXPECT_EQ(SiteInteraction::kNone, model->GetSiteInteraction(web_contents));
    EXPECT_FALSE(model->IsEnabled(web_contents));
    std::unique_ptr<IconWithBadgeImageSource> image_source =
        model->GetIconImageSourceForTesting(web_contents, kTestIconSize);
    EXPECT_TRUE(image_source->grayscale());
    EXPECT_FALSE(image_source->paint_blocked_actions_decoration());
    EXPECT_EQ(kNoAccessTooltip, model->GetTooltip(web_contents));
  }
}

// Tests that an extension with the activeTab permission has active tab site
// interaction except for restricted URLs.
IN_PROC_BROWSER_TEST_P(ExtensionActionViewModelFeatureRolloutBrowserTest,
                       GetSiteInteractionWithActiveTab) {
  Init();
  // Note: Not using `CreateAndAddExtensionWithGrantedHostPermissions` because
  // this adds an API permission (activeTab).
  scoped_refptr<const extensions::Extension> extension =
      extensions::ExtensionBuilder("active tab")
          .SetAction(extensions::ActionInfo::Type::kAction)
          .SetLocation(ManifestLocation::kInternal)
          .AddAPIPermission("activeTab")
          .Build();
  GrantActivePermissions(extension.get());
  extension_registrar()->AddExtension(extension.get());

  // Navigate the browser to google.com. Since clicking the extension would
  // grant access to the page, the page interaction status should show as
  // "pending".
  AddTab(GURL("https://www.google.com/"));
  ExtensionActionViewModel* const model = GetViewModelForId(extension->id());
  ASSERT_TRUE(model);
  content::WebContents* web_contents = GetActiveWebContents();

  EXPECT_EQ(SiteInteraction::kActiveTab,
            model->GetSiteInteraction(web_contents));

  // Click on the action, which grants activeTab and allows the extension to
  // access the page. This changes the page interaction status to "granted".
  {
    extensions::PermissionsManagerWaiter waiter(
        extensions::PermissionsManager::Get(profile()));
    model->ExecuteUserAction(
        ToolbarActionViewModel::InvocationSource::kToolbarButton);
    waiter.WaitForActiveTabPermissionGranted(extension->id());
  }
  EXPECT_EQ(SiteInteraction::kGranted, model->GetSiteInteraction(web_contents));

  // Now navigate to a restricted URL. Clicking the extension won't give access
  // here, so the page interaction status should be "none".
  NavigateAndCommitActiveTab(GURL("chrome://extensions"));
  EXPECT_EQ(SiteInteraction::kNone, model->GetSiteInteraction(web_contents));
  {
    extensions::PermissionsManagerWaiter waiter(
        extensions::PermissionsManager::Get(profile()));
    model->ExecuteUserAction(
        ToolbarActionViewModel::InvocationSource::kToolbarButton);
    waiter.WaitForActiveTabPermissionGranted(extension->id());
  }
  EXPECT_EQ(SiteInteraction::kNone, model->GetSiteInteraction(web_contents));
}

// Tests that file URLs only have active tab site interaction if the extension
// has active tab permission and file URL access.
IN_PROC_BROWSER_TEST_P(ExtensionActionViewModelFeatureRolloutBrowserTest,
                       GetSiteInteractionActiveTabWithFileURL) {
  // TODO(https://crbug.com/40804030): Remove this when updated to use MV3.
  extensions::ScopedTestMV2Enabler mv2_enabler;

  Init();
  // We need to use a TestExtensionDir here to allow for the reload when giving
  // an extension file URL access.
  extensions::TestExtensionDir test_dir;
  test_dir.WriteManifest(R"(
    {
      "name": "Active Tab Page Interaction with File URLs",
      "description": "Testing SiteInteraction and ActiveTab on file URLs",
      "version": "0.1",
      "manifest_version": 2,
      "browser_action": {},
      "permissions": ["activeTab"]
    })");
  extensions::ChromeTestExtensionLoader loader(profile());
  loader.set_allow_file_access(false);
  scoped_refptr<const extensions::Extension> extension =
      loader.LoadExtension(test_dir.UnpackedPath());

  // Navigate to a file URL. The page interaction status should be "none", as
  // the extension doesn't have file URL access granted. Clicking it should
  // result in no change.
  AddTab(GURL("file://foo"));
  ExtensionActionViewModel* model = GetViewModelForId(extension->id());
  ASSERT_TRUE(model);
  content::WebContents* web_contents = GetActiveWebContents();

  EXPECT_EQ(SiteInteraction::kNone, model->GetSiteInteraction(web_contents));
  {
    extensions::PermissionsManagerWaiter waiter(
        extensions::PermissionsManager::Get(profile()));
    model->ExecuteUserAction(
        ToolbarActionViewModel::InvocationSource::kToolbarButton);
    waiter.WaitForActiveTabPermissionGranted(extension->id());
  }
  EXPECT_EQ(SiteInteraction::kNone, model->GetSiteInteraction(web_contents));

  // After being granted access to file URLs the page interaction status should
  // show as "pending". A click will grant activeTab, giving access to the page
  // and will change the page interaction status to "active".
  extensions::TestExtensionRegistryObserver observer(
      extensions::ExtensionRegistry::Get(profile()), extension->id());
  extensions::util::SetAllowFileAccess(extension->id(), profile(),
                                       true /*allow*/);
  extension = observer.WaitForExtensionLoaded();
  ASSERT_TRUE(extension);
  // Refresh the model as the extension has been reloaded.
  model = GetViewModelForId(extension->id());
  EXPECT_EQ(SiteInteraction::kActiveTab,
            model->GetSiteInteraction(web_contents));
  {
    extensions::PermissionsManagerWaiter waiter(
        extensions::PermissionsManager::Get(profile()));
    model->ExecuteUserAction(
        ToolbarActionViewModel::InvocationSource::kToolbarButton);
    waiter.WaitForActiveTabPermissionGranted(extension->id());
  }
  EXPECT_EQ(SiteInteraction::kGranted, model->GetSiteInteraction(web_contents));
}

// ExtensionActionViewModel::GetIcon() can potentially be called with a
// null web contents if the tab strip model doesn't know of an active tab
// (though it's a bit unclear when this is the case).
// See https://crbug.com/41416544
IN_PROC_BROWSER_TEST_P(ExtensionActionViewModelFeatureRolloutBrowserTest,
                       TestGetIconWithNullWebContents) {
  Init();
  auto extension = CreateAndAddExtensionWithGrantedHostPermissions(
      "extension name", extensions::ActionInfo::Type::kAction,
      {"https://example.com/"});

  extensions::ScriptingPermissionsModifier permissions_modifier(profile(),
                                                                extension);
  permissions_modifier.SetWithholdHostPermissions(true);

  // Try getting an icon with no active web contents. Nothing should crash, and
  // a non-empty icon should be returned.
  ExtensionActionViewModel* const model = GetViewModelForId(extension->id());
  ui::ImageModel icon = model->GetIcon(nullptr, kTestIconSize);
  EXPECT_FALSE(icon.IsEmpty());
}

class ExtensionActionViewModelFeatureBrowserTest
    : public ExtensionActionViewModelBrowserTest {
 public:
  ExtensionActionViewModelFeatureBrowserTest() {
    scoped_feature_list_.InitAndEnableFeature(
        extensions_features::kExtensionsMenuAccessControl);
  }
  ~ExtensionActionViewModelFeatureBrowserTest() override = default;
  ExtensionActionViewModelFeatureBrowserTest(
      const ExtensionActionViewModelFeatureBrowserTest&) = delete;
  ExtensionActionViewModelFeatureBrowserTest& operator=(
      const ExtensionActionViewModelFeatureBrowserTest&) = delete;

  HoverCardState::SiteAccess GetHoverCardSiteAccessState(
      ExtensionActionViewModel* model,
      content::WebContents* web_contents) {
    return model->GetHoverCardState(web_contents).site_access;
  }

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

// Tests hover card status after changing user site settings and site access.
IN_PROC_BROWSER_TEST_F(ExtensionActionViewModelFeatureBrowserTest,
                       GetHoverCardStatus) {
  Init();
  std::string url_string = "https://example.com/";

  const GURL gurl =
      embedded_https_test_server().GetURL("example.com", "/simple.html");

  auto extensionA = CreateAndAddExtension(
      "Extension A", extensions::ActionInfo::Type::kAction);
  auto extensionB = CreateAndAddExtensionWithGrantedHostPermissions(
      "Extension B", extensions::ActionInfo::Type::kAction, {url_string});
  auto extensionC = CreateAndAddExtensionWithGrantedHostPermissions(
      "Extension c", extensions::ActionInfo::Type::kAction, {url_string});

  AddTab(gurl);

  content::WebContents* web_contents = GetActiveWebContents();
  ASSERT_TRUE(web_contents);
  auto url = url::Origin::Create(web_contents->GetLastCommittedURL());

  ExtensionActionViewModel* const modelA = GetViewModelForId(extensionA->id());
  ASSERT_TRUE(modelA);
  ExtensionActionViewModel* const modelB = GetViewModelForId(extensionB->id());
  ASSERT_TRUE(modelB);
  ExtensionActionViewModel* const modelC = GetViewModelForId(extensionC->id());
  ASSERT_TRUE(modelC);

  // By default, user site setting is "customize by extension" and site access
  // is granted to every extension that requests them. Thus, verify extension A
  // hover card state is "does not want access" and the rest is "have access".
  auto* permissions_manager = extensions::PermissionsManager::Get(profile());
  ASSERT_EQ(permissions_manager->GetUserSiteSetting(url),
            UserSiteSetting::kCustomizeByExtension);
  EXPECT_EQ(GetHoverCardSiteAccessState(modelA, web_contents),
            HoverCardState::SiteAccess::kExtensionDoesNotWantAccess);
  EXPECT_EQ(GetHoverCardSiteAccessState(modelB, web_contents),
            HoverCardState::SiteAccess::kExtensionHasAccess);
  EXPECT_EQ(GetHoverCardSiteAccessState(modelC, web_contents),
            HoverCardState::SiteAccess::kExtensionHasAccess);

  // Withhold extension C host permissions. Verify only extension C changed
  // hover card state to "requests access".
  extensions::ScriptingPermissionsModifier(profile(), extensionC)
      .SetWithholdHostPermissions(true);
  EXPECT_EQ(GetHoverCardSiteAccessState(modelA, web_contents),
            HoverCardState::SiteAccess::kExtensionDoesNotWantAccess);
  EXPECT_EQ(GetHoverCardSiteAccessState(modelB, web_contents),
            HoverCardState::SiteAccess::kExtensionHasAccess);
  EXPECT_EQ(GetHoverCardSiteAccessState(modelC, web_contents),
            HoverCardState::SiteAccess::kExtensionRequestsAccess);

  // Block all extensions site access. Verify all extensions appear as "all
  // extensions blocked" (even though extension A never requested access).
  permissions_manager->UpdateUserSiteSetting(
      url, UserSiteSetting::kBlockAllExtensions);
  EXPECT_EQ(GetHoverCardSiteAccessState(modelA, web_contents),
            HoverCardState::SiteAccess::kAllExtensionsBlocked);
  EXPECT_EQ(GetHoverCardSiteAccessState(modelB, web_contents),
            HoverCardState::SiteAccess::kAllExtensionsBlocked);
  EXPECT_EQ(GetHoverCardSiteAccessState(modelC, web_contents),
            HoverCardState::SiteAccess::kAllExtensionsBlocked);

  // Change back to customize site access by extension. Verify extension A
  // hover card state is "does not want access", extension B is "has access" and
  // extension C is "requests access".
  permissions_manager->UpdateUserSiteSetting(
      url, UserSiteSetting::kCustomizeByExtension);
  EXPECT_EQ(GetHoverCardSiteAccessState(modelA, web_contents),
            HoverCardState::SiteAccess::kExtensionDoesNotWantAccess);
  EXPECT_EQ(GetHoverCardSiteAccessState(modelB, web_contents),
            HoverCardState::SiteAccess::kExtensionHasAccess);
  EXPECT_EQ(GetHoverCardSiteAccessState(modelC, web_contents),
            HoverCardState::SiteAccess::kExtensionRequestsAccess);
}

// Tests correct tooltip text after changing user site settings and site access.
IN_PROC_BROWSER_TEST_F(ExtensionActionViewModelFeatureBrowserTest, GetTooltip) {
  Init();
  std::u16string extension_name = u"Extension";
  std::string requested_url_string = "https://a.com/";
  const GURL requested_gurl =
      embedded_https_test_server().GetURL("a.com", "/simple.html");
  const GURL not_requested_gurl =
      embedded_https_test_server().GetURL("b.com", "/simple.html");
  auto extension = CreateAndAddExtensionWithGrantedHostPermissions(
      base::UTF16ToUTF8(extension_name), extensions::ActionInfo::Type::kAction,
      {requested_url_string});

  // Navigate to a site the extension requests access to.
  AddTab(requested_gurl);
  content::WebContents* web_contents = GetActiveWebContents();
  ASSERT_TRUE(web_contents);
  auto requested_url = url::Origin::Create(web_contents->GetLastCommittedURL());

  ExtensionActionViewModel* const model = GetViewModelForId(extension->id());
  ASSERT_TRUE(model);

  // By default, user site setting is "customize by extension" and site access
  // is granted to every extension that requests them. Verify extension tooltip
  // is "has access".
  auto* permissions_manager = extensions::PermissionsManager::Get(profile());
  ASSERT_EQ(permissions_manager->GetUserSiteSetting(requested_url),
            UserSiteSetting::kCustomizeByExtension);
  EXPECT_EQ(
      model->GetTooltip(web_contents),
      base::JoinString(
          {extension_name,
           l10n_util::GetStringUTF16(
               IDS_EXTENSIONS_MENU_MAIN_PAGE_EXTENSION_BUTTON_HAS_ACCESS_TOOLTIP)},
          u"\n"));

  // Withhold extension host permissions. Verify extension tooltip is "requests
  // access".
  extensions::ScriptingPermissionsModifier(profile(), extension)
      .SetWithholdHostPermissions(true);
  EXPECT_EQ(
      model->GetTooltip(web_contents),
      base::JoinString(
          {extension_name,
           l10n_util::GetStringUTF16(
               IDS_EXTENSIONS_MENU_MAIN_PAGE_EXTENSION_BUTTON_REQUESTS_TOOLTIP)},
          u"\n"));

  // Block all extensions access to requested.com. Verify extension tooltip is
  // "blocked access".
  permissions_manager->UpdateUserSiteSetting(
      requested_url, UserSiteSetting::kBlockAllExtensions);
  EXPECT_EQ(
      model->GetTooltip(web_contents),
      base::JoinString(
          {extension_name,
           l10n_util::GetStringUTF16(
               IDS_EXTENSIONS_MENU_MAIN_PAGE_EXTENSION_BUTTON_BLOCKED_ACCESS_TOOLTIP)},
          u"\n"));

  // Navigate to a site that the extension didn't request access to.
  AddTab(not_requested_gurl);
  web_contents = GetActiveWebContents();
  ASSERT_TRUE(web_contents);
  auto non_requested_url =
      url::Origin::Create(web_contents->GetLastCommittedURL());

  // By default, user site setting is "customize by extension". Verify extension
  // tooltip is just the extension name since extension didn't request access
  // to this site.
  ASSERT_EQ(permissions_manager->GetUserSiteSetting(non_requested_url),
            UserSiteSetting::kCustomizeByExtension);
  EXPECT_EQ(model->GetTooltip(web_contents), extension_name);

  // Block all extensions access to non-requested.com. Verify extension tooltip
  // is "blocked access" regardless of extension not requesting access to this
  // site.
  permissions_manager->UpdateUserSiteSetting(
      non_requested_url, UserSiteSetting::kBlockAllExtensions);
  EXPECT_EQ(
      model->GetTooltip(web_contents),
      base::JoinString(
          {extension_name,
           l10n_util::GetStringUTF16(
               IDS_EXTENSIONS_MENU_MAIN_PAGE_EXTENSION_BUTTON_BLOCKED_ACCESS_TOOLTIP)},
          u"\n"));
}

class ExtensionActionViewModelFeatureWithPermittedSitesBrowserTest
    : public ExtensionActionViewModelFeatureBrowserTest {
 public:
  ExtensionActionViewModelFeatureWithPermittedSitesBrowserTest() {
    scoped_feature_list_.InitAndEnableFeature(
        extensions_features::kExtensionsMenuAccessControlWithPermittedSites);
  }
  ~ExtensionActionViewModelFeatureWithPermittedSitesBrowserTest() override =
      default;

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

// Tests hover card status after changing user site settings and site access.
IN_PROC_BROWSER_TEST_F(
    ExtensionActionViewModelFeatureWithPermittedSitesBrowserTest,
    GetHoverCardStatus) {
  Init();
  std::string url_string = "https://example.com/";
  const GURL gurl =
      embedded_https_test_server().GetURL("example.com", "/simple.html");
  auto extensionA = CreateAndAddExtension(
      "Extension A", extensions::ActionInfo::Type::kAction);
  auto extensionB = CreateAndAddExtensionWithGrantedHostPermissions(
      "Extension B", extensions::ActionInfo::Type::kAction, {url_string});
  auto extensionC = CreateAndAddExtensionWithGrantedHostPermissions(
      "Extension c", extensions::ActionInfo::Type::kAction, {url_string});

  AddTab(gurl);
  content::WebContents* web_contents = GetActiveWebContents();
  ASSERT_TRUE(web_contents);
  auto url = url::Origin::Create(web_contents->GetLastCommittedURL());

  ExtensionActionViewModel* const modelA = GetViewModelForId(extensionA->id());
  ASSERT_TRUE(modelA);
  ExtensionActionViewModel* const modelB = GetViewModelForId(extensionB->id());
  ASSERT_TRUE(modelB);
  ExtensionActionViewModel* const modelC = GetViewModelForId(extensionC->id());
  ASSERT_TRUE(modelC);

  // By default, user site setting is "customize by extension" and site access
  // is granted to every extension that requests them. Thus, verify extension A
  // hover card state is "does not want access" and the rest is "have access".
  auto* permissions_manager = extensions::PermissionsManager::Get(profile());
  ASSERT_EQ(permissions_manager->GetUserSiteSetting(url),
            UserSiteSetting::kCustomizeByExtension);
  EXPECT_EQ(GetHoverCardSiteAccessState(modelA, web_contents),
            HoverCardState::SiteAccess::kExtensionDoesNotWantAccess);
  EXPECT_EQ(GetHoverCardSiteAccessState(modelB, web_contents),
            HoverCardState::SiteAccess::kExtensionHasAccess);
  EXPECT_EQ(GetHoverCardSiteAccessState(modelC, web_contents),
            HoverCardState::SiteAccess::kExtensionHasAccess);

  // Withhold extension C host permissions. Verify only extension C changed
  // hover card state to "requests access".
  extensions::ScriptingPermissionsModifier(profile(), extensionC)
      .SetWithholdHostPermissions(true);
  EXPECT_EQ(GetHoverCardSiteAccessState(modelA, web_contents),
            HoverCardState::SiteAccess::kExtensionDoesNotWantAccess);
  EXPECT_EQ(GetHoverCardSiteAccessState(modelB, web_contents),
            HoverCardState::SiteAccess::kExtensionHasAccess);
  EXPECT_EQ(GetHoverCardSiteAccessState(modelC, web_contents),
            HoverCardState::SiteAccess::kExtensionRequestsAccess);

  // Grant all extensions site access. Verify extension A hover card state is
  // "does not want access" and extensions B and C is "all extensions allowed".
  permissions_manager->UpdateUserSiteSetting(
      url, UserSiteSetting::kGrantAllExtensions);
  EXPECT_EQ(GetHoverCardSiteAccessState(modelA, web_contents),
            HoverCardState::SiteAccess::kExtensionDoesNotWantAccess);
  EXPECT_EQ(GetHoverCardSiteAccessState(modelB, web_contents),
            HoverCardState::SiteAccess::kAllExtensionsAllowed);
  EXPECT_EQ(GetHoverCardSiteAccessState(modelC, web_contents),
            HoverCardState::SiteAccess::kAllExtensionsAllowed);
}

#if BUILDFLAG(ENABLE_EXTENSIONS)
// Test that the extension action is enabled if opening the side panel on icon
// click is enabled and the extension has a side panel for the current tab.
IN_PROC_BROWSER_TEST_P(ExtensionActionViewModelFeatureRolloutBrowserTest,
                       ActionEnabledIfSidePanelPresent) {
  Init();
  scoped_refptr<const extensions::Extension> extension =
      extensions::ExtensionBuilder("just side panel")
          .SetLocation(ManifestLocation::kInternal)
          .AddAPIPermission("sidePanel")
          .Build();

  GrantActivePermissions(extension.get());
  extension_registrar()->AddExtension(extension.get());
  side_panel_service()->SetOpenSidePanelOnIconClick(extension->id(), true);

  ExtensionActionViewModel* const model = GetViewModelForId(extension->id());
  ASSERT_TRUE(model);

  AddTab(GURL("https://www.chromium.org/"));
  content::WebContents* web_contents = GetActiveWebContents();
  int tab_id = sessions::SessionTabHelper::IdForTab(web_contents).id();

  // If the preference is true but there is no side panel for the current tab,
  // the action should be disabled.
  std::unique_ptr<IconWithBadgeImageSource> image_source =
      model->GetIconImageSourceForTesting(web_contents, kTestIconSize);
  EXPECT_TRUE(image_source->grayscale());
  EXPECT_FALSE(model->IsEnabled(web_contents));

  // Set a side panel for the current tab. This should enable the extension's
  // action.
  extensions::api::side_panel::PanelOptions options;
  options.enabled = true;
  options.path = "panel.html";
  options.tab_id = tab_id;
  side_panel_service()->SetOptions(*extension, std::move(options));

  image_source =
      model->GetIconImageSourceForTesting(web_contents, kTestIconSize);
  EXPECT_FALSE(image_source->grayscale());
  EXPECT_TRUE(model->IsEnabled(web_contents));

  // Setting the preference to false should disable the extension's action.
  side_panel_service()->SetOpenSidePanelOnIconClick(extension->id(), false);
  image_source =
      model->GetIconImageSourceForTesting(web_contents, kTestIconSize);
  EXPECT_TRUE(image_source->grayscale());
  EXPECT_FALSE(model->IsEnabled(web_contents));
}
#endif  // BUILDFLAG(ENABLE_EXTENSIONS)

// A fake implementation of ExtensionActionDelegate that does nothing.
class FakeExtensionActionDelegate : public ExtensionActionDelegate {
 public:
  void AttachToModel(ExtensionActionViewModel* model) override {}
  void DetachFromModel() override {}
  void RegisterCommand() override {}
  void UnregisterCommand() override {}
  bool IsShowingPopup() const override { return false; }
  void HidePopup() override {}
  gfx::NativeView GetPopupNativeViewForTesting() override {
    return gfx::NativeView();
  }
  void TriggerPopup(std::unique_ptr<extensions::ExtensionViewHost> host,
                    PopupShowAction show_action,
                    bool by_user,
                    ShowPopupCallback callback) override {}
  void ShowContextMenuAsFallback() override {}
  void CloseExtensionsMenuIfOpen() override {}
};

// Ensures that calling methods for stale extensions does not cause crashes.
IN_PROC_BROWSER_TEST_P(ExtensionActionViewModelFeatureRolloutBrowserTest,
                       NoCrashForStaleExtensions) {
  Init();
  const std::string id =
      CreateAndAddExtension("extension", extensions::ActionInfo::Type::kPage)
          ->id();

  content::WebContents* web_contents = GetActiveWebContents();

  // Create ExtensionActionViewModel that is not associated with the
  // lifetime of the view.
  auto action = ExtensionActionViewModel::Create(
      id, browser_window_interface(),
      std::make_unique<FakeExtensionActionDelegate>());

  extension_registrar()->DisableExtension(
      id, {extensions::disable_reason::DISABLE_USER_ACTION});

  // GetId() must return a consistent value.
  EXPECT_EQ(id, action->GetId());

  // Other getters may return undefined values, but never crash.
  // We expect specific values here, even though they're not in the contract.
  EXPECT_TRUE(action->GetIcon(web_contents, gfx::Size(12, 12)).IsEmpty());
  EXPECT_EQ(std::u16string(), action->GetActionName());
  EXPECT_EQ(std::u16string(), action->GetActionTitle(web_contents));
  EXPECT_EQ(std::u16string(), action->GetAccessibleName(web_contents));
  EXPECT_EQ(std::u16string(), action->GetTooltip(web_contents));
  HoverCardState state = action->GetHoverCardState(web_contents);
  EXPECT_EQ(HoverCardState::SiteAccess::kExtensionDoesNotWantAccess,
            state.site_access);
  EXPECT_EQ(HoverCardState::AdminPolicy::kNone, state.policy);
  EXPECT_EQ(SiteInteraction::kNone, action->GetSiteInteraction(web_contents));
  EXPECT_EQ(false, action->IsEnabled(web_contents));
  EXPECT_EQ(false, action->IsShowingPopup());
  EXPECT_EQ(gfx::NativeView(), action->GetPopupNativeViewForTesting());
  EXPECT_EQ(nullptr,
            action->GetContextMenu(extensions::ExtensionContextMenuModel::
                                       ContextMenuSource::kToolbarAction));

  // Calling action methods do not cause crashes.
  action->HidePopup();
  action->ExecuteUserAction(
      ExtensionActionViewModel::InvocationSource::kToolbarButton);
  action->TriggerPopupForAPI(base::DoNothing());
}
