910e62b5创建于 1月15日历史提交
// Copyright 2024 The Chromium Authors
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.

#include "pdf/pdf_ink_module.h"

#include <algorithm>
#include <array>
#include <set>
#include <string>
#include <string_view>
#include <vector>

#include "base/check_op.h"
#include "base/containers/contains.h"
#include "base/containers/span.h"
#include "base/containers/to_vector.h"
#include "base/files/file_path.h"
#include "base/strings/to_string.h"
#include "base/test/bind.h"
#include "base/test/metrics/histogram_tester.h"
#include "base/test/scoped_feature_list.h"
#include "base/test/task_environment.h"
#include "base/test/values_test_util.h"
#include "base/time/time.h"
#include "base/types/zip.h"
#include "base/values.h"
#include "pdf/page_orientation.h"
#include "pdf/pdf_caret.h"
#include "pdf/pdf_features.h"
#include "pdf/pdf_ink_brush.h"
#include "pdf/pdf_ink_conversions.h"
#include "pdf/pdf_ink_metrics_handler.h"
#include "pdf/pdf_ink_module_client.h"
#include "pdf/pdf_ink_transform.h"
#include "pdf/pdfium/pdfium_ink_reader.h"
#include "pdf/test/input_event_util.h"
#include "pdf/test/mock_pdf_caret_client.h"
#include "pdf/test/mouse_event_builder.h"
#include "pdf/test/pdf_ink_test_helpers.h"
#include "pdf/test/test_helpers.h"
#include "pdf/ui/thumbnail.h"
#include "printing/units.h"
#include "testing/gmock/include/gmock/gmock.h"
#include "testing/gtest/include/gtest/gtest.h"
#include "third_party/blink/public/common/input/web_input_event.h"
#include "third_party/blink/public/common/input/web_keyboard_event.h"
#include "third_party/blink/public/common/input/web_mouse_event.h"
#include "third_party/blink/public/common/input/web_touch_event.h"
#include "third_party/ink/src/ink/brush/brush.h"
#include "third_party/ink/src/ink/brush/type_matchers.h"
#include "third_party/ink/src/ink/geometry/affine_transform.h"
#include "third_party/ink/src/ink/strokes/input/stroke_input_batch.h"
#include "third_party/ink/src/ink/strokes/input/type_matchers.h"
#include "third_party/skia/include/core/SkBitmap.h"
#include "third_party/skia/include/core/SkColor.h"
#include "ui/base/cursor/cursor.h"
#include "ui/base/cursor/mojom/cursor_type.mojom.h"
#include "ui/events/event_constants.h"
#include "ui/events/keycodes/keyboard_codes.h"
#include "ui/gfx/geometry/point_f.h"
#include "ui/gfx/geometry/rect.h"
#include "ui/gfx/geometry/rect_conversions.h"
#include "ui/gfx/geometry/rect_f.h"
#include "ui/gfx/geometry/size.h"
#include "ui/gfx/geometry/vector2d_f.h"

using testing::_;
using testing::ElementsAre;
using testing::ElementsAreArray;
using testing::Field;
using testing::InSequence;
using testing::NiceMock;
using testing::Pair;
using testing::Pointwise;
using testing::Return;
using testing::SizeIs;

namespace chrome_pdf {

namespace {

// Some commonly used points with InitializeSimpleSinglePageBasicLayout().
constexpr gfx::PointF kLeftVerticalStrokePoint1(10.0f, 15.0f);
constexpr gfx::PointF kLeftVerticalStrokePoint2(10.0f, 35.0f);
constexpr gfx::PointF kRightVerticalStrokePoint1(40.0f, 15.0f);
constexpr gfx::PointF kRightVerticalStrokePoint2(40.0f, 35.0f);

// Constants to support a layout of 2 pages, arranged vertically with a small
// gap between them.
constexpr gfx::RectF kVerticalLayout2Pages[] = {
    gfx::RectF(/*x=*/5.0f,
               /*y=*/5.0f,
               /*width=*/50.0f,
               /*height=*/60.0f),
    gfx::RectF(/*x=*/5.0f,
               /*y=*/70.0f,
               /*width=*/50.0f,
               /*height=*/60.0f),
};

// Some commonly used points in relation to `kVerticalLayout2Pages`.
constexpr gfx::PointF kTwoPageVerticalLayoutPointOutsidePages(10.0f, 0.0f);
constexpr gfx::PointF kTwoPageVerticalLayoutPoint1InsidePage0(10.0f, 10.0f);
constexpr gfx::PointF kTwoPageVerticalLayoutPoint2InsidePage0(15.0f, 15.0f);
constexpr gfx::PointF kTwoPageVerticalLayoutPoint3InsidePage0(20.0f, 15.0f);
constexpr gfx::PointF kTwoPageVerticalLayoutPoint4InsidePage0(10.0f, 20.0f);
constexpr gfx::PointF kTwoPageVerticalLayoutPoint1InsidePage1(10.0f, 75.0f);
constexpr gfx::PointF kTwoPageVerticalLayoutPoint2InsidePage1(15.0f, 80.0f);
constexpr gfx::PointF kTwoPageVerticalLayoutPoint3InsidePage1(20.0f, 80.0f);

// Canonical points after stroking horizontal & vertical lines with some
// commonly used points.
// Horizontal line uses: kTwoPageVerticalLayoutPoint2InsidePage0 to
//                       kTwoPageVerticalLayoutPoint3InsidePage0
//                   or: kTwoPageVerticalLayoutPoint2InsidePage1 to
//                       kTwoPageVerticalLayoutPoint3InsidePage1
// Vertical line uses:   kTwoPageVerticalLayoutPoint1InsidePage0 to
//                       kTwoPageVerticalLayoutPoint4InsidePage0
constexpr gfx::PointF kTwoPageVerticalLayoutHorzLinePoint0Canonical(10.0f,
                                                                    10.0f);
constexpr gfx::PointF kTwoPageVerticalLayoutHorzLinePoint1Canonical(15.0f,
                                                                    10.0f);
constexpr gfx::PointF kTwoPageVerticalLayoutVertLinePoint0Canonical(5.0f, 5.0f);
constexpr gfx::PointF kTwoPageVerticalLayoutVertLinePoint1Canonical(5.0f,
                                                                    15.0f);

// The inputs for a stroke that starts in first page, leaves the bounds of that
// page, but then moves back into the page results in one stroke with two
// segments.
constexpr gfx::PointF kTwoPageVerticalLayoutPageExitAndReentryPoints[] = {
    gfx::PointF(10.0f, 5.0f), gfx::PointF(10.0f, 0.0f),
    gfx::PointF(15.0f, 0.0f), gfx::PointF(15.0f, 5.0f),
    gfx::PointF(15.0f, 10.0f)};
// The two segments created by the inputs above.
constexpr gfx::PointF kTwoPageVerticalLayoutPageExitAndReentrySegment1[] = {
    gfx::PointF(5.0f, 5.0f), gfx::PointF(5.0f, 0.0f)};
constexpr gfx::PointF kTwoPageVerticalLayoutPageExitAndReentrySegment2[] = {
    gfx::PointF(10.0f, 0.0f), gfx::PointF(10.0f, 5.0f),
    gfx::PointF(15.0f, 10.0f)};

// The stroke inputs for vertical and horizontal lines in the pages.  The
// `.time` fields intentionally get a common value, to match the behavior of
// `MouseEventBuilder`.
constexpr auto kTwoPageVerticalLayoutHorzLinePage0Inputs =
    std::to_array<PdfInkInputData>({
        {kTwoPageVerticalLayoutHorzLinePoint0Canonical, base::Seconds(0)},
        {kTwoPageVerticalLayoutHorzLinePoint1Canonical, base::Seconds(0)},
    });
constexpr auto kTwoPageVerticalLayoutVertLinePage0Inputs =
    std::to_array<PdfInkInputData>({
        {kTwoPageVerticalLayoutVertLinePoint0Canonical, base::Seconds(0)},
        {kTwoPageVerticalLayoutVertLinePoint1Canonical, base::Seconds(0)},
    });
constexpr auto kTwoPageVerticalLayoutHorzLinePage1Inputs =
    std::to_array<PdfInkInputData>({
        {kTwoPageVerticalLayoutHorzLinePoint0Canonical, base::Seconds(0)},
        {kTwoPageVerticalLayoutHorzLinePoint1Canonical, base::Seconds(0)},
    });

// Commonly used test brush color. The color corresponds to "Yellow 1" for pen
// brushes and "Light Yellow" for highlighter brushes.
constexpr SkColor kYellow = SkColorSetRGB(0xFD, 0xD6, 0x63);

// Commonly used test brush message params. The color corresponds to "Red 1" for
// pen brushes and "Light Red" for highlighter brushes.
constexpr TestAnnotationBrushMessageParams kRedBrushParams{
    SkColorSetRGB(0xF2, 0x8B, 0x82),
    /*size=*/6.0};

// Matcher for ink::Stroke objects against their expected brush and inputs.
MATCHER_P(InkStrokeEq, expected_brush, "") {
  const auto& [actual_stroke, expected_inputs] = arg;
  const auto brush_matcher = ink::BrushEq(expected_brush);
  const auto input_matcher = ink::StrokeInputBatchEq(expected_inputs);
  return testing::Matches(brush_matcher)(actual_stroke->GetBrush()) &&
         testing::Matches(input_matcher)(actual_stroke->GetInputs());
}

// Matcher for ink::Stroke objects against an expected brush color.
MATCHER_P(InkStrokeBrushColorEq, expected_color, "") {
  return chrome_pdf::GetSkColorFromInkBrush(arg.GetBrush()) == expected_color;
}

// Matcher for ink::Stroke objects against an expected brush size.
MATCHER_P(InkStrokeBrushSizeEq, expected_size, "") {
  return arg.GetBrush().GetSize() == expected_size;
}

// Matcher for ink::Stroke objects against an expected drawing brush type.
// A pen is opaque while a highlighter has transparency, so a drawing
// brush type can be deduced from the ink::Stroke's brush.
MATCHER_P(InkStrokeDrawingBrushTypeEq, expected_type, "") {
  const float opacity = GetOpacityMultiplierFromBrush(arg.GetBrush());
  if (expected_type == PdfInkBrush::Type::kPen) {
    return opacity == 1.0f;
  }

  CHECK_EQ(expected_type, PdfInkBrush::Type::kHighlighter);
  return opacity == 0.4f;
}

// Matcher for cursor with a custom bitmap against expected dimensions.
MATCHER_P(CursorBitmapImageSizeEq, dimensions, "") {
  return arg.type() == ui::mojom::CursorType::kCustom &&
         arg.custom_bitmap().dimensions() == dimensions;
}

// Matcher for keyboard events against the expected key and modifiers.
MATCHER_P2(WebKeyboardEventEq, key, modifiers, "") {
  if (arg.windows_key_code != key) {
    *result_listener << "Expected key: " << key
                     << " actual: " << arg.windows_key_code;
    return false;
  }
  if (arg.GetModifiers() != modifiers) {
    *result_listener << "Expected modifiers: " << modifiers
                     << " actual: " << arg.GetModifiers();
    return false;
  }
  return true;
}

base::Value::Dict CreateGetAnnotationBrushMessage(
    const std::string& brush_type) {
  auto message = base::Value::Dict()
                     .Set("type", "getAnnotationBrush")
                     .Set("messageId", "foo");
  if (!brush_type.empty()) {
    message.Set("brushType", brush_type);
  }
  return message;
}

blink::WebTouchEvent CreateTouchEvent(blink::WebInputEvent::Type type,
                                      base::span<const gfx::PointF> points) {
  CHECK_LE(points.size(), blink::WebTouchEvent::kTouchesLengthCap);

  constexpr int kNoModifiers = 0;
  blink::WebTouchEvent touch_event(
      type, kNoModifiers, blink::WebInputEvent::GetStaticTimeStampForTests());
  for (auto [touch, point] : base::zip(touch_event.touches, points)) {
    touch.SetPositionInWidget(point);
  }
  touch_event.touches_length = points.size();
  return touch_event;
}

blink::WebTouchEvent CreatePenEvent(blink::WebInputEvent::Type type,
                                    base::span<const gfx::PointF> points) {
  blink::WebTouchEvent pen_event = CreateTouchEvent(type, points);
  for (size_t i = 0; i < pen_event.touches_length; ++i) {
    pen_event.touches[i].pointer_type =
        blink::WebPointerProperties::PointerType::kPen;
  }
  return pen_event;
}

class FakeClient : public PdfInkModuleClient {
 public:
  FakeClient() = default;
  FakeClient(const FakeClient&) = delete;
  FakeClient& operator=(const FakeClient&) = delete;
  ~FakeClient() override = default;

  // PdfInkModuleClient:
  MOCK_METHOD(void,
              DiscardStroke,
              (int page_index, InkStrokeId id),
              (override));

  MOCK_METHOD(void,
              ExtendSelectionByPoint,
              (const gfx::PointF& point),
              (override));

  MOCK_METHOD(gfx::Transform,
              GetCanonicalToPdfTransform,
              (int page_index),
              (override));

  MOCK_METHOD(ui::Cursor, GetCursor, (), (override));

  PageOrientation GetOrientation() const override { return orientation_; }

  MOCK_METHOD(PdfCaret*, GetPdfCaret, (), (override));

  MOCK_METHOD(SelectionRectMap, GetSelectionRectMap, (), (override));

  gfx::Size GetThumbnailSize(int page_index) override {
    CHECK_GE(page_index, 0);
    CHECK_LT(static_cast<size_t>(page_index), page_layouts_.size());
    return Thumbnail::CalculateImageSize(page_layouts_[page_index].size(),
                                         /*device_pixel_ratio=*/1);
  }

  gfx::Vector2dF GetViewportOriginOffset() override {
    return viewport_origin_offset_;
  }

  gfx::Rect GetPageContentsRect(int page_index) override {
    CHECK_GE(page_index, 0);
    CHECK_LT(static_cast<size_t>(page_index), page_layouts_.size());
    return gfx::ToEnclosedRect(page_layouts_[page_index]);
  }

  gfx::SizeF GetPageSizeInPoints(int page_index) override {
    CHECK_GE(page_index, 0);
    CHECK_LT(static_cast<size_t>(page_index), page_layouts_.size());
    gfx::SizeF page_size = page_layouts_[page_index].size();
    page_size.Scale(printing::kUnitConversionFactorPixelsToPoints);
    return page_size;
  }

  float GetZoom() const override { return zoom_; }

  void Invalidate(const gfx::Rect& rect) override {
    invalidations_.push_back(rect);
  }

  bool IsPageVisible(int page_index) override {
    return base::Contains(visible_page_indices_, page_index);
  }

  MOCK_METHOD(bool,
              IsSelectableTextOrLinkArea,
              (const gfx::PointF& point),
              (override));

  MOCK_METHOD(PdfInkModuleClient::DocumentV2InkPathShapesMap,
              LoadV2InkPathsFromPdf,
              (),
              (override));

  MOCK_METHOD(void,
              OnTextOrLinkAreaClick,
              (const gfx::PointF& point, int click_count),
              (override));

  MOCK_METHOD(int, PageIndexFromPoint, (const gfx::PointF& point), (override));

  MOCK_METHOD(void, PostMessage, (base::Value::Dict message), (override));

  MOCK_METHOD(void,
              RequestThumbnail,
              (int page_index, SendThumbnailCallback callback),
              (override));

  MOCK_METHOD(void,
              StrokeAdded,
              (int page_index, InkStrokeId id, const ink::Stroke& stroke),
              (override));

  void StrokeFinished(bool modified) override {
    if (modified) {
      ++modified_stroke_finished_count_;
    } else {
      ++unmodified_stroke_finished_count_;
    }
  }

  void StrokeStarted() override { ++stroke_started_count_; }

  MOCK_METHOD(void, UpdateInkCursor, (const ui::Cursor&), (override));

  MOCK_METHOD(void,
              UpdateShapeActive,
              (int page_index, InkModeledShapeId id, bool active),
              (override));

  MOCK_METHOD(void,
              UpdateStrokeActive,
              (int page_index, InkStrokeId id, bool active),
              (override));

  int VisiblePageIndexFromPoint(const gfx::PointF& point) override {
    for (size_t i = 0; i < page_layouts_.size(); ++i) {
      if (IsPageVisible(i) && page_layouts_[i].Contains(point)) {
        return i;
      }
    }

    // Point is not over a visible page in the viewer plane.
    return -1;
  }

  int stroke_started_count() const { return stroke_started_count_; }
  int modified_stroke_finished_count() const {
    return modified_stroke_finished_count_;
  }
  int unmodified_stroke_finished_count() const {
    return unmodified_stroke_finished_count_;
  }

  const std::vector<gfx::Rect>& invalidations() const { return invalidations_; }

  // Provide the sequence of pages and the coordinates and dimensions for how
  // they are laid out in a viewer plane.  It is upon the caller to ensure the
  // positioning makes sense (e.g., pages do not overlap).
  void set_page_layouts(base::span<const gfx::RectF> page_layouts) {
    page_layouts_ = base::ToVector(page_layouts);
  }

  // Marks pages as visible or not. The caller is responsible for making sure
  // the values makes sense.
  void set_page_visibility(int index, bool visible) {
    if (visible) {
      visible_page_indices_.insert(index);
    } else {
      visible_page_indices_.erase(index);
    }
  }

  void set_orientation(PageOrientation orientation) {
    orientation_ = orientation;
  }

  void set_viewport_origin_offset(const gfx::Vector2dF& offset) {
    viewport_origin_offset_ = offset;
  }

  void set_zoom(float zoom) { zoom_ = zoom; }

 private:
  int stroke_started_count_ = 0;
  int modified_stroke_finished_count_ = 0;
  int unmodified_stroke_finished_count_ = 0;
  std::vector<gfx::RectF> page_layouts_;
  std::set<int> visible_page_indices_;
  PageOrientation orientation_ = PageOrientation::kOriginal;
  gfx::Vector2dF viewport_origin_offset_;
  float zoom_ = 1.0f;
  std::vector<gfx::Rect> invalidations_;
};

class MockPdfCaret : public PdfCaret {
 public:
  explicit MockPdfCaret(PdfCaretClient* client) : PdfCaret(client) {}
  MockPdfCaret(const MockPdfCaret&) = delete;
  MockPdfCaret& operator=(const MockPdfCaret&) = delete;
  ~MockPdfCaret() override = default;

  // PdfCaret:
  MOCK_METHOD(bool,
              OnKeyDown,
              (const blink::WebKeyboardEvent& event),
              (override));
};

class PdfInkModuleMetricsTestBase {
 protected:
  static constexpr char kHighlighterColorMetric[] =
      "PDF.Ink2StrokeHighlighterColor";
  static constexpr char kHighlighterSizeMetric[] =
      "PDF.Ink2StrokeHighlighterSize";
  static constexpr char kInputDeviceMetric[] = "PDF.Ink2StrokeInputDeviceType";
  static constexpr char kTypeMetric[] = "PDF.Ink2StrokeBrushType";

  base::HistogramTester& histograms() { return histograms_; }

 private:
  base::HistogramTester histograms_;
};

class PdfInkModuleTest : public testing::TestWithParam<InkTestVariation> {
 public:
  void SetUp() override {
    feature_list_.InitAndEnableFeatureWithParameters(
        chrome_pdf::features::kPdfInk2,
        {{features::kPdfInk2TextAnnotations.name,
          base::ToString(UseTextAnnotations())},
         {features::kPdfInk2TextHighlighting.name,
          base::ToString(UseTextHighlighting())}});
    ink_module_ = std::make_unique<PdfInkModule>(client_);
  }

 protected:
  bool UseTextAnnotations() const { return GetParam().use_text_annotations; }
  bool UseTextHighlighting() const { return GetParam().use_text_highlighting; }

  void EnableDrawAnnotationMode() {
    EXPECT_TRUE(ink_module().OnMessage(
        CreateSetAnnotationModeMessageForTesting(InkAnnotationMode::kDraw)));
    EXPECT_TRUE(ink_module().enabled());
  }

  void VerifyAndClearExpectations() {
    testing::Mock::VerifyAndClearExpectations(this);
  }

  FakeClient& client() { return client_; }
  PdfInkModule& ink_module() { return *ink_module_; }
  const PdfInkModule& ink_module() const { return *ink_module_; }

 private:
  base::test::ScopedFeatureList feature_list_;

  NiceMock<FakeClient> client_;
  std::unique_ptr<PdfInkModule> ink_module_;
};

}  // namespace

TEST_P(PdfInkModuleTest, UnknownMessage) {
  EXPECT_FALSE(
      ink_module().OnMessage(base::Value::Dict().Set("type", "nonInkMessage")));
}

// Verify that a get eraser message gets the eraser parameters.
TEST_P(PdfInkModuleTest, HandleGetAnnotationBrushMessageEraser) {
  EnableDrawAnnotationMode();

  EXPECT_CALL(client(), PostMessage)
      .WillOnce([](const base::Value::Dict& dict) {
        auto expected = base::test::ParseJsonDict(R"({
            "type": "getAnnotationBrushReply",
            "messageId": "foo",
            "data": {
              "type": "eraser",
            },
        })");
        EXPECT_THAT(dict, base::test::DictionaryHasValues(expected));
      });

  EXPECT_TRUE(
      ink_module().OnMessage(CreateGetAnnotationBrushMessage("eraser")));
}

// Verify that a get pen message gets the pen brush parameters.
TEST_P(PdfInkModuleTest, HandleGetAnnotationBrushMessagePen) {
  EnableDrawAnnotationMode();

  EXPECT_CALL(client(), PostMessage)
      .WillOnce([](const base::Value::Dict& dict) {
        auto expected = base::test::ParseJsonDict(R"({
            "type": "getAnnotationBrushReply",
            "messageId": "foo",
            "data": {
              "type": "pen",
              "size": 3.0,
              "color": {
                "r": 0,
                "g": 0,
                "b": 0,
              },
            },
        })");
        EXPECT_THAT(dict, base::test::DictionaryHasValues(expected));
      });

  EXPECT_TRUE(ink_module().OnMessage(CreateGetAnnotationBrushMessage("pen")));
}

// Verify that a get highlighter message gets the highlighter brush parameters.
TEST_P(PdfInkModuleTest, HandleGetAnnotationBrushMessageHighlighter) {
  EnableDrawAnnotationMode();

  EXPECT_CALL(client(), PostMessage)
      .WillOnce([](const base::Value::Dict& dict) {
        auto expected = base::test::ParseJsonDict(R"({
            "type": "getAnnotationBrushReply",
            "messageId": "foo",
            "data": {
              "type": "highlighter",
              "size": 8.0,
              "color": {
                "r": 242,
                "g": 139,
                "b": 130,
              },
            },
        })");
        EXPECT_THAT(dict, base::test::DictionaryHasValues(expected));
      });

  EXPECT_TRUE(
      ink_module().OnMessage(CreateGetAnnotationBrushMessage("highlighter")));
}

// Verify that a get brush message without a parameter gets the default brush
// parameters.
TEST_P(PdfInkModuleTest, HandleGetAnnotationBrushMessageDefault) {
  EnableDrawAnnotationMode();

  EXPECT_CALL(client(), PostMessage)
      .WillOnce([](const base::Value::Dict& dict) {
        auto expected = base::test::ParseJsonDict(R"({
            "type": "getAnnotationBrushReply",
            "messageId": "foo",
            "data": {
              "type": "pen",
              "size": 3.0,
              "color": {
                "r": 0,
                "g": 0,
                "b": 0,
              },
            },
        })");
        EXPECT_THAT(dict, base::test::DictionaryHasValues(expected));
      });

  EXPECT_TRUE(ink_module().OnMessage(CreateGetAnnotationBrushMessage("")));
}

// Verify that a get brush message without a parameter gets the current brush
// parameters if the brush has changed from the default brush.
TEST_P(PdfInkModuleTest, HandleGetAnnotationBrushMessageCurrent) {
  EnableDrawAnnotationMode();

  // Set the brush to eraser.
  EXPECT_TRUE(ink_module().OnMessage(
      CreateSetAnnotationBrushMessageForTesting("eraser", nullptr)));

  EXPECT_CALL(client(), PostMessage)
      .WillOnce([](const base::Value::Dict& dict) {
        auto expected = base::test::ParseJsonDict(R"({
            "type": "getAnnotationBrushReply",
            "messageId": "foo",
            "data": {
              "type": "eraser",
            },
        })");
        EXPECT_THAT(dict, base::test::DictionaryHasValues(expected));
      });

  EXPECT_TRUE(ink_module().OnMessage(CreateGetAnnotationBrushMessage("")));
}

// Verify that a set eraser message sets the annotation brush to an eraser. i.e.
// There is no `PdfInkBrush`.
TEST_P(PdfInkModuleTest, HandleSetAnnotationBrushMessageEraser) {
  EnableDrawAnnotationMode();

  base::Value::Dict message =
      CreateSetAnnotationBrushMessageForTesting("eraser", nullptr);
  EXPECT_TRUE(ink_module().OnMessage(message));

  const PdfInkBrush* brush = ink_module().GetPdfInkBrushForTesting();
  EXPECT_FALSE(brush);
}

// Verify that a set pen message sets the annotation brush to a pen, with the
// given params.
TEST_P(PdfInkModuleTest, HandleSetAnnotationBrushMessagePen) {
  EnableDrawAnnotationMode();

  // Select the "Yellow 1" color.
  TestAnnotationBrushMessageParams message_params{kYellow,
                                                  /*size=*/8.0};
  base::Value::Dict message =
      CreateSetAnnotationBrushMessageForTesting("pen", &message_params);
  EXPECT_TRUE(ink_module().OnMessage(message));

  const PdfInkBrush* brush = ink_module().GetPdfInkBrushForTesting();
  ASSERT_TRUE(brush);

  const ink::Brush& ink_brush = brush->ink_brush();
  EXPECT_EQ(kYellow, GetSkColorFromInkBrush(ink_brush));
  EXPECT_EQ(8.0f, ink_brush.GetSize());
  ASSERT_EQ(1u, ink_brush.CoatCount());
  const ink::BrushCoat& coat = ink_brush.GetCoats()[0];
  EXPECT_EQ(1.0f, coat.tip.corner_rounding);
  EXPECT_EQ(1.0f, GetOpacityMultiplierFromBrush(ink_brush));
}

// Verify that a set highlighter message sets the annotation brush to a
// highlighter, with the given params.
TEST_P(PdfInkModuleTest, HandleSetAnnotationBrushMessageHighlighter) {
  EnableDrawAnnotationMode();

  // Select the "Light Yellow" color.
  TestAnnotationBrushMessageParams message_params{kYellow,
                                                  /*size=*/4.5};
  base::Value::Dict message =
      CreateSetAnnotationBrushMessageForTesting("highlighter", &message_params);
  EXPECT_TRUE(ink_module().OnMessage(message));

  const PdfInkBrush* brush = ink_module().GetPdfInkBrushForTesting();
  ASSERT_TRUE(brush);

  const ink::Brush& ink_brush = brush->ink_brush();
  EXPECT_EQ(kYellow, GetSkColorFromInkBrush(ink_brush));
  EXPECT_EQ(4.5f, ink_brush.GetSize());
  ASSERT_EQ(1u, ink_brush.CoatCount());
  const ink::BrushCoat& coat = ink_brush.GetCoats()[0];
  EXPECT_EQ(0.0f, coat.tip.corner_rounding);
  EXPECT_EQ(0.4f, GetOpacityMultiplierFromBrush(ink_brush));
}

// Verify that brushes with zero color values can be set as the annotation
// brush.
TEST_P(PdfInkModuleTest, HandleSetAnnotationBrushMessageColorZero) {
  EnableDrawAnnotationMode();

  TestAnnotationBrushMessageParams message_params{
      SkColorSetRGB(0x00, 0x00, 0x00),
      /*size=*/4.5};
  base::Value::Dict message =
      CreateSetAnnotationBrushMessageForTesting("pen", &message_params);
  EXPECT_TRUE(ink_module().OnMessage(message));

  const PdfInkBrush* brush = ink_module().GetPdfInkBrushForTesting();
  ASSERT_TRUE(brush);

  const ink::Brush& ink_brush = brush->ink_brush();
  EXPECT_EQ(SK_ColorBLACK, GetSkColorFromInkBrush(ink_brush));
  EXPECT_EQ(4.5f, ink_brush.GetSize());
  ASSERT_EQ(1u, ink_brush.CoatCount());
  const ink::BrushCoat& coat = ink_brush.GetCoats()[0];
  EXPECT_EQ(1.0f, coat.tip.corner_rounding);
  EXPECT_EQ(1.0f, GetOpacityMultiplierFromBrush(ink_brush));
}

