// Copyright 2022 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/system/brightness/unified_brightness_view.h"

#include <optional>

#include "ash/strings/grit/ash_strings.h"
#include "ash/system/brightness/unified_brightness_slider_controller.h"
#include "ash/system/unified/unified_system_tray.h"
#include "ash/system/unified/unified_system_tray_bubble.h"
#include "ash/system/unified/unified_system_tray_controller.h"
#include "ash/system/unified/unified_system_tray_model.h"
#include "ash/test/ash_test_base.h"
#include "ash/wm/window_state.h"
#include "ash/wm/window_util.h"
#include "base/memory/raw_ptr.h"
#include "base/run_loop.h"
#include "chromeos/dbus/power/fake_power_manager_client.h"
#include "chromeos/dbus/power/power_manager_client.h"
#include "ui/base/l10n/l10n_util.h"
#include "ui/display/manager/display_manager.h"
#include "ui/display/test/display_manager_test_api.h"
#include "ui/display/util/display_util.h"
#include "ui/gfx/vector_icon_types.h"
#include "ui/views/accessibility/view_accessibility.h"
#include "ui/views/controls/image_view.h"
#include "ui/views/controls/slider.h"

namespace ash {

namespace {

constexpr float kMinBrightnessLevel = 0.05;

class ScreenBrightnessChangedWaiter
    : public chromeos::PowerManagerClient::Observer {
 public:
  ScreenBrightnessChangedWaiter() {
    chromeos::PowerManagerClient::Get()->AddObserver(this);
  }
  ScreenBrightnessChangedWaiter(const ScreenBrightnessChangedWaiter&) = delete;
  ScreenBrightnessChangedWaiter& operator=(
      const ScreenBrightnessChangedWaiter&) = delete;
  ~ScreenBrightnessChangedWaiter() override {
    chromeos::PowerManagerClient::Get()->RemoveObserver(this);
  }

  void Wait() { run_loop_.Run(); }

 private:
  void ScreenBrightnessChanged(
      const power_manager::BacklightBrightnessChange& change) override {
    run_loop_.Quit();
  }

  base::RunLoop run_loop_;
};

}  // namespace

class UnifiedBrightnessViewTest : public AshTestBase {
 public:
  UnifiedBrightnessViewTest()
      : AshTestBase(base::test::TaskEnvironment::TimeSource::MOCK_TIME) {}
  UnifiedBrightnessViewTest(const UnifiedBrightnessViewTest&) = delete;
  UnifiedBrightnessViewTest& operator=(const UnifiedBrightnessViewTest&) =
      delete;
  ~UnifiedBrightnessViewTest() override = default;

  void SetUp() override {
    AshTestBase::SetUp();
    GetPrimaryUnifiedSystemTray()->ShowBubble();
    brightness_slider_controller_ =
        controller()->brightness_slider_controller_.get();
    unified_brightness_view_ = static_cast<UnifiedBrightnessView*>(
        controller()->unified_brightness_view_);
    brightness_slider_ =
        brightness_slider_controller_->CreateBrightnessSlider();
  }

  void TearDown() override {
    // Tests the environment tears down with the bubble closed.
    // In `UnifiedVolumeViewTest`, the environment is torn down with the bubble
    // open, so we can test both cases.
    CloseBubble();
    AshTestBase::TearDown();
  }

  void CloseBubble() {
    brightness_slider_ = nullptr;
    unified_brightness_view_ = nullptr;
    brightness_slider_controller_ = nullptr;
    GetPrimaryUnifiedSystemTray()->CloseBubble();
  }

  void WaitUntilUpdated(ScreenBrightnessChangedWaiter& waiter,
                        float expected_level) {
    waiter.Wait();
    EXPECT_FLOAT_EQ(expected_level, slider()->GetValue());
  }

  UnifiedBrightnessSliderController* brightness_slider_controller() {
    return brightness_slider_controller_;
  }

