// 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 "ui/linux/dark_mode_manager_linux.h"

#include "base/memory/raw_ptr.h"
#include "base/test/scoped_feature_list.h"
#include "components/dbus/xdg/portal.h"
#include "dbus/mock_bus.h"
#include "dbus/mock_object_proxy.h"
#include "testing/gmock/include/gmock/gmock.h"
#include "testing/gtest/include/gtest/gtest.h"
#include "ui/base/ui_base_features.h"
#include "ui/linux/fake_linux_ui.h"
#include "ui/native_theme/mock_os_settings_provider.h"
#include "ui/native_theme/native_theme.h"

namespace ui {

namespace {

class MockLinuxUi : public FakeLinuxUi {
 public:
  MOCK_METHOD(void, SetDarkTheme, (bool dark), (override));
  MOCK_METHOD(void,
              SetColorScheme,
              (std::optional<bool> prefer_dark),
              (override));
  MOCK_METHOD(void, SetAccentColor, (std::optional<SkColor> color), (override));
};

// Matches a method call to the specified dbus target.
MATCHER_P2(Calls, interface, member, "") {
  return arg->GetInterface() == interface && arg->GetMember() == member;
}

}  // namespace

using testing::_;
using testing::Mock;
using testing::Return;
using testing::StrictMock;

class DarkModeManagerLinuxTest : public testing::Test {
 public:
  ~DarkModeManagerLinuxTest() override = default;

 protected:
  // The web color scheme is sourced from the active `OsSettingsProvider` via
  // `UpdateVariablesForToolkitSettings()`, so it reflects what the manager
  // pushed into the toolkit (emulated by the `SetColorScheme` wiring below).
  bool PrefersDarkTheme() const {
    return NativeTheme::GetInstanceForNativeUi()->preferred_color_scheme() ==
           NativeTheme::PreferredColorScheme::kDark;
  }

  dbus::ObjectProxy::SignalCallback& setting_changed_callback() {
    return setting_changed_callback_;
  }
  dbus::ObjectProxy::OnConnectedCallback& signal_connected_callback() {
    return signal_connected_callback_;
  }
  dbus::ObjectProxy::ResponseOrErrorCallback& color_scheme_callback() {
    return color_scheme_callback_;
  }
  dbus::ObjectProxy::ResponseOrErrorCallback& accent_color_callback() {
    return accent_color_callback_;
  }
  MockOsSettingsProvider& os_settings_provider() {
    return os_settings_provider_;
  }
  MockLinuxUi* mock_linux_ui() { return mock_linux_ui_.get(); }

 private:
  void SetUp() override {
    dbus_xdg::SetPortalStateForTesting(
        dbus_xdg::PortalRegistrarState::kSuccess);
    mock_bus_ = base::MakeRefCounted<dbus::MockBus>(dbus::Bus::Options());
    mock_dbus_proxy_ = base::MakeRefCounted<dbus::MockObjectProxy>(
        mock_bus_.get(), DBUS_SERVICE_DBUS, dbus::ObjectPath(DBUS_PATH_DBUS));
    mock_portal_proxy_ =
        base::MakeRefCounted<StrictMock<dbus::MockObjectProxy>>(
            mock_bus_.get(), DarkModeManagerLinux::kFreedesktopSettingsService,
            dbus::ObjectPath(
                DarkModeManagerLinux::kFreedesktopSettingsObjectPath));

    EXPECT_CALL(*mock_bus_, GetObjectProxy(DBUS_SERVICE_DBUS,
                                           dbus::ObjectPath(DBUS_PATH_DBUS)))
        .WillRepeatedly(Return(mock_dbus_proxy_.get()));

    EXPECT_CALL(*mock_bus_,
                GetObjectProxy(
                    DarkModeManagerLinux::kFreedesktopSettingsService,
                    dbus::ObjectPath(
                        DarkModeManagerLinux::kFreedesktopSettingsObjectPath)))
        .WillRepeatedly(Return(mock_portal_proxy_.get()));

    EXPECT_CALL(
        *mock_portal_proxy_,
        ConnectToSignal(DarkModeManagerLinux::kFreedesktopSettingsInterface,
                        DarkModeManagerLinux::kSettingChangedSignal, _, _))
        .WillOnce(
            [&](const std::string& interface_name,
                const std::string& signal_name,
                dbus::ObjectProxy::SignalCallback signal_callback,
                dbus::ObjectProxy::OnConnectedCallback on_connected_callback) {
              setting_changed_callback_ = signal_callback;
              signal_connected_callback_ = std::move(on_connected_callback);
            });

    EXPECT_CALL(*mock_portal_proxy_,
                CallMethodWithErrorResponse(
                    Calls(DarkModeManagerLinux::kFreedesktopSettingsInterface,
                          DarkModeManagerLinux::kReadMethod),
                    _, _))
        .WillOnce([&](dbus::MethodCall* method_call, int timeout_ms,
                      dbus::ObjectProxy::ResponseOrErrorCallback callback) {
          color_scheme_callback_ = std::move(callback);
        })
        .WillOnce([&](dbus::MethodCall* method_call, int timeout_ms,
                      dbus::ObjectProxy::ResponseOrErrorCallback callback) {
          accent_color_callback_ = std::move(callback);
        });

    mock_linux_ui_ = std::make_unique<MockLinuxUi>();
    linux_ui_themes_ = std::vector<raw_ptr<LinuxUiTheme, VectorExperimental>>{
        mock_linux_ui_.get()};

    // In production, each toolkit's `SetColorScheme()`/`SetAccentColor()` route
    // the value through its `OsSettingsProvider`, which is what drives the web
    // `NativeTheme` via `UpdateVariablesForToolkitSettings()`. Emulate that
    // here so the assertions below exercise the provider path rather than a
    // direct write from the manager.
    ON_CALL(*mock_linux_ui_, SetColorScheme(_))
        .WillByDefault([this](std::optional<bool> prefer_dark) {
          os_settings_provider_.SetPreferredColorScheme(
              prefer_dark
                  ? (*prefer_dark ? NativeTheme::PreferredColorScheme::kDark
                                  : NativeTheme::PreferredColorScheme::kLight)
                  : NativeTheme::PreferredColorScheme::kNoPreference);
        });
    ON_CALL(*mock_linux_ui_, SetAccentColor(_))
        .WillByDefault([this](std::optional<SkColor> color) {
          if (color.has_value()) {
            os_settings_provider_.SetAccentColor(color.value());
          }
        });

    enable_portal_accent_color_.InitAndEnableFeature(
        features::kUsePortalAccentColor);

    // Baseline: no portal preference yet, so the toolkit-sourced scheme (here,
    // the mock provider) is "no preference".
    os_settings_provider_.SetPreferredColorScheme(
        NativeTheme::PreferredColorScheme::kNoPreference);

    manager_ =
        std::make_unique<DarkModeManagerLinux>(mock_bus_, &linux_ui_themes_);

    auto* const native_theme = ui::NativeTheme::GetInstanceForNativeUi();
    EXPECT_FALSE(PrefersDarkTheme());
    EXPECT_EQ(native_theme->preferred_color_scheme(),
              NativeTheme::PreferredColorScheme::kNoPreference);
    EXPECT_FALSE(native_theme->user_color().has_value());
  }