TEST_P(PdfInkModuleTest, HandleSetAnnotationModeMessage) {
  EXPECT_CALL(client(), LoadV2InkPathsFromPdf())
      .WillOnce(Return(PdfInkModuleClient::DocumentV2InkPathShapesMap{
          {0,
           PdfInkModuleClient::PageV2InkPathShapesMap{
               {InkModeledShapeId(0), ink::PartitionedMesh()},
               {InkModeledShapeId(1), ink::PartitionedMesh()}}},
          {3,
           PdfInkModuleClient::PageV2InkPathShapesMap{
               {InkModeledShapeId(2), ink::PartitionedMesh()}}},
      }));

  const auto kShapeMapMatcher = ElementsAre(
      Pair(0, ElementsAre(Field(&PdfInkModule::LoadedV2ShapeState::id,
                                InkModeledShapeId(0)),
                          Field(&PdfInkModule::LoadedV2ShapeState::id,
                                InkModeledShapeId(1)))),
      Pair(3, ElementsAre(Field(&PdfInkModule::LoadedV2ShapeState::id,
                                InkModeledShapeId(2)))));

  EXPECT_FALSE(ink_module().enabled());

  base::Value::Dict message =
      CreateSetAnnotationModeMessageForTesting(InkAnnotationMode::kOff);

  EXPECT_TRUE(ink_module().OnMessage(message));
  EXPECT_FALSE(ink_module().enabled());
  EXPECT_TRUE(ink_module().loaded_v2_shapes_.empty());

  message.Set("mode", "draw");
  EXPECT_TRUE(ink_module().OnMessage(message));
  EXPECT_TRUE(ink_module().enabled());
  EXPECT_THAT(ink_module().loaded_v2_shapes_, kShapeMapMatcher);

  message.Set("mode", "off");
  EXPECT_TRUE(ink_module().OnMessage(message));
  EXPECT_FALSE(ink_module().enabled());
  EXPECT_THAT(ink_module().loaded_v2_shapes_, kShapeMapMatcher);

  if (UseTextAnnotations()) {
    message.Set("mode", "text");
    EXPECT_TRUE(ink_module().OnMessage(message));
    EXPECT_TRUE(ink_module().enabled());
    EXPECT_THAT(ink_module().loaded_v2_shapes_, kShapeMapMatcher);

    message.Set("mode", "off");
    EXPECT_TRUE(ink_module().OnMessage(message));
    EXPECT_FALSE(ink_module().enabled());
    EXPECT_THAT(ink_module().loaded_v2_shapes_, kShapeMapMatcher);
  }
}

TEST_P(PdfInkModuleTest, MaybeSetCursorWhenTogglingAnnotationMode) {
  // Toggle between annotations off vs. drawing.
  EXPECT_FALSE(ink_module().enabled());

  EXPECT_CALL(client(), UpdateInkCursor(_)).WillOnce([this]() {
    EXPECT_TRUE(ink_module().enabled());
  });

  base::Value::Dict message =
      CreateSetAnnotationModeMessageForTesting(InkAnnotationMode::kDraw);
  EXPECT_TRUE(ink_module().OnMessage(message));
  EXPECT_TRUE(ink_module().enabled());

  message.Set("mode", "off");
  EXPECT_TRUE(ink_module().OnMessage(message));
  EXPECT_FALSE(ink_module().enabled());
  VerifyAndClearExpectations();

  if (UseTextAnnotations()) {
    // Toggle between annotations off vs. text markup.
    EXPECT_CALL(client(), UpdateInkCursor(_));

    message.Set("mode", "text");
    EXPECT_TRUE(ink_module().OnMessage(message));
    EXPECT_TRUE(ink_module().enabled());

    message.Set("mode", "off");
    EXPECT_TRUE(ink_module().OnMessage(message));
    EXPECT_FALSE(ink_module().enabled());
    VerifyAndClearExpectations();

    // Toggle between annotations drawing vs. text markup.
    EXPECT_CALL(client(), UpdateInkCursor(_)).Times(3);

    message.Set("mode", "draw");
    EXPECT_TRUE(ink_module().OnMessage(message));
    EXPECT_TRUE(ink_module().enabled());

    message.Set("mode", "text");
    EXPECT_TRUE(ink_module().OnMessage(message));
    EXPECT_TRUE(ink_module().enabled());

    message.Set("mode", "draw");
    EXPECT_TRUE(ink_module().OnMessage(message));
    EXPECT_TRUE(ink_module().enabled());
  }
}

TEST_P(PdfInkModuleTest, MaybeSetCursorWhenChangingBrushes) {
  {
    InSequence seq;
    EXPECT_CALL(client(), UpdateInkCursor(_))
        .WillOnce([](const ui::Cursor& cursor) {
          ASSERT_EQ(ui::mojom::CursorType::kCustom, cursor.type());
          const SkBitmap& bitmap = cursor.custom_bitmap();
          EXPECT_EQ(6, bitmap.width());
          EXPECT_EQ(6, bitmap.height());
        });
    EXPECT_CALL(client(), UpdateInkCursor(_))
        .WillOnce([](const ui::Cursor& cursor) {
          ASSERT_EQ(ui::mojom::CursorType::kCustom, cursor.type());
          const SkBitmap& bitmap = cursor.custom_bitmap();
          EXPECT_EQ(20, bitmap.width());
          EXPECT_EQ(20, bitmap.height());
        });
    EXPECT_CALL(client(), UpdateInkCursor(_))
        .WillOnce([](const ui::Cursor& cursor) {
          ASSERT_EQ(ui::mojom::CursorType::kCustom, cursor.type());
          const SkBitmap& bitmap = cursor.custom_bitmap();
          EXPECT_EQ(6, bitmap.width());
          EXPECT_EQ(6, bitmap.height());
        });
  }

  EnableDrawAnnotationMode();

  TestAnnotationBrushMessageParams message_params{
      SkColorSetRGB(0x00, 0xFF, 0x00),
      /*size=*/16.0};
  base::Value::Dict message =
      CreateSetAnnotationBrushMessageForTesting("pen", &message_params);
  EXPECT_TRUE(ink_module().OnMessage(message));

  message = CreateSetAnnotationBrushMessageForTesting("eraser", nullptr);
  EXPECT_TRUE(ink_module().OnMessage(message));
}

TEST_P(PdfInkModuleTest, MaybeSetCursorWhenChangingZoom) {
  {
    InSequence seq;
    EXPECT_CALL(client(), UpdateInkCursor(_))
        .WillOnce([](const ui::Cursor& cursor) {
          ASSERT_EQ(ui::mojom::CursorType::kCustom, cursor.type());
          const SkBitmap& bitmap = cursor.custom_bitmap();
          EXPECT_EQ(6, bitmap.width());
          EXPECT_EQ(6, bitmap.height());
        });
    EXPECT_CALL(client(), UpdateInkCursor(_))
        .WillOnce([](const ui::Cursor& cursor) {
          ASSERT_EQ(ui::mojom::CursorType::kCustom, cursor.type());
          const SkBitmap& bitmap = cursor.custom_bitmap();
          EXPECT_EQ(20, bitmap.width());
          EXPECT_EQ(20, bitmap.height());
        });
    EXPECT_CALL(client(), UpdateInkCursor(_))
        .WillOnce([](const ui::Cursor& cursor) {
          ASSERT_EQ(ui::mojom::CursorType::kCustom, cursor.type());
          const SkBitmap& bitmap = cursor.custom_bitmap();
          EXPECT_EQ(10, bitmap.width());
          EXPECT_EQ(10, bitmap.height());
        });
  }

  EnableDrawAnnotationMode();

  TestAnnotationBrushMessageParams message_params{
      SkColorSetRGB(0x00, 0xFF, 0x00),
      /*size=*/16.0};
  base::Value::Dict message =
      CreateSetAnnotationBrushMessageForTesting("pen", &message_params);
  EXPECT_TRUE(ink_module().OnMessage(message));

  client().set_zoom(0.5f);
  ink_module().OnGeometryChanged();
}

class PdfInkModuleStrokeTest : public PdfInkModuleTest {
 protected:
  // Mouse locations used for `RunStrokeCheckTest()`.
  // Touch events may use the same coordinates.
  static constexpr gfx::PointF kMouseDownPoint = gfx::PointF(10.0f, 15.0f);
  static constexpr gfx::PointF kMouseMovePoint = gfx::PointF(20.0f, 25.0f);
  static constexpr gfx::PointF kMouseUpPoint = gfx::PointF(30.0f, 17.0f);
  static constexpr gfx::PointF kMousePoints[] = {
      kMouseDownPoint, kMouseMovePoint, kMouseUpPoint};

  using StrokeInputPoints = std::vector<gfx::PointF>;

  // Each page of a document can have many strokes.  The input points for each
  // stroke are restricted to just one page.
  using PageStrokeInputPoints = std::vector<StrokeInputPoints>;

  // Mapping of a 0-based page index to the input points that make up the
  // strokes for that page.
  using DocumentStrokeInputPointsMap = std::map<int, PageStrokeInputPoints>;

  // PdfInkModuleTest:
  void SetUp() override {
    PdfInkModuleTest::SetUp();

    EXPECT_CALL(client(), PostMessage)
        .WillRepeatedly([&](const base::Value::Dict& dict) {
          const std::string* type = dict.FindString("type");
          ASSERT_TRUE(type);
          if (*type != "updateInk2Thumbnail") {
            return;
          }

          std::optional<int> page_number = dict.FindInt("pageNumber");
          ASSERT_TRUE(page_number.has_value());

          std::optional<bool> is_ink = dict.FindBool("isInk");
          ASSERT_TRUE(is_ink.has_value());
          auto& updated = is_ink.value() ? updated_ink_thumbnail_page_indices_
                                         : updated_pdf_thumbnail_page_indices_;
          updated.push_back(page_number.value() - 1);
        });
  }

  void InitializeSimpleSinglePageBasicLayout() {
    // Single page layout that matches visible area.
    constexpr gfx::RectF kPage(0.0f, 0.0f, 50.0f, 60.0f);
    client().set_page_layouts(base::span_from_ref(kPage));
    client().set_page_visibility(0, true);
  }

  void InitializeScaledLandscapeSinglePageBasicLayout() {
    // Single page layout that matches visible area.
    constexpr gfx::RectF kPage(0.0f, 0.0f, 120.0f, 100.0f);
    client().set_page_layouts(base::span_from_ref(kPage));
    client().set_page_visibility(0, true);
  }

  void InitializeVerticalTwoPageLayout() {
    // Page 2 is below page 1. Not side-by-side.
    client().set_page_layouts(kVerticalLayout2Pages);
    client().set_page_visibility(0, true);
    client().set_page_visibility(1, true);
  }

  void ApplyStrokeWithMouseAtPoints(
      const gfx::PointF& mouse_down_point,
      base::span<const gfx::PointF> mouse_move_points,
      const gfx::PointF& mouse_up_point) {
    ApplyStrokeWithMouseAtPointsMaybeHandled(
        mouse_down_point, mouse_move_points, mouse_up_point,
        /*expect_mouse_events_handled=*/true);
  }
  void ApplyStrokeWithMouseAtPointsNotHandled(
      const gfx::PointF& mouse_down_point,
      base::span<const gfx::PointF> mouse_move_points,
      const gfx::PointF& mouse_up_point) {
    ApplyStrokeWithMouseAtPointsMaybeHandled(
        mouse_down_point, mouse_move_points, mouse_up_point,
        /*expect_mouse_events_handled=*/false);
  }

  void RunStrokeCheckTest(bool annotation_mode_enabled) {
    EXPECT_TRUE(ink_module().OnMessage(CreateSetAnnotationModeMessageForTesting(
        annotation_mode_enabled ? InkAnnotationMode::kDraw
                                : InkAnnotationMode::kOff)));
    EXPECT_EQ(annotation_mode_enabled, ink_module().enabled());

    ApplyStrokeWithMouseAtPointsMaybeHandled(
        kMouseDownPoint, base::span_from_ref(kMouseMovePoint), kMouseUpPoint,
        /*expect_mouse_events_handled=*/annotation_mode_enabled);

    ValidateRunStrokeCheckTest(
        /*expect_stroke_success=*/annotation_mode_enabled);
  }

  void ApplyStrokeWithMouseAtMouseDownPoint() {
    ApplyStrokeWithMouseAtPoints(
        kMouseDownPoint, base::span_from_ref(kMouseDownPoint), kMouseDownPoint);
  }

  void ApplyStrokeWithTouchAtPoints(
      base::span<const gfx::PointF> touch_start_points,
      std::vector<base::span<const gfx::PointF>> all_touch_move_points,
      base::span<const gfx::PointF> touch_end_points) {
    ApplyStrokeWithTouchAtPointsMaybeHandled(
        touch_start_points, all_touch_move_points, touch_end_points,
        /*expect_touch_events_handled=*/true);
  }

  void ApplyStrokeWithTouchAtPointsNotHandled(
      base::span<const gfx::PointF> touch_start_points,
      std::vector<base::span<const gfx::PointF>> all_touch_move_points,
      base::span<const gfx::PointF> touch_end_points) {
    ApplyStrokeWithTouchAtPointsMaybeHandled(
        touch_start_points, all_touch_move_points, touch_end_points,
        /*expect_touch_events_handled=*/false);
  }

  // TODO(crbug.com/377733396): Consider refactoring to combine with
  // RunStrokeCheckTest().
  void RunStrokeTouchCheckTest(bool annotation_mode_enabled) {
    EXPECT_TRUE(ink_module().OnMessage(CreateSetAnnotationModeMessageForTesting(
        annotation_mode_enabled ? InkAnnotationMode::kDraw
                                : InkAnnotationMode::kOff)));
    EXPECT_EQ(annotation_mode_enabled, ink_module().enabled());

    const std::vector<base::span<const gfx::PointF>> all_touch_move_points{
        base::span_from_ref(kMouseMovePoint),
    };
    ApplyStrokeWithTouchAtPointsMaybeHandled(
        base::span_from_ref(kMouseDownPoint), all_touch_move_points,
        base::span_from_ref(kMouseUpPoint),
        /*expect_touch_events_handled=*/annotation_mode_enabled);

    ValidateRunStrokeCheckTest(
        /*expect_stroke_success=*/annotation_mode_enabled);
  }

  // TODO(crbug.com/377733396): Consider refactoring to combine with
  // RunStrokeCheckTest().
  //
  // Note that currently multi-touch is not handled, so the test expectations
  // are different from the ones in RunStrokeTouchCheckTest().
  void RunStrokeMultiTouchCheckTest(bool annotation_mode_enabled) {
    EXPECT_TRUE(ink_module().OnMessage(CreateSetAnnotationModeMessageForTesting(
        annotation_mode_enabled ? InkAnnotationMode::kDraw
                                : InkAnnotationMode::kOff)));
    EXPECT_EQ(annotation_mode_enabled, ink_module().enabled());

    const std::vector<gfx::PointF> touch_start_points{kMouseDownPoint,
                                                      kMouseDownPoint};
    const std::vector<gfx::PointF> touch_move_points{kMouseMovePoint,
                                                     kMouseMovePoint};
    const std::vector<base::span<const gfx::PointF>> all_touch_move_points{
        touch_move_points,
    };
    const std::vector<gfx::PointF> touch_end_points{kMouseUpPoint,
                                                    kMouseUpPoint};
    ApplyStrokeWithTouchAtPointsMaybeHandled(
        touch_start_points, all_touch_move_points, touch_end_points,
        /*expect_touch_events_handled=*/false);

    ValidateRunStrokeCheckTest(/*expect_stroke_success=*/false);
  }

  void ApplyStrokeWithPenAtPoints(
      base::span<const gfx::PointF> pen_start_points,
      std::vector<base::span<const gfx::PointF>> all_pen_move_points,
      base::span<const gfx::PointF> pen_end_points) {
    ApplyStrokeWithPenAtPointsMaybeHandled(pen_start_points,
                                           all_pen_move_points, pen_end_points,
                                           /*expect_pen_events_handled=*/true);
  }

  // TODO(crbug.com/377733396): Consider refactoring to combine with
  // RunStrokeCheckTest().
  void RunStrokePenCheckTest(bool annotation_mode_enabled) {
    EXPECT_TRUE(ink_module().OnMessage(CreateSetAnnotationModeMessageForTesting(
        annotation_mode_enabled ? InkAnnotationMode::kDraw
                                : InkAnnotationMode::kOff)));
    EXPECT_EQ(annotation_mode_enabled, ink_module().enabled());

    const std::vector<base::span<const gfx::PointF>> all_pen_move_points{
        base::span_from_ref(kMouseMovePoint),
    };
    ApplyStrokeWithPenAtPointsMaybeHandled(
        base::span_from_ref(kMouseDownPoint), all_pen_move_points,
        base::span_from_ref(kMouseUpPoint),
        /*expect_pen_events_handled=*/annotation_mode_enabled);

    ValidateRunStrokeCheckTest(
        /*expect_stroke_success=*/annotation_mode_enabled);
  }

  void RunStrokeMissedEndEventThenMouseMoveTest() {
    {
      // Start a drawing or erase action.
      blink::WebMouseEvent mouse_down_event =
          CreateLeftClickWebMouseEventAtPosition(kMouseDownPoint);
      EXPECT_TRUE(ink_module().HandleInputEvent(mouse_down_event));

      // Simulate scenario where another view has taken the focus and consumed
      // the mouse up event, such that subsequent mouse moves don't show the
      // left mouse button being pressed.  This should be handled, as it treats
      // it as a signal to terminate the prior stroke.
      blink::WebMouseEvent mouse_move_event =
          CreateMoveWebMouseEventToPosition(kMouseMovePoint);
      EXPECT_TRUE(ink_module().HandleInputEvent(mouse_move_event));
    }

    {
      // Another mouse move event without the button down is no longer handled
      // since there is no longer any active drawing or erasing.
      constexpr gfx::PointF kMouseMovePoint2 = gfx::PointF(21.0f, 26.0f);
      blink::WebMouseEvent mouse_move_event =
          CreateMoveWebMouseEventToPosition(kMouseMovePoint2);
      EXPECT_FALSE(ink_module().HandleInputEvent(mouse_move_event));
    }

    {
      // Start another stroke with a new mouse down event, which is handled.
      blink::WebMouseEvent mouse_down_event =
          CreateLeftClickWebMouseEventAtPosition(kMouseDownPoint);
      EXPECT_TRUE(ink_module().HandleInputEvent(mouse_down_event));

      blink::WebMouseEvent mouse_up_event =
          CreateLeftClickWebMouseUpEventAtPosition(kMouseUpPoint);
      EXPECT_TRUE(ink_module().HandleInputEvent(mouse_up_event));
    }
  }

  void SelectBrushTool(PdfInkBrush::Type type,
                       const TestAnnotationBrushMessageParams& params) {
    EXPECT_TRUE(
        ink_module().OnMessage(CreateSetAnnotationBrushMessageForTesting(
            PdfInkBrush::TypeToString(type), &params)));
  }

  void SelectEraserTool() {
    EXPECT_TRUE(ink_module().OnMessage(
        CreateSetAnnotationBrushMessageForTesting("eraser", nullptr)));
  }

  // Returns the (visible) input positions used for all strokes in the document.
  DocumentStrokeInputPointsMap StrokeInputPositions() const {
    return StrokeInputPositionsImpl(/*only_visible=*/false);
  }
  DocumentStrokeInputPointsMap VisibleStrokeInputPositions() const {
    return StrokeInputPositionsImpl(/*only_visible=*/true);
  }

  std::map<int, std::vector<raw_ref<const ink::Stroke>>> CollectVisibleStrokes()
      const {
    PageInkStrokeIterator strokes_iter(ink_module().strokes_);
    std::map<int, std::vector<raw_ref<const ink::Stroke>>> visible_strokes;
    for (auto page_stroke = strokes_iter.GetNextStrokeAndAdvance();
         page_stroke.has_value();
         page_stroke = strokes_iter.GetNextStrokeAndAdvance()) {
      visible_strokes[page_stroke.value().page_index].push_back(
          page_stroke.value().stroke);
    }

    return visible_strokes;
  }

  int GetInputOfTypeCountForPage(int page_index,
                                 ink::StrokeInput::ToolType tool_type) const {
    CHECK_GE(page_index, 0);
    const auto& strokes = ink_module().strokes_;
    auto it = strokes.find(page_index);
    if (it == strokes.end()) {
      return 0;
    }

    int count = 0;
    for (const auto& stroke_state : it->second) {
      count +=
          std::ranges::count_if(stroke_state.stroke.GetInputs(),
                                [&tool_type](const ink::StrokeInput& input) {
                                  return input.tool_type == tool_type;
                                });
    }
    return count;
  }

  void ExpectStrokesAdded(int strokes_affected) {
    CHECK_GT(strokes_affected, 0);
    EXPECT_CALL(client(), StrokeAdded(_, _, _)).Times(strokes_affected);
  }

  void ExpectNoStrokeAdded() {
    EXPECT_CALL(client(), StrokeAdded(_, _, _)).Times(0);
  }

  void ExpectUpdateStrokesActive(int strokes_affected, bool expected_active) {
    CHECK_GT(strokes_affected, 0);
    EXPECT_CALL(client(), UpdateStrokeActive(_, _, expected_active))
        .Times(strokes_affected);
  }

  void ExpectNoUpdateStrokeActive() {
    EXPECT_CALL(client(), UpdateStrokeActive(_, _, _)).Times(0);
  }

  void ExpectStrokeCounts(int started,
                          int modified_finished,
                          int unmodified_finished) {
    EXPECT_EQ(started, client().stroke_started_count());
    EXPECT_EQ(modified_finished, client().modified_stroke_finished_count());
    EXPECT_EQ(unmodified_finished, client().unmodified_stroke_finished_count());
  }

  const std::vector<int>& updated_ink_thumbnail_page_indices() const {
    return updated_ink_thumbnail_page_indices_;
  }
  const std::vector<int>& updated_pdf_thumbnail_page_indices() const {
    return updated_pdf_thumbnail_page_indices_;
  }

 private:
  struct PageInkStroke {
    int page_index;
    raw_ref<const ink::Stroke> stroke;
  };

  // Iterator to get visible strokes.  Once created, the caller should ensure
  // that there is no further PdfInkModule interactions until the iterator has
  // been destroyed.
  class PageInkStrokeIterator {
   public:
    explicit PageInkStrokeIterator(
        const PdfInkModule::DocumentStrokesMap& strokes)
        : strokes_(strokes), pages_iterator_(strokes_->cbegin()) {
      // Set up internal iterators for the first visible stroke, if there is
      // one.
      AdvanceToNextPageWithVisibleStrokes();
    }
    PageInkStrokeIterator(const PageInkStrokeIterator&) = delete;
    PageInkStrokeIterator& operator=(const PageInkStrokeIterator&) = delete;
    ~PageInkStrokeIterator() = default;

    // Gets the next visible stroke if there is one, and advances the internal
    // iterator to the next visible stroke.
    std::optional<PageInkStroke> GetNextStrokeAndAdvance() {
      if (pages_iterator_ == strokes_->cend()) {
        return std::nullopt;
      }

      // `page_strokes_iterator_` is set up when finding the page, and is
      // updated after establishing the stroke to return.  So the return value
      // is based upon the current position of the iterator.  Callers should not
      // get here if the end of the strokes has been reached for the current
      // page.
      CHECK(page_strokes_iterator_ != pages_iterator_->second.cend());
      CHECK(page_strokes_iterator_->should_draw);
      const ink::Stroke& page_stroke = page_strokes_iterator_->stroke;
      int page_index = pages_iterator_->first;
      AdvanceForCurrentPage();

      if (page_strokes_iterator_ == pages_iterator_->second.cend()) {
        // This was the last stroke for the current page, so advancing requires
        // moving on to another page and reinitializing
        // `page_strokes_iterator_`.
        ++pages_iterator_;
        AdvanceToNextPageWithVisibleStrokes();
      }

      return PageInkStroke{page_index, raw_ref<const ink::Stroke>(page_stroke)};
    }

   private:
    // Helper to advance to the next page which has visible strokes.  If there
    // is another page with visible strokes, performs the iterators
    // initialization to be able to get the visible strokes for it.  Leaves
    // `pages_iterator_` at end position if there are no more pages with
    // visible strokes.
    void AdvanceToNextPageWithVisibleStrokes() {
      for (; pages_iterator_ != strokes_->cend(); ++pages_iterator_) {
        // Initialize and scan to the location of the first (if any) visible
        // stroke for this page.
        for (page_strokes_iterator_ = pages_iterator_->second.cbegin();
             page_strokes_iterator_ != pages_iterator_->second.cend();
             ++page_strokes_iterator_) {
          if (page_strokes_iterator_->should_draw) {
            // This page has visible strokes, and `page_strokes_iterator_` has
            // been initialized to the position of the first visible stroke.
            return;
          }
        }
      }
    }

    // Helper to advance to the next visible stroke for the current page, if
    // there is one.  Leaves `page_strokes_iterator_` at end position if there
    // are no more visible strokes.
    void AdvanceForCurrentPage() {
      CHECK(pages_iterator_ != strokes_->cend());

      // Advance the iterator to next visible stroke in this page (if any)
      // before returning.
      do {
        ++page_strokes_iterator_;
        if (page_strokes_iterator_ == pages_iterator_->second.cend()) {
          break;
        }
      } while (!page_strokes_iterator_->should_draw);
    }

    const raw_ref<const PdfInkModule::DocumentStrokesMap> strokes_;

    // Iterator for getting pages with visible strokes.
    PdfInkModule::DocumentStrokesMap::const_iterator pages_iterator_;

    // Iterator for getting visible strokes of a particular page.
    PdfInkModule::PageStrokes::const_iterator page_strokes_iterator_;
  };

  DocumentStrokeInputPointsMap StrokeInputPositionsImpl(
      bool only_visible) const {
    DocumentStrokeInputPointsMap all_strokes_points;
    for (const auto& [page_index, strokes] : ink_module().strokes_) {
      for (const auto& stroke : strokes) {
        if (only_visible && !stroke.should_draw) {
          continue;
        }
        const ink::StrokeInputBatch& input_batch = stroke.stroke.GetInputs();
        StrokeInputPoints stroke_points;
        stroke_points.reserve(input_batch.Size());
        for (ink::StrokeInput stroke_input : input_batch) {
          stroke_points.emplace_back(stroke_input.position.x,
                                     stroke_input.position.y);
        }
        all_strokes_points[page_index].push_back(std::move(stroke_points));
      }
    }
    return all_strokes_points;
  }

  void ApplyStrokeWithMouseAtPointsMaybeHandled(
      const gfx::PointF& mouse_down_point,
      base::span<const gfx::PointF> mouse_move_points,
      const gfx::PointF& mouse_up_point,
      bool expect_mouse_events_handled) {
    blink::WebMouseEvent mouse_down_event =
        CreateLeftClickWebMouseEventAtPosition(mouse_down_point);
    EXPECT_EQ(expect_mouse_events_handled,
              ink_module().HandleInputEvent(mouse_down_event));

    for (const gfx::PointF& mouse_move_point : mouse_move_points) {
      blink::WebMouseEvent mouse_move_event =
          CreateLeftClickWebMouseMoveEventAtPosition(mouse_move_point);
      EXPECT_EQ(expect_mouse_events_handled,
                ink_module().HandleInputEvent(mouse_move_event));
    }

    blink::WebMouseEvent mouse_up_event =
        CreateLeftClickWebMouseUpEventAtPosition(mouse_up_point);
    EXPECT_EQ(expect_mouse_events_handled,
              ink_module().HandleInputEvent(mouse_up_event));
  }

  void ApplyStrokeWithTouchAtPointsMaybeHandled(
      base::span<const gfx::PointF> touch_start_points,
      std::vector<base::span<const gfx::PointF>> all_touch_move_points,
      base::span<const gfx::PointF> touch_end_points,
      bool expect_touch_events_handled) {
    blink::WebTouchEvent touch_start_event = CreateTouchEvent(
        blink::WebInputEvent::Type::kTouchStart, touch_start_points);
    EXPECT_EQ(expect_touch_events_handled,
              ink_module().HandleInputEvent(touch_start_event));
    for (const auto& touch_move_points : all_touch_move_points) {
      blink::WebTouchEvent touch_move_event = CreateTouchEvent(
          blink::WebInputEvent::Type::kTouchMove, touch_move_points);
      EXPECT_EQ(expect_touch_events_handled,
                ink_module().HandleInputEvent(touch_move_event));
    }

    blink::WebTouchEvent touch_end_event = CreateTouchEvent(
        blink::WebInputEvent::Type::kTouchEnd, touch_end_points);
    EXPECT_EQ(expect_touch_events_handled,
              ink_module().HandleInputEvent(touch_end_event));
  }

  void ApplyStrokeWithPenAtPointsMaybeHandled(
      base::span<const gfx::PointF> pen_start_points,
      std::vector<base::span<const gfx::PointF>> all_pen_move_points,
      base::span<const gfx::PointF> pen_end_points,
      bool expect_pen_events_handled) {
    blink::WebTouchEvent pen_start_event = CreatePenEvent(
        blink::WebInputEvent::Type::kTouchStart, pen_start_points);
    EXPECT_EQ(expect_pen_events_handled,
              ink_module().HandleInputEvent(pen_start_event));
    for (const auto& pen_move_points : all_pen_move_points) {
      blink::WebTouchEvent pen_move_event = CreatePenEvent(
          blink::WebInputEvent::Type::kTouchMove, pen_move_points);
      EXPECT_EQ(expect_pen_events_handled,
                ink_module().HandleInputEvent(pen_move_event));
    }

    blink::WebTouchEvent pen_end_event =
        CreatePenEvent(blink::WebInputEvent::Type::kTouchEnd, pen_end_points);
    EXPECT_EQ(expect_pen_events_handled,
              ink_module().HandleInputEvent(pen_end_event));
  }

  void ValidateRunStrokeCheckTest(bool expect_stroke_success) {
    EXPECT_EQ(expect_stroke_success ? 1 : 0, client().stroke_started_count());
    EXPECT_EQ(expect_stroke_success ? 1 : 0,
              client().modified_stroke_finished_count());
    EXPECT_EQ(0, client().unmodified_stroke_finished_count());
    if (expect_stroke_success) {
      EXPECT_THAT(updated_ink_thumbnail_page_indices(), ElementsAre(0));
    } else {
      EXPECT_TRUE(updated_ink_thumbnail_page_indices().empty());
    }
  }

  std::vector<int> updated_ink_thumbnail_page_indices_;
  std::vector<int> updated_pdf_thumbnail_page_indices_;
};

TEST_P(PdfInkModuleStrokeTest, NoAnnotationWithMouseIfNotEnabled) {
  InitializeSimpleSinglePageBasicLayout();
  RunStrokeCheckTest(/*annotation_mode_enabled=*/false);
  EXPECT_EQ(0, GetInputOfTypeCountForPage(
                   /*page_index=*/0, ink::StrokeInput::ToolType::kMouse));
  EXPECT_EQ(0, GetInputOfTypeCountForPage(
                   /*page_index=*/0, ink::StrokeInput::ToolType::kTouch));
  EXPECT_EQ(0, GetInputOfTypeCountForPage(
                   /*page_index=*/0, ink::StrokeInput::ToolType::kStylus));
}

