// Copyright 2013 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 "content/renderer/render_widget.h"

#include <tuple>
#include <vector>

#include "base/macros.h"
#include "base/test/histogram_tester.h"
#include "content/common/input/synthetic_web_input_event_builders.h"
#include "content/common/input_messages.h"
#include "content/common/resize_params.h"
#include "content/public/test/mock_render_thread.h"
#include "content/renderer/devtools/render_widget_screen_metrics_emulator.h"
#include "content/test/fake_compositor_dependencies.h"
#include "content/test/mock_render_process.h"
#include "ipc/ipc_test_sink.h"
#include "testing/gmock/include/gmock/gmock.h"
#include "testing/gtest/include/gtest/gtest.h"
#include "third_party/WebKit/public/platform/WebInputEvent.h"
#include "third_party/WebKit/public/web/WebDeviceEmulationParams.h"
#include "ui/events/blink/web_input_event_traits.h"
#include "ui/gfx/geometry/rect.h"

using testing::_;

namespace content {

namespace {

const char* EVENT_LISTENER_RESULT_HISTOGRAM = "Event.PassiveListeners";

enum {
  PASSIVE_LISTENER_UMA_ENUM_PASSIVE,
  PASSIVE_LISTENER_UMA_ENUM_UNCANCELABLE,
  PASSIVE_LISTENER_UMA_ENUM_SUPPRESSED,
  PASSIVE_LISTENER_UMA_ENUM_CANCELABLE,
  PASSIVE_LISTENER_UMA_ENUM_CANCELABLE_AND_CANCELED,
  PASSIVE_LISTENER_UMA_ENUM_FORCED_NON_BLOCKING_DUE_TO_FLING,
  PASSIVE_LISTENER_UMA_ENUM_COUNT
};

class MockWebWidget : public blink::WebWidget {
 public:
  MOCK_METHOD1(handleInputEvent,
               blink::WebInputEventResult(const blink::WebInputEvent&));
};

}  // namespace

class InteractiveRenderWidget : public RenderWidget {
 public:
  explicit InteractiveRenderWidget(CompositorDependencies* compositor_deps)
      : RenderWidget(++next_routing_id_,
                     compositor_deps,
                     blink::WebPopupTypeNone,
                     ScreenInfo(),
                     false,
                     false,
                     false),
        always_overscroll_(false) {
    Init(MSG_ROUTING_NONE, mock_webwidget());
  }

  void SetTouchRegion(const std::vector<gfx::Rect>& rects) {
    rects_ = rects;
  }

  void SendInputEvent(const blink::WebInputEvent& event) {
    OnHandleInputEvent(
        &event, ui::LatencyInfo(),
        ui::WebInputEventTraits::ShouldBlockEventStream(event)
            ? InputEventDispatchType::DISPATCH_TYPE_BLOCKING
            : InputEventDispatchType::DISPATCH_TYPE_NON_BLOCKING);
  }

  void set_always_overscroll(bool overscroll) {
    always_overscroll_ = overscroll;
  }

  IPC::TestSink* sink() { return &sink_; }

  MockWebWidget* mock_webwidget() { return &mock_webwidget_; }

 protected:
  ~InteractiveRenderWidget() override { webwidget_internal_ = nullptr; }

  // Overridden from RenderWidget:
  bool HasTouchEventHandlersAt(const gfx::Point& point) const override {
    for (std::vector<gfx::Rect>::const_iterator iter = rects_.begin();
         iter != rects_.end(); ++iter) {
      if ((*iter).Contains(point))
        return true;
    }
    return false;
  }

  bool WillHandleGestureEvent(const blink::WebGestureEvent& event) override {
    if (always_overscroll_ &&
        event.type == blink::WebInputEvent::GestureScrollUpdate) {
      didOverscroll(blink::WebFloatSize(event.data.scrollUpdate.deltaX,
                                        event.data.scrollUpdate.deltaY),
                    blink::WebFloatSize(event.data.scrollUpdate.deltaX,
                                        event.data.scrollUpdate.deltaY),
                    blink::WebFloatPoint(event.x, event.y),
                    blink::WebFloatSize(event.data.scrollUpdate.velocityX,
                                        event.data.scrollUpdate.velocityY));
      return true;
    }

    return false;
  }

  bool Send(IPC::Message* msg) override {
    sink_.OnMessageReceived(*msg);
    delete msg;
    return true;
  }

 private:
  std::vector<gfx::Rect> rects_;
  IPC::TestSink sink_;
  bool always_overscroll_;
  MockWebWidget mock_webwidget_;
  static int next_routing_id_;

