// Copyright 2020 The Chromium Authors
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.

#include "base/check.h"
#include "base/run_loop.h"
#include "base/test/metrics/histogram_tester.h"
#include "base/test/metrics/user_action_tester.h"
#include "build/build_config.h"
#include "chrome/app/chrome_command_ids.h"
#include "chrome/browser/lifetime/browser_shutdown.h"
#include "chrome/browser/profiles/profile_metrics.h"
#include "chrome/browser/search_engine_choice/search_engine_choice_dialog_service.h"
#include "chrome/browser/search_engine_choice/search_engine_choice_dialog_service_factory.h"
#include "chrome/browser/ui/profiles/profile_picker.h"
#include "chrome/browser/ui/views/profiles/profile_picker_interactive_uitest_base.h"
#include "chrome/browser/ui/views/profiles/profile_picker_test_base.h"
#include "chrome/browser/ui/views/profiles/profile_picker_view.h"
#include "chrome/common/webui_url_constants.h"
#include "chrome/test/base/interactive_test_utils.h"
#include "chrome/test/interaction/interactive_browser_test.h"
#include "components/regional_capabilities/regional_capabilities_switches.h"
#include "components/search_engines/search_engine_choice/search_engine_choice_utils.h"
#include "components/search_engines/search_engines_switches.h"
#include "components/signin/public/base/signin_switches.h"
#include "components/variations/variations_switches.h"
#include "content/public/browser/navigation_controller.h"
#include "content/public/browser/navigation_entry.h"
#include "content/public/browser/web_contents.h"
#include "content/public/test/browser_test.h"
#include "content/public/test/browser_test_utils.h"
#include "testing/gtest/include/gtest/gtest.h"
#include "ui/views/view.h"
#include "ui/views/view_class_properties.h"
#include "ui/views/widget/widget.h"
#include "ui/views/widget/widget_observer.h"

using testing::Eq;
using testing::IsFalse;
using testing::IsTrue;

namespace {

DEFINE_LOCAL_ELEMENT_IDENTIFIER_VALUE(kProfilePickerViewId);
DEFINE_LOCAL_ELEMENT_IDENTIFIER_VALUE(kPickerWebContentsId);
DEFINE_LOCAL_CUSTOM_ELEMENT_EVENT_TYPE(kButtonEnabled);
DEFINE_LOCAL_CUSTOM_ELEMENT_EVENT_TYPE(kButtonDisabled);

const WebContentsInteractionTestUtil::DeepQuery kSignInButton = {
    "profile-picker-app", "profile-type-choice", "#signInButton"};
const WebContentsInteractionTestUtil::DeepQuery kContinueWithoutAccountButton =
    {"profile-picker-app", "profile-type-choice", "#notNowButton"};
const WebContentsInteractionTestUtil::DeepQuery kAddProfileButton = {
    "profile-picker-app", "profile-picker-main-view", "#addProfile"};

// Waits until the widget bounds change.
class WidgetBoundsChangeWaiter : public views::WidgetObserver {
 public:
  explicit WidgetBoundsChangeWaiter(views::Widget* widget) {
    DCHECK(widget);
    observation_.Observe(widget);
  }

  // Waits until the widget bounds change.
  void Wait() { run_loop_.Run(); }

 private:
  // WidgetObserver:
  void OnWidgetBoundsChanged(views::Widget* widget,
                             const gfx::Rect& new_bounds) override {
    run_loop_.Quit();
  }

  base::RunLoop run_loop_;
  base::ScopedObservation<views::Widget, views::WidgetObserver> observation_{
      this};
};

}  // namespace

struct NavState {
  int entry_count;
  int last_committed_entry_index;
  bool has_pending_entry = false;

  bool operator==(const NavState& other) const {
    return entry_count == other.entry_count &&
           last_committed_entry_index == other.last_committed_entry_index &&
           has_pending_entry == other.has_pending_entry;
  }

  friend std::ostream& operator<<(std::ostream& stream, const NavState& state) {
    return stream << "{entry_count=" << state.entry_count
                  << ", last_committed_entry_index="
                  << state.last_committed_entry_index
                  << ", has_pending_entry=" << state.has_pending_entry << "}";
  }
};