TEST_P(PdfInkModuleStrokeTest, AnnotationWithMouseIfEnabled) {
  InitializeSimpleSinglePageBasicLayout();
  RunStrokeCheckTest(/*annotation_mode_enabled=*/true);
  EXPECT_EQ(3, GetInputOfTypeCountForPage(
                   /*page_index=*/0, ink::StrokeInput::ToolType::kMouse));
  EXPECT_EQ(0, GetInputOfTypeCountForPage(
                   /*page_index=*/0, ink::StrokeInput::ToolType::kTouch));
  EXPECT_EQ(0, GetInputOfTypeCountForPage(
                   /*page_index=*/0, ink::StrokeInput::ToolType::kStylus));
}

TEST_P(PdfInkModuleStrokeTest, NoAnnotationWithTouchIfNotEnabled) {
  InitializeSimpleSinglePageBasicLayout();
  RunStrokeTouchCheckTest(/*annotation_mode_enabled=*/false);
  EXPECT_EQ(0, GetInputOfTypeCountForPage(
                   /*page_index=*/0, ink::StrokeInput::ToolType::kMouse));
  EXPECT_EQ(0, GetInputOfTypeCountForPage(
                   /*page_index=*/0, ink::StrokeInput::ToolType::kTouch));
  EXPECT_EQ(0, GetInputOfTypeCountForPage(
                   /*page_index=*/0, ink::StrokeInput::ToolType::kStylus));
}

TEST_P(PdfInkModuleStrokeTest, AnnotationWithTouchIfEnabled) {
  InitializeSimpleSinglePageBasicLayout();
  RunStrokeTouchCheckTest(/*annotation_mode_enabled=*/true);
  EXPECT_EQ(0, GetInputOfTypeCountForPage(
                   /*page_index=*/0, ink::StrokeInput::ToolType::kMouse));
  EXPECT_EQ(3, GetInputOfTypeCountForPage(
                   /*page_index=*/0, ink::StrokeInput::ToolType::kTouch));
  EXPECT_EQ(0, GetInputOfTypeCountForPage(
                   /*page_index=*/0, ink::StrokeInput::ToolType::kStylus));
}

TEST_P(PdfInkModuleStrokeTest, NoAnnotationWithMultiTouchIfNotEnabled) {
  InitializeSimpleSinglePageBasicLayout();
  RunStrokeMultiTouchCheckTest(/*annotation_mode_enabled=*/false);
  EXPECT_EQ(0, GetInputOfTypeCountForPage(
                   /*page_index=*/0, ink::StrokeInput::ToolType::kMouse));
  EXPECT_EQ(0, GetInputOfTypeCountForPage(
                   /*page_index=*/0, ink::StrokeInput::ToolType::kTouch));
  EXPECT_EQ(0, GetInputOfTypeCountForPage(
                   /*page_index=*/0, ink::StrokeInput::ToolType::kStylus));
}

TEST_P(PdfInkModuleStrokeTest, NoAnnotationWithMultiTouchIfEnabled) {
  InitializeSimpleSinglePageBasicLayout();
  RunStrokeMultiTouchCheckTest(/*annotation_mode_enabled=*/true);
  EXPECT_EQ(0, GetInputOfTypeCountForPage(
                   /*page_index=*/0, ink::StrokeInput::ToolType::kMouse));
  EXPECT_EQ(0, GetInputOfTypeCountForPage(
                   /*page_index=*/0, ink::StrokeInput::ToolType::kTouch));
  EXPECT_EQ(0, GetInputOfTypeCountForPage(
                   /*page_index=*/0, ink::StrokeInput::ToolType::kStylus));
}

TEST_P(PdfInkModuleStrokeTest, NoAnnotationWithPenIfNotEnabled) {
  InitializeSimpleSinglePageBasicLayout();
  RunStrokePenCheckTest(/*annotation_mode_enabled=*/false);
  EXPECT_EQ(0, GetInputOfTypeCountForPage(
                   /*page_index=*/0, ink::StrokeInput::ToolType::kMouse));
  EXPECT_EQ(0, GetInputOfTypeCountForPage(
                   /*page_index=*/0, ink::StrokeInput::ToolType::kTouch));
  EXPECT_EQ(0, GetInputOfTypeCountForPage(
                   /*page_index=*/0, ink::StrokeInput::ToolType::kStylus));
}

TEST_P(PdfInkModuleStrokeTest, AnnotationWithPenIfEnabled) {
  InitializeSimpleSinglePageBasicLayout();
  RunStrokePenCheckTest(/*annotation_mode_enabled=*/true);
  EXPECT_EQ(0, GetInputOfTypeCountForPage(
                   /*page_index=*/0, ink::StrokeInput::ToolType::kMouse));
  EXPECT_EQ(0, GetInputOfTypeCountForPage(
                   /*page_index=*/0, ink::StrokeInput::ToolType::kTouch));
  EXPECT_EQ(3, GetInputOfTypeCountForPage(
                   /*page_index=*/0, ink::StrokeInput::ToolType::kStylus));
}

TEST_P(PdfInkModuleStrokeTest, IgnoreTouchEventsAfterPenEvent) {
  EnableDrawAnnotationMode();
  InitializeSimpleSinglePageBasicLayout();

  const std::vector<base::span<const gfx::PointF>> all_move_points{
      base::span_from_ref(kMouseMovePoint),
  };
  ApplyStrokeWithTouchAtPoints(base::span_from_ref(kMouseDownPoint),
                               all_move_points,
                               base::span_from_ref(kMouseUpPoint));
  EXPECT_EQ(3, GetInputOfTypeCountForPage(
                   /*page_index=*/0, ink::StrokeInput::ToolType::kTouch));

  ApplyStrokeWithTouchAtPoints(base::span_from_ref(kMouseDownPoint),
                               all_move_points,
                               base::span_from_ref(kMouseUpPoint));
  EXPECT_EQ(6, GetInputOfTypeCountForPage(
                   /*page_index=*/0, ink::StrokeInput::ToolType::kTouch));
  EXPECT_EQ(0, GetInputOfTypeCountForPage(
                   /*page_index=*/0, ink::StrokeInput::ToolType::kStylus));

  ApplyStrokeWithPenAtPoints(base::span_from_ref(kMouseDownPoint),
                             all_move_points,
                             base::span_from_ref(kMouseUpPoint));
  EXPECT_EQ(6, GetInputOfTypeCountForPage(
                   /*page_index=*/0, ink::StrokeInput::ToolType::kTouch));
  EXPECT_EQ(3, GetInputOfTypeCountForPage(
                   /*page_index=*/0, ink::StrokeInput::ToolType::kStylus));

  ApplyStrokeWithTouchAtPointsNotHandled(base::span_from_ref(kMouseDownPoint),
                                         all_move_points,
                                         base::span_from_ref(kMouseUpPoint));
  EXPECT_EQ(6, GetInputOfTypeCountForPage(
                   /*page_index=*/0, ink::StrokeInput::ToolType::kTouch));
  EXPECT_EQ(3, GetInputOfTypeCountForPage(
                   /*page_index=*/0, ink::StrokeInput::ToolType::kStylus));

  ApplyStrokeWithPenAtPoints(base::span_from_ref(kMouseDownPoint),
                             all_move_points,
                             base::span_from_ref(kMouseUpPoint));
  EXPECT_EQ(6, GetInputOfTypeCountForPage(
                   /*page_index=*/0, ink::StrokeInput::ToolType::kTouch));
  EXPECT_EQ(6, GetInputOfTypeCountForPage(
                   /*page_index=*/0, ink::StrokeInput::ToolType::kStylus));

  ApplyStrokeWithTouchAtPointsNotHandled(base::span_from_ref(kMouseDownPoint),
                                         all_move_points,
                                         base::span_from_ref(kMouseUpPoint));
  EXPECT_EQ(0, GetInputOfTypeCountForPage(
                   /*page_index=*/0, ink::StrokeInput::ToolType::kMouse));
  EXPECT_EQ(6, GetInputOfTypeCountForPage(
                   /*page_index=*/0, ink::StrokeInput::ToolType::kTouch));
  EXPECT_EQ(6, GetInputOfTypeCountForPage(
                   /*page_index=*/0, ink::StrokeInput::ToolType::kStylus));
}

TEST_P(PdfInkModuleStrokeTest, AnnotationWithMouseInterruptedByPenEvents) {
  EnableDrawAnnotationMode();
  InitializeSimpleSinglePageBasicLayout();

  blink::WebMouseEvent mouse_down_event =
      CreateLeftClickWebMouseEventAtPosition(kMouseDownPoint);
  EXPECT_TRUE(ink_module().HandleInputEvent(mouse_down_event));

  blink::WebMouseEvent mouse_move_event =
      CreateLeftClickWebMouseMoveEventAtPosition(kMouseMovePoint);
  EXPECT_TRUE(ink_module().HandleInputEvent(mouse_move_event));

  // Per manual testing on a Windows laptop, pen input causes mouse events to
  // lose their left-button down state while the pen is active.
  blink::WebMouseEvent mouse_move_no_left_button_event =
      CreateMoveWebMouseEventToPosition(kMouseMovePoint);
  EXPECT_TRUE(ink_module().HandleInputEvent(mouse_move_no_left_button_event));

  const std::vector<base::span<const gfx::PointF>> all_move_points{
      base::span_from_ref(kMouseMovePoint),
  };
  ApplyStrokeWithPenAtPoints(base::span_from_ref(kMouseDownPoint),
                             all_move_points,
                             base::span_from_ref(kMouseUpPoint));

  // Per manual testing on a Windows laptop, after the pen inputs finish, the
  // mouse events regain their left-button down state. PdfInkModule treats these
  // as spurious events, and ignores them.
  EXPECT_FALSE(ink_module().HandleInputEvent(mouse_move_event));

  blink::WebMouseEvent mouse_up_event =
      CreateLeftClickWebMouseUpEventAtPosition(kMouseUpPoint);
  EXPECT_FALSE(ink_module().HandleInputEvent(mouse_up_event));

  ExpectStrokeCounts(/*started=*/2, /*modified_finished=*/2,
                     /*unmodified_finished=*/0);
  EXPECT_EQ(2, GetInputOfTypeCountForPage(
                   /*page_index=*/0, ink::StrokeInput::ToolType::kMouse));
  EXPECT_EQ(0, GetInputOfTypeCountForPage(
                   /*page_index=*/0, ink::StrokeInput::ToolType::kTouch));
  EXPECT_EQ(3, GetInputOfTypeCountForPage(
                   /*page_index=*/0, ink::StrokeInput::ToolType::kStylus));
}

TEST_P(PdfInkModuleStrokeTest, AnnotationWithPenIgnoresMouseEvents) {
  EnableDrawAnnotationMode();
  InitializeSimpleSinglePageBasicLayout();

  blink::WebTouchEvent pen_start_event =
      CreatePenEvent(blink::WebInputEvent::Type::kTouchStart,
                     base::span_from_ref(kMouseDownPoint));
  EXPECT_TRUE(ink_module().HandleInputEvent(pen_start_event));

  blink::WebMouseEvent mouse_move_event =
      CreateLeftClickWebMouseMoveEventAtPosition(kMouseMovePoint);
  EXPECT_TRUE(ink_module().HandleInputEvent(mouse_move_event));

  blink::WebTouchEvent pen_end_event =
      CreatePenEvent(blink::WebInputEvent::Type::kTouchEnd,
                     base::span_from_ref(kMouseUpPoint));
  EXPECT_TRUE(ink_module().HandleInputEvent(pen_end_event));

  ExpectStrokeCounts(/*started=*/1, /*modified_finished=*/1,
                     /*unmodified_finished=*/0);
  EXPECT_EQ(0, GetInputOfTypeCountForPage(
                   /*page_index=*/0, ink::StrokeInput::ToolType::kMouse));
  EXPECT_EQ(0, GetInputOfTypeCountForPage(
                   /*page_index=*/0, ink::StrokeInput::ToolType::kTouch));
  EXPECT_EQ(2, GetInputOfTypeCountForPage(
                   /*page_index=*/0, ink::StrokeInput::ToolType::kStylus));
}

TEST_P(PdfInkModuleStrokeTest, CanonicalAnnotationPoints) {
  // Setup to support examining the page stroke points for a layout that is
  // more complicated than what is provide by
  // `InitializeSimpleSinglePageBasicLayout()`.  Include viewport offset,
  // scroll, rotation, and zoom.
  constexpr gfx::SizeF kPageSize(100.0f, 120.0f);
  constexpr gfx::PointF kPageOrigin(5.0f, -15.0f);
  constexpr gfx::RectF kPageLayout(kPageOrigin, kPageSize);
  client().set_page_layouts(base::span_from_ref(kPageLayout));
  client().set_page_visibility(0, true);
  client().set_orientation(PageOrientation::kClockwise180);
  client().set_zoom(2.0f);

  RunStrokeCheckTest(/*annotation_mode_enabled=*/true);

  // There should be 3 points collected, for the mouse down, move, and up
  // events. Verify that the collected points match a canonical position for
  // the PdfInkModuleClient setup.
  constexpr gfx::PointF kCanonicalMouseDownPosition(47.0f, 44.5f);
  constexpr gfx::PointF kCanonicalMouseMovePosition(42.0f, 39.5f);
  constexpr gfx::PointF kCanonicalMouseUpPosition(37.0f, 43.5f);
  EXPECT_THAT(
      StrokeInputPositions(),
      ElementsAre(Pair(0, PageStrokeInputPoints{{kCanonicalMouseDownPosition,
                                                 kCanonicalMouseMovePosition,
                                                 kCanonicalMouseUpPosition}})));
}

TEST_P(PdfInkModuleStrokeTest, BasicLayoutInvalidationsFromStroke) {
  InitializeSimpleSinglePageBasicLayout();
  RunStrokeCheckTest(/*annotation_mode_enabled=*/true);

  // The default brush param size is 3.0.
  constexpr gfx::Rect kInvalidationAreaMouseDown(gfx::Point(8, 13),
                                                 gfx::Size(4, 4));
  constexpr gfx::Rect kInvalidationAreaMouseMove(gfx::Point(8, 13),
                                                 gfx::Size(14, 14));
  constexpr gfx::Rect kInvalidationAreaMouseUp(gfx::Point(18, 15),
                                               gfx::Size(14, 12));
  constexpr gfx::Rect kInvalidationAreaFinishedStroke(7, 12, 27, 9);
  EXPECT_THAT(
      client().invalidations(),
      ElementsAre(kInvalidationAreaMouseDown, kInvalidationAreaMouseMove,
                  kInvalidationAreaMouseUp, kInvalidationAreaFinishedStroke));
}

TEST_P(PdfInkModuleStrokeTest, TransformedLayoutInvalidationsFromStroke) {
  // Setup to support examining the invalidation areas from page stroke points
  // for a layout that is more complicated than what is provide by
  // `InitializeSimpleSinglePageBasicLayout()`.  Include viewport offset,
  // scroll, rotation, and zoom.
  constexpr gfx::SizeF kPageSize(100.0f, 120.0f);
  constexpr gfx::PointF kPageOrigin(5.0f, -15.0f);
  constexpr gfx::RectF kPageLayout(kPageOrigin, kPageSize);
  client().set_page_layouts(base::span_from_ref(kPageLayout));
  client().set_page_visibility(0, true);
  client().set_orientation(PageOrientation::kClockwise180);
  client().set_zoom(2.0f);

  RunStrokeCheckTest(/*annotation_mode_enabled=*/true);

  // The default brush param size is 3.0.
  constexpr gfx::Rect kInvalidationAreaMouseDown(gfx::Point(8, 13),
                                                 gfx::Size(4, 4));
  constexpr gfx::Rect kInvalidationAreaMouseMove(gfx::Point(8, 13),
                                                 gfx::Size(14, 14));
  constexpr gfx::Rect kInvalidationAreaMouseUp(gfx::Point(18, 15),
                                               gfx::Size(14, 12));
  constexpr gfx::Rect kInvalidationAreaFinishedStroke(6, 11, 29, 11);
  EXPECT_THAT(
      client().invalidations(),
      ElementsAre(kInvalidationAreaMouseDown, kInvalidationAreaMouseMove,
                  kInvalidationAreaMouseUp, kInvalidationAreaFinishedStroke));
}

TEST_P(PdfInkModuleStrokeTest, StrokeOutsidePage) {
  EnableDrawAnnotationMode();
  InitializeVerticalTwoPageLayout();

  // Start out without any strokes.
  EXPECT_TRUE(StrokeInputPositions().empty());

  // A stroke that starts outside of any page does not generate a stroke, even
  // if it crosses into a page.
  ApplyStrokeWithMouseAtPointsNotHandled(
      kTwoPageVerticalLayoutPointOutsidePages,
      base::span_from_ref(kTwoPageVerticalLayoutPoint2InsidePage0),
      kTwoPageVerticalLayoutPoint3InsidePage0);

  EXPECT_TRUE(StrokeInputPositions().empty());
}

TEST_P(PdfInkModuleStrokeTest, StrokeInsidePages) {
  EnableDrawAnnotationMode();
  InitializeVerticalTwoPageLayout();

  // Start out without any strokes.
  EXPECT_TRUE(StrokeInputPositions().empty());

  // A stroke in the first page generates a stroke only for that page.
  ApplyStrokeWithMouseAtPoints(
      kTwoPageVerticalLayoutPoint1InsidePage0,
      base::span_from_ref(kTwoPageVerticalLayoutPoint2InsidePage0),
      kTwoPageVerticalLayoutPoint3InsidePage0);

  EXPECT_THAT(StrokeInputPositions(), ElementsAre(Pair(0, SizeIs(1))));

  // A stroke in the second page generates a stroke only for that page.
  ApplyStrokeWithMouseAtPoints(
      kTwoPageVerticalLayoutPoint1InsidePage1,
      base::span_from_ref(kTwoPageVerticalLayoutPoint2InsidePage1),
      kTwoPageVerticalLayoutPoint3InsidePage1);

  EXPECT_THAT(StrokeInputPositions(),
              ElementsAre(Pair(0, SizeIs(1)), Pair(1, SizeIs(1))));
}

TEST_P(PdfInkModuleStrokeTest, StrokeAcrossPages) {
  EnableDrawAnnotationMode();
  InitializeVerticalTwoPageLayout();

  // Start out without any strokes.
  EXPECT_TRUE(StrokeInputPositions().empty());

  // A stroke that starts in first page and ends in the second page only
  // generates one stroke in the first page.
  ApplyStrokeWithMouseAtPoints(
      kTwoPageVerticalLayoutPoint1InsidePage0,
      base::span_from_ref(kTwoPageVerticalLayoutPoint2InsidePage1),
      kTwoPageVerticalLayoutPoint3InsidePage1);

  EXPECT_THAT(StrokeInputPositions(), ElementsAre(Pair(0, SizeIs(1))));
}

TEST_P(PdfInkModuleStrokeTest, StrokePageExitAndReentry) {
  EnableDrawAnnotationMode();
  InitializeVerticalTwoPageLayout();

  // Start out without any strokes.
  EXPECT_TRUE(StrokeInputPositions().empty());

  ApplyStrokeWithMouseAtPoints(kTwoPageVerticalLayoutPoint1InsidePage0,
                               kTwoPageVerticalLayoutPageExitAndReentryPoints,
                               kTwoPageVerticalLayoutPoint3InsidePage0);

  EXPECT_THAT(
      StrokeInputPositions(),
      ElementsAre(Pair(
          0,
          ElementsAre(ElementsAreArray(
                          kTwoPageVerticalLayoutPageExitAndReentrySegment1),
                      ElementsAreArray(
                          kTwoPageVerticalLayoutPageExitAndReentrySegment2)))));
}

TEST_P(PdfInkModuleStrokeTest, StrokePageExitAndReentryWithQuickMoves) {
  EnableDrawAnnotationMode();
  InitializeVerticalTwoPageLayout();

  // Start out without any strokes.
  EXPECT_TRUE(StrokeInputPositions().empty());

  // When the mouse cursor moves quickly, PdfInkModule gets fewer input events.
  // Simulate that here with fewer movement inputs compared to
  // `kTwoPageVerticalLayoutPageExitAndReentryPoints`.
  constexpr gfx::PointF kQuickPageExitAndReentryPoints[] = {
      kTwoPageVerticalLayoutPointOutsidePages,
      kTwoPageVerticalLayoutPoint2InsidePage0};
  ApplyStrokeWithMouseAtPoints(kTwoPageVerticalLayoutPoint1InsidePage0,
                               kQuickPageExitAndReentryPoints,
                               kTwoPageVerticalLayoutPoint2InsidePage0);

  EXPECT_THAT(
      StrokeInputPositions(),
      ElementsAre(Pair(
          0, ElementsAre(ElementsAreArray(
                             kTwoPageVerticalLayoutPageExitAndReentrySegment1),
                         ElementsAreArray({gfx::PointF(6.666667f, 0.0f),
                                           gfx::PointF(10.0f, 10.0f)})))));
}

TEST_P(PdfInkModuleStrokeTest, EraseStroke) {
  InitializeSimpleSinglePageBasicLayout();
  RunStrokeCheckTest(/*annotation_mode_enabled=*/true);

  // Check that there are now some visible strokes.
  EXPECT_THAT(
      VisibleStrokeInputPositions(),
      ElementsAre(Pair(0, ElementsAre(ElementsAreArray(kMousePoints)))));
  ExpectStrokeCounts(/*started=*/1, /*modified_finished=*/1,
                     /*unmodified_finished=*/0);
  EXPECT_THAT(updated_ink_thumbnail_page_indices(), ElementsAre(0));

  // Stroke with the eraser tool.
  SelectEraserTool();
  ApplyStrokeWithMouseAtMouseDownPoint();

  // Now there are no visible strokes left.
  EXPECT_TRUE(VisibleStrokeInputPositions().empty());
  // Erasing increments the modified stroke count.
  ExpectStrokeCounts(/*started=*/2, /*modified_finished=*/2,
                     /*unmodified_finished=*/0);
  EXPECT_THAT(updated_ink_thumbnail_page_indices(), ElementsAre(0, 0));

  // Stroke again. The stroke that have already been erased should stay erased.
  ApplyStrokeWithMouseAtMouseDownPoint();

  // Still no visible strokes.
  EXPECT_TRUE(VisibleStrokeInputPositions().empty());
  // Nothing got erased, so the modified count stays the same, and the
  // unmodified stroke count goes up by 1 instead.
  ExpectStrokeCounts(/*started=*/3, /*modified_finished=*/2,
                     /*unmodified_finished=*/1);
  EXPECT_THAT(updated_ink_thumbnail_page_indices(), ElementsAre(0, 0));

  // PDF thumbnail never needed to be updated.
  EXPECT_TRUE(updated_pdf_thumbnail_page_indices().empty());
}

TEST_P(PdfInkModuleStrokeTest, EraseOnPageWithoutStrokes) {
  EnableDrawAnnotationMode();
  InitializeSimpleSinglePageBasicLayout();

  // Verify there are no visible strokes to start with.
  EXPECT_TRUE(VisibleStrokeInputPositions().empty());

  // Stroke with the eraser tool when there are no strokes on the page.
  SelectEraserTool();
  ApplyStrokeWithMouseAtMouseDownPoint();

  // Verify there are still no visible strokes and the StrokeFinished() call is
  // for being unmodified.
  EXPECT_TRUE(VisibleStrokeInputPositions().empty());
  ExpectStrokeCounts(/*started=*/1, /*modified_finished=*/0,
                     /*unmodified_finished=*/1);
  EXPECT_TRUE(updated_ink_thumbnail_page_indices().empty());
}

TEST_P(PdfInkModuleStrokeTest, EraseStrokeEntirelyOffPage) {
  InitializeSimpleSinglePageBasicLayout();
  RunStrokeCheckTest(/*annotation_mode_enabled=*/true);

  // Check that there are now some visible strokes.
  EXPECT_THAT(
      VisibleStrokeInputPositions(),
      ElementsAre(Pair(0, ElementsAre(ElementsAreArray(kMousePoints)))));
  ExpectStrokeCounts(/*started=*/1, /*modified_finished=*/1,
                     /*unmodified_finished=*/0);
  EXPECT_THAT(updated_ink_thumbnail_page_indices(), ElementsAre(0));

  // Stroke with the eraser tool outside of the page.
  SelectEraserTool();
  constexpr gfx::PointF kOffPagePoint(99.0f, 99.0f);
  ApplyStrokeWithMouseAtPointsNotHandled(
      kOffPagePoint, base::span_from_ref(kOffPagePoint), kOffPagePoint);

  // Check that the visible strokes remain, and StrokeFinished() did not get
  // called again.
  EXPECT_THAT(
      VisibleStrokeInputPositions(),
      ElementsAre(Pair(0, ElementsAre(ElementsAreArray(kMousePoints)))));
  ExpectStrokeCounts(/*started=*/1, /*modified_finished=*/1,
                     /*unmodified_finished=*/0);
  EXPECT_THAT(updated_ink_thumbnail_page_indices(), ElementsAre(0));
}

TEST_P(PdfInkModuleStrokeTest, EraseStrokeErasesTwoStrokes) {
  InitializeSimpleSinglePageBasicLayout();
  ExpectStrokesAdded(/*strokes_affected=*/2);
  ExpectNoUpdateStrokeActive();
  RunStrokeCheckTest(/*annotation_mode_enabled=*/true);

  // Draw a second stroke.
  constexpr gfx::PointF kMouseDownPoint2 = gfx::PointF(10.0f, 30.0f);
  constexpr gfx::PointF kMouseUpPoint2 = gfx::PointF(30.0f, 30.0f);
  ApplyStrokeWithMouseAtPoints(
      kMouseDownPoint2, base::span_from_ref(kMouseMovePoint), kMouseUpPoint2);

  // Check that there are now some visible strokes.
  const auto kStroke2Matcher =
      ElementsAre(kMouseDownPoint2, kMouseMovePoint, kMouseUpPoint2);
  const auto kVisibleStrokesMatcher = ElementsAre(
      Pair(0, ElementsAre(ElementsAreArray(kMousePoints), kStroke2Matcher)));
  EXPECT_THAT(VisibleStrokeInputPositions(), kVisibleStrokesMatcher);
  ExpectStrokeCounts(/*started=*/2, /*modified_finished=*/2,
                     /*unmodified_finished=*/0);
  EXPECT_THAT(updated_ink_thumbnail_page_indices(), ElementsAre(0, 0));

  // Stroke with the eraser tool at `kMouseMovePoint`, where it should
  // intersect with both strokes, but does not because InkStrokeModeler modeled
  // the "V" shaped input into an input with a much gentler line slope.
  SelectEraserTool();
  ApplyStrokeWithMouseAtPoints(
      kMouseMovePoint, base::span_from_ref(kMouseMovePoint), kMouseMovePoint);

  // Check that the visible strokes are still there since the eraser tool missed
  // the strokes. This third stroke causes the unmodified stroke finished count
  // to go up by 1.
  EXPECT_THAT(VisibleStrokeInputPositions(), kVisibleStrokesMatcher);
  ExpectStrokeCounts(/*started=*/3, /*modified_finished=*/2,
                     /*unmodified_finished=*/1);
  EXPECT_THAT(updated_ink_thumbnail_page_indices(), ElementsAre(0, 0));

  // Stroke with the eraser tool again, but follow the stroke inputs. This will
  // intersect with both strokes and erase them.
  SelectEraserTool();
  VerifyAndClearExpectations();
  ExpectNoStrokeAdded();
  ExpectUpdateStrokesActive(/*strokes_affected=*/2, /*expected_active=*/false);
  ApplyStrokeWithMouseAtPoints(
      kMouseDownPoint, base::span_from_ref(kMouseMovePoint), kMouseUpPoint);
  ApplyStrokeWithMouseAtPoints(
      kMouseDownPoint2, base::span_from_ref(kMouseMovePoint), kMouseUpPoint2);

  // Check that there are now no visible strokes.
  EXPECT_TRUE(VisibleStrokeInputPositions().empty());
  ExpectStrokeCounts(/*started=*/5, /*modified_finished=*/4,
                     /*unmodified_finished=*/1);
  EXPECT_THAT(updated_ink_thumbnail_page_indices(), ElementsAre(0, 0, 0, 0));
}

TEST_P(PdfInkModuleStrokeTest, EraseStrokesAcrossTwoPages) {
  EnableDrawAnnotationMode();
  InitializeVerticalTwoPageLayout();

  // Start out without any strokes.
  EXPECT_TRUE(StrokeInputPositions().empty());
  ExpectStrokeCounts(/*started=*/0, /*modified_finished=*/0,
                     /*unmodified_finished=*/0);
  EXPECT_TRUE(updated_ink_thumbnail_page_indices().empty());

  ExpectStrokesAdded(/*strokes_affected=*/2);
  ExpectNoUpdateStrokeActive();

  // A stroke in the first page generates a stroke only for that page.
  ApplyStrokeWithMouseAtPoints(
      kTwoPageVerticalLayoutPoint1InsidePage0,
      base::span_from_ref(kTwoPageVerticalLayoutPoint2InsidePage0),
      kTwoPageVerticalLayoutPoint3InsidePage0);
  EXPECT_THAT(StrokeInputPositions(), ElementsAre(Pair(0, SizeIs(1))));
  ExpectStrokeCounts(/*started=*/1, /*modified_finished=*/1,
                     /*unmodified_finished=*/0);
  EXPECT_THAT(updated_ink_thumbnail_page_indices(), ElementsAre(0));

  // A stroke in the second page generates a stroke only for that page.
  ApplyStrokeWithMouseAtPoints(
      kTwoPageVerticalLayoutPoint1InsidePage1,
      base::span_from_ref(kTwoPageVerticalLayoutPoint2InsidePage1),
      kTwoPageVerticalLayoutPoint3InsidePage1);
  EXPECT_THAT(StrokeInputPositions(),
              ElementsAre(Pair(0, SizeIs(1)), Pair(1, SizeIs(1))));
  ExpectStrokeCounts(/*started=*/2, /*modified_finished=*/2,
                     /*unmodified_finished=*/0);
  EXPECT_THAT(updated_ink_thumbnail_page_indices(), ElementsAre(0, 1));

  // Erasing across the two pages should erase everything.
  SelectEraserTool();
  VerifyAndClearExpectations();
  ExpectNoStrokeAdded();
  ExpectUpdateStrokesActive(/*strokes_affected=*/2, /*expected_active=*/false);
  ApplyStrokeWithMouseAtPoints(
      kTwoPageVerticalLayoutPoint1InsidePage0,
      std::vector<gfx::PointF>{kTwoPageVerticalLayoutPoint2InsidePage0,
                               kTwoPageVerticalLayoutPoint1InsidePage1},
      kTwoPageVerticalLayoutPoint3InsidePage1);
  EXPECT_TRUE(VisibleStrokeInputPositions().empty());
  ExpectStrokeCounts(/*started=*/3, /*modified_finished=*/3,
                     /*unmodified_finished=*/0);
  EXPECT_THAT(updated_ink_thumbnail_page_indices(), ElementsAre(0, 1, 0, 1));
}