  DISALLOW_COPY_AND_ASSIGN(InteractiveRenderWidget);
};

int InteractiveRenderWidget::next_routing_id_ = 0;

class RenderWidgetUnittest : public testing::Test {
 public:
  RenderWidgetUnittest()
      : widget_(new InteractiveRenderWidget(&compositor_deps_)) {
    // RenderWidget::Init does an AddRef that's balanced by a browser-initiated
    // Close IPC. That Close will never happen in this test, so do a Release
    // here to ensure |widget_| is properly freed.
    widget_->Release();
    DCHECK(widget_->HasOneRef());
  }
  ~RenderWidgetUnittest() override {}

  InteractiveRenderWidget* widget() const { return widget_.get(); }

  const base::HistogramTester& histogram_tester() const {
    return histogram_tester_;
  }

 private:
  MockRenderProcess render_process_;
  MockRenderThread render_thread_;
  FakeCompositorDependencies compositor_deps_;
  scoped_refptr<InteractiveRenderWidget> widget_;
  base::HistogramTester histogram_tester_;

  DISALLOW_COPY_AND_ASSIGN(RenderWidgetUnittest);
};

TEST_F(RenderWidgetUnittest, TouchHitTestSinglePoint) {
  SyntheticWebTouchEvent touch;
  touch.PressPoint(10, 10);

  EXPECT_CALL(*widget()->mock_webwidget(), handleInputEvent(_))
      .WillRepeatedly(
          ::testing::Return(blink::WebInputEventResult::NotHandled));

  widget()->SendInputEvent(touch);
  ASSERT_EQ(1u, widget()->sink()->message_count());

  // Since there's currently no touch-event handling region, the response should
  // be 'no consumer exists'.
  const IPC::Message* message = widget()->sink()->GetMessageAt(0);
  EXPECT_EQ(InputHostMsg_HandleInputEvent_ACK::ID, message->type());
  InputHostMsg_HandleInputEvent_ACK::Param params;
  InputHostMsg_HandleInputEvent_ACK::Read(message, &params);
  InputEventAckState ack_state = std::get<0>(params).state;
  EXPECT_EQ(INPUT_EVENT_ACK_STATE_NO_CONSUMER_EXISTS, ack_state);
  widget()->sink()->ClearMessages();

  std::vector<gfx::Rect> rects;
  rects.push_back(gfx::Rect(0, 0, 20, 20));
  rects.push_back(gfx::Rect(25, 0, 10, 10));
  widget()->SetTouchRegion(rects);

  EXPECT_CALL(*widget()->mock_webwidget(), handleInputEvent(_))
      .WillRepeatedly(
          ::testing::Return(blink::WebInputEventResult::NotHandled));

  widget()->SendInputEvent(touch);
  ASSERT_EQ(1u, widget()->sink()->message_count());
  message = widget()->sink()->GetMessageAt(0);
  EXPECT_EQ(InputHostMsg_HandleInputEvent_ACK::ID, message->type());
  InputHostMsg_HandleInputEvent_ACK::Read(message, &params);
  ack_state = std::get<0>(params).state;
  EXPECT_EQ(INPUT_EVENT_ACK_STATE_NOT_CONSUMED, ack_state);
  widget()->sink()->ClearMessages();
}

TEST_F(RenderWidgetUnittest, TouchHitTestMultiplePoints) {
  std::vector<gfx::Rect> rects;
  rects.push_back(gfx::Rect(0, 0, 20, 20));
  rects.push_back(gfx::Rect(25, 0, 10, 10));
  widget()->SetTouchRegion(rects);

  SyntheticWebTouchEvent touch;
  touch.PressPoint(25, 25);

  EXPECT_CALL(*widget()->mock_webwidget(), handleInputEvent(_))
      .WillRepeatedly(
          ::testing::Return(blink::WebInputEventResult::NotHandled));

  widget()->SendInputEvent(touch);
  ASSERT_EQ(1u, widget()->sink()->message_count());

  // Since there's currently no touch-event handling region, the response should
  // be 'no consumer exists'.
  const IPC::Message* message = widget()->sink()->GetMessageAt(0);
  EXPECT_EQ(InputHostMsg_HandleInputEvent_ACK::ID, message->type());
  InputHostMsg_HandleInputEvent_ACK::Param params;
  InputHostMsg_HandleInputEvent_ACK::Read(message, &params);
  InputEventAckState ack_state = std::get<0>(params).state;
  EXPECT_EQ(INPUT_EVENT_ACK_STATE_NO_CONSUMER_EXISTS, ack_state);
  widget()->sink()->ClearMessages();

  // Press a second touch point. This time, on a touch-handling region.
  touch.PressPoint(10, 10);
  widget()->SendInputEvent(touch);
  ASSERT_EQ(1u, widget()->sink()->message_count());
  message = widget()->sink()->GetMessageAt(0);
  EXPECT_EQ(InputHostMsg_HandleInputEvent_ACK::ID, message->type());
  InputHostMsg_HandleInputEvent_ACK::Read(message, &params);
  ack_state = std::get<0>(params).state;
  EXPECT_EQ(INPUT_EVENT_ACK_STATE_NOT_CONSUMED, ack_state);
  widget()->sink()->ClearMessages();
}

TEST_F(RenderWidgetUnittest, EventOverscroll) {
  widget()->set_always_overscroll(true);

  EXPECT_CALL(*widget()->mock_webwidget(), handleInputEvent(_))
      .WillRepeatedly(
          ::testing::Return(blink::WebInputEventResult::NotHandled));

  blink::WebGestureEvent scroll;
  scroll.type = blink::WebInputEvent::GestureScrollUpdate;
  scroll.x = -10;
  scroll.data.scrollUpdate.deltaY = 10;
  widget()->SendInputEvent(scroll);

  // Overscroll notifications received while handling an input event should
  // be bundled with the event ack IPC.
  ASSERT_EQ(1u, widget()->sink()->message_count());
  const IPC::Message* message = widget()->sink()->GetMessageAt(0);
  ASSERT_EQ(InputHostMsg_HandleInputEvent_ACK::ID, message->type());
  InputHostMsg_HandleInputEvent_ACK::Param params;
  InputHostMsg_HandleInputEvent_ACK::Read(message, &params);
  const InputEventAck& ack = std::get<0>(params);
  ASSERT_EQ(ack.type, scroll.type);
  ASSERT_TRUE(ack.overscroll);
  EXPECT_EQ(gfx::Vector2dF(0, 10), ack.overscroll->accumulated_overscroll);
  EXPECT_EQ(gfx::Vector2dF(0, 10), ack.overscroll->latest_overscroll_delta);
  EXPECT_EQ(gfx::Vector2dF(), ack.overscroll->current_fling_velocity);
  EXPECT_EQ(gfx::PointF(-10, 0), ack.overscroll->causal_event_viewport_point);
  widget()->sink()->ClearMessages();
}

TEST_F(RenderWidgetUnittest, FlingOverscroll) {
  // Overscroll notifications received outside of handling an input event should
  // be sent as a separate IPC.
  widget()->didOverscroll(blink::WebFloatSize(10, 5), blink::WebFloatSize(5, 5),
                          blink::WebFloatPoint(1, 1),
                          blink::WebFloatSize(10, 5));
  ASSERT_EQ(1u, widget()->sink()->message_count());
  const IPC::Message* message = widget()->sink()->GetMessageAt(0);
  ASSERT_EQ(InputHostMsg_DidOverscroll::ID, message->type());
  InputHostMsg_DidOverscroll::Param params;
  InputHostMsg_DidOverscroll::Read(message, &params);
  const ui::DidOverscrollParams& overscroll = std::get<0>(params);
  EXPECT_EQ(gfx::Vector2dF(10, 5), overscroll.latest_overscroll_delta);
  EXPECT_EQ(gfx::Vector2dF(5, 5), overscroll.accumulated_overscroll);
  EXPECT_EQ(gfx::PointF(1, 1), overscroll.causal_event_viewport_point);
  EXPECT_EQ(gfx::Vector2dF(10, 5), overscroll.current_fling_velocity);
  widget()->sink()->ClearMessages();
}

TEST_F(RenderWidgetUnittest, RenderWidgetInputEventUmaMetrics) {
  SyntheticWebTouchEvent touch;
  touch.PressPoint(10, 10);
  touch.touchStartOrFirstTouchMove = true;

  EXPECT_CALL(*widget()->mock_webwidget(), handleInputEvent(_))
      .Times(5)
      .WillRepeatedly(
          ::testing::Return(blink::WebInputEventResult::NotHandled));

  widget()->SendInputEvent(touch);
  histogram_tester().ExpectBucketCount(EVENT_LISTENER_RESULT_HISTOGRAM,
                                       PASSIVE_LISTENER_UMA_ENUM_CANCELABLE, 1);

  touch.dispatchType = blink::WebInputEvent::DispatchType::EventNonBlocking;
  widget()->SendInputEvent(touch);
  histogram_tester().ExpectBucketCount(EVENT_LISTENER_RESULT_HISTOGRAM,
                                       PASSIVE_LISTENER_UMA_ENUM_UNCANCELABLE,
                                       1);

  touch.dispatchType =
      blink::WebInputEvent::DispatchType::ListenersNonBlockingPassive;
  widget()->SendInputEvent(touch);
  histogram_tester().ExpectBucketCount(EVENT_LISTENER_RESULT_HISTOGRAM,
                                       PASSIVE_LISTENER_UMA_ENUM_PASSIVE, 1);

  touch.dispatchType =
      blink::WebInputEvent::DispatchType::ListenersForcedNonBlockingDueToFling;
  widget()->SendInputEvent(touch);
  histogram_tester().ExpectBucketCount(
      EVENT_LISTENER_RESULT_HISTOGRAM,
      PASSIVE_LISTENER_UMA_ENUM_FORCED_NON_BLOCKING_DUE_TO_FLING, 1);

  touch.MovePoint(0, 10, 10);
  touch.touchStartOrFirstTouchMove = true;
  touch.dispatchType =
      blink::WebInputEvent::DispatchType::ListenersForcedNonBlockingDueToFling;
  widget()->SendInputEvent(touch);
  histogram_tester().ExpectBucketCount(
      EVENT_LISTENER_RESULT_HISTOGRAM,
      PASSIVE_LISTENER_UMA_ENUM_FORCED_NON_BLOCKING_DUE_TO_FLING, 2);

  EXPECT_CALL(*widget()->mock_webwidget(), handleInputEvent(_))
      .WillOnce(
          ::testing::Return(blink::WebInputEventResult::HandledSuppressed));
  touch.dispatchType = blink::WebInputEvent::DispatchType::Blocking;
  widget()->SendInputEvent(touch);
  histogram_tester().ExpectBucketCount(EVENT_LISTENER_RESULT_HISTOGRAM,
                                       PASSIVE_LISTENER_UMA_ENUM_SUPPRESSED, 1);

  EXPECT_CALL(*widget()->mock_webwidget(), handleInputEvent(_))
      .WillOnce(
          ::testing::Return(blink::WebInputEventResult::HandledApplication));
  touch.dispatchType = blink::WebInputEvent::DispatchType::Blocking;
  widget()->SendInputEvent(touch);
  histogram_tester().ExpectBucketCount(
      EVENT_LISTENER_RESULT_HISTOGRAM,
      PASSIVE_LISTENER_UMA_ENUM_CANCELABLE_AND_CANCELED, 1);
}

TEST_F(RenderWidgetUnittest, TouchDuringOrOutsideFlingUmaMetrics) {
  EXPECT_CALL(*widget()->mock_webwidget(), handleInputEvent(_))
      .Times(3)
      .WillRepeatedly(
          ::testing::Return(blink::WebInputEventResult::NotHandled));

  SyntheticWebTouchEvent touch;
  touch.PressPoint(10, 10);
  touch.dispatchType = blink::WebInputEvent::DispatchType::Blocking;
  touch.touchStartOrFirstTouchMove = true;
  widget()->SendInputEvent(touch);
  histogram_tester().ExpectTotalCount("Event.Touch.TouchLatencyOutsideFling",
                                      1);

  touch.MovePoint(0, 10, 10);
  touch.touchStartOrFirstTouchMove = true;
  widget()->SendInputEvent(touch);
  histogram_tester().ExpectTotalCount("Event.Touch.TouchLatencyOutsideFling",
                                      2);

  touch.MovePoint(0, 30, 30);
  touch.touchStartOrFirstTouchMove = false;
  widget()->SendInputEvent(touch);
  histogram_tester().ExpectTotalCount("Event.Touch.TouchLatencyOutsideFling",
                                      2);
}

class PopupRenderWidget : public RenderWidget {
 public:
  explicit PopupRenderWidget(CompositorDependencies* compositor_deps)
      : RenderWidget(1,
                     compositor_deps,
                     blink::WebPopupTypePage,
                     ScreenInfo(),
                     false,
                     false,
                     false) {
    Init(MSG_ROUTING_NONE, mock_webwidget());
    did_show_ = true;
  }

