// 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 "chromeos/ui/frame/multitask_menu/multitask_menu_nudge_controller.h"

#include "ash/constants/ash_features.h"
#include "ash/display/display_move_window_util.h"
#include "ash/frame/non_client_frame_view_ash.h"
#include "ash/shell.h"
#include "ash/test/ash_test_base.h"
#include "ash/wm/desks/desk.h"
#include "ash/wm/desks/desks_test_util.h"
#include "ash/wm/multitask_menu_nudge_delegate_ash.h"
#include "ash/wm/splitview/split_view_controller.h"
#include "ash/wm/tablet_mode/tablet_mode_controller_test_api.h"
#include "ash/wm/tablet_mode/tablet_mode_multitask_cue.h"
#include "ash/wm/tablet_mode/tablet_mode_multitask_menu_event_handler.h"
#include "ash/wm/tablet_mode/tablet_mode_window_manager.h"
#include "ash/wm/window_state.h"
#include "ash/wm/wm_event.h"
#include "base/test/scoped_feature_list.h"
#include "base/test/simple_test_clock.h"
#include "chromeos/ui/frame/caption_buttons/frame_caption_button_container_view.h"
#include "chromeos/ui/frame/caption_buttons/frame_size_button.h"
#include "chromeos/ui/frame/immersive/immersive_fullscreen_controller.h"
#include "chromeos/ui/frame/immersive/immersive_fullscreen_controller_test_api.h"
#include "chromeos/ui/frame/multitask_menu/multitask_button.h"
#include "chromeos/ui/frame/multitask_menu/multitask_menu.h"
#include "chromeos/ui/wm/features.h"
#include "ui/display/screen.h"
#include "ui/gfx/geometry/size.h"
#include "ui/views/widget/any_widget_observer.h"
#include "ui/wm/core/window_util.h"

namespace ash {

namespace {

// Returns the nudge controller associated with `window`.
chromeos::MultitaskMenuNudgeController* GetNudgeControllerForWindow(
    aura::Window* window) {
  if (Shell::Get()->tablet_mode_controller()->InTabletMode()) {
    return TabletModeControllerTestApi()
        .tablet_mode_window_manager()
        ->tablet_mode_multitask_menu_event_handler()
        ->multitask_cue()
        ->nudge_controller();
  }

  if (auto* frame = NonClientFrameViewAsh::Get(window)) {
    return chromeos::FrameCaptionButtonContainerView::TestApi(
               frame->GetHeaderView()->caption_button_container())
        .nudge_controller();
  }

  return nullptr;
}

}  // namespace

class MultitaskMenuNudgeControllerTest : public AshTestBase {
 public:
  MultitaskMenuNudgeControllerTest()
      : scoped_feature_list_(chromeos::wm::features::kWindowLayoutMenu) {}
  MultitaskMenuNudgeControllerTest(const MultitaskMenuNudgeControllerTest&) =
      delete;
  MultitaskMenuNudgeControllerTest& operator=(
      const MultitaskMenuNudgeControllerTest&) = delete;
  ~MultitaskMenuNudgeControllerTest() override = default;

  views::Widget* GetNudgeWidgetForWindow(aura::Window* window) {
    chromeos::MultitaskMenuNudgeController* controller =
        GetNudgeControllerForWindow(window);
    return controller ? controller->nudge_widget_.get() : nullptr;
  }

  void FireDismissNudgeTimer(aura::Window* window) {
    if (chromeos::MultitaskMenuNudgeController* controller =
            GetNudgeControllerForWindow(window)) {
      controller->clamshell_nudge_dismiss_timer_.FireNow();
    }
  }

  // AshTestBase:
  void SetUp() override {
    AshTestBase::SetUp();

    chromeos::MultitaskMenuNudgeController::SetSuppressNudgeForTesting(false);
    chromeos::MultitaskMenuNudgeController::SetOverrideClockForTesting(
        &test_clock_);

    // Advance the test clock so we aren't at zero time.
    test_clock_.Advance(base::Hours(50));
  }

  void TearDown() override {
    chromeos::MultitaskMenuNudgeController::SetOverrideClockForTesting(nullptr);

    AshTestBase::TearDown();
  }

