chromium/ash/style/color_palette_controller_unittest.cc

// 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 "ash/style/color_palette_controller.h"

#include <ostream>

#include "ash/constants/ash_features.h"
#include "ash/constants/ash_pref_names.h"
#include "ash/public/cpp/ash_prefs.h"
#include "ash/public/cpp/personalization_app/time_of_day_test_utils.h"
#include "ash/public/cpp/wallpaper/wallpaper_info.h"
#include "ash/public/cpp/wallpaper/wallpaper_types.h"
#include "ash/session/session_controller_impl.h"
#include "ash/shell.h"
#include "ash/style/color_util.h"
#include "ash/style/dark_light_mode_controller_impl.h"
#include "ash/style/mojom/color_scheme.mojom-shared.h"
#include "ash/test/ash_test_base.h"
#include "ash/wallpaper/wallpaper_constants.h"
#include "ash/wallpaper/wallpaper_controller_impl.h"
#include "ash/wallpaper/wallpaper_controller_test_api.h"
#include "ash/wallpaper/wallpaper_utils/wallpaper_calculated_colors.h"
#include "base/functional/callback_helpers.h"
#include "base/json/values_util.h"
#include "base/memory/raw_ptr.h"
#include "base/run_loop.h"
#include "base/strings/stringprintf.h"
#include "base/test/bind.h"
#include "base/test/scoped_feature_list.h"
#include "components/user_manager/known_user.h"
#include "testing/gmock/include/gmock/gmock.h"
#include "third_party/skia/include/core/SkColor.h"
#include "ui/gfx/image/image_skia.h"

namespace ash {

namespace {

const char kUser[] = "[email protected]";
const AccountId kAccountId = AccountId::FromUserEmailGaiaId(kUser, kUser);
const style::mojom::ColorScheme kLocalColorScheme =
    style::mojom::ColorScheme::kVibrant;
const style::mojom::ColorScheme kDefaultColorScheme =
    style::mojom::ColorScheme::kTonalSpot;
const SkColor kCelebiColor = gfx::kGoogleBlue400;

// Returns a wallpaper info that captures the time of day wallpaper.
WallpaperInfo CreateTimeOfDayWallpaperInfo() {
  WallpaperInfo info =
      WallpaperInfo(std::string(), WALLPAPER_LAYOUT_STRETCH,
                    WallpaperType::kDefault, base::Time::Now().LocalMidnight());
  info.collection_id = wallpaper_constants::kTimeOfDayWallpaperCollectionId;
  info.asset_id = 1;
  info.unit_id = 1;
  return info;
}

// A nice magenta that is in the acceptable lightness range for dark and light.
// Hue: 281, Saturation: 100, Lightness: 50%.
constexpr SkColor kKMeanColor = SkColorSetRGB(0xae, 0x00, 0xff);

class MockPaletteObserver : public ColorPaletteController::Observer {
 public:
  MOCK_METHOD(void,
              OnColorPaletteChanging,
              (const ColorPaletteSeed& seed),
              (override));
};

// A helper to record updates to a `ui::NativeTheme`.
class TestObserver : public ui::NativeThemeObserver {
 public:
  TestObserver() = default;
  ~TestObserver() override = default;

  void OnNativeThemeUpdated(ui::NativeTheme* observed_theme) override {
    last_theme_ = observed_theme;
    call_count_++;
  }

  int call_count() { return call_count_; }

  ui::NativeTheme* last_theme() { return last_theme_; }

 private:
  raw_ptr<ui::NativeTheme> last_theme_ = nullptr;
  int call_count_ = 0;
};

// Matches a `SampleColorScheme` based on the `scheme` and `primary` attributes.
MATCHER_P2(Sample,
           scheme,
           primary_color,
           base::StringPrintf("where scheme is %u and the primary color is %x",
                              static_cast<int>(scheme),
                              primary_color)) {
  return arg.scheme == scheme && arg.primary == primary_color;
}

}  // namespace

class ColorPaletteControllerTest : public NoSessionAshTestBase {
 public:
  void SetUp() override {
    NoSessionAshTestBase::SetUp();
    GetSessionControllerClient()->Reset();
    GetSessionControllerClient()->AddUserSession(kAccountId, kUser);
    wallpaper_controller_ = Shell::Get()->wallpaper_controller();
    color_palette_controller_ = Shell::Get()->color_palette_controller();

    dark_light_mode_controller_ = Shell::Get()->dark_light_mode_controller();
    // Fix dark mode as off.
    dark_light_mode_controller_->SetDarkModeEnabledForTest(false);
  }

  void TearDown() override { NoSessionAshTestBase::TearDown(); }

  ColorPaletteController* color_palette_controller() {
    return color_palette_controller_;
  }

  DarkLightModeControllerImpl* dark_light_controller() {
    return dark_light_mode_controller_;
  }

  WallpaperControllerImpl* wallpaper_controller() {
    return wallpaper_controller_;
  }

