// 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/views/profiles/profile_picker_view_test_utils.h"

#include <string>
#include <string_view>

#include "base/feature_list.h"
#include "base/files/file_path.h"
#include "base/functional/bind.h"
#include "base/location.h"
#include "base/notreached.h"
#include "base/run_loop.h"
#include "base/strings/stringprintf.h"
#include "build/build_config.h"
#include "chrome/browser/profiles/profile.h"
#include "chrome/browser/profiles/profile_test_util.h"
#include "chrome/browser/profiles/profile_window.h"
#include "chrome/browser/ui/profiles/profile_picker.h"
#include "chrome/browser/ui/profiles/profile_ui_test_utils.h"
#include "chrome/browser/ui/views/profiles/first_run_flow_controller.h"
#include "chrome/browser/ui/views/profiles/profile_management_step_controller.h"
#include "chrome/browser/ui/views/profiles/profile_management_types.h"
#include "chrome/browser/ui/views/profiles/profile_picker_view.h"
#include "chrome/browser/ui/webui/signin/managed_user_profile_notice_handler.h"
#include "chrome/browser/ui/webui/signin/managed_user_profile_notice_ui.h"
#include "chrome/test/base/ui_test_utils.h"
#include "components/signin/public/base/signin_switches.h"
#include "content/public/browser/web_contents.h"
#include "ui/views/controls/webview/webview.h"
#include "ui/views/view.h"

#if BUILDFLAG(IS_ANDROID)
#error This file should only be included on desktop.
#endif

namespace {

content::WebContents* GetPickerWebContents() {
  if (!ProfilePicker::GetWebViewForTesting()) {
    return nullptr;
  }
  return ProfilePicker::GetWebViewForTesting()->GetWebContents();
}

template <typename BaseFlowController>
class TestFlowControllerMixin : public BaseFlowController,
                                public content::WebContentsObserver {
 public:
  template <typename... Args>
  TestFlowControllerMixin(
      ProfileManagementFlowController::Step step,
      ProfileManagementStepTestView::StepControllerFactory factory,
      base::OnceClosure initial_step_load_finished_closure,
      Args&&... args)
      : BaseFlowController(std::forward<Args>(args)...),
        step_(step),
        step_controller_factory_(std::move(factory)),
        initial_step_load_finished_closure_(
            std::move(initial_step_load_finished_closure)) {}

  void Init() override {
    BaseFlowController::RegisterStep(
        step_, step_controller_factory_.Run(BaseFlowController::host()));
    BaseFlowController::SwitchToStep(
        step_, /*reset_state=*/true,
        /*step_switch_finished_callback=*/
        StepSwitchFinishedCallback(base::BindOnce(
            &TestFlowControllerMixin::OnInitialStepSwitchFinished,
            weak_ptr_factory_.GetWeakPtr())));
  }

  void OnInitialStepSwitchFinished(bool success) {
    if (BaseFlowController::host()->GetPickerContents()->IsLoading()) {
      Observe(BaseFlowController::host()->GetPickerContents());
    } else {
      DCHECK(initial_step_load_finished_closure_);
      std::move(initial_step_load_finished_closure_).Run();
    }
  }

  void DidStopLoading() override {
    Observe(nullptr);
    DCHECK(initial_step_load_finished_closure_);
    std::move(initial_step_load_finished_closure_).Run();
  }

  void CancelSigninFlow() override { NOTREACHED(); }

  void PickProfile(
      const base::FilePath& profile_path,
      ProfilePicker::ProfilePickingArgs args,
      base::OnceCallback<void(bool)> pick_profile_complete_callback) override {
    NOTREACHED();
  }

  ProfileManagementFlowController::Step step_;
  ProfileManagementStepTestView::StepControllerFactory step_controller_factory_;
  base::OnceClosure initial_step_load_finished_closure_;
  base::WeakPtrFactory<TestFlowControllerMixin> weak_ptr_factory_{this};
};

using TestProfileManagementFlowController =
    TestFlowControllerMixin<ProfileManagementFlowController>;

using TestFirstRunFlowController =
    TestFlowControllerMixin<FirstRunFlowController>;

}  // namespace

// -- ViewAddedWaiter ----------------------------------------------------------

ViewAddedWaiter::ViewAddedWaiter(views::View* view) : view_(view) {}
ViewAddedWaiter::~ViewAddedWaiter() = default;

void ViewAddedWaiter::Wait() {
  if (view_->GetWidget()) {
    return;
  }
  observation_.Observe(view_.get());
  run_loop_.Run();
}

void ViewAddedWaiter::OnViewAddedToWidget(views::View* observed_view) {
  if (observed_view == view_) {
    run_loop_.Quit();
  }
}

