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

#include <memory>
#include <optional>
#include <string>
#include <utility>

#include "ash/constants/ash_features.h"
#include "ash/public/cpp/login_screen_test_api.h"
#include "ash/shell.h"
#include "base/auto_reset.h"
#include "base/test/scoped_feature_list.h"
#include "chrome/browser/ash/login/login_manager_test.h"
#include "chrome/browser/ash/login/test/auth_ui_utils.h"
#include "chrome/browser/ash/login/test/cryptohome_mixin.h"
#include "chrome/browser/ash/login/test/fake_recovery_service_mixin.h"
#include "chrome/browser/ash/login/test/login_manager_mixin.h"
#include "chrome/browser/ash/login/test/oobe_window_visibility_waiter.h"
#include "chrome/browser/ash/login/test/user_auth_config.h"
#include "chrome/browser/ash/login/wizard_context.h"
#include "chrome/browser/browser_process.h"
#include "chrome/test/base/fake_gaia_mixin.h"
#include "chromeos/ash/components/osauth/public/common_types.h"
#include "components/user_manager/known_user.h"
#include "content/public/test/browser_test.h"
#include "testing/gtest/include/gtest/gtest.h"

namespace ash {

using test::UserAuthConfig;

// Base class for UI testing of various authentication that can
// take place at the login screen.
class AuthFlowsLoginTestBase : public LoginManagerTest {
 public:
  explicit AuthFlowsLoginTestBase(bool require_reauth)
      : with_gaia_pw_{LoginManagerMixin::CreateConsumerAccountId(1),
                      UserAuthConfig::Create({AshAuthFactor::kGaiaPassword})
                          .RequireReauth(require_reauth)},
        with_gaia_pw_recovery_{
            LoginManagerMixin::CreateConsumerAccountId(2),
            UserAuthConfig::Create(
                {AshAuthFactor::kGaiaPassword, AshAuthFactor::kRecovery})
                .RequireReauth(require_reauth)},
        with_local_pw_{LoginManagerMixin::CreateConsumerAccountId(3),
                       UserAuthConfig::Create({AshAuthFactor::kLocalPassword})
                           .RequireReauth(require_reauth)},
        with_local_pw_recovery_{
            LoginManagerMixin::CreateConsumerAccountId(4),
            UserAuthConfig::Create(
                {AshAuthFactor::kLocalPassword, AshAuthFactor::kRecovery})
                .RequireReauth(require_reauth)},
        with_local_pw_pin_{
            LoginManagerMixin::CreateConsumerAccountId(5),
            UserAuthConfig::Create(
                {AshAuthFactor::kLocalPassword, AshAuthFactor::kCryptohomePin})
                .RequireReauth(require_reauth)},
        with_local_pw_pin_recovery_{
            LoginManagerMixin::CreateConsumerAccountId(6),
            UserAuthConfig::Create({AshAuthFactor::kLocalPassword,
                                    AshAuthFactor::kCryptohomePin,
                                    AshAuthFactor::kRecovery})
                .RequireReauth(require_reauth)},
        with_pin_recovery_{
            LoginManagerMixin::CreateConsumerAccountId(7),
            UserAuthConfig::Create(
                {AshAuthFactor::kCryptohomePin, AshAuthFactor::kRecovery})
                .RequireReauth(require_reauth)},
        with_pin_{LoginManagerMixin::CreateConsumerAccountId(8),
                  UserAuthConfig::Create({AshAuthFactor::kCryptohomePin})
                      .RequireReauth(require_reauth)},

        login_mixin_{
            &mixin_host_,
            {with_gaia_pw_, with_gaia_pw_recovery_, with_local_pw_,
             with_local_pw_recovery_, with_local_pw_pin_,
             with_local_pw_pin_recovery_, with_pin_recovery_, with_pin_},
            &fake_gaia_,
            &cryptohome_}

  {
    SetHardwareSupportForPinLogin(true);
  }

  ~AuthFlowsLoginTestBase() override = default;

  void SetUpOnMainThread() override {
    // Make `FakeUserDataAuthClient` perform actual password checks when
    // handling authentication requests.
    FakeUserDataAuthClient::TestApi::Get()->set_enable_auth_check(true);
    LoginManagerTest::SetUpOnMainThread();
  }