  void UpdateWallpaperColor(SkColor color) {
    WallpaperControllerTestApi wallpaper(wallpaper_controller());
    wallpaper.SetCalculatedColors(
        WallpaperCalculatedColors(kKMeanColor, color));
    base::RunLoop().RunUntilIdle();
  }

  void SetUseKMeansPref(bool value) {
    PrefService* pref_service =
        Shell::Get()->session_controller()->GetUserPrefServiceForUser(
            kAccountId);
    pref_service->SetBoolean(prefs::kDynamicColorUseKMeans, value);
  }

 private:
  raw_ptr<DarkLightModeControllerImpl, DanglingUntriaged>
      dark_light_mode_controller_;  // unowned
  raw_ptr<WallpaperControllerImpl, DanglingUntriaged>
      wallpaper_controller_;  // unowned

  raw_ptr<ColorPaletteController, DanglingUntriaged> color_palette_controller_;
};

TEST_F(ColorPaletteControllerTest, ExpectedEmptyValues) {
  EXPECT_EQ(kDefaultColorScheme,
            color_palette_controller()->GetColorScheme(kAccountId));
  EXPECT_EQ(std::nullopt,
            color_palette_controller()->GetStaticColor(kAccountId));
}

// Verifies that when the TimeOfDayWallpaper feature is active but the wallpaper
// isn't a Time Of day wallpaper, the default color scheme is TonalSpot.
TEST_F(ColorPaletteControllerTest,
       ExpectedColorScheme_TimeOfDay_UsesDefaultScheme) {
  base::test::ScopedFeatureList feature_list;
  feature_list.InitWithFeatures(
      personalization_app::GetTimeOfDayEnabledFeatures(), {});
  EXPECT_EQ(kDefaultColorScheme,
            color_palette_controller()->GetColorScheme(kAccountId));
}

TEST_F(ColorPaletteControllerTest, SetColorScheme) {
  SimulateUserLogin(kAccountId);
  WallpaperControllerTestApi wallpaper(wallpaper_controller());
  wallpaper.SetCalculatedColors(
      WallpaperCalculatedColors(kKMeanColor, SK_ColorWHITE));
  const style::mojom::ColorScheme color_scheme =
      style::mojom::ColorScheme::kExpressive;

  color_palette_controller()->SetColorScheme(color_scheme, kAccountId,
                                             base::DoNothing());

  EXPECT_EQ(color_scheme,
            color_palette_controller()->GetColorScheme(kAccountId));
  EXPECT_EQ(std::nullopt,
            color_palette_controller()->GetStaticColor(kAccountId));
  auto color_palette_seed =
      color_palette_controller()->GetColorPaletteSeed(kAccountId);
  EXPECT_EQ(color_scheme, color_palette_seed->scheme);
  // Verify that the color scheme was saved to local state.
  auto local_color_scheme =
      user_manager::KnownUser(local_state())
          .FindIntPath(kAccountId, prefs::kDynamicColorColorScheme);
  EXPECT_EQ(color_scheme,
            static_cast<style::mojom::ColorScheme>(local_color_scheme.value()));
}

TEST_F(ColorPaletteControllerTest, SetStaticColor) {
  SimulateUserLogin(kAccountId);
  const SkColor static_color = SK_ColorGRAY;

  color_palette_controller()->SetStaticColor(static_color, kAccountId,
                                             base::DoNothing());

  EXPECT_EQ(static_color,
            color_palette_controller()->GetStaticColor(kAccountId));
  EXPECT_EQ(style::mojom::ColorScheme::kStatic,
            color_palette_controller()->GetColorScheme(kAccountId));
  auto color_palette_seed =
      color_palette_controller()->GetColorPaletteSeed(kAccountId);
  EXPECT_EQ(style::mojom::ColorScheme::kStatic, color_palette_seed->scheme);
  EXPECT_EQ(static_color, color_palette_seed->seed_color);
  auto local_color_scheme =
      user_manager::KnownUser(local_state())
          .FindIntPath(kAccountId, prefs::kDynamicColorColorScheme);
  EXPECT_EQ(style::mojom::ColorScheme::kStatic,
            static_cast<style::mojom::ColorScheme>(local_color_scheme.value()));
  const base::Value* value =
      user_manager::KnownUser(local_state())
          .FindPath(kAccountId, prefs::kDynamicColorSeedColor);
  // Verify that the color was saved to local state.
  const auto local_static_color = base::ValueToInt64(value);
  EXPECT_EQ(static_color, static_cast<SkColor>(local_static_color.value()));
}

TEST_F(ColorPaletteControllerTest, UpdateColorScheme_NotifiesObserver) {
  color_palette_controller()->SetColorScheme(
      style::mojom::ColorScheme::kVibrant, kAccountId, base::DoNothing());
  SimulateUserLogin(kAccountId);
  UpdateWallpaperColor(SK_ColorBLUE);
  const style::mojom::ColorScheme color_scheme =
      style::mojom::ColorScheme::kExpressive;
  PrefService* pref_service =
      Shell::Get()->session_controller()->GetUserPrefServiceForUser(kAccountId);

  MockPaletteObserver observer;
  base::ScopedObservation<ColorPaletteController,
                          ColorPaletteController::Observer>
      observation(&observer);
  observation.Observe(color_palette_controller());
  EXPECT_CALL(observer, OnColorPaletteChanging(testing::Field(
                            &ColorPaletteSeed::scheme, color_scheme)))
      .Times(1);

  pref_service->SetInteger(prefs::kDynamicColorColorScheme,
                           static_cast<int>(color_scheme));
  task_environment()->RunUntilIdle();
}

TEST_F(ColorPaletteControllerTest, UpdateStaticColor_NotifiesObserver) {
  color_palette_controller()->SetColorScheme(
      style::mojom::ColorScheme::kVibrant, kAccountId, base::DoNothing());
  SimulateUserLogin(kAccountId);
  color_palette_controller()->SetStaticColor(SK_ColorRED, kAccountId,
                                             base::DoNothing());
  UpdateWallpaperColor(SK_ColorBLUE);
  const SkColor static_color = SK_ColorGRAY;
  PrefService* pref_service =
      Shell::Get()->session_controller()->GetUserPrefServiceForUser(kAccountId);

  MockPaletteObserver observer;
  base::ScopedObservation<ColorPaletteController,
                          ColorPaletteController::Observer>
      observation(&observer);
  observation.Observe(color_palette_controller());
  EXPECT_CALL(observer, OnColorPaletteChanging(testing::Field(
                            &ColorPaletteSeed::seed_color, static_color)))
      .Times(1);

  pref_service->SetUint64(prefs::kDynamicColorSeedColor, static_color);
  task_environment()->RunUntilIdle();
}

TEST_F(ColorPaletteControllerTest, UpdateUseKMeans_NotifiesObserver) {
  color_palette_controller()->SetColorScheme(
      style::mojom::ColorScheme::kTonalSpot, kAccountId, base::DoNothing());
  SetUseKMeansPref(true);
  UpdateWallpaperColor(kCelebiColor);
  SimulateUserLogin(kAccountId);

  MockPaletteObserver observer;
  base::ScopedObservation<ColorPaletteController,
                          ColorPaletteController::Observer>
      observation(&observer);
  observation.Observe(color_palette_controller());
  EXPECT_CALL(observer,
              OnColorPaletteChanging(testing::AllOf(
                  testing::Field(&ColorPaletteSeed::scheme,
                                 style::mojom::ColorScheme::kTonalSpot),
                  testing::Field(&ColorPaletteSeed::seed_color, kCelebiColor))))
      .Times(1);

  SetUseKMeansPref(false);
  task_environment()->RunUntilIdle();
}

TEST_F(ColorPaletteControllerTest, ColorModeTriggersObserver) {
  // A seed color needs to be present for the observer to trigger.
  WallpaperControllerTestApi wallpaper(wallpaper_controller());
  wallpaper.SetCalculatedColors(
      WallpaperCalculatedColors(kKMeanColor, SK_ColorWHITE));

  // Initialize Dark mode to a known state.
  dark_light_controller()->SetDarkModeEnabledForTest(false);

  MockPaletteObserver observer;
  base::ScopedObservation<ColorPaletteController,
                          ColorPaletteController::Observer>
      observation(&observer);
  observation.Observe(color_palette_controller());

  EXPECT_CALL(observer, OnColorPaletteChanging(testing::Field(
                            &ColorPaletteSeed::color_mode,
                            ui::ColorProviderKey::ColorMode::kDark)))
      .Times(1);
  dark_light_controller()->SetDarkModeEnabledForTest(true);
}

TEST_F(ColorPaletteControllerTest, NativeTheme_DarkModeChanged) {
  // Set to a known state.
  dark_light_controller()->SetDarkModeEnabledForTest(true);
  WallpaperControllerTestApi wallpaper(wallpaper_controller());
  wallpaper.SetCalculatedColors(
      WallpaperCalculatedColors(SK_ColorWHITE, kCelebiColor));
  color_palette_controller()->SetColorScheme(
      style::mojom::ColorScheme::kVibrant, kAccountId, base::DoNothing());

  TestObserver observer;
  base::ScopedObservation<ui::NativeTheme, ui::NativeThemeObserver> observation(
      &observer);
  observation.Observe(ui::NativeTheme::GetInstanceForNativeUi());

  dark_light_controller()->SetDarkModeEnabledForTest(false);
  task_environment()->RunUntilIdle();

  EXPECT_EQ(1, observer.call_count());
  ASSERT_TRUE(observer.last_theme());
  EXPECT_EQ(ui::NativeTheme::ColorScheme::kLight,
            observer.last_theme()->GetDefaultSystemColorScheme());
  EXPECT_EQ(kCelebiColor, observer.last_theme()->user_color().value());
  EXPECT_THAT(observer.last_theme()->scheme_variant(),
              testing::Optional(ui::ColorProviderKey::SchemeVariant::kVibrant));
}

TEST_F(ColorPaletteControllerTest, GetSeedWithUnsetWallpaper) {
  WallpaperControllerTestApi wallpaper(wallpaper_controller());
  wallpaper.ResetCalculatedColors();

  // If we calculated wallpaper colors are unset, we can't produce a valid
  // seed.
  EXPECT_FALSE(color_palette_controller()->GetCurrentSeed().has_value());
}

TEST_F(ColorPaletteControllerTest, GenerateSampleScheme) {
  SimulateUserLogin(kAccountId);
  SetUseKMeansPref(false);

  SkColor seed = SkColorSetRGB(0xf5, 0x42, 0x45);  // Hue 359* Saturation 73%
                                                   // Vibrance 96%

  WallpaperControllerTestApi wallpaper(wallpaper_controller());
  wallpaper.SetCalculatedColors(WallpaperCalculatedColors(SK_ColorWHITE, seed));

  const style::mojom::ColorScheme schemes[] = {
      style::mojom::ColorScheme::kExpressive,
      style::mojom::ColorScheme::kTonalSpot};
  std::vector<SampleColorScheme> results;
  base::RunLoop runner;
  color_palette_controller()->GenerateSampleColorSchemes(
      schemes,
      base::BindLambdaForTesting(
          [&results, &runner](const std::vector<SampleColorScheme>& samples) {
            results.insert(results.begin(), samples.begin(), samples.end());
            runner.Quit();
          }));

  runner.Run();
  EXPECT_THAT(results, testing::UnorderedElementsAre(
                           Sample(style::mojom::ColorScheme::kTonalSpot,
                                  SkColorSetRGB(0xff, 0xb3, 0xae)),
                           Sample(style::mojom::ColorScheme::kExpressive,
                                  SkColorSetRGB(0xc8, 0xbf, 0xff))));
}

TEST_F(ColorPaletteControllerTest, GenerateSampleScheme_AllValues_Teal) {
  SkColor seed = SkColorSetRGB(0x00, 0xbf, 0x7f);  // Hue 160* Saturation 100%
                                                   // Vibrance 75%

  WallpaperControllerTestApi wallpaper(wallpaper_controller());
  wallpaper.SetCalculatedColors(WallpaperCalculatedColors(SK_ColorWHITE, seed));

  const style::mojom::ColorScheme schemes[] = {
      style::mojom::ColorScheme::kVibrant};
  std::vector<SampleColorScheme> results;
  base::RunLoop runner;
  color_palette_controller()->GenerateSampleColorSchemes(
      schemes,
      base::BindLambdaForTesting(
          [&results, &runner](const std::vector<SampleColorScheme>& samples) {
            results.insert(results.begin(), samples.begin(), samples.end());
            runner.Quit();
          }));

  runner.Run();
  ASSERT_THAT(results, testing::SizeIs(1));
  auto& result = results.front();
  EXPECT_THAT(result, testing::Eq(SampleColorScheme{
                          .scheme = style::mojom::ColorScheme::kVibrant,
                          .primary = SkColorSetRGB(0x00, 0xc3, 0x82),
                          .secondary = SkColorSetRGB(0x00, 0x88, 0x59),
                          .tertiary = SkColorSetRGB(0x70, 0xb7, 0xb7)}));
}

TEST_F(ColorPaletteControllerTest, NewUser_UsesCelebiColor) {
  SimulateNewUserFirstLogin("[email protected]");
  base::RunLoop().RunUntilIdle();

  UpdateWallpaperColor(kCelebiColor);

  ASSERT_EQ(kCelebiColor,
            color_palette_controller()->GetCurrentSeed()->seed_color);
}

TEST_F(ColorPaletteControllerTest,
       UseKMeans_LogsInForTheFirstTime_UsesCelebiColor) {
  const SkColor celebi_color = SK_ColorBLUE;
  SetUseKMeansPref(true);

  SimulateNewUserFirstLogin("[email protected]");
  base::RunLoop().RunUntilIdle();
  UpdateWallpaperColor(celebi_color);

  ASSERT_EQ(celebi_color,
            color_palette_controller()->GetCurrentSeed()->seed_color);
}

TEST_F(ColorPaletteControllerTest, ExistingUser_UsesKMeansColor) {
  const bool dark_mode = true;
  dark_light_controller()->SetDarkModeEnabledForTest(dark_mode);

  SimulateUserLogin(kAccountId);
  base::RunLoop().RunUntilIdle();
  UpdateWallpaperColor(kCelebiColor);

  ASSERT_EQ(ColorUtil::AdjustKMeansColor(kKMeanColor, dark_mode),
            color_palette_controller()->GetCurrentSeed()->seed_color);
}

TEST_F(ColorPaletteControllerTest,
       GetWallpaperColorOrDefault_UseKMeans_TonalSpot_ReturnsKMeans) {
  const bool dark_mode = true;
  dark_light_controller()->SetDarkModeEnabledForTest(dark_mode);
  SimulateUserLogin(kAccountId);
  UpdateWallpaperColor(kCelebiColor);
  color_palette_controller()->SetColorScheme(
      style::mojom::ColorScheme::kTonalSpot, kAccountId, base::DoNothing());
  SetUseKMeansPref(true);

  SkColor color =
      color_palette_controller()->GetUserWallpaperColorOrDefault(SK_ColorBLUE);

  ASSERT_EQ(ColorUtil::AdjustKMeansColor(kKMeanColor, dark_mode), color);
}

TEST_F(ColorPaletteControllerTest,
       GetWallpaperColorOrDefault_UseKMeans_NotTonalSpot_ReturnsCelebiColor) {
  SimulateUserLogin(kAccountId);
  UpdateWallpaperColor(kCelebiColor);
  color_palette_controller()->SetColorScheme(
      style::mojom::ColorScheme::kVibrant, kAccountId, base::DoNothing());
  SetUseKMeansPref(true);

  SkColor color =
      color_palette_controller()->GetUserWallpaperColorOrDefault(SK_ColorBLUE);

  ASSERT_EQ(kCelebiColor, color);
}

TEST_F(ColorPaletteControllerTest,
       GetWallpaperColorOrDefault_UseKMeansIsFalse_TonalSpot_ReturnsCelebi) {
  SimulateUserLogin(kAccountId);
  UpdateWallpaperColor(kCelebiColor);
  SetUseKMeansPref(false);
  color_palette_controller()->SetColorScheme(
      style::mojom::ColorScheme::kTonalSpot, kAccountId, base::DoNothing());

  SkColor color =
      color_palette_controller()->GetUserWallpaperColorOrDefault(SK_ColorBLUE);

  ASSERT_EQ(kCelebiColor, color);
}

TEST_F(ColorPaletteControllerTest, GuestLogin_UsesCelebiColor) {
  const SkColor celebi_color = SK_ColorBLUE;

  SimulateGuestLogin();
  base::RunLoop().RunUntilIdle();
  UpdateWallpaperColor(celebi_color);

  ASSERT_EQ(celebi_color,
            color_palette_controller()->GetCurrentSeed()->seed_color);
}

TEST_F(ColorPaletteControllerTest, WallpaperChanged_TurnsOffKMeans) {
  const SkColor celebi_color = SK_ColorBLUE;
  SetUseKMeansPref(true);
  SimulateUserLogin(kAccountId);
  UpdateWallpaperColor(celebi_color);
  gfx::Size display_size =
      display::Screen::GetScreen()->GetPrimaryDisplay().GetSizeInPixel();
  SkBitmap bitmap;
  bitmap.allocN32Pixels(display_size.width(), display_size.height(),
                        /*isOpaque=*/true);
  bitmap.eraseColor(SK_ColorRED);
  const auto image = gfx::ImageSkia::CreateFrom1xBitmap(std::move(bitmap));

  // Update wallpaper.
  wallpaper_controller()->SetDecodedCustomWallpaper(
      kAccountId, "bluey", ash::WALLPAPER_LAYOUT_CENTER_CROPPED,
      /*preview_mode=*/false, base::DoNothing(), /*file_path=*/"", image);

  ASSERT_EQ(celebi_color,
            color_palette_controller()->GetCurrentSeed()->seed_color);
}

TEST_F(ColorPaletteControllerTest, UseKMeansColor_OnlyTonalSpotUsesKMeans) {
  const bool dark_mode = true;
  dark_light_controller()->SetDarkModeEnabledForTest(dark_mode);
  SimulateUserLogin(kAccountId);
  SetUseKMeansPref(true);
  UpdateWallpaperColor(kCelebiColor);
  base::RunLoop().RunUntilIdle();

  color_palette_controller()->SetColorScheme(
      style::mojom::ColorScheme::kTonalSpot, kAccountId, base::DoNothing());
  ASSERT_EQ(ColorUtil::AdjustKMeansColor(kKMeanColor, dark_mode),
            color_palette_controller()->GetCurrentSeed()->seed_color);

  color_palette_controller()->SetColorScheme(
      style::mojom::ColorScheme::kNeutral, kAccountId, base::DoNothing());
  ASSERT_EQ(kCelebiColor,
            color_palette_controller()->GetCurrentSeed()->seed_color);

  color_palette_controller()->SetColorScheme(
      style::mojom::ColorScheme::kVibrant, kAccountId, base::DoNothing());
  ASSERT_EQ(kCelebiColor,
            color_palette_controller()->GetCurrentSeed()->seed_color);

  color_palette_controller()->SetColorScheme(
      style::mojom::ColorScheme::kExpressive, kAccountId, base::DoNothing());
  ASSERT_EQ(kCelebiColor,
            color_palette_controller()->GetCurrentSeed()->seed_color);
}

TEST_F(ColorPaletteControllerTest, WithoutUseKMeansColor_AllSchemesUseCelebi) {
  const SkColor celebi_color = SK_ColorBLUE;
  SimulateUserLogin(kAccountId);
  SetUseKMeansPref(false);
  UpdateWallpaperColor(SK_ColorBLUE);
  base::RunLoop().RunUntilIdle();

  color_palette_controller()->SetColorScheme(
      style::mojom::ColorScheme::kTonalSpot, kAccountId, base::DoNothing());
  ASSERT_EQ(celebi_color,
            color_palette_controller()->GetCurrentSeed()->seed_color);

  color_palette_controller()->SetColorScheme(
      style::mojom::ColorScheme::kNeutral, kAccountId, base::DoNothing());
  ASSERT_EQ(celebi_color,
            color_palette_controller()->GetCurrentSeed()->seed_color);

  color_palette_controller()->SetColorScheme(
      style::mojom::ColorScheme::kVibrant, kAccountId, base::DoNothing());
  ASSERT_EQ(celebi_color,
            color_palette_controller()->GetCurrentSeed()->seed_color);

  color_palette_controller()->SetColorScheme(
      style::mojom::ColorScheme::kExpressive, kAccountId, base::DoNothing());
  ASSERT_EQ(celebi_color,
            color_palette_controller()->GetCurrentSeed()->seed_color);
}

TEST_F(ColorPaletteControllerTest, GetSampleColorSchemes_WithKMeans) {
  SimulateUserLogin(kAccountId);
  SetUseKMeansPref(true);

  SkColor seed = SkColorSetRGB(0xf5, 0x42, 0x45);  // Hue 359* Saturation 73%
                                                   // Vibrance 96%

  WallpaperControllerTestApi wallpaper(wallpaper_controller());
  wallpaper.SetCalculatedColors(WallpaperCalculatedColors(SK_ColorWHITE, seed));

  const style::mojom::ColorScheme schemes[] = {
      style::mojom::ColorScheme::kExpressive,
      style::mojom::ColorScheme::kTonalSpot};
  std::vector<SampleColorScheme> results;
  base::RunLoop runner;
  color_palette_controller()->GenerateSampleColorSchemes(
      schemes,
      base::BindLambdaForTesting(
          [&results, &runner](const std::vector<SampleColorScheme>& samples) {
            results.insert(results.begin(), samples.begin(), samples.end());
            runner.Quit();
          }));

  runner.Run();
  // The tonal spot primary color differs from that in the
  // |GenerateSampleScheme| test, but the expressive primary color does not.
  EXPECT_THAT(results, testing::UnorderedElementsAre(
                           Sample(style::mojom::ColorScheme::kTonalSpot,
                                  SkColorSetRGB(0x74, 0xd5, 0xe4)),
                           Sample(style::mojom::ColorScheme::kExpressive,
                                  SkColorSetRGB(0xc8, 0xbf, 0xff))));
}

TEST_F(ColorPaletteControllerTest, OneNotificationOnActiveUserChange) {
  TestObserver observer;
  base::ScopedObservation<ui::NativeTheme, ui::NativeThemeObserver> observation(
      &observer);
  observation.Observe(ui::NativeTheme::GetInstanceForNativeUi());

  SimulateUserLogin(kAccountId);

  EXPECT_EQ(1, observer.call_count());
}

class ColorPaletteControllerLocalPrefTest : public ColorPaletteControllerTest {
 public:
  void SetUp() override {
    ColorPaletteControllerTest::SetUp();
    GetSessionControllerClient()->Reset();
  }