// -- ViewDeletedWaiter --------------------------------------------------------

ViewDeletedWaiter::ViewDeletedWaiter(views::View* view) {
  DCHECK(view);
  observation_.Observe(view);
}

ViewDeletedWaiter::~ViewDeletedWaiter() = default;

void ViewDeletedWaiter::Wait() {
  run_loop_.Run();
}

void ViewDeletedWaiter::OnViewIsDeleting(views::View* observed_view) {
  // Reset the observation before the view is actually deleted.
  observation_.Reset();
  run_loop_.Quit();
}

// -- PickerLoadStopWaiter -----------------------------------------------------

PickerLoadStopWaiter::PickerLoadStopWaiter(views::WebView* web_view,
                                           const GURL& expected_url,
                                           Mode wait_mode)
    : web_view_(*web_view), expected_url_(expected_url), wait_mode_(wait_mode) {
  CHECK(!expected_url.is_empty() || wait_mode_ == Mode::kCheckUrlAtNextLoad);

  // Observe attached WebContents changes. This can happen when navigating
  // between a page rendered in the system profile and one rendered in a user's
  // regular profile. Like the "profile type choice" -> "sign-in page"
  // transition for example.
  web_contents_attached_subscription_ =
      web_view->AddWebContentsAttachedCallback(
          base::BindRepeating(&PickerLoadStopWaiter::OnWebContentsAttached,
                              base::Unretained(this)));
}

void PickerLoadStopWaiter::DidStopLoading() {
  if (ShouldKeepWaiting()) {
    DVLOG(1) << "Load completed but stop condition not met, ignoring event.";
    return;
  }

  // Quitting the loop via a posted task to make sure that any prod work that
  // also is triggered via this same WebContents event can complete before the
  // test code resumes.
  base::SingleThreadTaskRunner::GetCurrentDefault()->PostTask(
      FROM_HERE, run_loop_.QuitClosure());
}

void PickerLoadStopWaiter::Wait() {
  if (!ShouldKeepWaiting()) {
    DVLOG(1) << "Stop condition already met, wait not needed.";
    return;
  }

  DVLOG(1) << "Starting wait for the completed navigation to " << expected_url_;
  Observe(web_view_->web_contents());
  run_loop_.Run();
}

void PickerLoadStopWaiter::OnWebContentsAttached(views::WebView* web_view) {
  DVLOG(1) << "New WebContents attached. Updating the observation target.";

  auto* web_contents = web_view_->web_contents();
  Observe(web_contents);

  // Attempt to process a load that might have happened before the `WebContents`
  // is swapped in. `ProfilePickerView` cross-profile page loads typically
  // happen that way.
  if (web_contents && !web_contents->IsLoading()) {
    DidStopLoading();
  }
}

bool PickerLoadStopWaiter::ShouldKeepWaiting() const {
  auto* web_contents = web_view_->web_contents();
  if (!web_contents || web_contents->IsLoading()) {
    return true;
  }

  auto& current_url = web_contents->GetLastCommittedURL();
  switch (wait_mode_) {
    case Mode::kWaitUntilUrlLoaded:
      if (current_url != expected_url_) {
        DVLOG(1) << "WebContents stopped loading on URL that doesn't match the "
                    "expected one. Actual URL: "
                 << current_url;
        return true;
      }
      return false;
    case Mode::kCheckUrlAtNextLoad:
      if (!expected_url_.is_empty()) {
        EXPECT_EQ(current_url, expected_url_);
      }
      return false;
  }
}

// -- ProfileManagementStepTestView --------------------------------------------

ProfileManagementStepTestView::ProfileManagementStepTestView(
    ProfilePicker::Params&& params,
    ProfileManagementFlowController::Step step,
    StepControllerFactory step_controller_factory)
    : ProfilePickerView(std::move(params)),
      step_(step),
      step_controller_factory_(std::move(step_controller_factory)) {}

ProfileManagementStepTestView::~ProfileManagementStepTestView() = default;

void ProfileManagementStepTestView::ShowAndWait(
    std::optional<gfx::Size> view_size) {
  Display();

  // waits for the view to be shown to return. If we don't wait enough
  // and the test is flaky, try to poll the page to check the presence of some
  // UI elements to know when to stop waiting.
  run_loop_.Run();

  if (view_size.has_value()) {
    GetWidget()->SetSize(view_size.value());
  }
}

