/* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
/* vim: set ts=8 sts=2 et sw=2 tw=80: */
/* This Source Code Form is subject to the terms of the Mozilla Public
 * License, v. 2.0. If a copy of the MPL was not distributed with this
 * file, You can obtain one at http://mozilla.org/MPL/2.0/. */

#include "APZCTreeManagerTester.h"
#include "APZTestCommon.h"
#include "InputUtils.h"
#include "mozilla/EventForwards.h"
#include "mozilla/layers/LayersTypes.h"
#include <tuple>

using LayersUpdateFlags = AsyncPanZoomController::LayersUpdateFlags;

class APZEventResultTester : public APZCTreeManagerTester {
 protected:
  UniquePtr<ScopedLayerTreeRegistration> registration;

  void UpdateOverscrollBehavior(OverscrollBehavior aX, OverscrollBehavior aY) {
    ModifyFrameMetrics(root, [aX, aY](ScrollMetadata& sm, FrameMetrics& _) {
      OverscrollBehaviorInfo overscroll;
      overscroll.mBehaviorX = aX;
      overscroll.mBehaviorY = aY;
      sm.SetOverscrollBehavior(overscroll);
    });
    UpdateHitTestingTree();
  }

  void SetScrollOffsetOnMainThread(const CSSPoint& aPoint) {
    RefPtr<TestAsyncPanZoomController> apzc = ApzcOf(root);

    ScrollMetadata metadata = apzc->GetScrollMetadata();
    metadata.GetMetrics().SetLayoutScrollOffset(aPoint);
    nsTArray<ScrollPositionUpdate> scrollUpdates;
    scrollUpdates.AppendElement(ScrollPositionUpdate::NewScroll(
        ScrollOrigin::Other, CSSPoint::ToAppUnits(aPoint)));
    metadata.SetScrollUpdates(scrollUpdates);
    metadata.GetMetrics().SetScrollGeneration(
        scrollUpdates.LastElement().GetGeneration());
    apzc->NotifyLayersUpdated(metadata,
                              LayersUpdateFlags{.mIsFirstPaint = false,
                                                .mThisLayerTreeUpdated = true});
  }

  void CreateScrollableRootLayer() {
    const char* treeShape = "x";
    LayerIntRect layerVisibleRects[] = {
        LayerIntRect(0, 0, 100, 100),
    };
    CreateScrollData(treeShape, layerVisibleRects);
    SetScrollableFrameMetrics(root, ScrollableLayerGuid::START_SCROLL_ID,
                              CSSRect(0, 0, 200, 200));
    ModifyFrameMetrics(root, [](ScrollMetadata& sm, FrameMetrics& metrics) {
      metrics.SetIsRootContent(true);
    });
    registration = MakeUnique<ScopedLayerTreeRegistration>(LayersId{0}, mcc);
    UpdateHitTestingTree();
  }

  enum class PreventDefaultFlag { No, Yes };
  std::tuple<APZEventResult, APZHandledResult> TapDispatchToContent(
      const ScreenIntPoint& aPoint, PreventDefaultFlag aPreventDefaultFlag) {
    APZEventResult result =
        Tap(manager, aPoint, TimeDuration::FromMilliseconds(100));

    APZHandledResult delayedAnswer{APZHandledPlace::Invalid, SideBits::eNone,
                                   ScrollDirections()};
    manager->AddInputBlockCallback(
        result.mInputBlockId, [&](uint64_t id, const APZHandledResult& answer) {
          EXPECT_EQ(id, result.mInputBlockId);
          delayedAnswer = answer;
        });
    manager->SetAllowedTouchBehavior(result.mInputBlockId,
                                     {AllowedTouchBehavior::VERTICAL_PAN});
    manager->SetTargetAPZC(result.mInputBlockId, {result.mTargetGuid});
    manager->ContentReceivedInputBlock(
        result.mInputBlockId, aPreventDefaultFlag == PreventDefaultFlag::Yes);
    return {result, delayedAnswer};
  }

