chromium/components/exo/wayland/zaura_shell_unittest.cc

// Copyright 2019 The Chromium Authors
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.

#include "components/exo/wayland/zaura_shell.h"

#include <aura-shell-server-protocol.h>

#include <sys/socket.h>
#include <memory>
#include <vector>

#include "ash/session/session_controller_impl.h"
#include "ash/shelf/shelf.h"
#include "ash/shell.h"
#include "ash/wm/desks/desks_util.h"
#include "ash/wm/window_util.h"
#include "base/memory/raw_ptr.h"
#include "base/time/time.h"
#include "components/exo/buffer.h"
#include "components/exo/shell_surface.h"
#include "components/exo/shell_surface_util.h"
#include "components/exo/test/exo_test_base.h"
#include "components/exo/test/shell_surface_builder.h"
#include "components/exo/wayland/scoped_wl.h"
#include "components/exo/wayland/wayland_display_observer.h"
#include "components/exo/wayland/wayland_display_output.h"
#include "components/exo/wayland/wayland_display_util.h"
#include "components/exo/wayland/wl_output.h"
#include "testing/gmock/include/gmock/gmock.h"
#include "testing/gtest/include/gtest/gtest.h"
#include "ui/aura/window_occlusion_tracker.h"
#include "ui/compositor/layer.h"
#include "ui/compositor/layer_animation_observer.h"
#include "ui/compositor/layer_animation_sequence.h"
#include "ui/compositor/layer_animator.h"
#include "ui/compositor/scoped_animation_duration_scale_mode.h"
#include "ui/compositor/scoped_layer_animation_settings.h"
#include "ui/compositor/test/layer_animator_test_controller.h"
#include "ui/display/screen.h"
#include "ui/events/base_event_utils.h"
#include "ui/events/event.h"
#include "ui/events/test/event_generator.h"
#include "ui/gfx/geometry/size_f.h"
#include "ui/views/corewm/tooltip_aura.h"
#include "ui/views/widget/widget.h"
#include "ui/wm/core/window_util.h"
#include "ui/wm/public/activation_change_observer.h"
#include "ui/wm/public/activation_client.h"

namespace exo {
namespace wayland {

namespace {

constexpr auto kTransitionDuration = base::Seconds(3);
constexpr int kTooltipExpectedHeight = 28;

class TestAuraSurface : public AuraSurface {
 public:
  explicit TestAuraSurface(Surface* surface)
      : AuraSurface(surface, /*resource=*/nullptr) {}

  TestAuraSurface(const TestAuraSurface&) = delete;
  TestAuraSurface& operator=(const TestAuraSurface&) = delete;

  float last_sent_occlusion_fraction() const {
    return last_sent_occlusion_fraction_;
  }
  aura::Window::OcclusionState last_sent_occlusion_state() const {
    return last_sent_occlusion_state_;
  }
  int num_occlusion_updates() const { return num_occlusion_updates_; }
  bool send_occlusion_state_called() const {
    return send_occlusion_state_called_;
  }

  MOCK_METHOD(void,
              OnTooltipShown,
              (Surface * surface,
               const std::u16string& text,
               const gfx::Rect& bounds),
              (override));
  MOCK_METHOD(void, OnTooltipHidden, (Surface * surface), (override));

 protected:
  void SendOcclusionFraction(float occlusion_fraction) override {
    last_sent_occlusion_fraction_ = occlusion_fraction;
    num_occlusion_updates_++;
  }

  void SendOcclusionState(
      const aura::Window::OcclusionState occlusion_state) override {
    last_sent_occlusion_state_ = occlusion_state;
    send_occlusion_state_called_ = true;
  }