  void ConfigureFakeGaiaFor(const LoginManagerMixin::TestUserInfo& user) {
    fake_gaia_.SetupFakeGaiaForLogin(user.account_id.GetUserEmail(),
                                     user.account_id.GetGaiaId(),
                                     FakeGaiaMixin::kFakeRefreshToken);
  }

  // This must be called very early (e.g. in the constructor) so that the
  // hardware support flag before `PinSetupScreen` reads it.
  static void SetHardwareSupportForPinLogin(bool is_supported) {
    FakeUserDataAuthClient::TestApi::Get()
        ->set_supports_low_entropy_credentials(is_supported);
  }

 protected:
  const LoginManagerMixin::TestUserInfo with_gaia_pw_;
  const LoginManagerMixin::TestUserInfo with_gaia_pw_recovery_;
  const LoginManagerMixin::TestUserInfo with_local_pw_;
  const LoginManagerMixin::TestUserInfo with_local_pw_recovery_;
  const LoginManagerMixin::TestUserInfo with_local_pw_pin_;
  const LoginManagerMixin::TestUserInfo with_local_pw_pin_recovery_;
  const LoginManagerMixin::TestUserInfo with_pin_recovery_;
  const LoginManagerMixin::TestUserInfo with_pin_;

  CryptohomeMixin cryptohome_{&mixin_host_};
  FakeGaiaMixin fake_gaia_{&mixin_host_};
  base::AutoReset<bool> branded_build{&WizardContext::g_is_branded_build, true};
  LoginManagerMixin login_mixin_;
  FakeRecoveryServiceMixin fake_recovery_service_{&mixin_host_,
                                                  embedded_test_server()};
};

// ----------------------------------------------------------

class AuthFlowsLoginReauthTest : public AuthFlowsLoginTestBase {
 public:
  AuthFlowsLoginReauthTest()
      : AuthFlowsLoginTestBase(/* require_reauth */ true) {}
  ~AuthFlowsLoginReauthTest() override = default;

  void TriggerUserOnlineAuth(const LoginManagerMixin::TestUserInfo user,
                             const std::string& password) {
    ConfigureFakeGaiaFor(user);

    test::OnLoginScreen()->SelectUserPod(user.account_id);
    auto gaia = test::AwaitGaiaSigninUI();

    gaia->ReauthConfirmEmail(user.account_id);
    gaia->TypePassword(password);
    gaia->ContinueLogin();
  }
};

// ----------------------------------------------------------

class AuthFlowsLoginReauthWithPinTest : public AuthFlowsLoginReauthTest {
 public:
  AuthFlowsLoginReauthWithPinTest() : AuthFlowsLoginReauthTest() {
    scoped_features_.InitAndEnableFeature(
        features::kLocalAuthenticationWithPin);
  }
  ~AuthFlowsLoginReauthWithPinTest() override = default;