  void OverscrollDirectionsWithEventHandlerTest(
      PreventDefaultFlag aPreventDefaultFlag) {
    UpdateHitTestingTree();

    APZHandledPlace expectedPlace =
        aPreventDefaultFlag == PreventDefaultFlag::No
            ? APZHandledPlace::HandledByRoot
            : APZHandledPlace::HandledByContent;
    {
      QueueMockHitResult(ScrollableLayerGuid::START_SCROLL_ID,
                         {CompositorHitTestFlags::eVisibleToHitTest,
                          CompositorHitTestFlags::eIrregularArea});
      auto [result, delayedHandledResult] =
          TapDispatchToContent(ScreenIntPoint(50, 50), aPreventDefaultFlag);
      EXPECT_EQ(result.GetHandledResult(), Nothing());
      EXPECT_EQ(
          delayedHandledResult,
          (APZHandledResult{expectedPlace, SideBits::eBottom | SideBits::eRight,
                            EitherScrollDirection}));
    }

    // overscroll-behavior: contain, contain.
    UpdateOverscrollBehavior(OverscrollBehavior::Contain,
                             OverscrollBehavior::Contain);
    {
      QueueMockHitResult(ScrollableLayerGuid::START_SCROLL_ID,
                         {CompositorHitTestFlags::eVisibleToHitTest,
                          CompositorHitTestFlags::eIrregularArea});
      auto [result, delayedHandledResult] =
          TapDispatchToContent(ScreenIntPoint(50, 50), aPreventDefaultFlag);
      EXPECT_EQ(result.GetHandledResult(), Nothing());
      EXPECT_EQ(
          delayedHandledResult,
          (APZHandledResult{expectedPlace, SideBits::eBottom | SideBits::eRight,
                            ScrollDirections()}));
    }

    // overscroll-behavior: none, none.
    UpdateOverscrollBehavior(OverscrollBehavior::None,
                             OverscrollBehavior::None);
    {
      QueueMockHitResult(ScrollableLayerGuid::START_SCROLL_ID,
                         {CompositorHitTestFlags::eVisibleToHitTest,
                          CompositorHitTestFlags::eIrregularArea});
      auto [result, delayedHandledResult] =
          TapDispatchToContent(ScreenIntPoint(50, 50), aPreventDefaultFlag);
      EXPECT_EQ(result.GetHandledResult(), Nothing());
      EXPECT_EQ(
          delayedHandledResult,
          (APZHandledResult{expectedPlace, SideBits::eBottom | SideBits::eRight,
                            ScrollDirections()}));
    }

    // overscroll-behavior: auto, none.
    UpdateOverscrollBehavior(OverscrollBehavior::Auto,
                             OverscrollBehavior::None);
    {
      QueueMockHitResult(ScrollableLayerGuid::START_SCROLL_ID,
                         {CompositorHitTestFlags::eVisibleToHitTest,
                          CompositorHitTestFlags::eIrregularArea});
      auto [result, delayedHandledResult] =
          TapDispatchToContent(ScreenIntPoint(50, 50), aPreventDefaultFlag);
      EXPECT_EQ(result.GetHandledResult(), Nothing());
      EXPECT_EQ(
          delayedHandledResult,
          (APZHandledResult{expectedPlace, SideBits::eBottom | SideBits::eRight,
                            HorizontalScrollDirection}));
    }

    // overscroll-behavior: none, auto.
    UpdateOverscrollBehavior(OverscrollBehavior::None,
                             OverscrollBehavior::Auto);
    {
      QueueMockHitResult(ScrollableLayerGuid::START_SCROLL_ID,
                         {CompositorHitTestFlags::eVisibleToHitTest,
                          CompositorHitTestFlags::eIrregularArea});
      auto [result, delayedHandledResult] =
          TapDispatchToContent(ScreenIntPoint(50, 50), aPreventDefaultFlag);
      EXPECT_EQ(result.GetHandledResult(), Nothing());
      EXPECT_EQ(
          delayedHandledResult,
          (APZHandledResult{expectedPlace, SideBits::eBottom | SideBits::eRight,
                            VerticalScrollDirection}));
    }
  }