 private:
  float last_sent_occlusion_fraction_ = -1.0f;
  aura::Window::OcclusionState last_sent_occlusion_state_ =
      aura::Window::OcclusionState::UNKNOWN;
  int num_occlusion_updates_ = 0;
  bool send_occlusion_state_called_ = false;
};

class MockSurfaceDelegate : public SurfaceDelegate {
 public:
  MOCK_METHOD(void, OnSurfaceCommit, (), (override));
  MOCK_METHOD(bool, IsSurfaceSynchronized, (), (const, override));
  MOCK_METHOD(bool, IsInputEnabled, (Surface * surface), (const, override));
  MOCK_METHOD(void, OnSetFrame, (SurfaceFrameType type), (override));
  MOCK_METHOD(void,
              OnSetFrameColors,
              (SkColor active_color, SkColor inactive_color),
              (override));
  MOCK_METHOD(void,
              OnSetParent,
              (Surface * parent, const gfx::Point& position),
              (override));
  MOCK_METHOD(void, OnSetStartupId, (const char* startup_id), (override));
  MOCK_METHOD(void,
              OnSetApplicationId,
              (const char* application_id),
              (override));
  MOCK_METHOD(void, SetUseImmersiveForFullscreen, (bool value), (override));
  MOCK_METHOD(void, OnActivationRequested, (), (override));
  MOCK_METHOD(void, OnNewOutputAdded, (), (override));
  MOCK_METHOD(void, OnSetServerStartResize, (), (override));
  MOCK_METHOD(void, ShowSnapPreviewToPrimary, (), (override));
  MOCK_METHOD(void, ShowSnapPreviewToSecondary, (), (override));
  MOCK_METHOD(void, HideSnapPreview, (), (override));
  MOCK_METHOD(void, SetSnapPrimary, (float snap_ratio), (override));
  MOCK_METHOD(void, SetSnapSecondary, (float snap_ratio), (override));
  MOCK_METHOD(void, UnsetSnap, (), (override));
  MOCK_METHOD(void, SetCanGoBack, (), (override));
  MOCK_METHOD(void, UnsetCanGoBack, (), (override));
  MOCK_METHOD(void, SetPip, (), (override));
  MOCK_METHOD(void, UnsetPip, (), (override));
  MOCK_METHOD(void,
              SetFloatToLocation,
              (chromeos::FloatStartLocation),
              (override));
  MOCK_METHOD(void,
              SetAspectRatio,
              (const gfx::SizeF& aspect_ratio),
              (override));
  MOCK_METHOD(void, MoveToDesk, (int desk_index), (override));
  MOCK_METHOD(void, SetVisibleOnAllWorkspaces, (), (override));
  MOCK_METHOD(void,
              SetInitialWorkspace,
              (const char* initial_workspace),
              (override));
  MOCK_METHOD(void, Pin, (bool trusted), (override));
  MOCK_METHOD(void, Unpin, (), (override));
  MOCK_METHOD(void, SetSystemModal, (bool modal), (override));
  MOCK_METHOD(void, SetTopInset, (int height), (override));
  MOCK_METHOD(SecurityDelegate*, GetSecurityDelegate, (), (override));
};

}  // namespace

class ZAuraSurfaceTest : public test::ExoTestBase,
                         public ::wm::ActivationChangeObserver {
 public:
  ZAuraSurfaceTest() {}

  ZAuraSurfaceTest(const ZAuraSurfaceTest&) = delete;
  ZAuraSurfaceTest& operator=(const ZAuraSurfaceTest&) = delete;

  ~ZAuraSurfaceTest() override {}

  // test::ExoTestBase overrides:
  void SetUp() override {
    test::ExoTestBase::SetUp();

    gfx::Size buffer_size(10, 10);
    auto buffer = test::ExoTestHelper::CreateBuffer(buffer_size);

    surface_ = std::make_unique<Surface>();
    surface_->Attach(buffer.get());

    aura_surface_ = std::make_unique<TestAuraSurface>(surface_.get());

    gfx::Transform transform;
    transform.Scale(1.5f, 1.5f);
    parent_widget_ =
        CreateTestWidget(views::Widget::InitParams::WIDGET_OWNS_NATIVE_WIDGET);
    parent_widget_->SetBounds(gfx::Rect(0, 0, 10, 10));
    parent_widget_->GetNativeWindow()->SetTransform(transform);
    parent_widget_->GetNativeWindow()->AddChild(surface_->window());
    parent_widget_->Show();
    surface_->window()->SetBounds(gfx::Rect(5, 5, 10, 10));
    surface_->window()->Show();

    ash::Shell::Get()->activation_client()->AddObserver(this);
    aura_surface_->SetOcclusionTracking(true);
  }

  void TearDown() override {
    ash::Shell::Get()->activation_client()->RemoveObserver(this);

    parent_widget_.reset();
    aura_surface_.reset();
    surface_.reset();

    test::ExoTestBase::TearDown();
  }

  // ::wm::ActivationChangeObserver overrides:
  void OnWindowActivated(ActivationReason reason,
                         aura::Window* gained_active,
                         aura::Window* lost_active) override {
    if (lost_active == parent_widget_->GetNativeWindow()) {
      occlusion_fraction_on_activation_loss_ =
          aura_surface().last_sent_occlusion_fraction();
    }
  }

 protected:
  TestAuraSurface& aura_surface() { return *aura_surface_; }
  Surface& surface() { return *surface_; }
  views::Widget& parent_widget() { return *parent_widget_; }
  float occlusion_fraction_on_activation_loss() const {
    return occlusion_fraction_on_activation_loss_;
  }

  std::unique_ptr<views::Widget> CreateOpaqueWidget(const gfx::Rect& bounds) {
    return CreateTestWidget(
        views::Widget::InitParams::WIDGET_OWNS_NATIVE_WIDGET,
        /*delegate=*/nullptr,
        /*container_id=*/ash::desks_util::GetActiveDeskContainerId(), bounds,
        /*show=*/false);
  }

 private:
  std::unique_ptr<TestAuraSurface> aura_surface_;
  std::unique_ptr<Surface> surface_;
  std::unique_ptr<views::Widget> parent_widget_;
  float occlusion_fraction_on_activation_loss_ = -1.0f;
};

TEST_F(ZAuraSurfaceTest, OcclusionTrackingStartsAfterCommit) {
  surface().OnWindowOcclusionChanged(aura::Window::OcclusionState::UNKNOWN,
                                     aura::Window::OcclusionState::UNKNOWN);

  EXPECT_EQ(-1.0f, aura_surface().last_sent_occlusion_fraction());
  EXPECT_EQ(aura::Window::OcclusionState::UNKNOWN,
            aura_surface().last_sent_occlusion_state());
  EXPECT_EQ(0, aura_surface().num_occlusion_updates());
  EXPECT_FALSE(surface().IsTrackingOcclusion());

  auto widget = CreateOpaqueWidget(gfx::Rect(0, 0, 10, 10));
  widget->Show();
  surface().Commit();

  EXPECT_EQ(0.2f, aura_surface().last_sent_occlusion_fraction());
  EXPECT_EQ(aura::Window::OcclusionState::VISIBLE,
            aura_surface().last_sent_occlusion_state());
  EXPECT_EQ(1, aura_surface().num_occlusion_updates());
  EXPECT_TRUE(surface().IsTrackingOcclusion());
}

TEST_F(ZAuraSurfaceTest,
       LosingActivationWithNoAnimatingWindowsSendsCorrectOcclusionFraction) {
  surface().Commit();
  EXPECT_EQ(0.0f, aura_surface().last_sent_occlusion_fraction());
  EXPECT_EQ(aura::Window::OcclusionState::VISIBLE,
            aura_surface().last_sent_occlusion_state());
  EXPECT_EQ(1, aura_surface().num_occlusion_updates());
  ::wm::ActivateWindow(parent_widget().GetNativeWindow());

  // Creating an opaque window but don't show it.
  auto widget = CreateOpaqueWidget(gfx::Rect(0, 0, 10, 10));

  // Occlusion sent before de-activation should include that widget.
  widget->Show();
  EXPECT_EQ(0.2f, occlusion_fraction_on_activation_loss());
  EXPECT_EQ(0.2f, aura_surface().last_sent_occlusion_fraction());
  EXPECT_EQ(aura::Window::OcclusionState::VISIBLE,
            aura_surface().last_sent_occlusion_state());
  EXPECT_EQ(2, aura_surface().num_occlusion_updates());
}

TEST_F(ZAuraSurfaceTest,
       LosingActivationWithAnimatingWindowsSendsTargetOcclusionFraction) {
  surface().Commit();
  EXPECT_EQ(0.0f, aura_surface().last_sent_occlusion_fraction());
  EXPECT_EQ(aura::Window::OcclusionState::VISIBLE,
            aura_surface().last_sent_occlusion_state());
  EXPECT_EQ(1, aura_surface().num_occlusion_updates());
  ::wm::ActivateWindow(parent_widget().GetNativeWindow());

  // Creating an opaque window but don't show it.
  auto widget = CreateOpaqueWidget(gfx::Rect(0, 0, 10, 10));
  widget->GetNativeWindow()->layer()->SetOpacity(0.0f);

  ui::ScopedAnimationDurationScaleMode scoped_animation_duration_scale_mode(
      ui::ScopedAnimationDurationScaleMode::NORMAL_DURATION);
  ui::LayerAnimatorTestController test_controller(
      ui::LayerAnimator::CreateImplicitAnimator());
  ui::ScopedLayerAnimationSettings layer_animation_settings(
      test_controller.animator());
  layer_animation_settings.SetTransitionDuration(kTransitionDuration);
  widget->GetNativeWindow()->layer()->SetAnimator(test_controller.animator());
  widget->GetNativeWindow()->layer()->SetOpacity(1.0f);

  // Opacity animation uses threaded animation.
  test_controller.StartThreadedAnimationsIfNeeded();
  test_controller.Step(kTransitionDuration / 3);

  // No occlusion updates should happen until the window is de-activated.
  EXPECT_EQ(1, aura_surface().num_occlusion_updates());

  // Occlusion sent before de-activation should include the window animating
  // to be completely opaque.
  widget->Show();
  EXPECT_EQ(0.2f, occlusion_fraction_on_activation_loss());
  EXPECT_EQ(0.2f, aura_surface().last_sent_occlusion_fraction());
  EXPECT_EQ(aura::Window::OcclusionState::VISIBLE,
            aura_surface().last_sent_occlusion_state());
  EXPECT_EQ(2, aura_surface().num_occlusion_updates());

  // Explicitly stop animation because threaded animation may have started
  // a bit later. |kTransitionDuration| may not be quite enough to reach the
  // end.
  test_controller.Step(kTransitionDuration / 3);
  test_controller.Step(kTransitionDuration / 3);
  widget->GetNativeWindow()->layer()->GetAnimator()->StopAnimating();
  widget->GetNativeWindow()->layer()->SetAnimator(nullptr);

  // Expect the occlusion tracker to send an update after the animation
  // finishes.
  EXPECT_EQ(0.2f, aura_surface().last_sent_occlusion_fraction());
  EXPECT_EQ(aura::Window::OcclusionState::VISIBLE,
            aura_surface().last_sent_occlusion_state());
  EXPECT_EQ(3, aura_surface().num_occlusion_updates());
}

TEST_F(ZAuraSurfaceTest,
       LosingActivationByTriggeringTheLockScreenDoesNotSendOccludedFraction) {
  surface().Commit();
  EXPECT_EQ(0.0f, aura_surface().last_sent_occlusion_fraction());
  EXPECT_EQ(aura::Window::OcclusionState::VISIBLE,
            aura_surface().last_sent_occlusion_state());
  EXPECT_EQ(1, aura_surface().num_occlusion_updates());
  ::wm::ActivateWindow(parent_widget().GetNativeWindow());

  // Lock the screen.
  views::Widget::InitParams params(
      views::Widget::InitParams::WIDGET_OWNS_NATIVE_WIDGET,
      views::Widget::InitParams::TYPE_WINDOW);
  auto lock_widget = std::make_unique<views::Widget>();
  params.context = GetContext();
  params.bounds = gfx::Rect(0, 0, 100, 100);
  lock_widget->Init(std::move(params));
  ash::Shell::GetContainer(ash::Shell::GetPrimaryRootWindow(),
                           ash::kShellWindowId_LockScreenContainer)
      ->AddChild(lock_widget->GetNativeView());

  // Simulate real screen locker to change session state to LOCKED
  // when it is shown.
  auto* controller = ash::Shell::Get()->session_controller();
  GetSessionControllerClient()->LockScreen();
  lock_widget->Show();
  EXPECT_TRUE(controller->IsScreenLocked());
  EXPECT_TRUE(lock_widget->GetNativeView()->HasFocus());

  // We should have lost focus, but not reported that the window has been
  // fully occluded.
  EXPECT_NE(parent_widget().GetNativeWindow(),
            ash::window_util::GetActiveWindow());
  EXPECT_EQ(0.0f, occlusion_fraction_on_activation_loss());
  EXPECT_EQ(0.0f, aura_surface().last_sent_occlusion_fraction());
  EXPECT_EQ(aura::Window::OcclusionState::VISIBLE,
            aura_surface().last_sent_occlusion_state());
}

TEST_F(ZAuraSurfaceTest, OcclusionIncludesOffScreenArea) {
  UpdateDisplay("200x150");

  gfx::Size buffer_size(80, 100);
  auto buffer = test::ExoTestHelper::CreateBuffer(buffer_size);
  // This is scaled by 1.5 - set the bounds to (-60, 75, 120, 150) in screen
  // coordinates so 75% of it is outside of the screen.
  surface().window()->SetBounds(gfx::Rect(-40, 50, 80, 100));
  surface().Attach(buffer.get());
  surface().Commit();

  ash::Shelf::ForWindow(surface().window())
      ->SetAutoHideBehavior(ash::ShelfAutoHideBehavior::kAlwaysHidden);

  surface().OnWindowOcclusionChanged(aura::Window::OcclusionState::UNKNOWN,
                                     aura::Window::OcclusionState::VISIBLE);

  EXPECT_EQ(0.75f, aura_surface().last_sent_occlusion_fraction());
  EXPECT_EQ(aura::Window::OcclusionState::VISIBLE,
            aura_surface().last_sent_occlusion_state());
}

TEST_F(ZAuraSurfaceTest, OcclusionFractionDoesNotDoubleCountOutsideOfScreen) {
  UpdateDisplay("600x800");

  // Create a surface which is halfway offscreen.
  gfx::Size buffer1_size(80, 100);
  auto buffer1 = test::ExoTestHelper::CreateBuffer(buffer1_size);
  surface().window()->SetBounds(gfx::Rect(-40, 50, 80, 100));
  surface().Attach(buffer1.get());
  surface().Commit();

  EXPECT_EQ(0.5f, aura_surface().last_sent_occlusion_fraction());
  EXPECT_EQ(aura::Window::OcclusionState::VISIBLE,
            aura_surface().last_sent_occlusion_state());

  // Occlude the previous surface but only offscreen. The occlusion fraction
  // should still be 0.5.
  auto window =
      std::make_unique<aura::Window>(nullptr, aura::client::WINDOW_TYPE_POPUP);
  window->Init(ui::LAYER_SOLID_COLOR);
  window->layer()->SetColor(SK_ColorBLACK);
  window->SetTransparent(false);
  window->SetBounds(gfx::Rect(-60, 75, 60, 150));
  window->Show();
  parent_widget().GetNativeWindow()->parent()->AddChild(window.get());

  surface().OnWindowOcclusionChanged(aura::Window::OcclusionState::UNKNOWN,
                                     aura::Window::OcclusionState::VISIBLE);

  EXPECT_EQ(0.5f, aura_surface().last_sent_occlusion_fraction());
  EXPECT_EQ(aura::Window::OcclusionState::VISIBLE,
            aura_surface().last_sent_occlusion_state());

  // Occlude the previous surface by 25% more additionally inside the screen.
  window->SetBounds(gfx::Rect(-60, 75, 90, 150));

  surface().OnWindowOcclusionChanged(aura::Window::OcclusionState::VISIBLE,
                                     aura::Window::OcclusionState::VISIBLE);

  EXPECT_EQ(0.75f, aura_surface().last_sent_occlusion_fraction());
  EXPECT_EQ(aura::Window::OcclusionState::VISIBLE,
            aura_surface().last_sent_occlusion_state());
}

TEST_F(ZAuraSurfaceTest, ZeroSizeWindowSendsZeroOcclusionFraction) {
  // Zero sized window should not be occluded.
  surface().window()->SetBounds(gfx::Rect(0, 0, 0, 0));
  surface().Commit();
  surface().OnWindowOcclusionChanged(aura::Window::OcclusionState::UNKNOWN,
                                     aura::Window::OcclusionState::VISIBLE);
  EXPECT_EQ(0.0f, aura_surface().last_sent_occlusion_fraction());
  EXPECT_EQ(aura::Window::OcclusionState::VISIBLE,
            aura_surface().last_sent_occlusion_state());
}

TEST_F(ZAuraSurfaceTest, CanPin) {
  MockSurfaceDelegate delegate;
  wl_resource resource;
  resource.data = &aura_surface();
  surface().SetSurfaceDelegate(&delegate);
  EXPECT_CALL(delegate, Pin(true));

  aura_surface().Pin(true);
}

TEST_F(ZAuraSurfaceTest, CanUnpin) {
  MockSurfaceDelegate delegate;
  wl_resource resource;
  resource.data = &aura_surface();
  surface().SetSurfaceDelegate(&delegate);
  EXPECT_CALL(delegate, Unpin());

  aura_surface().Unpin();
}

TEST_F(ZAuraSurfaceTest, CanSetFullscreenModeToPlain) {
  MockSurfaceDelegate delegate;
  wl_resource resource;
  resource.data = &aura_surface();
  surface().SetSurfaceDelegate(&delegate);
  EXPECT_CALL(delegate, SetUseImmersiveForFullscreen(false));

  aura_surface().SetFullscreenMode(ZAURA_SURFACE_FULLSCREEN_MODE_PLAIN);
}

TEST_F(ZAuraSurfaceTest, CanSetFullscreenModeToImmersive) {
  MockSurfaceDelegate delegate;
  surface().SetSurfaceDelegate(&delegate);
  EXPECT_CALL(delegate, SetUseImmersiveForFullscreen(true));

  aura_surface().SetFullscreenMode(ZAURA_SURFACE_FULLSCREEN_MODE_IMMERSIVE);
}

TEST_F(ZAuraSurfaceTest, CanSetAccessibilityId) {
  aura_surface().SetAccessibilityId(123);

  EXPECT_EQ(123, exo::GetShellClientAccessibilityId(surface().window()));
}

TEST_F(ZAuraSurfaceTest, CanUnsetAccessibilityId) {
  aura_surface().SetAccessibilityId(-1);

  EXPECT_FALSE(
      exo::GetShellClientAccessibilityId(surface().window()).has_value());
}

using ZAuraSurfaceOcclusionTest = test::ExoTestBase;

TEST_F(ZAuraSurfaceOcclusionTest, SkipFirstHidden) {
  Surface surface;
  TestAuraSurface aura_surface(&surface);

  surface.SetOcclusionTracking(true);
  surface.Commit();
  EXPECT_TRUE(surface.IsTrackingOcclusion());

  // Skip sending occlusion state change if its from UNKNOWN to HIDDEN because
  // the first state is calculated without a buffer attached to the surface.
  surface.OnWindowOcclusionChanged(aura::Window::OcclusionState::UNKNOWN,
                                   aura::Window::OcclusionState::HIDDEN);
  EXPECT_FALSE(aura_surface.send_occlusion_state_called());

  surface.OnWindowOcclusionChanged(aura::Window::OcclusionState::UNKNOWN,
                                   aura::Window::OcclusionState::VISIBLE);
  EXPECT_TRUE(aura_surface.send_occlusion_state_called());
}

// Test without setting surfaces on SetUp().
using ZAuraSurfaceCustomTest = test::ExoTestBase;

class MockSurfaceObserver : public SurfaceObserver {
 public:
  MockSurfaceObserver() = default;
  MockSurfaceObserver(const MockSurfaceObserver&) = delete;
  MockSurfaceObserver& operator=(const MockSurfaceObserver&) = delete;
  ~MockSurfaceObserver() override = default;