  //  Sets the local ColorScheme to kVibrant. The synced color scheme remains
  //  the default, kTonalSpot.
  void SetUpLocalPrefs() {
    user_manager::KnownUser(local_state())
        .SetIntegerPref(kAccountId, prefs::kDynamicColorColorScheme,
                        static_cast<int>(kLocalColorScheme));
  }

  style::mojom::ColorScheme GetLocalColorScheme() {
    auto local_color_scheme =
        user_manager::KnownUser(local_state())
            .FindIntPath(kAccountId, prefs::kDynamicColorColorScheme);
    return static_cast<style::mojom::ColorScheme>(local_color_scheme.value());
  }

  std::optional<bool> GetLocalUseKMeans() {
    const base::Value* local_color_scheme =
        user_manager::KnownUser(local_state())
            .FindPath(kAccountId, prefs::kDynamicColorUseKMeans);
    if (!local_color_scheme) {
      return {};
    }
    return local_color_scheme->GetIfBool();
  }

  void SetUseKMeansLocalPref(bool use_k_means) {
    user_manager::KnownUser(local_state())
        .SetBooleanPref(kAccountId, prefs::kDynamicColorUseKMeans, use_k_means);
  }
};

TEST_F(ColorPaletteControllerLocalPrefTest, OnUserLogin_UpdatesLocalPrefs) {
  SetUpLocalPrefs();
  const auto wallpaper_color = SK_ColorGRAY;
  UpdateWallpaperColor(wallpaper_color);
  SetUseKMeansLocalPref(false);
  EXPECT_EQ(kLocalColorScheme, GetLocalColorScheme());

  SimulateUserLogin(kAccountId);

  // Expect that the local prefs are updated when the user logs in.
  EXPECT_EQ(kDefaultColorScheme, GetLocalColorScheme());
  EXPECT_TRUE(*GetLocalUseKMeans());
}

TEST_F(ColorPaletteControllerLocalPrefTest,
       SelectLocalAccount_NotifiesObservers) {
  SetUpLocalPrefs();
  SessionController::Get()->SetClient(nullptr);

  MockPaletteObserver observer;
  base::ScopedObservation<ColorPaletteController,
                          ColorPaletteController::Observer>
      observation(&observer);
  observation.Observe(color_palette_controller());
  EXPECT_CALL(observer, OnColorPaletteChanging(testing::Field(
                            &ColorPaletteSeed::scheme, kLocalColorScheme)))
      .Times(1);

  color_palette_controller()->SelectLocalAccount(kAccountId);
}

TEST_F(ColorPaletteControllerLocalPrefTest,
       SelectLocalAccount_UseKMeansIsFalse_UsesCelebiColor) {
  SetUseKMeansLocalPref(false);
  UpdateWallpaperColor(kCelebiColor);

  MockPaletteObserver observer;
  base::ScopedObservation<ColorPaletteController,
                          ColorPaletteController::Observer>
      observation(&observer);
  observation.Observe(color_palette_controller());
  EXPECT_CALL(observer,
              OnColorPaletteChanging(testing::AllOf(
                  testing::Field(&ColorPaletteSeed::scheme,
                                 style::mojom::ColorScheme::kTonalSpot),
                  testing::Field(&ColorPaletteSeed::seed_color, kCelebiColor))))
      .Times(1);

  color_palette_controller()->SelectLocalAccount(kAccountId);
  base::RunLoop().RunUntilIdle();
}

TEST_F(ColorPaletteControllerLocalPrefTest,
       SelectLocalAccount_UseKMeansIsTrue_TonalSpot_UsesKMeansColor) {
  const bool dark_mode = true;
  dark_light_controller()->SetDarkModeEnabledForTest(dark_mode);
  SimulateUserLogin(kAccountId);
  SetUseKMeansLocalPref(true);
  SessionController::Get()->SetClient(nullptr);
  UpdateWallpaperColor(kCelebiColor);

  MockPaletteObserver observer;
  base::ScopedObservation<ColorPaletteController,
                          ColorPaletteController::Observer>
      observation(&observer);
  observation.Observe(color_palette_controller());
  EXPECT_CALL(observer,
              OnColorPaletteChanging(testing::AllOf(
                  testing::Field(&ColorPaletteSeed::scheme,
                                 style::mojom::ColorScheme::kTonalSpot),
                  testing::Field(
                      &ColorPaletteSeed::seed_color,
                      ColorUtil::AdjustKMeansColor(kKMeanColor, dark_mode)))))
      .Times(1);

  color_palette_controller()->SelectLocalAccount(kAccountId);
  base::RunLoop().RunUntilIdle();
}

TEST_F(ColorPaletteControllerLocalPrefTest,
       SelectLocalAccount_UseKMeansIsTrue_Vibrant_UsesCelebiColor) {
  SetUpLocalPrefs();
  SessionController::Get()->SetClient(nullptr);
  SetUseKMeansLocalPref(true);
  UpdateWallpaperColor(kCelebiColor);

  MockPaletteObserver observer;
  base::ScopedObservation<ColorPaletteController,
                          ColorPaletteController::Observer>
      observation(&observer);
  observation.Observe(color_palette_controller());
  EXPECT_CALL(observer,
              OnColorPaletteChanging(testing::AllOf(
                  testing::Field(&ColorPaletteSeed::scheme,
                                 style::mojom::ColorScheme::kVibrant),
                  testing::Field(&ColorPaletteSeed::seed_color, kCelebiColor))))
      .Times(1);

  color_palette_controller()->SelectLocalAccount(kAccountId);
  base::RunLoop().RunUntilIdle();
}

// Verifies that when the TimeOfDayWallpaper wallpaper is active, the default
// color scheme is Neutral instead of TonalSpot in local_state.
TEST_F(ColorPaletteControllerLocalPrefTest, NoLocalAccount_TimeOfDayScheme) {
  base::test::ScopedFeatureList feature_list;
  feature_list.InitWithFeatures(
      personalization_app::GetTimeOfDayEnabledFeatures(), {});
  // Sets the current wallpaper to be ToD.
  WallpaperControllerTestApi wallpaper(wallpaper_controller());
  wallpaper.ShowWallpaperImage(CreateTimeOfDayWallpaperInfo(),
                               /*preview_mode=*/false, /*is_override=*/false);
  base::RunLoop().RunUntilIdle();

  // Since `kAccountId` is not logged in, this triggers default local_state
  // behavior.
  EXPECT_EQ(style::mojom::ColorScheme::kNeutral,
            color_palette_controller()->GetColorScheme(kAccountId));
}

TEST_F(ColorPaletteControllerLocalPrefTest,
       SelectLocalAccount_NoLocalState_NotifiesObserversWithDefault) {
  SessionController::Get()->SetClient(nullptr);
  UpdateWallpaperColor(kCelebiColor);

  MockPaletteObserver observer;
  base::ScopedObservation<ColorPaletteController,
                          ColorPaletteController::Observer>
      observation(&observer);
  observation.Observe(color_palette_controller());
  EXPECT_CALL(
      observer,
      OnColorPaletteChanging(testing::AllOf(
          testing::Field(&ColorPaletteSeed::scheme, kDefaultColorScheme),
          testing::Field(&ColorPaletteSeed::seed_color, kCelebiColor))))
      .Times(1);

  color_palette_controller()->SelectLocalAccount(kAccountId);
}

TEST_F(ColorPaletteControllerLocalPrefTest,
       UpdateWallpaperColor_WithSession_NotifiesObservers) {
  SetUpLocalPrefs();
  SimulateUserLogin(kAccountId);
  color_palette_controller()->SetColorScheme(kLocalColorScheme, kAccountId,
                                             base::DoNothing());
  base::RunLoop().RunUntilIdle();

  MockPaletteObserver observer;
  base::ScopedObservation<ColorPaletteController,
                          ColorPaletteController::Observer>
      observation(&observer);
  observation.Observe(color_palette_controller());
  EXPECT_CALL(observer, OnColorPaletteChanging(testing::Field(
                            &ColorPaletteSeed::scheme, kLocalColorScheme)))
      .Times(1);

  UpdateWallpaperColor(SK_ColorWHITE);
}

TEST_F(ColorPaletteControllerLocalPrefTest,
       UpdateWallpaperColor_WithoutSession_DoesNotNotifyObservers) {
  MockPaletteObserver observer;
  base::ScopedObservation<ColorPaletteController,
                          ColorPaletteController::Observer>
      observation(&observer);
  observation.Observe(color_palette_controller());
  EXPECT_CALL(observer, OnColorPaletteChanging(testing::_)).Times(0);

  UpdateWallpaperColor(SK_ColorWHITE);
}

TEST_F(ColorPaletteControllerLocalPrefTest,
       UpdateWallpaperColor_WithOobeSession_NotifiesObservers) {
  GetSessionControllerClient()->SetSessionState(
      session_manager::SessionState::OOBE);
  // Set the UseKMeans pref to make sure that it does not affect OOBE.
  SetUseKMeansLocalPref(true);
  MockPaletteObserver observer;
  base::ScopedObservation<ColorPaletteController,
                          ColorPaletteController::Observer>
      observation(&observer);
  observation.Observe(color_palette_controller());
  // OOBE should always use the celebi color.
  EXPECT_CALL(observer, OnColorPaletteChanging(testing::Field(
                            &ColorPaletteSeed::seed_color, kCelebiColor)))
      .Times(1);

  UpdateWallpaperColor(kCelebiColor);
}

TEST_F(ColorPaletteControllerLocalPrefTest,
       UpdateWallpaperColor_WithOobeLogin_NotifiesObservers) {
  GetSessionControllerClient()->SetSessionState(
      session_manager::SessionState::LOGIN_PRIMARY);
  // Set the UseKMeans pref to make sure that it does not affect OOBE.
  SetUseKMeansLocalPref(true);
  MockPaletteObserver observer;
  base::ScopedObservation<ColorPaletteController,
                          ColorPaletteController::Observer>
      observation(&observer);
  observation.Observe(color_palette_controller());
  // OOBE should always use the celebi color.
  EXPECT_CALL(observer, OnColorPaletteChanging(testing::Field(
                            &ColorPaletteSeed::seed_color, kCelebiColor)))
      .Times(1);

  LoginScreen::Get()->GetModel()->NotifyOobeDialogState(
      OobeDialogState::GAIA_SIGNIN);
  UpdateWallpaperColor(kCelebiColor);
}

TEST_F(ColorPaletteControllerLocalPrefTest,
       UpdateWallpaperColor_WithNonOobeLogin_DoesNotNotifyObservers) {
  GetSessionControllerClient()->SetSessionState(
      session_manager::SessionState::LOGIN_PRIMARY);
  MockPaletteObserver observer;
  base::ScopedObservation<ColorPaletteController,
                          ColorPaletteController::Observer>
      observation(&observer);
  observation.Observe(color_palette_controller());
  EXPECT_CALL(observer, OnColorPaletteChanging(testing::_)).Times(0);

  LoginScreen::Get()->GetModel()->NotifyOobeDialogState(
      OobeDialogState::HIDDEN);
  UpdateWallpaperColor(SK_ColorWHITE);
}

// Helper to print better matcher errors.
void PrintTo(const SampleColorScheme& scheme, std::ostream* os) {
  *os << base::StringPrintf(
      "SampleColorScheme(scheme: %u primary: %x secondary: %x tertiary: %x)",
      static_cast<unsigned>(scheme.scheme), scheme.primary, scheme.secondary,
      scheme.tertiary);
}

}  // namespace ash