  void ScrollableDirectionsWithEventHandlerTest(
      PreventDefaultFlag aPreventDefaultFlag) {
    UpdateHitTestingTree();

    APZHandledPlace expectedPlace =
        aPreventDefaultFlag == PreventDefaultFlag::No
            ? APZHandledPlace::HandledByRoot
            : APZHandledPlace::HandledByContent;
    {
      QueueMockHitResult(ScrollableLayerGuid::START_SCROLL_ID,
                         {CompositorHitTestFlags::eVisibleToHitTest,
                          CompositorHitTestFlags::eIrregularArea});
      auto [result, delayedHandledResult] =
          TapDispatchToContent(ScreenIntPoint(50, 50), aPreventDefaultFlag);
      EXPECT_EQ(result.GetHandledResult(), Nothing());
      EXPECT_EQ(
          delayedHandledResult,
          (APZHandledResult{expectedPlace, SideBits::eBottom | SideBits::eRight,
                            EitherScrollDirection}));
    }

    // scroll down a bit.
    SetScrollOffsetOnMainThread(CSSPoint(0, 10));
    {
      QueueMockHitResult(ScrollableLayerGuid::START_SCROLL_ID,
                         {CompositorHitTestFlags::eVisibleToHitTest,
                          CompositorHitTestFlags::eIrregularArea});
      auto [result, delayedHandledResult] =
          TapDispatchToContent(ScreenIntPoint(50, 50), aPreventDefaultFlag);
      EXPECT_EQ(result.GetHandledResult(), Nothing());
      EXPECT_EQ(delayedHandledResult,
                (APZHandledResult{
                    expectedPlace,
                    SideBits::eTop | SideBits::eBottom | SideBits::eRight,
                    EitherScrollDirection}));
    }

    // scroll to the bottom edge
    SetScrollOffsetOnMainThread(CSSPoint(0, 100));
    {
      QueueMockHitResult(ScrollableLayerGuid::START_SCROLL_ID,
                         {CompositorHitTestFlags::eVisibleToHitTest,
                          CompositorHitTestFlags::eIrregularArea});
      auto [result, delayedHandledResult] =
          TapDispatchToContent(ScreenIntPoint(50, 50), aPreventDefaultFlag);
      EXPECT_EQ(result.GetHandledResult(), Nothing());
      EXPECT_EQ(
          delayedHandledResult,
          (APZHandledResult{expectedPlace, SideBits::eRight | SideBits::eTop,
                            EitherScrollDirection}));
    }

    // scroll to right a bit.
    SetScrollOffsetOnMainThread(CSSPoint(10, 100));
    {
      QueueMockHitResult(ScrollableLayerGuid::START_SCROLL_ID,
                         {CompositorHitTestFlags::eVisibleToHitTest,
                          CompositorHitTestFlags::eIrregularArea});
      auto [result, delayedHandledResult] =
          TapDispatchToContent(ScreenIntPoint(50, 50), aPreventDefaultFlag);
      EXPECT_EQ(result.GetHandledResult(), Nothing());
      EXPECT_EQ(
          delayedHandledResult,
          (APZHandledResult{expectedPlace,
                            SideBits::eLeft | SideBits::eRight | SideBits::eTop,
                            EitherScrollDirection}));
    }

    // scroll to the right edge.
    SetScrollOffsetOnMainThread(CSSPoint(100, 100));
    {
      QueueMockHitResult(ScrollableLayerGuid::START_SCROLL_ID,
                         {CompositorHitTestFlags::eVisibleToHitTest,
                          CompositorHitTestFlags::eIrregularArea});
      auto [result, delayedHandledResult] =
          TapDispatchToContent(ScreenIntPoint(50, 50), aPreventDefaultFlag);
      EXPECT_EQ(result.GetHandledResult(), Nothing());
      EXPECT_EQ(
          delayedHandledResult,
          (APZHandledResult{expectedPlace, SideBits::eTop | SideBits::eLeft,
                            EitherScrollDirection}));
    }
  }
};

