// Copyright 2024 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/ash/app_mode/load_profile.h"

#include <functional>
#include <memory>
#include <string>
#include <string_view>
#include <utility>

#include "base/functional/bind.h"
#include "base/location.h"
#include "base/run_loop.h"
#include "base/task/single_thread_task_runner.h"
#include "base/test/bind.h"
#include "base/test/task_environment.h"
#include "base/test/test_future.h"
#include "base/types/expected.h"
#include "chrome/browser/ash/app_mode/cancellable_job.h"
#include "chrome/browser/ash/app_mode/kiosk_app_launch_error.h"
#include "chrome/browser/ash/app_mode/kiosk_app_types.h"
#include "chrome/browser/profiles/profile.h"
#include "chrome/test/base/testing_browser_process.h"
#include "chrome/test/base/testing_profile.h"
#include "chromeos/ash/components/login/auth/public/auth_failure.h"
#include "chromeos/ash/components/login/auth/public/user_context.h"
#include "components/account_id/account_id.h"
#include "components/user_manager/user_type.h"
#include "content/public/test/browser_task_environment.h"
#include "google_apis/gaia/gaia_id.h"
#include "testing/gtest/include/gtest/gtest.h"

using ash::kiosk::CryptohomeMountState;
using ash::kiosk::CryptohomeMountStateCallback;
using ash::kiosk::LoadProfileResult;
using ash::kiosk::LoadProfileWithCallbacks;
using ash::kiosk::PerformSigninResultCallback;
using ash::kiosk::StartSessionResultCallback;
using base::test::TestFuture;

namespace ash {

namespace {

constexpr std::string_view kKioskEmail = "kiosk@example.com";

// App type used in tests. Could be any `KioskAppType`.
constexpr KioskAppType kTestAppType = KioskAppType::kWebApp;

// Helper to post a task with less boilerplate.
void PostTask(base::OnceClosure task) {
  base::SingleThreadTaskRunner::GetCurrentDefault()->PostTask(
      FROM_HERE, base::BindOnce(std::move(task)));
}

// Helper to return a handle to a `CancellableJob`.
std::unique_ptr<CancellableJob> NewCancellableJob() {
  return std::unique_ptr<CancellableJob>{};
}

// Account ID used in tests. Could be any `AccountId`.
AccountId TestAccountId() {
  return AccountId::FromUserEmailGaiaId(/*user_email=*/std::string(kKioskEmail),
                                        GaiaId("a fake gaia id"));
}

// User context used in tests. Could be any `UserContext` .
UserContext TestUserContext() {
  return UserContext(user_manager::UserType::kKioskWebApp, TestAccountId());
}

}  // namespace

class LoadProfileTest : public testing::Test {
 protected:
  // Set task environment with mock time to call `PostDelayedTask`.
  content::BrowserTaskEnvironment task_environment_{
      base::test::TaskEnvironment::TimeSource::MOCK_TIME};