  MOCK_METHOD(void, OnSurfaceDestroying, (Surface * surface), (override));
  MOCK_METHOD(void,
              OnTooltipShown,
              (Surface * surface,
               const std::u16string& text,
               const gfx::Rect& bounds),
              (override));
  MOCK_METHOD(void, OnTooltipHidden, (Surface * surface), (override));
};

TEST_F(ZAuraSurfaceCustomTest, ShowTooltipFromCursor) {
  std::unique_ptr<ShellSurface> shell_surface =
      test::ShellSurfaceBuilder({10, 10}).BuildShellSurface();

  Surface* surface = shell_surface->root_surface();
  auto aura_surface = std::make_unique<TestAuraSurface>(surface);

  shell_surface->GetWidget()->GetNativeWindow()->SetBounds(
      gfx::Rect(0, 0, 10, 10));
  shell_surface->GetWidget()->GetNativeWindow()->Show();
  surface->window()->SetBounds(gfx::Rect(5, 5, 10, 10));
  surface->window()->Show();
  surface->window()->SetCapture();

  MockSurfaceObserver observer;
  surface->AddSurfaceObserver(&observer);

  // Move mouse over the window to show tooltip.
  // This is required since Ash needs to know which window is targeted for a
  // given tooltip.
  gfx::Point mouse_position = gfx::Point(6, 6);
  auto* generator = GetEventGenerator();
  generator->MoveMouseTo(mouse_position);

  const char* text = "my tooltip";
  gfx::Rect expected_tooltip_position =
      gfx::Rect(mouse_position, gfx::Size(77, kTooltipExpectedHeight));
  views::corewm::TooltipAura::AdjustToCursor(&expected_tooltip_position);
  aura::Window::ConvertRectToTarget(surface->window(),
                                    surface->window()->GetToplevelWindow(),
                                    &expected_tooltip_position);

  EXPECT_CALL(observer, OnTooltipShown(surface, base::UTF8ToUTF16(text),
                                       expected_tooltip_position));
  aura_surface->ShowTooltip(text, gfx::Point(),
                            ZAURA_SURFACE_TOOLTIP_TRIGGER_CURSOR,
                            base::TimeDelta(), base::TimeDelta());

  surface->RemoveSurfaceObserver(&observer);
}

TEST_F(ZAuraSurfaceCustomTest, ShowTooltipFromKeyboard) {
  std::unique_ptr<ShellSurface> shell_surface =
      test::ShellSurfaceBuilder({10, 10}).BuildShellSurface();

  Surface* surface = shell_surface->root_surface();
  auto aura_surface = std::make_unique<TestAuraSurface>(surface);

  shell_surface->GetWidget()->GetNativeWindow()->SetBounds(
      gfx::Rect(0, 0, 10, 10));
  shell_surface->GetWidget()->GetNativeWindow()->Show();
  surface->window()->SetBounds(gfx::Rect(0, 0, 10, 10));
  surface->window()->Show();

  MockSurfaceObserver observer;
  surface->AddSurfaceObserver(&observer);

  const char* text = "my tooltip";
  gfx::Point anchor_point = surface->window()->bounds().bottom_center();
  gfx::Size expected_tooltip_size = gfx::Size(77, kTooltipExpectedHeight);
  // Calculate expected tooltip position. For keyboard tooltip, it should be
  // shown right below and in the center of tooltip target window while it must
  // fit inside the display bounds.
  gfx::Rect expected_tooltip_position =
      gfx::Rect(anchor_point, expected_tooltip_size);
  expected_tooltip_position.Offset(-expected_tooltip_size.width() / 2, 0);
  gfx::Rect display_bounds(display::Screen::GetScreen()
                               ->GetDisplayNearestPoint(anchor_point)
                               .bounds());
  expected_tooltip_position.AdjustToFit(display_bounds);
  aura::Window::ConvertRectToTarget(surface->window(),
                                    surface->window()->GetToplevelWindow(),
                                    &expected_tooltip_position);

  EXPECT_CALL(observer, OnTooltipShown(surface, base::UTF8ToUTF16(text),
                                       expected_tooltip_position));
  aura_surface->ShowTooltip(text, anchor_point,
                            ZAURA_SURFACE_TOOLTIP_TRIGGER_KEYBOARD,
                            base::TimeDelta(), base::TimeDelta());

  surface->RemoveSurfaceObserver(&observer);
}

TEST_F(ZAuraSurfaceCustomTest, ShowTooltipOnMenuFromCursor) {
  std::unique_ptr<ShellSurface> shell_surface =
      test::ShellSurfaceBuilder({10, 10}).SetAsMenu().BuildShellSurface();

  Surface* surface = shell_surface->root_surface();
  auto aura_surface = std::make_unique<TestAuraSurface>(surface);

  shell_surface->GetWidget()->GetNativeWindow()->SetBounds(
      gfx::Rect(0, 0, 10, 10));
  shell_surface->GetWidget()->GetNativeWindow()->Show();
  surface->window()->SetBounds(gfx::Rect(5, 5, 10, 10));
  surface->window()->Show();
  surface->window()->SetCapture();

  MockSurfaceObserver observer;
  surface->AddSurfaceObserver(&observer);

  // Move mouse over the window to show tooltip.
  // This is required since Ash needs to know which window is targeted for a
  // given tooltip.
  gfx::Point mouse_position = gfx::Point(6, 6);
  auto* generator = GetEventGenerator();
  generator->MoveMouseTo(mouse_position);

  const char* text = "my tooltip";
  // Size of the tooltip depends on the text to show.
  gfx::Rect expected_tooltip_position =
      gfx::Rect(mouse_position, gfx::Size(77, kTooltipExpectedHeight));
  views::corewm::TooltipAura::AdjustToCursor(&expected_tooltip_position);
  aura::Window::ConvertRectToTarget(surface->window(),
                                    surface->window()->GetToplevelWindow(),
                                    &expected_tooltip_position);

  EXPECT_CALL(observer, OnTooltipShown(surface, base::UTF8ToUTF16(text),
                                       expected_tooltip_position));
  aura_surface->ShowTooltip(text, gfx::Point(),
                            ZAURA_SURFACE_TOOLTIP_TRIGGER_CURSOR,
                            base::TimeDelta(), base::TimeDelta());

  surface->RemoveSurfaceObserver(&observer);
}

TEST_F(ZAuraSurfaceCustomTest, ShowTooltipOnMenuFromKeyboard) {
  std::unique_ptr<ShellSurface> shell_surface =
      test::ShellSurfaceBuilder({10, 10}).SetAsMenu().BuildShellSurface();

  Surface* surface = shell_surface->root_surface();
  auto aura_surface = std::make_unique<TestAuraSurface>(surface);

  shell_surface->GetWidget()->GetNativeWindow()->SetBounds(
      gfx::Rect(0, 0, 10, 10));
  shell_surface->GetWidget()->GetNativeWindow()->Show();
  surface->window()->SetBounds(gfx::Rect(0, 0, 10, 10));
  surface->window()->Show();

  MockSurfaceObserver observer;
  surface->AddSurfaceObserver(&observer);

  const char* text = "my tooltip";
  gfx::Point anchor_point = surface->window()->bounds().bottom_center();
  gfx::Size expected_tooltip_size = gfx::Size(77, kTooltipExpectedHeight);
  // Calculate expected tooltip position. For keyboard tooltip, it should be
  // shown right below and in the center of tooltip target window while it must
  // fit inside the display bounds.
  gfx::Rect expected_tooltip_position =
      gfx::Rect(anchor_point, expected_tooltip_size);
  expected_tooltip_position.Offset(-expected_tooltip_size.width() / 2, 0);
  gfx::Rect display_bounds(display::Screen::GetScreen()
                               ->GetDisplayNearestPoint(anchor_point)
                               .bounds());
  expected_tooltip_position.AdjustToFit(display_bounds);
  aura::Window::ConvertRectToTarget(surface->window(),
                                    surface->window()->GetToplevelWindow(),
                                    &expected_tooltip_position);

  EXPECT_CALL(observer, OnTooltipShown(surface, base::UTF8ToUTF16(text),
                                       expected_tooltip_position));
  aura_surface->ShowTooltip(text, anchor_point,
                            ZAURA_SURFACE_TOOLTIP_TRIGGER_KEYBOARD,
                            base::TimeDelta(), base::TimeDelta());

  surface->RemoveSurfaceObserver(&observer);
}

class MockAuraOutput : public AuraOutput {
 public:
  using AuraOutput::AuraOutput;