 protected:
  // Tests that the tablet mode nudge bounds in screen are correct.
  void ExpectCorrectTabletNudgeBounds(aura::Window* window) {
    const gfx::Size size =
        GetNudgeWidgetForWindow(window)->GetContentsView()->GetPreferredSize();
    const auto window_screen_bounds = window->GetBoundsInScreen();
    const int tablet_nudge_y_offset =
        MultitaskMenuNudgeDelegateAsh::kTabletNudgeAdditionalYOffset +
        TabletModeMultitaskCue::kCueHeight +
        TabletModeMultitaskCue::kCueYOffset;
    const gfx::Rect expected_bounds(
        (window_screen_bounds.width() - size.width()) / 2 +
            window_screen_bounds.x(),
        tablet_nudge_y_offset + window_screen_bounds.y(), size.width(),
        size.height());
    EXPECT_EQ(expected_bounds,
              GetNudgeWidgetForWindow(window)->GetWindowBoundsInScreen());
  }

  base::SimpleTestClock test_clock_;

 private:
  base::test::ScopedFeatureList scoped_feature_list_;
};

// Tests that there is no crash after toggling fullscreen on and off. Regression
// test for https://crbug.com/1341142.
TEST_F(MultitaskMenuNudgeControllerTest, NoCrashAfterFullscreening) {
  auto window = CreateAppWindow(gfx::Rect(300, 300));
  ASSERT_TRUE(GetNudgeWidgetForWindow(window.get()));

  // Turn of animations for immersive mode, so we don't have to wait for the top
  // container to hide on fullscreen.
  auto* immersive_controller = chromeos::ImmersiveFullscreenController::Get(
      views::Widget::GetWidgetForNativeView(window.get()));
  chromeos::ImmersiveFullscreenControllerTestApi(immersive_controller)
      .SetupForTest();

  const WMEvent event(WM_EVENT_TOGGLE_FULLSCREEN);
  WindowState::Get(window.get())->OnWMEvent(&event);
  EXPECT_FALSE(GetNudgeWidgetForWindow(window.get()));

  // Window needs to be immersive enabled, but not revealed for the bug to
  // reproduce.
  ASSERT_TRUE(immersive_controller->IsEnabled());
  ASSERT_FALSE(immersive_controller->IsRevealed());

  WindowState::Get(window.get())->OnWMEvent(&event);
  EXPECT_FALSE(GetNudgeWidgetForWindow(window.get()));
}

// Tests that there is no crash after floating a window via the multitask menu.
// Regression test for b/265189622.
TEST_F(MultitaskMenuNudgeControllerTest,
       NoCrashAfterFloatingFromMultitaskMenu) {
  auto window = CreateAppWindow(gfx::Rect(300, 300));
  ASSERT_TRUE(GetNudgeWidgetForWindow(window.get()));

  // Float the window from the multitask menu. Floating the window using the
  // accelerator does not cause the crash mentioned in the bug because the
  // presence of the multitask menu causes an activation change which leads to
  // restacking that does not happen otherwise.
  views::NamedWidgetShownWaiter waiter(
      views::test::AnyWidgetTestPasskey{},
      std::string("MultitaskMenuBubbleWidget"));
  auto* size_button = static_cast<chromeos::FrameSizeButton*>(
      NonClientFrameViewAsh::Get(window.get())
          ->GetHeaderView()
          ->caption_button_container()
          ->size_button());
  size_button->ShowMultitaskMenu(
      chromeos::MultitaskMenuEntryType::kFrameSizeButtonHover);
  views::WidgetDelegate* delegate =
      waiter.WaitIfNeededAndGet()->widget_delegate();
  auto* multitask_menu =
      static_cast<chromeos::MultitaskMenu*>(delegate->AsDialogDelegate());

  // After floating the window from the multitask menu, there is no crash.
  GetEventGenerator()->MoveMouseTo(
      multitask_menu->multitask_menu_view_for_testing()
          ->float_button_for_testing()
          ->GetBoundsInScreen()
          .CenterPoint());
  GetEventGenerator()->ClickLeftButton();
  EXPECT_TRUE(WindowState::Get(window.get())->IsFloated());
}

TEST_F(MultitaskMenuNudgeControllerTest, NudgeTimeout) {
  auto window = CreateAppWindow(gfx::Rect(300, 300));
  ASSERT_TRUE(GetNudgeWidgetForWindow(window.get()));

  FireDismissNudgeTimer(window.get());
  EXPECT_FALSE(GetNudgeWidgetForWindow(window.get()));
}

// Tests that the nudge bounds is within display bounds when the associated
// window is maximized.
TEST_F(MultitaskMenuNudgeControllerTest, ClamshellNudgeBounds) {
  auto window = CreateAppWindow(gfx::Rect(300, 300));
  ASSERT_TRUE(GetNudgeWidgetForWindow(window.get()));

  WindowState::Get(window.get())->Maximize();
  auto* nudge_widget = GetNudgeWidgetForWindow(window.get());
  ASSERT_TRUE(nudge_widget);
  EXPECT_TRUE(display::Screen::GetScreen()
                  ->GetDisplayNearestView(window.get())
                  .work_area()
                  .Contains(nudge_widget->GetWindowBoundsInScreen()));

  // Cleanup some state for the next test.
  FireDismissNudgeTimer(window.get());
  window.reset();
  test_clock_.Advance(base::Hours(26));

  // Test the same thing in RTL.
  base::i18n::SetRTLForTesting(true);
  window = CreateAppWindow(gfx::Rect(300, 300));
  WindowState::Get(window.get())->Maximize();
  nudge_widget = GetNudgeWidgetForWindow(window.get());
  ASSERT_TRUE(nudge_widget);
  EXPECT_TRUE(display::Screen::GetScreen()
                  ->GetDisplayNearestView(window.get())
                  .work_area()
                  .Contains(nudge_widget->GetWindowBoundsInScreen()));
}

TEST_F(MultitaskMenuNudgeControllerTest, NudgeMultiDisplay) {
  UpdateDisplay("800x700,801+0-800x700");
  ASSERT_EQ(2u, Shell::GetAllRootWindows().size());

  auto window = CreateAppWindow(gfx::Rect(300, 300));
  ASSERT_TRUE(GetNudgeWidgetForWindow(window.get()));

  // Move the window using the shortcut. Test that the nudge is on the correct
  // display.
  display_move_window_util::HandleMoveActiveWindowBetweenDisplays();
  EXPECT_EQ(Shell::GetAllRootWindows()[1], GetNudgeWidgetForWindow(window.get())
                                               ->GetNativeWindow()
                                               ->GetRootWindow());

  // Drag from the caption the window to the other display. The nudge should be
  // gone, but there is no crash.
  display_move_window_util::HandleMoveActiveWindowBetweenDisplays();
  auto* event_generator = GetEventGenerator();
  event_generator->set_current_screen_location(gfx::Point(150, 10));
  event_generator->PressLeftButton();
  event_generator->MoveMouseTo(gfx::Point(1200, 0));
  EXPECT_FALSE(GetNudgeWidgetForWindow(window.get()));
}

// Tests that based on preferences (shown count, and last shown time), the nudge
// may or may not be shown.
TEST_F(MultitaskMenuNudgeControllerTest, NudgePreferences) {
  auto window = CreateAppWindow(gfx::Rect(300, 300));
  ASSERT_TRUE(GetNudgeWidgetForWindow(window.get()));
  FireDismissNudgeTimer(window.get());
  ASSERT_FALSE(GetNudgeWidgetForWindow(window.get()));

  // Create the window. This does not show the nudge as 24 hours have not
  // elapsed since the nudge was shown.
  window.reset();
  window = CreateAppWindow(gfx::Rect(300, 300));
  ASSERT_FALSE(GetNudgeWidgetForWindow(window.get()));

  // Create the window again after waiting 25 hours. The nudge should now show
  // for the second time.
  test_clock_.Advance(base::Hours(25));
  window.reset();
  window = CreateAppWindow(gfx::Rect(300, 300));
  ASSERT_TRUE(GetNudgeWidgetForWindow(window.get()));
  FireDismissNudgeTimer(window.get());
  ASSERT_FALSE(GetNudgeWidgetForWindow(window.get()));

  // Show the nudge for a third time. This will be the last time it is shown.
  test_clock_.Advance(base::Hours(25));
  window.reset();
  window = CreateAppWindow(gfx::Rect(300, 300));
  ASSERT_TRUE(GetNudgeWidgetForWindow(window.get()));
  FireDismissNudgeTimer(window.get());
  ASSERT_FALSE(GetNudgeWidgetForWindow(window.get()));

  // Advance the clock and attempt to show the nudge for a forth time. Verify
  // that it will not show.
  test_clock_.Advance(base::Hours(25));
  window.reset();
  window = CreateAppWindow(gfx::Rect(300, 300));
  EXPECT_FALSE(GetNudgeWidgetForWindow(window.get()));
}

// Tests that after the multitask menu is shown, the nudge does not show
// anymore.
TEST_F(MultitaskMenuNudgeControllerTest, MenuShown) {
  // Create a window, the nudge is shown on new window activation.
  auto window = CreateAppWindow(gfx::Rect(300, 300));
  ASSERT_TRUE(GetNudgeWidgetForWindow(window.get()));

  // When opening the multitask menu, the nudge should dismiss immediately.
  views::NamedWidgetShownWaiter waiter(
      views::test::AnyWidgetTestPasskey{},
      std::string("MultitaskMenuBubbleWidget"));
  auto* size_button = static_cast<chromeos::FrameSizeButton*>(
      NonClientFrameViewAsh::Get(window.get())
          ->GetHeaderView()
          ->caption_button_container()
          ->size_button());
  size_button->ShowMultitaskMenu(
      chromeos::MultitaskMenuEntryType::kFrameSizeButtonHover);
  waiter.WaitIfNeededAndGet();
  EXPECT_FALSE(GetNudgeWidgetForWindow(window.get()));

  // Advance the clock and then destroy the window and create a new window.
  // Test that the nudge does not show up.
  test_clock_.Advance(base::Hours(25));
  window.reset();
  window = CreateAppWindow(gfx::Rect(300, 300));
  EXPECT_FALSE(GetNudgeWidgetForWindow(window.get()));
}

// Tests that the nudge gets properly hidden after switching desks with a
// floated window. Regression test for b/276786909.
TEST_F(MultitaskMenuNudgeControllerTest, FloatedWindowNudge) {
  // Create a new desk.
  NewDesk();
  ASSERT_TRUE(DesksController::Get()->desks()[0]->is_active());

  // Create a floated window, the nudge is shown on new window activation.
  auto window = CreateAppWindow(gfx::Rect(300, 300));
  PressAndReleaseKey(ui::VKEY_F, ui::EF_ALT_DOWN | ui::EF_COMMAND_DOWN);
  ASSERT_TRUE(WindowState::Get(window.get())->IsFloated());
  ASSERT_TRUE(GetNudgeWidgetForWindow(window.get()));

  ActivateDesk(DesksController::Get()->desks()[1].get());
  EXPECT_FALSE(GetNudgeWidgetForWindow(window.get()));
}

// Tests that the nudge works in tablet mode, and that its bounds in screen are
// correct.
TEST_F(MultitaskMenuNudgeControllerTest, TabletNudgeBounds) {
  TabletModeControllerTestApi().EnterTabletMode();

  // The widget should appear the first time a window is activated.
  auto window = CreateAppWindow();
  ASSERT_TRUE(GetNudgeWidgetForWindow(window.get()));

  // Test that the widget is shown at the correct bounds when the window is
  // first created.
  ExpectCorrectTabletNudgeBounds(window.get());

  auto* split_view_controller =
      SplitViewController::Get(Shell::GetPrimaryRootWindow());

  // Tests that the widget is shown at the correct bounds when the window is
  // snapped in the primary position.
  split_view_controller->SnapWindow(
      window.get(), SplitViewController::SnapPosition::kPrimary);
  ASSERT_TRUE(GetNudgeWidgetForWindow(window.get()));
  ExpectCorrectTabletNudgeBounds(window.get());

  // Tests that the widget is shown at the correct bounds when the window is
  // snapped in the secondary position.
  split_view_controller->SnapWindow(
      window.get(), SplitViewController::SnapPosition::kSecondary);
  ASSERT_TRUE(GetNudgeWidgetForWindow(window.get()));
  ExpectCorrectTabletNudgeBounds(window.get());
}

// Tests that if a window gets destroyed while the nduge is showing in tablet
// mode, the nudge disappears and there is no crash.
TEST_F(MultitaskMenuNudgeControllerTest, TabletWindowDestroyedWhileNudgeShown) {
  TabletModeControllerTestApi().EnterTabletMode();

  auto window = CreateAppWindow(gfx::Rect(300, 300));
  ASSERT_TRUE(GetNudgeWidgetForWindow(window.get()));

  window.reset();
  EXPECT_FALSE(GetNudgeWidgetForWindow(window.get()));
}

}  // namespace ash