  LoadProfileTest() = default;
};

TEST_F(LoadProfileTest, ReturnsProfileOnSuccess) {
  TestingProfile profile;
  profile.set_profile_name(std::string(kKioskEmail));

  auto fake_check_cryptohome = [](CryptohomeMountStateCallback cb) {
    PostTask(base::BindOnce(std::move(cb), CryptohomeMountState::kNotMounted));
    return NewCancellableJob();
  };

  auto fake_signin = [](PrefService* local_state, KioskAppType app_type,
                        AccountId id, PerformSigninResultCallback cb) {
    PostTask(base::BindOnce(std::move(cb), TestUserContext()));
    return NewCancellableJob();
  };

  auto fake_start_session = [&](const UserContext& user_context,
                                StartSessionResultCallback cb) {
    PostTask(base::BindOnce(std::move(cb), std::ref(profile)));
    return NewCancellableJob();
  };

  TestFuture<LoadProfileResult> future;
  auto handle = LoadProfileWithCallbacks(
      TestingBrowserProcess::GetGlobal()->local_state(), TestAccountId(),
      kTestAppType, base::BindOnce(fake_check_cryptohome),
      base::BindOnce(fake_signin),
      base::BindLambdaForTesting(fake_start_session), future.GetCallback());

  auto result = future.Take();
  ASSERT_TRUE(result.has_value());
  ASSERT_EQ(profile.GetProfileUserName(), result.value()->GetProfileUserName());
}

TEST_F(LoadProfileTest, ForwardsResultsBetweenStepsCorrectly) {
  TestingProfile profile;
  profile.set_profile_name(std::string(kKioskEmail));

  AccountId seen_account_id;
  KioskAppType seen_app_type;
  UserContext seen_user_context;

  auto fake_check_cryptohome = [](CryptohomeMountStateCallback cb) {
    PostTask(base::BindOnce(std::move(cb), CryptohomeMountState::kNotMounted));
    return NewCancellableJob();
  };
  auto fake_signin = [&](PrefService* local_state, KioskAppType app_type,
                         AccountId id, PerformSigninResultCallback cb) {
    seen_app_type = app_type;
    seen_account_id = TestAccountId();
    PostTask(base::BindOnce(std::move(cb), TestUserContext()));
    return NewCancellableJob();
  };
  auto fake_start_session = [&](const UserContext& user_context,
                                StartSessionResultCallback cb) {
    seen_user_context = user_context;
    PostTask(base::BindOnce(std::move(cb), std::ref(profile)));
    return NewCancellableJob();
  };

  TestFuture<LoadProfileResult> future;
  auto handle = LoadProfileWithCallbacks(
      TestingBrowserProcess::GetGlobal()->local_state(), TestAccountId(),
      kTestAppType, base::BindOnce(fake_check_cryptohome),
      base::BindLambdaForTesting(fake_signin),
      base::BindLambdaForTesting(fake_start_session), future.GetCallback());

  auto result = future.Take();
  ASSERT_TRUE(result.has_value());
  EXPECT_EQ(seen_account_id, TestAccountId());
  EXPECT_EQ(seen_app_type, kTestAppType);
  EXPECT_EQ(seen_user_context, TestUserContext());
  EXPECT_EQ(result.value()->GetProfileUserName(), profile.GetProfileUserName());
}

TEST_F(LoadProfileTest, ReturnsCryptohomeCheckError) {
  bool did_call_jobs_after_failure = false;

  auto fake_check_cryptohome = [](CryptohomeMountStateCallback cb) {
    PostTask(base::BindOnce(std::move(cb), CryptohomeMountState::kMounted));
    return NewCancellableJob();
  };
  auto fake_signin = [&](PrefService* local_state, KioskAppType app_type,
                         AccountId id, PerformSigninResultCallback cb) {
    did_call_jobs_after_failure = true;
    return NewCancellableJob();
  };
  auto fake_start_session = [&](const UserContext& user_context,
                                StartSessionResultCallback cb) {
    did_call_jobs_after_failure = true;
    return NewCancellableJob();
  };

  TestFuture<LoadProfileResult> future;
  auto handle = LoadProfileWithCallbacks(
      TestingBrowserProcess::GetGlobal()->local_state(), TestAccountId(),
      kTestAppType, base::BindOnce(fake_check_cryptohome),
      base::BindLambdaForTesting(fake_signin),
      base::BindLambdaForTesting(fake_start_session), future.GetCallback());

  auto result = future.Take();
  ASSERT_FALSE(result.has_value());
  EXPECT_EQ(result.error(), KioskAppLaunchError::Error::kAlreadyMounted);
  EXPECT_FALSE(did_call_jobs_after_failure);
}

TEST_F(LoadProfileTest, ReturnsSigninError) {
  bool did_call_jobs_after_failure = false;

  auto fake_check_cryptohome = [](CryptohomeMountStateCallback cb) {
    PostTask(base::BindOnce(std::move(cb), CryptohomeMountState::kNotMounted));
    return NewCancellableJob();
  };
  auto fake_signin = [](PrefService* local_state, KioskAppType app_type,
                        AccountId id, PerformSigninResultCallback cb) {
    PostTask(base::BindOnce(
        std::move(cb),
        base::unexpected(AuthFailure(AuthFailure::UNRECOVERABLE_CRYPTOHOME))));
    return NewCancellableJob();
  };
  auto fake_start_session = [&](const UserContext& user_context,
                                StartSessionResultCallback cb) {
    did_call_jobs_after_failure = true;
    return NewCancellableJob();
  };

  TestFuture<LoadProfileResult> future;
  auto handle = LoadProfileWithCallbacks(
      TestingBrowserProcess::GetGlobal()->local_state(), TestAccountId(),
      kTestAppType, base::BindOnce(fake_check_cryptohome),
      base::BindOnce(fake_signin),
      base::BindLambdaForTesting(fake_start_session), future.GetCallback());

  auto result = future.Take();
  ASSERT_FALSE(result.has_value());
  EXPECT_EQ(result.error(), KioskAppLaunchError::Error::kUnableToMount);
  EXPECT_FALSE(did_call_jobs_after_failure);
}

TEST_F(LoadProfileTest, StopsWhenHandleIsCancelled) {
  TestingProfile profile;
  profile.set_profile_name(std::string(kKioskEmail));

  TestFuture<void> signin_future;
  bool did_call_jobs_after_cancellation = false;

  auto fake_check_cryptohome = [](CryptohomeMountStateCallback cb) {
    PostTask(base::BindOnce(std::move(cb), CryptohomeMountState::kNotMounted));
    return NewCancellableJob();
  };

  auto fake_signin = [&](PrefService* local_state, KioskAppType app_type,
                         AccountId id, PerformSigninResultCallback cb) {
    signin_future.SetValue();
    PostTask(base::BindOnce(std::move(cb), TestUserContext()));
    return NewCancellableJob();
  };

  auto fake_start_session = [&](const UserContext& user_context,
                                StartSessionResultCallback cb) {
    did_call_jobs_after_cancellation = true;
    PostTask(base::BindOnce(std::move(cb), std::ref(profile)));
    return NewCancellableJob();
  };

  TestFuture<LoadProfileResult> future;
  auto handle = LoadProfileWithCallbacks(
      TestingBrowserProcess::GetGlobal()->local_state(), TestAccountId(),
      kTestAppType, base::BindOnce(fake_check_cryptohome),
      base::BindLambdaForTesting(fake_signin),
      base::BindLambdaForTesting(fake_start_session), future.GetCallback());

  // Cancel the job handle after signin.
  ASSERT_NE(handle, nullptr);
  ASSERT_TRUE(signin_future.Wait()) << "Did not signin";
  handle.reset();

  // The sign-in callback was already posted; this sentinel runs after it.
  base::RunLoop run_loop;
  PostTask(run_loop.QuitClosure());
  run_loop.Run();
  ASSERT_FALSE(did_call_jobs_after_cancellation);
  ASSERT_FALSE(future.IsReady());
}

}  // namespace ash