  MOCK_METHOD(void, SendInsets, (const gfx::Insets&), (override));
  MOCK_METHOD(void, SendLogicalTransform, (int32_t), (override));
  MOCK_METHOD(void, SendActiveDisplay, (), (override));
};

class ZAuraOutputTest : public test::ExoTestBase {
 public:
  ZAuraOutputTest() = default;
  ZAuraOutputTest(const ZAuraOutputTest&) = delete;
  ZAuraOutputTest& operator=(const ZAuraOutputTest&) = delete;
  ~ZAuraOutputTest() override = default;

  // test::ExoTestBase:
  void SetUp() override {
    test::ExoTestBase::SetUp();

    int fds[2];
    ASSERT_EQ(socketpair(AF_UNIX, SOCK_STREAM | SOCK_CLOEXEC, 0, fds), 0);
    wayland_display_.reset(wl_display_create());
    client_ = wl_client_create(wayland_display_.get(), fds[0]);

    UpdateDisplayOutput();
  }
  void TearDown() override {
    output_holder_list_.clear();
    test::ExoTestBase::TearDown();
  }

 protected:
  void ResetDisplayOutput() {
    for (auto& holder : output_holder_list_) {
      holder->aura_output.reset();
      holder->output.reset();
    }
  }

  void UpdateDisplayOutput() {
    auto display_list = display::Screen::GetScreen()->GetAllDisplays();
    auto iter = output_holder_list_.begin();
    while (iter != output_holder_list_.end()) {
      auto* out_ptr = (*iter)->output.get();
      bool erased = std::erase_if(display_list,
                                  [out_ptr](const display::Display& display) {
                                    return display.id() == out_ptr->id();
                                  });
      if (erased)
        ++iter;
      else
        iter = output_holder_list_.erase(iter);
    }

    for (auto& display : display_list) {
      auto output_holder = std::make_unique<OutputHolder>();
      output_holder->client = client_;
      output_holder->output = std::make_unique<WaylandDisplayOutput>(display);

      wl_resource* output_resource = wl_resource_create(
          client_, &wl_output_interface, kWlOutputVersion, 0);
      output_holder->handler = std::make_unique<WaylandDisplayHandler>(
          output_holder->output.get(), output_resource);
      output_holder->handler->Initialize();
      output_holder->CreateAuraOutput();

      output_holder_list_.push_back(std::move(output_holder));
    }
  }