class ProfilePickerInteractiveUiTest
    : public InteractiveBrowserTest,
      public WithProfilePickerInteractiveUiTestHelpers {
 public:
  ProfilePickerInteractiveUiTest() {
    scoped_chrome_build_override_ = std::make_unique<base::AutoReset<bool>>(
        SearchEngineChoiceDialogServiceFactory::
            ScopedChromeBuildOverrideForTesting(
                /*force_chrome_build=*/true));
  }

  ~ProfilePickerInteractiveUiTest() override = default;

  void ShowAndFocusPicker(ProfilePicker::EntryPoint entry_point,
                          const GURL& expected_url = GURL()) {
    ProfilePicker::Show(ProfilePicker::Params::FromEntryPoint(entry_point));
    WaitForPickerWidgetCreated();
    view()->SetProperty(views::kElementIdentifierKey, kProfilePickerViewId);
    if (!expected_url.is_empty()) {
      WaitForLoadStop(expected_url);
    }

    EXPECT_TRUE(
        ui_test_utils::ShowAndFocusNativeWindow(widget()->GetNativeWindow()));
  }

  auto GetNavState() {
    return [this]() {
      return NavState{
          .entry_count = web_contents()->GetController().GetEntryCount(),
          .last_committed_entry_index =
              web_contents()->GetController().GetLastCommittedEntryIndex(),
          .has_pending_entry =
              web_contents()->GetController().GetPendingEntry() != nullptr,
      };
    };
  }

  StateChange Exists(const DeepQuery& where) {
    DEFINE_LOCAL_CUSTOM_ELEMENT_EVENT_TYPE(kElementExistsEvent);
    StateChange state_change;
    state_change.type = StateChange::Type::kExists;
    state_change.where = where;
    state_change.event = kElementExistsEvent;
    return state_change;
  }

  // Used to wait for screen changes inside of the `profile-picker-app`, in
  // combination with `WaitForStateChange()`.
  //
  // In the profile picker app we modify the displayed URL but don't do a full
  // navigation, so `WaitForWebContentsNavigation()` does not detect these
  // changes.
  StateChange UrlEntryMatches(const GURL& url) {
    DEFINE_LOCAL_CUSTOM_ELEMENT_EVENT_TYPE(kUrlEntryMatchesEvent);
    StateChange state_change;
    state_change.test_function = base::StringPrintf(
        "(_) => navigation && navigation.currentEntry && "
        "navigation.currentEntry.url === '%s'",
        url.spec().c_str());
    state_change.type = StateChange::Type::kConditionTrue;
    state_change.event = kUrlEntryMatchesEvent;
    return state_change;
  }

  void SetUpOnMainThread() override {
    InteractiveBrowserTest::SetUpOnMainThread();
    SearchEngineChoiceDialogService::SetDialogDisabledForTests(
        /*dialog_disabled=*/false);
  }

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

    // Change the country to belgium because the search engine choice screen
    // is only displayed for EEA countries.
    command_line->AppendSwitchASCII(switches::kSearchEngineChoiceCountry, "BE");
    command_line->AppendSwitchASCII(
        variations::switches::kVariationsOverrideCountry, "BE");
    command_line->AppendSwitch(
        switches::kIgnoreNoFirstRunForSearchEngineChoiceScreen);
  }

  const base::HistogramTester& HistogramTester() const {
    return histogram_tester_;
  }

  const base::UserActionTester& UserActionTester() const {
    return user_action_tester_;
  }

  auto WaitForButtonEnabled(const ui::ElementIdentifier web_contents_id,
                            const DeepQuery& button_query) {
    StateChange button_enabled;
    button_enabled.event = kButtonEnabled;
    button_enabled.where = button_query;
    button_enabled.type = StateChange::Type::kExistsAndConditionTrue;
    button_enabled.test_function = "(btn) => !btn.disabled";
    return WaitForStateChange(web_contents_id, button_enabled);
  }

  auto WaitForButtonDisabled(const ui::ElementIdentifier web_contents_id,
                             const DeepQuery& button_query) {
    StateChange button_disabled;
    button_disabled.event = kButtonDisabled;
    button_disabled.where = button_query;
    button_disabled.type = StateChange::Type::kExistsAndConditionTrue;
    button_disabled.test_function = "(btn) => btn.disabled";
    return WaitForStateChange(web_contents_id, button_disabled);
  }

  auto PressJsButton(const ui::ElementIdentifier web_contents_id,
                     const DeepQuery& button_query) {
    // This can close/navigate the current page, so don't wait for success.
    return ExecuteJsAt(web_contents_id, button_query, "(btn) => btn.click()",
                       ExecuteJsMode::kFireAndForget);
  }

  const DeepQuery& GetSearchEngineChoiceActionButtonQuery() {
    if (base::FeatureList::IsEnabled(switches::kFirstRunDesktopRefresh) &&
        base::FeatureList::IsEnabled(
            switches::kFirstRunDesktopChoiceScreenRefresh)) {
      static const base::NoDestructor<DeepQuery> kQuery(
          {"search-engine-choice-app-refresh", "#actionButton"});
      return *kQuery;
    }

    static const base::NoDestructor<DeepQuery> kQuery(
        {"search-engine-choice-app", "#actionButton"});
    return *kQuery;
  }

  const DeepQuery& GetSearchEngineChoiceCrRadioButtonQuery() {
    if (base::FeatureList::IsEnabled(switches::kFirstRunDesktopRefresh) &&
        base::FeatureList::IsEnabled(
            switches::kFirstRunDesktopChoiceScreenRefresh)) {
      static const base::NoDestructor<DeepQuery> kQuery(
          {"search-engine-choice-app-refresh", "cr-radio-button"});
      return *kQuery;
    }

    static const base::NoDestructor<DeepQuery> kQuery(
        {"search-engine-choice-app", "cr-radio-button"});
    return *kQuery;
  }

  auto CompleteSearchEngineChoiceStep() {
    return Steps(
        WaitForWebContentsNavigation(
            kPickerWebContentsId, GURL(chrome::kChromeUISearchEngineChoiceURL)),

        Do([&] {
          HistogramTester().ExpectBucketCount(
              search_engines::kSearchEngineChoiceScreenEventsHistogram,
              search_engines::SearchEngineChoiceScreenEvents::
                  kProfileCreationChoiceScreenWasDisplayed,
              1);
          EXPECT_EQ(UserActionTester().GetActionCount(
                        "SearchEngineChoiceScreenShown"),
                    1);
        }),

        // Click on "More" to scroll to the bottom of the search engine list.
        PressJsButton(kPickerWebContentsId,
                      GetSearchEngineChoiceActionButtonQuery()),

        // The button should become disabled because we didn't make a choice.
        WaitForButtonDisabled(kPickerWebContentsId,
                              GetSearchEngineChoiceActionButtonQuery()),

        PressJsButton(kPickerWebContentsId,
                      GetSearchEngineChoiceCrRadioButtonQuery()),
        WaitForButtonEnabled(kPickerWebContentsId,
                             GetSearchEngineChoiceActionButtonQuery()),
        PressJsButton(kPickerWebContentsId,
                      GetSearchEngineChoiceActionButtonQuery()));
  }

 private:
  std::unique_ptr<base::AutoReset<bool>> scoped_chrome_build_override_;
  base::UserActionTester user_action_tester_;
  base::HistogramTester histogram_tester_;
};

