// Copyright 2021 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_view_web_plugin.h"

#include <stdint.h>

#include <array>
#include <functional>
#include <memory>
#include <string>
#include <string_view>
#include <utility>
#include <vector>

#include "base/containers/span.h"
#include "base/location.h"
#include "base/memory/raw_ptr.h"
#include "base/memory/weak_ptr.h"
#include "base/run_loop.h"
#include "base/strings/stringprintf.h"
#include "base/strings/to_string.h"
#include "base/task/single_thread_task_runner.h"
#include "base/test/icu_test_util.h"
#include "base/test/metrics/histogram_tester.h"
#include "base/test/run_until.h"
#include "base/test/scoped_feature_list.h"
#include "base/test/test_future.h"
#include "base/test/values_test_util.h"
#include "base/time/time.h"
#include "base/values.h"
#include "cc/paint/paint_canvas.h"
#include "cc/test/pixel_comparator.h"
#include "cc/test/pixel_test_utils.h"
#include "mojo/public/cpp/bindings/associated_receiver.h"
#include "mojo/public/cpp/bindings/associated_remote.h"
#include "net/cookies/site_for_cookies.h"
#include "pdf/accessibility_structs.h"
#include "pdf/buildflags.h"
#include "pdf/content_restriction.h"
#include "pdf/document_layout.h"
#include "pdf/mojom/pdf.mojom.h"
#include "pdf/page_orientation.h"
#include "pdf/paint_ready_rect.h"
#include "pdf/pdf_accessibility_data_handler.h"
#include "pdf/pdf_features.h"
#include "pdf/pdf_ink_annotation_mode.h"
#include "pdf/test/fake_annotation_agent_host.h"
#include "pdf/test/input_event_util.h"
#include "pdf/test/mock_web_associated_url_loader.h"
#include "pdf/test/mouse_event_builder.h"
#include "pdf/test/test_helpers.h"
#include "pdf/test/test_pdfium_engine.h"
#include "printing/metafile_skia.h"
#include "printing/units.h"
#include "services/screen_ai/buildflags/buildflags.h"
#include "testing/gmock/include/gmock/gmock.h"
#include "testing/gtest/include/gtest/gtest.h"
#include "third_party/blink/public/common/input/web_coalesced_input_event.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/loader/http_body_element_type.h"
#include "third_party/blink/public/common/renderer_preferences/renderer_preferences.h"
#include "third_party/blink/public/platform/web_data.h"
#include "third_party/blink/public/platform/web_http_body.h"
#include "third_party/blink/public/platform/web_http_header_visitor.h"
#include "third_party/blink/public/platform/web_input_event_result.h"
#include "third_party/blink/public/platform/web_string.h"
#include "third_party/blink/public/platform/web_text_input_type.h"
#include "third_party/blink/public/platform/web_url.h"
#include "third_party/blink/public/platform/web_url_request.h"
#include "third_party/blink/public/platform/web_url_response.h"
#include "third_party/blink/public/web/web_associated_url_loader.h"
#include "third_party/blink/public/web/web_associated_url_loader_client.h"
#include "third_party/blink/public/web/web_plugin_container.h"
#include "third_party/blink/public/web/web_plugin_params.h"
#include "third_party/blink/public/web/web_print_params.h"
#include "third_party/skia/include/core/SkBitmap.h"
#include "third_party/skia/include/core/SkCanvas.h"
#include "third_party/skia/include/core/SkColor.h"
#include "third_party/skia/include/core/SkRefCnt.h"
#include "third_party/skia/include/core/SkSurface.h"
#include "ui/base/cursor/cursor.h"
#include "ui/events/blink/blink_event_util.h"
#include "ui/events/keycodes/dom/dom_code.h"
#include "ui/events/keycodes/dom/dom_key.h"
#include "ui/gfx/canvas.h"
#include "ui/gfx/geometry/point.h"
#include "ui/gfx/geometry/point_f.h"
#include "ui/gfx/geometry/rect.h"
#include "ui/gfx/geometry/size.h"
#include "ui/gfx/geometry/skia_conversions.h"
#include "ui/gfx/geometry/vector2d_f.h"
#include "ui/gfx/range/range.h"
#include "ui/latency/latency_info.h"
#include "url/gurl.h"

#if BUILDFLAG(ENABLE_PDF_INK2)
#include "base/files/file_path.h"
#include "pdf/pdf_ink_brush.h"
#include "pdf/pdf_ink_metrics_handler.h"
#include "pdf/pdf_ink_module_client.h"
#include "pdf/test/pdf_ink_test_helpers.h"
#include "third_party/ink/src/ink/strokes/stroke.h"
#endif

using printing::kUnitConversionFactorPixelsToPoints;

namespace chrome_pdf {

namespace {

using ::testing::_;
using ::testing::AnyNumber;
using ::testing::ElementsAre;
using ::testing::ElementsAreArray;
using ::testing::Eq;
using ::testing::InSequence;
using ::testing::IsEmpty;
using ::testing::IsFalse;
using ::testing::IsTrue;
using ::testing::Matcher;
using ::testing::MockFunction;
using ::testing::NiceMock;
using ::testing::Optional;
using ::testing::Pair;
using ::testing::Pointwise;
using ::testing::Return;
using ::testing::SaveArg;
using ::testing::SizeIs;

#if BUILDFLAG(ENABLE_PDF_SAVE_TO_DRIVE)
constexpr uint8_t kSaveDataBuffer[] = {'b', 'u', 'f', 'f', 'e', 'r'};
#endif  // BUILDFLAG(ENABLE_PDF_SAVE_TO_DRIVE)

#if BUILDFLAG(ENABLE_PDF_INK2)
constexpr char kPdfLoadedWithInkTextAnnotationsMetric[] =
    "PDF.LoadedWithInkTextAnnotations";
constexpr char kPdfLoadedWithV2InkAnnotationsMetric[] =
    "PDF.LoadedWithV2InkAnnotations2";
#endif  // BUILDFLAG(ENABLE_PDF_INK2)

// `kCanvasSize` needs to be big enough to hold plugin's snapshots during
// testing.
constexpr gfx::Size kCanvasSize(100, 100);

// Note: Make sure `kDefaultColor` is different from `kPaintColor` and the
// plugin's background color. This will help identify bitmap changes after
// painting.
constexpr SkColor kDefaultColor = SK_ColorGREEN;

constexpr SkColor kPaintColor = SK_ColorRED;

struct PaintParams {
  // The plugin container's device scale.
  float device_scale;

  // The window area in CSS pixels.
  gfx::Rect window_rect;

  // The target painting area on the canvas in CSS pixels.
  gfx::Rect paint_rect;

  // The expected clipped area to be filled with paint color. The clipped area
  // should be the intersection of `paint_rect` and `window_rect`.
  gfx::Rect expected_clipped_rect;
};

MATCHER(SearchStringResultEq, "") {
  PDFiumEngineClient::SearchStringResult l = std::get<0>(arg);
  PDFiumEngineClient::SearchStringResult r = std::get<1>(arg);
  return l.start_index == r.start_index && l.length == r.length;
}

MATCHER_P(IsExpectedImeKeyEvent, expected_text, "") {
  if (arg.GetType() != blink::WebInputEvent::Type::kChar) {
    return false;
  }

  const auto& event = static_cast<const blink::WebKeyboardEvent&>(arg);
  return event.GetModifiers() == blink::WebInputEvent::kNoModifiers &&
         event.windows_key_code == expected_text[0] &&
         event.native_key_code == expected_text[0] &&
         event.dom_code == static_cast<int>(ui::DomCode::NONE) &&
         event.dom_key == ui::DomKey::NONE && !event.is_system_key &&
         !event.is_browser_shortcut && event.text.data() == expected_text &&
         event.unmodified_text.data() == expected_text;
}

base::DictValue ParseMessage(std::string_view json) {
  return std::move(base::test::ParseJson(json).GetDict());
}

// Generates the expected `SkBitmap` with `paint_color` filled in the expected
// clipped area and `kDefaultColor` as the background color.
SkBitmap GenerateExpectedBitmapForPaint(const gfx::Rect& expected_clipped_rect,
                                        SkColor paint_color) {
  sk_sp<SkSurface> expected_surface =
      CreateSkiaSurfaceForTesting(kCanvasSize, kDefaultColor);
  expected_surface->getCanvas()->clipIRect(
      gfx::RectToSkIRect(expected_clipped_rect));
  expected_surface->getCanvas()->clear(paint_color);

  SkBitmap expected_bitmap;
  expected_surface->makeImageSnapshot()->asLegacyBitmap(&expected_bitmap);
  return expected_bitmap;
}

#if BUILDFLAG(ENABLE_SCREEN_AI_SERVICE)
base::DictValue GenerateShowSearchifyInProgressMessage(bool show) {
  return base::DictValue()
      .Set("type", "showSearchifyInProgress")
      .Set("show", show);
}
#endif

base::DictValue GenerateResetPrintPreviewModeMessage(const char* id,
                                                     int page_number,
                                                     bool grayscale,
                                                     int page_count) {
  return base::DictValue()
      .Set("type", "resetPrintPreviewMode")
      .Set("url", base::StringPrintf("chrome-untrusted://print/%s/%d/print.pdf",
                                     id, page_number))
      .Set("grayscale", grayscale)
      .Set("pageCount", page_count);
}

class MockHeaderVisitor : public blink::WebHTTPHeaderVisitor {
 public:
  MOCK_METHOD(void,
              VisitHeader,
              (const blink::WebString&, const blink::WebString&),
              (override));
};

class MockPdfAccessibilityDataHandler : public PdfAccessibilityDataHandler {
 public:
  // PdfAccessibilityDataHandler:
  MOCK_METHOD(void,
              SetAccessibilityViewportInfo,
              (AccessibilityViewportInfo),
              (override));
  MOCK_METHOD(void,
              SetAccessibilityDocInfo,
              (std::unique_ptr<AccessibilityDocInfo>),
              (override));
  MOCK_METHOD(void,
              SetAccessibilityPageInfo,
              (AccessibilityPageInfo,
               std::vector<AccessibilityTextRunInfo>,
               std::vector<AccessibilityCharInfo>,
               AccessibilityPageObjects),
              (override));
#if BUILDFLAG(ENABLE_SCREEN_AI_SERVICE)
  MOCK_METHOD(void, OnHasSearchifyText, (), (override));
#endif
};

class FakePdfViewWebPluginClient : public PdfViewWebPlugin::Client {
 public:
  FakePdfViewWebPluginClient() {
    ON_CALL(*this, CreateAssociatedURLLoader).WillByDefault([]() {
      auto associated_loader =
          std::make_unique<NiceMock<MockWebAssociatedURLLoader>>();
      ON_CALL(*associated_loader, LoadAsynchronously)
          .WillByDefault([](const blink::WebURLRequest& /*request*/,
                            blink::WebAssociatedURLLoaderClient* client) {
            // TODO(crbug.com/40224475): Must trigger callback to free
            // `UrlLoader`.
            client->DidReceiveResponse(blink::WebURLResponse());
            client->DidFinishLoading();
          });
      return associated_loader;
    });
    ON_CALL(*this, GetIsolate).WillByDefault(Return(GetBlinkIsolate()));
    ON_CALL(*this, DeviceScaleFactor).WillByDefault(Return(1.0f));
    ON_CALL(*this, GetEmbedderOriginString)
        .WillByDefault(
            Return("chrome-extension://mhjfbmdgcfjbbpaeojofohoefgiehjai/"));
    ON_CALL(*this, HasFrame).WillByDefault(Return(true));
  }

  // PdfViewWebPlugin::Client:
  MOCK_METHOD(std::unique_ptr<base::Value>,
              FromV8Value,
              (v8::Local<v8::Value>, v8::Local<v8::Context>),
              (override));

  MOCK_METHOD(base::WeakPtr<Client>, GetWeakPtr, (), (override));

  MOCK_METHOD(std::unique_ptr<PDFiumEngine>,
              CreateEngine,
              (PDFiumEngineClient*, PDFiumFormFiller::ScriptOption),
              (override));

  MOCK_METHOD(void,
              SetPluginContainer,
              (blink::WebPluginContainer*),
              (override));
  MOCK_METHOD(blink::WebPluginContainer*, PluginContainer, (), (override));

  MOCK_METHOD(v8::Isolate*, GetIsolate, (), (override));

  MOCK_METHOD(net::SiteForCookies, SiteForCookies, (), (const override));

  MOCK_METHOD(blink::WebURL,
              CompleteURL,
              (const blink::WebString&),
              (const override));

  MOCK_METHOD(void, PostMessage, (base::DictValue), (override));

  MOCK_METHOD(void, Invalidate, (), (override));

  MOCK_METHOD(void,
              RequestTouchEventType,
              (blink::WebPluginContainer::TouchEventRequestType),
              (override));

  MOCK_METHOD(void, ReportFindInPageMatchCount, (int, int, bool), (override));

  MOCK_METHOD(void, ReportFindInPageSelection, (int, int, bool), (override));

  MOCK_METHOD(void,
              ReportFindInPageTickmarks,
              (const std::vector<gfx::Rect>&),
              (override));

  MOCK_METHOD(float, DeviceScaleFactor, (), (override));

  MOCK_METHOD(gfx::PointF, GetScrollPosition, (), (override));

  MOCK_METHOD(void, UsePluginAsFindHandler, (), (override));

  MOCK_METHOD(void,
              SetReferrerForRequest,
              (blink::WebURLRequest&, const blink::WebURL&),
              (override));

  MOCK_METHOD(void, Alert, (const blink::WebString&), (override));

  MOCK_METHOD(bool, Confirm, (const blink::WebString&), (override));

  MOCK_METHOD(blink::WebString,
              Prompt,
              (const blink::WebString&, const blink::WebString&),
              (override));

  MOCK_METHOD(void,
              TextSelectionChanged,
              (const blink::WebString&, uint32_t, const gfx::Range&),
              (override));

  MOCK_METHOD(std::unique_ptr<blink::WebAssociatedURLLoader>,
              CreateAssociatedURLLoader,
              (const blink::WebAssociatedURLLoaderOptions&),
              (override));

  MOCK_METHOD(void, UpdateTextInputState, (), (override));

  MOCK_METHOD(void, UpdateSelectionBounds, (), (override));

  MOCK_METHOD(std::string, GetEmbedderOriginString, (), (override));

  MOCK_METHOD(bool, HasFrame, (), (const override));

  MOCK_METHOD(void, DidStartLoading, (), (override));
  MOCK_METHOD(void, DidStopLoading, (), (override));

  MOCK_METHOD(void, RecordComputedAction, (const std::string&), (override));

  MOCK_METHOD(std::unique_ptr<PdfAccessibilityDataHandler>,
              CreateAccessibilityDataHandler,
              (PdfAccessibilityActionHandler*, blink::WebPluginContainer*),
              (override));
#if BUILDFLAG(ENABLE_SCREEN_AI_SERVICE)
  MOCK_METHOD(void,
              GetOcrMaxImageDimension,
              (base::OnceCallback<void(uint32_t)> callback),
              (override));

  MOCK_METHOD(void,
              PerformOcr,
              (const SkBitmap& image,
               base::OnceCallback<void(screen_ai::mojom::VisualAnnotationPtr)>
                   callback),
              (override));

  MOCK_METHOD(void,
              SetOcrDisconnectedCallback,
              (base::RepeatingClosure callback),
              (override));
#endif  // BUILDFLAG(ENABLE_SCREEN_AI_SERVICE)
};

class FakePdfHost : public pdf::mojom::PdfHost {
 public:
  MOCK_METHOD(void,
              SetListener,
              (mojo::PendingRemote<pdf::mojom::PdfListener>),
              (override));
  MOCK_METHOD(void, OnDocumentLoadComplete, (), (override));
  MOCK_METHOD(void, UpdateContentRestrictions, (int32_t), (override));
  MOCK_METHOD(void, SavePdf, (), (override));
  MOCK_METHOD(void,
              SelectionChanged,
              (const gfx::PointF&, int32_t, const gfx::PointF&, int32_t),
              (override));
  MOCK_METHOD(void, SetPluginCanSave, (bool), (override));
#if BUILDFLAG(ENABLE_SCREEN_AI_SERVICE)
  MOCK_METHOD(void, OnSearchifyStarted, (), (override));
#endif
};

}  // namespace

class PdfViewWebPluginWithoutInitializeTest : public testing::Test {
 protected:
  // Custom deleter for `plugin_`. PdfViewWebPlugin must be destroyed by
  // PdfViewWebPlugin::Destroy() instead of its destructor.
  struct PluginDeleter {
    void operator()(PdfViewWebPlugin* ptr) { ptr->Destroy(); }
  };

  static void AddToPluginParams(std::string_view name,
                                std::string_view value,
                                blink::WebPluginParams& params) {
    params.attribute_names.push_back(blink::WebString::FromUtf8(name));
    params.attribute_values.push_back(blink::WebString::FromUtf8(value));
  }

  void SetUpPlugin(std::string_view document_url,
                   blink::WebPluginParams params) {
    auto client = std::make_unique<NiceMock<FakePdfViewWebPluginClient>>();
    client_ptr_ = client.get();

    ON_CALL(*client_ptr_, CompleteURL)
        .WillByDefault([parsed_document_url = GURL(document_url)](
                           const blink::WebString& partial_url) {
          return parsed_document_url.Resolve(partial_url.Utf8());
        });
    ON_CALL(*client_ptr_, CreateEngine)
        .WillByDefault([this](
                           PDFiumEngineClient* client,
                           PDFiumFormFiller::ScriptOption /*script_option*/) {
          auto engine = std::make_unique<NiceMock<TestPDFiumEngine>>(client);
          engine_ptr_ = engine.get();
          return engine;
        });
    ON_CALL(*client_ptr_, CreateAccessibilityDataHandler)
        .WillByDefault([this]() {
          auto handler =
              std::make_unique<NiceMock<MockPdfAccessibilityDataHandler>>();
          accessibility_data_handler_ptr_ = handler.get();
          return handler;
        });
    SetUpClient();

    plugin_ =
        std::unique_ptr<PdfViewWebPlugin, PluginDeleter>(new PdfViewWebPlugin(
            std::move(client),
            mojo::AssociatedRemote<pdf::mojom::PdfHost>(
                pdf_receiver_.BindNewEndpointAndPassDedicatedRemote()),
            std::move(params)));
  }

  void SetUpPluginWithUrl(const std::string& url) {
    blink::WebPluginParams params;
    AddToPluginParams("src", url, params);
    SetUpPluginParams(params);

    SetUpPlugin(url, std::move(params));
  }

  // Allows derived classes to customize plugin parameters within
  // `SetUpPluginWithUrl()`.
  virtual void SetUpPluginParams(blink::WebPluginParams& params) {}

  // Allows derived classes to customize `client_ptr_` within `SetUpPlugin()`.
  virtual void SetUpClient() {}

  void ExpectUpdateTextInputState(
      blink::WebTextInputType expected_text_input_type) {
    EXPECT_CALL(*client_ptr_, UpdateTextInputState)
        .WillOnce([this, expected_text_input_type]() {
          EXPECT_EQ(expected_text_input_type,
                    plugin_->GetPluginTextInputType());
        });
  }

  void OnMessageWithEngineUpdate(const base::DictValue& message) {
    // New engine will be created making this unowned reference stale.
    engine_ptr_ = nullptr;
    plugin_->OnMessage(message);
  }

  NiceMock<FakePdfHost> pdf_host_;
  mojo::AssociatedReceiver<pdf::mojom::PdfHost> pdf_receiver_{&pdf_host_};

  // Must outlive raw_ptrs below.
  std::unique_ptr<PdfViewWebPlugin, PluginDeleter> plugin_;

  raw_ptr<FakePdfViewWebPluginClient> client_ptr_;
  raw_ptr<TestPDFiumEngine> engine_ptr_;
  raw_ptr<MockPdfAccessibilityDataHandler> accessibility_data_handler_ptr_;
};

class PdfViewWebPluginTest : public PdfViewWebPluginWithoutInitializeTest {
 protected:
  static constexpr char kPdfUrl[] = "http://localhost/example.pdf";

  void SetUp() override {
    SetUpPluginWithUrl(kPdfUrl);

    EXPECT_TRUE(plugin_->InitializeForTesting());
  }

  void SetDocumentDimensions(const gfx::Size& dimensions) {
    EXPECT_CALL(*engine_ptr_, ApplyDocumentLayout)
        .WillRepeatedly(Return(dimensions));
    SendViewportMessage(/*zoom=*/1.0);
  }

  void SendViewportMessage(double zoom) {
    base::DictValue message = ParseMessage(R"({
      "type": "viewport",
      "userInitiated": false,
      "zoom": 1,
      "layoutOptions": {
        "direction": 2,
        "defaultPageOrientation": 0,
        "twoUpViewEnabled": false,
      },
      "xOffset": 0,
      "yOffset": 0,
      "pinchPhase": 0,
    })");
    message.Set("zoom", zoom);
    plugin_->OnMessage(message);
  }

  void UpdatePluginGeometry(float device_scale, const gfx::Rect& window_rect) {
    UpdatePluginGeometryWithoutWaiting(device_scale, window_rect);

    // Waits for main thread callback scheduled by `PaintManager`.
    base::RunLoop run_loop;
    base::SingleThreadTaskRunner::GetCurrentDefault()->PostTask(
        FROM_HERE, run_loop.QuitClosure());
    run_loop.Run();
  }

  void UpdatePluginGeometryWithoutWaiting(float device_scale,
                                          const gfx::Rect& window_rect) {
    // The plugin container's device scale must be set before calling
    // UpdateGeometry().
    EXPECT_CALL(*client_ptr_, DeviceScaleFactor)
        .WillRepeatedly(Return(device_scale));
    plugin_->UpdateGeometry(window_rect, window_rect, window_rect,
                            /*is_visible=*/true);
  }

  void TestUpdateGeometrySetsPluginRect(float device_scale,
                                        const gfx::Rect& window_rect,
                                        float expected_device_scale,
                                        const gfx::Rect& expected_plugin_rect) {
    UpdatePluginGeometryWithoutWaiting(device_scale, window_rect);
    EXPECT_EQ(expected_device_scale, plugin_->GetDeviceScaleForTesting())
        << "Device scale comparison failure at device scale of "
        << device_scale;
    EXPECT_EQ(expected_plugin_rect, plugin_->GetPluginRectForTesting())
        << "Plugin rect comparison failure at device scale of " << device_scale
        << ", window rect of " << window_rect.ToString();
  }

  void TestPaintEmptySnapshots(float device_scale,
                               const gfx::Rect& window_rect,
                               const gfx::Rect& paint_rect,
                               const gfx::Rect& expected_clipped_rect) {
    UpdatePluginGeometryWithoutWaiting(device_scale, window_rect);
    canvas_.DrawColor(kDefaultColor);

    plugin_->Paint(canvas_.sk_canvas(), paint_rect);

    // Expect the clipped area on canvas to be filled with plugin's background
    // color.
    SkBitmap expected_bitmap = GenerateExpectedBitmapForPaint(
        expected_clipped_rect, plugin_->GetBackgroundColor());
    EXPECT_TRUE(cc::MatchesBitmap(canvas_.GetBitmap(), expected_bitmap,
                                  cc::ExactPixelComparator()))
        << "Failure at device scale of " << device_scale << ", window rect of "
        << window_rect.ToString();
  }

  void TestPaintSnapshots(float device_scale,
                          const gfx::Rect& window_rect,
                          const gfx::Rect& paint_rect,
                          const gfx::Rect& expected_clipped_rect) {
    UpdatePluginGeometry(device_scale, window_rect);
    canvas_.DrawColor(kDefaultColor);

    // Paint the plugin with `kPaintColor`.
    plugin_->UpdateSnapshot(CreateSkiaImageForTesting(
        plugin_->GetPluginRectForTesting().size(), kPaintColor));
    plugin_->Paint(canvas_.sk_canvas(), paint_rect);

    // Expect the clipped area on canvas to be filled with `kPaintColor`.
    SkBitmap expected_bitmap =
        GenerateExpectedBitmapForPaint(expected_clipped_rect, kPaintColor);
    EXPECT_TRUE(cc::MatchesBitmap(canvas_.GetBitmap(), expected_bitmap,
                                  cc::ExactPixelComparator()))
        << "Failure at device scale of " << device_scale << ", window rect of "
        << window_rect.ToString();
  }

  ui::Cursor TestSendInputEvent(const blink::WebInputEvent& event,
                                blink::WebInputEventResult expected_result) {
    ui::Cursor cursor;
    EXPECT_EQ(
        expected_result,
        plugin_->HandleInputEvent(
            blink::WebCoalescedInputEvent(event, ui::LatencyInfo()), &cursor));
    return cursor;
  }

  // Provides the cc::PaintCanvas for painting.
  gfx::Canvas canvas_{kCanvasSize, /*image_scale=*/1.0f, /*is_opaque=*/true};
};

class PdfViewWebPluginFullFrameTest : public PdfViewWebPluginTest {
 protected:
  void SetUpPluginParams(blink::WebPluginParams& params) override {
    AddToPluginParams("full-frame", "full-frame", params);
  }
};

TEST_F(PdfViewWebPluginWithoutInitializeTest, Initialize) {
  SetUpPluginWithUrl("http://localhost/example.pdf");

  EXPECT_CALL(*client_ptr_, CreateAssociatedURLLoader)
      .WillOnce([](const blink::WebAssociatedURLLoaderOptions& options) {
        EXPECT_TRUE(options.grant_universal_access);

        auto associated_loader =
            std::make_unique<NiceMock<MockWebAssociatedURLLoader>>();
        EXPECT_CALL(*associated_loader, LoadAsynchronously)
            .WillOnce([](const blink::WebURLRequest& request,
                         blink::WebAssociatedURLLoaderClient* client) {
              EXPECT_EQ("http://localhost/example.pdf",
                        request.Url().GetString().Utf8());
              EXPECT_EQ("GET", request.HttpMethod().Utf8());
              EXPECT_TRUE(request.HttpBody().IsNull());

              NiceMock<MockHeaderVisitor> header_visitor;
              EXPECT_CALL(header_visitor, VisitHeader).Times(0);
              request.VisitHttpHeaderFields(&header_visitor);

              EXPECT_FALSE(client->WillFollowRedirect(blink::WebURL(),
                                                      blink::WebURLResponse()));
              client->DidReceiveResponse(blink::WebURLResponse());
              client->DidFinishLoading();
            });
        return associated_loader;
      });
  EXPECT_CALL(*client_ptr_, SetReferrerForRequest).Times(0);

  EXPECT_TRUE(plugin_->InitializeForTesting());
}