  MockAuraOutput* GetPrimaryAuraOutput() {
    return GetAuraOutput(
        display::Screen::GetScreen()->GetPrimaryDisplay().id());
  }

  MockAuraOutput* GetAuraOutput(int64_t display_id) {
    return GetOutputHolder(display_id)->aura_output.get();
  }

  WaylandDisplayHandler* GetPrimaryDisplayHandler() {
    return GetOutputHolder(
               display::Screen::GetScreen()->GetPrimaryDisplay().id())
        ->handler.get();
  }

  struct OutputHolder {
    std::unique_ptr<MockAuraOutput> aura_output;
    std::unique_ptr<WaylandDisplayOutput> output;
    std::unique_ptr<WaylandDisplayHandler> handler;

    raw_ptr<wl_client> client;

    void CreateAuraOutput() {
      DCHECK(!aura_output);
      aura_output = std::make_unique<::testing::NiceMock<MockAuraOutput>>(
          wl_resource_create(client, &zaura_output_interface,
                             kZAuraShellVersion, 0),
          handler.get());
    }
  };

  OutputHolder* GetOutputHolder(int64_t display_id) {
    auto iter = base::ranges::find_if(
        output_holder_list_,
        [display_id](const std::unique_ptr<OutputHolder>& holder) {
          return holder->output->id() == display_id;
        });
    return iter == output_holder_list_.end() ? nullptr : iter->get();
  }