TEST_P(PdfInkModuleStrokeTest, EraseStrokePageExitAndReentry) {
  EnableDrawAnnotationMode();
  InitializeVerticalTwoPageLayout();

  // Start out without any strokes.
  EXPECT_TRUE(StrokeInputPositions().empty());

  ApplyStrokeWithMouseAtPoints(kTwoPageVerticalLayoutPoint1InsidePage0,
                               kTwoPageVerticalLayoutPageExitAndReentryPoints,
                               kTwoPageVerticalLayoutPoint3InsidePage0);

  EXPECT_THAT(
      StrokeInputPositions(),
      ElementsAre(Pair(
          0,
          ElementsAre(ElementsAreArray(
                          kTwoPageVerticalLayoutPageExitAndReentrySegment1),
                      ElementsAreArray(
                          kTwoPageVerticalLayoutPageExitAndReentrySegment2)))));
  ExpectStrokeCounts(/*started=*/1, /*modified_finished=*/1,
                     /*unmodified_finished=*/0);
  EXPECT_THAT(updated_ink_thumbnail_page_indices(), ElementsAre(0));

  // Select the eraser tool and call ApplyStrokeWithMouseAtPoints() again with
  // the same arguments.
  SelectEraserTool();
  ApplyStrokeWithMouseAtPoints(kTwoPageVerticalLayoutPoint1InsidePage0,
                               kTwoPageVerticalLayoutPageExitAndReentryPoints,
                               kTwoPageVerticalLayoutPoint3InsidePage0);

  // The strokes are all still there, but none of them are visible.
  EXPECT_THAT(
      StrokeInputPositions(),
      ElementsAre(Pair(
          0,
          ElementsAre(ElementsAreArray(
                          kTwoPageVerticalLayoutPageExitAndReentrySegment1),
                      ElementsAreArray(
                          kTwoPageVerticalLayoutPageExitAndReentrySegment2)))));
  EXPECT_TRUE(VisibleStrokeInputPositions().empty());
  // Erasing increments the modified stroke count.
  ExpectStrokeCounts(/*started=*/2, /*modified_finished=*/2,
                     /*unmodified_finished=*/0);
  EXPECT_THAT(updated_ink_thumbnail_page_indices(), ElementsAre(0, 0));
}

TEST_P(PdfInkModuleStrokeTest, EraseStrokeWithTouch) {
  InitializeSimpleSinglePageBasicLayout();
  RunStrokeTouchCheckTest(/*annotation_mode_enabled=*/true);

  // Check that there are now some visible strokes.
  EXPECT_THAT(
      VisibleStrokeInputPositions(),
      ElementsAre(Pair(0, ElementsAre(ElementsAreArray(kMousePoints)))));
  ExpectStrokeCounts(/*started=*/1, /*modified_finished=*/1,
                     /*unmodified_finished=*/0);
  EXPECT_THAT(updated_ink_thumbnail_page_indices(), ElementsAre(0));

  // Stroke with the eraser tool.
  SelectEraserTool();
  const std::vector<base::span<const gfx::PointF>> touch_move_points{
      base::span_from_ref(kMouseMovePoint),
  };
  ApplyStrokeWithTouchAtPoints(base::span_from_ref(kMouseDownPoint),
                               touch_move_points,
                               base::span_from_ref(kMouseDownPoint));

  // Now there are no visible strokes left.
  EXPECT_TRUE(VisibleStrokeInputPositions().empty());
  // Erasing increments the modified stroke count.
  ExpectStrokeCounts(/*started=*/2, /*modified_finished=*/2,
                     /*unmodified_finished=*/0);
  EXPECT_THAT(updated_ink_thumbnail_page_indices(), ElementsAre(0, 0));

  // Stroke again. The stroke that have already been erased should stay erased.
  ApplyStrokeWithTouchAtPoints(base::span_from_ref(kMouseDownPoint),
                               touch_move_points,
                               base::span_from_ref(kMouseDownPoint));

  // Still no visible strokes.
  EXPECT_TRUE(VisibleStrokeInputPositions().empty());
  // Nothing got erased, so the modified count stays the same, and the
  // unmodified stroke count goes up by 1 instead.
  ExpectStrokeCounts(/*started=*/3, /*modified_finished=*/2,
                     /*unmodified_finished=*/1);
  EXPECT_THAT(updated_ink_thumbnail_page_indices(), ElementsAre(0, 0));

  // Stroke again with the mouse gets the same results.
  ApplyStrokeWithMouseAtMouseDownPoint();

  // Still no visible strokes.
  EXPECT_TRUE(VisibleStrokeInputPositions().empty());
  // Nothing got erased, so the modified count stays the same, and the
  // unmodified stroke count goes up by 1 instead.
  ExpectStrokeCounts(/*started=*/4, /*modified_finished=*/2,
                     /*unmodified_finished=*/2);
  EXPECT_THAT(updated_ink_thumbnail_page_indices(), ElementsAre(0, 0));
}

TEST_P(PdfInkModuleStrokeTest, EraseStrokeWithPen) {
  InitializeSimpleSinglePageBasicLayout();
  RunStrokePenCheckTest(/*annotation_mode_enabled=*/true);

  // Check that there are now some visible strokes.
  EXPECT_THAT(
      VisibleStrokeInputPositions(),
      ElementsAre(Pair(0, ElementsAre(ElementsAreArray(kMousePoints)))));
  ExpectStrokeCounts(/*started=*/1, /*modified_finished=*/1,
                     /*unmodified_finished=*/0);
  EXPECT_THAT(updated_ink_thumbnail_page_indices(), ElementsAre(0));

  // Stroke with the eraser tool.
  SelectEraserTool();
  const std::vector<base::span<const gfx::PointF>> pen_move_points{
      base::span_from_ref(kMouseMovePoint),
  };
  ApplyStrokeWithPenAtPoints(base::span_from_ref(kMouseDownPoint),
                             pen_move_points,
                             base::span_from_ref(kMouseDownPoint));

  // Now there are no visible strokes left.
  EXPECT_TRUE(VisibleStrokeInputPositions().empty());
  // Erasing increments the modified stroke count.
  ExpectStrokeCounts(/*started=*/2, /*modified_finished=*/2,
                     /*unmodified_finished=*/0);
  EXPECT_THAT(updated_ink_thumbnail_page_indices(), ElementsAre(0, 0));

  // Stroke again. The stroke that have already been erased should stay erased.
  ApplyStrokeWithPenAtPoints(base::span_from_ref(kMouseDownPoint),
                             pen_move_points,
                             base::span_from_ref(kMouseDownPoint));

  // Still no visible strokes.
  EXPECT_TRUE(VisibleStrokeInputPositions().empty());
  // Nothing got erased, so the modified count stays the same, and the
  // unmodified stroke count goes up by 1 instead.
  ExpectStrokeCounts(/*started=*/3, /*modified_finished=*/2,
                     /*unmodified_finished=*/1);
  EXPECT_THAT(updated_ink_thumbnail_page_indices(), ElementsAre(0, 0));

  // Stroke again with the mouse gets the same results.
  ApplyStrokeWithMouseAtMouseDownPoint();

  // Still no visible strokes.
  EXPECT_TRUE(VisibleStrokeInputPositions().empty());
  // Nothing got erased, so the modified count stays the same, and the
  // unmodified stroke count goes up by 1 instead.
  ExpectStrokeCounts(/*started=*/4, /*modified_finished=*/2,
                     /*unmodified_finished=*/2);
  EXPECT_THAT(updated_ink_thumbnail_page_indices(), ElementsAre(0, 0));
}

TEST_P(PdfInkModuleStrokeTest, StrokeMissedEndEventThenMouseDown) {
  EnableDrawAnnotationMode();
  InitializeSimpleSinglePageBasicLayout();

  blink::WebMouseEvent mouse_down_event =
      CreateLeftClickWebMouseEventAtPosition(kMouseDownPoint);
  EXPECT_TRUE(ink_module().HandleInputEvent(mouse_down_event));

  blink::WebMouseEvent mouse_move_event =
      CreateLeftClickWebMouseMoveEventAtPosition(kMouseMovePoint);
  EXPECT_TRUE(ink_module().HandleInputEvent(mouse_move_event));

  // If the mouse up event went missing during stroking, the next mouse down
  // event should not cause a crash.
  EXPECT_TRUE(ink_module().HandleInputEvent(mouse_down_event));
}

TEST_P(PdfInkModuleStrokeTest, StrokeMissedEndEventThenMouseMoveDuringDrawing) {
  EnableDrawAnnotationMode();
  InitializeSimpleSinglePageBasicLayout();

  // No need to distinguish between pen or highlighter here.
  EXPECT_TRUE(ink_module().OnMessage(CreateGetAnnotationBrushMessage("pen")));

  RunStrokeMissedEndEventThenMouseMoveTest();
}

TEST_P(PdfInkModuleStrokeTest, StrokeMissedEndEventThenMouseMoveDuringErasing) {
  EnableDrawAnnotationMode();
  InitializeSimpleSinglePageBasicLayout();

  SelectEraserTool();

  RunStrokeMissedEndEventThenMouseMoveTest();
}

TEST_P(PdfInkModuleStrokeTest, StrokeWithNoEndEventThenTouchStart) {
  EnableDrawAnnotationMode();
  InitializeSimpleSinglePageBasicLayout();

  blink::WebMouseEvent mouse_down_event =
      CreateLeftClickWebMouseEventAtPosition(kMouseDownPoint);
  EXPECT_TRUE(ink_module().HandleInputEvent(mouse_down_event));

  blink::WebMouseEvent mouse_move_event =
      CreateLeftClickWebMouseMoveEventAtPosition(kMouseMovePoint);
  EXPECT_TRUE(ink_module().HandleInputEvent(mouse_move_event));

  // If the mouse up event has yet to happen, the next touch start event
  // should not cause a crash.
  EXPECT_TRUE(ink_module().HandleInputEvent(
      CreateTouchEvent(blink::WebInputEvent::Type::kTouchStart,
                       base::span_from_ref(kMouseDownPoint))));
}

TEST_P(PdfInkModuleStrokeTest, ChangeBrushColorDuringDrawing) {
  EnableDrawAnnotationMode();
  InitializeSimpleSinglePageBasicLayout();

  // Start drawing a stroke with a black pen.  The stroke will not finish
  // until the mouse-up event.
  EXPECT_CALL(client(), StrokeAdded(_, _, _)).Times(0);
  TestAnnotationBrushMessageParams black_pen_message_params{
      SkColorSetRGB(0x00, 0x00, 0x00),
      /*size=*/3.0};
  SelectBrushTool(PdfInkBrush::Type::kPen, black_pen_message_params);

  blink::WebMouseEvent mouse_down_event =
      CreateLeftClickWebMouseEventAtPosition(kLeftVerticalStrokePoint1);
  EXPECT_TRUE(ink_module().HandleInputEvent(mouse_down_event));

  // While the stroke is still in progress, change the pen color.  This has no
  // immediate effect on the in-progress stroke.
  TestAnnotationBrushMessageParams red_pen_message_params{
      SkColorSetRGB(0xF2, 0x8B, 0x82),
      /*size=*/3.0};
  SelectBrushTool(PdfInkBrush::Type::kPen, red_pen_message_params);
  VerifyAndClearExpectations();

  // Continue with mouse movement and then mouse up at a new location.  Notice
  // that the events are handled and the new stroke is added.
  static constexpr int kPageIndex = 0;
  EXPECT_CALL(client(), StrokeAdded(kPageIndex, InkStrokeId(0),
                                    InkStrokeBrushColorEq(SK_ColorBLACK)));
  blink::WebMouseEvent mouse_move_event =
      CreateLeftClickWebMouseMoveEventAtPosition(kLeftVerticalStrokePoint2);
  EXPECT_TRUE(ink_module().HandleInputEvent(mouse_move_event));
  blink::WebMouseEvent mouse_up_event =
      CreateLeftClickWebMouseUpEventAtPosition(kLeftVerticalStrokePoint2);
  EXPECT_TRUE(ink_module().HandleInputEvent(mouse_up_event));
  VerifyAndClearExpectations();

  // Do another stroke.  Notice that the changed pen color is in effect for
  // the new stroke that is added.
  EXPECT_CALL(
      client(),
      StrokeAdded(kPageIndex, InkStrokeId(1),
                  InkStrokeBrushColorEq(SkColorSetRGB(0xF2, 0x8B, 0x82))));
  EXPECT_TRUE(ink_module().HandleInputEvent(mouse_down_event));
  EXPECT_TRUE(ink_module().HandleInputEvent(mouse_up_event));
}

TEST_P(PdfInkModuleStrokeTest, ChangeBrushSizeDuringDrawing) {
  EnableDrawAnnotationMode();
  InitializeSimpleSinglePageBasicLayout();

  // Start drawing a stroke with a black pen.  The stroke will not finish
  // until the mouse-up event.  The cursor image will be updated only when
  // there is not a stroke in progress.
  EXPECT_CALL(client(), StrokeAdded(_, _, _)).Times(0);
  EXPECT_CALL(client(),
              UpdateInkCursor(CursorBitmapImageSizeEq(SkISize(6, 6))));
  TestAnnotationBrushMessageParams message_params{
      SkColorSetRGB(0x00, 0x00, 0x00),
      /*size=*/2.0};
  SelectBrushTool(PdfInkBrush::Type::kPen, message_params);

  blink::WebMouseEvent mouse_down_event =
      CreateLeftClickWebMouseEventAtPosition(kLeftVerticalStrokePoint1);
  EXPECT_TRUE(ink_module().HandleInputEvent(mouse_down_event));

  // While the stroke is still in progress, change the pen size.  This has no
  // immediate effect on the in-progress stroke.
  message_params.size = 6.0f;
  SelectBrushTool(PdfInkBrush::Type::kPen, message_params);
  VerifyAndClearExpectations();

  // Continue with mouse movement and then mouse up at a new location.  Notice
  // that the events are handled and the new stroke is added.  The cursor image
  // also gets updated once the stroke has started.
  // also gets updated once the stroke has finished.
  static constexpr int kPageIndex = 0;
  {
    InSequence seq;
    EXPECT_CALL(client(), StrokeAdded(kPageIndex, InkStrokeId(0),
                                      InkStrokeBrushSizeEq(2.0f)));
    EXPECT_CALL(client(),
                UpdateInkCursor(CursorBitmapImageSizeEq(SkISize(8, 8))));
  }
  blink::WebMouseEvent mouse_move_event =
      CreateLeftClickWebMouseMoveEventAtPosition(kLeftVerticalStrokePoint2);
  EXPECT_TRUE(ink_module().HandleInputEvent(mouse_move_event));
  blink::WebMouseEvent mouse_up_event =
      CreateLeftClickWebMouseUpEventAtPosition(kLeftVerticalStrokePoint2);
  EXPECT_TRUE(ink_module().HandleInputEvent(mouse_up_event));
  VerifyAndClearExpectations();

  // Do another stroke.  Notice that the changed pen color has now taken
  // effect for the new stroke that is added.
  EXPECT_CALL(client(), StrokeAdded(kPageIndex, InkStrokeId(1),
                                    InkStrokeBrushSizeEq(6.0f)));
  EXPECT_TRUE(ink_module().HandleInputEvent(mouse_down_event));
  EXPECT_TRUE(ink_module().HandleInputEvent(mouse_up_event));
}

TEST_P(PdfInkModuleStrokeTest, ChangeToEraserDuringDrawing) {
  InitializeSimpleSinglePageBasicLayout();

  // Draw an initial stroke.
  RunStrokeCheckTest(/*annotation_mode_enabled=*/true);

  // Start drawing another stroke.
  static constexpr int kPageIndex = 0;
  EXPECT_CALL(client(), StrokeAdded(kPageIndex, InkStrokeId(1), _));
  EXPECT_CALL(client(), UpdateStrokeActive(_, _, _)).Times(0);
  blink::WebMouseEvent mouse_down_event =
      CreateLeftClickWebMouseEventAtPosition(kRightVerticalStrokePoint1);
  EXPECT_TRUE(ink_module().HandleInputEvent(mouse_down_event));

  // While the stroke is still in progress, change to the eraser tool.  This
  // causes the in-progress stroke to finish even before the mouse-up event.
  SelectEraserTool();
  VerifyAndClearExpectations();

  // Continue with mouse movement and then mouse up at a new location.  Notice
  // that the events are not handled and there is no further effect for adding
  // or erasing strokes, since the prior stroke was already started.
  // or erasing strokes, since the prior stroke was already finished.
  EXPECT_CALL(client(), StrokeAdded(_, _, _)).Times(0);
  EXPECT_CALL(client(), UpdateStrokeActive(_, _, _)).Times(0);
  blink::WebMouseEvent mouse_move_event =
      CreateLeftClickWebMouseMoveEventAtPosition(kRightVerticalStrokePoint2);
  EXPECT_FALSE(ink_module().HandleInputEvent(mouse_move_event));
  blink::WebMouseEvent mouse_up_event =
      CreateLeftClickWebMouseUpEventAtPosition(kRightVerticalStrokePoint2);
  EXPECT_FALSE(ink_module().HandleInputEvent(mouse_up_event));
  VerifyAndClearExpectations();

  // Do a simple stroke in the same place where the last stroke was added.
  // Notice that the changed tool type has taken effect and the recently added
  // stroke is erased.
  EXPECT_CALL(client(), StrokeAdded(_, _, _)).Times(0);
  EXPECT_CALL(client(),
              UpdateStrokeActive(kPageIndex, InkStrokeId(1), /*active=*/false));
  EXPECT_TRUE(ink_module().HandleInputEvent(mouse_down_event));
  EXPECT_TRUE(ink_module().HandleInputEvent(mouse_up_event));
}

TEST_P(PdfInkModuleStrokeTest, ChangeToDrawingDuringErasing) {
  EnableDrawAnnotationMode();
  InitializeSimpleSinglePageBasicLayout();

  // Initialize to have two strokes, so there is something to erase.
  static constexpr int kPageIndex = 0;
  EXPECT_CALL(client(), StrokeAdded(kPageIndex, InkStrokeId(0), _));
  EXPECT_CALL(client(), StrokeAdded(kPageIndex, InkStrokeId(1), _));
  EXPECT_CALL(client(), UpdateStrokeActive(_, _, _)).Times(0);

  ApplyStrokeWithMouseAtPoints(kLeftVerticalStrokePoint1,
                               base::span_from_ref(kLeftVerticalStrokePoint2),
                               kLeftVerticalStrokePoint2);

  ApplyStrokeWithMouseAtPoints(kRightVerticalStrokePoint1,
                               base::span_from_ref(kRightVerticalStrokePoint2),
                               kRightVerticalStrokePoint2);

  // Set up for erasing.
  SelectEraserTool();
  VerifyAndClearExpectations();

  // Start erasing from where the first stroke was added.
  EXPECT_CALL(client(), StrokeAdded(_, _, _)).Times(0);
  EXPECT_CALL(client(), UpdateStrokeActive(kPageIndex, InkStrokeId(0), _));
  blink::WebMouseEvent mouse_down_event =
      CreateLeftClickWebMouseEventAtPosition(kLeftVerticalStrokePoint1);
  EXPECT_TRUE(ink_module().HandleInputEvent(mouse_down_event));

  // While the stroke is still in progress, change the input tool type to a
  // pen.  Note that this causes the in-progress erase stroke to finish even
  // before the mouse-up event.
  TestAnnotationBrushMessageParams message_params{
      SkColorSetRGB(0x00, 0x00, 0x00),
      /*size=*/8.0};
  SelectBrushTool(PdfInkBrush::Type::kPen, message_params);
  VerifyAndClearExpectations();

  // Continue with mouse movement and then mouse up at a new location.  Notice
  // that the events are not handled and there is no further effect for adding
  // or erasing strokes, since the prior stroke was already started.
  // or erasing strokes, since the prior stroke was already finished.
  EXPECT_CALL(client(), StrokeAdded(_, _, _)).Times(0);
  EXPECT_CALL(client(), UpdateStrokeActive(_, _, _)).Times(0);
  blink::WebMouseEvent mouse_move_event =
      CreateLeftClickWebMouseMoveEventAtPosition(kRightVerticalStrokePoint2);
  EXPECT_FALSE(ink_module().HandleInputEvent(mouse_move_event));
  blink::WebMouseEvent mouse_up_event =
      CreateLeftClickWebMouseUpEventAtPosition(kRightVerticalStrokePoint1);
  EXPECT_FALSE(ink_module().HandleInputEvent(mouse_up_event));
  VerifyAndClearExpectations();

  // Do another stroke.  Notice that the changed tool type has taken effect
  // and a new stroke is added.
  EXPECT_CALL(client(), StrokeAdded(kPageIndex, InkStrokeId(2), _));
  EXPECT_CALL(client(), UpdateStrokeActive(_, _, _)).Times(0);
  EXPECT_TRUE(ink_module().HandleInputEvent(mouse_down_event));
  EXPECT_TRUE(ink_module().HandleInputEvent(mouse_up_event));
}

TEST_P(PdfInkModuleStrokeTest, ChangeDrawingBrushTypeDuringDrawing) {
  EnableDrawAnnotationMode();
  InitializeSimpleSinglePageBasicLayout();

  // Start drawing a stroke with a black pen.  The stroke will not finish
  // until the mouse-up event.  The cursor image will be updated only if a
  // stroke is not in progress.
  EXPECT_CALL(client(), StrokeAdded(_, _, _)).Times(0);
  EXPECT_CALL(client(),
              UpdateInkCursor(CursorBitmapImageSizeEq(SkISize(6, 6))));
  TestAnnotationBrushMessageParams pen_message_params{
      SkColorSetRGB(0x00, 0x00, 0x00),
      /*size=*/2.0};
  SelectBrushTool(PdfInkBrush::Type::kPen, pen_message_params);

  blink::WebMouseEvent mouse_down_event =
      CreateLeftClickWebMouseEventAtPosition(kLeftVerticalStrokePoint1);
  EXPECT_TRUE(ink_module().HandleInputEvent(mouse_down_event));

  // While the stroke is still in progress, change the input tool type to a
  // highlighter.  The entire stroke changes to this new type.
  TestAnnotationBrushMessageParams highlighter_message_params{
      SkColorSetRGB(0xDD, 0xF3, 0x00),
      /*size=*/8.0};
  SelectBrushTool(PdfInkBrush::Type::kHighlighter, highlighter_message_params);
  VerifyAndClearExpectations();

  // Continue with mouse movement and then mouse up at a new location.  Notice
  // that the events are handled and the new stroke is added.  The cursor gets
  // updated to reflect the tool change to highlighter only after the stroke
  // is completed.
  static constexpr int kPageIndex = 0;
  {
    InSequence seq;
    EXPECT_CALL(
        client(),
        StrokeAdded(kPageIndex, InkStrokeId(0),
                    InkStrokeDrawingBrushTypeEq(PdfInkBrush::Type::kPen)));
    EXPECT_CALL(client(),
                UpdateInkCursor(CursorBitmapImageSizeEq(SkISize(10, 10))));
  }
  blink::WebMouseEvent mouse_move_event =
      CreateLeftClickWebMouseMoveEventAtPosition(kLeftVerticalStrokePoint2);
  EXPECT_TRUE(ink_module().HandleInputEvent(mouse_move_event));
  blink::WebMouseEvent mouse_up_event =
      CreateLeftClickWebMouseUpEventAtPosition(kLeftVerticalStrokePoint2);
  EXPECT_TRUE(ink_module().HandleInputEvent(mouse_up_event));
  VerifyAndClearExpectations();

  // Do another stroke.  Notice that the changed input tool type has taken
  // effect for the new stroke that is added.
  EXPECT_CALL(client(), StrokeAdded(kPageIndex, InkStrokeId(1),
                                    InkStrokeDrawingBrushTypeEq(
                                        PdfInkBrush::Type::kHighlighter)));
  EXPECT_TRUE(ink_module().HandleInputEvent(mouse_down_event));
  EXPECT_TRUE(ink_module().HandleInputEvent(mouse_up_event));
}

TEST_P(PdfInkModuleStrokeTest, EventWithPastTimeStamp) {
  EnableDrawAnnotationMode();
  InitializeSimpleSinglePageBasicLayout();

  blink::WebMouseEvent mouse_down_event =
      CreateLeftClickWebMouseEventAtPosition(kMouseDownPoint);
  EXPECT_TRUE(ink_module().HandleInputEvent(mouse_down_event));

  blink::WebMouseEvent mouse_move_event =
      CreateLeftClickWebMouseMoveEventAtPosition(kMouseMovePoint);
  // Simulate a condition from https://crbug.com/421120183 where the event time
  // stamp goes backwards in time. This should not crash.
  mouse_move_event.SetTimeStamp(mouse_move_event.TimeStamp() -
                                base::Milliseconds(10));
  EXPECT_TRUE(ink_module().HandleInputEvent(mouse_move_event));

  blink::WebMouseEvent mouse_up_event =
      CreateLeftClickWebMouseUpEventAtPosition(kMouseUpPoint);
  EXPECT_TRUE(ink_module().HandleInputEvent(mouse_up_event));

  EXPECT_EQ(2, GetInputOfTypeCountForPage(
                   /*page_index=*/0, ink::StrokeInput::ToolType::kMouse));
}

class PdfInkModuleUndoRedoTest : public PdfInkModuleStrokeTest {
 protected:
  void PerformUndo() {
    EXPECT_TRUE(
        ink_module().OnMessage(CreateSetAnnotationUndoRedoMessageForTesting(
            TestAnnotationUndoRedoMessageType::kUndo)));
  }
  void PerformRedo() {
    EXPECT_TRUE(
        ink_module().OnMessage(CreateSetAnnotationUndoRedoMessageForTesting(
            TestAnnotationUndoRedoMessageType::kRedo)));
  }
};

TEST_P(PdfInkModuleUndoRedoTest, UndoRedoEmpty) {
  InitializeSimpleSinglePageBasicLayout();
  EnableDrawAnnotationMode();

  EXPECT_TRUE(StrokeInputPositions().empty());
  EXPECT_TRUE(VisibleStrokeInputPositions().empty());

  // Spurious undo message is a no-op.
  PerformUndo();
  EXPECT_TRUE(StrokeInputPositions().empty());
  EXPECT_TRUE(VisibleStrokeInputPositions().empty());

  // Spurious redo message is a no-op.
  PerformRedo();
  EXPECT_TRUE(StrokeInputPositions().empty());
  EXPECT_TRUE(VisibleStrokeInputPositions().empty());
}

TEST_P(PdfInkModuleUndoRedoTest, UndoRedoBasic) {
  InitializeSimpleSinglePageBasicLayout();
  ExpectStrokesAdded(/*strokes_affected=*/1);
  ExpectUpdateStrokesActive(/*strokes_affected=*/1, /*expect_active=*/false);
  RunStrokeCheckTest(/*annotation_mode_enabled=*/true);

  const auto kMatcher =
      ElementsAre(Pair(0, ElementsAre(ElementsAreArray(kMousePoints))));
  EXPECT_THAT(StrokeInputPositions(), kMatcher);
  EXPECT_THAT(VisibleStrokeInputPositions(), kMatcher);
  // RunStrokeCheckTest() performed the only stroke.
  ExpectStrokeCounts(/*started=*/1, /*modified_finished=*/1,
                     /*unmodified_finished=*/0);
  EXPECT_THAT(updated_ink_thumbnail_page_indices(), ElementsAre(0));

  PerformUndo();
  EXPECT_THAT(StrokeInputPositions(), kMatcher);
  EXPECT_TRUE(VisibleStrokeInputPositions().empty());
  // Undo/redo here and below do not trigger StrokeFinished().
  ExpectStrokeCounts(/*started=*/1, /*modified_finished=*/1,
                     /*unmodified_finished=*/0);
  EXPECT_THAT(updated_ink_thumbnail_page_indices(), ElementsAre(0, 0));

  // Spurious undo message is a no-op.
  VerifyAndClearExpectations();
  ExpectNoStrokeAdded();
  ExpectNoUpdateStrokeActive();
  PerformUndo();
  EXPECT_THAT(StrokeInputPositions(), kMatcher);
  EXPECT_TRUE(VisibleStrokeInputPositions().empty());
  ExpectStrokeCounts(/*started=*/1, /*modified_finished=*/1,
                     /*unmodified_finished=*/0);
  EXPECT_THAT(updated_ink_thumbnail_page_indices(), ElementsAre(0, 0));

  VerifyAndClearExpectations();
  ExpectNoStrokeAdded();
  ExpectUpdateStrokesActive(/*strokes_affected=*/1, /*expect_active=*/true);
  PerformRedo();
  EXPECT_THAT(StrokeInputPositions(), kMatcher);
  EXPECT_THAT(VisibleStrokeInputPositions(), kMatcher);
  ExpectStrokeCounts(/*started=*/1, /*modified_finished=*/1,
                     /*unmodified_finished=*/0);
  EXPECT_THAT(updated_ink_thumbnail_page_indices(), ElementsAre(0, 0, 0));

  // Spurious redo message is a no-op.
  VerifyAndClearExpectations();
  ExpectNoStrokeAdded();
  ExpectNoUpdateStrokeActive();
  PerformRedo();
  EXPECT_THAT(StrokeInputPositions(), kMatcher);
  EXPECT_THAT(VisibleStrokeInputPositions(), kMatcher);
  ExpectStrokeCounts(/*started=*/1, /*modified_finished=*/1,
                     /*unmodified_finished=*/0);
  EXPECT_THAT(updated_ink_thumbnail_page_indices(), ElementsAre(0, 0, 0));
}

