// Copyright 2026 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/settings_overridden_dialog.h"

#include <algorithm>

#include "base/i18n/rtl.h"
#include "base/i18n/test/scoped_rtl_for_testing.h"
#include "base/memory/raw_ptr.h"
#include "base/path_service.h"
#include "base/test/scoped_feature_list.h"
#include "base/time/time.h"
#include "chrome/browser/extensions/chrome_test_extension_loader.h"
#include "chrome/browser/extensions/scoped_test_mv2_enabler.h"
#include "chrome/browser/image_fetcher/image_fetcher_service_factory.h"
#include "chrome/browser/profiles/profile.h"
#include "chrome/browser/profiles/profile_key.h"
#include "chrome/browser/search_engines/template_url_service_factory.h"
#include "chrome/browser/ui/browser_window.h"
#include "chrome/browser/ui/browser_window/public/browser_window_interface.h"
#include "chrome/browser/ui/extensions/extension_settings_overridden_dialog.h"
#include "chrome/browser/ui/hats/hats_service.h"
#include "chrome/browser/ui/hats/hats_service_factory.h"
#include "chrome/browser/ui/hats/mock_hats_service.h"
#include "chrome/browser/ui/hats/survey_config.h"
#include "chrome/browser/ui/tabs/tab_strip_model.h"
#include "chrome/browser/ui/views/extensions/rich_radio_button.h"
#include "chrome/common/chrome_features.h"
#include "chrome/common/chrome_paths.h"
#include "chrome/common/webui_url_constants.h"
#include "chrome/test/base/search_test_utils.h"
#include "chrome/test/base/ui_test_utils.h"
#include "chrome/test/interaction/interactive_browser_test.h"
#include "components/image_fetcher/core/image_fetcher_service.h"
#include "components/image_fetcher/core/mock_image_fetcher.h"
#include "components/image_fetcher/core/request_metadata.h"
#include "components/keyed_service/content/browser_context_dependency_manager.h"
#include "components/regional_capabilities/regional_capabilities_utils.h"
#include "components/search_engines/search_engines_test_util.h"
#include "components/search_engines/template_url.h"
#include "components/search_engines/template_url_prepopulate_data.h"
#include "components/search_engines/template_url_service.h"
#include "components/vector_icons/vector_icons.h"
#include "content/public/test/browser_test.h"
#include "content/public/test/browser_test_utils.h"
#include "extensions/common/extension_features.h"
#include "testing/gmock/include/gmock/gmock.h"
#include "ui/base/ui_base_features.h"
#include "ui/gfx/image/image_skia.h"
#include "ui/gfx/paint_vector_icon.h"
#include "ui/native_theme/mock_os_settings_provider.h"
#include "ui/views/view.h"
#include "ui/views/widget/widget.h"
#include "url/gurl.h"

namespace {

// The extension in "extensions/search_provider_override" uses "example.com"
// as the search URL.
constexpr char kExtensionSearchUrl[] = "https://www.example.com/?q=Penguin";

// Used to override the prepopulated search engine to a fake default search.
constexpr TemplateURLPrepopulateData::PrepopulatedEngine kDefaultSearch = {
    .name = u"Default Search",
    .keyword = u"defaultsearch.com",
    .favicon_url = "https://www.defaultsearch.com/favicon.ico",
    .search_url = "https://www.defaultsearch.com/search?q={searchTerms}",
    .encoding = "UTF-8",
    .type = SEARCH_ENGINE_OTHER,
    .id = 999999,
};

enum class DefaultSearch {
  kUseDefault,
  kUseNonGoogleFromDefaultList,
  kUseNewSearch,
};

DEFINE_LOCAL_ELEMENT_IDENTIFIER_VALUE(kWebContentsId);

using ::testing::_;
using ::testing::Contains;
using ::testing::IsEmpty;
using ::testing::Not;
using ::testing::Pair;

// This makes icon fetching testable on the dialog, without resorting to
// fallback/placeholder icons.
class MockImageFetcherService : public image_fetcher::ImageFetcherService {
 public:
  MockImageFetcherService() = default;
  ~MockImageFetcherService() override = default;

  image_fetcher::MockImageFetcher* mock_image_fetcher() {
    return &mock_image_fetcher_;
  }

  // image_fetcher::ImageFetcherService:
  image_fetcher::ImageFetcher* GetImageFetcher(
      image_fetcher::ImageFetcherConfig config) override {
    return &mock_image_fetcher_;
  }

