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

#include "ui/aura/hit_test_data_provider_aura.h"

#include "components/viz/client/hit_test_data_provider.h"
#include "ui/aura/client/aura_constants.h"
#include "ui/aura/test/aura_test_base.h"
#include "ui/aura/window.h"
#include "ui/aura/window_targeter.h"
#include "ui/gfx/geometry/rect.h"

namespace aura {

namespace {

const int kMouseInset = -5;
const int kTouchInset = -10;

// Custom WindowTargeter that expands hit-test regions of child windows.
class TestWindowTargeter : public WindowTargeter {
 public:
  TestWindowTargeter() {
    SetInsets(gfx::Insets(kMouseInset), gfx::Insets(kTouchInset));
  }
  ~TestWindowTargeter() override {}

 private:
  DISALLOW_COPY_AND_ASSIGN(TestWindowTargeter);
};

// Custom WindowTargeter that replaces hit-test area on a window with a frame
// rectangle and a hole in the middle 1/3.
//  ----------------------
// |   hit     hit        |
// |      ----------      |
// |     |          |     |
// |     |  No hit  | hit |
// |     |          |     |
// | hit |          |     |
// |      ----------      |
// |   hit        hit     |
//  ----------------------
class TestHoleWindowTargeter : public aura::WindowTargeter {
 public:
  TestHoleWindowTargeter() = default;
  ~TestHoleWindowTargeter() override {}

 private:
  // aura::WindowTargeter:
  std::unique_ptr<aura::WindowTargeter::HitTestRects> GetExtraHitTestShapeRects(
      aura::Window* target) const override {
    gfx::Rect bounds = target->bounds();
    int x0 = 0;
    int x1 = bounds.width() / 3;
    int x2 = bounds.width() - bounds.width() / 3;
    int x3 = bounds.width();
    int y0 = 0;
    int y1 = bounds.height() / 3;
    int y2 = bounds.height() - bounds.height() / 3;
    int y3 = bounds.height();
    auto shape_rects = std::make_unique<aura::WindowTargeter::HitTestRects>();
    shape_rects->emplace_back(x0, y0, bounds.width(), y1 - y0);
    shape_rects->emplace_back(x0, y1, x1 - x0, y2 - y1);
    shape_rects->emplace_back(x2, y1, x3 - x2, y2 - y1);
    shape_rects->emplace_back(x0, y2, bounds.width(), y3 - y2);
    return shape_rects;
  }

  DISALLOW_COPY_AND_ASSIGN(TestHoleWindowTargeter);
};

}  // namespace

// Creates a root window, child windows and a viz::HitTestDataProvider.
// root
//   +- window2
//   |_ window3
//        |_ window4
class HitTestDataProviderAuraTest : public test::AuraTestBaseMus {
 public:
  HitTestDataProviderAuraTest() {}
  ~HitTestDataProviderAuraTest() override {}

  void SetUp() override {
    test::AuraTestBaseMus::SetUp();

    root_ = std::make_unique<Window>(nullptr);
    root_->SetProperty(aura::client::kEmbedType,
                       aura::client::WindowEmbedType::TOP_LEVEL_IN_WM);
    root_->Init(ui::LAYER_NOT_DRAWN);
    root_->SetEventTargeter(std::make_unique<WindowTargeter>());
    root_->SetBounds(gfx::Rect(0, 0, 300, 200));
    root_->Show();

    window2_ = new Window(nullptr);
    window2_->SetProperty(aura::client::kEmbedType,
                          aura::client::WindowEmbedType::EMBED_IN_OWNER);
    window2_->Init(ui::LAYER_TEXTURED);
    window2_->SetBounds(gfx::Rect(20, 30, 40, 60));
    window2_->Show();

    window3_ = new Window(nullptr);
    window3_->SetProperty(aura::client::kEmbedType,
                          aura::client::WindowEmbedType::EMBED_IN_OWNER);
    window3_->Init(ui::LAYER_TEXTURED);
    window3_->SetEventTargeter(std::make_unique<WindowTargeter>());
    window3_->SetBounds(gfx::Rect(50, 60, 100, 40));
    window3_->Show();

    window4_ = new Window(nullptr);
    window4_->Init(ui::LAYER_TEXTURED);
    window4_->SetBounds(gfx::Rect(20, 10, 60, 30));
    window4_->Show();

    window3_->AddChild(window4_);
    root_->AddChild(window2_);
    root_->AddChild(window3_);

    hit_test_data_provider_ = std::make_unique<HitTestDataProviderAura>(root());
  }