// Checks that the main picker view can be closed with keyboard shortcut.
IN_PROC_BROWSER_TEST_F(ProfilePickerInteractiveUiTest, CloseWithKeyboard) {
  // Open a new picker.
  ShowAndFocusPicker(ProfilePicker::EntryPoint::kProfileMenuManageProfiles,
                     GURL("chrome://profile-picker"));
  EXPECT_TRUE(ProfilePicker::IsOpen());
  SendCloseWindowKeyboardCommand();
  WaitForPickerClosed();
  // Closing the picker does not exit Chrome.
  EXPECT_FALSE(browser_shutdown::IsTryingToQuit());
}

#if BUILDFLAG(IS_MAC)
// Checks that Chrome be closed with keyboard shortcut. Only MacOS has a
// keyboard shortcut to exit Chrome.
IN_PROC_BROWSER_TEST_F(ProfilePickerInteractiveUiTest, ExitWithKeyboard) {
  // Open a new picker.
  ShowAndFocusPicker(ProfilePicker::EntryPoint::kProfileMenuManageProfiles,
                     GURL("chrome://profile-picker"));
  EXPECT_TRUE(ProfilePicker::IsOpen());

  SendQuitAppKeyboardCommand();
  WaitForPickerClosed();
  EXPECT_TRUE(browser_shutdown::IsTryingToQuit());
}
#endif