  UnifiedBrightnessView* unified_brightness_view() {
    return unified_brightness_view_;
  }

  UnifiedBrightnessView* brightness_slider() {
    return brightness_slider_.get();
  }

  views::Slider* slider() { return unified_brightness_view_->slider(); }

  views::Button* more_button() {
    return static_cast<UnifiedBrightnessView*>(
               controller()->unified_brightness_view_)
        ->more_button();
  }

  views::Button* night_light_button() {
    return static_cast<UnifiedBrightnessView*>(
               controller()->unified_brightness_view_)
        ->night_light_button();
  }

  const gfx::VectorIcon& GetIcon(float level) {
    return unified_brightness_view_->GetBrightnessIconForLevel(level);
  }

  UnifiedSystemTrayController* controller() {
    return GetPrimaryUnifiedSystemTray()
        ->bubble()
        ->unified_system_tray_controller();
  }

 private:
  // The `UnifiedBrightnessView` containing a `QuickSettingsSlider`, a
  // `NightLight` button, and a drill-in button.
  raw_ptr<UnifiedBrightnessView> unified_brightness_view_ = nullptr;

  // The `UnifiedBrightnessView` containing only a `QuickSettingsSlider`.
  std::unique_ptr<UnifiedBrightnessView> brightness_slider_ = nullptr;