  void TearDown() override {
    manager_.reset();
    dbus_xdg::SetPortalStateForTesting(dbus_xdg::PortalRegistrarState::kIdle);
  }

  std::unique_ptr<MockLinuxUi> mock_linux_ui_;
  std::vector<raw_ptr<LinuxUiTheme, VectorExperimental>> linux_ui_themes_;

  MockOsSettingsProvider os_settings_provider_;

  scoped_refptr<dbus::MockBus> mock_bus_;
  scoped_refptr<dbus::MockObjectProxy> mock_dbus_proxy_;
  scoped_refptr<dbus::MockObjectProxy> mock_portal_proxy_;

  dbus::ObjectProxy::SignalCallback setting_changed_callback_;
  dbus::ObjectProxy::OnConnectedCallback signal_connected_callback_;

  dbus::ObjectProxy::ResponseOrErrorCallback color_scheme_callback_;
  dbus::ObjectProxy::ResponseOrErrorCallback accent_color_callback_;

  base::test::ScopedFeatureList enable_portal_accent_color_;

  std::unique_ptr<DarkModeManagerLinux> manager_;
};

TEST_F(DarkModeManagerLinuxTest, UseNativeThemeSetting) {
  // Without a portal preference, the toolkit-sourced scheme (the provider)
  // drives the web theme directly; the manager is not involved.
  os_settings_provider().SetPreferredColorScheme(
      NativeTheme::PreferredColorScheme::kDark);
  EXPECT_TRUE(PrefersDarkTheme());
  os_settings_provider().SetPreferredColorScheme(
      NativeTheme::PreferredColorScheme::kLight);
  EXPECT_FALSE(PrefersDarkTheme());

  // Let the manager know the DBus method call and signal connection failed.
  dbus::MethodCall method_call(
      DarkModeManagerLinux::kFreedesktopSettingsInterface,
      DarkModeManagerLinux::kReadMethod);
  method_call.SetSerial(123);
  auto error = dbus::ErrorResponse::FromMethodCall(
      &method_call, "org.freedesktop.DBus.Error.Failed", "");
  std::move(color_scheme_callback()).Run(nullptr, error.get());
  std::move(signal_connected_callback())
      .Run(DarkModeManagerLinux::kFreedesktopSettingsInterface,
           DarkModeManagerLinux::kSettingChangedSignal, false);

  // The toolkit-sourced scheme should still drive the web theme.
  os_settings_provider().SetPreferredColorScheme(
      NativeTheme::PreferredColorScheme::kDark);
  EXPECT_TRUE(PrefersDarkTheme());
  os_settings_provider().SetPreferredColorScheme(
      NativeTheme::PreferredColorScheme::kLight);
  EXPECT_FALSE(PrefersDarkTheme());
}

TEST_F(DarkModeManagerLinuxTest, UsePortalSetting) {
  // Let the manager know the DBus method call and signal connection succeeded.
  dbus::MethodCall method_call(
      DarkModeManagerLinux::kFreedesktopSettingsInterface,
      DarkModeManagerLinux::kReadMethod);
  method_call.SetSerial(123);
  auto response = dbus::Response::FromMethodCall(&method_call);
  dbus::MessageWriter writer(response.get());
  dbus::MessageWriter variant_writer(nullptr);
  writer.OpenVariant("v", &variant_writer);
  variant_writer.AppendVariantOfUint32(static_cast<uint32_t>(
      DarkModeManagerLinux::FreedesktopColorScheme::kDark));
  writer.CloseContainer(&variant_writer);
  // The manager pushes the portal preference into the toolkit: SetDarkTheme()
  // for native widgets, and SetColorScheme() for the web theme (via the
  // provider).
  EXPECT_CALL(*mock_linux_ui(), SetDarkTheme(true));
  EXPECT_CALL(*mock_linux_ui(), SetColorScheme(std::optional<bool>(true)));
  std::move(color_scheme_callback()).Run(response.get(), nullptr);
  EXPECT_TRUE(PrefersDarkTheme());

  // Changes in the portal preference should be processed by the manager and the
  // web theme should be updated.
  dbus::Signal signal(DarkModeManagerLinux::kFreedesktopSettingsInterface,
                      DarkModeManagerLinux::kSettingChangedSignal);
  dbus::MessageWriter signal_writer(&signal);
  signal_writer.AppendString(DarkModeManagerLinux::kSettingsNamespace);
  signal_writer.AppendString(DarkModeManagerLinux::kColorSchemeKey);
  signal_writer.AppendVariantOfUint32(static_cast<uint32_t>(
      DarkModeManagerLinux::FreedesktopColorScheme::kLight));
  EXPECT_CALL(*mock_linux_ui(), SetDarkTheme(false));
  EXPECT_CALL(*mock_linux_ui(), SetColorScheme(std::optional<bool>(false)));
  std::move(setting_changed_callback()).Run(&signal);
  EXPECT_FALSE(PrefersDarkTheme());
}

TEST_F(DarkModeManagerLinuxTest, UsePortalAccentColor) {
  // Let the manager know the DBus method call and signal connection succeeded.
  dbus::MethodCall method_call(
      DarkModeManagerLinux::kFreedesktopSettingsInterface,
      DarkModeManagerLinux::kReadMethod);
  method_call.SetSerial(123);
  auto response = dbus::Response::FromMethodCall(&method_call);
  dbus::MessageWriter writer(response.get());
  dbus::MessageWriter outer_variant_writer(nullptr);
  writer.OpenVariant("v", &outer_variant_writer);
  dbus::MessageWriter inner_variant_writer(nullptr);
  outer_variant_writer.OpenVariant("(ddd)", &inner_variant_writer);
  dbus::MessageWriter struct1_writer(nullptr);
  inner_variant_writer.OpenStruct(&struct1_writer);
  struct1_writer.AppendDouble(0.0);
  struct1_writer.AppendDouble(0.5);
  struct1_writer.AppendDouble(1.0);
  inner_variant_writer.CloseContainer(&struct1_writer);
  outer_variant_writer.CloseContainer(&inner_variant_writer);
  writer.CloseContainer(&outer_variant_writer);
  static constexpr std::optional<SkColor> kExpectedColor1 =
      SkColorSetRGB(0, 127, 255);
  EXPECT_CALL(*mock_linux_ui(), SetAccentColor(kExpectedColor1));
  std::move(accent_color_callback()).Run(response.get(), nullptr);
  auto* const native_theme = ui::NativeTheme::GetInstanceForNativeUi();
  EXPECT_EQ(native_theme->user_color(), kExpectedColor1);
  Mock::VerifyAndClearExpectations(mock_linux_ui());

  // Changes in the portal accent color should be processed by the manager and
  // the native theme should be updated.
  dbus::Signal signal(DarkModeManagerLinux::kFreedesktopSettingsInterface,
                      DarkModeManagerLinux::kSettingChangedSignal);
  dbus::MessageWriter signal_writer(&signal);
  signal_writer.AppendString(DarkModeManagerLinux::kSettingsNamespace);
  signal_writer.AppendString(DarkModeManagerLinux::kAccentColorKey);
  dbus::MessageWriter variant_writer(nullptr);
  signal_writer.OpenVariant("(ddd)", &variant_writer);
  dbus::MessageWriter struct2_writer(nullptr);
  variant_writer.OpenStruct(&struct2_writer);
  struct2_writer.AppendDouble(1.0);
  struct2_writer.AppendDouble(0.5);
  struct2_writer.AppendDouble(0.0);
  variant_writer.CloseContainer(&struct2_writer);
  signal_writer.CloseContainer(&variant_writer);
  static constexpr std::optional<SkColor> kExpectedColor2 =
      SkColorSetRGB(255, 127, 0);
  EXPECT_CALL(*mock_linux_ui(), SetAccentColor(kExpectedColor2));
  std::move(setting_changed_callback()).Run(&signal);
  EXPECT_EQ(native_theme->user_color(), kExpectedColor2);
}

}  // namespace ui