// Checks that the main picker view can switch to full screen.
IN_PROC_BROWSER_TEST_F(ProfilePickerInteractiveUiTest, FullscreenWithKeyboard) {
  // Open a new picker.
  ShowAndFocusPicker(ProfilePicker::EntryPoint::kProfileMenuManageProfiles,
                     GURL("chrome://profile-picker"));
  EXPECT_TRUE(ProfilePicker::IsOpen());
  EXPECT_FALSE(widget()->IsFullscreen());
  WidgetBoundsChangeWaiter bounds_waiter(widget());

  SendToggleFullscreenKeyboardCommand();

  // Fullscreen causes the bounds of the widget to change.
  bounds_waiter.Wait();
  EXPECT_TRUE(widget()->IsFullscreen());
}

IN_PROC_BROWSER_TEST_F(ProfilePickerInteractiveUiTest,
                       CloseDiceSigninWithKeyboard) {
  ShowAndFocusPicker(ProfilePicker::EntryPoint::kProfileMenuAddNewProfile);

  RunTestSequenceInContext(
      views::ElementTrackerViews::GetContextForView(view()),

      // Wait for the profile picker to show the profile type choice screen.
      WaitForShow(kProfilePickerViewId),
      InstrumentNonTabWebView(kPickerWebContentsId, web_view()),
      WaitForWebContentsReady(kPickerWebContentsId,
                              GURL("chrome://profile-picker/new-profile")),

      // Click "sign in".
      // Note: the button should be disabled after this, but there is no good
      // way to verify it in this sequence. It is verified by unit tests in
      // chrome/test/data/webui/signin/profile_picker_app_test.ts
      WaitForButtonEnabled(kPickerWebContentsId, kSignInButton),
      MoveMouseTo(kPickerWebContentsId, kSignInButton), ClickMouse(),

      // Wait for switch to the Gaia sign-in page to complete.
      // Note: kPickerWebContentsId now points to the new profile's WebContents.
      WaitForWebContentsNavigation(kPickerWebContentsId,
                                   GetSigninChromeSyncDiceUrl()),

      // Send "Close window" keyboard shortcut and wait for view to close.
      SendAccelerator(kProfilePickerViewId, GetAccelerator(IDC_CLOSE_WINDOW))
          .SetMustRemainVisible(false));

  WaitForPickerClosed();
}