 protected:
  const viz::HitTestDataProvider* hit_test_data_provider() const {
    return hit_test_data_provider_.get();
  }

  Window* root() { return root_.get(); }
  Window* window2() { return window2_; }
  Window* window3() { return window3_; }
  Window* window4() { return window4_; }

 private:
  std::unique_ptr<Window> root_;
  Window* window2_;
  Window* window3_;
  Window* window4_;
  std::unique_ptr<viz::HitTestDataProvider> hit_test_data_provider_;

  DISALLOW_COPY_AND_ASSIGN(HitTestDataProviderAuraTest);
};

// TODO(riajiang): Add test cases for kHitTestChildSurface to ensure
// that local_surface_id is set and used correctly.

// Tests that the order of reported hit-test regions matches windows Z-order.
TEST_F(HitTestDataProviderAuraTest, Stacking) {
  const auto hit_test_data_1 = hit_test_data_provider()->GetHitTestData();
  ASSERT_TRUE(hit_test_data_1);
  EXPECT_EQ(hit_test_data_1->flags, viz::mojom::kHitTestMine);
  EXPECT_EQ(hit_test_data_1->bounds, root()->bounds());
  Window* expected_order_1[] = {window3(), window4(), window2()};
  EXPECT_EQ(hit_test_data_1->regions.size(), arraysize(expected_order_1));
  int i = 0;
  for (const auto& region : hit_test_data_1->regions) {
    EXPECT_EQ(region->flags, viz::mojom::kHitTestMine |
                                 viz::mojom::kHitTestMouse |
                                 viz::mojom::kHitTestTouch);
    EXPECT_EQ(region->frame_sink_id, expected_order_1[i]->GetFrameSinkId());
    EXPECT_EQ(region->rect.ToString(),
              expected_order_1[i]->bounds().ToString());
    i++;
  }

  root()->StackChildAbove(window2(), window3());
  const auto hit_test_data_2 = hit_test_data_provider()->GetHitTestData();
  ASSERT_TRUE(hit_test_data_2);
  EXPECT_EQ(hit_test_data_2->flags, viz::mojom::kHitTestMine);
  EXPECT_EQ(hit_test_data_2->bounds, root()->bounds());

  Window* expected_order_2[] = {window2(), window3(), window4()};
  EXPECT_EQ(hit_test_data_2->regions.size(), arraysize(expected_order_2));
  i = 0;
  for (const auto& region : hit_test_data_2->regions) {
    EXPECT_EQ(region->flags, viz::mojom::kHitTestMine |
                                 viz::mojom::kHitTestMouse |
                                 viz::mojom::kHitTestTouch);
    EXPECT_EQ(region->frame_sink_id, expected_order_2[i]->GetFrameSinkId());
    EXPECT_EQ(region->rect.ToString(),
              expected_order_2[i]->bounds().ToString());
    i++;
  }
}

// Tests that the hit-test regions get expanded with a custom event targeter.
TEST_F(HitTestDataProviderAuraTest, CustomTargeter) {
  window3()->SetEventTargeter(std::make_unique<TestWindowTargeter>());
  window2()->set_embed_frame_sink_id(viz::FrameSinkId(1, 2));
  const auto hit_test_data = hit_test_data_provider()->GetHitTestData();
  ASSERT_TRUE(hit_test_data);
  EXPECT_EQ(hit_test_data->flags, viz::mojom::kHitTestMine);
  EXPECT_EQ(hit_test_data->bounds, root()->bounds());

  // Children of a window that has the custom targeter installed as well as that
  // window will get reported twice, once with hit-test bounds optimized for
  // mouse events and another time with bounds expanded more for touch input.
  struct {
    Window* window;
    uint32_t flags;
    int insets;
  } expected[] = {
      {window3(), viz::mojom::kHitTestMine | viz::mojom::kHitTestMouse,
       kMouseInset},
      {window3(), viz::mojom::kHitTestMine | viz::mojom::kHitTestTouch,
       kTouchInset},
      {window4(), viz::mojom::kHitTestMine | viz::mojom::kHitTestMouse,
       kMouseInset},
      {window4(), viz::mojom::kHitTestMine | viz::mojom::kHitTestTouch,
       kTouchInset},
      {window2(),
       viz::mojom::kHitTestChildSurface | viz::mojom::kHitTestMouse |
           viz::mojom::kHitTestTouch,
       0}};
  ASSERT_EQ(hit_test_data->regions.size(), arraysize(expected));
  ASSERT_EQ(hit_test_data->regions.size(), arraysize(expected));
  ASSERT_EQ(hit_test_data->regions.size(), arraysize(expected));
  int i = 0;
  for (const auto& region : hit_test_data->regions) {
    EXPECT_EQ(region->frame_sink_id, expected[i].window->GetFrameSinkId());
    EXPECT_EQ(region->flags, expected[i].flags);
    gfx::Rect expected_bounds = expected[i].window->bounds();
    expected_bounds.Inset(gfx::Insets(expected[i].insets));
    EXPECT_EQ(region->rect.ToString(), expected_bounds.ToString());
    i++;
  }
}

// Tests that the complex hit-test shape can be set with a custom targeter.
TEST_F(HitTestDataProviderAuraTest, HoleTargeter) {
  window3()->SetEventTargeter(std::make_unique<TestHoleWindowTargeter>());
  const auto hit_test_data = hit_test_data_provider()->GetHitTestData();
  ASSERT_TRUE(hit_test_data);
  EXPECT_EQ(hit_test_data->flags, viz::mojom::kHitTestMine);
  EXPECT_EQ(hit_test_data->bounds, root()->bounds());

  // Children of a container that has the custom targeter installed as well as
  // that container will get reported 4 times for each of the hit test regions
  // defined by the custom targeter.
  // original window3 is at gfx::Rect(50, 60, 100, 40).
  // original window4 is at gfx::Rect(20, 10, 60, 30).
  struct {
    Window* window;
    gfx::Rect bounds;
  } expected[] = {
      {window3(), {50, 60, 100, 13}},  {window3(), {50, 73, 33, 14}},
      {window3(), {117, 73, 33, 14}},  {window3(), {50, 87, 100, 13}},
      {window4(), {20, 10, 60, 10}},   {window4(), {20, 20, 20, 10}},
      {window4(), {60, 20, 20, 10}},   {window4(), {20, 30, 60, 10}},
      {window2(), window2()->bounds()}};
  constexpr uint32_t expected_flags = viz::mojom::kHitTestMine |
                                      viz::mojom::kHitTestMouse |
                                      viz::mojom::kHitTestTouch;
  ASSERT_EQ(hit_test_data->regions.size(), arraysize(expected));
  int i = 0;
  for (const auto& region : hit_test_data->regions) {
    EXPECT_EQ(region->frame_sink_id, expected[i].window->GetFrameSinkId());
    EXPECT_EQ(region->flags, expected_flags);
    EXPECT_EQ(region->rect.ToString(), expected[i].bounds.ToString());
    i++;
  }
}

TEST_F(HitTestDataProviderAuraTest, TargetingPolicies) {
  root()->SetEventTargetingPolicy(ui::mojom::EventTargetingPolicy::NONE);
  auto hit_test_data = hit_test_data_provider()->GetHitTestData();
  ASSERT_FALSE(hit_test_data);

  root()->SetEventTargetingPolicy(ui::mojom::EventTargetingPolicy::TARGET_ONLY);
  window3()->SetEventTargetingPolicy(
      ui::mojom::EventTargetingPolicy::TARGET_AND_DESCENDANTS);
  hit_test_data = hit_test_data_provider()->GetHitTestData();
  ASSERT_TRUE(hit_test_data);
  EXPECT_EQ(hit_test_data->flags, viz::mojom::kHitTestMine);
  EXPECT_EQ(hit_test_data->regions.size(), 3u);

  root()->SetEventTargetingPolicy(ui::mojom::EventTargetingPolicy::TARGET_ONLY);
  window3()->SetEventTargetingPolicy(
      ui::mojom::EventTargetingPolicy::TARGET_ONLY);
  hit_test_data = hit_test_data_provider()->GetHitTestData();
  ASSERT_TRUE(hit_test_data);
  EXPECT_EQ(hit_test_data->flags, viz::mojom::kHitTestMine);
  EXPECT_EQ(hit_test_data->regions.size(), 2u);

  root()->SetEventTargetingPolicy(
      ui::mojom::EventTargetingPolicy::DESCENDANTS_ONLY);
  window3()->SetEventTargetingPolicy(
      ui::mojom::EventTargetingPolicy::DESCENDANTS_ONLY);
  hit_test_data = hit_test_data_provider()->GetHitTestData();
  ASSERT_TRUE(hit_test_data);
  EXPECT_EQ(hit_test_data->flags, viz::mojom::kHitTestIgnore);
  EXPECT_EQ(hit_test_data->regions.size(), 2u);

  root()->SetEventTargetingPolicy(
      ui::mojom::EventTargetingPolicy::TARGET_AND_DESCENDANTS);
  window3()->SetEventTargetingPolicy(
      ui::mojom::EventTargetingPolicy::TARGET_AND_DESCENDANTS);
  hit_test_data = hit_test_data_provider()->GetHitTestData();
  ASSERT_TRUE(hit_test_data);
  EXPECT_EQ(hit_test_data->flags, viz::mojom::kHitTestMine);
  EXPECT_EQ(hit_test_data->regions.size(), 3u);
}

// Tests that we do not submit hit-test data for invisible windows and for
// children of a child surface.
TEST_F(HitTestDataProviderAuraTest, DoNotSubmit) {
  auto hit_test_data = hit_test_data_provider()->GetHitTestData();
  ASSERT_TRUE(hit_test_data);
  EXPECT_EQ(hit_test_data->regions.size(), 3u);

  window2()->Hide();
  hit_test_data = hit_test_data_provider()->GetHitTestData();
  ASSERT_TRUE(hit_test_data);
  EXPECT_EQ(hit_test_data->regions.size(), 2u);

  window3()->set_embed_frame_sink_id(viz::FrameSinkId(1, 3));
  hit_test_data = hit_test_data_provider()->GetHitTestData();
  ASSERT_TRUE(hit_test_data);
  EXPECT_EQ(hit_test_data->regions.size(), 1u);

  root()->Hide();
  hit_test_data = hit_test_data_provider()->GetHitTestData();
  ASSERT_FALSE(hit_test_data);

  root()->Show();
  hit_test_data = hit_test_data_provider()->GetHitTestData();
  ASSERT_TRUE(hit_test_data);
  EXPECT_EQ(hit_test_data->regions.size(), 1u);
  root()->set_embed_frame_sink_id(viz::FrameSinkId(1, 1));
  hit_test_data = hit_test_data_provider()->GetHitTestData();
  ASSERT_TRUE(hit_test_data);
  EXPECT_EQ(hit_test_data->regions.size(), 0u);
}

}  // namespace aura
