/* -*- 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 "gtest/gtest.h"
#include "gmock/gmock.h"

#include <string>

#include "AccessibleCaret.h"
#include "AccessibleCaretManager.h"
#include "mozilla/Preferences.h"

using ::testing::_;
using ::testing::DefaultValue;
using ::testing::Eq;
using ::testing::InSequence;
using ::testing::MockFunction;
using ::testing::Return;

// -----------------------------------------------------------------------------
// This file tests CaretStateChanged events and the appearance of the two
// AccessibleCarets manipulated by AccessibleCaretManager.

namespace mozilla {
using dom::CaretChangedReason;

class MOZ_RAII AutoRestoreBoolPref final {
 public:
  AutoRestoreBoolPref(const char* aPref, bool aValue) : mPref(aPref) {
    Preferences::GetBool(mPref, &mOldValue);
    Preferences::SetBool(mPref, aValue);
  }

  ~AutoRestoreBoolPref() { Preferences::SetBool(mPref, mOldValue); }

 private:
  const char* mPref = nullptr;
  bool mOldValue = false;
};

class AccessibleCaretManagerTester : public ::testing::Test {
 public:
  class MockAccessibleCaret : public AccessibleCaret {
   public:
    MockAccessibleCaret() : AccessibleCaret(nullptr) {}

    void SetAppearance(Appearance aAppearance) override {
      // A simplified version without touching CaretElement().
      mAppearance = aAppearance;
    }

    MOCK_METHOD2(SetPosition,
                 PositionChangedResult(nsIFrame* aFrame, int32_t aOffset));

  };  // class MockAccessibleCaret

  class MockAccessibleCaretManager : public AccessibleCaretManager {
   public:
    using CaretMode = AccessibleCaretManager::CaretMode;
    using AccessibleCaretManager::HideCaretsAndDispatchCaretStateChangedEvent;
    using AccessibleCaretManager::UpdateCarets;

    MockAccessibleCaretManager()
        : AccessibleCaretManager(nullptr,
                                 Carets{MakeUnique<MockAccessibleCaret>(),
                                        MakeUnique<MockAccessibleCaret>()}) {}

    MockAccessibleCaret& FirstCaret() {
      return static_cast<MockAccessibleCaret&>(*mCarets.GetFirst());
    }

    MockAccessibleCaret& SecondCaret() {
      return static_cast<MockAccessibleCaret&>(*mCarets.GetSecond());
    }

    bool CompareTreePosition(nsIFrame* aStartFrame,
                             nsIFrame* aEndFrame) const override {
      return true;
    }

    bool IsCaretDisplayableInCursorMode(
        nsIFrame** aOutFrame = nullptr,
        int32_t* aOutOffset = nullptr) const override {
      return true;
    }

    bool UpdateCaretsForOverlappingTilt() override { return true; }

    void UpdateCaretsForAlwaysTilt(const nsIFrame* aStartFrame,
                                   const nsIFrame* aEndFrame) override {
      if (mCarets.GetFirst()->IsVisuallyVisible()) {
        mCarets.GetFirst()->SetAppearance(Appearance::Left);
      }
      if (mCarets.GetSecond()->IsVisuallyVisible()) {
        mCarets.GetSecond()->SetAppearance(Appearance::Right);
      }
    }

    Terminated IsTerminated() const override { return Terminated::No; }
    bool IsScrollStarted() const { return mIsScrollStarted; }

    Terminated MaybeFlushLayout() override { return Terminated::No; }

    MOCK_CONST_METHOD0(GetCaretMode, CaretMode());
    MOCK_METHOD2(DispatchCaretStateChangedEvent,
                 void(CaretChangedReason aReason, const nsPoint* aPoint));
    MOCK_CONST_METHOD1(HasNonEmptyTextContent, bool(nsINode* aNode));

  };  // class MockAccessibleCaretManager

  using Appearance = AccessibleCaret::Appearance;
  using PositionChangedResult = AccessibleCaret::PositionChangedResult;
  using CaretMode = MockAccessibleCaretManager::CaretMode;

  AccessibleCaretManagerTester() {
    DefaultValue<CaretMode>::Set(CaretMode::None);
    DefaultValue<PositionChangedResult>::Set(PositionChangedResult::NotChanged);

    EXPECT_CALL(mManager.FirstCaret(), SetPosition(_, _))
        .WillRepeatedly(Return(PositionChangedResult::Position));

    EXPECT_CALL(mManager.SecondCaret(), SetPosition(_, _))
        .WillRepeatedly(Return(PositionChangedResult::Position));
  }

  AccessibleCaret::Appearance FirstCaretAppearance() {
    return mManager.FirstCaret().GetAppearance();
  }

  AccessibleCaret::Appearance SecondCaretAppearance() {
    return mManager.SecondCaret().GetAppearance();
  }

  // Member variables
  MockAccessibleCaretManager mManager;

};  // class AccessibleCaretManagerTester

TEST_F(AccessibleCaretManagerTester, TestUpdatesInSelectionMode)
MOZ_CAN_RUN_SCRIPT_FOR_DEFINITION {
  // Set default preference.
  AutoRestoreBoolPref savedPref("layout.accessiblecaret.always_tilt", false);

  EXPECT_CALL(mManager, GetCaretMode())
      .WillRepeatedly(Return(CaretMode::Selection));

  EXPECT_CALL(mManager, DispatchCaretStateChangedEvent(
                            CaretChangedReason::Updateposition, nullptr))
      .Times(3);

  mManager.UpdateCarets();
  EXPECT_EQ(FirstCaretAppearance(), Appearance::Normal);
  EXPECT_EQ(SecondCaretAppearance(), Appearance::Normal);

  mManager.OnReflow();
  EXPECT_EQ(FirstCaretAppearance(), Appearance::Normal);
  EXPECT_EQ(SecondCaretAppearance(), Appearance::Normal);

  mManager.OnScrollPositionChanged();
  EXPECT_EQ(FirstCaretAppearance(), Appearance::Normal);
  EXPECT_EQ(SecondCaretAppearance(), Appearance::Normal);
}

TEST_F(AccessibleCaretManagerTester, TestSingleTapOnNonEmptyInput)
MOZ_CAN_RUN_SCRIPT_FOR_DEFINITION {
  EXPECT_CALL(mManager, GetCaretMode())
      .WillRepeatedly(Return(CaretMode::Cursor));

  EXPECT_CALL(mManager, HasNonEmptyTextContent(_)).WillRepeatedly(Return(true));

  MockFunction<void(std::string aCheckPointName)> check;
  {
    InSequence dummy;

    EXPECT_CALL(mManager, DispatchCaretStateChangedEvent(
                              CaretChangedReason::Updateposition, nullptr))
        .Times(1);
    EXPECT_CALL(check, Call("update"));

    EXPECT_CALL(mManager, DispatchCaretStateChangedEvent(
                              CaretChangedReason::Visibilitychange, nullptr))
        .Times(1);
    EXPECT_CALL(check, Call("mouse down"));

    EXPECT_CALL(mManager, DispatchCaretStateChangedEvent(_, nullptr)).Times(0);
    EXPECT_CALL(check, Call("reflow"));

    EXPECT_CALL(mManager, DispatchCaretStateChangedEvent(_, nullptr)).Times(0);
    EXPECT_CALL(check, Call("blur"));

    EXPECT_CALL(mManager, DispatchCaretStateChangedEvent(
                              CaretChangedReason::Updateposition, nullptr))
        .Times(1);
    EXPECT_CALL(check, Call("mouse up"));

    EXPECT_CALL(mManager, DispatchCaretStateChangedEvent(
                              CaretChangedReason::Updateposition, nullptr))
        .Times(1);
    EXPECT_CALL(check, Call("reflow2"));

    EXPECT_CALL(mManager, DispatchCaretStateChangedEvent(
                              CaretChangedReason::Updateposition, nullptr))
        .Times(1);
  }

  // Simulate a single tap on a non-empty input.
  mManager.UpdateCarets();
  EXPECT_EQ(FirstCaretAppearance(), Appearance::Normal);
  check.Call("update");

  mManager.OnSelectionChanged(nullptr, nullptr,
                              nsISelectionListener::DRAG_REASON |
                                  nsISelectionListener::MOUSEDOWN_REASON);
  EXPECT_EQ(FirstCaretAppearance(), Appearance::None);
  check.Call("mouse down");

  mManager.OnReflow();
  EXPECT_EQ(FirstCaretAppearance(), Appearance::None);
  check.Call("reflow");

  mManager.OnBlur();
  EXPECT_EQ(FirstCaretAppearance(), Appearance::None);
  check.Call("blur");

  mManager.OnSelectionChanged(nullptr, nullptr,
                              nsISelectionListener::MOUSEUP_REASON);
  EXPECT_EQ(FirstCaretAppearance(), Appearance::Normal);
  check.Call("mouse up");

  mManager.OnReflow();
  EXPECT_EQ(FirstCaretAppearance(), Appearance::Normal);
  check.Call("reflow2");

  mManager.OnScrollPositionChanged();
  EXPECT_EQ(FirstCaretAppearance(), Appearance::Normal);
}

TEST_F(AccessibleCaretManagerTester, TestSingleTapOnEmptyInput)
MOZ_CAN_RUN_SCRIPT_FOR_DEFINITION {
  // Set default preference.
  AutoRestoreBoolPref savedPref(
      "layout.accessiblecaret.caret_shown_when_long_tapping_on_empty_content",
      false);

  EXPECT_CALL(mManager, GetCaretMode())
      .WillRepeatedly(Return(CaretMode::Cursor));

  EXPECT_CALL(mManager, HasNonEmptyTextContent(_))
      .WillRepeatedly(Return(false));

  MockFunction<void(std::string aCheckPointName)> check;
  {
    InSequence dummy;

    EXPECT_CALL(mManager, DispatchCaretStateChangedEvent(
                              CaretChangedReason::Updateposition, nullptr))
        .Times(1);
    EXPECT_CALL(check, Call("update"));

    EXPECT_CALL(mManager, DispatchCaretStateChangedEvent(
                              CaretChangedReason::Visibilitychange, nullptr))
        .Times(1);
    EXPECT_CALL(check, Call("mouse down"));

    EXPECT_CALL(mManager, DispatchCaretStateChangedEvent(_, nullptr)).Times(0);
    EXPECT_CALL(check, Call("reflow"));

    EXPECT_CALL(mManager, DispatchCaretStateChangedEvent(_, nullptr)).Times(0);
    EXPECT_CALL(check, Call("blur"));

    EXPECT_CALL(mManager, DispatchCaretStateChangedEvent(
                              CaretChangedReason::Updateposition, nullptr))
        .Times(1);
    EXPECT_CALL(check, Call("mouse up"));

    EXPECT_CALL(mManager, DispatchCaretStateChangedEvent(
                              CaretChangedReason::Updateposition, nullptr))
        .Times(1);
    EXPECT_CALL(check, Call("reflow2"));

    EXPECT_CALL(mManager, DispatchCaretStateChangedEvent(
                              CaretChangedReason::Updateposition, nullptr))
        .Times(1);
  }

  // Simulate a single tap on an empty input.
  mManager.UpdateCarets();
  EXPECT_EQ(FirstCaretAppearance(), Appearance::NormalNotShown);
  check.Call("update");

  mManager.OnSelectionChanged(nullptr, nullptr,
                              nsISelectionListener::DRAG_REASON |
                                  nsISelectionListener::MOUSEDOWN_REASON);
  EXPECT_EQ(FirstCaretAppearance(), Appearance::None);
  check.Call("mouse down");

  mManager.OnReflow();
  EXPECT_EQ(FirstCaretAppearance(), Appearance::None);
  check.Call("reflow");

  mManager.OnBlur();
  EXPECT_EQ(FirstCaretAppearance(), Appearance::None);
  check.Call("blur");

  mManager.OnSelectionChanged(nullptr, nullptr,
                              nsISelectionListener::MOUSEUP_REASON);
  EXPECT_EQ(FirstCaretAppearance(), Appearance::NormalNotShown);
  check.Call("mouse up");

  mManager.OnReflow();
  EXPECT_EQ(FirstCaretAppearance(), Appearance::NormalNotShown);
  check.Call("reflow2");

  mManager.OnScrollPositionChanged();
  EXPECT_EQ(FirstCaretAppearance(), Appearance::NormalNotShown);
}

TEST_F(AccessibleCaretManagerTester, TestTypingAtEndOfInput)
MOZ_CAN_RUN_SCRIPT_FOR_DEFINITION {
  EXPECT_CALL(mManager, GetCaretMode())
      .WillRepeatedly(Return(CaretMode::Cursor));

  EXPECT_CALL(mManager, HasNonEmptyTextContent(_)).WillRepeatedly(Return(true));

  MockFunction<void(std::string aCheckPointName)> check;
  {
    InSequence dummy;

    EXPECT_CALL(mManager, DispatchCaretStateChangedEvent(
                              CaretChangedReason::Updateposition, nullptr))
        .Times(1);
    EXPECT_CALL(check, Call("update"));

    EXPECT_CALL(mManager, DispatchCaretStateChangedEvent(
                              CaretChangedReason::Visibilitychange, nullptr))
        .Times(1);
    EXPECT_CALL(check, Call("keyboard"));

    // No CaretStateChanged events should be dispatched since the caret has
    // being hidden in cursor mode.
    EXPECT_CALL(mManager, DispatchCaretStateChangedEvent(_, nullptr)).Times(0);
  }

  // Simulate typing the end of the input.
  mManager.UpdateCarets();
  EXPECT_EQ(FirstCaretAppearance(), Appearance::Normal);
  check.Call("update");

  mManager.OnKeyboardEvent();
  EXPECT_EQ(FirstCaretAppearance(), Appearance::None);
  check.Call("keyboard");

  mManager.OnSelectionChanged(nullptr, nullptr,
                              nsISelectionListener::NO_REASON);
  EXPECT_EQ(FirstCaretAppearance(), Appearance::None);

  mManager.OnScrollPositionChanged();
  EXPECT_EQ(FirstCaretAppearance(), Appearance::None);
}

TEST_F(AccessibleCaretManagerTester, TestScrollInSelectionMode)
MOZ_CAN_RUN_SCRIPT_FOR_DEFINITION {
  // Set default preference.
  AutoRestoreBoolPref savedPref("layout.accessiblecaret.always_tilt", false);

  EXPECT_CALL(mManager, GetCaretMode())
      .WillRepeatedly(Return(CaretMode::Selection));

  MockFunction<void(std::string aCheckPointName)> check;
  {
    InSequence dummy;

    // Initially, first caret is out of scrollport, and second caret is visible.
    EXPECT_CALL(mManager.FirstCaret(), SetPosition(_, _))
        .WillOnce(Return(PositionChangedResult::Invisible));

    EXPECT_CALL(mManager, DispatchCaretStateChangedEvent(
                              CaretChangedReason::Updateposition, nullptr));
    EXPECT_CALL(check, Call("updatecarets"));

    EXPECT_CALL(mManager, DispatchCaretStateChangedEvent(
                              CaretChangedReason::Scroll, nullptr));
    EXPECT_CALL(check, Call("scrollstart1"));

    EXPECT_CALL(mManager, DispatchCaretStateChangedEvent(
                              CaretChangedReason::Updateposition, nullptr));
    EXPECT_CALL(check, Call("reflow1"));

    // After scroll ended, first caret is visible and second caret is out of
    // scroll port.
    EXPECT_CALL(mManager.SecondCaret(), SetPosition(_, _))
        .WillOnce(Return(PositionChangedResult::Invisible));

    EXPECT_CALL(mManager, DispatchCaretStateChangedEvent(
                              CaretChangedReason::Updateposition, nullptr));
    EXPECT_CALL(check, Call("scrollend1"));

    EXPECT_CALL(mManager, DispatchCaretStateChangedEvent(
                              CaretChangedReason::Scroll, nullptr));
    EXPECT_CALL(check, Call("scrollstart2"));

    EXPECT_CALL(mManager, DispatchCaretStateChangedEvent(
                              CaretChangedReason::Updateposition, nullptr));
    EXPECT_CALL(check, Call("reflow2"));

    // After the scroll ended, both carets are visible.
    EXPECT_CALL(mManager, DispatchCaretStateChangedEvent(
                              CaretChangedReason::Updateposition, nullptr));
    EXPECT_CALL(check, Call("scrollend2"));
  }

  mManager.UpdateCarets();
  EXPECT_EQ(FirstCaretAppearance(), Appearance::NormalNotShown);
  EXPECT_EQ(SecondCaretAppearance(), Appearance::Normal);
  check.Call("updatecarets");

  mManager.OnScrollStart();
  EXPECT_EQ(FirstCaretAppearance(), Appearance::NormalNotShown);
  EXPECT_EQ(SecondCaretAppearance(), Appearance::Normal);
  check.Call("scrollstart1");

  mManager.OnReflow();
  EXPECT_EQ(FirstCaretAppearance(), Appearance::NormalNotShown);
  EXPECT_EQ(SecondCaretAppearance(), Appearance::Normal);
  check.Call("reflow1");

  mManager.OnScrollEnd();
  EXPECT_EQ(FirstCaretAppearance(), Appearance::Normal);
  EXPECT_EQ(SecondCaretAppearance(), Appearance::NormalNotShown);
  check.Call("scrollend1");

  mManager.OnScrollStart();
  EXPECT_EQ(FirstCaretAppearance(), Appearance::Normal);
  EXPECT_EQ(SecondCaretAppearance(), Appearance::NormalNotShown);
  check.Call("scrollstart2");

  mManager.OnReflow();
  EXPECT_EQ(FirstCaretAppearance(), Appearance::Normal);
  EXPECT_EQ(SecondCaretAppearance(), Appearance::NormalNotShown);
  check.Call("reflow2");

  mManager.OnScrollEnd();
  EXPECT_EQ(FirstCaretAppearance(), Appearance::Normal);
  EXPECT_EQ(SecondCaretAppearance(), Appearance::Normal);
  check.Call("scrollend2");
}

TEST_F(AccessibleCaretManagerTester,
       TestScrollInSelectionModeWithAlwaysTiltPref)
MOZ_CAN_RUN_SCRIPT_FOR_DEFINITION {
  // Simulate Firefox Android preference.
  AutoRestoreBoolPref savedPref("layout.accessiblecaret.always_tilt", true);

  EXPECT_CALL(mManager, GetCaretMode())
      .WillRepeatedly(Return(CaretMode::Selection));

  MockFunction<void(std::string aCheckPointName)> check;
  {
    InSequence dummy;

    // Initially, first caret is out of scrollport, and second caret is visible.
    EXPECT_CALL(mManager.FirstCaret(), SetPosition(_, _))
        .WillOnce(Return(PositionChangedResult::Invisible));

    EXPECT_CALL(mManager, DispatchCaretStateChangedEvent(
                              CaretChangedReason::Updateposition, nullptr));
    EXPECT_CALL(check, Call("updatecarets"));

    EXPECT_CALL(mManager, DispatchCaretStateChangedEvent(
                              CaretChangedReason::Scroll, nullptr));
    EXPECT_CALL(check, Call("scrollstart1"));

    EXPECT_CALL(mManager, DispatchCaretStateChangedEvent(_, nullptr)).Times(0);
    EXPECT_CALL(check, Call("scrollPositionChanged1"));

    EXPECT_CALL(mManager, DispatchCaretStateChangedEvent(
                              CaretChangedReason::Updateposition, nullptr));
    EXPECT_CALL(check, Call("reflow1"));

    // After scroll ended, first caret is visible and second caret is out of
    // scroll port.
    EXPECT_CALL(mManager.SecondCaret(), SetPosition(_, _))
        .WillOnce(Return(PositionChangedResult::Invisible));

    EXPECT_CALL(mManager, DispatchCaretStateChangedEvent(
                              CaretChangedReason::Updateposition, nullptr));
    EXPECT_CALL(check, Call("scrollend1"));

    EXPECT_CALL(mManager, DispatchCaretStateChangedEvent(
                              CaretChangedReason::Scroll, nullptr));
    EXPECT_CALL(check, Call("scrollstart2"));

    EXPECT_CALL(mManager, DispatchCaretStateChangedEvent(_, nullptr)).Times(0);
    EXPECT_CALL(check, Call("scrollPositionChanged2"));

    EXPECT_CALL(mManager, DispatchCaretStateChangedEvent(
                              CaretChangedReason::Updateposition, nullptr));
    EXPECT_CALL(check, Call("reflow2"));

    // After the scroll ended, both carets are visible.
    EXPECT_CALL(mManager, DispatchCaretStateChangedEvent(
                              CaretChangedReason::Updateposition, nullptr));
    EXPECT_CALL(check, Call("scrollend2"));
  }

  mManager.UpdateCarets();
  EXPECT_EQ(FirstCaretAppearance(), Appearance::NormalNotShown);
  EXPECT_EQ(SecondCaretAppearance(), Appearance::Right);
  check.Call("updatecarets");

  mManager.OnScrollStart();
  EXPECT_EQ(FirstCaretAppearance(), Appearance::NormalNotShown);
  EXPECT_EQ(SecondCaretAppearance(), Appearance::Right);
  check.Call("scrollstart1");

  mManager.OnScrollPositionChanged();
  EXPECT_EQ(FirstCaretAppearance(), Appearance::NormalNotShown);
  EXPECT_EQ(SecondCaretAppearance(), Appearance::Right);
  check.Call("scrollPositionChanged1");

  mManager.OnReflow();
  EXPECT_EQ(FirstCaretAppearance(), Appearance::NormalNotShown);
  EXPECT_EQ(SecondCaretAppearance(), Appearance::Right);
  check.Call("reflow1");

  mManager.OnScrollEnd();
  EXPECT_EQ(FirstCaretAppearance(), Appearance::Left);
  EXPECT_EQ(SecondCaretAppearance(), Appearance::NormalNotShown);
  check.Call("scrollend1");

  mManager.OnScrollStart();
  EXPECT_EQ(FirstCaretAppearance(), Appearance::Left);
  EXPECT_EQ(SecondCaretAppearance(), Appearance::NormalNotShown);
  check.Call("scrollstart2");

  mManager.OnScrollPositionChanged();
  EXPECT_EQ(FirstCaretAppearance(), Appearance::Left);
  EXPECT_EQ(SecondCaretAppearance(), Appearance::NormalNotShown);
  check.Call("scrollPositionChanged2");

  mManager.OnReflow();
  EXPECT_EQ(FirstCaretAppearance(), Appearance::Left);
  EXPECT_EQ(SecondCaretAppearance(), Appearance::NormalNotShown);
  check.Call("reflow2");

  mManager.OnScrollEnd();
  EXPECT_EQ(FirstCaretAppearance(), Appearance::Left);
  EXPECT_EQ(SecondCaretAppearance(), Appearance::Right);
  check.Call("scrollend2");
}

TEST_F(AccessibleCaretManagerTester, TestScrollInCursorModeWhenLogicallyVisible)
MOZ_CAN_RUN_SCRIPT_FOR_DEFINITION {
  EXPECT_CALL(mManager, GetCaretMode())
      .WillRepeatedly(Return(CaretMode::Cursor));

  EXPECT_CALL(mManager, HasNonEmptyTextContent(_)).WillRepeatedly(Return(true));

  MockFunction<void(std::string aCheckPointName)> check;
  {
    InSequence dummy;

    EXPECT_CALL(mManager, DispatchCaretStateChangedEvent(
                              CaretChangedReason::Updateposition, nullptr))
        .Times(1);
    EXPECT_CALL(check, Call("updatecarets"));

    EXPECT_CALL(mManager, DispatchCaretStateChangedEvent(
                              CaretChangedReason::Scroll, nullptr))
        .Times(1);
    EXPECT_CALL(check, Call("scrollstart1"));

    // After scroll ended, the caret is out of scroll port.
    EXPECT_CALL(mManager.FirstCaret(), SetPosition(_, _))
        .WillRepeatedly(Return(PositionChangedResult::Invisible));
    EXPECT_CALL(mManager, DispatchCaretStateChangedEvent(
                              CaretChangedReason::Updateposition, nullptr))
        .Times(1);
    EXPECT_CALL(check, Call("scrollend1"));

    EXPECT_CALL(mManager, DispatchCaretStateChangedEvent(
                              CaretChangedReason::Scroll, nullptr))
        .Times(1);
    EXPECT_CALL(check, Call("scrollstart2"));

    // After scroll ended, the caret is visible again.
    EXPECT_CALL(mManager.FirstCaret(), SetPosition(_, _))
        .WillRepeatedly(Return(PositionChangedResult::Position));
    EXPECT_CALL(mManager, DispatchCaretStateChangedEvent(
                              CaretChangedReason::Updateposition, nullptr))
        .Times(1);
    EXPECT_CALL(check, Call("scrollend2"));
  }

  mManager.UpdateCarets();
  EXPECT_EQ(FirstCaretAppearance(), Appearance::Normal);
  check.Call("updatecarets");

  mManager.OnScrollStart();
  EXPECT_EQ(FirstCaretAppearance(), Appearance::Normal);
  check.Call("scrollstart1");

  mManager.OnScrollEnd();
  EXPECT_EQ(FirstCaretAppearance(), Appearance::NormalNotShown);
  check.Call("scrollend1");

  mManager.OnScrollStart();
  EXPECT_EQ(FirstCaretAppearance(), Appearance::NormalNotShown);
  check.Call("scrollstart2");

  mManager.OnScrollEnd();
  EXPECT_EQ(FirstCaretAppearance(), Appearance::Normal);
  check.Call("scrollend2");
}

TEST_F(AccessibleCaretManagerTester, TestScrollInCursorModeWhenHidden)
MOZ_CAN_RUN_SCRIPT_FOR_DEFINITION {
  EXPECT_CALL(mManager, GetCaretMode())
      .WillRepeatedly(Return(CaretMode::Cursor));

  EXPECT_CALL(mManager, HasNonEmptyTextContent(_)).WillRepeatedly(Return(true));

  MockFunction<void(std::string aCheckPointName)> check;
  {
    InSequence dummy;

    EXPECT_CALL(mManager, DispatchCaretStateChangedEvent(
                              CaretChangedReason::Updateposition, nullptr))
        .Times(1);
    EXPECT_CALL(check, Call("updatecarets"));

    EXPECT_CALL(mManager, DispatchCaretStateChangedEvent(
                              CaretChangedReason::Visibilitychange, nullptr))
        .Times(1);
    EXPECT_CALL(check, Call("hidecarets"));

    // After scroll ended, the caret is out of scroll port.
    EXPECT_CALL(mManager.FirstCaret(), SetPosition(_, _))
        .WillRepeatedly(Return(PositionChangedResult::Invisible));
    EXPECT_CALL(check, Call("scrollend1"));

    // After scroll ended, the caret is visible again.
    EXPECT_CALL(mManager.FirstCaret(), SetPosition(_, _))
        .WillRepeatedly(Return(PositionChangedResult::Position));
    EXPECT_CALL(check, Call("scrollend2"));
  }

  mManager.UpdateCarets();
  EXPECT_EQ(FirstCaretAppearance(), Appearance::Normal);
  check.Call("updatecarets");

  mManager.HideCaretsAndDispatchCaretStateChangedEvent();
  EXPECT_EQ(FirstCaretAppearance(), Appearance::None);
  check.Call("hidecarets");

  mManager.OnScrollStart();
  EXPECT_EQ(FirstCaretAppearance(), Appearance::None);

  mManager.OnScrollEnd();
  EXPECT_EQ(FirstCaretAppearance(), Appearance::None);
  check.Call("scrollend1");

  mManager.OnScrollStart();
  EXPECT_EQ(FirstCaretAppearance(), Appearance::None);

  mManager.OnScrollEnd();
  EXPECT_EQ(FirstCaretAppearance(), Appearance::None);
  check.Call("scrollend2");
}

TEST_F(AccessibleCaretManagerTester, TestScrollInCursorModeOnEmptyContent)
MOZ_CAN_RUN_SCRIPT_FOR_DEFINITION {
  // Set default preference.
  AutoRestoreBoolPref savedPref(
      "layout.accessiblecaret.caret_shown_when_long_tapping_on_empty_content",
      false);

  EXPECT_CALL(mManager, GetCaretMode())
      .WillRepeatedly(Return(CaretMode::Cursor));

  EXPECT_CALL(mManager, HasNonEmptyTextContent(_))
      .WillRepeatedly(Return(false));

  MockFunction<void(std::string aCheckPointName)> check;
  {
    InSequence dummy;

    EXPECT_CALL(mManager, DispatchCaretStateChangedEvent(
                              CaretChangedReason::Updateposition, nullptr));
    EXPECT_CALL(check, Call("updatecarets"));

    EXPECT_CALL(mManager, DispatchCaretStateChangedEvent(
                              CaretChangedReason::Scroll, nullptr));
    EXPECT_CALL(check, Call("scrollstart1"));

    EXPECT_CALL(mManager.FirstCaret(), SetPosition(_, _))
        .WillOnce(Return(PositionChangedResult::Invisible));
    EXPECT_CALL(mManager, DispatchCaretStateChangedEvent(
                              CaretChangedReason::Updateposition, nullptr));
    EXPECT_CALL(check, Call("scrollend1"));

    EXPECT_CALL(mManager, DispatchCaretStateChangedEvent(
                              CaretChangedReason::Scroll, nullptr));
    EXPECT_CALL(check, Call("scrollstart2"));

    EXPECT_CALL(mManager, DispatchCaretStateChangedEvent(
                              CaretChangedReason::Updateposition, nullptr));
    EXPECT_CALL(check, Call("scrollend2"));

    EXPECT_CALL(mManager, DispatchCaretStateChangedEvent(
                              CaretChangedReason::Scroll, nullptr));
    EXPECT_CALL(check, Call("scrollstart3"));

    EXPECT_CALL(mManager, DispatchCaretStateChangedEvent(
                              CaretChangedReason::Updateposition, nullptr));
    EXPECT_CALL(check, Call("scrollend3"));
  }

  // Simulate a pinch-zoom operation before tapping on an empty content.
  mManager.OnScrollStart();
  mManager.OnScrollEnd();
  EXPECT_EQ(mManager.IsScrollStarted(), false);

  // Simulate a single tap on an empty content.
  mManager.UpdateCarets();
  EXPECT_EQ(FirstCaretAppearance(), Appearance::NormalNotShown);
  check.Call("updatecarets");

  // Scroll the caret to be out of the viewport.
  mManager.OnScrollStart();
  check.Call("scrollstart1");
  mManager.OnScrollEnd();
  EXPECT_EQ(FirstCaretAppearance(), Appearance::NormalNotShown);
  check.Call("scrollend1");

  // Scroll the caret into the viewport.
  mManager.OnScrollStart();
  check.Call("scrollstart2");
  mManager.OnScrollEnd();
  EXPECT_EQ(FirstCaretAppearance(), Appearance::NormalNotShown);
  check.Call("scrollend2");

  // Scroll the caret within the viewport.
  mManager.OnScrollStart();
  check.Call("scrollstart3");
  mManager.OnScrollEnd();
  EXPECT_EQ(FirstCaretAppearance(), Appearance::NormalNotShown);
  check.Call("scrollend3");
}

TEST_F(AccessibleCaretManagerTester,
       TestScrollInCursorModeWithCaretShownWhenLongTappingOnEmptyContentPref)
MOZ_CAN_RUN_SCRIPT_FOR_DEFINITION {
  // Simulate Firefox Android preference.
  AutoRestoreBoolPref savedPref(
      "layout.accessiblecaret.caret_shown_when_long_tapping_on_empty_content",
      true);

  EXPECT_CALL(mManager, GetCaretMode())
      .WillRepeatedly(Return(CaretMode::Cursor));

  EXPECT_CALL(mManager, HasNonEmptyTextContent(_))
      .WillRepeatedly(Return(false));

  MockFunction<void(std::string aCheckPointName)> check;
  {
    InSequence dummy;

    EXPECT_CALL(mManager, DispatchCaretStateChangedEvent(
                              CaretChangedReason::Updateposition, nullptr));
    EXPECT_CALL(check, Call("singletap updatecarets"));

    EXPECT_CALL(mManager, DispatchCaretStateChangedEvent(
                              CaretChangedReason::Updateposition, nullptr));
    EXPECT_CALL(check, Call("longtap updatecarets"));

    EXPECT_CALL(mManager, DispatchCaretStateChangedEvent(
                              CaretChangedReason::Scroll, nullptr));
    EXPECT_CALL(check, Call("longtap scrollstart1"));

    EXPECT_CALL(mManager.FirstCaret(), SetPosition(_, _))
        .WillOnce(Return(PositionChangedResult::Invisible));
    EXPECT_CALL(mManager, DispatchCaretStateChangedEvent(
                              CaretChangedReason::Updateposition, nullptr));
    EXPECT_CALL(check, Call("longtap scrollend1"));

    EXPECT_CALL(mManager, DispatchCaretStateChangedEvent(
                              CaretChangedReason::Scroll, nullptr));
    EXPECT_CALL(check, Call("longtap scrollstart2"));

    EXPECT_CALL(mManager, DispatchCaretStateChangedEvent(
                              CaretChangedReason::Updateposition, nullptr));
    EXPECT_CALL(check, Call("longtap scrollend2"));

    EXPECT_CALL(mManager, DispatchCaretStateChangedEvent(
                              CaretChangedReason::Scroll, nullptr));
    EXPECT_CALL(check, Call("longtap scrollstart3"));

    EXPECT_CALL(mManager, DispatchCaretStateChangedEvent(
                              CaretChangedReason::Updateposition, nullptr));
    EXPECT_CALL(check, Call("longtap scrollend3"));
  }

  // Simulate a single tap on an empty input.
  mManager.FirstCaret().SetAppearance(Appearance::None);
  mManager.UpdateCarets();
  EXPECT_EQ(FirstCaretAppearance(), Appearance::None);
  check.Call("singletap updatecarets");

  // Scroll the caret within the viewport.
  mManager.OnScrollStart();
  mManager.OnScrollEnd();
  EXPECT_EQ(FirstCaretAppearance(), Appearance::None);

  // Simulate a long tap on an empty input.
  mManager.FirstCaret().SetAppearance(Appearance::Normal);
  mManager.UpdateCarets();
  EXPECT_EQ(FirstCaretAppearance(), Appearance::Normal);
  check.Call("longtap updatecarets");

  // Scroll the caret to be out of the viewport.
  mManager.OnScrollStart();
  check.Call("longtap scrollstart1");
  mManager.OnScrollEnd();
  EXPECT_EQ(FirstCaretAppearance(), Appearance::NormalNotShown);
  check.Call("longtap scrollend1");

  // Scroll the caret into the viewport.
  mManager.OnScrollStart();
  check.Call("longtap scrollstart2");
  mManager.OnScrollPositionChanged();
  mManager.OnScrollEnd();
  EXPECT_EQ(FirstCaretAppearance(), Appearance::Normal);
  check.Call("longtap scrollend2");

  // Scroll the caret within the viewport.
  mManager.OnScrollStart();
  check.Call("longtap scrollstart3");
  mManager.OnScrollPositionChanged();
  mManager.OnScrollEnd();
  EXPECT_EQ(FirstCaretAppearance(), Appearance::Normal);
  check.Call("longtap scrollend3");
}

}  // namespace mozilla