// Checks that both the signin web view and the main picker view are able to
// process a back keyboard event.
IN_PROC_BROWSER_TEST_F(ProfilePickerInteractiveUiTest,
                       NavigateBackFromDiceSigninWithKeyboard) {
  // Simulate walking through the flow starting at the picker so that navigating
  // back to the picker makes sense. Check that the navigation list is populated
  // correctly.
  ShowAndFocusPicker(ProfilePicker::EntryPoint::kProfileMenuManageProfiles,
                     GURL("chrome://profile-picker"));

  RunTestSequenceInContext(
      views::ElementTrackerViews::GetContextForView(view()),

      WaitForShow(kProfilePickerViewId),
      InstrumentNonTabWebView(kPickerWebContentsId, web_view()),
      WaitForWebContentsReady(kPickerWebContentsId,
                              GURL("chrome://profile-picker")),
      CheckResult(GetNavState(), Eq(NavState{.entry_count = 1,
                                             .last_committed_entry_index = 0})),

      // Advance to the profile type choice screen.
      WaitForButtonEnabled(kPickerWebContentsId, kAddProfileButton),
      MoveMouseTo(kPickerWebContentsId, kAddProfileButton), ClickMouse(),
      WaitForStateChange(
          kPickerWebContentsId,
          UrlEntryMatches(GURL("chrome://profile-picker/new-profile"))),
      CheckResult(GetNavState(), Eq(NavState{.entry_count = 2,
                                             .last_committed_entry_index = 1})),

      // Advance to the sign-in page.
      WaitForButtonEnabled(kPickerWebContentsId, kSignInButton),
      MoveMouseTo(kPickerWebContentsId, kSignInButton), ClickMouse(),
      WaitForWebContentsNavigation(kPickerWebContentsId,
                                   GetSigninChromeSyncDiceUrl()),

      // Navigate back with the keyboard, switch back to the picker.
      SendAccelerator(kProfilePickerViewId, GetAccelerator(IDC_BACK)),
      WaitForStateChange(
          kPickerWebContentsId,
          UrlEntryMatches(GURL("chrome://profile-picker/new-profile"))),
      CheckResult(GetNavState(), Eq(NavState{.entry_count = 2,
                                             .last_committed_entry_index = 1})),

      // Navigate again back with the keyboard.
      SendAccelerator(kProfilePickerViewId, GetAccelerator(IDC_BACK)),
      WaitForStateChange(kPickerWebContentsId,
                         UrlEntryMatches(GURL("chrome://profile-picker"))),
      CheckResult(GetNavState(), Eq(NavState{.entry_count = 2,
                                             .last_committed_entry_index = 0})),

      // Navigating back once again does nothing.
      CheckResult(
          [this] { return web_contents()->GetController().CanGoBack(); },
          IsFalse()),
      SendAccelerator(kProfilePickerViewId, GetAccelerator(IDC_BACK)),
      CheckResult(GetNavState(), Eq(NavState{.entry_count = 2,
                                             .last_committed_entry_index = 0,
                                             .has_pending_entry = false})));
}

IN_PROC_BROWSER_TEST_F(ProfilePickerInteractiveUiTest,
                       NavigateBackFromProfileTypeChoiceWithKeyboard) {
  // Simulate walking through the flow starting at the picker so that navigating
  // back to the picker makes sense. Check that the navigation list is populated
  // correctly.
  ShowAndFocusPicker(ProfilePicker::EntryPoint::kProfileMenuManageProfiles,
                     GURL("chrome://profile-picker"));

  RunTestSequenceInContext(
      views::ElementTrackerViews::GetContextForView(view()),

      WaitForShow(kProfilePickerViewId),
      InstrumentNonTabWebView(kPickerWebContentsId, web_view()),
      WaitForWebContentsReady(kPickerWebContentsId,
                              GURL("chrome://profile-picker")),
      CheckResult(GetNavState(), Eq(NavState{.entry_count = 1,
                                             .last_committed_entry_index = 0})),

      // Advance to the profile type choice screen.
      WaitForButtonEnabled(kPickerWebContentsId, kAddProfileButton),
      MoveMouseTo(kPickerWebContentsId, kAddProfileButton), ClickMouse(),
      WaitForStateChange(
          kPickerWebContentsId,
          UrlEntryMatches(GURL("chrome://profile-picker/new-profile"))),
      CheckResult(GetNavState(), Eq(NavState{.entry_count = 2,
                                             .last_committed_entry_index = 1})),

      // Navigate back with the keyboard.
      SendAccelerator(kProfilePickerViewId, GetAccelerator(IDC_BACK)),
      WaitForStateChange(kPickerWebContentsId,
                         UrlEntryMatches(GURL("chrome://profile-picker"))),
      CheckResult(GetNavState(), Eq(NavState{.entry_count = 2,
                                             .last_committed_entry_index = 0})),

      // Navigating back once again does nothing.
      CheckResult(
          [this] { return web_contents()->GetController().CanGoBack(); },
          IsFalse()),
      SendAccelerator(kProfilePickerViewId, GetAccelerator(IDC_BACK)),
      CheckResult(GetNavState(), Eq(NavState{.entry_count = 2,
                                             .last_committed_entry_index = 0,
                                             .has_pending_entry = false})));
}