  raw_ptr<UnifiedBrightnessSliderController> brightness_slider_controller_ =
      nullptr;
};

// Tests to ensure that the `slider_button` does not handle any events,
// letting them get through to the slider. Effectively the `slider_button` is
// part of the slider in the brightness view.
TEST_F(UnifiedBrightnessViewTest, SliderButtonClickThrough) {
  display::test::DisplayManagerTestApi(display_manager())
      .SetFirstDisplayAsInternalDisplay();
  unified_brightness_view()->UpdateBrightnessSlider();
  slider()->SetValue(1.0);
  EXPECT_FLOAT_EQ(unified_brightness_view()->slider()->GetValue(), 1.0);

  // A click on the `slider_button` for `unified_brightness_view()` should go
  // through to the slider and change the value to the minimum.
  LeftClickOn(unified_brightness_view()->slider_button());
  EXPECT_FLOAT_EQ(unified_brightness_view()->slider()->GetValue(), 0.0);
}

// Tests that `UnifiedBrightnessView` is made up of a `QuickSettingsSlider`, a
// `NightLight` button, and a drill-in button that leads to the display subpage.
TEST_F(UnifiedBrightnessViewTest, SliderButtonComponents) {
  EXPECT_EQ(unified_brightness_view()->children().size(), 3u);
  EXPECT_EQ(unified_brightness_view()->children()[0]->GetClassName(),
            "QuickSettingsSlider");

  auto* night_light_button =
      static_cast<IconButton*>(unified_brightness_view()->children()[1]);
  EXPECT_EQ(night_light_button->GetClassName(), "IconButton");
  EXPECT_EQ(night_light_button->GetViewAccessibility().GetCachedName(),
            l10n_util::GetStringFUTF16(
                IDS_ASH_STATUS_TRAY_NIGHT_LIGHT_TOGGLE_TOOLTIP,
                l10n_util::GetStringUTF16(
                    IDS_ASH_STATUS_TRAY_NIGHT_LIGHT_DISABLED_STATE_TOOLTIP)));
  EXPECT_EQ(night_light_button->GetTooltipText(),
            u"Toggle Night Light. Night Light is off.");

  auto* display_subpage_drill_in_button =
      static_cast<IconButton*>(unified_brightness_view()->children()[2]);
  EXPECT_EQ(display_subpage_drill_in_button->GetClassName(), "IconButton");
  EXPECT_EQ(
      display_subpage_drill_in_button->GetViewAccessibility().GetCachedName(),
      l10n_util::GetStringUTF16(
          IDS_ASH_STATUS_TRAY_NIGHT_LIGHT_SETTINGS_TOOLTIP));
  EXPECT_EQ(display_subpage_drill_in_button->GetTooltipText(),
            u"Show display settings");

  // Clicks on the drill-in button and checks `DisplayDetailedView` is shown.
  EXPECT_FALSE(controller()->IsDetailedViewShown());
  LeftClickOn(unified_brightness_view()->children()[2]);
  EXPECT_TRUE(controller()->showing_display_detailed_view());
}

// Tests that `UnifiedBrightnessView` in the display subpage is made up of a
// `QuickSettingsSlider`.
TEST_F(UnifiedBrightnessViewTest, SliderComponent) {
  EXPECT_EQ(brightness_slider()->children().size(), 1u);
  EXPECT_EQ(brightness_slider()->children()[0]->GetClassName(),
            "QuickSettingsSlider");
}

// Tests the slider icon matches the slider level.
TEST_F(UnifiedBrightnessViewTest, SliderIcon) {
  const float levels[] = {0.0, 0.04, 0.2, 0.25, 0.49, 0.5, 0.7, 0.75, 0.9, 1};

  float previous_level = 1.0f;
  for (auto level : levels) {
    // `SliderValueChanged()` skips power-manager updates while both levels are
    // below the enforced minimum brightness.
    const bool brightness_update_expected =
        level >= kMinBrightnessLevel || previous_level >= kMinBrightnessLevel;
    std::optional<ScreenBrightnessChangedWaiter> waiter;
    if (brightness_update_expected) {
      waiter.emplace();
    }

    // Sets the slider value. `SetValue()` will pass in
    // `SliderChangeReason::kByApi` as the slider change reason and will not
    // trigger UI updates, so we should call `SliderValueChanged()` afterwards
    // to update the icon.
    slider()->SetValue(level);
    brightness_slider_controller()->SliderValueChanged(
        slider(), level, slider()->GetValue(),
        views::SliderChangeReason::kByUser);

    const float expected_level = std::max(level, kMinBrightnessLevel);
    if (brightness_update_expected) {
      WaitUntilUpdated(*waiter, expected_level);
    }
    previous_level = level;

    // The minimum level for brightness is 0.05, since `SliderValueChanged()`
    // will adjust the brightness level and set the icon accordingly.
    const gfx::VectorIcon& icon = GetIcon(expected_level);

    if (level <= 0.0) {
      EXPECT_EQ(icon.name,
                UnifiedBrightnessView::kBrightnessLevelIcons[1]->name);
    } else if (level <= 0.5) {
      EXPECT_EQ(icon.name,
                UnifiedBrightnessView::kBrightnessLevelIcons[1]->name);
    } else {
      EXPECT_EQ(icon.name,
                UnifiedBrightnessView::kBrightnessLevelIcons[2]->name);
    }
  }
}

// Tests that the `UnifiedBrightnessView` `more_button` is not enabled if and
// only if there is a trusted pinned window.
TEST_F(UnifiedBrightnessViewTest, MoreButton) {
  // At the start of the test, the system tray containing the brightness view is
  // already shown. Since there is no pinned window, the `more_button` should
  // not be disabled.
  EXPECT_TRUE(more_button()->GetEnabled());

  // Close the bubble so the brightness view can be recreated.
  CloseBubble();

  // Create and trusted pin a window.
  std::unique_ptr<aura::Window> window = CreateWindowWithAppType();
  wm::ActivateWindow(window.get());
  window_util::PinWindow(window.get(), /*trusted=*/true);

  // Open the bubble and check that the new brightness view more button is in
  // the correct state.
  GetPrimaryUnifiedSystemTray()->ShowBubble();
  EXPECT_FALSE(more_button()->GetEnabled());

  // Close the bubble so the brightness view can be recreated.
  CloseBubble();

  // Unpin the window
  WindowState::Get(window.get())->Restore();

  // Make sure the more button is not disabled.
  GetPrimaryUnifiedSystemTray()->ShowBubble();
  EXPECT_TRUE(more_button()->GetEnabled());
}

// Tests that the night light button is disabled in the sign-in screen, and is
// enabled in the locked screen.
TEST_F(UnifiedBrightnessViewTest, NightLightButtonState) {
  // Close the bubble so the brightness view can be recreated.
  CloseBubble();

  // In the sign-in screen, the `night_light_button_` is disabled.
  GetSessionControllerClient()->SetSessionState(
      session_manager::SessionState::LOGIN_PRIMARY);
  GetPrimaryUnifiedSystemTray()->ShowBubble();
  EXPECT_FALSE(night_light_button()->GetEnabled());

  CloseBubble();
  // In the locked screen, the `night_light_button_` is enabled.
  GetSessionControllerClient()->SetSessionState(
      session_manager::SessionState::LOCKED);
  GetPrimaryUnifiedSystemTray()->ShowBubble();
  EXPECT_TRUE(night_light_button()->GetEnabled());

  CloseBubble();
  // In the active user session, the `night_light_button_` is enabled.
  GetSessionControllerClient()->SetSessionState(
      session_manager::SessionState::ACTIVE);
  GetPrimaryUnifiedSystemTray()->ShowBubble();
  EXPECT_TRUE(night_light_button()->GetEnabled());
}

TEST_F(UnifiedBrightnessViewTest,
       BrightnessSliderDisabledInDockedModeLidClosed) {
  // Scenario 1: Only external display -> slider disabled.
  ASSERT_EQ(1u, display_manager()->GetNumDisplays());

  auto* tray = GetPrimaryUnifiedSystemTray();
  tray->ShowBubble();

  EXPECT_FALSE(slider()->GetEnabled());

  // Scenario 2: Internal display only + lid open -> slider enabled.
  const int64_t internal_display_id =
      display::test::DisplayManagerTestApi(display_manager())
          .SetFirstDisplayAsInternalDisplay();
  const auto internal_info =
      display_manager()->GetDisplayInfo(internal_display_id);

  chromeos::FakePowerManagerClient::Get()->SetLidState(
      chromeos::PowerManagerClient::LidState::OPEN, {});

  EXPECT_EQ(1u, display_manager()->GetNumDisplays());
  tray->ShowBubble();

  EXPECT_TRUE(slider()->GetEnabled());

  // Scenario 3: Internal + External + lid open -> slider enabled.
  constexpr int64_t external_id = 210000010;
  const auto external_info =
      display::ManagedDisplayInfo::CreateFromSpecWithID("400x300", external_id);

  std::vector<display::ManagedDisplayInfo> display_info_list;
  display_info_list.push_back(internal_info);
  display_info_list.push_back(external_info);
  display_manager()->OnNativeDisplaysChanged(display_info_list);

  EXPECT_EQ(2U, display_manager()->GetNumDisplays());
  EXPECT_TRUE(slider()->GetEnabled());

  // Scenario 4: Docked mode, external only, lid open -> slider enabled.
  power_manager::SetBacklightBrightnessRequest request;
  request.set_percent(0);
  chromeos::FakePowerManagerClient::Get()->SetScreenBrightness(request);
  display_info_list.clear();
  display_info_list.push_back(external_info);
  display_manager()->OnNativeDisplaysChanged(display_info_list);

  EXPECT_EQ(1u, display_manager()->GetNumDisplays());
  EXPECT_TRUE(slider()->GetEnabled());

  // Scenario 5: Docked mode, external only, lid closed -> slider disabled.
  // Close the lid.
  chromeos::FakePowerManagerClient::Get()->SetLidState(
      chromeos::PowerManagerClient::LidState::CLOSED, {});

  EXPECT_FALSE(slider()->GetEnabled());
}

}  // namespace ash