TEST_P(PdfInkModuleUndoRedoTest, UndoRedoInvalidationsBasic) {
  InitializeSimpleSinglePageBasicLayout();
  RunStrokeCheckTest(/*annotation_mode_enabled=*/true);

  // The default brush param size is 3.0.  Invalidation areas are in screen
  // coordinates.
  constexpr gfx::Rect kInvalidationAreaMouseDown(gfx::Point(8, 13),
                                                 gfx::Size(4, 4));
  constexpr gfx::Rect kInvalidationAreaMouseMove(gfx::Point(8, 13),
                                                 gfx::Size(14, 14));
  constexpr gfx::Rect kInvalidationAreaMouseUp(gfx::Point(18, 15),
                                               gfx::Size(14, 12));
  // This size is smaller than the area of the merged invalidation constants
  // above because InkStrokeModeler modeled the "V" shaped input into an input
  // with a much gentler line slope.
  constexpr gfx::Rect kInvalidationAreaEntireStroke(gfx::Point(7, 12),
                                                    gfx::Size(27, 9));
  EXPECT_THAT(
      client().invalidations(),
      ElementsAre(kInvalidationAreaMouseDown, kInvalidationAreaMouseMove,
                  kInvalidationAreaMouseUp, kInvalidationAreaEntireStroke));

  PerformUndo();
  EXPECT_THAT(
      client().invalidations(),
      ElementsAre(kInvalidationAreaMouseDown, kInvalidationAreaMouseMove,
                  kInvalidationAreaMouseUp, kInvalidationAreaEntireStroke,
                  kInvalidationAreaEntireStroke));

  PerformRedo();
  EXPECT_THAT(
      client().invalidations(),
      ElementsAre(kInvalidationAreaMouseDown, kInvalidationAreaMouseMove,
                  kInvalidationAreaMouseUp, kInvalidationAreaEntireStroke,
                  kInvalidationAreaEntireStroke,
                  kInvalidationAreaEntireStroke));
}

TEST_P(PdfInkModuleUndoRedoTest, UndoRedoInvalidationsScaledRotated90) {
  InitializeScaledLandscapeSinglePageBasicLayout();
  client().set_orientation(PageOrientation::kClockwise90);
  client().set_zoom(2.0f);
  RunStrokeCheckTest(/*annotation_mode_enabled=*/true);

  // The default brush param size is 3.0.  Invalidation areas are in screen
  // coordinates.
  constexpr gfx::Rect kInvalidationAreaMouseDown(gfx::Point(8, 13),
                                                 gfx::Size(4, 4));
  constexpr gfx::Rect kInvalidationAreaMouseMove(gfx::Point(8, 13),
                                                 gfx::Size(14, 14));
  constexpr gfx::Rect kInvalidationAreaMouseUp(gfx::Point(18, 15),
                                               gfx::Size(14, 12));
  // This size is smaller than the area of the merged invalidation constants
  // above because InkStrokeModeler modeled the "V" shaped input into an input
  // with a much gentler line slope.
  constexpr gfx::Rect kInvalidationAreaEntireStroke(gfx::Point(6, 11),
                                                    gfx::Size(29, 11));
  EXPECT_THAT(
      client().invalidations(),
      ElementsAre(kInvalidationAreaMouseDown, kInvalidationAreaMouseMove,
                  kInvalidationAreaMouseUp, kInvalidationAreaEntireStroke));

  PerformUndo();
  EXPECT_THAT(
      client().invalidations(),
      ElementsAre(kInvalidationAreaMouseDown, kInvalidationAreaMouseMove,
                  kInvalidationAreaMouseUp, kInvalidationAreaEntireStroke,
                  kInvalidationAreaEntireStroke));

  PerformRedo();
  EXPECT_THAT(
      client().invalidations(),
      ElementsAre(kInvalidationAreaMouseDown, kInvalidationAreaMouseMove,
                  kInvalidationAreaMouseUp, kInvalidationAreaEntireStroke,
                  kInvalidationAreaEntireStroke,
                  kInvalidationAreaEntireStroke));
}

TEST_P(PdfInkModuleUndoRedoTest, UndoRedoAnnotationModeDisabled) {
  InitializeSimpleSinglePageBasicLayout();
  RunStrokeCheckTest(/*annotation_mode_enabled=*/true);

  const auto kMatcher =
      ElementsAre(Pair(0, ElementsAre(ElementsAreArray(kMousePoints))));
  EXPECT_THAT(StrokeInputPositions(), kMatcher);
  EXPECT_THAT(VisibleStrokeInputPositions(), kMatcher);
  // RunStrokeCheckTest() performed the only stroke.
  ExpectStrokeCounts(/*started=*/1, /*modified_finished=*/1,
                     /*unmodified_finished=*/0);
  EXPECT_THAT(updated_ink_thumbnail_page_indices(), ElementsAre(0));

  // Disable annotation mode. Undo/redo should still work.
  EXPECT_TRUE(ink_module().OnMessage(
      CreateSetAnnotationModeMessageForTesting(InkAnnotationMode::kOff)));
  EXPECT_FALSE(ink_module().enabled());

  PerformUndo();
  EXPECT_THAT(StrokeInputPositions(), kMatcher);
  EXPECT_TRUE(VisibleStrokeInputPositions().empty());
  ExpectStrokeCounts(/*started=*/1, /*modified_finished=*/1,
                     /*unmodified_finished=*/0);
  EXPECT_THAT(updated_ink_thumbnail_page_indices(), ElementsAre(0, 0));

  PerformRedo();
  EXPECT_THAT(StrokeInputPositions(), kMatcher);
  EXPECT_THAT(VisibleStrokeInputPositions(), kMatcher);
  ExpectStrokeCounts(/*started=*/1, /*modified_finished=*/1,
                     /*unmodified_finished=*/0);
  EXPECT_THAT(updated_ink_thumbnail_page_indices(), ElementsAre(0, 0, 0));
}

TEST_P(PdfInkModuleUndoRedoTest, UndoRedoBetweenDraws) {
  InitializeSimpleSinglePageBasicLayout();
  RunStrokeCheckTest(/*annotation_mode_enabled=*/true);

  constexpr gfx::PointF kMouseDownPoint1 = gfx::PointF(11.0f, 15.0f);
  constexpr gfx::PointF kMouseMovePoint1 = gfx::PointF(21.0f, 25.0f);
  constexpr gfx::PointF kMouseUpPoint1 = gfx::PointF(31.0f, 17.0f);
  ApplyStrokeWithMouseAtPoints(
      kMouseDownPoint1, base::span_from_ref(kMouseMovePoint1), kMouseUpPoint1);

  constexpr gfx::PointF kMouseDownPoint2 = gfx::PointF(12.0f, 15.0f);
  constexpr gfx::PointF kMouseMovePoint2 = gfx::PointF(22.0f, 25.0f);
  constexpr gfx::PointF kMouseUpPoint2 = gfx::PointF(32.0f, 17.0f);
  ApplyStrokeWithMouseAtPoints(
      kMouseDownPoint2, base::span_from_ref(kMouseMovePoint2), kMouseUpPoint2);

  constexpr gfx::PointF kMouseDownPoint3 = gfx::PointF(13.0f, 15.0f);
  constexpr gfx::PointF kMouseMovePoint3 = gfx::PointF(23.0f, 25.0f);
  constexpr gfx::PointF kMouseUpPoint3 = gfx::PointF(33.0f, 17.0f);
  ApplyStrokeWithMouseAtPoints(
      kMouseDownPoint3, base::span_from_ref(kMouseMovePoint3), kMouseUpPoint3);

  // After drawing 4 strokes above, there should be 4 strokes that are all
  // visible.
  const auto kInitial4StrokeMatchers = {
      ElementsAre(kMouseDownPoint, kMouseMovePoint, kMouseUpPoint),
      ElementsAre(kMouseDownPoint1, kMouseMovePoint1, kMouseUpPoint1),
      ElementsAre(kMouseDownPoint2, kMouseMovePoint2, kMouseUpPoint2),
      ElementsAre(kMouseDownPoint3, kMouseMovePoint3, kMouseUpPoint3)};
  const auto kInitial4StrokeMatchersSpan = base::span(kInitial4StrokeMatchers);
  EXPECT_THAT(
      StrokeInputPositions(),
      ElementsAre(Pair(0, ElementsAreArray(kInitial4StrokeMatchersSpan))));
  EXPECT_THAT(
      VisibleStrokeInputPositions(),
      ElementsAre(Pair(0, ElementsAreArray(kInitial4StrokeMatchersSpan))));

  // Undo makes 3 strokes visible.
  PerformUndo();
  EXPECT_THAT(
      StrokeInputPositions(),
      ElementsAre(Pair(0, ElementsAreArray(kInitial4StrokeMatchersSpan))));
  EXPECT_THAT(VisibleStrokeInputPositions(),
              ElementsAre(Pair(
                  0, ElementsAreArray(kInitial4StrokeMatchersSpan.first(3u)))));

  // Undo again makes 2 strokes visible.
  PerformUndo();
  EXPECT_THAT(
      StrokeInputPositions(),
      ElementsAre(Pair(0, ElementsAreArray(kInitial4StrokeMatchersSpan))));
  EXPECT_THAT(VisibleStrokeInputPositions(),
              ElementsAre(Pair(
                  0, ElementsAreArray(kInitial4StrokeMatchersSpan.first(2u)))));

  constexpr int kPageIndex = 0;
  EXPECT_CALL(client(), DiscardStroke(kPageIndex, InkStrokeId(2)));
  EXPECT_CALL(client(), DiscardStroke(kPageIndex, InkStrokeId(3)));

  ApplyStrokeWithMouseAtPoints(
      kMouseDownPoint3, base::span_from_ref(kMouseMovePoint3), kMouseUpPoint3);
  VerifyAndClearExpectations();

  EXPECT_CALL(client(), DiscardStroke(_, _)).Times(0);

  // The 2 strokes that were undone have been discarded, and the newly drawn
  // stroke takes their place.
  const auto kNext3StrokeMatchers = {
      ElementsAre(kMouseDownPoint, kMouseMovePoint, kMouseUpPoint),
      ElementsAre(kMouseDownPoint1, kMouseMovePoint1, kMouseUpPoint1),
      ElementsAre(kMouseDownPoint3, kMouseMovePoint3, kMouseUpPoint3)};
  const auto kNext3StrokeMatchersSpan = base::span(kNext3StrokeMatchers);
  EXPECT_THAT(StrokeInputPositions(),
              ElementsAre(Pair(0, ElementsAreArray(kNext3StrokeMatchersSpan))));
  EXPECT_THAT(VisibleStrokeInputPositions(),
              ElementsAre(Pair(0, ElementsAreArray(kNext3StrokeMatchersSpan))));

  // Undo makes 2 strokes visible.
  PerformUndo();
  EXPECT_THAT(StrokeInputPositions(),
              ElementsAre(Pair(0, ElementsAreArray(kNext3StrokeMatchersSpan))));
  EXPECT_THAT(VisibleStrokeInputPositions(),
              ElementsAre(Pair(
                  0, ElementsAreArray(kNext3StrokeMatchersSpan.first(2u)))));

  // Undo again makes 1 strokes visible.
  PerformUndo();
  EXPECT_THAT(StrokeInputPositions(),
              ElementsAre(Pair(0, ElementsAreArray(kNext3StrokeMatchersSpan))));
  EXPECT_THAT(VisibleStrokeInputPositions(),
              ElementsAre(Pair(
                  0, ElementsAreArray(kNext3StrokeMatchersSpan.first(1u)))));

  // Undo again makes no strokes visible.
  PerformUndo();
  EXPECT_THAT(StrokeInputPositions(),
              ElementsAre(Pair(0, ElementsAreArray(kNext3StrokeMatchersSpan))));
  EXPECT_TRUE(VisibleStrokeInputPositions().empty());
  VerifyAndClearExpectations();

  EXPECT_CALL(client(), DiscardStroke(kPageIndex, InkStrokeId(0)));
  EXPECT_CALL(client(), DiscardStroke(kPageIndex, InkStrokeId(1)));
  EXPECT_CALL(client(), DiscardStroke(kPageIndex, InkStrokeId(2)));

  ApplyStrokeWithMouseAtPoints(
      kMouseDownPoint2, base::span_from_ref(kMouseMovePoint2), kMouseUpPoint2);

  // All strokes were undone, so they all got discarded. The newly drawn stroke
  // is the only one remaining.
  const auto kFinal1StrokeMatcher =
      ElementsAre(kMouseDownPoint2, kMouseMovePoint2, kMouseUpPoint2);
  EXPECT_THAT(StrokeInputPositions(),
              ElementsAre(Pair(0, ElementsAre(kFinal1StrokeMatcher))));
  EXPECT_THAT(VisibleStrokeInputPositions(),
              ElementsAre(Pair(0, ElementsAre(kFinal1StrokeMatcher))));
}

TEST_P(PdfInkModuleUndoRedoTest, UndoRedoOnTwoPages) {
  EnableDrawAnnotationMode();
  InitializeVerticalTwoPageLayout();

  ApplyStrokeWithMouseAtPoints(
      kTwoPageVerticalLayoutPoint1InsidePage0,
      base::span_from_ref(kTwoPageVerticalLayoutPoint2InsidePage0),
      kTwoPageVerticalLayoutPoint3InsidePage0);
  ApplyStrokeWithMouseAtPoints(
      kTwoPageVerticalLayoutPoint1InsidePage1,
      base::span_from_ref(kTwoPageVerticalLayoutPoint2InsidePage1),
      kTwoPageVerticalLayoutPoint3InsidePage1);

  // Canonical coordinates.
  const auto kPage0Matcher =
      Pair(0, ElementsAre(ElementsAre(gfx::PointF(5.0f, 5.0f),
                                      gfx::PointF(10.0f, 10.0f),
                                      gfx::PointF(15.0f, 10.0f))));
  const auto kPage1Matcher =
      Pair(1, ElementsAre(ElementsAre(gfx::PointF(5.0f, 5.0f),
                                      gfx::PointF(10.0f, 10.0f),
                                      gfx::PointF(15.0f, 10.0f))));
  EXPECT_THAT(StrokeInputPositions(),
              ElementsAre(kPage0Matcher, kPage1Matcher));
  EXPECT_THAT(VisibleStrokeInputPositions(),
              ElementsAre(kPage0Matcher, kPage1Matcher));

  PerformUndo();
  EXPECT_THAT(StrokeInputPositions(),
              ElementsAre(kPage0Matcher, kPage1Matcher));
  EXPECT_THAT(VisibleStrokeInputPositions(), ElementsAre(kPage0Matcher));

  PerformUndo();
  EXPECT_THAT(StrokeInputPositions(),
              ElementsAre(kPage0Matcher, kPage1Matcher));
  EXPECT_TRUE(VisibleStrokeInputPositions().empty());

  PerformRedo();
  EXPECT_THAT(StrokeInputPositions(),
              ElementsAre(kPage0Matcher, kPage1Matcher));
  EXPECT_THAT(VisibleStrokeInputPositions(), ElementsAre(kPage0Matcher));

  PerformRedo();
  EXPECT_THAT(StrokeInputPositions(),
              ElementsAre(kPage0Matcher, kPage1Matcher));
  EXPECT_THAT(VisibleStrokeInputPositions(),
              ElementsAre(kPage0Matcher, kPage1Matcher));
}

TEST_P(PdfInkModuleUndoRedoTest, UndoRedoEraseLoadedV2Shapes) {
  constexpr int kPageIndex = 0;
  constexpr InkModeledShapeId kShapeId0(0);
  constexpr InkModeledShapeId kShapeId1(1);

  const auto ink_points = base::ToVector(
      kMousePoints,
      [](const gfx::PointF& point) { return InkPointFromGfxPoint(point); });
  std::optional<ink::Mesh> mesh0 =
      CreateInkMeshFromPolylineForTesting(ink_points);
  ASSERT_TRUE(mesh0.has_value());
  auto shape0 =
      ink::PartitionedMesh::FromMeshes(base::span_from_ref(mesh0.value()));
  ASSERT_TRUE(shape0.ok());

  constexpr ink::Point kCornerPoints[] = {
      {49, 59},
      {48, 59},
      {48, 58},
  };
  std::optional<ink::Mesh> mesh1 =
      CreateInkMeshFromPolylineForTesting(kCornerPoints);
  ASSERT_TRUE(mesh1.has_value());
  auto shape1 =
      ink::PartitionedMesh::FromMeshes(base::span_from_ref(mesh1.value()));
  ASSERT_TRUE(shape1.ok());

  EXPECT_CALL(client(), LoadV2InkPathsFromPdf())
      .WillOnce(Return(PdfInkModuleClient::DocumentV2InkPathShapesMap{
          {kPageIndex, PdfInkModuleClient::PageV2InkPathShapesMap{
                           {kShapeId0, *shape0},
                           {kShapeId1, *shape1},
                       }}}));
  ExpectNoStrokeAdded();
  ExpectNoUpdateStrokeActive();
  EXPECT_CALL(client(), UpdateShapeActive(_, _, _)).Times(0);

  InitializeSimpleSinglePageBasicLayout();
  EnableDrawAnnotationMode();
  EXPECT_TRUE(updated_ink_thumbnail_page_indices().empty());
  EXPECT_TRUE(updated_pdf_thumbnail_page_indices().empty());

  EXPECT_CALL(client(), RequestThumbnail)
      .WillRepeatedly([&](int page_index, SendThumbnailCallback callback) {
        std::move(callback).Run(
            Thumbnail(gfx::SizeF(50, 25), /*device_pixel_ratio=*/1));
      });

  // Stroke with the eraser tool in the corner opposite from `kCornerPoints`,
  // which does nothing.
  SelectEraserTool();
  ApplyStrokeWithMouseAtPoints(
      gfx::PointF(), base::span_from_ref(gfx::PointF()), gfx::PointF());
  VerifyAndClearExpectations();
  EXPECT_TRUE(updated_pdf_thumbnail_page_indices().empty());

  // Stroke twice where `shape0` is, and that should deactivate only that shape
  // and only once.
  ExpectNoStrokeAdded();
  ExpectNoUpdateStrokeActive();
  EXPECT_CALL(client(),
              UpdateShapeActive(kPageIndex, kShapeId0, /*active=*/false));
  EXPECT_CALL(client(), UpdateShapeActive(_, kShapeId1, _)).Times(0);
  ApplyStrokeWithMouseAtPoints(
      kMouseDownPoint, base::span_from_ref(kMouseMovePoint), kMouseUpPoint);
  ApplyStrokeWithMouseAtPoints(
      kMouseDownPoint, base::span_from_ref(kMouseMovePoint), kMouseUpPoint);
  VerifyAndClearExpectations();
  EXPECT_THAT(updated_pdf_thumbnail_page_indices(), ElementsAre(0));

  // Undo should reactivate `shape0`.
  ExpectNoStrokeAdded();
  ExpectNoUpdateStrokeActive();
  EXPECT_CALL(client(),
              UpdateShapeActive(kPageIndex, kShapeId0, /*active=*/true));
  EXPECT_CALL(client(), UpdateShapeActive(_, kShapeId1, _)).Times(0);
  PerformUndo();
  VerifyAndClearExpectations();
  EXPECT_THAT(updated_pdf_thumbnail_page_indices(), ElementsAre(0, 0));

  // Redo should deactivate `shape0`.
  ExpectNoStrokeAdded();
  ExpectNoUpdateStrokeActive();
  EXPECT_CALL(client(),
              UpdateShapeActive(kPageIndex, kShapeId0, /*active=*/false));
  EXPECT_CALL(client(), UpdateShapeActive(_, kShapeId1, _)).Times(0);
  PerformRedo();
  EXPECT_TRUE(updated_ink_thumbnail_page_indices().empty());
  EXPECT_THAT(updated_pdf_thumbnail_page_indices(), ElementsAre(0, 0, 0));
}

// Regression test for crbug.com/378724153.
TEST_P(PdfInkModuleUndoRedoTest, StrokeStrokeUndoStroke) {
  InitializeSimpleSinglePageBasicLayout();

  // Draw stroke 1.
  RunStrokeCheckTest(/*annotation_mode_enabled=*/true);

  // Draw stroke 2.
  constexpr gfx::PointF kMouseDownPoint2 = gfx::PointF(11.0f, 15.0f);
  constexpr gfx::PointF kMouseMovePoint2 = gfx::PointF(21.0f, 25.0f);
  constexpr gfx::PointF kMouseUpPoint2 = gfx::PointF(31.0f, 17.0f);
  ApplyStrokeWithMouseAtPoints(
      kMouseDownPoint2, base::span_from_ref(kMouseMovePoint2), kMouseUpPoint2);

  // Strokes 1 and 2 should be visible.
  const auto kInitialStrokeMatchers = {
      ElementsAre(kMouseDownPoint, kMouseMovePoint, kMouseUpPoint),
      ElementsAre(kMouseDownPoint2, kMouseMovePoint2, kMouseUpPoint2)};
  const auto kInitialStrokeMatchersSpan = base::span(kInitialStrokeMatchers);
  EXPECT_THAT(
      StrokeInputPositions(),
      ElementsAre(Pair(0, ElementsAreArray(kInitialStrokeMatchersSpan))));
  EXPECT_THAT(
      VisibleStrokeInputPositions(),
      ElementsAre(Pair(0, ElementsAreArray(kInitialStrokeMatchersSpan))));

  // Undo makes 1 stroke visible.
  PerformUndo();
  EXPECT_THAT(
      StrokeInputPositions(),
      ElementsAre(Pair(0, ElementsAreArray(kInitialStrokeMatchersSpan))));
  EXPECT_THAT(VisibleStrokeInputPositions(),
              ElementsAre(Pair(
                  0, ElementsAreArray(kInitialStrokeMatchersSpan.first(1u)))));

  // Stroke IDs are 0-indexed, so stroke 2 has a stroke ID of 1.
  EXPECT_CALL(client(), DiscardStroke(/*page_index=*/0, InkStrokeId(1)));

  // Draw stroke 3. Stroke 2 was undone and should be discarded.
  constexpr gfx::PointF kMouseDownPoint3 = gfx::PointF(12.0f, 15.0f);
  constexpr gfx::PointF kMouseMovePoint3 = gfx::PointF(22.0f, 25.0f);
  constexpr gfx::PointF kMouseUpPoint3 = gfx::PointF(32.0f, 17.0f);
  ApplyStrokeWithMouseAtPoints(
      kMouseDownPoint3, base::span_from_ref(kMouseMovePoint3), kMouseUpPoint3);

  // Strokes 1 and 3 should be visible.
  const auto kNextStrokeMatchers = {
      ElementsAre(kMouseDownPoint, kMouseMovePoint, kMouseUpPoint),
      ElementsAre(kMouseDownPoint3, kMouseMovePoint3, kMouseUpPoint3)};
  const auto kNextStrokeMatchersSpan = base::span(kNextStrokeMatchers);
  EXPECT_THAT(StrokeInputPositions(),
              ElementsAre(Pair(0, ElementsAreArray(kNextStrokeMatchersSpan))));
  EXPECT_THAT(VisibleStrokeInputPositions(),
              ElementsAre(Pair(0, ElementsAreArray(kNextStrokeMatchersSpan))));
}

using PdfInkModuleGetVisibleStrokesTest = PdfInkModuleStrokeTest;

TEST_P(PdfInkModuleGetVisibleStrokesTest, NoPageStrokes) {
  EXPECT_TRUE(CollectVisibleStrokes().empty());
}

TEST_P(PdfInkModuleGetVisibleStrokesTest, MultiplePageStrokes) {
  EnableDrawAnnotationMode();
  InitializeVerticalTwoPageLayout();

  // Multiple strokes on one page, plus a stroke on another page.
  ApplyStrokeWithMouseAtPoints(
      kTwoPageVerticalLayoutPoint2InsidePage0,
      base::span_from_ref(kTwoPageVerticalLayoutPoint3InsidePage0),
      kTwoPageVerticalLayoutPoint3InsidePage0);
  ApplyStrokeWithMouseAtPoints(
      kTwoPageVerticalLayoutPoint1InsidePage0,
      base::span_from_ref(kTwoPageVerticalLayoutPoint4InsidePage0),
      kTwoPageVerticalLayoutPoint4InsidePage0);
  ApplyStrokeWithMouseAtPoints(
      kTwoPageVerticalLayoutPoint2InsidePage1,
      base::span_from_ref(kTwoPageVerticalLayoutPoint3InsidePage1),
      kTwoPageVerticalLayoutPoint3InsidePage1);

  std::optional<ink::StrokeInputBatch> expected_page0_horz_line_input_batch =
      CreateInkInputBatch(kTwoPageVerticalLayoutHorzLinePage0Inputs);
  ASSERT_TRUE(expected_page0_horz_line_input_batch.has_value());
  std::optional<ink::StrokeInputBatch> expected_page0_vert_line_input_batch =
      CreateInkInputBatch(kTwoPageVerticalLayoutVertLinePage0Inputs);
  ASSERT_TRUE(expected_page0_vert_line_input_batch.has_value());
  std::optional<ink::StrokeInputBatch> expected_page1_horz_line_input_batch =
      CreateInkInputBatch(kTwoPageVerticalLayoutHorzLinePage1Inputs);
  ASSERT_TRUE(expected_page1_horz_line_input_batch.has_value());

  const PdfInkBrush* brush = ink_module().GetPdfInkBrushForTesting();
  ASSERT_TRUE(brush);

  EXPECT_THAT(
      CollectVisibleStrokes(),
      ElementsAre(
          Pair(0, Pointwise(InkStrokeEq(brush->ink_brush()),
                            {expected_page0_horz_line_input_batch.value(),
                             expected_page0_vert_line_input_batch.value()})),
          Pair(1, Pointwise(InkStrokeEq(brush->ink_brush()),
                            {expected_page1_horz_line_input_batch.value()}))));
}

class PdfInkModuleMetricsTest : public PdfInkModuleMetricsTestBase,
                                public PdfInkModuleUndoRedoTest {
 protected:
  static constexpr char kPenColorMetric[] = "PDF.Ink2StrokePenColor";
  static constexpr char kPenSizeMetric[] = "PDF.Ink2StrokePenSize";
};

TEST_P(PdfInkModuleMetricsTest, StrokeUndoRedoDoesNotAffectMetrics) {
  InitializeSimpleSinglePageBasicLayout();

  // Draw a pen stroke.
  RunStrokeCheckTest(/*annotation_mode_enabled=*/true);

  histograms().ExpectUniqueSample(kTypeMetric, StrokeMetricBrushType::kPen, 1);
  histograms().ExpectUniqueSample(kInputDeviceMetric,
                                  StrokeMetricInputDeviceType::kMouse, 1);
  histograms().ExpectUniqueSample(kPenSizeMetric,
                                  StrokeMetricBrushSize::kMedium, 1);
  histograms().ExpectUniqueSample(kPenColorMetric, StrokeMetricPenColor::kBlack,
                                  1);

  // Undo and redo.
  PerformUndo();
  PerformRedo();

  // The metrics should stay the same.
  histograms().ExpectUniqueSample(kTypeMetric, StrokeMetricBrushType::kPen, 1);
  histograms().ExpectUniqueSample(kInputDeviceMetric,
                                  StrokeMetricInputDeviceType::kMouse, 1);
  histograms().ExpectUniqueSample(kPenSizeMetric,
                                  StrokeMetricBrushSize::kMedium, 1);
  histograms().ExpectUniqueSample(kPenColorMetric, StrokeMetricPenColor::kBlack,
                                  1);
}

TEST_P(PdfInkModuleMetricsTest, StrokeBrushColorPen) {
  InitializeSimpleSinglePageBasicLayout();

  RunStrokeCheckTest(/*annotation_mode_enabled=*/false);

  histograms().ExpectTotalCount(kPenColorMetric, 0);

  // Draw a stroke with the default black color.
  RunStrokeCheckTest(/*annotation_mode_enabled=*/true);

  histograms().ExpectUniqueSample(kPenColorMetric, StrokeMetricPenColor::kBlack,
                                  1);

  // Draw a stroke with "Red 1" color.
  TestAnnotationBrushMessageParams params = kRedBrushParams;
  SelectBrushTool(PdfInkBrush::Type::kPen, params);
  ApplyStrokeWithMouseAtMouseDownPoint();

  histograms().ExpectBucketCount(kPenColorMetric, StrokeMetricPenColor::kRed1,
                                 1);
  histograms().ExpectTotalCount(kPenColorMetric, 2);

  // Draw a stroke with "Tan 3" color.
  params.color = SkColorSetRGB(0x88, 0x59, 0x45);
  SelectBrushTool(PdfInkBrush::Type::kPen, params);
  ApplyStrokeWithMouseAtMouseDownPoint();

  histograms().ExpectBucketCount(kPenColorMetric, StrokeMetricPenColor::kTan3,
                                 1);
  histograms().ExpectTotalCount(kPenColorMetric, 3);
  histograms().ExpectTotalCount(kHighlighterColorMetric, 0);
}