  IPC::TestSink* sink() { return &sink_; }

  MockWebWidget* mock_webwidget() { return &mock_webwidget_; }

  void SetScreenMetricsEmulationParameters(
      bool,
      const blink::WebDeviceEmulationParams&) override {}

 protected:
  ~PopupRenderWidget() override { webwidget_internal_ = nullptr; }

  bool Send(IPC::Message* msg) override {
    sink_.OnMessageReceived(*msg);
    delete msg;
    return true;
  }

 private:
  IPC::TestSink sink_;
  MockWebWidget mock_webwidget_;

  DISALLOW_COPY_AND_ASSIGN(PopupRenderWidget);
};

class RenderWidgetPopupUnittest : public testing::Test {
 public:
  RenderWidgetPopupUnittest()
      : widget_(new PopupRenderWidget(&compositor_deps_)) {
    // RenderWidget::Init does an AddRef that's balanced by a browser-initiated
    // Close IPC. That Close will never happen in this test, so do a Release
    // here to ensure |widget_| is properly freed.
    widget_->Release();
    DCHECK(widget_->HasOneRef());
  }
  ~RenderWidgetPopupUnittest() override {}

  PopupRenderWidget* widget() const { return widget_.get(); }
  FakeCompositorDependencies compositor_deps_;

 private:
  MockRenderProcess render_process_;
  MockRenderThread render_thread_;
  scoped_refptr<PopupRenderWidget> widget_;