TEST_F(APZEventResultTester, OverscrollDirections) {
  CreateScrollableRootLayer();

  TimeDuration tapDuration = TimeDuration::FromMilliseconds(100);

  // The default value of overscroll-behavior is auto.
  APZEventResult result = Tap(manager, ScreenIntPoint(50, 50), tapDuration);
  EXPECT_EQ(result.GetHandledResult()->mOverscrollDirections,
            EitherScrollDirection);

  // overscroll-behavior: contain, contain.
  UpdateOverscrollBehavior(OverscrollBehavior::Contain,
                           OverscrollBehavior::Contain);
  result = Tap(manager, ScreenIntPoint(50, 50), tapDuration);
  EXPECT_EQ(result.GetHandledResult()->mOverscrollDirections,
            ScrollDirections());

  // overscroll-behavior: none, none.
  UpdateOverscrollBehavior(OverscrollBehavior::None, OverscrollBehavior::None);
  result = Tap(manager, ScreenIntPoint(50, 50), tapDuration);
  EXPECT_EQ(result.GetHandledResult()->mOverscrollDirections,
            ScrollDirections());

  // overscroll-behavior: auto, none.
  UpdateOverscrollBehavior(OverscrollBehavior::Auto, OverscrollBehavior::None);
  result = Tap(manager, ScreenIntPoint(50, 50), tapDuration);
  EXPECT_EQ(result.GetHandledResult()->mOverscrollDirections,
            HorizontalScrollDirection);

  // overscroll-behavior: none, auto.
  UpdateOverscrollBehavior(OverscrollBehavior::None, OverscrollBehavior::Auto);
  result = Tap(manager, ScreenIntPoint(50, 50), tapDuration);
  EXPECT_EQ(result.GetHandledResult()->mOverscrollDirections,
            VerticalScrollDirection);
}

TEST_F(APZEventResultTester, ScrollableDirections) {
  CreateScrollableRootLayer();

  TimeDuration tapDuration = TimeDuration::FromMilliseconds(100);

  APZEventResult result = Tap(manager, ScreenIntPoint(50, 50), tapDuration);
  // scrollable to down/right.
  EXPECT_EQ(result.GetHandledResult()->mScrollableDirections,
            SideBits::eBottom | SideBits::eRight);

  // scroll down a bit.
  SetScrollOffsetOnMainThread(CSSPoint(0, 10));
  result = Tap(manager, ScreenIntPoint(50, 50), tapDuration);
  // also scrollable toward top.
  EXPECT_EQ(result.GetHandledResult()->mScrollableDirections,
            SideBits::eTop | SideBits::eBottom | SideBits::eRight);

  // scroll to the bottom edge
  SetScrollOffsetOnMainThread(CSSPoint(0, 100));
  result = Tap(manager, ScreenIntPoint(50, 50), tapDuration);
  EXPECT_EQ(result.GetHandledResult()->mScrollableDirections,
            SideBits::eRight | SideBits::eTop);

  // scroll to right a bit.
  SetScrollOffsetOnMainThread(CSSPoint(10, 100));
  result = Tap(manager, ScreenIntPoint(50, 50), tapDuration);
  EXPECT_EQ(result.GetHandledResult()->mScrollableDirections,
            SideBits::eLeft | SideBits::eRight | SideBits::eTop);

  // scroll to the right edge.
  SetScrollOffsetOnMainThread(CSSPoint(100, 100));
  result = Tap(manager, ScreenIntPoint(50, 50), tapDuration);
  EXPECT_EQ(result.GetHandledResult()->mScrollableDirections,
            SideBits::eLeft | SideBits::eTop);
}