TEST_P(PdfInkModuleMetricsTest, StrokeBrushColorHighlighter) {
  EnableDrawAnnotationMode();
  InitializeSimpleSinglePageBasicLayout();

  // Draw a stroke with "Light Red" color.
  TestAnnotationBrushMessageParams params = kRedBrushParams;
  SelectBrushTool(PdfInkBrush::Type::kHighlighter, params);
  ApplyStrokeWithMouseAtMouseDownPoint();

  histograms().ExpectBucketCount(kHighlighterColorMetric,
                                 StrokeMetricHighlighterColor::kLightRed, 1);
  histograms().ExpectTotalCount(kHighlighterColorMetric, 1);

  // Draw a stroke with "Orange" color.
  params.color = SkColorSetRGB(0xFF, 0x63, 0x0C);
  SelectBrushTool(PdfInkBrush::Type::kHighlighter, params);
  ApplyStrokeWithMouseAtMouseDownPoint();

  histograms().ExpectBucketCount(kHighlighterColorMetric,
                                 StrokeMetricHighlighterColor::kOrange, 1);
  histograms().ExpectTotalCount(kHighlighterColorMetric, 2);
  histograms().ExpectTotalCount(kPenColorMetric, 0);
}

TEST_P(PdfInkModuleMetricsTest, StrokeBrushSizePen) {
  InitializeSimpleSinglePageBasicLayout();

  // Draw a stroke.
  RunStrokeCheckTest(/*annotation_mode_enabled=*/true);

  histograms().ExpectUniqueSample(kPenSizeMetric,
                                  StrokeMetricBrushSize::kMedium, 1);

  TestAnnotationBrushMessageParams params = {SkColorSetRGB(0xF2, 0x8B, 0x82),
                                             /*size=*/1.0};
  SelectBrushTool(PdfInkBrush::Type::kPen, params);
  ApplyStrokeWithMouseAtMouseDownPoint();

  histograms().ExpectBucketCount(kPenSizeMetric,
                                 StrokeMetricBrushSize::kExtraThin, 1);
  histograms().ExpectTotalCount(kPenSizeMetric, 2);

  params.size = 8.0f;
  SelectBrushTool(PdfInkBrush::Type::kPen, params);
  ApplyStrokeWithMouseAtMouseDownPoint();

  histograms().ExpectBucketCount(kPenSizeMetric,
                                 StrokeMetricBrushSize::kExtraThick, 1);
  histograms().ExpectTotalCount(kPenSizeMetric, 3);
  histograms().ExpectTotalCount(kHighlighterSizeMetric, 0);
}

TEST_P(PdfInkModuleMetricsTest, StrokeBrushSizeHighlighter) {
  EnableDrawAnnotationMode();
  InitializeSimpleSinglePageBasicLayout();

  // Draw a stroke with medium size.
  TestAnnotationBrushMessageParams params = {SkColorSetRGB(0xF2, 0x8B, 0x82),
                                             /*size=*/8.0};
  SelectBrushTool(PdfInkBrush::Type::kHighlighter, params);
  ApplyStrokeWithMouseAtMouseDownPoint();

  histograms().ExpectUniqueSample(kHighlighterSizeMetric,
                                  StrokeMetricBrushSize::kMedium, 1);

  // Draw a stroke with extra thin size.
  params.size = 4.0f;
  SelectBrushTool(PdfInkBrush::Type::kHighlighter, params);
  ApplyStrokeWithMouseAtMouseDownPoint();

  histograms().ExpectBucketCount(kHighlighterSizeMetric,
                                 StrokeMetricBrushSize::kExtraThin, 1);
  histograms().ExpectTotalCount(kHighlighterSizeMetric, 2);

  // Draw a stroke with extra thick size.
  params.size = 16.0f;
  SelectBrushTool(PdfInkBrush::Type::kHighlighter, params);
  ApplyStrokeWithMouseAtMouseDownPoint();

  histograms().ExpectBucketCount(kHighlighterSizeMetric,
                                 StrokeMetricBrushSize::kExtraThick, 1);
  histograms().ExpectTotalCount(kPenSizeMetric, 0);
  histograms().ExpectTotalCount(kHighlighterSizeMetric, 3);
}

TEST_P(PdfInkModuleMetricsTest, StrokeBrushType) {
  InitializeSimpleSinglePageBasicLayout();

  RunStrokeCheckTest(/*annotation_mode_enabled=*/false);

  histograms().ExpectTotalCount(kTypeMetric, 0);

  // Draw a pen stroke.
  RunStrokeCheckTest(/*annotation_mode_enabled=*/true);

  histograms().ExpectUniqueSample(kTypeMetric, StrokeMetricBrushType::kPen, 1);

  // Draw a highlighter stroke.
  TestAnnotationBrushMessageParams params = kRedBrushParams;
  SelectBrushTool(PdfInkBrush::Type::kHighlighter, params);
  ApplyStrokeWithMouseAtMouseDownPoint();

  histograms().ExpectBucketCount(kTypeMetric,
                                 StrokeMetricBrushType::kHighlighter, 1);
  histograms().ExpectTotalCount(kTypeMetric, 2);

  // Draw an eraser stroke.
  SelectEraserTool();
  ApplyStrokeWithMouseAtMouseDownPoint();

  histograms().ExpectBucketCount(kTypeMetric, StrokeMetricBrushType::kEraser,
                                 1);
  histograms().ExpectTotalCount(kTypeMetric, 3);

  // Draw an eraser stroke at a different point that does not erase any other
  // strokes. The metric should stay the same.
  ApplyStrokeWithMouseAtPoints(
      kMouseUpPoint, base::span_from_ref(kMouseUpPoint), kMouseUpPoint);

  histograms().ExpectBucketCount(kTypeMetric, StrokeMetricBrushType::kEraser,
                                 1);
  histograms().ExpectTotalCount(kTypeMetric, 3);

  // Draw another pen stroke.
  params.size = 3.0f;
  SelectBrushTool(PdfInkBrush::Type::kPen, params);
  ApplyStrokeWithMouseAtMouseDownPoint();

  histograms().ExpectBucketCount(kTypeMetric, StrokeMetricBrushType::kPen, 2);
  histograms().ExpectTotalCount(kTypeMetric, 4);
}

TEST_P(PdfInkModuleMetricsTest, StrokeInputDeviceMouse) {
  InitializeSimpleSinglePageBasicLayout();

  RunStrokeCheckTest(/*annotation_mode_enabled=*/false);

  histograms().ExpectTotalCount(kInputDeviceMetric, 0);

  // Draw a stroke with a mouse.
  RunStrokeCheckTest(/*annotation_mode_enabled=*/true);

  histograms().ExpectUniqueSample(kInputDeviceMetric,
                                  StrokeMetricInputDeviceType::kMouse, 1);

  // Draw an eraser stroke with a mouse that erases the first stroke.
  SelectEraserTool();
  ApplyStrokeWithMouseAtMouseDownPoint();

  histograms().ExpectUniqueSample(kInputDeviceMetric,
                                  StrokeMetricInputDeviceType::kMouse, 2);

  // Draw another eraser stroke with a mouse that erases nothing.
  ApplyStrokeWithMouseAtMouseDownPoint();

  histograms().ExpectUniqueSample(kInputDeviceMetric,
                                  StrokeMetricInputDeviceType::kMouse, 2);
}

TEST_P(PdfInkModuleMetricsTest, StrokeInputDeviceTouch) {
  InitializeSimpleSinglePageBasicLayout();

  RunStrokeTouchCheckTest(/*annotation_mode_enabled=*/false);

  histograms().ExpectTotalCount(kInputDeviceMetric, 0);

  // Draw a stroke with touch.
  RunStrokeTouchCheckTest(/*annotation_mode_enabled=*/true);

  histograms().ExpectUniqueSample(kInputDeviceMetric,
                                  StrokeMetricInputDeviceType::kTouch, 1);

  // Draw an eraser stroke with touch that erases the first stroke.
  SelectEraserTool();
  const std::vector<base::span<const gfx::PointF>> move_point{
      base::span_from_ref(kMouseDownPoint),
  };
  ApplyStrokeWithTouchAtPoints(base::span_from_ref(kMouseDownPoint), move_point,
                               base::span_from_ref(kMouseDownPoint));

  histograms().ExpectUniqueSample(kInputDeviceMetric,
                                  StrokeMetricInputDeviceType::kTouch, 2);

  // Draw another eraser stroke with touch that erases nothing.
  ApplyStrokeWithTouchAtPoints(base::span_from_ref(kMouseDownPoint), move_point,
                               base::span_from_ref(kMouseDownPoint));

  histograms().ExpectUniqueSample(kInputDeviceMetric,
                                  StrokeMetricInputDeviceType::kTouch, 2);
}

TEST_P(PdfInkModuleMetricsTest, StrokeInputDevicePen) {
  InitializeSimpleSinglePageBasicLayout();

  RunStrokePenCheckTest(/*annotation_mode_enabled=*/false);

  histograms().ExpectTotalCount(kInputDeviceMetric, 0);

  // Draw a stroke with a pen.
  RunStrokePenCheckTest(/*annotation_mode_enabled=*/true);

  histograms().ExpectUniqueSample(kInputDeviceMetric,
                                  StrokeMetricInputDeviceType::kPen, 1);

  // Draw an eraser stroke with a pen that erases the first stroke.
  SelectEraserTool();
  const std::vector<base::span<const gfx::PointF>> move_point{
      base::span_from_ref(kMouseDownPoint),
  };
  ApplyStrokeWithPenAtPoints(base::span_from_ref(kMouseDownPoint), move_point,
                             base::span_from_ref(kMouseDownPoint));

  histograms().ExpectUniqueSample(kInputDeviceMetric,
                                  StrokeMetricInputDeviceType::kPen, 2);

  // Draw another eraser stroke with a pen that erases nothing.
  ApplyStrokeWithPenAtPoints(base::span_from_ref(kMouseDownPoint), move_point,
                             base::span_from_ref(kMouseDownPoint));

  histograms().ExpectUniqueSample(kInputDeviceMetric,
                                  StrokeMetricInputDeviceType::kPen, 2);
}

class PdfInkModuleTextHighlightTest : public PdfInkModuleUndoRedoTest {
 public:
  static constexpr TestAnnotationBrushMessageParams kOrangeBrushParams{
      SkColorSetRGB(0xFF, 0x63, 0x0C),
      /*size=*/6.0};
  static constexpr gfx::Rect kHorizontalSelection{10, 15, 30, 10};
  static constexpr gfx::Rect kVerticalSelection{10, 15, 6, 10};
  static constexpr gfx::PointF kStartPointInsidePage0{10.0, 10.0};
  static constexpr gfx::PointF kEndPointInsidePage0{15.0, 10.0};
  static constexpr SkColor kOrangeColor = SkColorSetRGB(0xFF, 0x63, 0x0C);

 protected:
  // Helper method for running a simple text highlighting test using text
  // selected by mouse with a single selection rect on page zero.
  // `selection_rect` is in screen coordinates, so it is easier to see the
  // relationship between `selection_rect` and `expected_inputs`.
  void RunSingleSelectionWithMouseTest(
      const gfx::Rect& selection_rect,
      base::span<const PdfInkInputData> expected_inputs,
      float expected_size) {
    SetUpSingleSelectionTest(selection_rect);

    // Apply a text highlight stroke at the given points.
    ApplyStrokeWithMouseAtPoints(kStartPointInsidePage0, {kEndPointInsidePage0},
                                 kEndPointInsidePage0);

    VerifySingleSelectionTest(expected_inputs, expected_size);
  }

  // Sets the selection rects that will be given by the client.
  // `selection_map` uses screen coordinates.
  void SetSelectionRectMap(
      const std::map<int, std::vector<gfx::Rect>>& selection_map) {
    // Do screen to PDF coordinate conversion. Set GetCanonicalToPdfTransform()
    // to return an identity transform to simplify testing. Now a screen to
    // canonical conversion is sufficient.
    ON_CALL(client(), GetCanonicalToPdfTransform(_))
        .WillByDefault(Return(gfx::Transform()));

    PdfInkModuleClient::SelectionRectMap pdf_selection_map;
    for (const auto& [page_index, selection_rects] : selection_map) {
      const gfx::Transform transform = GetEventToCanonicalTransform(
          client().GetOrientation(), client().GetPageContentsRect(page_index),
          client().GetZoom());
      std::vector<PdfRect>& pdf_rects = pdf_selection_map[page_index];
      pdf_rects.reserve(selection_rects.size());
      for (const gfx::Rect& selection_rect : selection_rects) {
        gfx::RectF mapped_rect = transform.MapRect(gfx::RectF(selection_rect));
        pdf_rects.push_back(PdfRect(mapped_rect));
      }
    }

    ON_CALL(client(), GetSelectionRectMap())
        .WillByDefault(Return(pdf_selection_map));
  }

  // Wrapper for SetSelectionRectMap() that puts all the rects on page 0.
  // `selection_rects` uses screen coordinates.
  void SetSelectionRectsOnFirstPage(
      base::span<const gfx::Rect> selection_rects) {
    // Call `SetSelectionRectMap({})` if there are no selections at all.
    CHECK(!selection_rects.empty());

    SetSelectionRectMap({{0, base::ToVector(selection_rects)}});
  }

  // Sets `points` as selectable text areas. Any points not included will be
  // considered non-selectable.
  void SetTextAreaPoints(base::span<const gfx::PointF> points) {
    EXPECT_CALL(client(), IsSelectableTextOrLinkArea(_))
        .WillRepeatedly(Return(false));
    for (const auto& point : points) {
      EXPECT_CALL(client(), IsSelectableTextOrLinkArea(point))
          .WillRepeatedly(Return(true));
    }
  }

  // Sets up single selection test expectations before text selection strokes
  // have been applied.
  void SetUpSingleSelectionTest(const gfx::Rect& selection_rect) {
    EnableDrawAnnotationMode();
    InitializeSimpleSinglePageBasicLayout();

    SelectBrushTool(PdfInkBrush::Type::kHighlighter, kOrangeBrushParams);

    SetSelectionRectsOnFirstPage(base::span_from_ref(selection_rect));
    SetTextAreaPoints(base::span_from_ref(kStartPointInsidePage0));

    EXPECT_CALL(client(), OnTextOrLinkAreaClick(kStartPointInsidePage0,
                                                /*click_count=*/1));
    EXPECT_CALL(client(), ExtendSelectionByPoint(kEndPointInsidePage0));
  }

  // Verifies single selection test results after applying text selection
  // strokes.
  void VerifySingleSelectionTest(
      base::span<const PdfInkInputData> expected_inputs,
      float expected_size) {
    ExpectStrokeCounts(/*started=*/1, /*modified_finished=*/1,
                       /*unmodified_finished=*/0);
    EXPECT_THAT(updated_ink_thumbnail_page_indices(), ElementsAre(0));

    std::optional<ink::StrokeInputBatch> expected_batch =
        CreateInkInputBatch(expected_inputs);
    ASSERT_TRUE(expected_batch.has_value());

    const PdfInkBrush expected_brush(PdfInkBrush::Type::kHighlighter,
                                     kOrangeColor, expected_size);
    EXPECT_THAT(
        CollectVisibleStrokes(),
        ElementsAre(Pair(0, Pointwise(InkStrokeEq(expected_brush.ink_brush()),
                                      {expected_batch.value()}))));

    // The current brush should remain a highlighter.
    const PdfInkBrush* brush = ink_module().GetPdfInkBrushForTesting();
    ASSERT_TRUE(brush);
    const ink::Brush& ink_brush = brush->ink_brush();

    EXPECT_EQ(kOrangeColor, GetSkColorFromInkBrush(ink_brush));
    EXPECT_EQ(6.0f, ink_brush.GetSize());
    EXPECT_EQ(0.4f, GetOpacityMultiplierFromBrush(ink_brush));
  }

  void ClickTextAtPoint(const gfx::PointF& point, int click_count) {
    blink::WebMouseEvent mouse_down_event =
        MouseEventBuilder()
            .CreateLeftClickAtPosition(point)
            .SetClickCount(click_count)
            .Build();
    EXPECT_TRUE(ink_module().HandleInputEvent(mouse_down_event));

    // Do not create a mouse move event, as it would reset the click count.

    blink::WebMouseEvent mouse_up_event =
        MouseEventBuilder()
            .CreateLeftMouseUpAtPosition(point)
            .SetClickCount(click_count)
            .Build();
    EXPECT_TRUE(ink_module().HandleInputEvent(mouse_up_event));
  }
};

TEST_P(PdfInkModuleTextHighlightTest, PenDoesNotSelectText) {
  EnableDrawAnnotationMode();
  InitializeSimpleSinglePageBasicLayout();

  // Select the pen tool with a "Light Red" color.
  SelectBrushTool(PdfInkBrush::Type::kPen, kRedBrushParams);

  EXPECT_CALL(client(), GetSelectionRectMap()).Times(0);
  SetTextAreaPoints(base::span_from_ref(kStartPointInsidePage0));

  EXPECT_CALL(client(), OnTextOrLinkAreaClick(_, _)).Times(0);
  EXPECT_CALL(client(), ExtendSelectionByPoint(_)).Times(0);

  // Apply a pen stroke at the given points.
  ApplyStrokeWithMouseAtPoints(kStartPointInsidePage0, {kEndPointInsidePage0},
                               kEndPointInsidePage0);

  ExpectStrokeCounts(/*started=*/1, /*modified_finished=*/1,
                     /*unmodified_finished=*/0);
  EXPECT_THAT(updated_ink_thumbnail_page_indices(), ElementsAre(0));

  // The stroke inputs should match exactly.
  std::optional<ink::StrokeInputBatch> expected_batch =
      CreateInkInputBatch({PdfInkInputData(kStartPointInsidePage0),
                           PdfInkInputData(kEndPointInsidePage0)});
  ASSERT_TRUE(expected_batch.has_value());

  // The stroke should be a pen stroke.
  const PdfInkBrush* brush = ink_module().GetPdfInkBrushForTesting();
  ASSERT_TRUE(brush);

  EXPECT_THAT(CollectVisibleStrokes(),
              ElementsAre(Pair(0, Pointwise(InkStrokeEq(brush->ink_brush()),
                                            {expected_batch.value()}))));
}

TEST_P(PdfInkModuleTextHighlightTest, SingleHorizontalSelection) {
  RunSingleSelectionWithMouseTest(
      /*selection_rect=*/kHorizontalSelection,
      /*expected_inputs=*/
      {PdfInkInputData(gfx::PointF(15.0, 20.0)),
       PdfInkInputData(gfx::PointF(35.0, 20.0))},
      /*expected_size=*/10.0);
}

TEST_P(PdfInkModuleTextHighlightTest, SingleVerticalSelection) {
  RunSingleSelectionWithMouseTest(
      /*selection_rect=*/kVerticalSelection,
      /*expected_inputs=*/
      {PdfInkInputData(gfx::PointF(13.0, 18.0)),
       PdfInkInputData(gfx::PointF(13.0, 22.0))},
      /*expected_size=*/6.0);
}

TEST_P(PdfInkModuleTextHighlightTest, SingleSquareSelection) {
  RunSingleSelectionWithMouseTest(
      /*selection_rect=*/gfx::Rect(10, 15, 12, 12),
      /*expected_inputs=*/
      {PdfInkInputData(gfx::PointF(16.0, 21.0))},
      /*expected_size=*/12.0);
}

TEST_P(PdfInkModuleTextHighlightTest,
       SingleHorizontalSelectionRotatedClockwise90) {
  client().set_orientation(PageOrientation::kClockwise90);
  RunSingleSelectionWithMouseTest(
      /*selection_rect=*/kHorizontalSelection,
      /*expected_inputs=*/
      {PdfInkInputData(gfx::PointF(20.0, 14.0)),
       PdfInkInputData(gfx::PointF(20.0, 34.0))},
      /*expected_size=*/10.0);
}

TEST_P(PdfInkModuleTextHighlightTest,
       SingleVerticalSelectionRotatedClockwise90) {
  client().set_orientation(PageOrientation::kClockwise90);
  RunSingleSelectionWithMouseTest(
      /*selection_rect=*/kVerticalSelection,
      /*expected_inputs=*/
      {PdfInkInputData(gfx::PointF(18.0, 36.0)),
       PdfInkInputData(gfx::PointF(22.0, 36.0))},
      /*expected_size=*/6.0);
}

TEST_P(PdfInkModuleTextHighlightTest,
       SingleHorizontalSelectionRotatedClockwise180) {
  client().set_orientation(PageOrientation::kClockwise180);
  RunSingleSelectionWithMouseTest(
      /*selection_rect=*/kHorizontalSelection,
      /*expected_inputs=*/
      {PdfInkInputData(gfx::PointF(14.0, 39.0)),
       PdfInkInputData(gfx::PointF(34.0, 39.0))},
      /*expected_size=*/10.0);
}

TEST_P(PdfInkModuleTextHighlightTest,
       SingleVerticalSelectionRotatedClockwise180) {
  client().set_orientation(PageOrientation::kClockwise180);
  RunSingleSelectionWithMouseTest(
      /*selection_rect=*/kVerticalSelection,
      /*expected_inputs=*/
      {PdfInkInputData(gfx::PointF(36.0, 37.0)),
       PdfInkInputData(gfx::PointF(36.0, 41.0))},
      /*expected_size=*/6.0);
}

TEST_P(PdfInkModuleTextHighlightTest,
       SingleHorizontalSelectionRotatedClockwise270) {
  client().set_orientation(PageOrientation::kClockwise270);
  RunSingleSelectionWithMouseTest(
      /*selection_rect=*/kHorizontalSelection,
      /*expected_inputs=*/
      {PdfInkInputData(gfx::PointF(39.0, 15.0)),
       PdfInkInputData(gfx::PointF(39.0, 35.0))},
      /*expected_size=*/10.0);
}

TEST_P(PdfInkModuleTextHighlightTest,
       SingleVerticalSelectionRotatedClockwise270) {
  client().set_orientation(PageOrientation::kClockwise270);
  RunSingleSelectionWithMouseTest(
      /*selection_rect=*/kVerticalSelection,
      /*expected_inputs=*/
      {PdfInkInputData(gfx::PointF(37.0, 13.0)),
       PdfInkInputData(gfx::PointF(41.0, 13.0))},
      /*expected_size=*/6.0);
}

TEST_P(PdfInkModuleTextHighlightTest, SingleSelectionZoomedIn) {
  EnableDrawAnnotationMode();
  InitializeSimpleSinglePageBasicLayout();
  client().set_zoom(2.0f);

  RunSingleSelectionWithMouseTest(
      /*selection_rect=*/kHorizontalSelection,
      /*expected_inputs=*/
      {PdfInkInputData(gfx::PointF(7.5, 10.0)),
       PdfInkInputData(gfx::PointF(17.5, 10.0))},
      /*expected_size=*/5.0);
}

TEST_P(PdfInkModuleTextHighlightTest, SingleSelectionZoomedOut) {
  EnableDrawAnnotationMode();
  InitializeSimpleSinglePageBasicLayout();
  client().set_zoom(0.5f);

  RunSingleSelectionWithMouseTest(
      /*selection_rect=*/kHorizontalSelection,
      /*expected_inputs=*/
      {PdfInkInputData(gfx::PointF(30.0, 40.0)),
       PdfInkInputData(gfx::PointF(70.0, 40.0))},
      /*expected_size=*/20.0);
}

TEST_P(PdfInkModuleTextHighlightTest, MultipleSelection) {
  EnableDrawAnnotationMode();
  InitializeSimpleSinglePageBasicLayout();

  SelectBrushTool(PdfInkBrush::Type::kHighlighter, kOrangeBrushParams);

  SetSelectionRectsOnFirstPage(
      {kHorizontalSelection, gfx::Rect(15, 25, 10, 5)});
  constexpr gfx::PointF kEndPoint2InsidePage0{25.0, 30.0};
  SetTextAreaPoints({kStartPointInsidePage0, kEndPoint2InsidePage0});

  EXPECT_CALL(client(), OnTextOrLinkAreaClick(kStartPointInsidePage0,
                                              /*click_count=*/1));
  EXPECT_CALL(client(), ExtendSelectionByPoint(kEndPoint2InsidePage0));

  // Apply a text highlight stroke at the given points.
  ApplyStrokeWithMouseAtPoints(kStartPointInsidePage0, {kEndPoint2InsidePage0},
                               kEndPoint2InsidePage0);

  ExpectStrokeCounts(/*started=*/1, /*modified_finished=*/1,
                     /*unmodified_finished=*/0);
  EXPECT_THAT(updated_ink_thumbnail_page_indices(), ElementsAre(0));

  std::optional<ink::StrokeInputBatch> expected_selection0_batch =
      CreateInkInputBatch({PdfInkInputData(gfx::PointF(15.0, 20.0)),
                           PdfInkInputData(gfx::PointF(35.0, 20.0))});
  ASSERT_TRUE(expected_selection0_batch.has_value());
  std::optional<ink::StrokeInputBatch> expected_selection1_batch =
      CreateInkInputBatch({PdfInkInputData(gfx::PointF(17.5, 27.5)),
                           PdfInkInputData(gfx::PointF(22.5, 27.5))});
  ASSERT_TRUE(expected_selection1_batch.has_value());

  std::map<int, std::vector<raw_ref<const ink::Stroke>>> collected_strokes =
      CollectVisibleStrokes();
  ASSERT_EQ(1u, collected_strokes.size());

  std::vector<raw_ref<const ink::Stroke>>& strokes_on_page0 =
      collected_strokes[0];
  ASSERT_EQ(2u, strokes_on_page0.size());

  const PdfInkBrush expected_selection0_brush(PdfInkBrush::Type::kHighlighter,
                                              kOrangeColor, /*size=*/10.0f);

  raw_ref<const ink::Stroke> actual_selection0 = strokes_on_page0[0];
  EXPECT_THAT(actual_selection0->GetBrush(),
              ink::BrushEq(expected_selection0_brush.ink_brush()));
  EXPECT_THAT(actual_selection0->GetInputs(),
              ink::StrokeInputBatchEq(expected_selection0_batch.value()));

  const PdfInkBrush expected_selection1_brush(PdfInkBrush::Type::kHighlighter,
                                              kOrangeColor, /*size=*/5.0f);

  raw_ref<const ink::Stroke> actual_selection1 = strokes_on_page0[1];
  EXPECT_THAT(actual_selection1->GetBrush(),
              ink::BrushEq(expected_selection1_brush.ink_brush()));
  EXPECT_THAT(actual_selection1->GetInputs(),
              ink::StrokeInputBatchEq(expected_selection1_batch.value()));
}

TEST_P(PdfInkModuleTextHighlightTest, OneClickCount) {
  EnableDrawAnnotationMode();
  InitializeSimpleSinglePageBasicLayout();

  SelectBrushTool(PdfInkBrush::Type::kHighlighter, kOrangeBrushParams);

  // There will be no text selection rects.
  SetSelectionRectMap({});
  SetTextAreaPoints(base::span_from_ref(kStartPointInsidePage0));

  EXPECT_CALL(client(), OnTextOrLinkAreaClick(kStartPointInsidePage0,
                                              /*click_count=*/1));
  EXPECT_CALL(client(), ExtendSelectionByPoint(_)).Times(0);

  ClickTextAtPoint(kStartPointInsidePage0, /*click_count=*/1);

  ExpectStrokeCounts(/*started=*/1, /*modified_finished=*/0,
                     /*unmodified_finished=*/1);
  EXPECT_TRUE(updated_ink_thumbnail_page_indices().empty());

  EXPECT_TRUE(CollectVisibleStrokes().empty());
}

TEST_P(PdfInkModuleTextHighlightTest, TwoClickCount) {
  EnableDrawAnnotationMode();
  InitializeSimpleSinglePageBasicLayout();

  SelectBrushTool(PdfInkBrush::Type::kHighlighter, kOrangeBrushParams);
  SetTextAreaPoints(base::span_from_ref(kStartPointInsidePage0));

  ClickTextAtPoint(kStartPointInsidePage0, /*click_count=*/1);

  // The second text click will select the word.
  SetSelectionRectsOnFirstPage(base::span_from_ref(kHorizontalSelection));

  EXPECT_CALL(client(), OnTextOrLinkAreaClick(kStartPointInsidePage0,
                                              /*click_count=*/2));
  EXPECT_CALL(client(), ExtendSelectionByPoint(_)).Times(0);

  blink::WebMouseEvent mouse_down_event =
      MouseEventBuilder()
          .CreateLeftClickAtPosition(kStartPointInsidePage0)
          .SetClickCount(2)
          .Build();
  EXPECT_TRUE(ink_module().HandleInputEvent(mouse_down_event));

  ExpectStrokeCounts(/*started=*/2, /*modified_finished=*/1,
                     /*unmodified_finished=*/1);
  EXPECT_THAT(updated_ink_thumbnail_page_indices(), ElementsAre(0));

  std::optional<ink::StrokeInputBatch> expected_batch =
      CreateInkInputBatch({PdfInkInputData(gfx::PointF(15.0, 20.0)),
                           PdfInkInputData(gfx::PointF(35.0, 20.0))});
  ASSERT_TRUE(expected_batch.has_value());

  const PdfInkBrush expected_brush(PdfInkBrush::Type::kHighlighter,
                                   kOrangeColor,
                                   /*size=*/10.0f);
  EXPECT_THAT(
      CollectVisibleStrokes(),
      ElementsAre(Pair(0, Pointwise(InkStrokeEq(expected_brush.ink_brush()),
                                    {expected_batch.value()}))));

  // Mousemove and mouseup events will be handled but will not result in any
  // additional strokes.
  EXPECT_TRUE(ink_module().HandleInputEvent(
      CreateLeftClickWebMouseMoveEventAtPosition(kStartPointInsidePage0)));
  blink::WebMouseEvent mouse_up_event =
      CreateLeftClickWebMouseUpEventAtPosition(kStartPointInsidePage0);
  EXPECT_TRUE(ink_module().HandleInputEvent(mouse_up_event));

  ExpectStrokeCounts(/*started=*/2, /*modified_finished=*/1,
                     /*unmodified_finished=*/1);
}