std::unique_ptr<ProfileManagementFlowController>
ProfileManagementStepTestView::CreateFlowController(
    Profile* picker_profile,
    ClearHostClosure clear_host_callback) {
  if (params().entry_point() == ProfilePicker::EntryPoint::kFirstRun) {
    return std::make_unique<TestFirstRunFlowController>(
        step_, step_controller_factory_, run_loop_.QuitClosure(), this,
        std::move(clear_host_callback), picker_profile,
        /*first_run_exited_callback=*/base::DoNothing());
  }

  return std::make_unique<TestProfileManagementFlowController>(
      step_, step_controller_factory_, run_loop_.QuitClosure(), this,
      std::move(clear_host_callback), /*flow_type_string=*/"TestFlow");
}

MockProfilePickerWebContentsHost::MockProfilePickerWebContentsHost() = default;
MockProfilePickerWebContentsHost::~MockProfilePickerWebContentsHost() = default;

// -- Other utils --------------------------------------------------------------
namespace profiles::testing {

void WaitForPickerWidgetCreated() {
  if (!ProfilePicker::IsOpen()) {
    base::RunLoop run_loop;
    ProfilePicker::AddOnProfilePickerOpenedCallbackForTesting(
        run_loop.QuitClosure());
    run_loop.Run();
  }
  ViewAddedWaiter(ProfilePicker::GetViewForTesting()).Wait();
}

void WaitForPickerLoadStop(const GURL& expected_url) {
  if (!ProfilePicker::IsOpen()) {
    base::RunLoop run_loop;
    ProfilePicker::AddOnProfilePickerOpenedCallbackForTesting(
        run_loop.QuitClosure());
    run_loop.Run();
  }

  auto* web_view = ProfilePicker::GetWebViewForTesting();
  ASSERT_NE(web_view, nullptr);

  PickerLoadStopWaiter(web_view, expected_url,
                       PickerLoadStopWaiter::Mode::kCheckUrlAtNextLoad)
      .Wait();
}

void WaitForPickerUrl(const GURL& url) {
  if (!ProfilePicker::IsOpen()) {
    base::RunLoop run_loop;
    ProfilePicker::AddOnProfilePickerOpenedCallbackForTesting(
        run_loop.QuitClosure());
    run_loop.Run();
  }

  auto* web_view = ProfilePicker::GetWebViewForTesting();
  ASSERT_NE(web_view, nullptr);

  PickerLoadStopWaiter(web_view, url,
                       PickerLoadStopWaiter::Mode::kWaitUntilUrlLoaded)
      .Wait();
}

void WaitForPickerClosed() {
  if (auto* view = ProfilePicker::GetViewForTesting()) {
    ViewDeletedWaiter(view).Wait();

    // The profile picker might still be open if for example it was scheduled to
    // reopen on closure. But the view should not be the same anyway (it would
    // just be null in most cases).
    ASSERT_NE(view, ProfilePicker::GetViewForTesting());
  } else {
    ASSERT_FALSE(ProfilePicker::IsOpen());
  }
}

ManagedUserProfileNoticeHandler* ExpectPickerNoticeScreenType(
    ManagedUserProfileNoticeUI::ScreenType expected_type) {
  content::WebContents* web_contents = GetPickerWebContents();
  EXPECT_TRUE(web_contents);
  ManagedUserProfileNoticeHandler* handler =
      web_contents->GetWebUI()
          ->GetController()
          ->GetAs<ManagedUserProfileNoticeUI>()
          ->GetHandlerForTesting();
  EXPECT_TRUE(handler);
  EXPECT_EQ(handler->GetTypeForTesting(), expected_type);
  return handler;
}

void ExpectPickerManagedUserNoticeScreenTypeAndProceed(
    ManagedUserProfileNoticeUI::ScreenType expected_type,
    signin::SigninChoice choice) {
  ManagedUserProfileNoticeHandler* handler =
      ExpectPickerNoticeScreenType(expected_type);

  // Simulate clicking on the next button.
  handler->CallProceedCallbackForTesting(choice);
}

namespace {

std::string GetClickButtonWithinAppScript(std::string_view app_id,
                                          std::string_view button_id) {
  return base::StringPrintf(R"(
      (() => {
        const app = document.querySelector('%s');
        const button = app.shadowRoot.querySelector('%s');
        button.click();
        return true;
      })();
    )",
                            app_id, button_id);
}

std::string_view GetHistorySyncOptinAppId() {
  return base::FeatureList::IsEnabled(switches::kFirstRunDesktopRefresh)
             ? "history-sync-optin-app-refresh"
             : "history-sync-optin-app";
}

}  // namespace

std::string GetRejectHistoryOptinScript() {
  return GetClickButtonWithinAppScript(GetHistorySyncOptinAppId(),
                                       "#rejectButton");
}

std::string GetAcceptHistoryOptinScript() {
  return GetClickButtonWithinAppScript(GetHistorySyncOptinAppId(),
                                       "#acceptButton");
}

}  // namespace profiles::testing