TEST_F(PdfViewWebPluginWithoutInitializeTest, InitializeWithEmptyUrl) {
  SetUpPluginWithUrl("");

  EXPECT_CALL(*client_ptr_, CreateAssociatedURLLoader).Times(0);

  EXPECT_FALSE(plugin_->InitializeForTesting());
}

TEST_F(PdfViewWebPluginWithoutInitializeTest, InitializeForPrintPreview) {
  SetUpPluginWithUrl("about:blank");

  EXPECT_CALL(*client_ptr_, GetEmbedderOriginString)
      .WillRepeatedly(Return("chrome://print/"));
  EXPECT_CALL(*client_ptr_, CreateAssociatedURLLoader).Times(0);

  EXPECT_TRUE(plugin_->InitializeForTesting());
}

TEST_F(PdfViewWebPluginWithoutInitializeTest,
       RecordSchemeIsFileMetricForHttps) {
  base::HistogramTester histograms;

  SetUpPluginWithUrl("https://www.example.com/path/to/the.pdf");
  EXPECT_TRUE(plugin_->InitializeForTesting());

  histograms.ExpectUniqueSample("PDF.SchemeIsFile", 0, 1);
}

TEST_F(PdfViewWebPluginWithoutInitializeTest, RecordSchemeIsFileMetricForFile) {
  base::HistogramTester histograms;

  SetUpPluginWithUrl("file:///path/to/the.pdf");
  EXPECT_TRUE(plugin_->InitializeForTesting());

  histograms.ExpectUniqueSample("PDF.SchemeIsFile", 1, 1);
}

TEST_F(PdfViewWebPluginTest, CreateUrlLoader) {
  EXPECT_CALL(*client_ptr_, DidStartLoading).Times(0);
  EXPECT_CALL(pdf_host_, UpdateContentRestrictions).Times(0);
  plugin_->CreateUrlLoader();

  EXPECT_EQ(PdfViewWebPlugin::DocumentLoadState::kLoading,
            plugin_->document_load_state_for_testing());
  pdf_receiver_.FlushForTesting();
}

TEST_F(PdfViewWebPluginFullFrameTest, CreateUrlLoader) {
  EXPECT_CALL(*client_ptr_, DidStartLoading);
  EXPECT_CALL(pdf_host_, UpdateContentRestrictions(kContentRestrictionSave |
                                                   kContentRestrictionPrint));
  plugin_->CreateUrlLoader();

  EXPECT_EQ(PdfViewWebPlugin::DocumentLoadState::kLoading,
            plugin_->document_load_state_for_testing());
  pdf_receiver_.FlushForTesting();
}

TEST_F(PdfViewWebPluginFullFrameTest, CreateUrlLoaderMultipleTimes) {
  plugin_->CreateUrlLoader();

  EXPECT_CALL(*client_ptr_, DidStartLoading).Times(0);
  plugin_->CreateUrlLoader();
}

TEST_F(PdfViewWebPluginFullFrameTest, CreateUrlLoaderAfterDocumentLoadFailed) {
  plugin_->CreateUrlLoader();
  plugin_->DocumentLoadFailed();

  EXPECT_CALL(*client_ptr_, DidStartLoading);
  plugin_->CreateUrlLoader();
}

