// Copyright 2022 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/webui/signin/sync_confirmation_ui.h"

#include <utility>
#include <vector>

#include "base/functional/bind.h"
#include "base/scoped_environment_variable_override.h"
#include "base/scoped_observation.h"
#include "base/strings/strcat.h"
#include "base/test/scoped_feature_list.h"
#include "base/types/strong_alias.h"
#include "build/build_config.h"
#include "chrome/browser/consent_auditor/consent_auditor_factory.h"
#include "chrome/browser/consent_auditor/consent_auditor_test_utils.h"
#include "chrome/browser/signin/signin_browser_test_base.h"
#include "chrome/browser/ui/browser_window/public/browser_window_features.h"
#include "chrome/browser/ui/browser_window/public/browser_window_interface.h"
#include "chrome/browser/ui/signin/signin_view_controller.h"
#include "chrome/browser/ui/test/test_browser_dialog.h"
#include "chrome/browser/ui/test/test_browser_ui.h"
#include "chrome/browser/ui/views/profiles/profile_management_step_controller.h"
#include "chrome/browser/ui/views/profiles/profile_picker_view_test_utils.h"
#include "chrome/browser/ui/views/profiles/profiles_pixel_test_utils.h"
#include "chrome/browser/ui/webui/signin/login_ui_service.h"
#include "chrome/browser/ui/webui/signin/login_ui_service_factory.h"
#include "chrome/browser/ui/webui/signin/login_ui_test_utils.h"
#include "chrome/browser/ui/webui/signin/signin_url_utils.h"
#include "chrome/common/webui_url_constants.h"
#include "chrome/grit/generated_resources.h"
#include "chrome/test/base/in_process_browser_test.h"
#include "components/consent_auditor/consent_auditor.h"
#include "components/consent_auditor/fake_consent_auditor.h"
#include "components/signin/public/base/consent_level.h"
#include "components/signin/public/base/signin_buildflags.h"
#include "components/signin/public/base/signin_switches.h"
#include "components/signin/public/identity_manager/account_info.h"
#include "components/signin/public/identity_manager/tribool.h"
#include "content/public/common/content_features.h"
#include "content/public/test/browser_test.h"
#include "content/public/test/test_navigation_observer.h"
#include "ui/base/ui_base_switches.h"
#include "ui/gfx/scoped_animation_duration_scale_mode.h"
#include "ui/views/widget/any_widget_observer.h"

#if !BUILDFLAG(ENABLE_DICE_SUPPORT)
#error Platform not supported
#endif

// TODO(crbug.com/40242558): Move this file next to sync_confirmation_ui.cc.
// Render the page in a browser instead of a profile_picker_view to be able to
// do so.