  base::test::ScopedFeatureList scoped_features_;
};

// ----------------------------------------------------------
class AuthFlowsLoginRecoverUserTest : public AuthFlowsLoginTestBase,
                                      public testing::WithParamInterface<bool> {
 public:
  AuthFlowsLoginRecoverUserTest()
      : AuthFlowsLoginTestBase(/* require_reauth */ false) {
    std::vector<base::test::FeatureRef> enabled;
    if (GetParam()) {
      enabled.push_back(ash::features::kManagedLocalPinAndPassword);
      enabled.push_back(ash::features::kRecoveryFlowReorder);
    }
    scoped_features_.InitWithFeatures(enabled, {});
  }

  ~AuthFlowsLoginRecoverUserTest() override = default;

  void TriggerRecoveryOnAuthErrorBubble(
      const LoginManagerMixin::TestUserInfo user) {
    ConfigureFakeGaiaFor(user);

    test::OnLoginScreen()->SelectUserPod(user.account_id);

    test::OnLoginScreen()->SubmitPassword(user.account_id,
                                          test::kWrongPassword);

    auto auth_error_bubble = test::OnLoginScreen()->WaitForAuthErrorBubble();

    auth_error_bubble->PressRecoveryButton();
    test::AuthErrorBubbleDismissWaiter()->Wait();
  }

  void TriggerUserOnlineAuth(const LoginManagerMixin::TestUserInfo user,
                             const std::string& password) {
    TriggerRecoveryOnAuthErrorBubble(user);

    // Use gaia to authenticate for recovery.
    auto gaia = test::AwaitGaiaSigninUI();

    gaia->ReauthConfirmEmail(user.account_id);
    gaia->TypePassword(password);
    gaia->ContinueLogin();
  }

 private:
  base::test::ScopedFeatureList scoped_features_;
};

// ----------------------------------------------------------

IN_PROC_BROWSER_TEST_F(AuthFlowsLoginReauthTest, GaiaPasswordNotChanged) {
  const auto& user = with_gaia_pw_;

  TriggerUserOnlineAuth(user, test::kGaiaPassword);

  login_mixin_.WaitForActiveSession();
}

IN_PROC_BROWSER_TEST_F(AuthFlowsLoginReauthTest,
                       GaiaPasswordRecoveryNotChanged) {
  const auto& user = with_gaia_pw_recovery_;

  TriggerUserOnlineAuth(user, test::kGaiaPassword);

  login_mixin_.WaitForActiveSession();
}

IN_PROC_BROWSER_TEST_F(AuthFlowsLoginReauthTest, GaiaPasswordChangedRecovery) {
  const auto& user = with_gaia_pw_recovery_;

  TriggerUserOnlineAuth(user, test::kNewPassword);

  auto pw_updated = test::AwaitPasswordUpdatedUI();
  pw_updated->ExpectPasswordUpdateState();
  pw_updated->ConfirmPasswordUpdate();

  login_mixin_.WaitForActiveSession();
}

IN_PROC_BROWSER_TEST_F(AuthFlowsLoginReauthTest, GaiaPasswordChangedManual) {
  const auto& user = with_gaia_pw_;

  TriggerUserOnlineAuth(user, test::kNewPassword);

  auto pw_changed = test::AwaitPasswordChangedUI();
  pw_changed->TypePreviousPassword(test::kGaiaPassword);
  pw_changed->SubmitPreviousPassword();

  auto pw_updated = test::AwaitPasswordUpdatedUI();
  pw_updated->ExpectPasswordUpdateState();
  pw_updated->ConfirmPasswordUpdate();

  login_mixin_.WaitForActiveSession();
}

// ----------------------------------------------------------

class AuthFlowsLoginAddExistingUserTest : public AuthFlowsLoginTestBase {
 public:
  AuthFlowsLoginAddExistingUserTest()
      : AuthFlowsLoginTestBase(/* require_reauth */ false) {}
  ~AuthFlowsLoginAddExistingUserTest() override = default;

  void TriggerUserOnlineAuth(const LoginManagerMixin::TestUserInfo user,
                             const std::string& password) {
    ConfigureFakeGaiaFor(user);

    test::OnLoginScreen()->AddNewUser();
    auto new_user = test::AwaitNewUserSelectionUI();

    new_user->ChooseConsumerUser();
    new_user->AwaitNextButton();
    new_user->Next();

    auto gaia = test::AwaitGaiaSigninUI();

    gaia->SubmitFullAuthEmail(user.account_id);
    gaia->TypePassword(password);
    gaia->ContinueLogin();
  }