class APZEventResultTesterMock : public APZEventResultTester {
 public:
  APZEventResultTesterMock() { CreateMockHitTester(); }
};

TEST_F(APZEventResultTesterMock, OverscrollDirectionsWithEventHandler) {
  CreateScrollableRootLayer();

  OverscrollDirectionsWithEventHandlerTest(PreventDefaultFlag::No);
}

TEST_F(APZEventResultTesterMock,
       OverscrollDirectionsWithPreventDefaultEventHandler) {
  CreateScrollableRootLayer();

  OverscrollDirectionsWithEventHandlerTest(PreventDefaultFlag::Yes);
}

TEST_F(APZEventResultTesterMock, ScrollableDirectionsWithEventHandler) {
  CreateScrollableRootLayer();

  ScrollableDirectionsWithEventHandlerTest(PreventDefaultFlag::No);
}

TEST_F(APZEventResultTesterMock,
       ScrollableDirectionsWithPreventDefaultEventHandler) {
  CreateScrollableRootLayer();

  ScrollableDirectionsWithEventHandlerTest(PreventDefaultFlag::Yes);
}

// Test that APZEventResult::GetHandledResult() is correctly
// populated.
TEST_F(APZEventResultTesterMock, HandledByRootApzcFlag) {
  // Create simple layer tree containing a dispatch-to-content region
  // that covers part but not all of its area.
  const char* treeShape = "x";
  LayerIntRect layerVisibleRects[] = {
      LayerIntRect(0, 0, 100, 100),
  };
  CreateScrollData(treeShape, layerVisibleRects);
  SetScrollableFrameMetrics(root, ScrollableLayerGuid::START_SCROLL_ID,
                            CSSRect(0, 0, 100, 200));
  ModifyFrameMetrics(root, [](ScrollMetadata& sm, FrameMetrics& metrics) {
    metrics.SetIsRootContent(true);
  });
  // away from the scrolling container layer.
  registration = MakeUnique<ScopedLayerTreeRegistration>(LayersId{0}, mcc);
  UpdateHitTestingTree();

  // Tap the top half and check that we report that the event was
  // handled by the root APZC.
  QueueMockHitResult(ScrollableLayerGuid::START_SCROLL_ID);
  APZEventResult result =
      TouchDown(manager, ScreenIntPoint(50, 25), mcc->Time());
  TouchUp(manager, ScreenIntPoint(50, 25), mcc->Time());
  EXPECT_EQ(result.GetHandledResult(),
            Some(APZHandledResult{APZHandledPlace::HandledByRoot,
                                  SideBits::eBottom, EitherScrollDirection}));

  // Tap the bottom half and check that we report that we're not
  // sure whether the event was handled by the root APZC.
  QueueMockHitResult(ScrollableLayerGuid::START_SCROLL_ID,
                     {CompositorHitTestFlags::eVisibleToHitTest,
                      CompositorHitTestFlags::eIrregularArea});
  result = TouchDown(manager, ScreenIntPoint(50, 75), mcc->Time());
  TouchUp(manager, ScreenIntPoint(50, 75), mcc->Time());
  EXPECT_EQ(result.GetHandledResult(), Nothing());

  // Register an input block callback that will tell us the
  // delayed answer.
  APZHandledResult delayedAnswer{APZHandledPlace::Invalid, SideBits::eNone,
                                 ScrollDirections()};
  manager->AddInputBlockCallback(
      result.mInputBlockId, [&](uint64_t id, const APZHandledResult& answer) {
        EXPECT_EQ(id, result.mInputBlockId);
        delayedAnswer = answer;
      });

  // Send APZ the relevant notifications to allow it to process the
  // input block.
  manager->SetAllowedTouchBehavior(result.mInputBlockId,
                                   {AllowedTouchBehavior::VERTICAL_PAN});
  manager->SetTargetAPZC(result.mInputBlockId, {result.mTargetGuid});
  manager->ContentReceivedInputBlock(result.mInputBlockId,
                                     /*aPreventDefault=*/false);

  // Check that we received the delayed answer and it is what we expect.
  EXPECT_EQ(delayedAnswer,
            (APZHandledResult{APZHandledPlace::HandledByRoot, SideBits::eBottom,
                              EitherScrollDirection}));

  // Now repeat the tap on the bottom half, but simulate a prevent-default.
  // This time, we expect a delayed answer of `HandledByContent`.
  QueueMockHitResult(ScrollableLayerGuid::START_SCROLL_ID,
                     {CompositorHitTestFlags::eVisibleToHitTest,
                      CompositorHitTestFlags::eIrregularArea});
  result = TouchDown(manager, ScreenIntPoint(50, 75), mcc->Time());
  TouchUp(manager, ScreenIntPoint(50, 75), mcc->Time());
  EXPECT_EQ(result.GetHandledResult(), Nothing());
  manager->AddInputBlockCallback(
      result.mInputBlockId, [&](uint64_t id, const APZHandledResult& answer) {
        EXPECT_EQ(id, result.mInputBlockId);
        delayedAnswer = answer;
      });
  manager->SetAllowedTouchBehavior(result.mInputBlockId,
                                   {AllowedTouchBehavior::VERTICAL_PAN});
  manager->SetTargetAPZC(result.mInputBlockId, {result.mTargetGuid});
  manager->ContentReceivedInputBlock(result.mInputBlockId,
                                     /*aPreventDefault=*/true);
  EXPECT_EQ(delayedAnswer,
            (APZHandledResult{APZHandledPlace::HandledByContent,
                              SideBits::eBottom, EitherScrollDirection}));

  // Shrink the scrollable area, now it's no longer scrollable.
  ModifyFrameMetrics(root, [](ScrollMetadata& sm, FrameMetrics& metrics) {
    metrics.SetScrollableRect(CSSRect(0, 0, 100, 100));
  });
  UpdateHitTestingTree();
  // Now repeat the tap on the bottom half with an event handler.
  // This time, we expect a delayed answer of `Unhandled`.
  QueueMockHitResult(ScrollableLayerGuid::START_SCROLL_ID,
                     {CompositorHitTestFlags::eVisibleToHitTest,
                      CompositorHitTestFlags::eIrregularArea});
  result = TouchDown(manager, ScreenIntPoint(50, 75), mcc->Time());
  TouchUp(manager, ScreenIntPoint(50, 75), mcc->Time());
  EXPECT_EQ(result.GetHandledResult(), Nothing());
  manager->AddInputBlockCallback(
      result.mInputBlockId, [&](uint64_t id, const APZHandledResult& answer) {
        EXPECT_EQ(id, result.mInputBlockId);
        delayedAnswer = answer;
      });
  manager->SetAllowedTouchBehavior(result.mInputBlockId,
                                   {AllowedTouchBehavior::VERTICAL_PAN});
  manager->SetTargetAPZC(result.mInputBlockId, {result.mTargetGuid});
  manager->ContentReceivedInputBlock(result.mInputBlockId,
                                     /*aPreventDefault=*/false);
  EXPECT_EQ(delayedAnswer,
            (APZHandledResult{APZHandledPlace::Unhandled, SideBits::eNone,
                              EitherScrollDirection}));

  // Repeat the tap on the bottom half, with no event handler.
  // Make sure we get an eager answer of `Unhandled`.
  QueueMockHitResult(ScrollableLayerGuid::START_SCROLL_ID);
  result = TouchDown(manager, ScreenIntPoint(50, 75), mcc->Time());
  TouchUp(manager, ScreenIntPoint(50, 75), mcc->Time());
  EXPECT_EQ(result.GetStatus(), nsEventStatus_eIgnore);
  EXPECT_EQ(result.GetHandledResult(),
            Some(APZHandledResult{APZHandledPlace::Unhandled, SideBits::eNone,
                                  EitherScrollDirection}));
}