 private:
  image_fetcher::MockImageFetcher mock_image_fetcher_;
};

class SettingsOverriddenDialogInteractiveUiTest
    : public InteractiveBrowserTest {
 protected:
  const ui::ElementIdentifier kPreviousSettingButtonId =
      kSettingsOverriddenDialogPreviousSettingButtonId;
  const ui::ElementIdentifier kNewSettingButtonId =
      kSettingsOverriddenDialogNewSettingButtonId;
  const ui::ElementIdentifier kSaveButtonId =
      kSettingsOverriddenDialogSaveButtonId;
  const ui::ElementIdentifier kKeepItButtonId =
      kSettingsOverriddenDialogKeepItButtonId;

  SettingsOverriddenDialogInteractiveUiTest() {
    // Register a mock image-fetcher factory service so that it's created
    // in place of the default image-fetcher service.
    create_services_subscription_ =
        BrowserContextDependencyManager::GetInstance()
            ->RegisterCreateServicesCallbackForTesting(
                base::BindRepeating(&SettingsOverriddenDialogInteractiveUiTest::
                                        OnWillCreateBrowserContextServices,
                                    base::Unretained(this)));
  }
  ~SettingsOverriddenDialogInteractiveUiTest() override = default;

  void SetUpOnMainThread() override {
    InteractiveBrowserTest::SetUpOnMainThread();
    SetUpMockImageFetcher();
    search_test_utils::WaitForTemplateURLServiceToLoad(
        TemplateURLServiceFactory::GetForProfile(browser()->GetProfile()));
  }

  virtual void OnWillCreateBrowserContextServices(
      content::BrowserContext* context) {
    Profile* profile = Profile::FromBrowserContext(context);
    ImageFetcherServiceFactory::GetInstance()->SetTestingFactory(
        profile->GetProfileKey(),
        base::BindRepeating(
            [](SimpleFactoryKey* key) -> std::unique_ptr<KeyedService> {
              return std::make_unique<MockImageFetcherService>();
            }));
  }

  auto SetNewSearchProvider(DefaultSearch search) {
    return Do([this, search]() {
      if (search == DefaultSearch::kUseDefault) {
        return;
      }

      TemplateURLService* const template_url_service =
          TemplateURLServiceFactory::GetForProfile(browser()->GetProfile());

      bool new_search_shows_in_default_list = true;
      // If the test requires a search engine that doesn't show in the default
      // list, we need to add one.
      if (search == DefaultSearch::kUseNewSearch) {
        new_search_shows_in_default_list = false;
        template_url_service->Add(std::make_unique<TemplateURL>(
            *GenerateDummyTemplateURLData("test")));
      }

      TemplateURLService::TemplateURLVector template_urls =
          template_url_service->GetTemplateURLs();
      auto iter = std::ranges::find_if(
          template_urls,
          [template_url_service,
           new_search_shows_in_default_list](const TemplateURL* turl) {
            return !turl->HasGoogleBaseURLs(
                       template_url_service->search_terms_data()) &&
                   template_url_service->ShowInDefaultList(turl) ==
                       new_search_shows_in_default_list;
          });
      ASSERT_TRUE(iter != template_urls.end());

      template_url_service->SetUserSelectedDefaultSearchProvider(*iter);
    });
  }

  auto LoadExtensionOverridingSearch() {
    return Do([this]() {
      base::FilePath test_root_path;
      ASSERT_TRUE(
          base::PathService::Get(chrome::DIR_TEST_DATA, &test_root_path));

      Profile* const profile = browser()->GetProfile();
      scoped_refptr<const extensions::Extension> extension =
          extensions::ChromeTestExtensionLoader(profile).LoadExtension(
              test_root_path.AppendASCII(
                  "extensions/search_provider_override"));
      ASSERT_TRUE(extension);
    });
  }

  auto PerformSearchFromOmnibox(
      BrowserWindowInterface* target_browser = nullptr) {
    return Do([this, target_browser]() {
      ui_test_utils::SendToOmniboxAndSubmit(
          target_browser ? target_browser : browser(), "Penguin",
          base::TimeTicks::Now(), /*wait_for_autocomplete_done=*/false);
    });
  }

  // Waits for the dialog to show, and verifies it's explicit-choice type.
  auto WaitForDialogToShow() {
    auto steps = Steps(WaitForShow(kSettingsOverriddenDialogId),
                       EnsurePresent(kSaveButtonId),
                       EnsurePresent(kPreviousSettingButtonId),
                       EnsurePresent(kNewSettingButtonId));
    AddDescriptionPrefix(steps, __func__);
    return steps;
  }

  auto ScreenshotDialog() {
    auto steps =
        Steps(SetOnIncompatibleAction(
                  OnIncompatibleAction::kIgnoreAndContinue,
                  "Screenshot can only run in pixel_tests on Windows."),
              ScreenshotSurface(
                  kSettingsOverriddenDialogId,
                  /*screenshot_name=*/"ExtensionSettingsOverridenDialog",
                  /*baseline_cl=*/"7645077"));
    return steps;
  }

  auto CheckActiveUrl(const GURL& expected_url) {
    return CheckResult(
        [this]() {
          return browser()
              ->GetTabStripModel()
              ->GetActiveWebContents()
              ->GetLastCommittedURL();
        },
        expected_url,
        "Check that navigation is deferred and hasn't reached the extension "
        "URL");
  }

  // Some Google search URL parameters are dynamic. Therefore, for testing
  // purpose, the host will be used for validation.
  auto CheckWebContentsNavigateToGoogle() {
    return CheckResult(
        [this]() {
          return browser()
              ->GetTabStripModel()
              ->GetActiveWebContents()
              ->GetLastCommittedURL()
              .host();
        },
        "www.google.com", "Wait for navigation to Google");
  }

  auto CheckFocused(ui::ElementIdentifier id, bool is_focused) {
    return CheckView(id, [is_focused](views::View* view) {
      const views::View* focused_view =
          view->GetFocusManager()->GetFocusedView();
      bool contains_focus = focused_view && (view == focused_view ||
                                             view->Contains(focused_view));
      return contains_focus == is_focused;
    });
  }

  auto CheckSelected(ui::ElementIdentifier id, bool is_selected) {
    return CheckViewProperty(
        id, &extensions::RichRadioButton::GetCheckedForTesting, is_selected);
  }

  auto CheckSelectedAndFocused(ui::ElementIdentifier id,
                               bool is_selected_and_focused) {
    return Steps(CheckSelected(id, is_selected_and_focused),
                 CheckFocused(id, is_selected_and_focused));
  }

 private:
  // Sets up the mock ImageFetcher so that we can exercise icon fetching, rather
  // than having the dialog fall back to generated placeholder icons. Note that
  // we could also try and do this end-to-end by setting up URL fetch
  // intercepters, but that's more complexity and fragility just to include
  // ImageFetcher in this dialog testing.
  void SetUpMockImageFetcher() {
    auto* service = static_cast<MockImageFetcherService*>(
        ImageFetcherServiceFactory::GetForKey(
            browser()->GetProfile()->GetProfileKey()));
    // These icons are arbitrary. The goal is to ensure icons are fetched, vs.
    // falling back to generated placeholder icons.
    constexpr int kFaviconSize = 32;
    gfx::Image icon(gfx::CreateVectorIcon(features::IsRoundedIconsEnabled()
                                              ? vector_icons::kDomainIcon
                                              : vector_icons::kBusinessOldIcon,
                                          kFaviconSize, SK_ColorRED));
    gfx::Image google_icon(gfx::CreateVectorIcon(vector_icons::kGoogleColorIcon,
                                                 kFaviconSize, SK_ColorBLUE));

    EXPECT_CALL(*service->mock_image_fetcher(), FetchImageAndData_(_, _, _, _))
        .WillRepeatedly([icon, google_icon](
                            const GURL& url,
                            image_fetcher::ImageDataFetcherCallback*,
                            image_fetcher::ImageFetcherCallback* callback,
                            image_fetcher::ImageFetcherParams) {
          const gfx::Image& image =
              url.host().find("google.com") != std::string::npos ? google_icon
                                                                 : icon;
          std::move(*callback).Run(image, image_fetcher::RequestMetadata());
        });
  }

 private:
  base::CallbackListSubscription create_services_subscription_;
  extensions::ScopedTestMV2Enabler mv2_enabler_;

  // Override the list of prepopulated search engines to a deterministic set.
  // This prevents test breaks when the production list changes. See
  // crbug.com/530120931.
  regional_capabilities::ScopedPrepopulatedEnginesOverride
      prepopulated_engines_override_ =
          regional_capabilities::SetPrepopulatedEnginesOverrideForTesting(
              /*regional_engines=*/{&TemplateURLPrepopulateData::google,
                                    &kDefaultSearch},
              /*other_known_engines=*/{});
};

class SettingsOverriddenExplicitChoiceDialogInteractiveUiTest
    : public SettingsOverriddenDialogInteractiveUiTest {
 protected:
  SettingsOverriddenExplicitChoiceDialogInteractiveUiTest() {
    feature_list_.InitWithFeatures(
        {extensions_features::kSearchEngineExplicitChoiceDialog}, {});
  }

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

IN_PROC_BROWSER_TEST_F(SettingsOverriddenExplicitChoiceDialogInteractiveUiTest,
                       ChoosingPreviousSettingRestoresPrevious) {
  RunTestSequence(InstrumentTab(kWebContentsId),
                  SetNewSearchProvider(DefaultSearch::kUseDefault),
                  LoadExtensionOverridingSearch(), PerformSearchFromOmnibox(),
                  WaitForDialogToShow(),
                  // Ensure navigation hasn't happened yet.
                  CheckActiveUrl(GURL("about:blank")),
                  // Select previous search setting.
                  PressButton(kPreviousSettingButtonId),
                  PressButton(kSaveButtonId),
                  WaitForHide(kSettingsOverriddenDialogId),
                  // Verify navigation proceeds to the default search URL
                  // (Google) instead of the extension URL.
                  WaitForWebContentsNavigation(kWebContentsId),
                  CheckWebContentsNavigateToGoogle());
}

IN_PROC_BROWSER_TEST_F(SettingsOverriddenExplicitChoiceDialogInteractiveUiTest,
                       ChoosingNewSettingSearchesWithExtension) {
  RunTestSequence(
      InstrumentTab(kWebContentsId),
      SetNewSearchProvider(DefaultSearch::kUseNonGoogleFromDefaultList),
      LoadExtensionOverridingSearch(), PerformSearchFromOmnibox(),
      WaitForDialogToShow(),
      // Ensure navigation hasn't happened yet.
      CheckActiveUrl(GURL("about:blank")),
      // Select new search setting.
      PressButton(kNewSettingButtonId), PressButton(kSaveButtonId),
      WaitForHide(kSettingsOverriddenDialogId),
      // Verify navigation proceeds to the extension's search URL.
      WaitForWebContentsNavigation(kWebContentsId, GURL(kExtensionSearchUrl)));
}

IN_PROC_BROWSER_TEST_F(SettingsOverriddenExplicitChoiceDialogInteractiveUiTest,
                       ScreenshotPreviouslyGoogle) {
  RunTestSequence(InstrumentTab(kWebContentsId),
                  SetNewSearchProvider(DefaultSearch::kUseDefault),
                  LoadExtensionOverridingSearch(), PerformSearchFromOmnibox(),
                  WaitForDialogToShow(), ScreenshotDialog());
}

IN_PROC_BROWSER_TEST_F(SettingsOverriddenExplicitChoiceDialogInteractiveUiTest,
                       ScreenshotPreviouslyNonGoogle) {
  RunTestSequence(
      InstrumentTab(kWebContentsId),
      SetNewSearchProvider(DefaultSearch::kUseNonGoogleFromDefaultList),
      LoadExtensionOverridingSearch(), PerformSearchFromOmnibox(),
      WaitForDialogToShow(), ScreenshotDialog());
}

IN_PROC_BROWSER_TEST_F(SettingsOverriddenExplicitChoiceDialogInteractiveUiTest,
                       ScreenshotPreviouslyOldExtension) {
  RunTestSequence(SetNewSearchProvider(DefaultSearch::kUseNewSearch),
                  LoadExtensionOverridingSearch(), PerformSearchFromOmnibox(),
                  WaitForDialogToShow(), ScreenshotDialog());
}

IN_PROC_BROWSER_TEST_F(SettingsOverriddenExplicitChoiceDialogInteractiveUiTest,
                       ScreenshotRtlLayout) {
  base::i18n::ScopedRTLForTesting scoped_rtl(true);
  RunTestSequence(InstrumentTab(kWebContentsId),
                  SetNewSearchProvider(DefaultSearch::kUseDefault),
                  LoadExtensionOverridingSearch(), PerformSearchFromOmnibox(),
                  WaitForDialogToShow(), ScreenshotDialog());
}

IN_PROC_BROWSER_TEST_F(SettingsOverriddenExplicitChoiceDialogInteractiveUiTest,
                       ScreenshotDarkMode) {
  ui::MockOsSettingsProvider os_settings_provider;
  os_settings_provider.SetPreferredColorScheme(
      ui::NativeTheme::PreferredColorScheme::kDark);
  RunTestSequence(InstrumentTab(kWebContentsId),
                  SetNewSearchProvider(DefaultSearch::kUseDefault),
                  LoadExtensionOverridingSearch(), PerformSearchFromOmnibox(),
                  WaitForDialogToShow(), ScreenshotDialog());
}

IN_PROC_BROWSER_TEST_F(SettingsOverriddenExplicitChoiceDialogInteractiveUiTest,
                       SelectOption) {
  // This test covers the unconventional explicit-choice dialog behavior,
  // having no initially-selected radio button.
  RunTestSequence(
      InstrumentTab(kWebContentsId),
      SetNewSearchProvider(DefaultSearch::kUseDefault),
      LoadExtensionOverridingSearch(), PerformSearchFromOmnibox(),
      WaitForDialogToShow(), CheckActiveUrl(GURL("about:blank")),
      // Assert that neither radio button is initially selected or focused.
      CheckSelectedAndFocused(kPreviousSettingButtonId, false),
      CheckSelectedAndFocused(kNewSettingButtonId, false),
      // Assert that the Save button is disabled initially. Choosing an
      // option enables it.
      CheckViewProperty(kSaveButtonId, &views::View::GetEnabled, false),
      // Select an option.
      PressButton(kNewSettingButtonId),
      // Ensure the selected option is checked (but not focused).
      CheckSelected(kNewSettingButtonId, true),
      CheckFocused(kNewSettingButtonId, false),
      // Assert that the save button is now enabled.
      CheckViewProperty(kSaveButtonId, &views::View::GetEnabled, true),
      // Click the save button.
      PressButton(kSaveButtonId), WaitForHide(kSettingsOverriddenDialogId),
      WaitForWebContentsNavigation(kWebContentsId, GURL(kExtensionSearchUrl)));
}

IN_PROC_BROWSER_TEST_F(SettingsOverriddenExplicitChoiceDialogInteractiveUiTest,
                       EscapeDoesNotCloseDialog) {
  // This test verifies that pressing Escape does not close the dialog.
  RunTestSequence(InstrumentTab(kWebContentsId),
                  SetNewSearchProvider(DefaultSearch::kUseDefault),
                  LoadExtensionOverridingSearch(), PerformSearchFromOmnibox(),
                  WaitForDialogToShow(),
                  // Send Escape.
                  SendKeyPress(kSettingsOverriddenDialogId, ui::VKEY_ESCAPE),
                  // Verify dialog is still present.
                  EnsurePresent(kSettingsOverriddenDialogId),
                  // Ensure we can still close it via interaction.
                  PressButton(kNewSettingButtonId), PressButton(kSaveButtonId),
                  WaitForHide(kSettingsOverriddenDialogId));
}

IN_PROC_BROWSER_TEST_F(SettingsOverriddenExplicitChoiceDialogInteractiveUiTest,
                       OnlyOneDialogShownAtATimeAcrossWindows) {
  // Create a second browser window.
  BrowserWindowInterface* second_browser =
      CreateBrowser(browser()->GetProfile());
  ASSERT_TRUE(second_browser);

  DEFINE_LOCAL_ELEMENT_IDENTIFIER_VALUE(kSecondWebContentsId);

  RunTestSequence(
      InstrumentTab(kWebContentsId),
      InstrumentTab(kSecondWebContentsId, /*tab_index=*/std::nullopt,
                    second_browser),

      SetNewSearchProvider(DefaultSearch::kUseDefault),
      LoadExtensionOverridingSearch(),

      // Perform search from the first browser's omnibox to trigger the dialog.
      PerformSearchFromOmnibox(browser()), WaitForDialogToShow(),

      // Now perform search from the second browser's omnibox. Since a dialog is
      // already showing, it should NOT show the dialog in the second window and
      // the second window navigation should complete immediately.
      PerformSearchFromOmnibox(second_browser),
      // Verify second window navigated to the extension URL immediately.
      WaitForWebContentsNavigation(kSecondWebContentsId,
                                   GURL(kExtensionSearchUrl)));
}

class SettingsOverriddenExplicitChoiceDialogEscapableInteractiveUiTest
    : public SettingsOverriddenExplicitChoiceDialogInteractiveUiTest {
 public:
  SettingsOverriddenExplicitChoiceDialogEscapableInteractiveUiTest() {
    feature_list_.InitAndEnableFeatureWithParameters(
        extensions_features::kSearchEngineExplicitChoiceDialog,
        {{"escapable", "true"}});
  }

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

// TODO(crbug.com/489293133): Re-enable the test
#if BUILDFLAG(IS_MAC)
#define MAYBE_EscapeClosesDialog DISABLED_EscapeClosesDialog
#else
#define MAYBE_EscapeClosesDialog EscapeClosesDialog
#endif
IN_PROC_BROWSER_TEST_F(
    SettingsOverriddenExplicitChoiceDialogEscapableInteractiveUiTest,
    MAYBE_EscapeClosesDialog) {
  const GURL kInitialUrl("https://www.google.com/");

  // This test verifies that pressing Escape closes the dialog when the
  // "escapable" feature parameter is set.
  RunTestSequence(InstrumentTab(kWebContentsId),
                  NavigateWebContents(kWebContentsId, kInitialUrl),
                  SetNewSearchProvider(DefaultSearch::kUseDefault),
                  LoadExtensionOverridingSearch(), PerformSearchFromOmnibox(),
                  WaitForDialogToShow(),
                  SendKeyPress(kSettingsOverriddenDialogId, ui::VKEY_ESCAPE),
                  // Verify dialog is dismissed.
                  WaitForHide(kSettingsOverriddenDialogId),
                  // Verify that no navigation happens.
                  CheckActiveUrl(kInitialUrl)

  );
}

IN_PROC_BROWSER_TEST_F(SettingsOverriddenExplicitChoiceDialogInteractiveUiTest,
                       DialogShownOnNonNewTabPage) {
  // Test that if we are on a real website (e.g. google.com) and perform a
  // search that is intercepted by the extension, the visible URL remains
  // on the existing page until the dialog is resolved.
  const GURL kInitialUrl("https://www.google.com/");

  RunTestSequence(
      InstrumentTab(kWebContentsId),
      NavigateWebContents(kWebContentsId, kInitialUrl),
      SetNewSearchProvider(DefaultSearch::kUseDefault),
      LoadExtensionOverridingSearch(), PerformSearchFromOmnibox(),
      WaitForDialogToShow(),
      // Visible URL should still be the initial site, not the
      // extension's search.
      CheckActiveUrl(kInitialUrl),
      // Select previous search setting.
      PressButton(kNewSettingButtonId), PressButton(kSaveButtonId),
      WaitForHide(kSettingsOverriddenDialogId),
      // Only now should the navigation complete.
      WaitForWebContentsNavigation(kWebContentsId, GURL(kExtensionSearchUrl)));
}

class SettingsOverriddenExplicitChoiceDialogHatsInteractiveUiTest
    : public SettingsOverriddenExplicitChoiceDialogInteractiveUiTest {
 protected:
  SettingsOverriddenExplicitChoiceDialogHatsInteractiveUiTest() {
    feature_list_.InitAndEnableFeature(
        features::kHappinessTrackingSurveysForDesktopSEHijacking);
  }

  void OnWillCreateBrowserContextServices(
      content::BrowserContext* context) override {
    SettingsOverriddenDialogInteractiveUiTest::
        OnWillCreateBrowserContextServices(context);
    Profile* profile = Profile::FromBrowserContext(context);
    HatsServiceFactory::GetInstance()->SetTestingFactory(
        profile, base::BindRepeating([](content::BrowserContext* context)
                                         -> std::unique_ptr<KeyedService> {
          return std::make_unique<testing::NiceMock<MockHatsService>>(
              Profile::FromBrowserContext(context));
        }));
  }

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

IN_PROC_BROWSER_TEST_F(
    SettingsOverriddenExplicitChoiceDialogHatsInteractiveUiTest,
    HatsSurveyTriggered) {
  using HatsDialog = ExtensionSettingsOverriddenDialog;
  RunTestSequence(
      SetNewSearchProvider(DefaultSearch::kUseDefault),
      LoadExtensionOverridingSearch(), PerformSearchFromOmnibox(),
      WaitForDialogToShow(), Do([this]() {
        HatsService* hats_service = HatsServiceFactory::GetForProfile(
            browser()->GetProfile(), /*create_if_necessary=*/true);
        CHECK(hats_service);
        MockHatsService* mock_hats_service =
            static_cast<MockHatsService*>(hats_service);
        EXPECT_CALL(
            *mock_hats_service,
            LaunchDelayedSurvey(
                kHatsSurveyTriggerSEHijacking, 5000, _,
                testing::AllOf(
                    Contains(Pair(HatsDialog::kHatsPsdChannel, _)),
                    Contains(Pair(HatsDialog::kHatsPsdUserChoice,
                                  HatsDialog::kHatsUserChoiceNewProvider)),
                    Contains(Pair(HatsDialog::kHatsPsdTimeDialogVisible, _)),
                    Contains(Pair(HatsDialog::kHatsPsdNewExtensionId,
                                  Not(IsEmpty()))),
                    Contains(Pair(HatsDialog::kHatsPsdNewExtensionName,
                                  "Search Override Extension")))))
            .WillOnce(testing::Return(HatsService::LaunchError::kNone));
      }),
      PressButton(kNewSettingButtonId), PressButton(kSaveButtonId),
      WaitForHide(kSaveButtonId));
}

IN_PROC_BROWSER_TEST_F(
    SettingsOverriddenExplicitChoiceDialogHatsInteractiveUiTest,
    HatsSurveyTriggered_Reject) {
  using HatsDialog = ExtensionSettingsOverriddenDialog;
  RunTestSequence(
      SetNewSearchProvider(DefaultSearch::kUseDefault),
      LoadExtensionOverridingSearch(), PerformSearchFromOmnibox(),
      WaitForDialogToShow(), Do([this]() {
        HatsService* hats_service = HatsServiceFactory::GetForProfile(
            browser()->GetProfile(), /*create_if_necessary=*/true);
        CHECK(hats_service);
        MockHatsService* mock_hats_service =
            static_cast<MockHatsService*>(hats_service);
        EXPECT_CALL(*mock_hats_service,
                    LaunchDelayedSurvey(
                        kHatsSurveyTriggerSEHijacking, 5000, _,
                        testing::AllOf(
                            // No need to check all parameters here, they are
                            // covered in other HaTS tests.
                            Contains(Pair(
                                HatsDialog::kHatsPsdUserChoice,
                                HatsDialog::kHatsUserChoicePreviousProvider)))))
            .WillOnce(testing::Return(HatsService::LaunchError::kNone));
      }),
      PressButton(kPreviousSettingButtonId), PressButton(kSaveButtonId),
      WaitForHide(kSaveButtonId));
}

// Ensures that the dialog operates correctly under keyboard input.
IN_PROC_BROWSER_TEST_F(SettingsOverriddenExplicitChoiceDialogInteractiveUiTest,
                       VerifyKeyboardInteraction) {
  auto kFirstOption = kPreviousSettingButtonId;
  auto kSecondOption = kNewSettingButtonId;

// See implementation. Depending on platform, the descriptive paragraph in the
// dialog either acts as a focusable element, or not.
#if BUILDFLAG(IS_WIN)
  const bool focusable_description = true;
#else
  const bool focusable_description = false;
#endif

  RunTestSequence(
      InstrumentTab(kWebContentsId),
      SetNewSearchProvider(DefaultSearch::kUseDefault),
      LoadExtensionOverridingSearch(), PerformSearchFromOmnibox(),
      WaitForDialogToShow(), CheckActiveUrl(GURL("about:blank")),

      // Initially, neither option is focused or selected.
      CheckSelectedAndFocused(kFirstOption, false),
      CheckSelectedAndFocused(kSecondOption, false),

      // Press Tab. First radio button should focus (and become selected).
      SendKeyPress(kSettingsOverriddenDialogId, ui::VKEY_TAB),
      CheckSelectedAndFocused(kFirstOption, true),
      CheckSelectedAndFocused(kSecondOption, false),

      // Press Tab. Save button should be focused.
      SendKeyPress(kSettingsOverriddenDialogId, ui::VKEY_TAB),
      CheckSelected(kFirstOption, true), CheckFocused(kFirstOption, false),
      CheckSelectedAndFocused(kSecondOption, false),

      // Press Tab. Focus should rotate back to the descriptive paragraph,
      // if selectable.
      focusable_description
          ? Steps(SendKeyPress(kSettingsOverriddenDialogId, ui::VKEY_TAB),
                  CheckFocused(kSettingsOverriddenDialogParagraphId, true))
          : Steps(),

      // Press Tab again. First radio button should focus.
      SendKeyPress(kSettingsOverriddenDialogId, ui::VKEY_TAB),
      CheckSelectedAndFocused(kFirstOption, true),
      CheckSelectedAndFocused(kSecondOption, false),

      // Press Down Arrow key. Second radio button should now be focused and
      // selected.
      SendKeyPress(kSettingsOverriddenDialogId, ui::VKEY_DOWN),
      CheckSelectedAndFocused(kFirstOption, false),
      CheckSelectedAndFocused(kSecondOption, true),
      // Press Right Arrow key. First radio button should now be focused and
      // selected, as focus cycles back to the first element.
      SendKeyPress(kSettingsOverriddenDialogId, ui::VKEY_RIGHT),
      CheckSelectedAndFocused(kFirstOption, true),
      CheckSelectedAndFocused(kSecondOption, false),

      // Finish the dialog to clean up.
      PressButton(kSaveButtonId), WaitForHide(kSettingsOverriddenDialogId));
}

class SettingsOverriddenLegacyDialogInteractiveUiTest
    : public SettingsOverriddenDialogInteractiveUiTest {
 protected:
  SettingsOverriddenLegacyDialogInteractiveUiTest() {
    feature_list_.InitAndDisableFeature(
        extensions_features::kSearchEngineExplicitChoiceDialog);
  }

  // Waits for the legacy dialog to show.
  auto WaitForDialogToShow() {
    auto steps =
        Steps(WaitForShow(kSettingsOverriddenDialogId),
              EnsurePresent(kSaveButtonId), EnsurePresent(kKeepItButtonId));
    AddDescriptionPrefix(steps, __func__);
    return steps;
  }

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

// Don't bother moving HaTS logic into the base class, because this survey will
// only live for the duration of the experimental rollout.  Keeping it separate
// makes for easier cleanup.
class SettingsOverriddenLegacyDialogHatsInteractiveUiTest
    : public SettingsOverriddenLegacyDialogInteractiveUiTest {
 protected:
  SettingsOverriddenLegacyDialogHatsInteractiveUiTest() {
    feature_list_.InitAndEnableFeature(
        features::kHappinessTrackingSurveysForDesktopSEHijacking);
  }

  void OnWillCreateBrowserContextServices(
      content::BrowserContext* context) override {
    SettingsOverriddenDialogInteractiveUiTest::
        OnWillCreateBrowserContextServices(context);
    Profile* profile = Profile::FromBrowserContext(context);
    HatsServiceFactory::GetInstance()->SetTestingFactory(
        profile, base::BindRepeating([](content::BrowserContext* context)
                                         -> std::unique_ptr<KeyedService> {
          return std::make_unique<testing::NiceMock<MockHatsService>>(
              Profile::FromBrowserContext(context));
        }));
  }

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

// This test covers the HaTS survey launched after interacting with the
// original (non-explicit-choice) confirmation dialog.  We how a survey in that
// case as well, to compare user experience across the two dialogs types.
IN_PROC_BROWSER_TEST_F(SettingsOverriddenLegacyDialogHatsInteractiveUiTest,
                       HatsSurveyTriggered) {
  using HatsDialog = ExtensionSettingsOverriddenDialog;
  RunTestSequence(
      SetNewSearchProvider(DefaultSearch::kUseDefault),
      LoadExtensionOverridingSearch(), PerformSearchFromOmnibox(),
      WaitForDialogToShow(), Do([this]() {
        HatsService* hats_service = HatsServiceFactory::GetForProfile(
            browser()->GetProfile(), /*create_if_necessary=*/true);
        CHECK(hats_service);
        MockHatsService* mock_hats_service =
            static_cast<MockHatsService*>(hats_service);
        EXPECT_CALL(*mock_hats_service,
                    LaunchDelayedSurvey(
                        kHatsSurveyTriggerSEHijacking, 5000, _,
                        testing::AllOf(
                            // No need to check all parameters here, they are
                            // covered in other HaTS tests.
                            Contains(Pair(HatsDialog::kHatsPsdNewExtensionName,
                                          "Search Override Extension")))))
            .WillOnce(testing::Return(HatsService::LaunchError::kNone));
      }),
      // Press "Keep it" button.
      PressButton(kKeepItButtonId), WaitForHide(kKeepItButtonId));
}

}  // namespace
