// 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/ui/ash/graduation/graduation_manager_impl.h"

#include <algorithm>
#include <ranges>

#include "ash/constants/ash_features.h"
#include "ash/constants/ash_pref_names.h"
#include "ash/constants/web_app_id_constants.h"
#include "ash/public/cpp/shelf_model.h"
#include "ash/public/cpp/shelf_model_observer.h"
#include "base/memory/scoped_refptr.h"
#include "base/run_loop.h"
#include "base/test/scoped_feature_list.h"
#include "base/test/test_mock_time_task_runner.h"
#include "base/time/time.h"
#include "base/time/time_override.h"
#include "base/values.h"
#include "chrome/browser/apps/app_service/app_service_proxy.h"
#include "chrome/browser/apps/app_service/app_service_proxy_factory.h"
#include "chrome/browser/ash/base/locale_util.h"
#include "chrome/browser/ash/login/test/logged_in_user_mixin.h"
#include "chrome/browser/ash/system_web_apps/system_web_app_manager.h"
#include "chrome/browser/ash/system_web_apps/test_support/system_web_app_browsertest_base.h"
#include "chrome/browser/browser_process.h"
#include "chrome/browser/global_features.h"
#include "chrome/browser/profiles/profile_manager.h"
#include "chrome/browser/ui/browser.h"
#include "chrome/browser/web_applications/web_app_command_manager.h"
#include "chrome/browser/web_applications/web_app_provider.h"
#include "chrome/test/base/in_process_browser_test.h"
#include "chrome/test/base/mixin_based_in_process_browser_test.h"
#include "components/prefs/pref_service.h"
#include "content/public/test/browser_test.h"
#include "testing/gtest/include/gtest/gtest.h"