TEST_F(PdfViewWebPluginTest, DocumentLoadComplete) {
  plugin_->CreateUrlLoader();

  EXPECT_CALL(*client_ptr_, RecordComputedAction("PDF.LoadSuccess"));
  EXPECT_CALL(*client_ptr_, PostMessage);
  EXPECT_CALL(*client_ptr_, PostMessage(base::test::IsJson(R"({
    "type": "formFocusChange",
    "focused": "none",
  })")));
  ExpectUpdateTextInputState(blink::WebTextInputType::kWebTextInputTypeNone);
  EXPECT_CALL(*client_ptr_, PostMessage(base::test::IsJson(R"({
    "type": "printPreviewLoaded",
  })")))
      .Times(0);
  EXPECT_CALL(*accessibility_data_handler_ptr_, SetAccessibilityDocInfo)
      .Times(0);
  EXPECT_CALL(*client_ptr_, DidStopLoading).Times(0);
  EXPECT_CALL(pdf_host_, UpdateContentRestrictions).Times(0);
  plugin_->DocumentLoadComplete();

  EXPECT_EQ(PdfViewWebPlugin::DocumentLoadState::kComplete,
            plugin_->document_load_state_for_testing());
  pdf_receiver_.FlushForTesting();
}

TEST_F(PdfViewWebPluginFullFrameTest, DocumentLoadComplete) {
  // Must flush IPCs after `CreateUrlLoader()` in full-frame mode, otherwise
  // there's an unexpected `UpdateContentRestrictions()` call (see the
  // `PdfViewWebPluginFullFrameTest.CreateUrlLoader` test).
  plugin_->CreateUrlLoader();
  pdf_receiver_.FlushForTesting();

  EXPECT_CALL(*client_ptr_, RecordComputedAction("PDF.LoadSuccess"));
  EXPECT_CALL(*client_ptr_, PostMessage);
  EXPECT_CALL(*client_ptr_, PostMessage(base::test::IsJson(R"({
    "type": "formFocusChange",
    "focused": "none",
  })")));
  ExpectUpdateTextInputState(blink::WebTextInputType::kWebTextInputTypeNone);
  EXPECT_CALL(*client_ptr_, PostMessage(base::test::IsJson(R"({
    "type": "printPreviewLoaded",
  })")))
      .Times(0);
  EXPECT_CALL(*accessibility_data_handler_ptr_, SetAccessibilityDocInfo)
      .Times(0);
  EXPECT_CALL(*client_ptr_, DidStopLoading);
  EXPECT_CALL(pdf_host_, UpdateContentRestrictions(kContentRestrictionPrint |
                                                   kContentRestrictionPaste |
                                                   kContentRestrictionCut |
                                                   kContentRestrictionCopy));
  EXPECT_CALL(pdf_host_, OnDocumentLoadComplete);
  plugin_->DocumentLoadComplete();

  EXPECT_EQ(PdfViewWebPlugin::DocumentLoadState::kComplete,
            plugin_->document_load_state_for_testing());
  pdf_receiver_.FlushForTesting();
}

TEST_F(PdfViewWebPluginTest, DocumentLoadFailed) {
  plugin_->CreateUrlLoader();

  EXPECT_CALL(*client_ptr_, RecordComputedAction("PDF.LoadFailure"));
  EXPECT_CALL(*client_ptr_, DidStopLoading).Times(0);
  EXPECT_CALL(pdf_host_, OnDocumentLoadComplete).Times(0);
  plugin_->DocumentLoadFailed();

  EXPECT_EQ(PdfViewWebPlugin::DocumentLoadState::kFailed,
            plugin_->document_load_state_for_testing());
}

TEST_F(PdfViewWebPluginFullFrameTest, DocumentLoadFailed) {
  plugin_->CreateUrlLoader();

  EXPECT_CALL(*client_ptr_, RecordComputedAction("PDF.LoadFailure"));
  EXPECT_CALL(*client_ptr_, DidStopLoading);
  plugin_->DocumentLoadFailed();

  EXPECT_EQ(PdfViewWebPlugin::DocumentLoadState::kFailed,
            plugin_->document_load_state_for_testing());
}

TEST_F(PdfViewWebPluginTest, DocumentHasUnsupportedFeature) {
  EXPECT_CALL(*client_ptr_, RecordComputedAction).Times(AnyNumber());
  EXPECT_CALL(*client_ptr_, RecordComputedAction("PDF_Unsupported_feature1"));
  EXPECT_CALL(*client_ptr_, RecordComputedAction("PDF_Unsupported_feature2"));

  plugin_->DocumentHasUnsupportedFeature("feature1");
  plugin_->DocumentHasUnsupportedFeature("feature2");

  pdf_receiver_.FlushForTesting();
}

TEST_F(PdfViewWebPluginTest, DocumentHasUnsupportedFeatureWithRepeatedFeature) {
  // Metrics should only be recorded once per feature.
  EXPECT_CALL(*client_ptr_, RecordComputedAction).Times(AnyNumber());
  EXPECT_CALL(*client_ptr_, RecordComputedAction("PDF_Unsupported_feature"));

  plugin_->DocumentHasUnsupportedFeature("feature");
  plugin_->DocumentHasUnsupportedFeature("feature");

  pdf_receiver_.FlushForTesting();
}

TEST_F(PdfViewWebPluginFullFrameTest, DocumentHasUnsupportedFeature) {
  EXPECT_CALL(*client_ptr_, RecordComputedAction).Times(AnyNumber());
  EXPECT_CALL(*client_ptr_, RecordComputedAction("PDF_Unsupported_feature1"));
  EXPECT_CALL(*client_ptr_, RecordComputedAction("PDF_Unsupported_feature2"));

  plugin_->DocumentHasUnsupportedFeature("feature1");
  plugin_->DocumentHasUnsupportedFeature("feature2");

  pdf_receiver_.FlushForTesting();
}

TEST_F(PdfViewWebPluginFullFrameTest,
       DocumentHasUnsupportedFeatureWithRepeatedFeature) {
  // Metrics should only be recorded once per feature.
  EXPECT_CALL(*client_ptr_, RecordComputedAction).Times(AnyNumber());
  EXPECT_CALL(*client_ptr_, RecordComputedAction("PDF_Unsupported_feature"));

  plugin_->DocumentHasUnsupportedFeature("feature");
  plugin_->DocumentHasUnsupportedFeature("feature");

  pdf_receiver_.FlushForTesting();
}

TEST_F(PdfViewWebPluginTest, DocumentLoadProgress) {
  EXPECT_CALL(*client_ptr_, PostMessage(base::test::IsJson(R"({
    "type": "loadProgress",
    "progress": 5.0,
  })")));
  plugin_->DocumentLoadProgress(10, 200);
}

TEST_F(PdfViewWebPluginTest, DocumentLoadProgressIgnoreSmall) {
  plugin_->DocumentLoadProgress(2, 100);

  EXPECT_CALL(*client_ptr_, PostMessage).Times(0);
  plugin_->DocumentLoadProgress(3, 100);
}

TEST_F(PdfViewWebPluginTest, DocumentLoadProgressMultipleSmall) {
  plugin_->DocumentLoadProgress(2, 100);

  EXPECT_CALL(*client_ptr_, PostMessage(base::test::IsJson(R"({
    "type": "loadProgress",
    "progress": 4.0,
  })")));
  plugin_->DocumentLoadProgress(3, 100);
  plugin_->DocumentLoadProgress(4, 100);
}

TEST_F(PdfViewWebPluginTest, EnableAccessibilityBeforeDocumentLoadComplete) {
  EXPECT_CALL(*accessibility_data_handler_ptr_, SetAccessibilityDocInfo)
      .Times(0);
  plugin_->EnableAccessibility();

  EXPECT_CALL(*accessibility_data_handler_ptr_, SetAccessibilityDocInfo);
  plugin_->CreateUrlLoader();
  plugin_->DocumentLoadComplete();
}

TEST_F(PdfViewWebPluginTest,
       EnableAccessibilityBeforeDocumentLoadCompleteRepeated) {
  EXPECT_CALL(*accessibility_data_handler_ptr_, SetAccessibilityDocInfo)
      .Times(0);
  plugin_->EnableAccessibility();
  plugin_->EnableAccessibility();

  EXPECT_CALL(*accessibility_data_handler_ptr_, SetAccessibilityDocInfo);
  plugin_->CreateUrlLoader();
  plugin_->DocumentLoadComplete();
}

TEST_F(PdfViewWebPluginTest, EnableAccessibilityAfterDocumentLoadComplete) {
  EXPECT_CALL(*accessibility_data_handler_ptr_, SetAccessibilityDocInfo)
      .Times(0);
  plugin_->CreateUrlLoader();
  plugin_->DocumentLoadComplete();

  EXPECT_CALL(*accessibility_data_handler_ptr_, SetAccessibilityDocInfo);
  plugin_->EnableAccessibility();
}

TEST_F(PdfViewWebPluginTest,
       EnableAccessibilityAfterDocumentLoadCompleteRepeated) {
  plugin_->CreateUrlLoader();
  plugin_->DocumentLoadComplete();
  plugin_->EnableAccessibility();

  EXPECT_CALL(*accessibility_data_handler_ptr_, SetAccessibilityDocInfo)
      .Times(0);
  plugin_->EnableAccessibility();
}

TEST_F(PdfViewWebPluginTest,
       LoadOrReloadAccessibilityBeforeDocumentLoadComplete) {
  EXPECT_CALL(*accessibility_data_handler_ptr_, SetAccessibilityDocInfo)
      .Times(0);
  plugin_->LoadOrReloadAccessibility();

  EXPECT_CALL(*accessibility_data_handler_ptr_, SetAccessibilityDocInfo);
  plugin_->CreateUrlLoader();
  plugin_->DocumentLoadComplete();
}

TEST_F(PdfViewWebPluginTest,
       LoadOrReloadAccessibilityBeforeDocumentLoadCompleteRepeated) {
  EXPECT_CALL(*accessibility_data_handler_ptr_, SetAccessibilityDocInfo)
      .Times(0);
  plugin_->LoadOrReloadAccessibility();
  plugin_->LoadOrReloadAccessibility();

  EXPECT_CALL(*accessibility_data_handler_ptr_, SetAccessibilityDocInfo);
  plugin_->CreateUrlLoader();
  plugin_->DocumentLoadComplete();
}

TEST_F(PdfViewWebPluginTest,
       LoadOrReloadAccessibilityAfterDocumentLoadComplete) {
  EXPECT_CALL(*accessibility_data_handler_ptr_, SetAccessibilityDocInfo)
      .Times(0);
  plugin_->CreateUrlLoader();
  plugin_->DocumentLoadComplete();

  EXPECT_CALL(*accessibility_data_handler_ptr_, SetAccessibilityDocInfo);
  plugin_->LoadOrReloadAccessibility();
}

TEST_F(PdfViewWebPluginTest,
       LoadOrReloadAccessibilityAfterDocumentLoadCompleteRepeated) {
  plugin_->CreateUrlLoader();
  plugin_->DocumentLoadComplete();
  plugin_->LoadOrReloadAccessibility();

  EXPECT_CALL(*accessibility_data_handler_ptr_, SetAccessibilityDocInfo);
  plugin_->LoadOrReloadAccessibility();
}

TEST_F(PdfViewWebPluginTest,
       LoadOrReloadAccessibilityResetsAccessibilityPageIndex) {
  plugin_->CreateUrlLoader();
  plugin_->DocumentLoadComplete();
  plugin_->LoadOrReloadAccessibility();
  EXPECT_EQ(plugin_->next_accessibility_page_index_for_testing(), 0);
  plugin_->set_next_accessibility_page_index_for_testing(5);

  EXPECT_CALL(*accessibility_data_handler_ptr_, SetAccessibilityDocInfo);
  plugin_->LoadOrReloadAccessibility();
  EXPECT_EQ(plugin_->next_accessibility_page_index_for_testing(), 0);
}

TEST_F(PdfViewWebPluginTest, GetContentRestrictionsWithNoPermissions) {
  EXPECT_EQ(kContentRestrictionCopy | kContentRestrictionCut |
                kContentRestrictionPaste | kContentRestrictionPrint,
            plugin_->GetContentRestrictionsForTesting());
  EXPECT_FALSE(plugin_->CanCopy());
}

TEST_F(PdfViewWebPluginTest, GetContentRestrictionsWithCopyAllowed) {
  EXPECT_CALL(*engine_ptr_, HasPermission).WillRepeatedly(Return(false));
  EXPECT_CALL(*engine_ptr_, HasPermission(DocumentPermission::kCopy))
      .WillRepeatedly(Return(true));

  EXPECT_EQ(kContentRestrictionCut | kContentRestrictionPaste |
                kContentRestrictionPrint,
            plugin_->GetContentRestrictionsForTesting());
  EXPECT_TRUE(plugin_->CanCopy());
}

TEST_F(PdfViewWebPluginTest, GetContentRestrictionsWithPrintLowQualityAllowed) {
  EXPECT_CALL(*engine_ptr_, HasPermission).WillRepeatedly(Return(false));
  EXPECT_CALL(*engine_ptr_, HasPermission(DocumentPermission::kPrintLowQuality))
      .WillRepeatedly(Return(true));

  EXPECT_EQ(kContentRestrictionCopy | kContentRestrictionCut |
                kContentRestrictionPaste,
            plugin_->GetContentRestrictionsForTesting());
}

TEST_F(PdfViewWebPluginTest,
       GetContentRestrictionsWithCopyAndPrintLowQualityAllowed) {
  EXPECT_CALL(*engine_ptr_, HasPermission).WillRepeatedly(Return(false));
  EXPECT_CALL(*engine_ptr_, HasPermission(DocumentPermission::kCopy))
      .WillRepeatedly(Return(true));
  EXPECT_CALL(*engine_ptr_, HasPermission(DocumentPermission::kPrintLowQuality))
      .WillRepeatedly(Return(true));

  EXPECT_EQ(kContentRestrictionCut | kContentRestrictionPaste,
            plugin_->GetContentRestrictionsForTesting());
}

TEST_F(PdfViewWebPluginTest, GetContentRestrictionsWithPrintAllowed) {
  EXPECT_CALL(*engine_ptr_, HasPermission).WillRepeatedly(Return(false));
  EXPECT_CALL(*engine_ptr_,
              HasPermission(DocumentPermission::kPrintHighQuality))
      .WillRepeatedly(Return(true));
  EXPECT_CALL(*engine_ptr_, HasPermission(DocumentPermission::kPrintLowQuality))
      .WillRepeatedly(Return(true));

  EXPECT_EQ(kContentRestrictionCopy | kContentRestrictionCut |
                kContentRestrictionPaste,
            plugin_->GetContentRestrictionsForTesting());
}

TEST_F(PdfViewWebPluginTest, GetContentRestrictionsWithCopyAndPrintAllowed) {
  EXPECT_CALL(*engine_ptr_, HasPermission).WillRepeatedly(Return(false));
  EXPECT_CALL(*engine_ptr_, HasPermission(DocumentPermission::kCopy))
      .WillRepeatedly(Return(true));
  EXPECT_CALL(*engine_ptr_,
              HasPermission(DocumentPermission::kPrintHighQuality))
      .WillRepeatedly(Return(true));
  EXPECT_CALL(*engine_ptr_, HasPermission(DocumentPermission::kPrintLowQuality))
      .WillRepeatedly(Return(true));

  EXPECT_EQ(kContentRestrictionCut | kContentRestrictionPaste,
            plugin_->GetContentRestrictionsForTesting());
}

TEST_F(PdfViewWebPluginTest, GetAccessibilityDocInfoWithNoPermissions) {
  std::unique_ptr<AccessibilityDocInfo> doc_info =
      plugin_->GetAccessibilityDocInfoForTesting();

  EXPECT_EQ(TestPDFiumEngine::kPageNumber, doc_info->page_count);
  EXPECT_FALSE(doc_info->is_tagged);
  EXPECT_FALSE(doc_info->text_accessible);
  EXPECT_FALSE(doc_info->text_copyable);
}

TEST_F(PdfViewWebPluginTest, GetAccessibilityDocInfoWithPDFDocTagged) {
  base::test::ScopedFeatureList scoped_feature_list(features::kPdfTags);
  EXPECT_CALL(*engine_ptr_, IsPDFDocTagged).WillRepeatedly(Return(true));

  std::unique_ptr<AccessibilityDocInfo> doc_info =
      plugin_->GetAccessibilityDocInfoForTesting();

  EXPECT_EQ(TestPDFiumEngine::kPageNumber, doc_info->page_count);
  EXPECT_TRUE(doc_info->is_tagged);
  EXPECT_FALSE(doc_info->text_accessible);
  EXPECT_FALSE(doc_info->text_copyable);
}

TEST_F(PdfViewWebPluginTest, HasMeaningfulText) {
  EXPECT_CALL(*engine_ptr_, HasMeaningfulText).WillOnce(Return(true));

  base::test::TestFuture<bool> future;
  plugin_->HasMeaningfulText(future.GetCallback());
  EXPECT_TRUE(future.Get());
}

TEST_F(PdfViewWebPluginTest, HasJavaScript) {
  EXPECT_CALL(*engine_ptr_, HasJavaScript).WillOnce(Return(true));

  base::test::TestFuture<bool> future;
  plugin_->HasJavaScript(future.GetCallback());
  EXPECT_TRUE(future.Get());
}

TEST_F(PdfViewWebPluginTest, IsPasswordProtected) {
  EXPECT_CALL(*engine_ptr_, IsPasswordProtected).WillOnce(Return(true));

  base::test::TestFuture<bool> future;
  plugin_->IsPasswordProtected(future.GetCallback());
  EXPECT_TRUE(future.Get());
}

TEST_F(PdfViewWebPluginTest, GetAccessibilityDocInfoWithCopyAccessibleAllowed) {
  EXPECT_CALL(*engine_ptr_, HasPermission).WillRepeatedly(Return(false));
  EXPECT_CALL(*engine_ptr_, HasPermission(DocumentPermission::kCopyAccessible))
      .WillRepeatedly(Return(true));

  std::unique_ptr<AccessibilityDocInfo> doc_info =
      plugin_->GetAccessibilityDocInfoForTesting();

  EXPECT_EQ(TestPDFiumEngine::kPageNumber, doc_info->page_count);
  EXPECT_FALSE(doc_info->is_tagged);
  EXPECT_TRUE(doc_info->text_accessible);
  EXPECT_FALSE(doc_info->text_copyable);
}

TEST_F(PdfViewWebPluginTest, GetAccessibilityDocInfoWithCopyAllowed) {
  EXPECT_CALL(*engine_ptr_, HasPermission).WillRepeatedly(Return(false));
  EXPECT_CALL(*engine_ptr_, HasPermission(DocumentPermission::kCopy))
      .WillRepeatedly(Return(true));

  std::unique_ptr<AccessibilityDocInfo> doc_info =
      plugin_->GetAccessibilityDocInfoForTesting();

  EXPECT_EQ(TestPDFiumEngine::kPageNumber, doc_info->page_count);
  EXPECT_FALSE(doc_info->is_tagged);
  EXPECT_FALSE(doc_info->text_accessible);
  EXPECT_TRUE(doc_info->text_copyable);
}

TEST_F(PdfViewWebPluginTest,
       GetAccessibilityDocInfoWithCopyAndCopyAccessibleAllowed) {
  EXPECT_CALL(*engine_ptr_, HasPermission).WillRepeatedly(Return(false));
  EXPECT_CALL(*engine_ptr_, HasPermission(DocumentPermission::kCopy))
      .WillRepeatedly(Return(true));
  EXPECT_CALL(*engine_ptr_, HasPermission(DocumentPermission::kCopyAccessible))
      .WillRepeatedly(Return(true));

  std::unique_ptr<AccessibilityDocInfo> doc_info =
      plugin_->GetAccessibilityDocInfoForTesting();

  EXPECT_EQ(TestPDFiumEngine::kPageNumber, doc_info->page_count);
  EXPECT_FALSE(doc_info->is_tagged);
  EXPECT_TRUE(doc_info->text_accessible);
  EXPECT_TRUE(doc_info->text_copyable);
}

TEST_F(
    PdfViewWebPluginTest,
    GetAccessibilityDocInfoWithPDFDocTaggedAndPDFCopyAndCopyAccessibleAllowed) {
  base::test::ScopedFeatureList scoped_feature_list(features::kPdfTags);
  EXPECT_CALL(*engine_ptr_, IsPDFDocTagged).WillRepeatedly(Return(true));
  EXPECT_CALL(*engine_ptr_, HasPermission).WillRepeatedly(Return(false));
  EXPECT_CALL(*engine_ptr_, HasPermission(DocumentPermission::kCopy))
      .WillRepeatedly(Return(true));
  EXPECT_CALL(*engine_ptr_, HasPermission(DocumentPermission::kCopyAccessible))
      .WillRepeatedly(Return(true));

  std::unique_ptr<AccessibilityDocInfo> doc_info =
      plugin_->GetAccessibilityDocInfoForTesting();

  EXPECT_EQ(TestPDFiumEngine::kPageNumber, doc_info->page_count);
  EXPECT_TRUE(doc_info->is_tagged);
  EXPECT_TRUE(doc_info->text_accessible);
  EXPECT_TRUE(doc_info->text_copyable);
}

TEST_F(PdfViewWebPluginTest, UpdateGeometrySetsPluginRect) {
  EXPECT_CALL(*engine_ptr_, ZoomUpdated(2.0f));
  TestUpdateGeometrySetsPluginRect(
      /*device_scale=*/2.0f, /*window_rect=*/gfx::Rect(4, 4, 12, 12),
      /*expected_device_scale=*/2.0f,
      /*expected_plugin_rect=*/gfx::Rect(4, 4, 12, 12));
}

TEST_F(PdfViewWebPluginTest,
       UpdateGeometrySetsPluginRectOnVariousDeviceScales) {
  struct UpdateGeometryParams {
    // The plugin container's device scale.
    float device_scale;

    //  The window rect in CSS pixels.
    gfx::Rect window_rect;

    // The expected plugin device scale.
    float expected_device_scale;

    // The expected plugin rect in device pixels.
    gfx::Rect expected_plugin_rect;
  };

  static constexpr UpdateGeometryParams kUpdateGeometryParams[] = {
      {1.0f, gfx::Rect(3, 4, 5, 6), 1.0f, gfx::Rect(3, 4, 5, 6)},
      {2.0f, gfx::Rect(3, 4, 5, 6), 2.0f, gfx::Rect(3, 4, 5, 6)},
  };

  for (const auto& params : kUpdateGeometryParams) {
    TestUpdateGeometrySetsPluginRect(params.device_scale, params.window_rect,
                                     params.expected_device_scale,
                                     params.expected_plugin_rect);
  }
}

TEST_F(PdfViewWebPluginTest, UpdateGeometrySetsPluginRectWithEmptyWindow) {
  EXPECT_CALL(*engine_ptr_, ZoomUpdated).Times(0);
  TestUpdateGeometrySetsPluginRect(
      /*device_scale=*/2.0f, /*window_rect=*/gfx::Rect(2, 2, 0, 0),
      /*expected_device_scale=*/1.0f, /*expected_plugin_rect=*/gfx::Rect());
}

TEST_F(PdfViewWebPluginTest, UpdateGeometryScroll) {
  SetDocumentDimensions({100, 200});

  EXPECT_CALL(*client_ptr_, GetScrollPosition)
      .WillRepeatedly(Return(gfx::PointF(4.0f, 6.0f)));
  EXPECT_CALL(*engine_ptr_, ScrolledToXPosition(4));
  EXPECT_CALL(*engine_ptr_, ScrolledToYPosition(6));
  UpdatePluginGeometryWithoutWaiting(1.0f, gfx::Rect(3, 4, 5, 6));
}

TEST_F(PdfViewWebPluginTest, UpdateGeometryScrollStopped) {
  SetDocumentDimensions({100, 200});

  plugin_->OnMessage(ParseMessage(R"({
    "type": "stopScrolling",
  })"));

  EXPECT_CALL(*client_ptr_, GetScrollPosition)
      .WillRepeatedly(Return(gfx::PointF(4.0f, 6.0f)));
  EXPECT_CALL(*engine_ptr_, ScrolledToXPosition).Times(0);
  EXPECT_CALL(*engine_ptr_, ScrolledToYPosition).Times(0);
  UpdatePluginGeometryWithoutWaiting(1.0f, gfx::Rect(3, 4, 5, 6));
}

TEST_F(PdfViewWebPluginTest, UpdateGeometryScrollUnderflow) {
  SetDocumentDimensions({100, 200});

  EXPECT_CALL(*client_ptr_, GetScrollPosition)
      .WillRepeatedly(Return(gfx::PointF(-1.0f, -1.0f)));
  EXPECT_CALL(*engine_ptr_, ScrolledToXPosition(0));
  EXPECT_CALL(*engine_ptr_, ScrolledToYPosition(0));
  UpdatePluginGeometryWithoutWaiting(1.0f, gfx::Rect(3, 4, 5, 6));
}

TEST_F(PdfViewWebPluginTest, UpdateGeometryScrollOverflow) {
  SetDocumentDimensions({100, 200});

  EXPECT_CALL(*client_ptr_, GetScrollPosition)
      .WillRepeatedly(Return(gfx::PointF(96.0f, 195.0f)));
  EXPECT_CALL(*engine_ptr_, ScrolledToXPosition(95));
  EXPECT_CALL(*engine_ptr_, ScrolledToYPosition(194));
  UpdatePluginGeometryWithoutWaiting(1.0f, gfx::Rect(3, 4, 5, 6));
}

TEST_F(PdfViewWebPluginTest, UpdateGeometryScrollOverflowZoomed) {
  SetDocumentDimensions({100, 200});
  SendViewportMessage(/*zoom=*/2.0);

  EXPECT_CALL(*client_ptr_, GetScrollPosition)
      .WillRepeatedly(Return(gfx::PointF(196.0f, 395.0f)));
  EXPECT_CALL(*engine_ptr_, ScrolledToXPosition(195));
  EXPECT_CALL(*engine_ptr_, ScrolledToYPosition(394));
  UpdatePluginGeometryWithoutWaiting(1.0f, gfx::Rect(3, 4, 5, 6));
}

TEST_F(PdfViewWebPluginTest, UpdateGeometryScrollScaled) {
  SetDocumentDimensions({100, 200});

  EXPECT_CALL(*client_ptr_, GetScrollPosition)
      .WillRepeatedly(Return(gfx::PointF(4.0f, 6.0f)));
  EXPECT_CALL(*engine_ptr_, ScrolledToXPosition(4));
  EXPECT_CALL(*engine_ptr_, ScrolledToYPosition(6));
  UpdatePluginGeometryWithoutWaiting(2.0f, gfx::Rect(3, 4, 5, 6));
}

TEST_F(PdfViewWebPluginTest, UpdateGeometryScrollOverflowScaled) {
  SetDocumentDimensions({100, 200});

  EXPECT_CALL(*client_ptr_, GetScrollPosition)
      .WillRepeatedly(Return(gfx::PointF(195.0f, 395.0f)));
  EXPECT_CALL(*engine_ptr_, ScrolledToXPosition(194));
  EXPECT_CALL(*engine_ptr_, ScrolledToYPosition(394));
  UpdatePluginGeometryWithoutWaiting(2.0f, gfx::Rect(3, 4, 5, 6));
}

TEST_F(PdfViewWebPluginTest, SetCaretPosition) {
  SetDocumentDimensions({16, 9});
  UpdatePluginGeometryWithoutWaiting(1.0f, {10, 20, 20, 5});

  EXPECT_CALL(*engine_ptr_, SetCaretPosition(gfx::Point(2, 3)));
  plugin_->SetCaretPosition({4.0f, 3.0f});
}

TEST_F(PdfViewWebPluginTest, SetCaretPositionNegativeOrigin) {
  SetDocumentDimensions({16, 9});
  UpdatePluginGeometryWithoutWaiting(1.0f, {-10, -20, 20, 5});

  EXPECT_CALL(*engine_ptr_, SetCaretPosition(gfx::Point(2, 3)));
  plugin_->SetCaretPosition({4.0f, 3.0f});
}

TEST_F(PdfViewWebPluginTest, SetCaretPositionFractional) {
  SetDocumentDimensions({16, 9});
  UpdatePluginGeometryWithoutWaiting(1.0f, {10, 20, 20, 5});

  EXPECT_CALL(*engine_ptr_, SetCaretPosition(gfx::Point(1, 2)));
  plugin_->SetCaretPosition({3.9f, 2.9f});
}

TEST_F(PdfViewWebPluginTest, SetCaretPositionScaled) {
  SetDocumentDimensions({16, 9});
  UpdatePluginGeometryWithoutWaiting(2.0f, {20, 40, 40, 10});

  EXPECT_CALL(*engine_ptr_, SetCaretPosition(gfx::Point(4, 6)));
  plugin_->SetCaretPosition({4.0f, 3.0f});
}

TEST_F(PdfViewWebPluginTest, PaintEmptySnapshots) {
  TestPaintEmptySnapshots(/*device_scale=*/4.0f,
                          /*window_rect=*/gfx::Rect(10, 10, 20, 20),
                          /*paint_rect=*/gfx::Rect(5, 5, 15, 15),
                          /*expected_clipped_rect=*/gfx::Rect(10, 10, 10, 10));
}

TEST_F(PdfViewWebPluginTest, PaintSnapshots) {
  TestPaintSnapshots(/*device_scale=*/4.0f,
                     /*window_rect=*/gfx::Rect(10, 10, 20, 20),
                     /*paint_rect=*/gfx::Rect(5, 5, 15, 15),
                     /*expected_clipped_rect=*/gfx::Rect(10, 10, 10, 10));
}

TEST_F(PdfViewWebPluginTest, PaintSnapshotsWithVariousDeviceScales) {
  static constexpr PaintParams kPaintWithVariousScalesParams[] = {
      {0.4f, gfx::Rect(8, 8, 30, 30), gfx::Rect(10, 10, 30, 30),
       gfx::Rect(10, 10, 28, 28)},
      {1.0f, gfx::Rect(8, 8, 30, 30), gfx::Rect(10, 10, 30, 30),
       gfx::Rect(10, 10, 28, 28)},
      {4.0f, gfx::Rect(8, 8, 30, 30), gfx::Rect(10, 10, 30, 30),
       gfx::Rect(10, 10, 28, 28)},
  };

  for (const auto& params : kPaintWithVariousScalesParams) {
    TestPaintSnapshots(params.device_scale, params.window_rect,
                       params.paint_rect, params.expected_clipped_rect);
  }
}

TEST_F(PdfViewWebPluginTest, PaintSnapshotsWithVariousRectPositions) {
  static constexpr PaintParams kPaintWithVariousPositionsParams[] = {
      // The window origin falls outside the `paint_rect` area.
      {4.0f, gfx::Rect(10, 10, 20, 20), gfx::Rect(5, 5, 15, 15),
       gfx::Rect(10, 10, 10, 10)},
      // The window origin falls within the `paint_rect` area.
      {4.0f, gfx::Rect(4, 4, 20, 20), gfx::Rect(8, 8, 15, 15),
       gfx::Rect(8, 8, 15, 15)},
  };

  for (const auto& params : kPaintWithVariousPositionsParams) {
    TestPaintSnapshots(params.device_scale, params.window_rect,
                       params.paint_rect, params.expected_clipped_rect);
  }
}

TEST_F(PdfViewWebPluginTest, OnPaintWithMultiplePaintRects) {
  SetDocumentDimensions({100, 200});
  UpdatePluginGeometryWithoutWaiting(/*device_scale=*/1.0f,
                                     gfx::Rect(0, 0, 40, 40));

  EXPECT_CALL(*engine_ptr_, Paint)
      .WillRepeatedly(
          [](const gfx::Rect& rect, SkBitmap& /*image_data*/,
             std::vector<gfx::Rect>& ready,
             std::vector<gfx::Rect>& /*pending*/) { ready.push_back(rect); });
  std::vector<PaintReadyRect> ready;
  std::vector<gfx::Rect> pending;
  plugin_->OnPaint(
      /*paint_rects=*/{gfx::Rect(5, 5, 10, 10), gfx::Rect(20, 20, 10, 10)},
      ready, pending);

  // Expect three paints: an initial background-clearing paint, and one for each
  // requested paint rectangle.
  ASSERT_THAT(ready, SizeIs(3));
  EXPECT_THAT(pending, IsEmpty());

  EXPECT_EQ(gfx::Rect(0, 0, 90, 90), ready[0].rect());
  EXPECT_TRUE(ready[0].flush_now());

  EXPECT_EQ(gfx::Rect(5, 5, 10, 10), ready[1].rect());
  EXPECT_FALSE(ready[1].flush_now());

  EXPECT_EQ(gfx::Rect(20, 20, 10, 10), ready[2].rect());
  EXPECT_FALSE(ready[2].flush_now());

  // All the requested paints should share the same `SkImage`.
  EXPECT_NE(ready[0].image(), ready[1].image());
  EXPECT_EQ(ready[1].image(), ready[2].image());
}

TEST_F(PdfViewWebPluginTest, UpdateLayerTransformWithIdentity) {
  plugin_->UpdateLayerTransform(1.0f, gfx::Vector2dF());
  TestPaintSnapshots(/*device_scale=*/4.0f,
                     /*window_rect=*/gfx::Rect(10, 10, 20, 20),
                     /*paint_rect=*/gfx::Rect(10, 10, 20, 20),
                     /*expected_clipped_rect=*/gfx::Rect(10, 10, 20, 20));
}

TEST_F(PdfViewWebPluginTest, UpdateLayerTransformWithScale) {
  plugin_->UpdateLayerTransform(0.5f, gfx::Vector2dF());
  TestPaintSnapshots(/*device_scale=*/4.0f,
                     /*window_rect=*/gfx::Rect(10, 10, 20, 20),
                     /*paint_rect=*/gfx::Rect(10, 10, 20, 20),
                     /*expected_clipped_rect=*/gfx::Rect(10, 10, 10, 10));
}

TEST_F(PdfViewWebPluginTest, UpdateLayerTransformWithTranslate) {
  plugin_->UpdateLayerTransform(1.0f, gfx::Vector2dF(-1.25, 1.25));
  TestPaintSnapshots(/*device_scale=*/4.0f,
                     /*window_rect=*/gfx::Rect(10, 10, 20, 20),
                     /*paint_rect=*/gfx::Rect(10, 10, 20, 20),
                     /*expected_clipped_rect=*/gfx::Rect(10, 15, 15, 15));
}

TEST_F(PdfViewWebPluginTest, UpdateLayerTransformWithScaleAndTranslate) {
  plugin_->UpdateLayerTransform(0.5f, gfx::Vector2dF(-1.25, 1.25));
  TestPaintSnapshots(/*device_scale=*/4.0f,
                     /*window_rect=*/gfx::Rect(10, 10, 20, 20),
                     /*paint_rect=*/gfx::Rect(10, 10, 20, 20),
                     /*expected_clipped_rect=*/gfx::Rect(10, 15, 5, 10));
}

TEST_F(PdfViewWebPluginTest, HandleViewportMessageBeforeDocumentLoadComplete) {
  EXPECT_CALL(*engine_ptr_, ApplyDocumentLayout(DocumentLayout::Options()));
  EXPECT_CALL(*client_ptr_, PostMessage).Times(0);

  plugin_->OnMessage(ParseMessage(R"({
    "type": "viewport",
    "userInitiated": false,
    "zoom": 1,
    "layoutOptions": {
      "direction": 0,
      "defaultPageOrientation": 0,
      "twoUpViewEnabled": false,
    },
    "xOffset": 0,
    "yOffset": 0,
    "pinchPhase": 0,
  })"));
}

TEST_F(PdfViewWebPluginTest, HandleViewportMessageAfterDocumentLoadComplete) {
  plugin_->DocumentLoadComplete();

  EXPECT_CALL(*engine_ptr_, ApplyDocumentLayout(DocumentLayout::Options()));
  EXPECT_CALL(*client_ptr_, PostMessage(base::test::IsJson(R"({
    "type": "loadProgress",
    "progress": 100.0,
  })")));

  plugin_->OnMessage(ParseMessage(R"({
    "type": "viewport",
    "userInitiated": false,
    "zoom": 1,
    "layoutOptions": {
      "direction": 0,
      "defaultPageOrientation": 0,
      "twoUpViewEnabled": false,
    },
    "xOffset": 0,
    "yOffset": 0,
    "pinchPhase": 0,
  })"));
}

TEST_F(PdfViewWebPluginTest, HandleViewportMessageSubsequently) {
  plugin_->OnMessage(ParseMessage(R"({
    "type": "viewport",
    "userInitiated": false,
    "zoom": 1,
    "layoutOptions": {
      "direction": 0,
      "defaultPageOrientation": 0,
      "twoUpViewEnabled": false,
    },
    "xOffset": 0,
    "yOffset": 0,
    "pinchPhase": 0,
  })"));

  DocumentLayout::Options two_up_options;
  two_up_options.set_page_spread(DocumentLayout::PageSpread::kTwoUpOdd);
  EXPECT_CALL(*engine_ptr_, ApplyDocumentLayout(two_up_options));
  EXPECT_CALL(*client_ptr_, PostMessage).Times(0);

  plugin_->OnMessage(ParseMessage(R"({
    "type": "viewport",
    "userInitiated": false,
    "zoom": 1,
    "layoutOptions": {
      "direction": 0,
      "defaultPageOrientation": 0,
      "twoUpViewEnabled": true,
    },
    "xOffset": 0,
    "yOffset": 0,
    "pinchPhase": 0,
  })"));
}

TEST_F(PdfViewWebPluginTest, HandleViewportMessageScroll) {
  EXPECT_CALL(*engine_ptr_, ApplyDocumentLayout)
      .WillRepeatedly(Return(gfx::Size(16, 9)));
  EXPECT_CALL(*engine_ptr_, ScrolledToXPosition(2));
  EXPECT_CALL(*engine_ptr_, ScrolledToYPosition(3));

  plugin_->OnMessage(ParseMessage(R"({
    "type": "viewport",
    "userInitiated": false,
    "zoom": 1,
    "layoutOptions": {
      "direction": 2,
      "defaultPageOrientation": 0,
      "twoUpViewEnabled": false,
    },
    "xOffset": 2,
    "yOffset": 3,
    "pinchPhase": 0,
  })"));
}

TEST_F(PdfViewWebPluginTest, HandleViewportMessageScrollRightToLeft) {
  EXPECT_CALL(*engine_ptr_, ApplyDocumentLayout)
      .WillRepeatedly(Return(gfx::Size(16, 9)));
  EXPECT_CALL(*engine_ptr_, ScrolledToXPosition(2));
  EXPECT_CALL(*engine_ptr_, ScrolledToYPosition(3));

  plugin_->OnMessage(ParseMessage(R"({
    "type": "viewport",
    "userInitiated": false,
    "zoom": 1,
    "layoutOptions": {
      "direction": 1,
      "defaultPageOrientation": 0,
      "twoUpViewEnabled": false,
    },
    "xOffset": 2,
    "yOffset": 3,
    "pinchPhase": 0,
  })"));
}

TEST_F(PdfViewWebPluginTest, HandleSetBackgroundColorMessage) {
  ASSERT_NE(SK_ColorGREEN, plugin_->GetBackgroundColor());

  plugin_->OnMessage(base::DictValue()
                         .Set("type", "setBackgroundColor")
                         .Set("color", static_cast<double>(SK_ColorGREEN)));

  EXPECT_EQ(SK_ColorGREEN, plugin_->GetBackgroundColor());
}

TEST_F(PdfViewWebPluginTest, HandleSetPresentationModeMessage) {
  EXPECT_FALSE(engine_ptr_->IsReadOnly());
  plugin_->set_cursor_type_for_testing(ui::mojom::CursorType::kIBeam);

  auto message = base::DictValue()
                     .Set("type", "setPresentationMode")
                     .Set("enablePresentationMode", true);
  plugin_->OnMessage(message);

  // After entering presentation mode, PDFiumEngine is read-only and the cursor
  // type has been reset to a pointer.
  EXPECT_TRUE(engine_ptr_->IsReadOnly());
  EXPECT_EQ(ui::mojom::CursorType::kPointer,
            plugin_->cursor_for_testing().type());

  message.Set("enablePresentationMode", false);
  plugin_->OnMessage(message);

  // After exiting presentation mode, PDFiumEngine is no longer read-only.
  // The cursor remains as a pointer until the next input event updates it.
  EXPECT_FALSE(engine_ptr_->IsReadOnly());
  EXPECT_EQ(ui::mojom::CursorType::kPointer,
            plugin_->cursor_for_testing().type());
}

TEST_F(PdfViewWebPluginTest, HandleInputEvent) {
  UpdatePluginGeometryWithoutWaiting(2.0f, {0, 0, 20, 20});

  EXPECT_CALL(*engine_ptr_, HandleInputEvent)
      .WillRepeatedly([](const blink::WebInputEvent& event) {
        if (!blink::WebInputEvent::IsMouseEventType(event.GetType())) {
          ADD_FAILURE() << "Unexpected event type: " << event.GetType();
          return false;
        }

        const auto& mouse_event =
            static_cast<const blink::WebMouseEvent&>(event);
        EXPECT_EQ(blink::WebInputEvent::Type::kMouseDown,
                  mouse_event.GetType());
        EXPECT_EQ(gfx::PointF(10.0f, 40.0f), mouse_event.PositionInWidget());
        return true;
      });

  blink::WebMouseEvent mouse_event;
  mouse_event.SetType(blink::WebInputEvent::Type::kMouseDown);
  mouse_event.SetPositionInWidget(10.0f, 20.0f);

  TestSendInputEvent(mouse_event,
                     blink::WebInputEventResult::kHandledApplication);
}

class PdfViewWebPluginImeTest : public PdfViewWebPluginTest {
 public:
  void TestImeSetCompositionForPlugin(const blink::WebString& text) {
    EXPECT_CALL(*engine_ptr_, HandleInputEvent).Times(0);
    plugin_->ImeSetCompositionForPlugin(text, std::vector<ui::ImeTextSpan>(),
                                        gfx::Range(),
                                        /*selection_start=*/0,
                                        /*selection_end=*/0);
  }

  void TestImeFinishComposingTextForPlugin(
      const blink::WebString& expected_text) {
    InSequence sequence;
    std::u16string expected_text16 = expected_text.Utf16();
    if (expected_text16.size()) {
      for (const auto& c : expected_text16) {
        std::u16string_view expected_key(&c, 1);
        EXPECT_CALL(*engine_ptr_,
                    HandleInputEvent(IsExpectedImeKeyEvent(expected_key)))
            .WillOnce(Return(true));
      }
    } else {
      EXPECT_CALL(*engine_ptr_, HandleInputEvent).Times(0);
    }
    plugin_->ImeFinishComposingTextForPlugin(false);
  }

  void TestImeCommitTextForPlugin(const blink::WebString& text) {
    InSequence sequence;
    std::u16string expected_text16 = text.Utf16();
    if (expected_text16.size()) {
      for (const auto& c : expected_text16) {
        std::u16string_view event(&c, 1);
        EXPECT_CALL(*engine_ptr_,
                    HandleInputEvent(IsExpectedImeKeyEvent(event)))
            .WillOnce(Return(true));
      }
    } else {
      EXPECT_CALL(*engine_ptr_, HandleInputEvent).Times(0);
    }
    plugin_->ImeCommitTextForPlugin(text, std::vector<ui::ImeTextSpan>(),
                                    gfx::Range(),
                                    /*relative_cursor_pos=*/0);
  }
};

TEST_F(PdfViewWebPluginImeTest, ImeSetCompositionAndFinishAscii) {
  const blink::WebString text = blink::WebString::FromAscii("input");
  TestImeSetCompositionForPlugin(text);
  TestImeFinishComposingTextForPlugin(text);
}

TEST_F(PdfViewWebPluginImeTest, ImeSetCompositionAndFinishUnicode) {
  const blink::WebString text = blink::WebString::FromUtf16(u"你好");
  TestImeSetCompositionForPlugin(text);
  TestImeFinishComposingTextForPlugin(text);
  // Calling ImeFinishComposingTextForPlugin() again is a no-op.
  TestImeFinishComposingTextForPlugin("");
}

TEST_F(PdfViewWebPluginImeTest, ImeSetCompositionAndFinishEmpty) {
  const blink::WebString text;
  TestImeSetCompositionForPlugin(text);
  TestImeFinishComposingTextForPlugin(text);
}

TEST_F(PdfViewWebPluginImeTest, ImeCommitTextForPluginAscii) {
  const blink::WebString text = blink::WebString::FromAscii("a b");
  TestImeCommitTextForPlugin(text);
}

TEST_F(PdfViewWebPluginImeTest, ImeCommitTextForPluginUnicode) {
  const blink::WebString text = blink::WebString::FromUtf16(u"さようなら");
  TestImeCommitTextForPlugin(text);
}

TEST_F(PdfViewWebPluginImeTest, ImeCommitTextForPluginEmpty) {
  const blink::WebString text;
  TestImeCommitTextForPlugin(text);
}

TEST_F(PdfViewWebPluginTest, SelectionChanged) {
  plugin_->EnableAccessibility();
  plugin_->DocumentLoadComplete();
  UpdatePluginGeometryWithoutWaiting(1.0f, {300, 56, 20, 5});
  SetDocumentDimensions({16, 9});

  AccessibilityViewportInfo viewport_info;
  EXPECT_CALL(pdf_host_, SelectionChanged(gfx::PointF(-8.0f, -20.0f), 40,
                                          gfx::PointF(52.0f, 60.0f), 80));
  EXPECT_CALL(*accessibility_data_handler_ptr_, SetAccessibilityViewportInfo)
      .WillOnce(SaveArg<0>(&viewport_info));
  plugin_->SelectionChanged({-10, -20, 30, 40}, {50, 60, 70, 80});

  EXPECT_EQ(gfx::Point(), viewport_info.scroll);
  pdf_receiver_.FlushForTesting();
}

TEST_F(PdfViewWebPluginTest, SelectionChangedNegativeOrigin) {
  plugin_->EnableAccessibility();
  plugin_->DocumentLoadComplete();
  UpdatePluginGeometryWithoutWaiting(1.0f, {-300, -56, 20, 5});
  SetDocumentDimensions({16, 9});

  AccessibilityViewportInfo viewport_info;
  EXPECT_CALL(pdf_host_, SelectionChanged(gfx::PointF(-8.0f, -20.0f), 40,
                                          gfx::PointF(52.0f, 60.0f), 80));
  EXPECT_CALL(*accessibility_data_handler_ptr_, SetAccessibilityViewportInfo)
      .WillOnce(SaveArg<0>(&viewport_info));
  plugin_->SelectionChanged({-10, -20, 30, 40}, {50, 60, 70, 80});

  EXPECT_EQ(gfx::Point(), viewport_info.scroll);
  pdf_receiver_.FlushForTesting();
}

TEST_F(PdfViewWebPluginTest, SelectionChangedScaled) {
  plugin_->EnableAccessibility();
  plugin_->DocumentLoadComplete();
  UpdatePluginGeometryWithoutWaiting(2.0f, {600, 112, 40, 10});
  SetDocumentDimensions({16, 9});

  AccessibilityViewportInfo viewport_info;
  EXPECT_CALL(pdf_host_, SelectionChanged(gfx::PointF(-8.0f, -20.0f), 40,
                                          gfx::PointF(52.0f, 60.0f), 80));
  EXPECT_CALL(*accessibility_data_handler_ptr_, SetAccessibilityViewportInfo)
      .WillOnce(SaveArg<0>(&viewport_info));
  plugin_->SelectionChanged({-20, -40, 60, 80}, {100, 120, 140, 160});

  EXPECT_EQ(gfx::Point(), viewport_info.scroll);
  pdf_receiver_.FlushForTesting();
}

TEST_F(PdfViewWebPluginTest, ChangeTextSelection) {
  ASSERT_FALSE(plugin_->HasSelection());
  ASSERT_TRUE(plugin_->SelectionAsText().IsEmpty());
  ASSERT_TRUE(plugin_->SelectionAsMarkup().IsEmpty());

  static constexpr char kSelectedText[] = "1234";
  EXPECT_CALL(*client_ptr_,
              TextSelectionChanged(blink::WebString(kSelectedText), 0,
                                   gfx::Range(0, 4)));

  plugin_->SetSelectedText(kSelectedText);
  EXPECT_TRUE(plugin_->HasSelection());
  EXPECT_EQ(kSelectedText, plugin_->SelectionAsText().Utf8());
  EXPECT_EQ(kSelectedText, plugin_->SelectionAsMarkup().Utf8());

  static constexpr char kEmptyText[] = "";
  EXPECT_CALL(*client_ptr_, TextSelectionChanged(blink::WebString(kEmptyText),
                                                 0, gfx::Range(0, 0)));
  plugin_->SetSelectedText(kEmptyText);
  EXPECT_FALSE(plugin_->HasSelection());
  EXPECT_TRUE(plugin_->SelectionAsText().IsEmpty());
  EXPECT_TRUE(plugin_->SelectionAsMarkup().IsEmpty());
}

TEST_F(PdfViewWebPluginTest, SelectAll) {
  EXPECT_CALL(*engine_ptr_, SelectAll);

  EXPECT_TRUE(plugin_->ExecuteEditCommand(
      /*name=*/blink::WebString::FromAscii("SelectAll"),
      /*value=*/blink::WebString()));
}

TEST_F(PdfViewWebPluginTest, MakeTextWritingDirection) {
  // When text editing is allowed, the command should succeed and call the
  // engine.
  EXPECT_CALL(*engine_ptr_, CanEditText()).WillRepeatedly(Return(true));
  EXPECT_CALL(*engine_ptr_,
              SetFocusedFormTextDirection(base::i18n::LEFT_TO_RIGHT))
      .WillOnce(Return(true));
  EXPECT_TRUE(plugin_->ExecuteEditCommand(
      /*name=*/blink::WebString::FromAscii(
          "MakeTextWritingDirectionLeftToRight"),
      /*value=*/blink::WebString()));

  EXPECT_CALL(*engine_ptr_,
              SetFocusedFormTextDirection(base::i18n::RIGHT_TO_LEFT))
      .WillOnce(Return(true));
  EXPECT_TRUE(plugin_->ExecuteEditCommand(
      /*name=*/blink::WebString::FromAscii(
          "MakeTextWritingDirectionRightToLeft"),
      /*value=*/blink::WebString()));

  EXPECT_CALL(*engine_ptr_,
              SetFocusedFormTextDirection(base::i18n::UNKNOWN_DIRECTION))
      .WillOnce(Return(true));
  EXPECT_TRUE(plugin_->ExecuteEditCommand(
      /*name=*/blink::WebString::FromAscii("MakeTextWritingDirectionNatural"),
      /*value=*/blink::WebString()));
}

TEST_F(PdfViewWebPluginTest, MakeTextWritingDirectionDenied) {
  // When text editing is not allowed, the command should fail and not call the
  // engine.
  EXPECT_CALL(*engine_ptr_, CanEditText()).WillRepeatedly(Return(false));
  EXPECT_CALL(*engine_ptr_, SetFocusedFormTextDirection(_)).Times(0);
  EXPECT_FALSE(plugin_->ExecuteEditCommand(
      /*name=*/blink::WebString::FromAscii(
          "MakeTextWritingDirectionLeftToRight"),
      /*value=*/blink::WebString()));
  EXPECT_FALSE(plugin_->ExecuteEditCommand(
      /*name=*/blink::WebString::FromAscii(
          "MakeTextWritingDirectionRightToLeft"),
      /*value=*/blink::WebString()));
  EXPECT_FALSE(plugin_->ExecuteEditCommand(
      /*name=*/blink::WebString::FromAscii("MakeTextWritingDirectionNatural"),
      /*value=*/blink::WebString()));
}

TEST_F(PdfViewWebPluginTest, GetFocusedFormTextDirection) {
  EXPECT_CALL(*engine_ptr_, GetFocusedFormTextDirection())
      .WillOnce(Return(base::i18n::RIGHT_TO_LEFT));
  EXPECT_THAT(plugin_->GetFocusedFormTextDirection(),
              Optional(base::i18n::RIGHT_TO_LEFT));
}

TEST_F(PdfViewWebPluginTest, FormTextFieldFocusChangeUpdatesTextInputType) {
  ASSERT_EQ(blink::WebTextInputType::kWebTextInputTypeNone,
            plugin_->GetPluginTextInputType());

  ExpectUpdateTextInputState(blink::WebTextInputType::kWebTextInputTypeText);
  plugin_->FormFieldFocusChange(PDFiumEngineClient::FocusFieldType::kText);

  ExpectUpdateTextInputState(blink::WebTextInputType::kWebTextInputTypeNone);
  plugin_->FormFieldFocusChange(PDFiumEngineClient::FocusFieldType::kNoFocus);

  ExpectUpdateTextInputState(blink::WebTextInputType::kWebTextInputTypeText);
  plugin_->FormFieldFocusChange(PDFiumEngineClient::FocusFieldType::kText);

  ExpectUpdateTextInputState(blink::WebTextInputType::kWebTextInputTypeNone);
  plugin_->FormFieldFocusChange(PDFiumEngineClient::FocusFieldType::kNonText);
}

TEST_F(PdfViewWebPluginTest, SearchString) {
  static constexpr char16_t kNeedle[] = u"fox";
  static constexpr char16_t kHaystack[] =
      u"The quick brown fox jumped over the lazy Fox";

  {
    static constexpr PDFiumEngineClient::SearchStringResult kExpectation[] = {
        {16, 3}};
    EXPECT_THAT(
        plugin_->SearchString(kNeedle, kHaystack, /*case_sensitive=*/true),
        Pointwise(SearchStringResultEq(), kExpectation));
  }
  {
    static constexpr PDFiumEngineClient::SearchStringResult kExpectation[] = {
        {16, 3}, {41, 3}};
    EXPECT_THAT(
        plugin_->SearchString(kNeedle, kHaystack, /*case_sensitive=*/false),
        Pointwise(SearchStringResultEq(), kExpectation));
  }
}

TEST_F(PdfViewWebPluginTest, UpdateFocus) {
  MockFunction<void(int checkpoint_num)> checkpoint;

  {
    InSequence sequence;

    // Focus false -> true: Triggers updates.
    EXPECT_CALL(*client_ptr_, UpdateTextInputState);
    EXPECT_CALL(*client_ptr_, UpdateSelectionBounds);
    EXPECT_CALL(checkpoint, Call(1));

    // Focus true -> true: No updates.
    EXPECT_CALL(checkpoint, Call(2));

    // Focus true -> false: Triggers updates.
    EXPECT_CALL(*client_ptr_, UpdateTextInputState);
    EXPECT_CALL(*client_ptr_, UpdateSelectionBounds);
    EXPECT_CALL(checkpoint, Call(3));

    // Focus false -> false: No updates.
    EXPECT_CALL(checkpoint, Call(4));

    // Focus false -> true: Triggers updates.
    EXPECT_CALL(*client_ptr_, UpdateTextInputState);
    EXPECT_CALL(*client_ptr_, UpdateSelectionBounds);
  }

  // The focus type does not matter in this test.
  plugin_->UpdateFocus(/*focused=*/true, blink::mojom::FocusType::kNone);
  checkpoint.Call(1);
  plugin_->UpdateFocus(/*focused=*/true, blink::mojom::FocusType::kNone);
  checkpoint.Call(2);
  plugin_->UpdateFocus(/*focused=*/false, blink::mojom::FocusType::kNone);
  checkpoint.Call(3);
  plugin_->UpdateFocus(/*focused=*/false, blink::mojom::FocusType::kNone);
  checkpoint.Call(4);
  plugin_->UpdateFocus(/*focused=*/true, blink::mojom::FocusType::kNone);
}

TEST_F(PdfViewWebPluginTest, ShouldDispatchImeEventsToPlugin) {
  ASSERT_TRUE(plugin_->ShouldDispatchImeEventsToPlugin());
}

TEST_F(PdfViewWebPluginTest, CaretChange) {
  EXPECT_CALL(*engine_ptr_, ZoomUpdated(2.0f));
  UpdatePluginGeometry(
      /*device_scale=*/2.0f, /*window_rect=*/gfx::Rect(12, 24, 36, 48));
  plugin_->CaretChanged(gfx::Rect(10, 20, 30, 40));
  EXPECT_EQ(gfx::Rect(28, 20, 30, 40), plugin_->GetPluginCaretBounds());
}

TEST_F(PdfViewWebPluginTest, EnteredEditMode) {
  EXPECT_CALL(pdf_host_, SetPluginCanSave(true));
  EXPECT_CALL(*client_ptr_, PostMessage).Times(AnyNumber());
  EXPECT_CALL(*client_ptr_, PostMessage(base::test::IsJson(R"({
    "type": "setIsEditing",
  })")));
  plugin_->EnteredEditMode();

  pdf_receiver_.FlushForTesting();
}

TEST_F(PdfViewWebPluginTest, NotifyNumberOfFindResultsChanged) {
  plugin_->StartFind("x", /*case_sensitive=*/false, /*identifier=*/123);

  const std::vector<gfx::Rect> tickmarks = {gfx::Rect(1, 2), gfx::Rect(3, 4)};
  plugin_->UpdateTickMarks(tickmarks);

  EXPECT_CALL(*client_ptr_, ReportFindInPageTickmarks(tickmarks));
  EXPECT_CALL(*client_ptr_, ReportFindInPageMatchCount(123, 5, true));
  plugin_->NotifyNumberOfFindResultsChanged(/*total=*/5, /*final_result=*/true);
}

TEST_F(PdfViewWebPluginTest, OnDocumentLoadComplete) {
  const auto message =
      base::DictValue()
          .Set("type", "metadata")
          .Set("metadataData", base::DictValue()
                                   .Set("fileSize", "0 B")
                                   .Set("linearized", false)
                                   .Set("pageSize", "Varies")
                                   .Set("canSerializeDocument", true));

  EXPECT_CALL(*client_ptr_, PostMessage);
  EXPECT_CALL(*client_ptr_, PostMessage(Eq(std::ref(message))));
  plugin_->DocumentLoadComplete();
}

TEST_F(PdfViewWebPluginTest, OnDocumentLoadCompleteWithContentDisposition) {
  EXPECT_CALL(*engine_ptr_, GetFileNameFromContentDisposition)
      .WillOnce(Return("custom.pdf"));

  const auto message =
      base::DictValue()
          .Set("type", "metadata")
          .Set("metadataData", base::DictValue()
                                   .Set("fileSize", "0 B")
                                   .Set("linearized", false)
                                   .Set("pageSize", "Varies")
                                   .Set("title", "custom.pdf")
                                   .Set("canSerializeDocument", true));

  EXPECT_CALL(*client_ptr_, PostMessage);
  EXPECT_CALL(*client_ptr_, PostMessage(Eq(std::ref(message))));
  plugin_->DocumentLoadComplete();
}

TEST_F(PdfViewWebPluginTest, OnRendererPreferencesUpdated) {
  auto message = base::DictValue()
                     .Set("type", "rendererPreferencesUpdated")
                     .Set("caretBrowsingEnabled", false);

  InSequence seq;

  EXPECT_CALL(*client_ptr_, PostMessage(Eq(std::ref(message))));
  EXPECT_CALL(*engine_ptr_, SetCaretBrowsingEnabled(false));
  EXPECT_CALL(*engine_ptr_,
              SetCaretBlinkInterval(PdfCaret::kDefaultBlinkInterval));

  blink::RendererPreferences prefs;
  plugin_->OnRendererPreferencesUpdated(prefs);

  message.Set("caretBrowsingEnabled", true);

  EXPECT_CALL(*client_ptr_, PostMessage(Eq(std::ref(message))));
  EXPECT_CALL(*engine_ptr_, SetCaretBrowsingEnabled(true));
  constexpr base::TimeDelta kBlinkInterval = base::Milliseconds(300);
  EXPECT_CALL(*engine_ptr_, SetCaretBlinkInterval(kBlinkInterval));

  prefs.caret_browsing_enabled = true;
  prefs.caret_blink_interval = kBlinkInterval;
  plugin_->OnRendererPreferencesUpdated(prefs);

  EXPECT_CALL(*client_ptr_, PostMessage(Eq(std::ref(message))));
  EXPECT_CALL(*engine_ptr_, SetCaretBrowsingEnabled(true));
  EXPECT_CALL(*engine_ptr_, SetCaretBlinkInterval(base::TimeDelta()));

  prefs.caret_blink_interval = base::TimeDelta();
  plugin_->OnRendererPreferencesUpdated(prefs);
}

#if BUILDFLAG(ENABLE_SCREEN_AI_SERVICE)
// Searchify in progress not shown when searchify just starts.
TEST_F(PdfViewWebPluginTest, OnSearchifyStarted) {
  base::DictValue message = GenerateShowSearchifyInProgressMessage(true);

  EXPECT_CALL(*client_ptr_, PostMessage(Eq(std::ref(message)))).Times(0);
  EXPECT_CALL(pdf_host_, OnSearchifyStarted);

  plugin_->OnSearchifyStateChange(true);

  pdf_receiver_.FlushForTesting();
}

// Searchify in progress not shown when searchify didn't starts.
TEST_F(PdfViewWebPluginTest, OnSearchifyNotStartedAndMaybeShowInProgress) {
  base::DictValue message = GenerateShowSearchifyInProgressMessage(true);

  EXPECT_CALL(*client_ptr_, PostMessage(Eq(std::ref(message)))).Times(0);
  EXPECT_CALL(pdf_host_, OnSearchifyStarted).Times(0);

  plugin_->MaybeShowSearchifyInProgress();

  pdf_receiver_.FlushForTesting();
}

// Searchify in progress shown when asked after start.
TEST_F(PdfViewWebPluginTest, OnSearchifyStartedAndMaybeShowInProgress) {
  base::DictValue message = GenerateShowSearchifyInProgressMessage(true);

  EXPECT_CALL(*client_ptr_, PostMessage(Eq(std::ref(message))));
  EXPECT_CALL(pdf_host_, OnSearchifyStarted);

  plugin_->OnSearchifyStateChange(true);
  plugin_->MaybeShowSearchifyInProgress();

  pdf_receiver_.FlushForTesting();
}

// Searchify in progress not shown when asked after stop.
TEST_F(PdfViewWebPluginTest,
       OnSearchifyStartedAndStoppedAndMaybeShowInProgress) {
  base::DictValue message_show = GenerateShowSearchifyInProgressMessage(true);

  EXPECT_CALL(*client_ptr_, PostMessage(Eq(std::ref(message_show)))).Times(0);
  EXPECT_CALL(pdf_host_, OnSearchifyStarted);

  plugin_->OnSearchifyStateChange(true);
  plugin_->OnSearchifyStateChange(false);
  plugin_->MaybeShowSearchifyInProgress();

  pdf_receiver_.FlushForTesting();
}

// Searchify in progress hides after stop.
TEST_F(PdfViewWebPluginTest, OnSearchifyShowProgressHideAfterStopped) {
  base::DictValue message_show = GenerateShowSearchifyInProgressMessage(true);
  base::DictValue message_hide = GenerateShowSearchifyInProgressMessage(false);

  EXPECT_CALL(*client_ptr_, PostMessage(Eq(std::ref(message_show))));
  EXPECT_CALL(*client_ptr_, PostMessage(Eq(std::ref(message_hide))));

  plugin_->OnSearchifyStateChange(true);
  plugin_->MaybeShowSearchifyInProgress();
  plugin_->OnSearchifyStateChange(false);

  pdf_receiver_.FlushForTesting();
}

TEST_F(PdfViewWebPluginTest, OnSearchifyStartedMoreThanOnce) {
  plugin_->OnSearchifyStateChange(true);
  plugin_->OnSearchifyStateChange(false);
  plugin_->OnSearchifyStateChange(true);

  EXPECT_CALL(pdf_host_, OnSearchifyStarted);

  pdf_receiver_.FlushForTesting();
}

TEST_F(PdfViewWebPluginTest, OnHasSearchifyText) {
  auto message = base::DictValue().Set("type", "setHasSearchifyText");

  EXPECT_CALL(*client_ptr_, PostMessage(Eq(std::ref(message))));
  plugin_->OnHasSearchifyText();
}
#endif  // BUILDFLAG(ENABLE_SCREEN_AI_SERVICE)

TEST_F(PdfViewWebPluginTest, FindAndHighlightTextFragments) {
  EXPECT_CALL(*engine_ptr_, FindAndHighlightTextFragments(
                                ElementsAre("hello-,world", "world,-hello")))
      .WillOnce(Return(true));
  EXPECT_CALL(*engine_ptr_, ScrollToFirstTextFragment);

  plugin_->OnMessage(
      base::DictValue()
          .Set("type", "highlightTextFragments")
          .Set(
              "textFragments",
              base::ListValue().Append("hello-,world").Append("world,-hello")));
}

TEST_F(PdfViewWebPluginTest, FindAndHighlightTextFragmentsNotFound) {
  EXPECT_CALL(*engine_ptr_, FindAndHighlightTextFragments(
                                ElementsAre("hello-,world", "world,-hello")))
      .WillOnce(Return(false));
  EXPECT_CALL(*engine_ptr_, ScrollToFirstTextFragment).Times(0);

  plugin_->OnMessage(
      base::DictValue()
          .Set("type", "highlightTextFragments")
          .Set(
              "textFragments",
              base::ListValue().Append("hello-,world").Append("world,-hello")));
}

class PdfViewWebPluginWithDocInfoTest
    : public PdfViewWebPluginTest,
      public testing::WithParamInterface<bool> {
 public:
  void SetUp() override {
    PdfViewWebPluginTest::SetUp();
    if (IsPortfolioEnabled()) {
      scoped_feature_list_.InitAndEnableFeature(features::kPdfPortfolio);
    }
  }

  bool IsPortfolioEnabled() { return GetParam(); }

 protected:
  class TestPDFiumEngineWithDocInfo : public TestPDFiumEngine {
   public:
    explicit TestPDFiumEngineWithDocInfo(PDFiumEngineClient* client)
        : TestPDFiumEngine(client) {
      InitializeDocumentAttachments();
      InitializeDocumentMetadata();
    }

    base::ListValue GetBookmarks() override {
      // Create `bookmark2` which navigates to a web page. This bookmark will be
      // a child of `bookmark1`.
      auto bookmark2 =
          base::DictValue().Set("title", "Bookmark 2").Set("uri", "test.com");

      // Create `bookmark1` which navigates to an in-doc position. This bookmark
      // will be in the top-level bookmark list.
      auto bookmark1 =
          base::DictValue()
              .Set("title", "Bookmark 1")
              .Set("page", 2)
              .Set("x", 10)
              .Set("y", 20)
              .Set("zoom", 2.0)
              .Set("children", base::ListValue().Append(std::move(bookmark2)));

      // Create the top-level bookmark list.
      return base::ListValue().Append(std::move(bookmark1));
    }

    std::optional<gfx::Size> GetUniformPageSizePoints() override {
      return gfx::Size(1000, 1200);
    }

   private:
    void InitializeDocumentAttachments() {
      doc_attachment_info_list().resize(3);

      // A regular attachment.
      doc_attachment_info_list()[0].name = u"attachment1.txt";
      doc_attachment_info_list()[0].creation_date = u"D:20170712214438-07'00'";
      doc_attachment_info_list()[0].modified_date = u"D:20160115091400";
      doc_attachment_info_list()[0].is_readable = true;
      doc_attachment_info_list()[0].size_bytes = 13u;

      // An unreadable attachment.
      doc_attachment_info_list()[1].name = u"attachment2.pdf";
      doc_attachment_info_list()[1].is_readable = false;

      // A readable attachment that exceeds download size limit.
      doc_attachment_info_list()[2].name = u"attachment3.mov";
      doc_attachment_info_list()[2].is_readable = true;
      doc_attachment_info_list()[2].size_bytes =
          PdfViewWebPlugin::kMaximumSavedFileSize + 1;
    }

    void InitializeDocumentMetadata() {
      metadata().version = PdfVersion::k1_7;
      metadata().size_bytes = 13u;
      metadata().page_count = 13u;
      metadata().linearized = true;
      metadata().has_attachments = true;
      metadata().form_type = FormType::kAcroForm;
      metadata().title = "Title";
      metadata().author = "Author";
      metadata().subject = "Subject";
      metadata().keywords = "Keywords";
      metadata().creator = "Creator";
      metadata().producer = "Producer";
      ASSERT_TRUE(base::Time::FromUTCString("2021-05-04 11:12:13",
                                            &metadata().creation_date));
      ASSERT_TRUE(base::Time::FromUTCString("2021-06-04 15:16:17",
                                            &metadata().mod_date));
    }

    // Note: In the metadata message `creation_date` and `mod_date` are
    // locale-specific strings for display to the user. So this test uses a
    // specific locale.
    base::test::ScopedRestoreICUDefaultLocale restore_locale_{"en_US"};
    base::test::ScopedRestoreDefaultTimezone restore_timezone_{
        "America/Los_Angeles"};
  };

  static base::DictValue CreateExpectedAttachmentsResponse() {
    return base::DictValue()
        .Set("type", "attachments")
        .Set("attachmentsData", base::ListValue()
                                    .Append(base::DictValue()
                                                .Set("name", "attachment1.txt")
                                                .Set("size", 13)
                                                .Set("readable", true))
                                    .Append(base::DictValue()
                                                .Set("name", "attachment2.pdf")
                                                .Set("size", 0)
                                                .Set("readable", false))
                                    .Append(base::DictValue()
                                                .Set("name", "attachment3.mov")
                                                .Set("size", -1)
                                                .Set("readable", true)));
  }

  static base::DictValue CreateExpectedBookmarksResponse(
      base::ListValue bookmarks) {
    return base::DictValue()
        .Set("type", "bookmarks")
        .Set("bookmarksData", std::move(bookmarks));
  }

  static base::DictValue CreateExpectedMetadataResponse() {
    return base::DictValue()
        .Set("type", "metadata")
        .Set("metadataData", base::DictValue()
                                 .Set("version", "1.7")
                                 .Set("fileSize", "13 B")
                                 .Set("linearized", true)
                                 .Set("title", "Title")
                                 .Set("author", "Author")
                                 .Set("subject", "Subject")
                                 .Set("keywords", "Keywords")
                                 .Set("creator", "Creator")
                                 .Set("producer", "Producer")
                                 .Set("creationDate",
                                      "5/4/21, 4:12:13\xE2\x80\xAF"
                                      "AM")
                                 .Set("modDate",
                                      "6/4/21, 8:16:17\xE2\x80\xAF"
                                      "AM")
                                 .Set("pageSize", "13.89 × 16.67 in (portrait)")
                                 .Set("canSerializeDocument", true));
  }

  void SetUpClient() override {
    EXPECT_CALL(*client_ptr_, CreateEngine).WillOnce([this]() {
      auto engine = std::make_unique<NiceMock<TestPDFiumEngineWithDocInfo>>(
          plugin_.get());
      engine_ptr_ = engine.get();
      return engine;
    });
  }

  base::test::ScopedFeatureList scoped_feature_list_;
};

TEST_P(PdfViewWebPluginWithDocInfoTest, OnDocumentLoadComplete) {
  const base::DictValue expect_attachments =
      CreateExpectedAttachmentsResponse();
  const base::DictValue expect_bookmarks =
      CreateExpectedBookmarksResponse(engine_ptr_->GetBookmarks());
  const base::DictValue expect_metadata = CreateExpectedMetadataResponse();
  EXPECT_CALL(*client_ptr_, PostMessage);
  EXPECT_CALL(*client_ptr_, PostMessage(Eq(std::ref(expect_attachments))))
      .Times(IsPortfolioEnabled() ? 1 : 0);
  EXPECT_CALL(*client_ptr_, PostMessage(Eq(std::ref(expect_bookmarks))));
  EXPECT_CALL(*client_ptr_, PostMessage(Eq(std::ref(expect_metadata))));
  plugin_->DocumentLoadComplete();
}

INSTANTIATE_TEST_SUITE_P(All, PdfViewWebPluginWithDocInfoTest, testing::Bool());

class PdfViewWebPluginSaveTest : public PdfViewWebPluginTest {
 protected:
  static void AddDataToValue(base::span<const uint8_t> data,
                             base::Value& value) {
    value.GetDict().Set("dataToSave", base::Value(data));
  }

  void SetUpClient() override {
    // Ignore non-"saveData" `PdfViewWebPlugin::Client::PostMessage()` calls.
    EXPECT_CALL(*client_ptr_, PostMessage)
        .WillRepeatedly([](const base::DictValue& message) {
          EXPECT_NE("saveData", *message.FindString("type"));
        });
  }
};

TEST_F(PdfViewWebPluginSaveTest, OriginalInNonEditMode) {
  {
    InSequence pdf_host_sequence;
    EXPECT_CALL(pdf_host_, SavePdf());
  }

  EXPECT_EQ(blink::WebTextInputType::kWebTextInputTypeNone,
            plugin_->GetPluginTextInputType());

  EXPECT_CALL(*client_ptr_, PostMessage(base::test::IsJson(R"({
    "type": "consumeSaveToken",
    "token": "original-in-non-edit-mode",
  })")));

  plugin_->OnMessage(ParseMessage(R"({
    "type": "save",
    "saveRequestType": "ORIGINAL",
    "token": "original-in-non-edit-mode",
  })"));

  pdf_receiver_.FlushForTesting();
}

TEST_F(PdfViewWebPluginSaveTest, OriginalInEditMode) {
  plugin_->EnteredEditMode();
  pdf_receiver_.FlushForTesting();

  {
    InSequence pdf_host_sequence;

    EXPECT_CALL(pdf_host_, SetPluginCanSave(false));
    EXPECT_CALL(pdf_host_, SavePdf());
    EXPECT_CALL(pdf_host_, SetPluginCanSave(true));
  }

  EXPECT_EQ(blink::WebTextInputType::kWebTextInputTypeNone,
            plugin_->GetPluginTextInputType());

  EXPECT_CALL(*client_ptr_, PostMessage(base::test::IsJson(R"({
    "type": "consumeSaveToken",
    "token": "original-in-edit-mode",
  })")));

  plugin_->OnMessage(ParseMessage(R"({
    "type": "save",
    "saveRequestType": "ORIGINAL",
    "token": "original-in-edit-mode",
  })"));

  pdf_receiver_.FlushForTesting();
}

TEST_F(PdfViewWebPluginSaveTest, EditedInEditMode) {
  plugin_->EnteredEditMode();

  EXPECT_EQ(blink::WebTextInputType::kWebTextInputTypeNone,
            plugin_->GetPluginTextInputType());

  base::Value expected_response = base::test::ParseJson(R"({
    "type": "saveData",
    "token": "edited-in-edit-mode",
    "fileName": "example.pdf",
    "editModeForTesting": true,
  })");
  AddDataToValue(base::span(TestPDFiumEngine::kSaveData), expected_response);
  EXPECT_CALL(*client_ptr_, PostMessage(base::test::IsJson(expected_response)));

  plugin_->OnMessage(ParseMessage(R"({
    "type": "save",
    "saveRequestType": "EDITED",
    "token": "edited-in-edit-mode",
  })"));
}

class PdfViewWebPluginSaveInBlocksTest : public PdfViewWebPluginTest {
 protected:
  base::DictValue CreateRequest(pdf::mojom::SaveRequestType request_type,
                                uint32_t offset,
                                uint32_t block_size,
                                std::string token) {
    std::string request_type_string;
    switch (request_type) {
      case pdf::mojom::SaveRequestType::kAnnotation:
        request_type_string = "ANNOTATION";
        break;
      case pdf::mojom::SaveRequestType::kOriginal:
        request_type_string = "ORIGINAL";
        break;
      case pdf::mojom::SaveRequestType::kEdited:
        request_type_string = "EDITED";
        break;
      case pdf::mojom::SaveRequestType::kSearchified:
        request_type_string = "SEARCHIFIED";
        break;
    }
    return base::DictValue()
        .Set("type", "getSaveDataBlock")
        .Set("saveRequestType", request_type_string)
        .Set("offset", static_cast<int>(offset))
        .Set("blockSize", static_cast<int>(block_size))
        .Set("token", token);
  }

  void ExpectResponse(base::span<const uint8_t> data,
                      uint32_t offset,
                      uint32_t block_size,
                      std::string token) {
    auto data_to_save = data.subspan(offset, block_size);
    base::BlobStorage data_to_save_blob(data_to_save.begin(),
                                        data_to_save.end());
    auto dict = base::DictValue()
                    .Set("type", "saveDataBlock")
                    .Set("token", std::move(token))
                    .Set("dataToSave", std::move(data_to_save_blob))
                    .Set("totalFileSize", static_cast<int>(data.size()));
    EXPECT_CALL(*client_ptr_, PostMessage(base::test::IsJson(dict)));
  }

  void SetUpClient() override {
    // Ignore non - "saveDataBlock" `PdfViewWebPlugin::Client::PostMessage()`
    // calls.
    EXPECT_CALL(*client_ptr_, PostMessage)
        .WillRepeatedly([](const base::DictValue& message) {
          EXPECT_NE("saveDataBlock", *message.FindString("type"));
        });
  }
};

TEST_F(PdfViewWebPluginSaveInBlocksTest, GetSuggestedFileName) {
  EXPECT_CALL(*client_ptr_, PostMessage(base::test::IsJson(R"({
    "type": "getSuggestedFileNameReply",
    "messageId": "foo",
    "fileName": "example.pdf",
  })")));

  plugin_->OnMessage(ParseMessage(R"({
    "type": "getSuggestedFileName",
    "messageId": "foo",
  })"));

  pdf_receiver_.FlushForTesting();
}

TEST_F(PdfViewWebPluginSaveInBlocksTest, OriginalInOneBlock) {
  base::span data(TestPDFiumEngine::kLoadedData);
  ExpectResponse(data, 0, data.size(), "token-1");
  plugin_->OnMessage(
      CreateRequest(pdf::mojom::SaveRequestType::kOriginal, 0, 0, "token-1"));
  EXPECT_TRUE(plugin_->IsSaveDataBufferEmptyForTesting());
  pdf_receiver_.FlushForTesting();
}

TEST_F(PdfViewWebPluginSaveInBlocksTest, OriginalInMulipleBlocks) {
  plugin_->SetMaxSaveBufferSizeForTesting(3);

  base::span data(TestPDFiumEngine::kLoadedData);
  ASSERT_GT(data.size(), 3u);
  ExpectResponse(data, 0, 3, "token-1");
  ExpectResponse(data, 3, data.size() - 3, "token-2");
  plugin_->OnMessage(
      CreateRequest(pdf::mojom::SaveRequestType::kOriginal, 0, 0, "token-1"));
  EXPECT_TRUE(plugin_->IsSaveDataBufferEmptyForTesting());
  plugin_->OnMessage(CreateRequest(pdf::mojom::SaveRequestType::kOriginal, 3,
                                   data.size() - 3, "token-2"));
  EXPECT_TRUE(plugin_->IsSaveDataBufferEmptyForTesting());
  pdf_receiver_.FlushForTesting();
}

TEST_F(PdfViewWebPluginSaveInBlocksTest, EditedInOneBlock) {
  plugin_->EnteredEditMode();

  base::span data(TestPDFiumEngine::kSaveData);
  ExpectResponse(data, 0, data.size(), "token-1");
  plugin_->OnMessage(
      CreateRequest(pdf::mojom::SaveRequestType::kEdited, 0, 0, "token-1"));
  EXPECT_TRUE(plugin_->IsSaveDataBufferEmptyForTesting());
  pdf_receiver_.FlushForTesting();
}

TEST_F(PdfViewWebPluginSaveInBlocksTest, EditedInMultipleBlock) {
  plugin_->EnteredEditMode();
  plugin_->SetMaxSaveBufferSizeForTesting(2);

  base::span data(TestPDFiumEngine::kSaveData);
  ASSERT_GT(data.size(), 2u);
  ExpectResponse(data, 0, 2, "token-1");
  ExpectResponse(data, 2, data.size() - 2, "token-2");

  plugin_->OnMessage(
      CreateRequest(pdf::mojom::SaveRequestType::kEdited, 0, 0, "token-1"));
  EXPECT_FALSE(plugin_->IsSaveDataBufferEmptyForTesting());
  plugin_->OnMessage(CreateRequest(pdf::mojom::SaveRequestType::kEdited, 2,
                                   data.size() - 2, "token-2"));
  EXPECT_TRUE(plugin_->IsSaveDataBufferEmptyForTesting());
  pdf_receiver_.FlushForTesting();
}

TEST_F(PdfViewWebPluginSaveInBlocksTest, ReleaseSaveBuffer) {
  plugin_->EnteredEditMode();
  plugin_->SetMaxSaveBufferSizeForTesting(2);

  base::span data(TestPDFiumEngine::kSaveData);
  ASSERT_GT(data.size(), 2u);
  ExpectResponse(data, 0, 2, "token-1");

  plugin_->OnMessage(
      CreateRequest(pdf::mojom::SaveRequestType::kEdited, 0, 0, "token-1"));
  EXPECT_FALSE(plugin_->IsSaveDataBufferEmptyForTesting());

  plugin_->OnMessage(
      base::DictValue().Set("type", "releaseSaveInBlockBuffers"));
  EXPECT_TRUE(plugin_->IsSaveDataBufferEmptyForTesting());

  pdf_receiver_.FlushForTesting();
}

#if BUILDFLAG(ENABLE_PDF_INK2)
using PdfViewWebPluginInk2SaveInBlocksTest = PdfViewWebPluginSaveInBlocksTest;

TEST_F(PdfViewWebPluginInk2SaveInBlocksTest, AnnotationInEditModeMetrics) {
  DocumentInkTextBoxesMap map;
  EXPECT_CALL(*engine_ptr_, LoadTextAnnotationsFromPdf())
      .WillOnce(Return(std::move(map)));

  plugin_->OnMessage(ParseMessage(R"({
    "type": "getAllTextAnnotations",
    "messageId": "foo",
  })"));

  plugin_->EnteredEditMode();

  base::span data(TestPDFiumEngine::kSaveData);
  ExpectResponse(data, 0, data.size(), "token-1");

  base::HistogramTester histograms;
  plugin_->OnMessage(CreateRequest(pdf::mojom::SaveRequestType::kAnnotation,
                                   /*offset=*/0, /*block_size=*/0, "token-1"));
  EXPECT_TRUE(plugin_->IsSaveDataBufferEmptyForTesting());
  pdf_receiver_.FlushForTesting();

  histograms.ExpectTotalCount("PDF.Ink2TextAnnotationAddedCountOnSave", 0);
  histograms.ExpectTotalCount("PDF.Ink2TextAnnotationRemovedCountOnSave", 0);
}

TEST_F(PdfViewWebPluginInk2SaveInBlocksTest, AnnotationInEditModeMetricsAdded) {
  DocumentInkTextBoxesMap map;
  EXPECT_CALL(*engine_ptr_, LoadTextAnnotationsFromPdf())
      .WillOnce(Return(std::move(map)));

  plugin_->OnMessage(ParseMessage(R"({
    "type": "getAllTextAnnotations",
    "messageId": "foo",
  })"));

  plugin_->EnteredEditMode();

  EXPECT_CALL(*engine_ptr_, DrawText(_, _, _, _, _, _));
  EXPECT_CALL(*engine_ptr_, RequestThumbnail(_, _, _));

  plugin_->OnMessage(CreateFinishTextAnnotationMessage(
      SampleFinishTextAnnotationData(/*frontend_id=*/10, FontId(123),
                                     /*page_index=*/0, /*pdf_zoom=*/1.0)));

  base::span data(TestPDFiumEngine::kSaveData);
  ExpectResponse(data, 0, data.size(), "token-1");

  base::HistogramTester histograms;
  plugin_->OnMessage(CreateRequest(pdf::mojom::SaveRequestType::kAnnotation,
                                   /*offset=*/0, /*block_size=*/0, "token-1"));
  EXPECT_TRUE(plugin_->IsSaveDataBufferEmptyForTesting());
  pdf_receiver_.FlushForTesting();

  histograms.ExpectUniqueSample("PDF.Ink2TextAnnotationAddedCountOnSave", 1, 1);
  histograms.ExpectTotalCount("PDF.Ink2TextAnnotationRemovedCountOnSave", 0);
}

TEST_F(PdfViewWebPluginInk2SaveInBlocksTest,
       AnnotationInEditModeMetricsRemoved) {
  DocumentInkTextBoxesMap map;
  map[0].push_back(InkTextBox(/*id=*/1, SampleInkTextBoxAttributes()));
  EXPECT_CALL(*engine_ptr_, LoadTextAnnotationsFromPdf())
      .WillOnce(Return(std::move(map)));

  plugin_->OnMessage(ParseMessage(R"({
    "type": "getAllTextAnnotations",
    "messageId": "foo",
  })"));

  plugin_->EnteredEditMode();

  base::DictValue finish_data =
      SampleFinishTextAnnotationData(/*frontend_id=*/0, FontId(123),
                                     /*page_index=*/0, /*pdf_zoom=*/1.0);
  finish_data.Set("text", "");

  EXPECT_CALL(*engine_ptr_, UpdateTextActiveAndInvalidate(_, false));
  EXPECT_CALL(*engine_ptr_, RequestThumbnail(_, _, _));

  plugin_->OnMessage(CreateFinishTextAnnotationMessage(std::move(finish_data)));

  base::span data(TestPDFiumEngine::kSaveData);
  ExpectResponse(data, 0, data.size(), "token-1");

  base::HistogramTester histograms;
  plugin_->OnMessage(CreateRequest(pdf::mojom::SaveRequestType::kAnnotation,
                                   /*offset=*/0, /*block_size=*/0, "token-1"));
  EXPECT_TRUE(plugin_->IsSaveDataBufferEmptyForTesting());
  pdf_receiver_.FlushForTesting();

  histograms.ExpectTotalCount("PDF.Ink2TextAnnotationAddedCountOnSave", 0);
  histograms.ExpectUniqueSample("PDF.Ink2TextAnnotationRemovedCountOnSave", 1,
                                1);
}
#endif  // BUILDFLAG(ENABLE_PDF_INK2)

#if BUILDFLAG(ENABLE_PDF_SAVE_TO_DRIVE)
class PdfViewWebPluginSaveInBlocksToGoogleDriveTest
    : public PdfViewWebPluginSaveInBlocksTest {
 protected:
  void FreeHandler(mojo::Remote<pdf::mojom::SaveDataBufferHandler>& handler) {
    handler.reset();
    base::RunLoop run_loop;
    base::SingleThreadTaskRunner::GetCurrentDefault()->PostTask(
        FROM_HERE, run_loop.QuitClosure());
    run_loop.Run();
  }

  std::pair<mojo::Remote<pdf::mojom::SaveDataBufferHandler>, uint32_t>
  GetSaveDataBufferHandler(pdf::mojom::SaveRequestType request_type) {
    base::test::TestFuture<pdf::mojom::SaveDataBufferHandlerGetResultPtr>
        future;
    plugin_->GetSaveDataBufferHandlerForDrive(
        request_type,
        future.GetCallback<pdf::mojom::SaveDataBufferHandlerGetResultPtr>());
    pdf::mojom::SaveDataBufferHandlerGetResultPtr result = future.Take();
    mojo::Remote<pdf::mojom::SaveDataBufferHandler> handler(
        std::move(result->handler));
    return {std::move(handler), result->total_file_size};
  }

  void ReadSaveDataBufferAndExpectResult(
      mojo::Remote<pdf::mojom::SaveDataBufferHandler>& handler,
      base::span<const uint8_t> expected_file_data,
      uint32_t offset,
      uint32_t block_size) {
    base::test::TestFuture<mojo_base::BigBuffer> read_future;
    handler->Read(offset, block_size,
                  read_future.GetCallback<mojo_base::BigBuffer>());
    mojo_base::BigBuffer block = read_future.Take();
    auto expected_block_data = expected_file_data.subspan(offset, block_size);
    EXPECT_THAT(block, ElementsAreArray(expected_block_data));
  }
};

TEST_F(PdfViewWebPluginSaveInBlocksToGoogleDriveTest, OriginalInOneBlock) {
  base::span<const uint8_t> data(TestPDFiumEngine::kLoadedData);
  auto [handler, total_file_size] =
      GetSaveDataBufferHandler(pdf::mojom::SaveRequestType::kOriginal);
  EXPECT_EQ(total_file_size, static_cast<uint32_t>(data.size()));
  ReadSaveDataBufferAndExpectResult(handler, data, 0, data.size());
  FreeHandler(handler);
  EXPECT_EQ(plugin_->GetSaveToDriveBufferHandlerReceiverSizeForTesting(), 0u);
}

TEST_F(PdfViewWebPluginSaveInBlocksToGoogleDriveTest, OriginalInMulipleBlocks) {
  plugin_->SetMaxSaveBufferSizeForTesting(3);

  base::span<const uint8_t> data(TestPDFiumEngine::kLoadedData);
  ASSERT_GT(data.size(), 3u);
  auto [handler, total_file_size] =
      GetSaveDataBufferHandler(pdf::mojom::SaveRequestType::kOriginal);
  EXPECT_EQ(total_file_size, static_cast<uint32_t>(data.size()));
  ReadSaveDataBufferAndExpectResult(handler, data, 0, 3);
  EXPECT_EQ(plugin_->GetSaveToDriveBufferHandlerReceiverSizeForTesting(), 1u);
  ReadSaveDataBufferAndExpectResult(handler, data, 3, data.size() - 3);
  FreeHandler(handler);
  EXPECT_EQ(plugin_->GetSaveToDriveBufferHandlerReceiverSizeForTesting(), 0u);
}

TEST_F(PdfViewWebPluginSaveInBlocksToGoogleDriveTest,
       GetNullptrForDataSizeGreaterThanIntMax) {
  EXPECT_CALL(*engine_ptr_, GetLoadedByteSize)
      .WillRepeatedly(Return(static_cast<uint32_t>(INT_MAX) + 1));
  base::test::TestFuture<pdf::mojom::SaveDataBufferHandlerGetResultPtr> future;
  plugin_->GetSaveDataBufferHandlerForDrive(
      pdf::mojom::SaveRequestType::kOriginal,
      future.GetCallback<pdf::mojom::SaveDataBufferHandlerGetResultPtr>());
  pdf::mojom::SaveDataBufferHandlerGetResultPtr result = future.Take();
  EXPECT_FALSE(result);
  EXPECT_EQ(plugin_->GetSaveToDriveBufferHandlerReceiverSizeForTesting(), 0u);
}

TEST_F(PdfViewWebPluginSaveInBlocksToGoogleDriveTest, EditedInOneBlock) {
  plugin_->EnteredEditMode();

  base::span<const uint8_t> data(TestPDFiumEngine::kSaveData);
  auto [handler, total_file_size] =
      GetSaveDataBufferHandler(pdf::mojom::SaveRequestType::kEdited);
  EXPECT_EQ(total_file_size, static_cast<uint32_t>(data.size()));
  ReadSaveDataBufferAndExpectResult(handler, data, 0, data.size());
  FreeHandler(handler);
  EXPECT_EQ(plugin_->GetSaveToDriveBufferHandlerReceiverSizeForTesting(), 0u);
}

TEST_F(PdfViewWebPluginSaveInBlocksToGoogleDriveTest, EditedInMultipleBlocks) {
  plugin_->EnteredEditMode();
  plugin_->SetMaxSaveBufferSizeForTesting(2);

  base::span<const uint8_t> data(TestPDFiumEngine::kSaveData);
  ASSERT_GT(data.size(), 2u);

  auto [handler, total_file_size] =
      GetSaveDataBufferHandler(pdf::mojom::SaveRequestType::kEdited);
  EXPECT_EQ(total_file_size, static_cast<uint32_t>(data.size()));
  ReadSaveDataBufferAndExpectResult(handler, data, 0, 2);
  EXPECT_EQ(plugin_->GetSaveToDriveBufferHandlerReceiverSizeForTesting(), 1u);

  ReadSaveDataBufferAndExpectResult(handler, data, 2, data.size() - 2);
  FreeHandler(handler);
  EXPECT_EQ(plugin_->GetSaveToDriveBufferHandlerReceiverSizeForTesting(), 0u);
}

TEST_F(PdfViewWebPluginSaveInBlocksToGoogleDriveTest,
       GetEditedInMultipleBlockForDriveAndSaveSimulatenously) {
  plugin_->EnteredEditMode();
  plugin_->SetMaxSaveBufferSizeForTesting(2);

  base::span<const uint8_t> data(TestPDFiumEngine::kSaveData);
  ASSERT_GT(data.size(), 2u);
  ExpectResponse(data, 0, 2, "token-1");
  ExpectResponse(data, 2, data.size() - 2, "token-2");

  auto [handler, total_file_size] =
      GetSaveDataBufferHandler(pdf::mojom::SaveRequestType::kEdited);
  EXPECT_EQ(total_file_size, static_cast<uint32_t>(data.size()));
  ReadSaveDataBufferAndExpectResult(handler, data, 0, 2);
  plugin_->OnMessage(
      CreateRequest(pdf::mojom::SaveRequestType::kEdited, 0, 0, "token-1"));
  EXPECT_EQ(plugin_->GetSaveToDriveBufferHandlerReceiverSizeForTesting(), 1u);
  EXPECT_FALSE(plugin_->IsSaveDataBufferEmptyForTesting());

  ReadSaveDataBufferAndExpectResult(handler, data, 2, data.size() - 2);
  FreeHandler(handler);
  EXPECT_EQ(plugin_->GetSaveToDriveBufferHandlerReceiverSizeForTesting(), 0u);
  EXPECT_FALSE(plugin_->IsSaveDataBufferEmptyForTesting());

  plugin_->OnMessage(CreateRequest(pdf::mojom::SaveRequestType::kEdited, 2,
                                   data.size() - 2, "token-2"));
  EXPECT_EQ(plugin_->GetSaveToDriveBufferHandlerReceiverSizeForTesting(), 0u);
  EXPECT_TRUE(plugin_->IsSaveDataBufferEmptyForTesting());
  pdf_receiver_.FlushForTesting();
}

TEST_F(PdfViewWebPluginSaveInBlocksToGoogleDriveTest,
       MultipleHandlersEditedInMultipleBlocks) {
  plugin_->EnteredEditMode();
  plugin_->SetMaxSaveBufferSizeForTesting(3);

  base::span<const uint8_t> data(TestPDFiumEngine::kSaveData);
  ASSERT_GT(data.size(), 3u);
  base::span<const uint8_t> data2(kSaveDataBuffer);
  ASSERT_GT(data2.size(), 3u);

  EXPECT_CALL(*engine_ptr_, GetSaveData)
      .WillOnce(Return(std::vector<uint8_t>(data.begin(), data.end())))
      .WillOnce(Return(std::vector<uint8_t>(data2.begin(), data2.end())));

  auto [handler, total_file_size] =
      GetSaveDataBufferHandler(pdf::mojom::SaveRequestType::kEdited);
  EXPECT_EQ(total_file_size, data.size());
  ReadSaveDataBufferAndExpectResult(handler, data, 0, 2);
  EXPECT_EQ(plugin_->GetSaveToDriveBufferHandlerReceiverSizeForTesting(), 1u);

  auto [handler2, total_file_size2] =
      GetSaveDataBufferHandler(pdf::mojom::SaveRequestType::kEdited);
  EXPECT_EQ(total_file_size2, data2.size());
  ReadSaveDataBufferAndExpectResult(handler2, data2, 0, 2);
  EXPECT_EQ(plugin_->GetSaveToDriveBufferHandlerReceiverSizeForTesting(), 2u);

  ReadSaveDataBufferAndExpectResult(handler, data, 2, data.size() - 2);
  FreeHandler(handler);
  EXPECT_EQ(plugin_->GetSaveToDriveBufferHandlerReceiverSizeForTesting(), 1u);

  ReadSaveDataBufferAndExpectResult(handler2, data2, 2, data2.size() - 2);
  FreeHandler(handler2);
  EXPECT_EQ(plugin_->GetSaveToDriveBufferHandlerReceiverSizeForTesting(), 0u);
}

#if BUILDFLAG(ENABLE_PDF_INK2)
TEST_F(PdfViewWebPluginSaveInBlocksToGoogleDriveTest,
       AnnotationInEditModeMetrics) {
  DocumentInkTextBoxesMap map;
  EXPECT_CALL(*engine_ptr_, LoadTextAnnotationsFromPdf())
      .WillOnce(Return(std::move(map)));

  plugin_->OnMessage(ParseMessage(R"({
    "type": "getAllTextAnnotations",
    "messageId": "foo",
  })"));

  plugin_->EnteredEditMode();

  base::HistogramTester histograms;
  base::span<const uint8_t> data(TestPDFiumEngine::kSaveData);
  auto [handler, total_file_size] =
      GetSaveDataBufferHandler(pdf::mojom::SaveRequestType::kAnnotation);
  EXPECT_EQ(total_file_size, static_cast<uint32_t>(data.size()));
  ReadSaveDataBufferAndExpectResult(handler, data, 0, data.size());
  FreeHandler(handler);
  EXPECT_EQ(plugin_->GetSaveToDriveBufferHandlerReceiverSizeForTesting(), 0u);

  histograms.ExpectTotalCount("PDF.Ink2TextAnnotationAddedCountOnSave", 0);
  histograms.ExpectTotalCount("PDF.Ink2TextAnnotationRemovedCountOnSave", 0);
}

TEST_F(PdfViewWebPluginSaveInBlocksToGoogleDriveTest,
       AnnotationInEditModeMetricsAdded) {
  DocumentInkTextBoxesMap map;
  EXPECT_CALL(*engine_ptr_, LoadTextAnnotationsFromPdf())
      .WillOnce(Return(std::move(map)));

  plugin_->OnMessage(ParseMessage(R"({
    "type": "getAllTextAnnotations",
    "messageId": "foo",
  })"));

  plugin_->EnteredEditMode();

  EXPECT_CALL(*engine_ptr_, DrawText(_, _, _, _, _, _));
  EXPECT_CALL(*engine_ptr_, RequestThumbnail(_, _, _));

  plugin_->OnMessage(CreateFinishTextAnnotationMessage(
      SampleFinishTextAnnotationData(/*frontend_id=*/10, FontId(123),
                                     /*page_index=*/0, /*pdf_zoom=*/1.0)));

  base::HistogramTester histograms;
  base::span<const uint8_t> data(TestPDFiumEngine::kSaveData);
  auto [handler, total_file_size] =
      GetSaveDataBufferHandler(pdf::mojom::SaveRequestType::kAnnotation);
  EXPECT_EQ(total_file_size, static_cast<uint32_t>(data.size()));
  ReadSaveDataBufferAndExpectResult(handler, data, 0, data.size());
  FreeHandler(handler);
  EXPECT_EQ(plugin_->GetSaveToDriveBufferHandlerReceiverSizeForTesting(), 0u);

  histograms.ExpectUniqueSample("PDF.Ink2TextAnnotationAddedCountOnSave", 1, 1);
  histograms.ExpectTotalCount("PDF.Ink2TextAnnotationRemovedCountOnSave", 0);
}

TEST_F(PdfViewWebPluginSaveInBlocksToGoogleDriveTest,
       AnnotationInEditModeMetricsRemoved) {
  DocumentInkTextBoxesMap map;
  map[0].push_back(InkTextBox(/*id=*/1, SampleInkTextBoxAttributes()));
  EXPECT_CALL(*engine_ptr_, LoadTextAnnotationsFromPdf())
      .WillOnce(Return(std::move(map)));

  plugin_->OnMessage(ParseMessage(R"({
    "type": "getAllTextAnnotations",
    "messageId": "foo",
  })"));

  plugin_->EnteredEditMode();

  base::DictValue finish_data =
      SampleFinishTextAnnotationData(/*frontend_id=*/0, FontId(123),
                                     /*page_index=*/0, /*pdf_zoom=*/1.0);
  finish_data.Set("text", "");

  EXPECT_CALL(*engine_ptr_, UpdateTextActiveAndInvalidate(_, false));
  EXPECT_CALL(*engine_ptr_, RequestThumbnail(_, _, _));

  plugin_->OnMessage(CreateFinishTextAnnotationMessage(std::move(finish_data)));

  base::HistogramTester histograms;
  base::span<const uint8_t> data(TestPDFiumEngine::kSaveData);
  auto [handler, total_file_size] =
      GetSaveDataBufferHandler(pdf::mojom::SaveRequestType::kAnnotation);
  EXPECT_EQ(total_file_size, static_cast<uint32_t>(data.size()));
  ReadSaveDataBufferAndExpectResult(handler, data, 0, data.size());
  FreeHandler(handler);
  EXPECT_EQ(plugin_->GetSaveToDriveBufferHandlerReceiverSizeForTesting(), 0u);

  histograms.ExpectTotalCount("PDF.Ink2TextAnnotationAddedCountOnSave", 0);
  histograms.ExpectUniqueSample("PDF.Ink2TextAnnotationRemovedCountOnSave", 1,
                                1);
}
#endif  // BUILDFLAG(ENABLE_PDF_INK2)

#endif  // BUILDFLAG(ENABLE_PDF_SAVE_TO_DRIVE)

class PdfViewWebPluginSubmitFormTest
    : public PdfViewWebPluginWithoutInitializeTest {
 protected:
  void SubmitForm(const std::string& url, std::string_view form_data = "data") {
    EXPECT_TRUE(plugin_->InitializeForTesting());

    EXPECT_CALL(*client_ptr_, CreateAssociatedURLLoader).WillOnce([this]() {
      auto associated_loader =
          std::make_unique<NiceMock<MockWebAssociatedURLLoader>>();
      EXPECT_CALL(*associated_loader, LoadAsynchronously)
          .WillOnce([this](const blink::WebURLRequest& request,
                           blink::WebAssociatedURLLoaderClient* /*client*/) {
            // TODO(crbug.com/40224475): The `UrlLoader` created by `LoadUrl()`
            // and `SubmitForm()` shouldn't use different ownership semantics.
            // The loader created by `SubmitForm()` is owned by the plugin, and
            // cannot leak past the destruction of the plugin.
            request_.CopyFrom(request);
          });
      return associated_loader;
    });

    plugin_->SubmitForm(url, base::as_byte_span(form_data));
  }

  void SubmitFailingForm(const std::string& url) {
    EXPECT_TRUE(plugin_->InitializeForTesting());

    EXPECT_CALL(*client_ptr_, CreateAssociatedURLLoader).Times(0);

    constexpr std::string_view kFormData = "form data";
    plugin_->SubmitForm(url, base::as_byte_span(kFormData));
  }

  blink::WebURLRequest request_;
};

TEST_F(PdfViewWebPluginSubmitFormTest, RequestMethod) {
  SetUpPluginWithUrl("https://www.example.com/path/to/the.pdf");

  SubmitForm(/*url=*/"");

  EXPECT_EQ(request_.HttpMethod().Utf8(), "POST");
}

TEST_F(PdfViewWebPluginSubmitFormTest, RequestBody) {
  SetUpPluginWithUrl("https://www.example.com/path/to/the.pdf");

  constexpr std::string_view kFormData = "form data";
  SubmitForm(/*url=*/"", kFormData);

  blink::WebHTTPBody::Element element;
  EXPECT_EQ(request_.HttpBody().ElementCount(), 1u);
  ASSERT_TRUE(request_.HttpBody().ElementAt(0, element));
  ASSERT_EQ(element.type, blink::HTTPBodyElementType::kTypeData);
  EXPECT_THAT(element.data.Copy(), testing::ElementsAreArray(kFormData));
}

TEST_F(PdfViewWebPluginSubmitFormTest, RelativeUrl) {
  SetUpPluginWithUrl("https://www.example.com/path/to/the.pdf");

  SubmitForm("relative_endpoint");

  EXPECT_EQ(request_.Url().GetString().Utf8(),
            "https://www.example.com/path/to/relative_endpoint");
}

TEST_F(PdfViewWebPluginSubmitFormTest, NoRelativeUrl) {
  SetUpPluginWithUrl("https://www.example.com/path/to/the.pdf");

  SubmitForm("");

  EXPECT_EQ(request_.Url().GetString().Utf8(),
            "https://www.example.com/path/to/the.pdf");
}

TEST_F(PdfViewWebPluginSubmitFormTest, AbsoluteUrl) {
  SetUpPluginWithUrl("https://a.example.com/path/to/the.pdf");

  SubmitForm("https://b.example.com/relative_endpoint");

  EXPECT_EQ(request_.Url().GetString().Utf8(),
            "https://b.example.com/relative_endpoint");
}

TEST_F(PdfViewWebPluginSubmitFormTest, RelativeUrlInvalidDocumentUrl) {
  SetUpPluginWithUrl("https://www.%B%Ad.com/path/to/the.pdf");

  SubmitFailingForm("relative_endpoint");
}

TEST_F(PdfViewWebPluginSubmitFormTest, AbsoluteUrlInvalidDocumentUrl) {
  SetUpPluginWithUrl("https://www.%B%Ad.com/path/to/the.pdf");

  SubmitFailingForm("https://wwww.example.com");
}

class PdfViewWebPluginPrintTest : public PdfViewWebPluginTest {
 protected:
  void SetUp() override {
    PdfViewWebPluginTest::SetUp();

    // Size must be at least 1 for conversion to `SkMemoryStream`.
    ON_CALL(*engine_ptr_, PrintPages)
        .WillByDefault(Return(std::vector<uint8_t>(1)));

    canvas_.sk_canvas()->SetPrintingMetafile(&metafile_);
  }

  printing::MetafileSkia metafile_;
};

TEST_F(PdfViewWebPluginPrintTest, HighQuality) {
  EXPECT_CALL(*engine_ptr_,
              HasPermission(DocumentPermission::kPrintHighQuality))
      .WillRepeatedly(Return(true));
  EXPECT_CALL(*engine_ptr_, HasPermission(DocumentPermission::kPrintLowQuality))
      .WillRepeatedly(Return(true));
  ASSERT_EQ(static_cast<int>(TestPDFiumEngine::kPageNumber),
            plugin_->PrintBegin(blink::WebPrintParams()));

  EXPECT_CALL(
      *engine_ptr_,
      PrintPages(ElementsAre(0),
                 Field(&blink::WebPrintParams::rasterize_pdf, IsFalse())));
  plugin_->PrintPage(0, canvas_.sk_canvas());
  plugin_->PrintEnd();
}

TEST_F(PdfViewWebPluginPrintTest, HighQualityRasterized) {
  EXPECT_CALL(*engine_ptr_,
              HasPermission(DocumentPermission::kPrintHighQuality))
      .WillRepeatedly(Return(true));
  EXPECT_CALL(*engine_ptr_, HasPermission(DocumentPermission::kPrintLowQuality))
      .WillRepeatedly(Return(true));

  blink::WebPrintParams params;
  params.rasterize_pdf = true;
  ASSERT_EQ(static_cast<int>(TestPDFiumEngine::kPageNumber),
            plugin_->PrintBegin(params));

  EXPECT_CALL(
      *engine_ptr_,
      PrintPages(ElementsAre(0),
                 Field(&blink::WebPrintParams::rasterize_pdf, IsTrue())));
  plugin_->PrintPage(0, canvas_.sk_canvas());
  plugin_->PrintEnd();
}

// Regression test for crbug.com/1307219.
TEST_F(PdfViewWebPluginPrintTest, LowQuality) {
  EXPECT_CALL(*engine_ptr_,
              HasPermission(DocumentPermission::kPrintHighQuality))
      .WillRepeatedly(Return(false));
  EXPECT_CALL(*engine_ptr_, HasPermission(DocumentPermission::kPrintLowQuality))
      .WillRepeatedly(Return(true));
  ASSERT_EQ(static_cast<int>(TestPDFiumEngine::kPageNumber),
            plugin_->PrintBegin(blink::WebPrintParams()));

  EXPECT_CALL(
      *engine_ptr_,
      PrintPages(ElementsAre(0),
                 Field(&blink::WebPrintParams::rasterize_pdf, IsTrue())));
  plugin_->PrintPage(0, canvas_.sk_canvas());
  plugin_->PrintEnd();
}

// Regression test for crbug.com/1307219.
TEST_F(PdfViewWebPluginPrintTest, LowQualityRasterized) {
  EXPECT_CALL(*engine_ptr_,
              HasPermission(DocumentPermission::kPrintHighQuality))
      .WillRepeatedly(Return(false));
  EXPECT_CALL(*engine_ptr_, HasPermission(DocumentPermission::kPrintLowQuality))
      .WillRepeatedly(Return(true));

  blink::WebPrintParams params;
  params.rasterize_pdf = true;
  ASSERT_EQ(static_cast<int>(TestPDFiumEngine::kPageNumber),
            plugin_->PrintBegin(params));

  EXPECT_CALL(
      *engine_ptr_,
      PrintPages(ElementsAre(0),
                 Field(&blink::WebPrintParams::rasterize_pdf, IsTrue())));
  plugin_->PrintPage(0, canvas_.sk_canvas());
  plugin_->PrintEnd();
}

TEST_F(PdfViewWebPluginPrintTest, Disabled) {
  EXPECT_EQ(0, plugin_->PrintBegin(blink::WebPrintParams()));
}

TEST_F(PdfViewWebPluginPrintTest, DisabledRasterized) {
  blink::WebPrintParams params;
  params.rasterize_pdf = true;
  EXPECT_EQ(0, plugin_->PrintBegin(params));
}

class PdfViewWebPluginPrintPreviewTest : public PdfViewWebPluginTest {
 protected:
  void SetUpClient() override {
    EXPECT_CALL(*client_ptr_, GetEmbedderOriginString)
        .WillRepeatedly(Return("chrome://print/"));
  }

  // For use with `base::StringPrintf()`.
  static constexpr char kTestId[] = "1234567890abcdef1234567890abcdef";
};

TEST_F(PdfViewWebPluginPrintPreviewTest, HandleResetPrintPreviewModeMessage) {
  EXPECT_CALL(*client_ptr_, CreateEngine)
      .WillOnce([](PDFiumEngineClient* client,
                   PDFiumFormFiller::ScriptOption script_option) {
        EXPECT_EQ(PDFiumFormFiller::ScriptOption::kNoJavaScript, script_option);

        auto engine = std::make_unique<NiceMock<TestPDFiumEngine>>(client);
        EXPECT_CALL(*engine, ZoomUpdated);
        EXPECT_CALL(*engine, PageOffsetUpdated);
        EXPECT_CALL(*engine, PluginSizeUpdated);
        EXPECT_CALL(*engine, SetGrayscale(false));
        return engine;
      });

  OnMessageWithEngineUpdate(GenerateResetPrintPreviewModeMessage(
      /*id=*/kTestId, /*page_number=*/0, /*grayscale=*/false,
      /*page_count=*/1));
}

TEST_F(PdfViewWebPluginPrintPreviewTest,
       HandleResetPrintPreviewModeMessageForPdf) {
  EXPECT_CALL(*client_ptr_, CreateEngine)
      .WillOnce([](PDFiumEngineClient* client,
                   PDFiumFormFiller::ScriptOption script_option) {
        EXPECT_EQ(PDFiumFormFiller::ScriptOption::kNoJavaScript, script_option);

        return std::make_unique<NiceMock<TestPDFiumEngine>>(client);
      });

  // The UI ID token in the URL is arbitrary.
  // The page index value of -1, AKA `kCompletePDFIndex`, is required for PDFs.
  OnMessageWithEngineUpdate(GenerateResetPrintPreviewModeMessage(
      /*id=*/kTestId, /*page_number=*/-1, /*grayscale=*/false,
      /*page_count=*/0));

  EXPECT_CALL(*client_ptr_, PostMessage).Times(AnyNumber());
  EXPECT_CALL(*client_ptr_, PostMessage(base::test::IsJson(R"({
    "type": "printPreviewLoaded",
  })")));
  plugin_->DocumentLoadComplete();
  pdf_receiver_.FlushForTesting();
}

TEST_F(PdfViewWebPluginPrintPreviewTest,
       HandleResetPrintPreviewModeMessageSetGrayscale) {
  EXPECT_CALL(*client_ptr_, CreateEngine)
      .WillOnce([](PDFiumEngineClient* client,
                   PDFiumFormFiller::ScriptOption /*script_option*/) {
        auto engine = std::make_unique<NiceMock<TestPDFiumEngine>>(client);
        EXPECT_CALL(*engine, SetGrayscale(true));
        return engine;
      });

  OnMessageWithEngineUpdate(GenerateResetPrintPreviewModeMessage(
      /*id=*/kTestId, /*page_number=*/0, /*grayscale=*/true,
      /*page_count=*/1));
}

TEST_F(PdfViewWebPluginPrintPreviewTest, DocumentLoadComplete) {
  OnMessageWithEngineUpdate(GenerateResetPrintPreviewModeMessage(
      /*id=*/kTestId, /*page_number=*/0, /*grayscale=*/false,
      /*page_count=*/1));

  EXPECT_CALL(*client_ptr_, RecordComputedAction("PDF.LoadSuccess"));
  EXPECT_CALL(*client_ptr_, PostMessage);
  EXPECT_CALL(*client_ptr_, PostMessage(base::test::IsJson(R"({
    "type": "formFocusChange",
    "focused": "none",
  })")));
  ExpectUpdateTextInputState(blink::WebTextInputType::kWebTextInputTypeNone);
  EXPECT_CALL(*client_ptr_, PostMessage(base::test::IsJson(R"({
    "type": "printPreviewLoaded",
  })")));
  EXPECT_CALL(*accessibility_data_handler_ptr_, SetAccessibilityDocInfo)
      .Times(0);
  EXPECT_CALL(*client_ptr_, DidStopLoading).Times(0);
  EXPECT_CALL(pdf_host_, UpdateContentRestrictions).Times(0);
  plugin_->DocumentLoadComplete();

  EXPECT_EQ(PdfViewWebPlugin::DocumentLoadState::kComplete,
            plugin_->document_load_state_for_testing());
  pdf_receiver_.FlushForTesting();
}

TEST_F(PdfViewWebPluginPrintPreviewTest,
       DocumentLoadProgressResetByResetPrintPreviewModeMessage) {
  plugin_->DocumentLoadProgress(2, 100);

  OnMessageWithEngineUpdate(GenerateResetPrintPreviewModeMessage(
      /*id=*/kTestId, /*page_number=*/0, /*grayscale=*/false,
      /*page_count=*/2));

  EXPECT_CALL(*client_ptr_, PostMessage(base::test::IsJson(R"({
    "type": "loadProgress",
    "progress": 3.0,
  })")));
  plugin_->DocumentLoadProgress(3, 100);
}

TEST_F(PdfViewWebPluginPrintPreviewTest,
       DocumentLoadProgressNotResetByLoadPreviewPageMessage) {
  OnMessageWithEngineUpdate(GenerateResetPrintPreviewModeMessage(
      /*id=*/kTestId, /*page_number=*/0, /*grayscale=*/false,
      /*page_count=*/2));

  plugin_->DocumentLoadProgress(2, 100);

  plugin_->OnMessage(ParseMessage(base::StringPrintf(R"({
    "type": "loadPreviewPage",
    "url": "chrome-untrusted://print/%s/1/print.pdf",
    "index": 1,
  })",
                                                     kTestId)));

  EXPECT_CALL(*client_ptr_, PostMessage).Times(0);
  plugin_->DocumentLoadProgress(3, 100);
}

TEST_F(PdfViewWebPluginPrintPreviewTest,
       HandleViewportMessageScrollRightToLeft) {
  EXPECT_CALL(*engine_ptr_, ApplyDocumentLayout)
      .WillRepeatedly(Return(gfx::Size(16, 9)));
  EXPECT_CALL(*engine_ptr_, ScrolledToXPosition(14));
  EXPECT_CALL(*engine_ptr_, ScrolledToYPosition(3));

  plugin_->OnMessage(ParseMessage(R"({
    "type": "viewport",
    "userInitiated": false,
    "zoom": 1,
    "layoutOptions": {
      "direction": 1,
      "defaultPageOrientation": 0,
      "twoUpViewEnabled": false,
    },
    "xOffset": -2,
    "yOffset": 3,
    "pinchPhase": 0,
  })"));
}

#if BUILDFLAG(ENABLE_PDF_INK2)
class PdfViewWebPluginInkTest
    : public PdfViewWebPluginTest,
      public testing::WithParamInterface<InkTestVariation> {
 public:
  void SetUp() override {
    // Feature and parameters need to be initialized first as it impacts plugin
    // setup in PdfViewWebPluginTest::SetUp().
    feature_list_.InitAndEnableFeatureWithParameters(
        chrome_pdf::features::kPdfInk2,
        {
            {features::kPdfInk2TextAnnotations.name,
             base::ToString(UseTextAnnotations())},
        });

    PdfViewWebPluginTest::SetUp();
  }

 protected:
  static constexpr gfx::PointF kDiagonalStrokeStartPosition{95.0f, 85.0f};
  static constexpr gfx::PointF kDiagonalStrokeMiddlePosition{72.5f, 65.0f};
  static constexpr gfx::PointF kDiagonalStrokeEndPosition{50.0f, 45.0f};

  struct DiagonalStrokeEvents {
    blink::WebTouchEvent start_event;
    blink::WebTouchEvent move_event1;
    blink::WebTouchEvent move_event2;
    blink::WebTouchEvent end_event;
  };

  bool UseTextAnnotations() const { return GetParam().use_text_annotations; }

  void SetUpWithTrivialInkStrokes() {
    // Set up the engine so the plugin can draw strokes. The exact strokes do
    // not matter.
    EXPECT_CALL(*engine_ptr_, HandleInputEvent).Times(0);
    ON_CALL(*engine_ptr_, GetPageContentsRect)
        .WillByDefault(
            Return(gfx::Rect(/*x=*/0, /*y=*/0, /*width=*/100, /*height=*/50)));
    ON_CALL(*engine_ptr_, GetPageSizeInPoints)
        .WillByDefault(Return(gfx::SizeF(75.0f, 37.5f)));
    ON_CALL(*engine_ptr_, GetThumbnailSize)
        .WillByDefault(Return(gfx::Size(50, 25)));
    ON_CALL(*engine_ptr_, IsPageVisible).WillByDefault(Return(true));

    // Draw some trivial strokes.
    plugin_->OnMessage(
        CreateSetAnnotationModeMessageForTesting(InkAnnotationMode::kDraw));
    TestSendInputEvent(CreateLeftClickWebMouseEventAtPosition({10, 10}),
                       blink::WebInputEventResult::kHandledApplication);
    TestSendInputEvent(CreateLeftClickWebMouseUpEventAtPosition({20, 20}),
                       blink::WebInputEventResult::kHandledApplication);
  }

  void SendThumbnail(std::string_view message_id, const gfx::SizeF& page_size) {
    auto reply = base::DictValue()
                     .Set("type", "getThumbnailReply")
                     .Set("messageId", message_id);
    plugin_->SendThumbnailForTesting(
        std::move(reply), /*page_index=*/0,
        Thumbnail(page_size, /*device_pixel_ratio=*/1));
  }

  // Helper method to test PageIndexFromPoint() and VisiblePageIndexFromPoint().
  // If `expected_visible`, then VisiblePageIndexFromPoint() should have a page
  // index of `expected_page_index`, otherwise -1.
  void TestPageIndexFromPoint(const gfx::PointF& point,
                              int expected_page_index,
                              bool expected_visible) {
    PdfInkModuleClient* ink_module_client =
        plugin_->ink_module_client_for_testing();
    EXPECT_EQ(expected_page_index,
              ink_module_client->PageIndexFromPoint(point));
    EXPECT_EQ(expected_visible ? expected_page_index : -1,
              ink_module_client->VisiblePageIndexFromPoint(point));
  }

  void TestInProgressDraw(
      base::FilePath::StringViewType expected_filename,
      const blink::WebInputEvent& down_event,
      base::span<const blink::WebInputEvent* const> move_events,
      const blink::WebInputEvent& up_event) {
    plugin_->set_in_paint_for_testing(true);
    constexpr gfx::Rect kScreenRect(kCanvasSize);
    constexpr gfx::SizeF kPageSizeInPoints(
        kCanvasSize.width() * kUnitConversionFactorPixelsToPoints,
        kCanvasSize.height() * kUnitConversionFactorPixelsToPoints);
    ON_CALL(*engine_ptr_, GetPageContentsRect)
        .WillByDefault(Return(kScreenRect));
    ON_CALL(*engine_ptr_, GetPageSizeInPoints)
        .WillByDefault(Return(kPageSizeInPoints));
    ON_CALL(*engine_ptr_, GetThumbnailSize)
        .WillByDefault(Return(gfx::Size(50, 50)));
    ON_CALL(*engine_ptr_, IsPageVisible).WillByDefault(Return(true));

    UpdatePluginGeometry(/*device_scale=*/1.0f, kScreenRect);

    // The canvas starts blank.
    canvas_.DrawColor(SK_ColorWHITE);
    plugin_->Paint(canvas_.sk_canvas(), kScreenRect);
    SkBitmap blank_bitmap =
        GenerateExpectedBitmapForPaint(kScreenRect, SK_ColorWHITE);
    EXPECT_TRUE(cc::MatchesBitmap(canvas_.GetBitmap(), blank_bitmap,
                                  cc::ExactPixelComparator()));

    // Start to draw a stroke.  There should not be a call to apply the stroke
    // until drawing is finished.
    EXPECT_CALL(*engine_ptr_, ApplyStroke(_, _, _)).Times(0);
    // The final imaging for a stroke saved to a PDF should match what was final
    // drawn result when it was in-progress.
    TestSendInputEvent(down_event,
                       blink::WebInputEventResult::kHandledApplication);
    for (const auto* move_event : move_events) {
      TestSendInputEvent(*move_event,
                         blink::WebInputEventResult::kHandledApplication);
    }

    // Draw the canvas for the in-progress stroke.
    plugin_->Paint(canvas_.sk_canvas(), kScreenRect);
    const base::FilePath stroked_image_png_file =
        GetInkTestDataFilePath(expected_filename);
    EXPECT_TRUE(
        MatchesPngFile(*canvas_.GetBitmap().asImage(), stroked_image_png_file));

    // Finish the stroke.  After a stroke is finished there is nothing more to
    // be drawn by PdfInkModule, as the completed stroke is provided by a
    // callback to be applied to a PDF page.
    testing::Mock::VerifyAndClearExpectations(engine_ptr_);
    EXPECT_CALL(*engine_ptr_, ApplyStroke(/*page_index=*/0, InkStrokeId(0), _));
    TestSendInputEvent(up_event,
                       blink::WebInputEventResult::kHandledApplication);

    // Updating of `PdfViewWebPlugin::snapshot_` does not happen automatically
    // on the invalidate call, but later after the tasks PaintManager posted
    // have a chance to run.  This means painting uses the last snapshot, which
    // does not include the last Ink stroke.  This results in the most recent
    // stroke disappearing, causing a flash for the user unless the snapshot
    // from the most recent stroke is reused.
    plugin_->Paint(canvas_.sk_canvas(), kScreenRect);
    EXPECT_TRUE(
        MatchesPngFile(*canvas_.GetBitmap().asImage(), stroked_image_png_file));
    EXPECT_TRUE(plugin_->HasInkInputsSnapshotForTesting());

    // Simulate how the snapshot eventually gets updated, after all necessary
    // tasks that normally happen from the PaintManager finally complete.  That
    // results in a blank canvas here for this test, as PdfViewWebPlugin no
    // longer uses the last Ink rendering snapshot for painting, and
    // ApplyStroke() was mocked out so there is nothing to draw from the PDF
    // engine.
    plugin_->UpdateSnapshot(CreateSkiaImageForTesting(
        plugin_->GetPluginRectForTesting().size(), SK_ColorWHITE));
    plugin_->Paint(canvas_.sk_canvas(), kScreenRect);
    EXPECT_TRUE(cc::MatchesBitmap(canvas_.GetBitmap(), blank_bitmap,
                                  cc::ExactPixelComparator()));
    EXPECT_FALSE(plugin_->HasInkInputsSnapshotForTesting());
  }

  // TODO(thestig): Deduplicate with CreateTouchEvent() code in other tests.
  static blink::WebTouchEvent CreateTouchEvent(blink::WebInputEvent::Type type,
                                               const gfx::PointF& point) {
    constexpr int kNoModifiers = 0;
    blink::WebTouchEvent touch_event(
        type, kNoModifiers, blink::WebInputEvent::GetStaticTimeStampForTests());
    touch_event.touches[0].SetPositionInWidget(point);
    touch_event.touches_length = 1;
    return touch_event;
  }

  static blink::WebTouchEvent CreatePenEvent(blink::WebInputEvent::Type type,
                                             const gfx::PointF& point) {
    blink::WebTouchEvent pen_event = CreateTouchEvent(type, point);
    pen_event.touches[0].pointer_type =
        blink::WebPointerProperties::PointerType::kPen;
    return pen_event;
  }

  DiagonalStrokeEvents CreateInputsForDiagonalInProgressStrokeTest(
      bool is_touch) {
    auto create_func = is_touch ? &PdfViewWebPluginInkTest::CreateTouchEvent
                                : &PdfViewWebPluginInkTest::CreatePenEvent;
    DiagonalStrokeEvents events{
        .start_event = create_func(blink::WebInputEvent::Type::kTouchStart,
                                   kDiagonalStrokeStartPosition),
        .move_event1 = create_func(blink::WebInputEvent::Type::kTouchMove,
                                   kDiagonalStrokeMiddlePosition),
        .move_event2 = create_func(blink::WebInputEvent::Type::kTouchMove,
                                   kDiagonalStrokeEndPosition),
        .end_event = create_func(blink::WebInputEvent::Type::kTouchEnd,
                                 kDiagonalStrokeEndPosition)};
    events.move_event1.SetTimeStamp(events.start_event.TimeStamp() +
                                    base::Seconds(0.1f));
    events.move_event2.SetTimeStamp(events.move_event1.TimeStamp() +
                                    base::Seconds(0.5f));
    events.end_event.SetTimeStamp(events.move_event2.TimeStamp() +
                                  base::Seconds(0.1f));
    return events;
  }

 private:
  base::test::ScopedFeatureList feature_list_;
};

TEST_P(PdfViewWebPluginInkTest, Invalidate) {
  plugin_->set_in_paint_for_testing(true);
  EXPECT_EQ(0u, plugin_->deferred_invalidates_for_testing().size());
  SetUpWithTrivialInkStrokes();
  EXPECT_EQ(3u, plugin_->deferred_invalidates_for_testing().size());
}

TEST_P(PdfViewWebPluginInkTest, LoadV2InkPathsForPageAndUpdateShapeActive) {
  const std::map<InkModeledShapeId, ink::PartitionedMesh> kEmptyMap;
  const std::map<InkModeledShapeId, ink::PartitionedMesh> kMap0{
      {InkModeledShapeId(0), ink::PartitionedMesh()},
  };
  const std::map<InkModeledShapeId, ink::PartitionedMesh> kMap1{
      {InkModeledShapeId(1), ink::PartitionedMesh()},
      {InkModeledShapeId(2), ink::PartitionedMesh()},
  };
  const std::map<InkModeledShapeId, ink::PartitionedMesh> kMap2{
      {InkModeledShapeId(3), ink::PartitionedMesh()},
      {InkModeledShapeId(4), ink::PartitionedMesh()},
      {InkModeledShapeId(5), ink::PartitionedMesh()},
  };

  EXPECT_CALL(*engine_ptr_, LoadV2InkPathsForPage(testing::Lt(12)))
      .Times(12)
      .WillOnce(Return(kMap0))
      .WillOnce(Return(kMap1))
      .WillRepeatedly(Return(kEmptyMap));
  EXPECT_CALL(*engine_ptr_, LoadV2InkPathsForPage(12)).WillOnce(Return(kMap2));

  const PdfInkModuleClient::DocumentV2InkPathShapesMap result =
      plugin_->ink_module_client_for_testing()->LoadV2InkPathsFromPdf();
  EXPECT_THAT(
      result,
      ElementsAre(Pair(0, ElementsAre(Pair(InkModeledShapeId(0), _))),
                  Pair(1, ElementsAre(Pair(InkModeledShapeId(1), _),
                                      Pair(InkModeledShapeId(2), _))),
                  Pair(12, ElementsAre(Pair(InkModeledShapeId(3), _),
                                       Pair(InkModeledShapeId(4), _),
                                       Pair(InkModeledShapeId(5), _)))));

  EXPECT_CALL(*engine_ptr_, UpdateShapeActive(0, InkModeledShapeId(0), false));
  plugin_->ink_module_client_for_testing()->UpdateShapeActive(
      /*page_index=*/0, InkModeledShapeId(0),
      /*active=*/false);
}

TEST_P(PdfViewWebPluginInkTest, SendThumbnail) {
  SetUpWithTrivialInkStrokes();

  EXPECT_CALL(*client_ptr_, PostMessage)
      .WillOnce([](const base::DictValue& dict) {
        auto expected = base::test::ParseJsonDict(R"({
            "type": "getThumbnailReply",
            "messageId": "foo",
            "width": 216,
            "height": 108,
        })");
        EXPECT_THAT(dict, base::test::DictionaryHasValues(expected));

        // Test `dict` contains the image data, but not the exact value.
        const auto* blob = dict.FindBlob("imageData");
        ASSERT_TRUE(blob);
        EXPECT_FALSE(blob->empty());
      });
  SendThumbnail(/*message_id=*/"foo", /*page_size=*/{50, 25});
}

TEST_P(PdfViewWebPluginInkTest, SendThumbnailWithNoStrokes) {
  EXPECT_CALL(*client_ptr_, PostMessage)
      .WillOnce([](const base::DictValue& dict) {
        auto expected = base::test::ParseJsonDict(R"({
            "type": "getThumbnailReply",
            "messageId": "foo",
            "width": 216,
            "height": 108,
        })");
        EXPECT_THAT(dict, base::test::DictionaryHasValues(expected));

        // Test `dict` contains the image data, but not the exact value.
        const auto* blob = dict.FindBlob("imageData");
        ASSERT_TRUE(blob);
        EXPECT_FALSE(blob->empty());
      });
  SendThumbnail(/*message_id=*/"foo", /*page_size=*/{50, 25});
}

TEST_P(PdfViewWebPluginInkTest, GetCursor) {
  plugin_->set_cursor_type_for_testing(ui::mojom::CursorType::kPointer);
  EXPECT_EQ(ui::mojom::CursorType::kPointer,
            plugin_->ink_module_client_for_testing()->GetCursor().type());

  plugin_->set_cursor_type_for_testing(ui::mojom::CursorType::kIBeam);
  EXPECT_EQ(ui::mojom::CursorType::kIBeam,
            plugin_->ink_module_client_for_testing()->GetCursor().type());
}

TEST_P(PdfViewWebPluginInkTest, UpdateCursor) {
  UpdatePluginGeometryWithoutWaiting(2.0f, {0, 0, 20, 20});

  ON_CALL(*engine_ptr_, HandleInputEvent)
      .WillByDefault([this](const blink::WebInputEvent& event) -> bool {
        plugin_->UpdateCursor(ui::mojom::CursorType::kPointer);
        return false;
      });

  blink::WebMouseEvent mouse_event;
  mouse_event.SetType(blink::WebInputEvent::Type::kMouseMove);
  mouse_event.SetPositionInWidget(10.0f, 20.0f);

  ui::Cursor cursor;
  EXPECT_EQ(ui::mojom::CursorType::kNull, cursor.type());
  cursor =
      TestSendInputEvent(mouse_event, blink::WebInputEventResult::kNotHandled);
  EXPECT_EQ(ui::mojom::CursorType::kPointer, cursor.type());

  plugin_->OnMessage(
      CreateSetAnnotationModeMessageForTesting(InkAnnotationMode::kDraw));
  cursor =
      TestSendInputEvent(mouse_event, blink::WebInputEventResult::kNotHandled);
  EXPECT_EQ(ui::mojom::CursorType::kCustom, cursor.type());

  plugin_->OnMessage(
      CreateSetAnnotationModeMessageForTesting(InkAnnotationMode::kOff));
  cursor =
      TestSendInputEvent(mouse_event, blink::WebInputEventResult::kNotHandled);
  EXPECT_EQ(ui::mojom::CursorType::kPointer, cursor.type());
}

TEST_P(PdfViewWebPluginInkTest, ClearSelection) {
  EXPECT_CALL(*engine_ptr_, ClearTextSelection);
  plugin_->ink_module_client_for_testing()->ClearSelection();
}

TEST_P(PdfViewWebPluginInkTest, ExtendSelectionByPoint) {
  EXPECT_CALL(*engine_ptr_, ExtendSelectionByPoint(gfx::PointF(10.5f, 20.1f)));
  plugin_->ink_module_client_for_testing()->ExtendSelectionByPoint(
      gfx::PointF(10.5f, 20.1f));
}

TEST_P(PdfViewWebPluginInkTest, GetCanonicalToPdfTransform) {
  static constexpr auto kTransform = gfx::Transform::MakeScale(3.0f);
  ON_CALL(*engine_ptr_, GetCanonicalToPdfTransform)
      .WillByDefault(Return(kTransform));
  EXPECT_EQ(kTransform, plugin_->ink_module_client_for_testing()
                            ->GetCanonicalToPdfTransform(/*page_index=*/0));
}

TEST_P(PdfViewWebPluginInkTest, GetPageSizeInPoints) {
  SetUpWithTrivialInkStrokes();
  EXPECT_EQ(gfx::SizeF(75.0f, 37.5f),
            plugin_->ink_module_client_for_testing()->GetPageSizeInPoints(
                /*page_index=*/0));
}

TEST_P(PdfViewWebPluginInkTest, GetSelectionRectMap) {
  static constexpr PdfRect kRect(10, 20, 40, 60);
  PdfInkModuleClient::SelectionRectMap selection_map{{0, {kRect}}};
  ON_CALL(*engine_ptr_, GetSelectionRectMap)
      .WillByDefault(Return(selection_map));
  EXPECT_THAT(plugin_->ink_module_client_for_testing()->GetSelectionRectMap(),
              ElementsAre(Pair(0, ElementsAre(kRect))));
}

TEST_P(PdfViewWebPluginInkTest, GetThumbnailSize) {
  SetUpWithTrivialInkStrokes();
  EXPECT_EQ(gfx::Size(50, 25),
            plugin_->ink_module_client_for_testing()->GetThumbnailSize(
                /*page_index=*/0));
}

TEST_P(PdfViewWebPluginInkTest, GetZoom) {
  // Demonstrate that default zoom is identity.
  EXPECT_EQ(1.0f, plugin_->ink_module_client_for_testing()->GetZoom());

  // Verify that changing the plugin zoom shows effect.
  EXPECT_CALL(*engine_ptr_, ZoomUpdated(2.0f));
  plugin_->OnMessage(ParseMessage(R"({
    "type": "viewport",
    "userInitiated": false,
    "zoom": 2,
    "layoutOptions": {
      "direction": 0,
      "defaultPageOrientation": 0,
      "twoUpViewEnabled": false,
    },
    "xOffset": 0,
    "yOffset": 0,
    "pinchPhase": 0,
  })"));
  EXPECT_EQ(2.0f, plugin_->ink_module_client_for_testing()->GetZoom());

  // Verify that changing the platform device scale shows effect.
  ON_CALL(*client_ptr_, DeviceScaleFactor).WillByDefault(Return(1.25f));
  EXPECT_CALL(*engine_ptr_, ZoomUpdated(2.5f));
  constexpr gfx::Rect kWindowRect(12, 24, 36, 48);
  plugin_->UpdateGeometry(kWindowRect, kWindowRect, kWindowRect,
                          /*is_visible=*/true);
  EXPECT_EQ(2.5f, plugin_->ink_module_client_for_testing()->GetZoom());
}

TEST_P(PdfViewWebPluginInkTest, RequestThumbnail) {
  EXPECT_CALL(*engine_ptr_, RequestThumbnail)
      .WillOnce([](int page_index, float device_pixel_ratio,
                   SendThumbnailCallback send_callback) {
        EXPECT_EQ(0, page_index);
        EXPECT_EQ(1.0f, device_pixel_ratio);
        std::move(send_callback)
            .Run(Thumbnail(gfx::SizeF(612, 792), device_pixel_ratio));
      });

  base::test::TestFuture<Thumbnail> future;
  plugin_->ink_module_client_for_testing()->RequestThumbnail(
      /*page_index=*/0, future.GetCallback());
  ASSERT_TRUE(future.Wait());
  Thumbnail thumbnail = future.Take();
  EXPECT_EQ(gfx::Size(108, 140), thumbnail.image_size());
  EXPECT_EQ(1.0f, thumbnail.device_pixel_ratio());
}

TEST_P(PdfViewWebPluginInkTest, AddUpdateDiscardStroke) {
  const PdfInkBrush kBrush(PdfInkBrush::Type::kPen, SK_ColorRED, /*size=*/4.0f);
  constexpr InkStrokeId kStrokeId(1);
  constexpr int kPageIndex = 0;

  EXPECT_CALL(*engine_ptr_, ApplyStroke(kPageIndex, kStrokeId, _));

  const ink::Stroke kStroke(kBrush.ink_brush());
  plugin_->ink_module_client_for_testing()->StrokeAdded(kPageIndex, kStrokeId,
                                                        kStroke);

  EXPECT_CALL(*engine_ptr_, UpdateStrokeActive(kPageIndex, kStrokeId, false));

  plugin_->ink_module_client_for_testing()->UpdateStrokeActive(
      kPageIndex, kStrokeId,
      /*active=*/false);

  EXPECT_CALL(*engine_ptr_, DiscardStroke(kPageIndex, kStrokeId));

  plugin_->ink_module_client_for_testing()->DiscardStroke(kPageIndex,
                                                          kStrokeId);
}

TEST_P(PdfViewWebPluginInkTest, PageIndexFromPoint) {
  ON_CALL(*engine_ptr_, GetPageContentsRect)
      .WillByDefault([](int page_index) -> gfx::Rect {
        // Uniform 80x180 page sizes, with a `kVerticalEmptySpace` gap above
        // every page.
        constexpr int kVerticalEmptySpace = 20;
        constexpr int kHeight = 180;
        int y = kHeight * page_index + kVerticalEmptySpace * (page_index + 1);
        return gfx::Rect(/*x=*/10, /*y=*/y, /*width=*/80, /*height=*/kHeight);
      });

  // Top-left corner of screen.
  constexpr gfx::PointF kScreenTopLeftCorner(0.0f, 0.0f);
  // Top-left corner of first page.
  constexpr gfx::PointF kPage0TopLeftCorner(10.0f, 20.0f);
  // Just outside the top-left corner of first page.
  constexpr gfx::PointF kPage0OutsideTopLeftCorner(10.0f, 19.938f);
  // Bottom-right corner of first page.
  constexpr gfx::PointF kPage0BottomRightCorner(89.999f, 199.0f);
  // Just outside the bottom-right corner of first page.
  constexpr gfx::PointF kPage0OutsideBottomRightCorner(90.0f, 199.0f);
  // Gap between first and second page.
  constexpr gfx::PointF kPage0Page1Gap(50.0f, 201.0f);
  // Top of second page.
  constexpr gfx::PointF kPage1Top(50.0f, 220.0f);
  // Middle of last page.
  constexpr gfx::PointF kPage12Middle(50.0f, 2510.0f);
  // Bottom of last page.
  constexpr gfx::PointF kPage12Bottom(60.0f, 2599.0f);
  // Beyond the last page.
  constexpr gfx::PointF kPageBelowLast(60.0f, 2700.0f);

  // Start with the first 2 pages visible.
  ON_CALL(*engine_ptr_, IsPageVisible)
      .WillByDefault([](int page_index) -> bool {
        return page_index >= 0 && page_index <= 1;
      });

  TestPageIndexFromPoint(kScreenTopLeftCorner, -1, /*expected_visible=*/false);
  TestPageIndexFromPoint(kPage0TopLeftCorner, 0, /*expected_visible=*/true);
  TestPageIndexFromPoint(kPage0OutsideTopLeftCorner, -1,
                         /*expected_visible=*/false);
  TestPageIndexFromPoint(kPage0BottomRightCorner, 0, true);
  TestPageIndexFromPoint(kPage0OutsideBottomRightCorner, -1,
                         /*expected_visible=*/false);
  TestPageIndexFromPoint(kPage0Page1Gap, -1, /*expected_visible=*/false);
  TestPageIndexFromPoint(kPage1Top, 1, /*expected_visible=*/true);
  TestPageIndexFromPoint(kPage12Middle, 12, /*expected_visible=*/false);
  TestPageIndexFromPoint(kPage12Bottom, 12, /*expected_visible=*/false);
  TestPageIndexFromPoint(kPageBelowLast, -1, /*expected_visible=*/false);

  // Change the visible page to the last page.
  ON_CALL(*engine_ptr_, IsPageVisible)
      .WillByDefault([](int page_index) -> bool { return page_index == 12; });

  TestPageIndexFromPoint(kScreenTopLeftCorner, -1, /*expected_visible=*/false);
  TestPageIndexFromPoint(kPage0TopLeftCorner, 0, /*expected_visible=*/false);
  TestPageIndexFromPoint(kPage0OutsideTopLeftCorner, -1,
                         /*expected_visible=*/false);
  TestPageIndexFromPoint(kPage0BottomRightCorner, 0,
                         /*expected_visible=*/false);
  TestPageIndexFromPoint(kPage0OutsideBottomRightCorner, -1,
                         /*expected_visible=*/false);
  TestPageIndexFromPoint(kPage0Page1Gap, -1, /*expected_visible=*/false);
  TestPageIndexFromPoint(kPage1Top, 1, /*expected_visible=*/false);
  TestPageIndexFromPoint(kPage12Middle, 12, /*expected_visible=*/true);
  TestPageIndexFromPoint(kPage12Bottom, 12, /*expected_visible=*/true);
  TestPageIndexFromPoint(kPageBelowLast, -1, /*expected_visible=*/false);
}

TEST_P(PdfViewWebPluginInkTest, IsSelectableTextOrLinkArea) {
  ON_CALL(*engine_ptr_, IsSelectableTextOrLinkArea(gfx::PointF(10.1f, 10.2f)))
      .WillByDefault(Return(true));
  EXPECT_TRUE(
      plugin_->ink_module_client_for_testing()->IsSelectableTextOrLinkArea(
          gfx::PointF(10.1f, 10.2f)));
  EXPECT_FALSE(
      plugin_->ink_module_client_for_testing()->IsSelectableTextOrLinkArea(
          gfx::PointF(20.1f, 20.2f)));
}

TEST_P(PdfViewWebPluginInkTest, OnTextOrLinkAreaClick) {
  EXPECT_CALL(*engine_ptr_, OnTextOrLinkAreaClick(gfx::PointF(1.1f, 2.2f),
                                                  /*click_count=*/2));
  plugin_->ink_module_client_for_testing()->OnTextOrLinkAreaClick(
      gfx::PointF(1.1f, 2.2f), /*click_count=*/2);
}

TEST_P(PdfViewWebPluginInkTest, AnnotationModeSetsFormAndClearsText) {
  EXPECT_CALL(*engine_ptr_, SetFormHighlight(false));
  EXPECT_CALL(*engine_ptr_, ClearTextSelection());

  plugin_->OnMessage(
      CreateSetAnnotationModeMessageForTesting(InkAnnotationMode::kDraw));
  EXPECT_TRUE(plugin_->IsInAnnotationMode());

  EXPECT_CALL(*engine_ptr_, SetFormHighlight(true));

  plugin_->OnMessage(
      CreateSetAnnotationModeMessageForTesting(InkAnnotationMode::kOff));
  EXPECT_FALSE(plugin_->IsInAnnotationMode());
}

TEST_P(PdfViewWebPluginInkTest, DrawInProgressStroke) {
  plugin_->OnMessage(
      CreateSetAnnotationModeMessageForTesting(InkAnnotationMode::kDraw));
  blink::WebMouseEvent start_event =
      CreateLeftClickWebMouseEventAtPosition(kDiagonalStrokeStartPosition);
  blink::WebMouseEvent move_event =
      CreateLeftClickWebMouseMoveEventAtPosition(kDiagonalStrokeEndPosition);
  blink::WebMouseEvent end_event =
      CreateLeftClickWebMouseUpEventAtPosition(kDiagonalStrokeEndPosition);
  TestInProgressDraw(
      /*expected_filename=*/FILE_PATH_LITERAL("diagonal_stroke.png"),
      start_event, {&move_event}, end_event);
}

TEST_P(PdfViewWebPluginInkTest, DrawInProgressStrokeWithTouchWithoutPressure) {
  plugin_->OnMessage(
      CreateSetAnnotationModeMessageForTesting(InkAnnotationMode::kDraw));
  DiagonalStrokeEvents events =
      CreateInputsForDiagonalInProgressStrokeTest(/*is_touch=*/true);
  TestInProgressDraw(
      /*expected_filename=*/FILE_PATH_LITERAL(
          "diagonal_stroke_pen_without_pressure.png"),
      events.start_event, {&events.move_event1, &events.move_event2},
      events.end_event);
}

TEST_P(PdfViewWebPluginInkTest,
       DrawInProgressStrokeWithTouchWithIgnoredPressure) {
  plugin_->OnMessage(
      CreateSetAnnotationModeMessageForTesting(InkAnnotationMode::kDraw));
  DiagonalStrokeEvents events =
      CreateInputsForDiagonalInProgressStrokeTest(/*is_touch=*/true);
  events.start_event.touches[0].force = 0.1f;
  events.move_event1.touches[0].force = 1.0f;
  events.move_event2.touches[0].force = 1.0f;
  events.end_event.touches[0].force = 0.1f;
  TestInProgressDraw(
      /*expected_filename=*/FILE_PATH_LITERAL(
          "diagonal_stroke_pen_without_pressure.png"),
      events.start_event, {&events.move_event1, &events.move_event2},
      events.end_event);
}

TEST_P(PdfViewWebPluginInkTest, DrawInProgressStrokeWithPenWithoutPressure) {
  plugin_->OnMessage(
      CreateSetAnnotationModeMessageForTesting(InkAnnotationMode::kDraw));
  DiagonalStrokeEvents events =
      CreateInputsForDiagonalInProgressStrokeTest(/*is_touch=*/false);
  TestInProgressDraw(
      /*expected_filename=*/FILE_PATH_LITERAL(
          "diagonal_stroke_pen_without_pressure.png"),
      events.start_event, {&events.move_event1, &events.move_event2},
      events.end_event);
}

TEST_P(PdfViewWebPluginInkTest, DrawInProgressStrokeWithPenWithPressure) {
  plugin_->OnMessage(
      CreateSetAnnotationModeMessageForTesting(InkAnnotationMode::kDraw));
  DiagonalStrokeEvents events =
      CreateInputsForDiagonalInProgressStrokeTest(/*is_touch=*/false);
  events.start_event.touches[0].force = 0.1f;
  events.move_event1.touches[0].force = 1.0f;
  events.move_event2.touches[0].force = 1.0f;
  events.end_event.touches[0].force = 0.1f;

  TestInProgressDraw(
      /*expected_filename=*/FILE_PATH_LITERAL(
          "diagonal_stroke_pen_with_pressure.png"),
      events.start_event, {&events.move_event1, &events.move_event2},
      events.end_event);
}

TEST_P(PdfViewWebPluginInkTest, AddFont) {
  static constexpr FontId kFontId(1);
  static constexpr auto kSerializedTypeface =
      std::to_array<const uint8_t>({1, 2, 3});

  EXPECT_CALL(*engine_ptr_,
              AddFont(kFontId, "test",
                      Matcher<base::span<const uint8_t>>(kSerializedTypeface)));

  plugin_->ink_module_client_for_testing()->AddFont(kFontId, "test",
                                                    kSerializedTypeface);
}

TEST_P(PdfViewWebPluginInkTest, DiscardText) {
  static constexpr InkTextId kTextId(1);

  EXPECT_CALL(*engine_ptr_, DiscardText(kTextId));

  plugin_->ink_module_client_for_testing()->DiscardText(kTextId);
}

TEST_P(PdfViewWebPluginInkTest, DrawText) {
  static constexpr int kPageIndex = 0;
  static constexpr InkTextId kTextId(1);
  static constexpr double kZoom = 1.5;
  static constexpr float kAscent = 5;

  EXPECT_CALL(*engine_ptr_,
              DrawText(kPageIndex, kTextId, _, kAscent, kZoom, _));

  const InkTextBoxAttributes text_box_attributes{
      .rect = gfx::RectF(20.0f, 20.0f, 100.0f, 100.0f),
      .color = SK_ColorBLACK,
      .css_font_size = 10.0f,
      .typeface = TextTypeface::kSansSerif,
      .alignment = TextAlignment::kLeft,
      .orientation = 0,
      .viewport_orientation = PageOrientation::kOriginal,
      .is_bold = true,
      .is_italic = false,
      .text = "Hello",
  };
  plugin_->ink_module_client_for_testing()->DrawText(
      kPageIndex, kTextId, {}, kAscent, kZoom, text_box_attributes);
}

TEST_P(PdfViewWebPluginInkTest, UpdateTextActiveAndInvalidate) {
  static constexpr InkTextId kTextId(1);

  EXPECT_CALL(*engine_ptr_,
              UpdateTextActiveAndInvalidate(TextId(kTextId), /*active=*/false));

  plugin_->ink_module_client_for_testing()->UpdateTextActiveAndInvalidate(
      kTextId, /*active=*/false);
}

class PdfViewWebPluginInkTextHighlightTest : public PdfViewWebPluginInkTest {
 public:
  static constexpr TestAnnotationBrushMessageParams kLightGreenBrushParams{
      SkColorSetRGB(0x34, 0xA8, 0x53),
      /*size=*/4.5};
  static constexpr gfx::PointF kStartTextPosition{55.0f, 60.0f};
  static constexpr gfx::PointF kEndTextPosition{75.0f, 65.0f};

  // Sets up test expectations for clicking on `kStartTextPosition` and moving
  // to `kEndTextPosition` with a mouse.
  void SetUpMouseDownMoveTextTestExpectations() {
    // The start position and end position are in screen coordinates, while the
    // values passed to and returned from PDFiumEngine are in PDF coordinates.
    // However, in this test fixture, the transforms from canonical coordinates
    // to screen and PDF coordinates are all identity transforms, so the test
    // cases can use the same values regardless of the coordinates system.
    EXPECT_CALL(*engine_ptr_,
                OnTextOrLinkAreaClick(gfx::PointF(5.0f, 60.0f), 1));
    EXPECT_CALL(*engine_ptr_,
                ExtendSelectionByPoint(gfx::PointF(25.0f, 65.0f)));
    PdfInkModuleClient::SelectionRectMap mock_selection_rect_map{
        {0, {PdfRect(5, 60, 25, 65)}}};
    ON_CALL(*engine_ptr_, GetSelectionRectMap())
        .WillByDefault(Return(mock_selection_rect_map));
    ON_CALL(*engine_ptr_, IsSelectableTextOrLinkArea(_))
        .WillByDefault(Return(true));
  }
};

TEST_P(PdfViewWebPluginInkTextHighlightTest, SelectionDoesNotChange) {
  constexpr gfx::Rect kScreenRect(kCanvasSize);
  ON_CALL(*engine_ptr_, GetPageContentsRect).WillByDefault(Return(kScreenRect));
  UpdatePluginGeometry(/*device_scale=*/1.0f, kScreenRect);

  // Enter annotation mode and select the highlighter.
  plugin_->OnMessage(
      CreateSetAnnotationModeMessageForTesting(InkAnnotationMode::kDraw));
  plugin_->OnMessage(CreateSetAnnotationBrushMessageForTesting(
      "highlighter", &kLightGreenBrushParams));

  SetUpMouseDownMoveTextTestExpectations();
  TestSendInputEvent(CreateLeftClickWebMouseEventAtPosition(kStartTextPosition),
                     blink::WebInputEventResult::kHandledApplication);
  TestSendInputEvent(
      CreateLeftClickWebMouseMoveEventAtPosition(kEndTextPosition),
      blink::WebInputEventResult::kHandledApplication);

  EXPECT_CALL(*client_ptr_, TextSelectionChanged(_, _, _)).Times(0);

  plugin_->SetSelectedText("text");

  EXPECT_CALL(pdf_host_, SelectionChanged(_, _, _, _)).Times(0);

  plugin_->SelectionChanged({-10, -20, 30, 40}, {50, 60, 70, 80});
  pdf_receiver_.FlushForTesting();
}

TEST_P(PdfViewWebPluginInkTextHighlightTest, DrawInProgressTextHighlight) {
  // Enter annotation mode and select the highlighter.
  plugin_->OnMessage(
      CreateSetAnnotationModeMessageForTesting(InkAnnotationMode::kDraw));
  plugin_->OnMessage(CreateSetAnnotationBrushMessageForTesting(
      "highlighter", &kLightGreenBrushParams));

  SetUpMouseDownMoveTextTestExpectations();

  static constexpr gfx::PointF kStartPosition{55.0f, 60.0f};
  static constexpr gfx::PointF kEndPosition{75.0f, 65.0f};
  blink::WebMouseEvent start_event =
      CreateLeftClickWebMouseEventAtPosition(kStartPosition);
  blink::WebMouseEvent move_event =
      CreateLeftClickWebMouseMoveEventAtPosition(kEndPosition);
  blink::WebMouseEvent end_event =
      CreateLeftClickWebMouseUpEventAtPosition(kEndPosition);
  TestInProgressDraw(
      /*expected_filename=*/FILE_PATH_LITERAL("text_highlight_stroke.png"),
      start_event, {&move_event}, end_event);
}

TEST_P(PdfViewWebPluginInkTextHighlightTest,
       DrawInProgressTextHighlightMultipage) {
  plugin_->set_in_paint_for_testing(true);

  // Plugin size is 100x100 (kCanvasSize).
  constexpr gfx::Rect kPluginRect(kCanvasSize);
  UpdatePluginGeometry(/*device_scale=*/1.0f, kPluginRect);
  SetDocumentDimensions(kCanvasSize);

  // Page 0: top half, Page 1: bottom half.
  constexpr gfx::Rect kPage0Rect(0, 0, 100, 50);
  constexpr gfx::Rect kPage1Rect(0, 50, 100, 50);

  ON_CALL(*engine_ptr_, GetPageContentsRect)
      .WillByDefault([kPage0Rect, kPage1Rect](int page_index) {
        if (page_index == 0) {
          return kPage0Rect;
        }
        if (page_index == 1) {
          return kPage1Rect;
        }
        return gfx::Rect();
      });
  ON_CALL(*engine_ptr_, GetPageSizeInPoints)
      .WillByDefault(Return(gfx::SizeF(100.0f, 50.0f)));
  ON_CALL(*engine_ptr_, GetThumbnailSize)
      .WillByDefault(Return(gfx::Size(50, 25)));
  ON_CALL(*engine_ptr_, IsPageVisible).WillByDefault(Return(true));

  // Enter annotation mode and select the highlighter.
  plugin_->OnMessage(
      CreateSetAnnotationModeMessageForTesting(InkAnnotationMode::kDraw));
  plugin_->OnMessage(CreateSetAnnotationBrushMessageForTesting(
      "highlighter", &kLightGreenBrushParams));

  // The canvas starts blank.
  canvas_.DrawColor(SK_ColorWHITE);
  plugin_->Paint(canvas_.sk_canvas(), kPluginRect);
  SkBitmap blank_bitmap =
      GenerateExpectedBitmapForPaint(kPluginRect, SK_ColorWHITE);
  EXPECT_TRUE(cc::MatchesBitmap(canvas_.GetBitmap(), blank_bitmap,
                                cc::ExactPixelComparator()));

  constexpr gfx::PointF kStartPosition{50.0f, 25.0f};
  constexpr gfx::PointF kEndPosition{50.0f, 75.0f};

  EXPECT_CALL(*engine_ptr_, OnTextOrLinkAreaClick(kStartPosition, 1));
  EXPECT_CALL(*engine_ptr_, ExtendSelectionByPoint(kEndPosition));

  // Mock the selection rect map.
  PdfInkModuleClient::SelectionRectMap mock_selection_rect_map{
      {0, {PdfRect(50, 15, 100, 35)}}, {1, {PdfRect(0, 15, 50, 35)}}};
  ON_CALL(*engine_ptr_, GetSelectionRectMap())
      .WillByDefault(Return(mock_selection_rect_map));
  ON_CALL(*engine_ptr_, IsSelectableTextOrLinkArea(_))
      .WillByDefault(Return(true));

  // Start to draw a stroke.  There should not be a call to apply the stroke
  // until drawing is finished.
  EXPECT_CALL(*engine_ptr_, ApplyStroke(_, _, _)).Times(0);

  // Send mouse down on Page 0.
  TestSendInputEvent(CreateLeftClickWebMouseEventAtPosition(kStartPosition),
                     blink::WebInputEventResult::kHandledApplication);
  // Send mouse move to Page 1.
  TestSendInputEvent(CreateLeftClickWebMouseMoveEventAtPosition(kEndPosition),
                     blink::WebInputEventResult::kHandledApplication);

  // Draw the canvas for the in-progress stroke.
  plugin_->Paint(canvas_.sk_canvas(), kPluginRect);
  const base::FilePath stroked_image_png_file = GetInkTestDataFilePath(
      FILE_PATH_LITERAL("text_highlight_multipage_stroke.png"));
  EXPECT_TRUE(
      MatchesPngFile(*canvas_.GetBitmap().asImage(), stroked_image_png_file));

  // Finish the stroke.
  testing::Mock::VerifyAndClearExpectations(engine_ptr_);
  EXPECT_CALL(*engine_ptr_, ApplyStroke(0, InkStrokeId(0), _));
  EXPECT_CALL(*engine_ptr_, ApplyStroke(1, InkStrokeId(1), _));

  blink::WebMouseEvent end_event =
      CreateLeftClickWebMouseUpEventAtPosition(kEndPosition);
  TestSendInputEvent(end_event,
                     blink::WebInputEventResult::kHandledApplication);

  // Verify the snapshot.
  plugin_->Paint(canvas_.sk_canvas(), kPluginRect);
  EXPECT_TRUE(
      MatchesPngFile(*canvas_.GetBitmap().asImage(), stroked_image_png_file));
  EXPECT_TRUE(plugin_->HasInkInputsSnapshotForTesting());

  plugin_->UpdateSnapshot(CreateSkiaImageForTesting(
      plugin_->GetPluginRectForTesting().size(), SK_ColorWHITE));
  plugin_->Paint(canvas_.sk_canvas(), kPluginRect);
  EXPECT_TRUE(cc::MatchesBitmap(canvas_.GetBitmap(), blank_bitmap,
                                cc::ExactPixelComparator()));
  EXPECT_FALSE(plugin_->HasInkInputsSnapshotForTesting());
}

using PdfViewWebPluginInk2SaveTest = PdfViewWebPluginSaveTest;

TEST_F(PdfViewWebPluginInk2SaveTest, AnnotationInNonEditMode) {
  // Modify the document with an Ink stroke.
  plugin_->ink_module_client_for_testing()->StrokeFinished(/*modified=*/true);

  EXPECT_EQ(blink::WebTextInputType::kWebTextInputTypeNone,
            plugin_->GetPluginTextInputType());

  base::Value expected_response = base::test::ParseJson(R"({
    "type": "saveData",
    "token": "annotation-in-non-edit-mode",
    "fileName": "example.pdf",
    "editModeForTesting": false,
  })");
  AddDataToValue(base::span(TestPDFiumEngine::kSaveData), expected_response);
  EXPECT_CALL(*client_ptr_, PostMessage(base::test::IsJson(expected_response)));

  plugin_->OnMessage(ParseMessage(R"({
    "type": "save",
    "saveRequestType": "ANNOTATION",
    "token": "annotation-in-non-edit-mode",
  })"));

  pdf_receiver_.FlushForTesting();
}

TEST_F(PdfViewWebPluginInk2SaveTest, AnnotationInEditMode) {
  // Modify the document with an Ink stroke.
  plugin_->ink_module_client_for_testing()->StrokeFinished(/*modified=*/true);

  plugin_->EnteredEditMode();
  pdf_receiver_.FlushForTesting();

  EXPECT_EQ(blink::WebTextInputType::kWebTextInputTypeNone,
            plugin_->GetPluginTextInputType());

  base::Value expected_response = base::test::ParseJson(R"({
    "type": "saveData",
    "token": "annotation-in-edit-mode",
    "fileName": "example.pdf",
    "editModeForTesting": true,
  })");
  AddDataToValue(base::span(TestPDFiumEngine::kSaveData), expected_response);
  EXPECT_CALL(*client_ptr_, PostMessage(base::test::IsJson(expected_response)));

  plugin_->OnMessage(ParseMessage(R"({
    "type": "save",
    "saveRequestType": "ANNOTATION",
    "token": "annotation-in-edit-mode",
  })"));

  pdf_receiver_.FlushForTesting();
}

TEST_F(PdfViewWebPluginInk2SaveTest, AnnotationInEditModeMetrics) {
  DocumentInkTextBoxesMap map;
  EXPECT_CALL(*engine_ptr_, LoadTextAnnotationsFromPdf())
      .WillOnce(Return(std::move(map)));

  plugin_->OnMessage(ParseMessage(R"({
    "type": "getAllTextAnnotations",
    "messageId": "foo",
  })"));

  plugin_->EnteredEditMode();
  pdf_receiver_.FlushForTesting();

  base::Value expected_response = base::test::ParseJson(R"({
    "type": "saveData",
    "token": "annotation-in-edit-mode",
    "fileName": "example.pdf",
    "editModeForTesting": true,
  })");
  AddDataToValue(base::span(TestPDFiumEngine::kSaveData), expected_response);
  EXPECT_CALL(*client_ptr_, PostMessage(base::test::IsJson(expected_response)));

  base::HistogramTester histograms;
  plugin_->OnMessage(ParseMessage(R"({
    "type": "save",
    "saveRequestType": "ANNOTATION",
    "token": "annotation-in-edit-mode",
  })"));

  histograms.ExpectTotalCount("PDF.Ink2TextAnnotationAddedCountOnSave", 0);
  histograms.ExpectTotalCount("PDF.Ink2TextAnnotationRemovedCountOnSave", 0);
}

TEST_F(PdfViewWebPluginInk2SaveTest, AnnotationInEditModeMetricsAdded) {
  DocumentInkTextBoxesMap map;
  EXPECT_CALL(*engine_ptr_, LoadTextAnnotationsFromPdf())
      .WillOnce(Return(std::move(map)));

  plugin_->OnMessage(ParseMessage(R"({
    "type": "getAllTextAnnotations",
    "messageId": "foo",
  })"));

  plugin_->EnteredEditMode();

  EXPECT_CALL(*engine_ptr_, DrawText(_, _, _, _, _, _));
  EXPECT_CALL(*engine_ptr_, RequestThumbnail(_, _, _));

  plugin_->OnMessage(CreateFinishTextAnnotationMessage(
      SampleFinishTextAnnotationData(/*frontend_id=*/10, FontId(123),
                                     /*page_index=*/0, /*pdf_zoom=*/1.0)));

  pdf_receiver_.FlushForTesting();

  base::Value expected_response = base::test::ParseJson(R"({
    "type": "saveData",
    "token": "annotation-in-edit-mode",
    "fileName": "example.pdf",
    "editModeForTesting": true,
  })");
  AddDataToValue(base::span(TestPDFiumEngine::kSaveData), expected_response);
  EXPECT_CALL(*client_ptr_, PostMessage(base::test::IsJson(expected_response)));

  base::HistogramTester histograms;
  plugin_->OnMessage(ParseMessage(R"({
    "type": "save",
    "saveRequestType": "ANNOTATION",
    "token": "annotation-in-edit-mode",
  })"));

  histograms.ExpectUniqueSample("PDF.Ink2TextAnnotationAddedCountOnSave", 1, 1);
  histograms.ExpectTotalCount("PDF.Ink2TextAnnotationRemovedCountOnSave", 0);
}

TEST_F(PdfViewWebPluginInk2SaveTest, AnnotationInEditModeMetricsRemoved) {
  DocumentInkTextBoxesMap map;
  map[0].push_back(InkTextBox(/*id=*/1, SampleInkTextBoxAttributes()));
  EXPECT_CALL(*engine_ptr_, LoadTextAnnotationsFromPdf())
      .WillOnce(Return(std::move(map)));

  plugin_->OnMessage(ParseMessage(R"({
    "type": "getAllTextAnnotations",
    "messageId": "foo",
  })"));

  plugin_->EnteredEditMode();

  base::DictValue finish_data =
      SampleFinishTextAnnotationData(/*frontend_id=*/0, FontId(123),
                                     /*page_index=*/0, /*pdf_zoom=*/1.0);
  finish_data.Set("text", "");

  EXPECT_CALL(*engine_ptr_, UpdateTextActiveAndInvalidate(_, false));
  EXPECT_CALL(*engine_ptr_, RequestThumbnail(_, _, _));

  plugin_->OnMessage(CreateFinishTextAnnotationMessage(std::move(finish_data)));

  pdf_receiver_.FlushForTesting();

  base::Value expected_response = base::test::ParseJson(R"({
    "type": "saveData",
    "token": "annotation-in-edit-mode",
    "fileName": "example.pdf",
    "editModeForTesting": true,
  })");
  AddDataToValue(base::span(TestPDFiumEngine::kSaveData), expected_response);
  EXPECT_CALL(*client_ptr_, PostMessage(base::test::IsJson(expected_response)));

  base::HistogramTester histograms;
  plugin_->OnMessage(ParseMessage(R"({
    "type": "save",
    "saveRequestType": "ANNOTATION",
    "token": "annotation-in-edit-mode",
  })"));

  histograms.ExpectTotalCount("PDF.Ink2TextAnnotationAddedCountOnSave", 0);
  histograms.ExpectUniqueSample("PDF.Ink2TextAnnotationRemovedCountOnSave", 1,
                                1);
}

using PdfViewWebPluginInkMetricTest = PdfViewWebPluginInkTest;

TEST_P(PdfViewWebPluginInkMetricTest, LoadedWithoutInkTextAnnotations) {
  base::HistogramTester histograms;

  EXPECT_CALL(*engine_ptr_, ScanForInkAnnotations(_))
      .WillOnce(Return(PDFiumEngine::InkIdentifiers{
          .ink_text_annotations = PDFLoadedWithInkTextAnnotations::kFalse,
          .v2_ink_path = PDFLoadedWithV2InkAnnotations::kFalse,
      }));
  plugin_->DocumentLoadComplete();

  histograms.ExpectUniqueSample(kPdfLoadedWithInkTextAnnotationsMetric,
                                PDFLoadedWithInkTextAnnotations::kFalse, 1);
}

TEST_P(PdfViewWebPluginInkMetricTest, LoadedWithInkTextAnnotations) {
  base::HistogramTester histograms;

  EXPECT_CALL(*engine_ptr_, ScanForInkAnnotations(_))
      .WillOnce(Return(PDFiumEngine::InkIdentifiers{
          .ink_text_annotations = PDFLoadedWithInkTextAnnotations::kTrue,
          .v2_ink_path = PDFLoadedWithV2InkAnnotations::kFalse,
      }));
  plugin_->DocumentLoadComplete();

  histograms.ExpectUniqueSample(kPdfLoadedWithInkTextAnnotationsMetric,
                                PDFLoadedWithInkTextAnnotations::kTrue, 1);
}

TEST_P(PdfViewWebPluginInkMetricTest, LoadedWithInkTextAnnotationsTimeout) {
  base::HistogramTester histograms;
  EXPECT_CALL(*engine_ptr_, ScanForInkAnnotations(_))
      .WillOnce(Return(PDFiumEngine::InkIdentifiers{
          .ink_text_annotations = PDFLoadedWithInkTextAnnotations::kUnknown,
          .v2_ink_path = PDFLoadedWithV2InkAnnotations::kFalse,
      }));
  plugin_->DocumentLoadComplete();

  histograms.ExpectUniqueSample(kPdfLoadedWithInkTextAnnotationsMetric,
                                PDFLoadedWithInkTextAnnotations::kUnknown, 1);
}

TEST_P(PdfViewWebPluginInkMetricTest, LoadedWithoutV2InkAnnotations) {
  base::HistogramTester histograms;

  EXPECT_CALL(*engine_ptr_, ScanForInkAnnotations(_))
      .WillOnce(Return(PDFiumEngine::InkIdentifiers{
          .ink_text_annotations = PDFLoadedWithInkTextAnnotations::kFalse,
          .v2_ink_path = PDFLoadedWithV2InkAnnotations::kFalse,
      }));
  plugin_->DocumentLoadComplete();

  histograms.ExpectUniqueSample(kPdfLoadedWithV2InkAnnotationsMetric,
                                PDFLoadedWithV2InkAnnotations::kFalse, 1);
}

TEST_P(PdfViewWebPluginInkMetricTest, LoadedWithV2InkAnnotations) {
  base::HistogramTester histograms;

  EXPECT_CALL(*engine_ptr_, ScanForInkAnnotations(_))
      .WillOnce(Return(PDFiumEngine::InkIdentifiers{
          .ink_text_annotations = PDFLoadedWithInkTextAnnotations::kFalse,
          .v2_ink_path = PDFLoadedWithV2InkAnnotations::kTrue,
      }));
  plugin_->DocumentLoadComplete();

  histograms.ExpectUniqueSample(kPdfLoadedWithV2InkAnnotationsMetric,
                                PDFLoadedWithV2InkAnnotations::kTrue, 1);
}

TEST_P(PdfViewWebPluginInkMetricTest, LoadedWithV2InkAnnotationsTimeout) {
  base::HistogramTester histograms;
  EXPECT_CALL(*engine_ptr_, ScanForInkAnnotations(_))
      .WillOnce(Return(PDFiumEngine::InkIdentifiers{
          .ink_text_annotations = PDFLoadedWithInkTextAnnotations::kFalse,
          .v2_ink_path = PDFLoadedWithV2InkAnnotations::kUnknown,
      }));
  plugin_->DocumentLoadComplete();

  histograms.ExpectUniqueSample(kPdfLoadedWithV2InkAnnotationsMetric,
                                PDFLoadedWithV2InkAnnotations::kUnknown, 1);
}

using PdfViewWebPluginPrintPreviewInkMetricTest =
    PdfViewWebPluginPrintPreviewTest;

TEST_F(PdfViewWebPluginPrintPreviewInkMetricTest,
       LoadedWithInkAnnotationsDoesNotCountPrintPreview) {
  base::HistogramTester histograms;

  OnMessageWithEngineUpdate(GenerateResetPrintPreviewModeMessage(
      /*id=*/kTestId, /*page_number=*/0, /*grayscale=*/false,
      /*page_count=*/1));

  EXPECT_CALL(*engine_ptr_, ScanForInkAnnotations(_)).Times(0);
  plugin_->DocumentLoadComplete();

  // The Ink annotations PDF load metrics should not increment for Print
  // Preview.
  histograms.ExpectTotalCount(kPdfLoadedWithV2InkAnnotationsMetric, 0);
  histograms.ExpectTotalCount(kPdfLoadedWithInkTextAnnotationsMetric, 0);
}

INSTANTIATE_TEST_SUITE_P(All,
                         PdfViewWebPluginInkTest,
                         testing::ValuesIn(GetAllInkTestVariations()));
INSTANTIATE_TEST_SUITE_P(All,
                         PdfViewWebPluginInkMetricTest,
                         testing::ValuesIn(GetAllInkTestVariations()));
INSTANTIATE_TEST_SUITE_P(All,
                         PdfViewWebPluginInkTextHighlightTest,
                         testing::ValuesIn(GetAllInkTestVariations()));
#endif  // BUILDFLAG(ENABLE_PDF_INK2)

class PdfViewWebPluginAnnotationAgentContainerTest
    : public PdfViewWebPluginTest {
 public:
  ~PdfViewWebPluginAnnotationAgentContainerTest() override = default;

  void CreateAgent(blink::mojom::SelectorPtr selector) {
    fake_annotation_agent_host_.reset();
    // IPC disconnection is asynchronous. FlushForTesting() does not work.
    base::RunLoop().RunUntilIdle();

    mojo::PendingReceiver<blink::mojom::AnnotationAgentHost>
        annotation_agent_host_receiver;
    mojo::PendingRemote<blink::mojom::AnnotationAgent> annotation_agent_remote;

    plugin_->CreateAgent(
        annotation_agent_host_receiver.InitWithNewPipeAndPassRemote(),
        annotation_agent_remote.InitWithNewPipeAndPassReceiver(),
        blink::mojom::AnnotationType::kGlic, std::move(selector), std::nullopt);
    fake_annotation_agent_host_ = std::make_unique<FakeAnnotationAgentHost>(
        std::move(annotation_agent_host_receiver),
        std::move(annotation_agent_remote));
  }

 protected:
  std::unique_ptr<FakeAnnotationAgentHost> fake_annotation_agent_host_;
};

TEST_F(PdfViewWebPluginAnnotationAgentContainerTest, TextFragmentFound) {
  EXPECT_CALL(*engine_ptr_,
              FindAndHighlightTextFragments(ElementsAre("does_not_matter")))
      .WillOnce(Return(true));
  EXPECT_CALL(*engine_ptr_, ScrollToFirstTextFragment).Times(0);
  CreateAgent(blink::mojom::Selector::NewSerializedSelector("does_not_matter"));
  EXPECT_EQ(fake_annotation_agent_host_->WaitForAttachmentResult(),
            blink::mojom::AttachmentResult::kSuccess);
}

TEST_F(PdfViewWebPluginAnnotationAgentContainerTest, TextFragmentNotFound) {
  EXPECT_CALL(*engine_ptr_,
              FindAndHighlightTextFragments(ElementsAre("does_not_matter")))
      .WillOnce(Return(false));
  EXPECT_CALL(*engine_ptr_, ScrollToFirstTextFragment).Times(0);
  CreateAgent(blink::mojom::Selector::NewSerializedSelector("does_not_matter"));
  EXPECT_EQ(fake_annotation_agent_host_->WaitForAttachmentResult(),
            blink::mojom::AttachmentResult::kSelectorNotMatched);
}

TEST_F(PdfViewWebPluginAnnotationAgentContainerTest, EmptySelector) {
  EXPECT_CALL(*engine_ptr_, FindAndHighlightTextFragments).Times(0);
  EXPECT_CALL(*engine_ptr_, ScrollToFirstTextFragment).Times(0);
  CreateAgent(blink::mojom::Selector::NewSerializedSelector(""));
}

TEST_F(PdfViewWebPluginAnnotationAgentContainerTest, NodeSelector) {
  EXPECT_CALL(*engine_ptr_, FindAndHighlightTextFragments).Times(0);
  EXPECT_CALL(*engine_ptr_, ScrollToFirstTextFragment).Times(0);
  CreateAgent(blink::mojom::Selector::NewNodeId(1));
}

TEST_F(PdfViewWebPluginAnnotationAgentContainerTest,
       ResetPreviousTextFragment) {
  EXPECT_CALL(*engine_ptr_, FindAndHighlightTextFragments)
      .Times(2)
      .WillRepeatedly(Return(true));
  EXPECT_CALL(*engine_ptr_, ScrollToFirstTextFragment).Times(0);
  // Every agent will eventually disconnect and trigger `RemoveTextFragments()`.
  EXPECT_CALL(*engine_ptr_, RemoveTextFragments).Times(2);
  CreateAgent(blink::mojom::Selector::NewSerializedSelector("does_not_matter"));
  CreateAgent(blink::mojom::Selector::NewSerializedSelector("does_not_matter"));
}

TEST_F(PdfViewWebPluginAnnotationAgentContainerTest, ScrollIntoView) {
  EXPECT_CALL(*engine_ptr_, FindAndHighlightTextFragments)
      .WillOnce(Return(true));
  EXPECT_CALL(*engine_ptr_,
              ScrollToFirstTextFragment(/*force_smooth_scroll=*/true));
  CreateAgent(blink::mojom::Selector::NewSerializedSelector("does_not_matter"));
  fake_annotation_agent_host_->ScrollIntoView();
}

TEST_F(PdfViewWebPluginAnnotationAgentContainerTest, ConsecutiveQueries) {
  EXPECT_CALL(*engine_ptr_, FindAndHighlightTextFragments)
      .Times(2)
      .WillRepeatedly(Return(true));
  EXPECT_CALL(*engine_ptr_, RemoveTextFragments).Times(2);
  {
    EXPECT_CALL(*engine_ptr_,
                ScrollToFirstTextFragment(/*force_smooth_scroll=*/true));
    CreateAgent(
        blink::mojom::Selector::NewSerializedSelector("does_not_matter"));
    fake_annotation_agent_host_->ScrollIntoView();
  }
  {
    EXPECT_CALL(*engine_ptr_,
                ScrollToFirstTextFragment(/*force_smooth_scroll=*/true));
    CreateAgent(
        blink::mojom::Selector::NewSerializedSelector("does_not_matter"));
    fake_annotation_agent_host_->ScrollIntoView();
  }
}

TEST_F(PdfViewWebPluginAnnotationAgentContainerTest,
       CannotScrollAfterInvalidation) {
  EXPECT_CALL(*engine_ptr_, FindAndHighlightTextFragments)
      .WillOnce(Return(true));
  EXPECT_CALL(*engine_ptr_, ScrollToFirstTextFragment).Times(0);
  EXPECT_CALL(*engine_ptr_, RemoveTextFragments);

  CreateAgent(blink::mojom::Selector::NewSerializedSelector("does_not_matter"));
  plugin_->OnNewTextFragmentsSearchStarted();
  fake_annotation_agent_host_->ScrollIntoView();
}

}  // namespace chrome_pdf