TEST_P(PdfInkModuleTextHighlightTest, ThreeClickCount) {
  EnableDrawAnnotationMode();
  InitializeSimpleSinglePageBasicLayout();

  SelectBrushTool(PdfInkBrush::Type::kHighlighter, kOrangeBrushParams);
  SetTextAreaPoints(base::span_from_ref(kStartPointInsidePage0));

  ClickTextAtPoint(kStartPointInsidePage0, /*click_count=*/1);

  SetSelectionRectsOnFirstPage(base::span_from_ref(kHorizontalSelection));
  ClickTextAtPoint(kStartPointInsidePage0, /*click_count=*/2);

  // The third text click will remove the original word text highlight and
  // select the line.
  SetSelectionRectsOnFirstPage(base::span_from_ref(gfx::Rect(5, 15, 45, 12)));

  EXPECT_CALL(client(), OnTextOrLinkAreaClick(kStartPointInsidePage0,
                                              /*click_count=*/3));
  EXPECT_CALL(client(), ExtendSelectionByPoint(_)).Times(0);

  blink::WebMouseEvent mouse_down_event =
      MouseEventBuilder()
          .CreateLeftClickAtPosition(kStartPointInsidePage0)
          .SetClickCount(3)
          .Build();
  EXPECT_TRUE(ink_module().HandleInputEvent(mouse_down_event));

  // Two modified strokes: one from the double-click rect and another from the
  // triple-click rect.
  // One unmodified stroke: From the single-click.
  // Three ink thumbnail updates: one from the double-click rect, one from the
  // undo, and another from the triple-click rect.
  ExpectStrokeCounts(/*started=*/3, /*modified_finished=*/2,
                     /*unmodified_finished=*/1);
  EXPECT_THAT(updated_ink_thumbnail_page_indices(), ElementsAre(0, 0, 0));

  std::optional<ink::StrokeInputBatch> expected_batch =
      CreateInkInputBatch({PdfInkInputData(gfx::PointF(11.0, 21.0)),
                           PdfInkInputData(gfx::PointF(44.0, 21.0))});
  ASSERT_TRUE(expected_batch.has_value());

  const PdfInkBrush expected_brush(PdfInkBrush::Type::kHighlighter,
                                   kOrangeColor,
                                   /*size=*/12.0f);
  EXPECT_THAT(
      CollectVisibleStrokes(),
      ElementsAre(Pair(0, Pointwise(InkStrokeEq(expected_brush.ink_brush()),
                                    {expected_batch.value()}))));

  // Mousemove and mouseup events will be handled but will not result in any
  // additional strokes.
  EXPECT_TRUE(ink_module().HandleInputEvent(
      CreateLeftClickWebMouseMoveEventAtPosition(kStartPointInsidePage0)));
  blink::WebMouseEvent mouse_up_event =
      CreateLeftClickWebMouseUpEventAtPosition(kStartPointInsidePage0);
  EXPECT_TRUE(ink_module().HandleInputEvent(mouse_up_event));

  ExpectStrokeCounts(/*started=*/3, /*modified_finished=*/2,
                     /*unmodified_finished=*/1);
}

TEST_P(PdfInkModuleTextHighlightTest, MouseUpOnNonSelection) {
  EnableDrawAnnotationMode();
  InitializeSimpleSinglePageBasicLayout();

  SelectBrushTool(PdfInkBrush::Type::kHighlighter, kOrangeBrushParams);

  // Start in a text area.
  SetTextAreaPoints(base::span_from_ref(kStartPointInsidePage0));

  EXPECT_CALL(client(), OnTextOrLinkAreaClick(kStartPointInsidePage0,
                                              /*click_count=*/1));

  blink::WebMouseEvent mouse_down_event =
      CreateLeftClickWebMouseEventAtPosition(kStartPointInsidePage0);
  EXPECT_TRUE(ink_module().HandleInputEvent(mouse_down_event));

  VerifyAndClearExpectations();

  // Move and end in a non-text area. Make the mock selection rect smaller than
  // the distance between the mousedown and mouseup points.
  SetSelectionRectsOnFirstPage(base::span_from_ref(gfx::Rect(10, 15, 2, 10)));

  EXPECT_CALL(client(), ExtendSelectionByPoint(kEndPointInsidePage0));

  blink::WebMouseEvent mouse_move_event =
      CreateLeftClickWebMouseMoveEventAtPosition(kEndPointInsidePage0);
  EXPECT_TRUE(ink_module().HandleInputEvent(mouse_move_event));

  blink::WebMouseEvent mouse_up_event =
      CreateLeftClickWebMouseUpEventAtPosition(kEndPointInsidePage0);
  EXPECT_TRUE(ink_module().HandleInputEvent(mouse_up_event));

  ExpectStrokeCounts(/*started=*/1, /*modified_finished=*/1,
                     /*unmodified_finished=*/0);
  EXPECT_THAT(updated_ink_thumbnail_page_indices(), ElementsAre(0));

  std::optional<ink::StrokeInputBatch> expected_batch =
      CreateInkInputBatch({PdfInkInputData(gfx::PointF(11.0, 16.0)),
                           PdfInkInputData(gfx::PointF(11.0, 24.0))});
  ASSERT_TRUE(expected_batch.has_value());

  const PdfInkBrush expected_brush(PdfInkBrush::Type::kHighlighter,
                                   kOrangeColor,
                                   /*size=*/2.0f);
  EXPECT_THAT(
      CollectVisibleStrokes(),
      ElementsAre(Pair(0, Pointwise(InkStrokeEq(expected_brush.ink_brush()),
                                    {expected_batch.value()}))));
}

TEST_P(PdfInkModuleTextHighlightTest, MultiplePages) {
  EnableDrawAnnotationMode();
  InitializeVerticalTwoPageLayout();

  SelectBrushTool(PdfInkBrush::Type::kHighlighter, kOrangeBrushParams);

  SetTextAreaPoints(
      {kStartPointInsidePage0, kTwoPageVerticalLayoutPoint1InsidePage1});

  // Start on page 0.
  EXPECT_CALL(client(), OnTextOrLinkAreaClick(kStartPointInsidePage0,
                                              /*click_count=*/1));

  blink::WebMouseEvent mouse_down_event =
      CreateLeftClickWebMouseEventAtPosition(kStartPointInsidePage0);
  EXPECT_TRUE(ink_module().HandleInputEvent(mouse_down_event));

  // Move to page 1. Select rects from both pages.
  constexpr gfx::Rect kHorizontalSelectionInPage1{10, 75, 15, 14};
  SetSelectionRectMap(
      {{0, {kHorizontalSelection}}, {1, {kHorizontalSelectionInPage1}}});
  EXPECT_CALL(client(),
              PageIndexFromPoint(gfx::PointF(kHorizontalSelection.origin())))
      .WillRepeatedly(Return(0));
  EXPECT_CALL(client(), PageIndexFromPoint(
                            gfx::PointF(kHorizontalSelectionInPage1.origin())))
      .WillRepeatedly(Return(1));

  EXPECT_CALL(client(),
              ExtendSelectionByPoint(kTwoPageVerticalLayoutPoint1InsidePage1));

  blink::WebMouseEvent mouse_move_event =
      CreateLeftClickWebMouseMoveEventAtPosition(
          kTwoPageVerticalLayoutPoint1InsidePage1);
  EXPECT_TRUE(ink_module().HandleInputEvent(mouse_move_event));

  blink::WebMouseEvent mouse_up_event =
      CreateLeftClickWebMouseUpEventAtPosition(
          kTwoPageVerticalLayoutPoint1InsidePage1);
  EXPECT_TRUE(ink_module().HandleInputEvent(mouse_up_event));

  // All the selection strokes are considered one stroke.
  ExpectStrokeCounts(/*started=*/1, /*modified_finished=*/1,
                     /*unmodified_finished=*/0);
  EXPECT_THAT(updated_ink_thumbnail_page_indices(), ElementsAre(0, 1));

  std::optional<ink::StrokeInputBatch> expected_page0_batch =
      CreateInkInputBatch({PdfInkInputData(gfx::PointF(10.0, 15.0)),
                           PdfInkInputData(gfx::PointF(30.0, 15.0))});
  ASSERT_TRUE(expected_page0_batch.has_value());
  std::optional<ink::StrokeInputBatch> expected_page1_batch =
      CreateInkInputBatch({PdfInkInputData(gfx::PointF(12.0, 12.0)),
                           PdfInkInputData(gfx::PointF(13.0, 12.0))});
  ASSERT_TRUE(expected_page1_batch.has_value());

  const PdfInkBrush expected_page0_brush(PdfInkBrush::Type::kHighlighter,
                                         kOrangeColor, /*size=*/10.0f);
  const PdfInkBrush expected_page1_brush(PdfInkBrush::Type::kHighlighter,
                                         kOrangeColor, /*size=*/14.0f);
  EXPECT_THAT(
      CollectVisibleStrokes(),
      ElementsAre(
          Pair(0, Pointwise(InkStrokeEq(expected_page0_brush.ink_brush()),
                            {expected_page0_batch.value()})),
          Pair(1, Pointwise(InkStrokeEq(expected_page1_brush.ink_brush()),
                            {expected_page1_batch.value()}))));
}

TEST_P(PdfInkModuleTextHighlightTest,
       StrokeMissedEndEventThenMouseMoveDuringTextSelecting) {
  EnableDrawAnnotationMode();
  InitializeSimpleSinglePageBasicLayout();

  SelectBrushTool(PdfInkBrush::Type::kHighlighter, kOrangeBrushParams);

  SetSelectionRectsOnFirstPage(base::span_from_ref(gfx::Rect(9, 14, 5, 10)));
  EXPECT_CALL(client(), IsSelectableTextOrLinkArea(_))
      .WillRepeatedly(Return(true));
  EXPECT_CALL(client(), ExtendSelectionByPoint(kMouseUpPoint));

  RunStrokeMissedEndEventThenMouseMoveTest();
}

TEST_P(PdfInkModuleTextHighlightTest, TouchSingleHorizontalSelection) {
  SetUpSingleSelectionTest(kHorizontalSelection);

  // Apply a text highlight stroke at the given points.
  ApplyStrokeWithTouchAtPoints(base::span_from_ref(kStartPointInsidePage0),
                               {base::span_from_ref(kEndPointInsidePage0)},
                               base::span_from_ref(kEndPointInsidePage0));

  constexpr auto kExpectedInputs = std::to_array<PdfInkInputData>(
      {PdfInkInputData(gfx::PointF(15.0, 20.0)),
       PdfInkInputData(gfx::PointF(35.0, 20.0))});
  VerifySingleSelectionTest(kExpectedInputs, /*expected_size=*/10.0f);
}

TEST_P(PdfInkModuleTextHighlightTest, TouchOneClickCount) {
  EnableDrawAnnotationMode();
  InitializeSimpleSinglePageBasicLayout();

  SelectBrushTool(PdfInkBrush::Type::kHighlighter, kOrangeBrushParams);

  // There will be no text selection rects.
  SetSelectionRectMap({});
  SetTextAreaPoints(base::span_from_ref(kStartPointInsidePage0));

  EXPECT_CALL(client(), OnTextOrLinkAreaClick(kStartPointInsidePage0,
                                              /*click_count=*/1));
  EXPECT_CALL(client(), ExtendSelectionByPoint(_)).Times(0);

  blink::WebTouchEvent touch_event =
      CreateTouchEvent(blink::WebInputEvent::Type::kTouchStart,
                       base::span_from_ref(kStartPointInsidePage0));
  EXPECT_TRUE(ink_module().HandleInputEvent(touch_event));

  touch_event = CreateTouchEvent(blink::WebInputEvent::Type::kTouchEnd,
                                 base::span_from_ref(kStartPointInsidePage0));
  EXPECT_TRUE(ink_module().HandleInputEvent(touch_event));

  ExpectStrokeCounts(/*started=*/1, /*modified_finished=*/0,
                     /*unmodified_finished=*/1);
  EXPECT_TRUE(updated_ink_thumbnail_page_indices().empty());

  EXPECT_TRUE(CollectVisibleStrokes().empty());
}

TEST_P(PdfInkModuleTextHighlightTest, MultiTouchDoesNotSelectText) {
  EnableDrawAnnotationMode();
  InitializeSimpleSinglePageBasicLayout();

  SelectBrushTool(PdfInkBrush::Type::kHighlighter, kOrangeBrushParams);

  EXPECT_CALL(client(), IsSelectableTextOrLinkArea(_)).Times(0);

  ApplyStrokeWithTouchAtPointsNotHandled(
      {kStartPointInsidePage0, kStartPointInsidePage0},
      {{kEndPointInsidePage0, kEndPointInsidePage0}},
      {kEndPointInsidePage0, kEndPointInsidePage0});
}

TEST_P(PdfInkModuleTextHighlightTest, PenSingleHorizontalSelection) {
  const std::vector<PdfInkInputData> expected_inputs{
      PdfInkInputData(gfx::PointF(15.0, 20.0)),
      PdfInkInputData(gfx::PointF(35.0, 20.0))};
  SetUpSingleSelectionTest(kHorizontalSelection);

  // Apply a text highlight stroke at the given points.
  ApplyStrokeWithPenAtPoints(base::span_from_ref(kStartPointInsidePage0),
                             {base::span_from_ref(kEndPointInsidePage0)},
                             base::span_from_ref(kEndPointInsidePage0));

  VerifySingleSelectionTest(expected_inputs, /*expected_size=*/10.0f);
}

TEST_P(PdfInkModuleTextHighlightTest, PenOneClickCount) {
  EnableDrawAnnotationMode();
  InitializeSimpleSinglePageBasicLayout();

  SelectBrushTool(PdfInkBrush::Type::kHighlighter, kOrangeBrushParams);

  // There will be no text selection rects.
  SetSelectionRectMap({});
  SetTextAreaPoints(base::span_from_ref(kStartPointInsidePage0));

  EXPECT_CALL(client(), OnTextOrLinkAreaClick(kStartPointInsidePage0,
                                              /*click_count=*/1));
  EXPECT_CALL(client(), ExtendSelectionByPoint(_)).Times(0);

  blink::WebTouchEvent pen_event =
      CreatePenEvent(blink::WebInputEvent::Type::kTouchStart,
                     base::span_from_ref(kStartPointInsidePage0));
  EXPECT_TRUE(ink_module().HandleInputEvent(pen_event));

  pen_event = CreatePenEvent(blink::WebInputEvent::Type::kTouchEnd,
                             base::span_from_ref(kStartPointInsidePage0));
  EXPECT_TRUE(ink_module().HandleInputEvent(pen_event));

  ExpectStrokeCounts(/*started=*/1, /*modified_finished=*/0,
                     /*unmodified_finished=*/1);
  EXPECT_TRUE(updated_ink_thumbnail_page_indices().empty());

  EXPECT_TRUE(CollectVisibleStrokes().empty());
}

TEST_P(PdfInkModuleTextHighlightTest, CursorOnMouseMove) {
  EnableDrawAnnotationMode();
  InitializeSimpleSinglePageBasicLayout();

  // Select the pen tool with a "Light Red" color. The cursor should be the
  // custom pen cursor.
  EXPECT_CALL(client(),
              UpdateInkCursor(CursorBitmapImageSizeEq(SkISize(8, 8))));

  TestAnnotationBrushMessageParams params = kRedBrushParams;
  SelectBrushTool(PdfInkBrush::Type::kPen, params);

  SetTextAreaPoints(base::span_from_ref(kStartPointInsidePage0));

  // Move to a text position. The cursor should remain as the custom pen cursor.
  blink::WebMouseEvent mouse_move_event =
      CreateMoveWebMouseEventToPosition(kEndPointInsidePage0);
  // The event will be considered not handled, but the cursor will still update.
  EXPECT_FALSE(ink_module().HandleInputEvent(mouse_move_event));

  VerifyAndClearExpectations();

  // Select the highlighter tool. The cursor should be the custom highlighter
  // cursor.
  EXPECT_CALL(client(),
              UpdateInkCursor(CursorBitmapImageSizeEq(SkISize(10, 10))));
  params.size = 8.0f;
  SelectBrushTool(PdfInkBrush::Type::kHighlighter, params);

  VerifyAndClearExpectations();

  // Move to a text position. The cursor should be an I-beam.
  EXPECT_CALL(client(),
              UpdateInkCursor(ui::Cursor(ui::mojom::CursorType::kIBeam)));
  mouse_move_event = CreateMoveWebMouseEventToPosition(kStartPointInsidePage0);
  EXPECT_FALSE(ink_module().HandleInputEvent(mouse_move_event));

  VerifyAndClearExpectations();

  // Move to a non-text position. The cursor should restore to the custom
  // highlighter cursor.
  {
    InSequence seq;
    EXPECT_CALL(client(), GetCursor())
        .WillOnce(Return(ui::mojom::CursorType::kIBeam));
    EXPECT_CALL(client(),
                UpdateInkCursor(CursorBitmapImageSizeEq(SkISize(10, 10))));
  }
  mouse_move_event = CreateMoveWebMouseEventToPosition(kEndPointInsidePage0);
  EXPECT_FALSE(ink_module().HandleInputEvent(mouse_move_event));
}

TEST_P(PdfInkModuleTextHighlightTest, CursorOnMouseMoveWhileTextSelecting) {
  EnableDrawAnnotationMode();
  InitializeSimpleSinglePageBasicLayout();

  // Select the highlighter tool. The cursor should be the custom highlighter
  // cursor.
  EXPECT_CALL(client(),
              UpdateInkCursor(CursorBitmapImageSizeEq(SkISize(8, 8))));
  SelectBrushTool(PdfInkBrush::Type::kHighlighter, kRedBrushParams);

  VerifyAndClearExpectations();

  SetTextAreaPoints(base::span_from_ref(kStartPointInsidePage0));

  // Move to a text position. The cursor should be an I-beam.
  EXPECT_CALL(client(),
              UpdateInkCursor(ui::Cursor(ui::mojom::CursorType::kIBeam)));
  blink::WebMouseEvent mouse_move_event =
      CreateMoveWebMouseEventToPosition(kStartPointInsidePage0);
  EXPECT_FALSE(ink_module().HandleInputEvent(mouse_move_event));

  VerifyAndClearExpectations();

  // Start text highlighting and move to a non-text position. The cursor should
  // remain as an I-beam.
  EXPECT_CALL(client(), UpdateInkCursor(_)).Times(0);
  blink::WebMouseEvent mouse_down_event =
      CreateLeftClickWebMouseEventAtPosition(kStartPointInsidePage0);
  EXPECT_TRUE(ink_module().HandleInputEvent(mouse_down_event));
  mouse_move_event =
      CreateLeftClickWebMouseMoveEventAtPosition(kEndPointInsidePage0);
  EXPECT_TRUE(ink_module().HandleInputEvent(mouse_move_event));

  VerifyAndClearExpectations();

  // End text highlighting. The cursor should restore to the custom highlighter
  // cursor.
  EXPECT_CALL(client(),
              UpdateInkCursor(CursorBitmapImageSizeEq(SkISize(8, 8))));
  blink::WebMouseEvent mouse_up_event =
      CreateLeftClickWebMouseUpEventAtPosition(kEndPointInsidePage0);
  EXPECT_TRUE(ink_module().HandleInputEvent(mouse_up_event));
}

TEST_P(PdfInkModuleTextHighlightTest, CursorOnMouseMoveWhileBrushDrawing) {
  EnableDrawAnnotationMode();
  InitializeSimpleSinglePageBasicLayout();

  // Select the highlighter tool. The cursor should be the custom highlighter
  // cursor.
  EXPECT_CALL(client(),
              UpdateInkCursor(CursorBitmapImageSizeEq(SkISize(8, 8))));
  SelectBrushTool(PdfInkBrush::Type::kHighlighter, kRedBrushParams);

  VerifyAndClearExpectations();

  SetTextAreaPoints(base::span_from_ref(kEndPointInsidePage0));

  // Move to a non-text position. The cursor should remain as the custom
  // highlighter cursor.
  EXPECT_CALL(client(), UpdateInkCursor(_)).Times(0);
  blink::WebMouseEvent mouse_move_event =
      CreateMoveWebMouseEventToPosition(kStartPointInsidePage0);
  EXPECT_FALSE(ink_module().HandleInputEvent(mouse_move_event));

  VerifyAndClearExpectations();

  // Start drawing and move to a text position. The cursor should remain as the
  // custom highlighter cursor.
  EXPECT_CALL(client(), UpdateInkCursor(_)).Times(0);
  blink::WebMouseEvent mouse_down_event =
      CreateLeftClickWebMouseEventAtPosition(kStartPointInsidePage0);
  EXPECT_TRUE(ink_module().HandleInputEvent(mouse_down_event));
  mouse_move_event =
      CreateLeftClickWebMouseMoveEventAtPosition(kEndPointInsidePage0);
  EXPECT_TRUE(ink_module().HandleInputEvent(mouse_move_event));

  VerifyAndClearExpectations();

  // End drawing. The cursor should be an I-beam.
  EXPECT_CALL(client(),
              UpdateInkCursor(ui::Cursor(ui::mojom::CursorType::kIBeam)));
  blink::WebMouseEvent mouse_up_event =
      CreateLeftClickWebMouseUpEventAtPosition(kEndPointInsidePage0);
  EXPECT_TRUE(ink_module().HandleInputEvent(mouse_up_event));
}

TEST_P(PdfInkModuleTextHighlightTest, IgnoreVerySmallTextSelection) {
  EnableDrawAnnotationMode();
  InitializeSimpleSinglePageBasicLayout();

  SelectBrushTool(PdfInkBrush::Type::kHighlighter, kOrangeBrushParams);

  static constexpr PdfRect kVerySmallSelectionRect(
      kStartPointInsidePage0.x(), kStartPointInsidePage0.y(),
      kStartPointInsidePage0.x() + 1.0f, kStartPointInsidePage0.y() + 0.001f);
  PdfInkModuleClient::SelectionRectMap pdf_selection_map;
  pdf_selection_map[0].push_back(kVerySmallSelectionRect);
  EXPECT_CALL(client(), GetSelectionRectMap())
      .WillRepeatedly(Return(pdf_selection_map));
  SetTextAreaPoints({kStartPointInsidePage0, kEndPointInsidePage0});
  ApplyStrokeWithMouseAtPoints(kStartPointInsidePage0, {kEndPointInsidePage0},
                               kEndPointInsidePage0);

  // The test case should not crash. Instead, the very small text selection
  // simply gets ignored.
  ExpectStrokeCounts(/*started=*/1, /*modified_finished=*/1,
                     /*unmodified_finished=*/0);
  EXPECT_TRUE(updated_ink_thumbnail_page_indices().empty());
  EXPECT_TRUE(CollectVisibleStrokes().empty());
}

TEST_P(PdfInkModuleTextHighlightTest,
       TextHighlightMissedEndEventThenMouseDown) {
  EnableDrawAnnotationMode();
  InitializeSimpleSinglePageBasicLayout();

  SelectBrushTool(PdfInkBrush::Type::kHighlighter, kOrangeBrushParams);

  SetSelectionRectsOnFirstPage(base::span_from_ref(kHorizontalSelection));
  SetTextAreaPoints(base::span_from_ref(kStartPointInsidePage0));

  EXPECT_CALL(client(), OnTextOrLinkAreaClick(kStartPointInsidePage0,
                                              /*click_count=*/1))
      .Times(2);
  EXPECT_CALL(client(), ExtendSelectionByPoint(kEndPointInsidePage0));

  blink::WebMouseEvent mouse_down_event =
      CreateLeftClickWebMouseEventAtPosition(kStartPointInsidePage0);
  EXPECT_TRUE(ink_module().HandleInputEvent(mouse_down_event));

  blink::WebMouseEvent mouse_move_event =
      CreateLeftClickWebMouseMoveEventAtPosition(kEndPointInsidePage0);
  EXPECT_TRUE(ink_module().HandleInputEvent(mouse_move_event));

  // If the mouse up event went missing during stroking, the next mouse down
  // event should not cause a crash.
  EXPECT_TRUE(ink_module().HandleInputEvent(mouse_down_event));

  ExpectStrokeCounts(/*started=*/2, /*modified_finished=*/1,
                     /*unmodified_finished=*/0);
}

TEST_P(PdfInkModuleTextHighlightTest,
       TextHighlightWithNoEndEventThenTouchStart) {
  EnableDrawAnnotationMode();
  InitializeSimpleSinglePageBasicLayout();

  SelectBrushTool(PdfInkBrush::Type::kHighlighter, kOrangeBrushParams);

  SetSelectionRectsOnFirstPage(base::span_from_ref(kHorizontalSelection));
  SetTextAreaPoints(base::span_from_ref(kStartPointInsidePage0));

  EXPECT_CALL(client(), OnTextOrLinkAreaClick(kStartPointInsidePage0,
                                              /*click_count=*/1))
      .Times(2);
  EXPECT_CALL(client(), ExtendSelectionByPoint(kEndPointInsidePage0));

  blink::WebMouseEvent mouse_down_event =
      CreateLeftClickWebMouseEventAtPosition(kStartPointInsidePage0);
  EXPECT_TRUE(ink_module().HandleInputEvent(mouse_down_event));

  blink::WebMouseEvent mouse_move_event =
      CreateLeftClickWebMouseMoveEventAtPosition(kEndPointInsidePage0);
  EXPECT_TRUE(ink_module().HandleInputEvent(mouse_move_event));

  // If the mouse up event has yet to happen, the next touch start event
  // should not cause a crash.
  EXPECT_TRUE(ink_module().HandleInputEvent(
      CreateTouchEvent(blink::WebInputEvent::Type::kTouchStart,
                       base::span_from_ref(kStartPointInsidePage0))));

  ExpectStrokeCounts(/*started=*/2, /*modified_finished=*/1,
                     /*unmodified_finished=*/0);
}

class PdfInkModuleTextHighlightCaretTest
    : public PdfInkModuleTextHighlightTest {
 protected:
  static constexpr gfx::Rect kTestSelectionRect{22, 15, 12, 12};
  static constexpr PdfInkInputData kExpectedInkInputData{
      .position = gfx::PointF(28.0f, 21.0f)};
  static constexpr float kExpectedBrushSize = 12.0f;

  PdfInkModuleTextHighlightCaretTest() : caret_(&caret_client_) {}

  MockPdfCaret* caret() { return &caret_; }

  // PdfInkModuleTextHighlightTest:
  void SetUp() override {
    PdfInkModuleTextHighlightTest::SetUp();
    EnableDrawAnnotationMode();
    InitializeSimpleSinglePageBasicLayout();
  }

  // Sets up and enables the caret.
  void SetUpCaret() {
    SelectBrushTool(PdfInkBrush::Type::kHighlighter, kOrangeBrushParams);
    ON_CALL(client(), GetPdfCaret()).WillByDefault(Return(caret()));
    caret()->SetEnabled(true);
  }

  // Returns whether `PdfInkModule` handled the keyboard event or not.
  bool HandleKeyboardEvent(const blink::WebKeyboardEvent& event) {
    return ink_module().HandleInputEvent(event);
  }

  // Expects `PdfInkModule` to handle `event` and for it to forward the event to
  // the caret.
  void HandleKeyboardEventAndExpectCaret(const blink::WebKeyboardEvent& event) {
    EXPECT_CALL(*caret(), OnKeyDown(WebKeyboardEventEq(event.windows_key_code,
                                                       event.GetModifiers())))
        .WillOnce(Return(true));

    EXPECT_TRUE(HandleKeyboardEvent(event));
  }

  // Returns a keydown event for `key` with `modifiers`.
  blink::WebKeyboardEvent GenerateKeyDownEvent(ui::KeyboardCode key,
                                               int modifiers) {
    blink::WebKeyboardEvent event(
        blink::WebInputEvent::Type::kKeyDown, modifiers,
        blink::WebInputEvent::GetStaticTimeStampForTests());
    event.windows_key_code = key;
    return event;
  }

  // Runs a test that checks that the arrow key events are not handled when the
  // highlighter brush is not selected or when the PDF caret does not exist or
  // is disabled.
  void RunUnhandledArrowKeyTest(ui::KeyboardCode key) {
    blink::WebKeyboardEvent arrow_event = GenerateKeyDownEvent(
        key, blink::WebInputEvent::Modifiers::kNoModifiers);
    blink::WebKeyboardEvent shift_arrow_event =
        GenerateKeyDownEvent(key, blink::WebInputEvent::Modifiers::kShiftKey);

    // Highlighter not selected.
    EXPECT_FALSE(HandleKeyboardEvent(arrow_event));
    EXPECT_FALSE(HandleKeyboardEvent(shift_arrow_event));

    SelectBrushTool(PdfInkBrush::Type::kHighlighter, kOrangeBrushParams);
    ON_CALL(client(), GetPdfCaret()).WillByDefault(Return(nullptr));

    // Caret does not exist yet.
    EXPECT_FALSE(HandleKeyboardEvent(arrow_event));
    EXPECT_FALSE(HandleKeyboardEvent(shift_arrow_event));

    ON_CALL(client(), GetPdfCaret()).WillByDefault(Return(caret()));

    // Caret not enabled yet.
    EXPECT_FALSE(HandleKeyboardEvent(arrow_event));
    EXPECT_FALSE(HandleKeyboardEvent(shift_arrow_event));
  }

  // Runs a test that checks that the arrow key events are handled and highlight
  // text.
  void RunHandledArrowKeyTest(ui::KeyboardCode key) {
    SetUpCaret();

    // When not yet text highlighting, arrow key events without modifiers are
    // passed directly to `PdfCaret`.
    InSequence seq;
    SetSelectionRectMap({});

    blink::WebKeyboardEvent arrow_event = GenerateKeyDownEvent(
        key, blink::WebInputEvent::Modifiers::kNoModifiers);
    HandleKeyboardEventAndExpectCaret(arrow_event);

    EXPECT_EQ(0, client().stroke_started_count());

    blink::WebKeyboardEvent shift_arrow_event =
        GenerateKeyDownEvent(key, blink::WebInputEvent::Modifiers::kShiftKey);

    // Start text highlight.
    TestKeyEventTextHighlights(shift_arrow_event,
                               /*test_selection_rect=*/{10, 15, 12, 12});
    // Continue text highlight.
    TestKeyEventTextHighlights(shift_arrow_event,
                               /*test_selection_rect=*/kTestSelectionRect);
    TestKeyEventEndsTextHighlight(arrow_event,
                                  /*expected_event_handled=*/true,
                                  base::span_from_ref(kExpectedInkInputData),
                                  kExpectedBrushSize);
  }

  // Tests that `event` is passed to `PdfCaret` and starts or continues text
  // highlighting. The selection rect after text highlighting will be set to
  // `text_selection_rect`.
  void TestKeyEventTextHighlights(const blink::WebKeyboardEvent& event,
                                  const gfx::Rect& test_selection_rect) {
    SetSelectionRectsOnFirstPage(base::span_from_ref(test_selection_rect));
    HandleKeyboardEventAndExpectCaret(event);

    ExpectStrokeCounts(/*started=*/1, /*modified_finished=*/0,
                       /*unmodified_finished=*/0);
  }

  // Tests that `event` ends an existing text highlight.
  // `expected_event_handled` indicates whether `event` should be considered
  // handled or not by `PdfInkModule`. `expected_inputs` is the expected brush
  // inputs, and `expected_size` is the expected brush size.
  void TestKeyEventEndsTextHighlight(
      const blink::WebKeyboardEvent& event,
      bool expected_event_handled,
      base::span<const PdfInkInputData> expected_inputs,
      float expected_size) {
    if (expected_event_handled) {
      EXPECT_CALL(*caret(), OnKeyDown(WebKeyboardEventEq(event.windows_key_code,
                                                         event.GetModifiers())))
          .WillOnce(Return(true));
    }

    EXPECT_EQ(expected_event_handled, HandleKeyboardEvent(event));

    ExpectStrokeCounts(/*started=*/1, /*modified_finished=*/1,
                       /*unmodified_finished=*/0);
    EXPECT_THAT(updated_ink_thumbnail_page_indices(), ElementsAre(0));

    std::optional<ink::StrokeInputBatch> expected_batch =
        CreateInkInputBatch(expected_inputs);
    ASSERT_TRUE(expected_batch.has_value());

    const PdfInkBrush expected_brush(PdfInkBrush::Type::kHighlighter,
                                     kOrangeColor, /*size=*/expected_size);
    EXPECT_THAT(
        CollectVisibleStrokes(),
        ElementsAre(Pair(0, Pointwise(InkStrokeEq(expected_brush.ink_brush()),
                                      {expected_batch.value()}))));
  }

  // `caret_client_` must be declared before `caret_`.
  MockPdfCaretClient caret_client_;
  MockPdfCaret caret_;
};