namespace ash {
namespace {
constexpr char kSessionStartTime[] = "30 Sep 2024 00:00:00 PST";
apps::AppServiceProxy* GetAppServiceProxy(Profile* profile) {
  return apps::AppServiceProxyFactory::GetForProfile(profile);
}

void WaitForAppRegistryCommands(Profile* profile) {
  auto* web_app_provider = web_app::WebAppProvider::GetForTest(profile);
  base::RunLoop run_loop;
  web_app_provider->on_registry_ready().Post(FROM_HERE, run_loop.QuitClosure());
  run_loop.Run();
  web_app_provider->command_manager().AwaitAllCommandsCompleteForTesting();
}

// Called on completion of locale_util::SwitchLanguage.
void OnLocaleSwitched(base::RunLoop* run_loop,
                      const locale_util::LanguageSwitchResult& result) {
  run_loop->Quit();
}
}  // namespace

class GraduationManagerTest : public SystemWebAppBrowserTestBase,
                              public ash::ShelfModelObserver {
 public:
  GraduationManagerTest() {
    scoped_feature_list_.InitAndEnableFeature(features::kGraduation);
  }
  ~GraduationManagerTest() override = default;
  GraduationManagerTest(const GraduationManagerTest&) = delete;
  GraduationManagerTest& operator=(const GraduationManagerTest&) = delete;

  void SetUpOnMainThread() override {
    SystemWebAppBrowserTestBase::SetUpOnMainThread();
    logged_in_user_mixin_.LogInUser();
    SetMockClocksAndTaskRunner();
    WaitForTestSystemAppInstall();
    WaitForAppRegistryCommands(browser()->GetProfile());
  }

  static void SetTimeNow(base::Time new_time_now) { time_now_ = new_time_now; }

  static base::Time GetTimeNow() { return time_now_; }

  void SetMockClocksAndTaskRunner() {
    // Set the system time in the task runner.
    base::Time start_time;
    EXPECT_TRUE(base::Time::FromUTCString(kSessionStartTime, &start_time));
    task_runner_ = base::MakeRefCounted<base::TestMockTimeTaskRunner>(
        start_time, base::TimeTicks());

    ash::graduation::GraduationManagerImpl::Get()->SetClocksForTesting(
        task_runner_->GetMockClock(), task_runner_->GetMockTickClock());

    // Override base::Time::Now() so the util functions work properly.
    SetTimeNow(start_time);

    time_override_ = std::make_unique<base::subtle::ScopedTimeClockOverrides>(
        /*time_override=*/&GraduationManagerTest::GetTimeNow,
        /*time_ticks_override=*/nullptr,
        /*thread_ticks_override=*/nullptr);
  }

  void AdvanceTimeBy(base::TimeDelta advance_length) {
    task_runner_->FastForwardBy(advance_length);
    SetTimeNow(base::Time::Now() + advance_length);
    ResumeTimer();
  }

  // task_runner_->FastForwardBy(TimeDelta) doesn't trigger the midnight timer
  // so it needs to be manually resumed.
  void ResumeTimer() {
    ash::graduation::GraduationManagerImpl::Get()->ResumeTimerForTesting();
  }

  bool IsItemPinned(const std::string& item_id) {
    const auto& shelf_items = ShelfModel::Get()->items();
    auto pinned_item =
        std::ranges::find_if(shelf_items, [&item_id](const auto& shelf_item) {
          return shelf_item.id.app_id == item_id;
        });
    return pinned_item != std::ranges::end(shelf_items);
  }

  apps::Readiness GetAppReadiness(const webapps::AppId& app_id) {
    apps::Readiness readiness;
    bool app_found =
        GetAppServiceProxy(browser()->GetProfile())
            ->AppRegistryCache()
            .ForOneApp(app_id, [&readiness](const apps::AppUpdate& update) {
              readiness = update.Readiness();
            });
    EXPECT_TRUE(app_found);
    return readiness;
  }

  void SetGraduationEnablement(bool is_enabled) {
    base::DictValue status;
    status.Set("is_enabled", is_enabled);
    browser()->GetProfile()->GetPrefs()->SetDict(
        prefs::kGraduationEnablementStatus, status.Clone());
  }

  void SetGraduationEnablementWithStartDate(bool is_enabled,
                                            int day,
                                            int month,
                                            int year) {
    base::DictValue status;
    status.Set("is_enabled", is_enabled);
    base::DictValue start_date;
    start_date.Set("day", day);
    start_date.Set("month", month);
    start_date.Set("year", year);
    status.Set("start_date", start_date.Clone());
    browser()->GetProfile()->GetPrefs()->SetDict(
        prefs::kGraduationEnablementStatus, status.Clone());
  }

  void SetGraduationEnablementWithEndDate(bool is_enabled,
                                          int day,
                                          int month,
                                          int year) {
    base::DictValue status;
    status.Set("is_enabled", is_enabled);
    base::DictValue end_date;
    end_date.Set("day", day);
    end_date.Set("month", month);
    end_date.Set("year", year);
    status.Set("end_date", end_date.Clone());
    browser()->GetProfile()->GetPrefs()->SetDict(
        prefs::kGraduationEnablementStatus, status.Clone());
  }

  std::string GetLanguageCode() {
    return ash::graduation::GraduationManagerImpl::Get()->GetLanguageCode();
  }

  void WaitForShelfItemAdd() {
    base::RunLoop run_loop;
    auto* shelf_model = ShelfModel::Get();
    shelf_model->AddObserver(this);
    item_added_callback_ = run_loop.QuitClosure();
    run_loop.Run();
    shelf_model->RemoveObserver(this);
  }

  void WaitForShefItemRemoved() {
    base::RunLoop run_loop;
    auto* shelf_model = ShelfModel::Get();
    shelf_model->AddObserver(this);
    item_removed_callback_ = run_loop.QuitClosure();
    run_loop.Run();
    shelf_model->RemoveObserver(this);
  }

  // ShelfModelObserver:
  void ShelfItemAdded(int index) override {
    if (item_added_callback_) {
      item_added_callback_.Run();
    }
  }

  void ShelfItemRemoved(int index, const ShelfItem& old_item) override {
    if (item_removed_callback_) {
      item_removed_callback_.Run();
    }
  }

 private:
  base::test::ScopedFeatureList scoped_feature_list_;

  base::RepeatingClosure item_added_callback_;
  base::RepeatingClosure item_removed_callback_;

  scoped_refptr<base::TestMockTimeTaskRunner> task_runner_;

  std::unique_ptr<base::subtle::ScopedTimeClockOverrides> time_override_;

  static base::Time time_now_;

  LoggedInUserMixin logged_in_user_mixin_{
      &mixin_host_, /*test_base=*/this, embedded_test_server(),
      LoggedInUserMixin::LogInType::kManaged};
};

// static
base::Time GraduationManagerTest::time_now_;

IN_PROC_BROWSER_TEST_F(GraduationManagerTest, PRE_AppPinnedWhenPolicyEnabled) {
  // Set pref value in PRE_ to ensure that the pref value is set at the time of
  // user session start in the test.
  SetGraduationEnablement(true);
}

IN_PROC_BROWSER_TEST_F(GraduationManagerTest, AppPinnedWhenPolicyEnabled) {
  EXPECT_EQ(apps::Readiness::kReady, GetAppReadiness(ash::kGraduationAppId));
  EXPECT_TRUE(IsItemPinned(ash::kGraduationAppId));

  SetGraduationEnablement(false);
  WaitForAppRegistryCommands(browser()->GetProfile());

  EXPECT_FALSE(IsItemPinned(ash::kGraduationAppId));
  EXPECT_EQ(apps::Readiness::kDisabledByPolicy,
            GetAppReadiness(ash::kGraduationAppId));
}

IN_PROC_BROWSER_TEST_F(GraduationManagerTest,
                       PRE_AppPinnedWhenStartDateIsReached) {
  // Set pref value in PRE_ to ensure that the pref value is set at the time of
  // user session start in the test.
  SetGraduationEnablementWithStartDate(true, 1, 10, 2024);
}

IN_PROC_BROWSER_TEST_F(GraduationManagerTest, AppPinnedWhenStartDateIsReached) {
  EXPECT_EQ(apps::Readiness::kDisabledByPolicy,
            GetAppReadiness(ash::kGraduationAppId));
  EXPECT_FALSE(IsItemPinned(ash::kGraduationAppId));

  // Fast forward to the policy enablement start date set in the pre-test.
  AdvanceTimeBy(base::Days(1));
  WaitForAppRegistryCommands(browser()->GetProfile());
  WaitForShelfItemAdd();

  EXPECT_TRUE(IsItemPinned(ash::kGraduationAppId));
  EXPECT_EQ(apps::Readiness::kReady, GetAppReadiness(ash::kGraduationAppId));
}

IN_PROC_BROWSER_TEST_F(GraduationManagerTest,
                       PRE_AppPinnedWhenStartDateIsReachedInMoreThanOneDay) {
  // Set pref value in PRE_ to ensure that the pref value is set at the time of
  // user session start in the test.
  SetGraduationEnablementWithStartDate(true, 2, 10, 2024);
}

IN_PROC_BROWSER_TEST_F(GraduationManagerTest,
                       AppPinnedWhenStartDateIsReachedInMoreThanOneDay) {
  EXPECT_EQ(apps::Readiness::kDisabledByPolicy,
            GetAppReadiness(ash::kGraduationAppId));
  EXPECT_FALSE(IsItemPinned(ash::kGraduationAppId));

  // Fast forward to the policy enablement start date set in the pre-test.
  AdvanceTimeBy(base::Days(2));
  WaitForAppRegistryCommands(browser()->GetProfile());
  // Wait for the new shelf iteme to finish.
  WaitForShelfItemAdd();

  EXPECT_TRUE(IsItemPinned(ash::kGraduationAppId));
  EXPECT_EQ(apps::Readiness::kReady, GetAppReadiness(ash::kGraduationAppId));
}

IN_PROC_BROWSER_TEST_F(GraduationManagerTest, PRE_AppPinnedOnEndDate) {
  // Set pref value in PRE_ to ensure that the pref value is set at the time of
  // user session start in the test.
  SetGraduationEnablementWithEndDate(true, 1, 10, 2024);
}

IN_PROC_BROWSER_TEST_F(GraduationManagerTest, AppPinnedOnEndDate) {
  EXPECT_TRUE(IsItemPinned(ash::kGraduationAppId));
  EXPECT_EQ(apps::Readiness::kReady, GetAppReadiness(ash::kGraduationAppId));

  // Fast forward to the policy enablement end date set in the pre-test.
  AdvanceTimeBy(base::Days(1));
  WaitForAppRegistryCommands(browser()->GetProfile());

  // Since this is the last day the app is available, the app should be pinned.
  EXPECT_TRUE(IsItemPinned(ash::kGraduationAppId));
  EXPECT_EQ(apps::Readiness::kReady, GetAppReadiness(ash::kGraduationAppId));
}

IN_PROC_BROWSER_TEST_F(GraduationManagerTest, AppUnpinnedWhenPolicyUnset) {
  EXPECT_FALSE(IsItemPinned(ash::kGraduationAppId));
  EXPECT_EQ(apps::Readiness::kDisabledByPolicy,
            GetAppReadiness(ash::kGraduationAppId));

  SetGraduationEnablement(true);
  WaitForAppRegistryCommands(browser()->GetProfile());

  EXPECT_EQ(apps::Readiness::kReady, GetAppReadiness(ash::kGraduationAppId));
  EXPECT_TRUE(IsItemPinned(ash::kGraduationAppId));
}

IN_PROC_BROWSER_TEST_F(GraduationManagerTest,
                       PRE_AppUnpinnedWhenPolicyDisabled) {
  // Set pref value in PRE_ to ensure that the pref value is set at the time of
  // user session start in the test.
  SetGraduationEnablement(false);
}

IN_PROC_BROWSER_TEST_F(GraduationManagerTest, AppUnpinnedWhenPolicyDisabled) {
  EXPECT_FALSE(IsItemPinned(ash::kGraduationAppId));
  EXPECT_EQ(apps::Readiness::kDisabledByPolicy,
            GetAppReadiness(ash::kGraduationAppId));

  SetGraduationEnablement(true);
  WaitForAppRegistryCommands(browser()->GetProfile());

  EXPECT_EQ(apps::Readiness::kReady, GetAppReadiness(ash::kGraduationAppId));
  EXPECT_TRUE(IsItemPinned(ash::kGraduationAppId));
}

IN_PROC_BROWSER_TEST_F(GraduationManagerTest,
                       PRE_AppUnpinnedWhenEndDateHasPassed) {
  // Set pref value in PRE_ to ensure that the pref value is set at the time of
  // user session start in the test.
  SetGraduationEnablementWithEndDate(true, 1, 10, 2024);
}

IN_PROC_BROWSER_TEST_F(GraduationManagerTest, AppUnpinnedWhenEndDateHasPassed) {
  EXPECT_TRUE(IsItemPinned(ash::kGraduationAppId));
  EXPECT_EQ(apps::Readiness::kReady, GetAppReadiness(ash::kGraduationAppId));

  // Fast forward to one day past the policy enablement end date set in the
  // pre-test.
  AdvanceTimeBy(base::Days(2));
  WaitForAppRegistryCommands(browser()->GetProfile());
  // Wait for the shelf item to finish being removed.
  WaitForShefItemRemoved();

  EXPECT_EQ(apps::Readiness::kDisabledByPolicy,
            GetAppReadiness(ash::kGraduationAppId));
  EXPECT_FALSE(IsItemPinned(ash::kGraduationAppId));
}

IN_PROC_BROWSER_TEST_F(GraduationManagerTest, GetLanguageCode) {
  // Browser tests should default to English.
  EXPECT_EQ("en-US", GetLanguageCode());

  // Switch the application locale to Spanish.
  base::RunLoop run_loop;
  locale_util::SwitchLanguage(
      g_browser_process->GetFeatures()->application_locale_storage(), "es",
      true, false, base::BindRepeating(&OnLocaleSwitched, &run_loop),
      ProfileManager::GetActiveUserProfile());
  run_loop.Run();

  EXPECT_EQ("es", GetLanguageCode());
}

class GraduationManagerWithConsumerUserTest
    : public SystemWebAppBrowserTestBase,
      public ::testing::WithParamInterface<bool> {
 public:
  GraduationManagerWithConsumerUserTest() {
    scoped_feature_list_.InitAndEnableFeature(features::kGraduation);
  }
  ~GraduationManagerWithConsumerUserTest() override = default;
  GraduationManagerWithConsumerUserTest(
      const GraduationManagerWithConsumerUserTest&) = delete;
  GraduationManagerWithConsumerUserTest& operator=(
      const GraduationManagerWithConsumerUserTest&) = delete;

  void SetUpOnMainThread() override {
    SystemWebAppBrowserTestBase::SetUpOnMainThread();
    logged_in_user_mixin_.LogInUser();
    WaitForTestSystemAppInstall();
    WaitForAppRegistryCommands(browser()->GetProfile());
  }

 private:
  bool IsChildUser() const { return GetParam(); }

  base::test::ScopedFeatureList scoped_feature_list_;

  LoggedInUserMixin logged_in_user_mixin_{
      &mixin_host_, /*test_base=*/this, embedded_test_server(),
      IsChildUser() ? LoggedInUserMixin::LogInType::kChild
                    : LoggedInUserMixin::LogInType::kConsumer};
};

IN_PROC_BROWSER_TEST_P(GraduationManagerWithConsumerUserTest, AppNotInstalled) {
  EXPECT_FALSE(GetManager().IsSystemWebApp(ash::kGraduationAppId));
}

INSTANTIATE_TEST_SUITE_P(All,
                         GraduationManagerWithConsumerUserTest,
                         testing::Bool());

}  // namespace ash