IN_PROC_BROWSER_TEST_F(ProfilePickerInteractiveUiTest,
                       NavigateBackFromNewProfileWithKeyboard) {
  // Check that when deep-linking into the flow via the "Add profile" menu entry
  // populates the navigation list is populated correctly such that back
  // navigations make sense.
  ShowAndFocusPicker(ProfilePicker::EntryPoint::kProfileMenuAddNewProfile,
                     GURL("chrome://profile-picker/new-profile"));

  RunTestSequenceInContext(
      views::ElementTrackerViews::GetContextForView(view()),

      WaitForShow(kProfilePickerViewId),
      InstrumentNonTabWebView(kPickerWebContentsId, web_view()),
      WaitForWebContentsReady(kPickerWebContentsId,
                              GURL("chrome://profile-picker/new-profile")),

      // Even though we start straight on the "new-profile" page, the picker
      // main view should be loaded under it in the nav stack.
      CheckResult(GetNavState(), Eq(NavState{.entry_count = 2,
                                             .last_committed_entry_index = 1})),

      // Focus the window to ensure the keyboard shortcut reaches it.
      FocusWebContents(kPickerWebContentsId),

      // Navigate back with the keyboard.
      SendAccelerator(kPickerWebContentsId, GetAccelerator(IDC_BACK)),
      WaitForStateChange(kPickerWebContentsId,
                         UrlEntryMatches(GURL("chrome://profile-picker"))),
      CheckResult(GetNavState(), Eq(NavState{.entry_count = 2,
                                             .last_committed_entry_index = 0})),

      // Navigating back once again does nothing.
      CheckResult(
          [this] { return web_contents()->GetController().CanGoBack(); },
          IsFalse()),
      SendAccelerator(kProfilePickerViewId, GetAccelerator(IDC_BACK)),
      CheckResult(GetNavState(), Eq(NavState{.entry_count = 2,
                                             .last_committed_entry_index = 0,
                                             .has_pending_entry = false})));
}

IN_PROC_BROWSER_TEST_F(ProfilePickerInteractiveUiTest, ContinueWithoutAccount) {
  ShowAndFocusPicker(ProfilePicker::EntryPoint::kProfileMenuManageProfiles,
                     GURL("chrome://profile-picker"));

  RunTestSequenceInContext(
      views::ElementTrackerViews::GetContextForView(view()),

      WaitForShow(kProfilePickerViewId),
      InstrumentNonTabWebView(kPickerWebContentsId, web_view()),
      WaitForWebContentsReady(kPickerWebContentsId,
                              GURL("chrome://profile-picker")),
      CheckResult(GetNavState(), Eq(NavState{.entry_count = 1,
                                             .last_committed_entry_index = 0})),

      // Advance to the profile type choice screen.
      EnsurePresent(kPickerWebContentsId, kAddProfileButton),
      PressJsButton(kPickerWebContentsId, kAddProfileButton)
          .SetMustRemainVisible(false),
      WaitForStateChange(
          kPickerWebContentsId,
          UrlEntryMatches(GURL("chrome://profile-picker/new-profile"))),
      CheckResult(GetNavState(), Eq(NavState{.entry_count = 2,
                                             .last_committed_entry_index = 1})),

      // Advance to the post signed out flow
      WaitForStateChange(kPickerWebContentsId,
                         Exists(kContinueWithoutAccountButton)),
      PressJsButton(kPickerWebContentsId, kContinueWithoutAccountButton)
          .SetMustRemainVisible(false),

      CompleteSearchEngineChoiceStep());

  WaitForPickerClosed();

  HistogramTester().ExpectUniqueSample(
      "Profile.AddNewUser", ProfileMetrics::ADD_NEW_PROFILE_PICKER_LOCAL, 1);

  HistogramTester().ExpectBucketCount(
      search_engines::kSearchEngineChoiceScreenEventsHistogram,
      search_engines::SearchEngineChoiceScreenEvents::
          kProfileCreationDefaultWasSet,
      1);
}