TEST_P(PdfInkModuleTextHighlightCaretTest, UnhandledArrowKeyLeft) {
  RunUnhandledArrowKeyTest(ui::KeyboardCode::VKEY_LEFT);
}

TEST_P(PdfInkModuleTextHighlightCaretTest, UnhandledArrowKeyRight) {
  RunUnhandledArrowKeyTest(ui::KeyboardCode::VKEY_RIGHT);
}

TEST_P(PdfInkModuleTextHighlightCaretTest, UnhandledArrowKeyUp) {
  RunUnhandledArrowKeyTest(ui::KeyboardCode::VKEY_UP);
}

TEST_P(PdfInkModuleTextHighlightCaretTest, UnhandledArrowKeyDown) {
  RunUnhandledArrowKeyTest(ui::KeyboardCode::VKEY_DOWN);
}

TEST_P(PdfInkModuleTextHighlightCaretTest, HandledArrowKeyLeft) {
  RunHandledArrowKeyTest(ui::KeyboardCode::VKEY_LEFT);
}

TEST_P(PdfInkModuleTextHighlightCaretTest, HandledArrowKeyRight) {
  RunHandledArrowKeyTest(ui::KeyboardCode::VKEY_RIGHT);
}

TEST_P(PdfInkModuleTextHighlightCaretTest, HandledArrowKeyUp) {
  RunHandledArrowKeyTest(ui::KeyboardCode::VKEY_UP);
}

TEST_P(PdfInkModuleTextHighlightCaretTest, HandledArrowKeyDown) {
  RunHandledArrowKeyTest(ui::KeyboardCode::VKEY_DOWN);
}

TEST_P(PdfInkModuleTextHighlightCaretTest, ControlPEndsTextHighlight) {
  SetUpCaret();
  InSequence seq;
  TestKeyEventTextHighlights(
      GenerateKeyDownEvent(ui::KeyboardCode::VKEY_LEFT,
                           blink::WebInputEvent::Modifiers::kShiftKey),
      kTestSelectionRect);
  TestKeyEventEndsTextHighlight(
      GenerateKeyDownEvent(ui::KeyboardCode::VKEY_P,
                           blink::WebInputEvent::Modifiers::kControlKey),
      /*expected_event_handled=*/false,
      base::span_from_ref(kExpectedInkInputData), kExpectedBrushSize);
}

TEST_P(PdfInkModuleTextHighlightCaretTest, ControlSEndsTextHighlight) {
  SetUpCaret();
  InSequence seq;
  TestKeyEventTextHighlights(
      GenerateKeyDownEvent(ui::KeyboardCode::VKEY_LEFT,
                           blink::WebInputEvent::Modifiers::kShiftKey),
      kTestSelectionRect);
  TestKeyEventEndsTextHighlight(
      GenerateKeyDownEvent(ui::KeyboardCode::VKEY_S,
                           blink::WebInputEvent::Modifiers::kControlKey),
      /*expected_event_handled=*/false,
      base::span_from_ref(kExpectedInkInputData), kExpectedBrushSize);
}

TEST_P(PdfInkModuleTextHighlightCaretTest, UnhandledKeyEvents) {
  blink::WebKeyboardEvent a_event = GenerateKeyDownEvent(
      ui::KeyboardCode::VKEY_A, blink::WebInputEvent::Modifiers::kNoModifiers);
  blink::WebKeyboardEvent control_p_event = GenerateKeyDownEvent(
      ui::KeyboardCode::VKEY_P, blink::WebInputEvent::Modifiers::kControlKey);
  blink::WebKeyboardEvent control_s_event = GenerateKeyDownEvent(
      ui::KeyboardCode::VKEY_S, blink::WebInputEvent::Modifiers::kControlKey);

  // Highlighter not selected.
  EXPECT_FALSE(HandleKeyboardEvent(a_event));
  EXPECT_FALSE(HandleKeyboardEvent(control_p_event));
  EXPECT_FALSE(HandleKeyboardEvent(control_s_event));

  SelectBrushTool(PdfInkBrush::Type::kHighlighter, kOrangeBrushParams);
  ON_CALL(client(), GetPdfCaret()).WillByDefault(Return(nullptr));

  // Caret does not exist yet.
  EXPECT_FALSE(HandleKeyboardEvent(a_event));
  EXPECT_FALSE(HandleKeyboardEvent(control_p_event));
  EXPECT_FALSE(HandleKeyboardEvent(control_s_event));

  ON_CALL(client(), GetPdfCaret()).WillByDefault(Return(caret()));

  // Caret not enabled yet.
  EXPECT_FALSE(HandleKeyboardEvent(a_event));
  EXPECT_FALSE(HandleKeyboardEvent(control_p_event));
  EXPECT_FALSE(HandleKeyboardEvent(control_s_event));

  caret()->SetEnabled(true);

  // `PdfInkModule` ignores these key events.
  EXPECT_FALSE(HandleKeyboardEvent(a_event));
  EXPECT_FALSE(HandleKeyboardEvent(control_p_event));
  EXPECT_FALSE(HandleKeyboardEvent(control_s_event));
}

class PdfInkModuleTextHighlightMetricsTestBase
    : public PdfInkModuleMetricsTestBase {
 protected:
  static constexpr char kTextHighlightColorMetric[] =
      "PDF.Ink2TextHighlighterColor";
  static constexpr char kTextHighlightInputDeviceMetric[] =
      "PDF.Ink2TextHighlightInputDeviceType";

  // Validates the the total counts of all relevant text highlight metrics.
  void ValidateHighlightMetricCounts(int expected_metric_count) {
    histograms().ExpectTotalCount(kTextHighlightColorMetric,
                                  expected_metric_count);
    histograms().ExpectTotalCount(kTextHighlightInputDeviceMetric,
                                  expected_metric_count);
  }
};

class PdfInkModuleTextHighlightMetricsTest
    : public PdfInkModuleTextHighlightMetricsTestBase,
      public PdfInkModuleTextHighlightTest {
 protected:
  static constexpr base::TimeDelta kOneMs = base::Milliseconds(1);
  static constexpr base::TimeDelta kTextSelectionClickTimeMs =
      base::Milliseconds(ui::kDoubleClickTimeMs);

  // Helper for moving the mouse and releasing left click at `point` with
  // `click_count` clicks.
  void MouseMoveAndUpAtPoint(const gfx::PointF& point, int click_count) {
    EXPECT_TRUE(ink_module().HandleInputEvent(
        CreateLeftClickWebMouseMoveEventAtPosition(point)));

    blink::WebMouseEvent mouse_up_event =
        MouseEventBuilder()
            .CreateLeftMouseUpAtPosition(point)
            .SetClickCount(click_count)
            .Build();
    EXPECT_TRUE(ink_module().HandleInputEvent(mouse_up_event));
  }
};

TEST_P(PdfInkModuleTextHighlightMetricsTest,
       StrokeDoesNotAffectTextHighlightMetrics) {
  EnableDrawAnnotationMode();
  InitializeSimpleSinglePageBasicLayout();

  SelectBrushTool(PdfInkBrush::Type::kHighlighter, kOrangeBrushParams);
  RunStrokeCheckTest(/*annotation_mode_enabled=*/true);

  ExpectStrokeCounts(/*started=*/1, /*modified_finished=*/1,
                     /*unmodified_finished=*/0);
  ValidateHighlightMetricCounts(0);
}

TEST_P(PdfInkModuleTextHighlightMetricsTest,
       TextHighlightDoesNotAffectStrokeMetrics) {
  RunSingleSelectionWithMouseTest(
      /*selection_rect=*/kHorizontalSelection,
      /*expected_inputs=*/
      {PdfInkInputData(gfx::PointF(15.0, 20.0)),
       PdfInkInputData(gfx::PointF(35.0, 20.0))},
      /*expected_size=*/10.0);

  histograms().ExpectTotalCount(kHighlighterColorMetric, 0);
  histograms().ExpectTotalCount(kInputDeviceMetric, 0);
  histograms().ExpectTotalCount(kHighlighterSizeMetric, 0);
  histograms().ExpectTotalCount(kTypeMetric, 0);
}

TEST_P(PdfInkModuleTextHighlightMetricsTest,
       TextHighlightUndoRedoDoesNotAffectMetrics) {
  EnableDrawAnnotationMode();
  InitializeSimpleSinglePageBasicLayout();

  RunSingleSelectionWithMouseTest(
      /*selection_rect=*/kHorizontalSelection,
      /*expected_inputs=*/
      {PdfInkInputData(gfx::PointF(15.0, 20.0)),
       PdfInkInputData(gfx::PointF(35.0, 20.0))},
      /*expected_size=*/10.0);

  ExpectStrokeCounts(/*started=*/1, /*modified_finished=*/1,
                     /*unmodified_finished=*/0);
  ValidateHighlightMetricCounts(1);

  PerformUndo();
  PerformRedo();

  ExpectStrokeCounts(/*started=*/1, /*modified_finished=*/1,
                     /*unmodified_finished=*/0);
  ValidateHighlightMetricCounts(1);
}

TEST_P(PdfInkModuleTextHighlightMetricsTest, Color) {
  EnableDrawAnnotationMode();
  InitializeSimpleSinglePageBasicLayout();

  SelectBrushTool(PdfInkBrush::Type::kHighlighter, kRedBrushParams);

  histograms().ExpectTotalCount(kTextHighlightColorMetric, 0);

  SetSelectionRectsOnFirstPage(base::span_from_ref(kHorizontalSelection));
  SetTextAreaPoints({kStartPointInsidePage0, kEndPointInsidePage0});

  ApplyStrokeWithMouseAtPoints(kStartPointInsidePage0, {kEndPointInsidePage0},
                               kEndPointInsidePage0);

  histograms().ExpectUniqueSample(kTextHighlightColorMetric,
                                  StrokeMetricHighlighterColor::kLightRed, 1);

  SelectBrushTool(PdfInkBrush::Type::kHighlighter, kOrangeBrushParams);
  ApplyStrokeWithMouseAtPoints(kStartPointInsidePage0, {kEndPointInsidePage0},
                               kEndPointInsidePage0);

  ExpectStrokeCounts(/*started=*/2, /*modified_finished=*/2,
                     /*unmodified_finished=*/0);
  histograms().ExpectBucketCount(kTextHighlightColorMetric,
                                 StrokeMetricHighlighterColor::kOrange, 1);
  histograms().ExpectTotalCount(kTextHighlightColorMetric, 2);
}

TEST_P(PdfInkModuleTextHighlightMetricsTest, InputDevice) {
  EnableDrawAnnotationMode();
  InitializeSimpleSinglePageBasicLayout();

  SelectBrushTool(PdfInkBrush::Type::kHighlighter, kOrangeBrushParams);

  histograms().ExpectTotalCount(kTextHighlightInputDeviceMetric, 0);

  SetSelectionRectsOnFirstPage(base::span_from_ref(kHorizontalSelection));
  SetTextAreaPoints({kStartPointInsidePage0, kEndPointInsidePage0});

  // Apply a text highlight stroke with mouse.
  ApplyStrokeWithMouseAtPoints(kStartPointInsidePage0, {kEndPointInsidePage0},
                               kEndPointInsidePage0);

  histograms().ExpectUniqueSample(kTextHighlightInputDeviceMetric,
                                  StrokeMetricInputDeviceType::kMouse, 1);

  // Apply a text highlight stroke with touch.
  ApplyStrokeWithTouchAtPoints(base::span_from_ref(kStartPointInsidePage0),
                               {base::span_from_ref(kEndPointInsidePage0)},
                               base::span_from_ref(kEndPointInsidePage0));

  ExpectStrokeCounts(/*started=*/2, /*modified_finished=*/2,
                     /*unmodified_finished=*/0);
  histograms().ExpectBucketCount(kTextHighlightInputDeviceMetric,
                                 StrokeMetricInputDeviceType::kTouch, 1);
  histograms().ExpectTotalCount(kTextHighlightInputDeviceMetric, 2);

  // Apply a text highlight stroke with pen.
  ApplyStrokeWithPenAtPoints(base::span_from_ref(kStartPointInsidePage0),
                             {base::span_from_ref(kEndPointInsidePage0)},
                             base::span_from_ref(kEndPointInsidePage0));

  ExpectStrokeCounts(/*started=*/3, /*modified_finished=*/3,
                     /*unmodified_finished=*/0);
  histograms().ExpectBucketCount(kTextHighlightInputDeviceMetric,
                                 StrokeMetricInputDeviceType::kPen, 1);
  histograms().ExpectTotalCount(kTextHighlightInputDeviceMetric, 3);
}

TEST_P(PdfInkModuleTextHighlightMetricsTest, TwoClickDelay) {
  EnableDrawAnnotationMode();
  InitializeSimpleSinglePageBasicLayout();

  SelectBrushTool(PdfInkBrush::Type::kHighlighter, kOrangeBrushParams);
  SetTextAreaPoints(base::span_from_ref(kStartPointInsidePage0));

  // Click twice.
  ClickTextAtPoint(kStartPointInsidePage0, /*click_count=*/1);
  SetSelectionRectsOnFirstPage(base::span_from_ref(kHorizontalSelection));
  ClickTextAtPoint(kStartPointInsidePage0, /*click_count=*/2);

  ExpectStrokeCounts(/*started=*/2, /*modified_finished=*/1,
                     /*unmodified_finished=*/1);
  ValidateHighlightMetricCounts(0);

  // Fast forward to just one ms before the timer should fire.
  GetPdfTestTaskEnvironment().FastForwardBy(kTextSelectionClickTimeMs - kOneMs);

  ExpectStrokeCounts(/*started=*/2, /*modified_finished=*/1,
                     /*unmodified_finished=*/1);
  ValidateHighlightMetricCounts(0);

  // Fast forward by one ms so the timer fires.
  GetPdfTestTaskEnvironment().FastForwardBy(kOneMs);

  ExpectStrokeCounts(/*started=*/2, /*modified_finished=*/1,
                     /*unmodified_finished=*/1);
  ValidateHighlightMetricCounts(1);
}

TEST_P(PdfInkModuleTextHighlightMetricsTest, TwoClickMove) {
  EnableDrawAnnotationMode();
  InitializeSimpleSinglePageBasicLayout();

  SelectBrushTool(PdfInkBrush::Type::kHighlighter, kOrangeBrushParams);
  SetTextAreaPoints(base::span_from_ref(kStartPointInsidePage0));

  // Click once.
  ClickTextAtPoint(kStartPointInsidePage0, /*click_count=*/1);

  // Click the second time, but without mouseup.
  SetSelectionRectsOnFirstPage(base::span_from_ref(kHorizontalSelection));
  blink::WebMouseEvent mouse_event =
      MouseEventBuilder()
          .CreateLeftClickAtPosition(kStartPointInsidePage0)
          .SetClickCount(2)
          .Build();
  EXPECT_TRUE(ink_module().HandleInputEvent(mouse_event));

  ExpectStrokeCounts(/*started=*/2, /*modified_finished=*/1,
                     /*unmodified_finished=*/1);
  ValidateHighlightMetricCounts(0);

  // Fast forward by the click time duration.
  GetPdfTestTaskEnvironment().FastForwardBy(kTextSelectionClickTimeMs);

  ExpectStrokeCounts(/*started=*/2, /*modified_finished=*/1,
                     /*unmodified_finished=*/1);
  ValidateHighlightMetricCounts(1);

  // Now move and release at a new position.
  MouseMoveAndUpAtPoint(kEndPointInsidePage0, /*click_count=*/2);

  ExpectStrokeCounts(/*started=*/2, /*modified_finished=*/1,
                     /*unmodified_finished=*/1);
  ValidateHighlightMetricCounts(1);

  // Fast forward by the click time duration.
  GetPdfTestTaskEnvironment().FastForwardBy(kTextSelectionClickTimeMs);

  ExpectStrokeCounts(/*started=*/2, /*modified_finished=*/1,
                     /*unmodified_finished=*/1);
  ValidateHighlightMetricCounts(1);
}

TEST_P(PdfInkModuleTextHighlightMetricsTest, TwoClickMoveHighlight) {
  EnableDrawAnnotationMode();
  InitializeSimpleSinglePageBasicLayout();

  SelectBrushTool(PdfInkBrush::Type::kHighlighter, kOrangeBrushParams);
  constexpr gfx::PointF kStartPoint2InsidePage0{12.0f, 10.0f};
  SetTextAreaPoints({kStartPointInsidePage0, kStartPoint2InsidePage0});

  // Click twice.
  ClickTextAtPoint(kStartPointInsidePage0, /*click_count=*/1);
  SetSelectionRectsOnFirstPage(base::span_from_ref(kHorizontalSelection));
  ClickTextAtPoint(kStartPointInsidePage0, /*click_count=*/2);

  ExpectStrokeCounts(/*started=*/2, /*modified_finished=*/1,
                     /*unmodified_finished=*/1);
  ValidateHighlightMetricCounts(0);

  // Text highlight elsewhere.
  EXPECT_CALL(client(), OnTextOrLinkAreaClick(kStartPoint2InsidePage0,
                                              /*click_count=*/1));
  EXPECT_CALL(client(), ExtendSelectionByPoint(kEndPointInsidePage0));
  ApplyStrokeWithMouseAtPoints(kStartPoint2InsidePage0, {kEndPointInsidePage0},
                               kEndPointInsidePage0);

  // There should be reports for the double-click highlight and the new
  // highlight.
  ExpectStrokeCounts(/*started=*/3, /*modified_finished=*/2,
                     /*unmodified_finished=*/1);
  ValidateHighlightMetricCounts(2);

  // Fast forward by the click time duration.
  GetPdfTestTaskEnvironment().FastForwardBy(kTextSelectionClickTimeMs);

  ExpectStrokeCounts(/*started=*/3, /*modified_finished=*/2,
                     /*unmodified_finished=*/1);
  ValidateHighlightMetricCounts(2);
}

TEST_P(PdfInkModuleTextHighlightMetricsTest, ThreeClickDelay) {
  EnableDrawAnnotationMode();
  InitializeSimpleSinglePageBasicLayout();

  SelectBrushTool(PdfInkBrush::Type::kHighlighter, kOrangeBrushParams);
  SetTextAreaPoints(base::span_from_ref(kStartPointInsidePage0));

  // Click twice.
  ClickTextAtPoint(kStartPointInsidePage0, /*click_count=*/1);
  SetSelectionRectsOnFirstPage(base::span_from_ref(kHorizontalSelection));
  ClickTextAtPoint(kStartPointInsidePage0, /*click_count=*/2);

  ExpectStrokeCounts(/*started=*/2, /*modified_finished=*/1,
                     /*unmodified_finished=*/1);
  ValidateHighlightMetricCounts(0);

  // Fast forward to just one ms before the timer would fire.
  GetPdfTestTaskEnvironment().FastForwardBy(kTextSelectionClickTimeMs - kOneMs);

  ExpectStrokeCounts(/*started=*/2, /*modified_finished=*/1,
                     /*unmodified_finished=*/1);
  ValidateHighlightMetricCounts(0);

  // Click the third time.
  ClickTextAtPoint(kStartPointInsidePage0, /*click_count=*/3);

  ExpectStrokeCounts(/*started=*/3, /*modified_finished=*/2,
                     /*unmodified_finished=*/1);
  ValidateHighlightMetricCounts(1);

  // Fast forward by the click time duration.
  GetPdfTestTaskEnvironment().FastForwardBy(kTextSelectionClickTimeMs);

  ExpectStrokeCounts(/*started=*/3, /*modified_finished=*/2,
                     /*unmodified_finished=*/1);
  ValidateHighlightMetricCounts(1);
}

TEST_P(PdfInkModuleTextHighlightMetricsTest, ThreeClickMove) {
  EnableDrawAnnotationMode();
  InitializeSimpleSinglePageBasicLayout();

  SelectBrushTool(PdfInkBrush::Type::kHighlighter, kOrangeBrushParams);
  SetTextAreaPoints(base::span_from_ref(kStartPointInsidePage0));

  // Click twice.
  ClickTextAtPoint(kStartPointInsidePage0, /*click_count=*/1);
  SetSelectionRectsOnFirstPage(base::span_from_ref(kHorizontalSelection));
  ClickTextAtPoint(kStartPointInsidePage0, /*click_count=*/2);

  ExpectStrokeCounts(/*started=*/2, /*modified_finished=*/1,
                     /*unmodified_finished=*/1);
  ValidateHighlightMetricCounts(0);

  // Click the third time, but without mouseup.
  blink::WebMouseEvent mouse_event =
      MouseEventBuilder()
          .CreateLeftClickAtPosition(kStartPointInsidePage0)
          .SetClickCount(3)
          .Build();
  EXPECT_TRUE(ink_module().HandleInputEvent(mouse_event));

  ExpectStrokeCounts(/*started=*/3, /*modified_finished=*/2,
                     /*unmodified_finished=*/1);
  ValidateHighlightMetricCounts(1);

  // Fast forward by the click time duration.
  GetPdfTestTaskEnvironment().FastForwardBy(kTextSelectionClickTimeMs);

  ExpectStrokeCounts(/*started=*/3, /*modified_finished=*/2,
                     /*unmodified_finished=*/1);
  ValidateHighlightMetricCounts(1);

  // Now move and release at a new position.
  MouseMoveAndUpAtPoint(kEndPointInsidePage0, /*click_count=*/3);

  ExpectStrokeCounts(/*started=*/3, /*modified_finished=*/2,
                     /*unmodified_finished=*/1);
  ValidateHighlightMetricCounts(1);
}

class PdfInkModuleTextHighlightCaretMetricsTest
    : public PdfInkModuleTextHighlightMetricsTestBase,
      public PdfInkModuleTextHighlightCaretTest {
 protected:
  void ApplySampleTextHighlight() {
    SetSelectionRectsOnFirstPage(base::span_from_ref(kHorizontalSelection));
    InSequence seq;
    HandleKeyboardEventAndExpectCaret(
        GenerateKeyDownEvent(ui::KeyboardCode::VKEY_RIGHT,
                             blink::WebInputEvent::Modifiers::kShiftKey));
    HandleKeyboardEventAndExpectCaret(
        GenerateKeyDownEvent(ui::KeyboardCode::VKEY_RIGHT,
                             blink::WebInputEvent::Modifiers::kNoModifiers));
  }
};

TEST_P(PdfInkModuleTextHighlightCaretMetricsTest,
       TextHighlightUndoRedoDoesNotAffectMetrics) {
  SetUpCaret();

  ValidateHighlightMetricCounts(0);

  RunHandledArrowKeyTest(ui::KeyboardCode::VKEY_RIGHT);

  ValidateHighlightMetricCounts(1);

  PerformUndo();
  PerformRedo();

  ExpectStrokeCounts(/*started=*/1, /*modified_finished=*/1,
                     /*unmodified_finished=*/0);
  ValidateHighlightMetricCounts(1);
}

TEST_P(PdfInkModuleTextHighlightCaretMetricsTest, Color) {
  SetUpCaret();

  SelectBrushTool(PdfInkBrush::Type::kHighlighter, kRedBrushParams);

  histograms().ExpectTotalCount(kTextHighlightColorMetric, 0);

  ApplySampleTextHighlight();

  ExpectStrokeCounts(/*started=*/1, /*modified_finished=*/1,
                     /*unmodified_finished=*/0);
  histograms().ExpectUniqueSample(kTextHighlightColorMetric,
                                  StrokeMetricHighlighterColor::kLightRed, 1);

  SelectBrushTool(PdfInkBrush::Type::kHighlighter, kOrangeBrushParams);
  ApplySampleTextHighlight();

  ExpectStrokeCounts(/*started=*/2, /*modified_finished=*/2,
                     /*unmodified_finished=*/0);
  histograms().ExpectBucketCount(kTextHighlightColorMetric,
                                 StrokeMetricHighlighterColor::kOrange, 1);
  histograms().ExpectTotalCount(kTextHighlightColorMetric, 2);
}

TEST_P(PdfInkModuleTextHighlightCaretMetricsTest, InputDevice) {
  SetUpCaret();

  histograms().ExpectTotalCount(kTextHighlightInputDeviceMetric, 0);

  ApplySampleTextHighlight();

  // Apply a text highlight stroke with keyboard.
  histograms().ExpectUniqueSample(kTextHighlightInputDeviceMetric,
                                  StrokeMetricInputDeviceType::kKeyboard, 1);

  // Apply a text highlight stroke with mouse.
  SetTextAreaPoints({kStartPointInsidePage0, kEndPointInsidePage0});
  ApplyStrokeWithMouseAtPoints(kStartPointInsidePage0, {kEndPointInsidePage0},
                               kEndPointInsidePage0);

  histograms().ExpectBucketCount(kTextHighlightInputDeviceMetric,
                                 StrokeMetricInputDeviceType::kMouse, 1);
  histograms().ExpectBucketCount(kTextHighlightInputDeviceMetric,
                                 StrokeMetricInputDeviceType::kKeyboard, 1);
  histograms().ExpectTotalCount(kTextHighlightInputDeviceMetric, 2);
}

INSTANTIATE_TEST_SUITE_P(All,
                         PdfInkModuleTest,
                         testing::ValuesIn(GetAllInkTestVariations()));
INSTANTIATE_TEST_SUITE_P(All,
                         PdfInkModuleStrokeTest,
                         testing::ValuesIn(GetAllInkTestVariations()));
INSTANTIATE_TEST_SUITE_P(All,
                         PdfInkModuleUndoRedoTest,
                         testing::ValuesIn(GetAllInkTestVariations()));
INSTANTIATE_TEST_SUITE_P(All,
                         PdfInkModuleGetVisibleStrokesTest,
                         testing::ValuesIn(GetAllInkTestVariations()));
INSTANTIATE_TEST_SUITE_P(All,
                         PdfInkModuleMetricsTest,
                         testing::ValuesIn(GetAllInkTestVariations()));
INSTANTIATE_TEST_SUITE_P(
    All,
    PdfInkModuleTextHighlightTest,
    testing::ValuesIn(GetInkTestVariationsWithTextHighlighting()));
INSTANTIATE_TEST_SUITE_P(
    All,
    PdfInkModuleTextHighlightCaretTest,
    testing::ValuesIn(GetInkTestVariationsWithTextHighlighting()));
INSTANTIATE_TEST_SUITE_P(
    All,
    PdfInkModuleTextHighlightMetricsTest,
    testing::ValuesIn(GetInkTestVariationsWithTextHighlighting()));
INSTANTIATE_TEST_SUITE_P(
    All,
    PdfInkModuleTextHighlightCaretMetricsTest,
    testing::ValuesIn(GetInkTestVariationsWithTextHighlighting()));

}  // namespace chrome_pdf