  DISALLOW_COPY_AND_ASSIGN(RenderWidgetPopupUnittest);
};

TEST_F(RenderWidgetPopupUnittest, EmulatingPopupRect) {
  blink::WebRect popup_screen_rect(200, 250, 100, 400);
  widget()->setWindowRect(popup_screen_rect);

  // The view and window rect on a popup type RenderWidget should be
  // immediately set, without requiring an ACK.
  EXPECT_EQ(popup_screen_rect.x, widget()->windowRect().x);
  EXPECT_EQ(popup_screen_rect.y, widget()->windowRect().y);

  EXPECT_EQ(popup_screen_rect.x, widget()->viewRect().x);
  EXPECT_EQ(popup_screen_rect.y, widget()->viewRect().y);

  gfx::Rect emulated_window_rect(0, 0, 980, 1200);

  blink::WebDeviceEmulationParams emulation_params;
  emulation_params.screenPosition = blink::WebDeviceEmulationParams::Mobile;
  emulation_params.viewSize = emulated_window_rect.size();
  emulation_params.viewPosition = blink::WebPoint(150, 160);
  emulation_params.fitToView = true;

  gfx::Rect parent_window_rect = gfx::Rect(0, 0, 800, 600);

  ResizeParams resize_params;
  resize_params.new_size = parent_window_rect.size();

  scoped_refptr<PopupRenderWidget> parent_widget(
      new PopupRenderWidget(&compositor_deps_));
  parent_widget->Release();  // Balance Init().
  RenderWidgetScreenMetricsEmulator emulator(
      parent_widget.get(), emulation_params, resize_params, parent_window_rect,
      parent_window_rect);
  emulator.Apply();

  widget()->SetPopupOriginAdjustmentsForEmulation(&emulator);

  // Emulation-applied scale factor to fit the emulated device in the window.
  float scale =
      (float)parent_window_rect.height() / emulated_window_rect.height();

  // Used to center the emulated device in the window.
  gfx::Point offset(
      (parent_window_rect.width() - emulated_window_rect.width() * scale) / 2,
      (parent_window_rect.height() - emulated_window_rect.height() * scale) /
          2);

  // Position of the popup as seen by the emulated widget.
  gfx::Point emulated_position(emulation_params.viewPosition.x +
                                   (popup_screen_rect.x - offset.x()) / scale,
                               emulation_params.viewPosition.y +
                                   (popup_screen_rect.y - offset.y()) / scale);

  // Both the window and view rects as read from the accessors should have the
  // emulation parameters applied.
  EXPECT_EQ(emulated_position.x(), widget()->windowRect().x);
  EXPECT_EQ(emulated_position.y(), widget()->windowRect().y);
  EXPECT_EQ(emulated_position.x(), widget()->viewRect().x);
  EXPECT_EQ(emulated_position.y(), widget()->viewRect().y);

  // Setting a new window rect while emulated should remove the emulation
  // transformation from the given rect so that getting the rect, which applies
  // the transformation to the raw rect, should result in the same value.
  blink::WebRect popup_emulated_rect(130, 170, 100, 400);
  widget()->setWindowRect(popup_emulated_rect);

  EXPECT_EQ(popup_emulated_rect.x, widget()->windowRect().x);
  EXPECT_EQ(popup_emulated_rect.y, widget()->windowRect().y);
  EXPECT_EQ(popup_emulated_rect.x, widget()->viewRect().x);
  EXPECT_EQ(popup_emulated_rect.y, widget()->viewRect().y);
}

}  // namespace content