  void ExpectReauthForRapt(const LoginManagerMixin::TestUserInfo user,
                           const std::string& password) {
    auto reauth = test::AwaitRecoveryReauthUI();
    reauth->ConfirmReauth();

    auto gaia = test::AwaitGaiaSigninUI();

    gaia->ReauthConfirmEmail(user.account_id);
    gaia->TypePassword(password);
    gaia->ContinueLogin();
  }
};

IN_PROC_BROWSER_TEST_F(AuthFlowsLoginAddExistingUserTest,
                       GaiaPasswordNotChanged) {
  const auto& user = with_gaia_pw_;

  TriggerUserOnlineAuth(user, test::kGaiaPassword);

  login_mixin_.WaitForActiveSession();
}

IN_PROC_BROWSER_TEST_F(AuthFlowsLoginAddExistingUserTest,
                       GaiaPassworRecoverydNotChanged) {
  const auto& user = with_gaia_pw_recovery_;

  TriggerUserOnlineAuth(user, test::kGaiaPassword);

  login_mixin_.WaitForActiveSession();
}

IN_PROC_BROWSER_TEST_F(AuthFlowsLoginAddExistingUserTest,
                       GaiaPasswordChangedRecovery) {
  const auto& user = with_gaia_pw_recovery_;

  TriggerUserOnlineAuth(user, test::kNewPassword);

  // Add new user flow does not provide RAPT token required for recovery.
  // Expect a request to reauthenticate and proceed with recovery.
  ExpectReauthForRapt(user, test::kNewPassword);

  auto pw_updated = test::AwaitPasswordUpdatedUI();
  pw_updated->ExpectPasswordUpdateState();
  pw_updated->ConfirmPasswordUpdate();

  login_mixin_.WaitForActiveSession();
}

IN_PROC_BROWSER_TEST_F(AuthFlowsLoginAddExistingUserTest,
                       GaiaPasswordChangedManual) {
  const auto& user = with_gaia_pw_;

  TriggerUserOnlineAuth(user, test::kNewPassword);

  auto pw_changed = test::AwaitPasswordChangedUI();
  pw_changed->TypePreviousPassword(test::kGaiaPassword);
  pw_changed->SubmitPreviousPassword();

  auto pw_updated = test::AwaitPasswordUpdatedUI();
  pw_updated->ExpectPasswordUpdateState();
  pw_updated->ConfirmPasswordUpdate();

  login_mixin_.WaitForActiveSession();
}

// ----------------------------------------------------------

IN_PROC_BROWSER_TEST_F(AuthFlowsLoginReauthWithPinTest,
                       GaiaPasswordChangedWithRecoveryPin) {
  const auto& user = with_local_pw_pin_recovery_;

  test::OnLoginScreen()->SelectUserPod(user.account_id);
  auto gaia = test::AwaitGaiaSigninUI();

  gaia->ReauthConfirmEmail(user.account_id);
  gaia->TypePassword(test::kNewPassword);
  gaia->ContinueLogin();

  auto local_authentication =
      test::OnLoginScreen()->WaitForLocalAuthenticationDialog();

  local_authentication->SubmitPin(test::kAuthPin);
  login_mixin_.WaitForActiveSession();
}

IN_PROC_BROWSER_TEST_F(AuthFlowsLoginReauthWithPinTest, PinCorrectPassword) {
  const auto& user = with_local_pw_pin_;

  test::OnLoginScreen()->SelectUserPod(user.account_id);
  auto gaia = test::AwaitGaiaSigninUI();

  gaia->ReauthConfirmEmail(user.account_id);
  gaia->TypePassword(test::kNewPassword);
  gaia->ContinueLogin();

  auto local_authentication =
      test::OnLoginScreen()->WaitForLocalAuthenticationDialog();

  local_authentication->SubmitPin(test::kAuthPin);
  login_mixin_.WaitForActiveSession();
}

IN_PROC_BROWSER_TEST_F(AuthFlowsLoginReauthWithPinTest,
                       CancelLocalAuthenticationDialog) {
  const auto& user = with_local_pw_pin_;

  TriggerUserOnlineAuth(user, test::kGaiaPassword);

  auto local_authentication =
      test::OnLoginScreen()->WaitForLocalAuthenticationDialog();

  local_authentication->CancelDialog();

  local_authentication->WaitUntilDismissed();

  // After dismissing the local authentication dialog,
  // the flow should return to and display the login screen.
  test::OnLoginScreen()->SelectUserPod(user.account_id);
}

// ----------------------------------------------------------

IN_PROC_BROWSER_TEST_F(AuthFlowsLoginReauthTest,
                       GaiaPasswordChangedWithRecoveryLocalPassword) {
  const auto& user = with_local_pw_recovery_;

  test::OnLoginScreen()->SelectUserPod(user.account_id);
  auto gaia = test::AwaitGaiaSigninUI();

  gaia->ReauthConfirmEmail(user.account_id);
  gaia->TypePassword(test::kNewPassword);
  gaia->ContinueLogin();

  auto local_authentication =
      test::OnLoginScreen()->WaitForLocalAuthenticationDialog();

  local_authentication->SubmitPassword(test::kLocalPassword);
  login_mixin_.WaitForActiveSession();
}

IN_PROC_BROWSER_TEST_F(AuthFlowsLoginReauthTest, LocalPasswordCorrectPassword) {
  const auto& user = with_local_pw_;

  test::OnLoginScreen()->SelectUserPod(user.account_id);
  auto gaia = test::AwaitGaiaSigninUI();

  gaia->ReauthConfirmEmail(user.account_id);
  gaia->TypePassword(test::kNewPassword);
  gaia->ContinueLogin();

  auto local_authentication =
      test::OnLoginScreen()->WaitForLocalAuthenticationDialog();

  local_authentication->SubmitPassword(test::kLocalPassword);
  login_mixin_.WaitForActiveSession();
}

IN_PROC_BROWSER_TEST_F(AuthFlowsLoginReauthTest,
                       CancelLocalAuthenticationDialog) {
  const auto& user = with_local_pw_;

  TriggerUserOnlineAuth(user, test::kGaiaPassword);

  auto local_authentication =
      test::OnLoginScreen()->WaitForLocalAuthenticationDialog();

  local_authentication->CancelDialog();

  local_authentication->WaitUntilDismissed();

  // After dismissing the local authentication dialog,
  // the flow should return to and display the login screen.
  test::OnLoginScreen()->SelectUserPod(user.account_id);
}

IN_PROC_BROWSER_TEST_F(AuthFlowsLoginReauthTest, AuthenticateWithRecovery) {
  const auto& user = with_local_pw_recovery_;

  TriggerUserOnlineAuth(user, test::kGaiaPassword);

  // After successful GAIA authentication in recovery session,
  // the session should start automatically.
  login_mixin_.WaitForActiveSession();
}

// ----------------------------------------------------------

IN_PROC_BROWSER_TEST_P(AuthFlowsLoginRecoverUserTest,
                       LocalPasswordWithRecovery) {
  const auto& user = with_local_pw_recovery_;

  TriggerUserOnlineAuth(user, FakeGaiaMixin::kFakeUserPassword);

  // Set up a new password.
  auto local_password_setup = test::AwaitLocalPasswordSetupUI();

  local_password_setup->TypeFirstPassword(test::kNewPassword);
  local_password_setup->TypeConfirmPassword(test::kNewPassword);
  local_password_setup->Submit();

  // Recovery password update confirmation.
  test::RecoveryPasswordUpdatedPageWaiter()->Wait();
  test::RecoveryPasswordUpdatedProceedAction();
  // After successful password update, the session should start automatically.
  login_mixin_.WaitForActiveSession();
}

IN_PROC_BROWSER_TEST_P(AuthFlowsLoginRecoverUserTest,
                       LocalPasswordWithoutRecoveryCancelLAD) {
  const auto& user = with_local_pw_;
  // Start recovery flow without recovery auth factor.
  TriggerUserOnlineAuth(user, FakeGaiaMixin::kFakeUserPassword);

  // Wait for local data loss warning.
  test::LocalDataLossWarningPageWaiter()->Wait();
}

IN_PROC_BROWSER_TEST_P(AuthFlowsLoginRecoverUserTest,
                       GaiaPasswordWithRecovery) {
  const auto& user = with_gaia_pw_recovery_;

  // Start recovery flow with recovery auth factor.
  TriggerUserOnlineAuth(user, test::kNewPassword);

  // Wait for password update dialog.
  auto pw_updated = test::AwaitPasswordUpdatedUI();
  pw_updated->ExpectPasswordUpdateState();
  pw_updated->ConfirmPasswordUpdate();

  login_mixin_.WaitForActiveSession();
}

IN_PROC_BROWSER_TEST_P(AuthFlowsLoginRecoverUserTest,
                       GaiaPasswordWithoutRecovery) {
  const auto& user = with_gaia_pw_;

  // Start recovery flow without recovery auth factor.
  TriggerUserOnlineAuth(user, test::kWrongPassword);

  auto pw_changed = test::AwaitPasswordChangedUI();
  pw_changed->TypePreviousPassword(test::kGaiaPassword);
  pw_changed->SubmitPreviousPassword();

  // Wait for password update dialog.
  auto pw_updated = test::AwaitPasswordUpdatedUI();
  pw_updated->ExpectPasswordUpdateState();
  pw_updated->ConfirmPasswordUpdate();

  login_mixin_.WaitForActiveSession();
}

IN_PROC_BROWSER_TEST_P(AuthFlowsLoginRecoverUserTest,
                       GaiaPasswordWithoutRecoveryInvalidPassword) {
  const auto& user = with_gaia_pw_;

  // Start recovery flow without recovery auth factor.
  TriggerUserOnlineAuth(user, test::kWrongPassword);

  auto pw_changed = test::AwaitPasswordChangedUI();
  pw_changed->TypePreviousPassword(test::kWrongPassword);
  pw_changed->SubmitPreviousPassword();

  // Keep user on passwordChanged screen after incorrect password, display error
  // message.
  pw_changed->InvalidPasswordFeedback()->Wait();
}

IN_PROC_BROWSER_TEST_P(AuthFlowsLoginRecoverUserTest,
                       GaiaPasswordWithoutRecoveryForgotPasswordClick) {
  const auto& user = with_gaia_pw_;

  // Start recovery flow without recovery auth factor.
  TriggerUserOnlineAuth(user, test::kWrongPassword);

  auto pw_changed = test::AwaitPasswordChangedUI();
  // Clicks on forgot password button.
  pw_changed->ForgotPreviousPassword();

  // Wait for local data loss warning.
  test::LocalDataLossWarningPageWaiter()->Wait();
}

// Parameterized on a boolean that represents whether the recovery flow password
// reset order changes (kRecoveryFlowReorder) are enabled or not.
class AuthFlowsLoginRecoverUserTestPasswordlessRecovery
    : public AuthFlowsLoginRecoverUserTest {
 public:
  AuthFlowsLoginRecoverUserTestPasswordlessRecovery() = default;
  ~AuthFlowsLoginRecoverUserTestPasswordlessRecovery() override = default;
};

// Ensures that a user with PIN-only (without recovery) is shown the local data
// loss warning screen.
IN_PROC_BROWSER_TEST_P(AuthFlowsLoginRecoverUserTestPasswordlessRecovery,
                       PinOnlyWithoutRecovery) {
  const auto& user = with_pin_;
  // Start recovery flow without recovery auth factor.
  TriggerUserOnlineAuth(user, FakeGaiaMixin::kFakeUserPassword);

  // Wait for local data loss warning.
  test::LocalDataLossWarningPageWaiter()->Wait();
}

// Ensures that going through recovery for the PIN-only scenario works.
IN_PROC_BROWSER_TEST_P(AuthFlowsLoginRecoverUserTestPasswordlessRecovery,
                       RecoveryWithPinOnly) {
  const auto& user = with_pin_recovery_;
  // Start recovery flow with recovery auth factor.
  TriggerUserOnlineAuth(user, FakeGaiaMixin::kFakeUserPassword);

  auto pin_setup = test::AwaitPinSetupUI();

  pin_setup->InsertAndConfirmPin("123456");
  pin_setup->TapDone();

  login_mixin_.WaitForActiveSession();
}

// Checks that the PIN autosubmit length is properly updated.
IN_PROC_BROWSER_TEST_P(AuthFlowsLoginRecoverUserTestPasswordlessRecovery,
                       AutoSubmitLengthIsCorrectlyUpdated) {
  const auto& user = with_pin_recovery_;

  // Set initial fictional start value to be '6'.
  user_manager::KnownUser known_user(g_browser_process->local_state());
  known_user.SetUserPinLength(user.account_id, 6);

  // Use recovery to set a new PIN that is 8 digits long.
  TriggerUserOnlineAuth(user, FakeGaiaMixin::kFakeUserPassword);
  auto pin_setup = test::AwaitPinSetupUI();
  pin_setup->InsertAndConfirmPin("12345678");
  pin_setup->TapDone();

  login_mixin_.WaitForActiveSession();
  EXPECT_EQ(known_user.GetUserPinLength(user.account_id), 8);
}

INSTANTIATE_TEST_SUITE_P(AuthFlowsLoginRecoverUserTestPasswordlessRecoveryTests,
                         AuthFlowsLoginRecoverUserTestPasswordlessRecovery,
                         ::testing::ValuesIn({true, false}));

INSTANTIATE_TEST_SUITE_P(AuthFlowsLoginRecoverUserTestTests,
                         AuthFlowsLoginRecoverUserTest,
                         ::testing::Bool());

}  // namespace ash