 private:
  std::vector<std::unique_ptr<OutputHolder>> output_holder_list_;
  std::unique_ptr<wl_display, WlDisplayDeleter> wayland_display_;
  raw_ptr<wl_client> client_ = nullptr;
};

TEST_F(ZAuraOutputTest, SendInsets) {
  auto* mock_aura_output = GetPrimaryAuraOutput();

  UpdateDisplay("800x600");
  display::Display display =
      display_manager()->GetDisplayForId(display_manager()->first_display_id());
  const gfx::Rect initial_bounds{800, 600};
  EXPECT_EQ(display.bounds(), initial_bounds);
  const gfx::Rect new_work_area{10, 20, 500, 400};
  EXPECT_NE(display.work_area(), new_work_area);
  display.set_work_area(new_work_area);

  const gfx::Insets expected_insets = initial_bounds.InsetsFrom(new_work_area);
  EXPECT_CALL(*mock_aura_output, SendInsets(expected_insets)).Times(1);
  mock_aura_output->SendDisplayMetrics(
      display, display::DisplayObserver::DISPLAY_METRIC_WORK_AREA);
}

TEST_F(ZAuraOutputTest, SendLogicalTransform) {
  auto* mock_aura_output = GetPrimaryAuraOutput();

  UpdateDisplay("800x600");
  display::Display display =
      display_manager()->GetDisplayForId(display_manager()->first_display_id());

  // Make sure the expected calls happen in order.
  ::testing::InSequence seq;

  EXPECT_EQ(display.rotation(), display::Display::ROTATE_0);
  EXPECT_EQ(display.panel_rotation(), display::Display::ROTATE_0);
  EXPECT_CALL(*mock_aura_output,
              SendLogicalTransform(OutputTransform(display.rotation())))
      .Times(1);
  mock_aura_output->SendDisplayMetrics(
      display, display::DisplayObserver::DISPLAY_METRIC_ROTATION);

  display.set_rotation(display::Display::ROTATE_270);
  display.set_panel_rotation(display::Display::ROTATE_180);
  EXPECT_CALL(*mock_aura_output,
              SendLogicalTransform(OutputTransform(display.rotation())))
      .Times(1);
  mock_aura_output->SendDisplayMetrics(
      display, display::DisplayObserver::DISPLAY_METRIC_ROTATION);

  display.set_rotation(display::Display::ROTATE_90);
  display.set_panel_rotation(display::Display::ROTATE_180);
  EXPECT_CALL(*mock_aura_output,
              SendLogicalTransform(OutputTransform(display.rotation())))
      .Times(1);
  mock_aura_output->SendDisplayMetrics(
      display, display::DisplayObserver::DISPLAY_METRIC_ROTATION);

  display.set_rotation(display::Display::ROTATE_270);
  display.set_panel_rotation(display::Display::ROTATE_270);
  EXPECT_CALL(*mock_aura_output,
              SendLogicalTransform(OutputTransform(display.rotation())))
      .Times(1);
  mock_aura_output->SendDisplayMetrics(
      display, display::DisplayObserver::DISPLAY_METRIC_ROTATION);
}

// Make sure that data associated with wl/aura outputs are destroyed
// properly regardless of which one is destroyed first.
TEST_F(ZAuraOutputTest, DestroyAuraOutput) {
  auto* output_holder =
      GetOutputHolder(display::Screen::GetScreen()->GetPrimaryDisplay().id());

  EXPECT_EQ(1u, GetPrimaryDisplayHandler()->CountObserversForTesting());
  output_holder->aura_output.reset();
  EXPECT_EQ(0u, GetPrimaryDisplayHandler()->CountObserversForTesting());
  output_holder->CreateAuraOutput();

  EXPECT_EQ(1u, GetPrimaryDisplayHandler()->CountObserversForTesting());
  EXPECT_TRUE(output_holder->aura_output->HasDisplayHandlerForTesting());
  output_holder->handler.reset();
  EXPECT_FALSE(output_holder->aura_output->HasDisplayHandlerForTesting());
}

}  // namespace wayland
}  // namespace exo