// Tests for the chrome://sync-confirmation WebUI page. They live here and not
// in the webui directory because they manipulate views.
namespace {

using ::testing::AllOf;
using ::testing::Contains;
using ::testing::ElementsAre;
using ::testing::ValuesIn;

// Configures the can_show_history_sync_opt_ins_without_minor_mode_restrictions
// account capability, which determines minor mode restrictions status.
using MinorModeRestrictions =
    base::StrongAlias<class MinorModeRestrictionsTag, signin::Tribool>;

constexpr MinorModeRestrictions kWithUnrestrictedUser(signin::Tribool::kTrue);
constexpr MinorModeRestrictions kWithRestrictedUser(signin::Tribool::kFalse);

struct SyncConfirmationTestParam {
  PixelTestParam pixel_test_param;
  AccountManagementStatus account_management_status =
      AccountManagementStatus::kNonManaged;
  SyncConfirmationStyle sync_style = SyncConfirmationStyle::kWindow;
  bool is_sync_promo = false;
  MinorModeRestrictions minor_mode_restrictions = kWithUnrestrictedUser;
};

enum class FirstRunVersion {
  kLegacy,
  kRefresh,
  kRevamp,
};

struct SyncConfirmationPixelTestParam {
  SyncConfirmationTestParam sync_param;
  FirstRunVersion first_run_version = FirstRunVersion::kLegacy;
};

struct SyncConfirmationWindowPixelTestParam {
  SyncConfirmationPixelTestParam context_param;
  bool is_first_run = false;
};

std::string ParamToTestSuffix(const SyncConfirmationPixelTestParam& param) {
  std::string first_run_version_suffix;
  switch (param.first_run_version) {
    case FirstRunVersion::kLegacy:
      break;
    case FirstRunVersion::kRefresh:
      first_run_version_suffix = "Refresh";
      break;
    case FirstRunVersion::kRevamp:
      first_run_version_suffix = "Revamp";
      break;
  }
  return base::StrCat({param.sync_param.pixel_test_param.test_suffix,
                       first_run_version_suffix});
}

std::string ParamToTestSuffix(
    const SyncConfirmationWindowPixelTestParam& info) {
  return base::StrCat({ParamToTestSuffix(info.context_param),
                       info.is_first_run ? "FirstRun" : ""});
}

std::vector<SyncConfirmationWindowPixelTestParam> GetWindowPixelTestParams() {
  // Permutations of supported parameters.
  const SyncConfirmationTestParam base_params[] = {
      {.pixel_test_param = {.test_suffix = "Regular"}},
      {.pixel_test_param = {.test_suffix = "DarkTheme",
                            .use_dark_theme = true}},
      {.pixel_test_param = {.test_suffix = "Rtl",
                            .use_right_to_left_language = true}},
      {.pixel_test_param = {.test_suffix = "SmallWindow",
                            .window_size = PixelTestParam::kSmallWindowSize}},
      {.pixel_test_param = {.test_suffix = "ManagedAccount"},
       .account_management_status = AccountManagementStatus::kManaged},
      {.pixel_test_param = {.test_suffix =
                                "RegularWithRestrictionsWithUnrestrictedUser"},
       .minor_mode_restrictions = kWithUnrestrictedUser},
      {.pixel_test_param = {.test_suffix =
                                "RegularWithRestrictionsWithRestrictedUser"},
       .minor_mode_restrictions = kWithRestrictedUser},
  };
  const FirstRunVersion versions[] = {FirstRunVersion::kLegacy,
                                      FirstRunVersion::kRefresh,
                                      FirstRunVersion::kRevamp};
  const bool first_run_states[] = {true, false};
  std::vector<SyncConfirmationWindowPixelTestParam> params;
  for (const auto& sync_param : base_params) {
    for (auto version : versions) {
      for (bool is_first_run : first_run_states) {
        params.push_back({.context_param = {.sync_param = sync_param,
                                            .first_run_version = version},
                          .is_first_run = is_first_run});
      }
    }
  }
  return params;
}

std::vector<SyncConfirmationPixelTestParam> GetDialogPixelTestParams() {
  const SyncConfirmationTestParam base_params[] = {
      {.pixel_test_param = {.test_suffix = "Regular"},
       .sync_style = SyncConfirmationStyle::kDefaultModal},
      {.pixel_test_param = {.test_suffix = "SigninInterceptStyle"},
       .sync_style = SyncConfirmationStyle::kSigninInterceptModal,
       .is_sync_promo = true},
      {.pixel_test_param = {.test_suffix = "DarkTheme", .use_dark_theme = true},
       .sync_style = SyncConfirmationStyle::kDefaultModal},
      {.pixel_test_param = {.test_suffix = "Rtl",
                            .use_right_to_left_language = true},
       .sync_style = SyncConfirmationStyle::kDefaultModal},
      {.pixel_test_param = {.test_suffix = "Promo"},
       .sync_style = SyncConfirmationStyle::kDefaultModal,
       .is_sync_promo = true},
      {.pixel_test_param = {.test_suffix = "ManagedAccount"},
       .account_management_status = AccountManagementStatus::kManaged,
       .sync_style = SyncConfirmationStyle::kDefaultModal},
      {.pixel_test_param = {.test_suffix =
                                "RegularWithRestrictionsWithUnrestrictedUser"},
       .sync_style = SyncConfirmationStyle::kDefaultModal,
       .minor_mode_restrictions = kWithUnrestrictedUser},
      {.pixel_test_param = {.test_suffix =
                                "RegularWithRestrictionsWithRestrictedUser"},
       .sync_style = SyncConfirmationStyle::kDefaultModal,
       .minor_mode_restrictions = kWithRestrictedUser},
  };
  const FirstRunVersion versions[] = {
      FirstRunVersion::kLegacy,
      FirstRunVersion::kRefresh,
      // There is no revamp version for the dialog.
  };
  std::vector<SyncConfirmationPixelTestParam> params;
  for (const auto& sync_param : base_params) {
    for (auto version : versions) {
      params.push_back(
          {.sync_param = sync_param, .first_run_version = version});
    }
  }
  return params;
}

GURL BuildSyncConfirmationWindowURL() {
  std::string url_string = chrome::kChromeUISyncConfirmationURL;
  return AppendSyncConfirmationQueryParams(GURL(url_string),
                                           SyncConfirmationStyle::kWindow,
                                           /*is_sync_promo=*/true);
}

// Creates a step to represent the sync-confirmation.
class SyncConfirmationStepControllerForTest
    : public ProfileManagementStepController {
 public:
  explicit SyncConfirmationStepControllerForTest(
      ProfilePickerWebContentsHost* host)
      : ProfileManagementStepController(host),
        sync_confirmation_url_(BuildSyncConfirmationWindowURL()) {}

  ~SyncConfirmationStepControllerForTest() override = default;

  void Show(StepSwitchFinishedCallback step_shown_callback,
            bool reset_state) override {
    // Reload the WebUI in the picker contents.
    host()->ShowScreenInPickerContents(
        sync_confirmation_url_,
        base::BindOnce(
            &SyncConfirmationStepControllerForTest::OnSyncConfirmationLoaded,
            weak_ptr_factory_.GetWeakPtr(), std::move(step_shown_callback)));
  }

  void OnSyncConfirmationLoaded(
      StepSwitchFinishedCallback step_shown_callback) {
    SyncConfirmationUI* sync_confirmation_ui = static_cast<SyncConfirmationUI*>(
        host()->GetPickerContents()->GetWebUI()->GetController());

    sync_confirmation_ui->InitializeMessageHandlerWithBrowser(nullptr);

    if (!step_shown_callback->is_null()) {
      std::move(step_shown_callback.value()).Run(/*success=*/true);
    }
  }

 private:
  const GURL sync_confirmation_url_;
  base::WeakPtrFactory<SyncConfirmationStepControllerForTest> weak_ptr_factory_{
      this};
};

template <typename T>
class SyncConfirmationPixelTestBase : public ProfilesPixelTestBaseT<T> {
 public:
  template <typename... Args>
  SyncConfirmationPixelTestBase(const PixelTestParam& pixel_test_param,
                                FirstRunVersion first_run_version,
                                Args&&... args)
      : ProfilesPixelTestBaseT<T>(pixel_test_param,
                                  std::forward<Args>(args)...) {
    std::vector<base::test::FeatureRef> enabled_features;
    std::vector<base::test::FeatureRef> disabled_features;
    switch (first_run_version) {
      case FirstRunVersion::kLegacy:
        disabled_features.push_back(switches::kFirstRunDesktopRefresh);
        disabled_features.push_back(switches::kFirstRunDesktopRevamp);
        break;
      case FirstRunVersion::kRefresh:
        enabled_features.push_back(switches::kFirstRunDesktopRefresh);
        enabled_features.push_back(
            switches::kFirstRunDesktopChoiceScreenRefresh);
        disabled_features.push_back(switches::kFirstRunDesktopRevamp);
        break;
      case FirstRunVersion::kRevamp:
        enabled_features.push_back(switches::kFirstRunDesktopRefresh);
        enabled_features.push_back(
            switches::kFirstRunDesktopChoiceScreenRefresh);
        enabled_features.push_back(switches::kFirstRunDesktopRevamp);
        break;
    }
    scoped_feature_list_.InitWithFeatures(enabled_features, disabled_features);
  }

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

}  // namespace

class SyncConfirmationUIWindowPixelTest
    : public SyncConfirmationPixelTestBase<UiBrowserTest>,
      public testing::WithParamInterface<SyncConfirmationWindowPixelTestParam> {
 public:
  SyncConfirmationUIWindowPixelTest()
      : SyncConfirmationPixelTestBase<UiBrowserTest>(
            GetParam().context_param.sync_param.pixel_test_param,
            GetParam().context_param.first_run_version) {
    DCHECK(GetParam().context_param.sync_param.sync_style ==
           SyncConfirmationStyle::kWindow);
  }

  void ShowUi(const std::string& name) override {
    gfx::ScopedAnimationDurationScaleMode disable_animation(
        gfx::ScopedAnimationDurationScaleMode::ZERO_DURATION);
    DCHECK(browser());

    const SyncConfirmationTestParam& sync_param =
        GetParam().context_param.sync_param;

    SignInWithAccount(sync_param.account_management_status,
                      signin::ConsentLevel::kSignin,
                      sync_param.minor_mode_restrictions.value());
    profile_picker_view_ = new ProfileManagementStepTestView(
        ProfilePicker::Params::ForTesting(
            GetParam().is_first_run ? ProfilePicker::EntryPoint::kFirstRun
                                    : ProfilePicker::EntryPoint::kOnStartup,
            browser()->GetProfile()->GetPath()),
        ProfileManagementFlowController::Step::kPostSignInFlow,
        /*step_controller_factory=*/
        base::BindRepeating([](ProfilePickerWebContentsHost* host) {
          return std::unique_ptr<ProfileManagementStepController>(
              new SyncConfirmationStepControllerForTest(host));
        }));
    profile_picker_view_->ShowAndWait(sync_param.pixel_test_param.window_size);
  }

  bool VerifyUi() override {
    views::Widget* widget = GetWidgetForScreenshot();

    auto* test_info = testing::UnitTest::GetInstance()->current_test_info();
    const std::string screenshot_name =
        base::StrCat({test_info->test_suite_name(), "_", test_info->name()});

    return VerifyPixelUi(widget, "SyncConfirmationUIWindowPixelTest",
                         screenshot_name) != ui::test::ActionResult::kFailed;
  }

  void WaitForUserDismissal() override {
    DCHECK(GetWidgetForScreenshot());
    ViewDeletedWaiter(profile_picker_view_).Wait();
  }

 private:
  views::Widget* GetWidgetForScreenshot() {
    return profile_picker_view_->GetWidget();
  }

  raw_ptr<ProfileManagementStepTestView, DanglingUntriaged>
      profile_picker_view_;
};

IN_PROC_BROWSER_TEST_P(SyncConfirmationUIWindowPixelTest, InvokeUi_default) {
  ShowAndVerifyUi();
}

INSTANTIATE_TEST_SUITE_P(
    ,
    SyncConfirmationUIWindowPixelTest,
    ValuesIn(GetWindowPixelTestParams()),
    [](const testing::TestParamInfo<SyncConfirmationWindowPixelTestParam>&
           info) { return ParamToTestSuffix(info.param); });

class SyncConfirmationUIDialogPixelTest
    : public SyncConfirmationPixelTestBase<DialogBrowserTest>,
      public testing::WithParamInterface<SyncConfirmationPixelTestParam> {
 public:
  SyncConfirmationUIDialogPixelTest()
      : SyncConfirmationPixelTestBase<DialogBrowserTest>(
            GetParam().sync_param.pixel_test_param,
            GetParam().first_run_version) {
    DCHECK(GetParam().sync_param.sync_style != SyncConfirmationStyle::kWindow);
  }

  ~SyncConfirmationUIDialogPixelTest() override = default;

  // DialogBrowserTest:
  void ShowUi(const std::string& name) override {
    DCHECK(browser());

    const SyncConfirmationTestParam& sync_param = GetParam().sync_param;
    SignInWithAccount(sync_param.account_management_status,
                      signin::ConsentLevel::kSignin,
                      sync_param.minor_mode_restrictions.value());
    auto url = GURL(chrome::kChromeUISyncConfirmationURL);
    url = AppendSyncConfirmationQueryParams(url, sync_param.sync_style,
                                            sync_param.is_sync_promo);
    content::TestNavigationObserver observer(url);
    observer.StartWatchingNewWebContents();

    // ShowUi() can sometimes return before the dialog widget is shown because
    // the call to show the latter is asynchronous. Adding
    // NamedWidgetShownWaiter will prevent that from happening.
    views::NamedWidgetShownWaiter widget_waiter(
        views::test::AnyWidgetTestPasskey{},
        "SigninViewControllerDelegateViews");

    auto* controller = browser()->GetFeatures().signin_view_controller();
    controller->ShowModalSyncConfirmationDialog(
        sync_param.sync_style == SyncConfirmationStyle::kSigninInterceptModal,
        sync_param.is_sync_promo);
    widget_waiter.WaitIfNeededAndGet();
    observer.Wait();
  }
};

IN_PROC_BROWSER_TEST_P(SyncConfirmationUIDialogPixelTest, InvokeUi_default) {
#if BUILDFLAG(IS_WIN)
  if (base::FeatureList::IsEnabled(features::kInitialWebUI)) {
    GTEST_SKIP() << "Skipping test because it fails with InitialWebUI enabled. "
                    "See b/477426026.";
  }
#endif
  ShowAndVerifyUi();
}

INSTANTIATE_TEST_SUITE_P(
    ,
    SyncConfirmationUIDialogPixelTest,
    ValuesIn(GetDialogPixelTestParams()),
    [](const testing::TestParamInfo<SyncConfirmationPixelTestParam>& info) {
      return ParamToTestSuffix(info.param);
    });

enum class SyncConfirmationUIAction { kTurnSyncOn, kGoToSettings };

class SyncConfirmationUITest
    : public SigninBrowserTestBase,
      public testing::WithParamInterface<
          std::tuple<bool, SyncConfirmationUIAction, std::string>>,
      public LoginUIService::Observer {
 public:
  void SetUpOnMainThread() override {
    SigninBrowserTestBase::SetUpOnMainThread();
    CHECK(GetProfile());
    // The test should close the sync confirmation dialog once the observer
    // method is called to simulate the real behavior more closely.
    login_ui_service_observation_.Observe(
        LoginUIServiceFactory::GetForProfile(GetProfile()));
  }

  void TearDownOnMainThread() override {
    // Stop observing the LoginUIService before destroying the profile.
    login_ui_service_observation_.Reset();
    SigninBrowserTestBase::TearDownOnMainThread();
  }

  void SetUpCommandLine(base::CommandLine* command_line) override {
    SigninBrowserTestBase::SetUpCommandLine(command_line);

    if (GetLanguage().empty()) {
      return;
    }
    command_line->AppendSwitchASCII(switches::kLang, GetLanguage());

    // On Linux the command line switch has no effect, we need to use
    // environment variables to change the language.
    scoped_env_override_ =
        std::make_unique<base::ScopedEnvironmentVariableOverride>(
            "LANGUAGE", GetLanguage());
  }

  // LoginUIService::Observer:
  void OnSyncConfirmationUIClosed(
      LoginUIService::SyncConfirmationUIClosedResult result) override {
    browser()->GetFeatures().signin_view_controller()->CloseModalSignin();
  }

  [[nodiscard]] AccountInfo FillAccountInfoWithEscapedHtmlCharacters(
      const AccountInfo& account_info) {
    // The account name contains characters that are escaped in HTML.
    std::string name = "The name's <>&\"', James\u00a0<>&\"'";
    AccountInfo new_account_info =
        AccountInfo::Builder(account_info)
            .SetFullName(name)
            .SetGivenName(name)
            .SetHostedDomain(std::string())
            .SetAvatarUrl("https://example.org/avatar")
            .Build();
    CHECK(new_account_info.IsValid());
    return new_account_info;
  }

  bool IsSigninIntercept() { return std::get<0>(GetParam()); }

  SyncConfirmationUIAction GetAction() { return std::get<1>(GetParam()); }

  std::string GetLanguage() { return std::get<2>(GetParam()); }

  consent_auditor::FakeConsentAuditor* consent_auditor() {
    return static_cast<consent_auditor::FakeConsentAuditor*>(
        ConsentAuditorFactory::GetForProfile(GetProfile()));
  }

  int GetActionButtonLabelId() {
    switch (GetAction()) {
      case SyncConfirmationUIAction::kTurnSyncOn:
        return IsSigninIntercept()
                   ? IDS_SYNC_CONFIRMATION_TURN_ON_SYNC_BUTTON_LABEL
                   : IDS_SYNC_CONFIRMATION_CONFIRM_BUTTON_LABEL;
      case SyncConfirmationUIAction::kGoToSettings:
        return IDS_SYNC_CONFIRMATION_SETTINGS_BUTTON_LABEL;
    }
  }

  int GetTitleId() {
    return IsSigninIntercept()
               ? IDS_SYNC_CONFIRMATION_TANGIBLE_SYNC_INFO_TITLE_SIGNIN_INTERCEPT_V2
               : IDS_SYNC_CONFIRMATION_TANGIBLE_SYNC_INFO_TITLE;
  }

  int GetDescriptionId() {
    return IDS_SYNC_CONFIRMATION_TANGIBLE_SYNC_INFO_DESC;
  }

 protected:
  void OnWillCreateBrowserContextServices(
      content::BrowserContext* context) override {
    SigninBrowserTestBase::OnWillCreateBrowserContextServices(context);
    ConsentAuditorFactory::GetInstance()->SetTestingFactory(
        context, base::BindRepeating(&BuildFakeConsentAuditor));
  }

 private:
  base::ScopedObservation<LoginUIService, LoginUIService::Observer>
      login_ui_service_observation_{this};
  std::unique_ptr<base::ScopedEnvironmentVariableOverride> scoped_env_override_;
};

// Regression test for https://crbug.com/325749258.
IN_PROC_BROWSER_TEST_P(SyncConfirmationUITest,
                       RecordConsentWithEscapedHtmlCharacters) {
  AccountInfo account_info = identity_test_env()->MakePrimaryAccountAvailable(
      "test@gmail.com", signin::ConsentLevel::kSignin);
  account_info = FillAccountInfoWithEscapedHtmlCharacters(account_info);
  identity_test_env()->UpdateAccountInfoForAccount(account_info);

  browser()
      ->GetFeatures()
      .signin_view_controller()
      ->ShowModalSyncConfirmationDialog(IsSigninIntercept(),
                                        /*is_sync_promo=*/true);
  switch (GetAction()) {
    case SyncConfirmationUIAction::kTurnSyncOn:
      EXPECT_TRUE(
          login_ui_test_utils::ConfirmSyncConfirmationDialog(browser()));
      break;
    case SyncConfirmationUIAction::kGoToSettings:
      EXPECT_TRUE(
          login_ui_test_utils::GoToSettingsSyncConfirmationDialog(browser()));
      break;
  }

  EXPECT_THAT(consent_auditor()->recorded_confirmation_ids(),
              ElementsAre(GetActionButtonLabelId()));
  EXPECT_THAT(
      consent_auditor()->recorded_id_vectors(),
      ElementsAre(AllOf(Contains(GetTitleId()), Contains(GetDescriptionId()))));
  EXPECT_THAT(consent_auditor()->recorded_features(),
              ElementsAre(consent_auditor::Feature::CHROME_SYNC));
}

std::string SyncConfirmationUITestParamToTestSuffix(
    const testing::TestParamInfo<SyncConfirmationUITest::ParamType>& info) {
  auto [is_signin_intercept, action, language] = info.param;
  return base::StrCat({language, is_signin_intercept ? "Intercept" : "",
                       action == SyncConfirmationUIAction::kTurnSyncOn
                           ? "Accept"
                           : "GoToSettings"});
}

INSTANTIATE_TEST_SUITE_P(
    ,
    SyncConfirmationUITest,
    testing::Combine(testing::Bool(),
                     testing::Values(SyncConfirmationUIAction::kTurnSyncOn,
                                     SyncConfirmationUIAction::kGoToSettings),
                     testing::Values("", "pl")),
    &SyncConfirmationUITestParamToTestSuffix);
