// 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 <functional>
#include <memory>
#include <string>
#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/string_piece.h"
#include "base/task/single_thread_task_runner.h"
#include "base/test/bind.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/paint_ready_rect.h"
#include "pdf/pdf_accessibility_data_handler.h"
#include "pdf/test/mock_web_associated_url_loader.h"
#include "pdf/test/test_helpers.h"
#include "pdf/test/test_pdfium_engine.h"
#include "printing/metafile_skia.h"
#include "services/network/public/mojom/referrer_policy.mojom-shared.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/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"

namespace chrome_pdf {

namespace {

using ::testing::AnyNumber;
using ::testing::ElementsAre;
using ::testing::ElementsAreArray;
using ::testing::Eq;
using ::testing::InSequence;
using ::testing::Invoke;
using ::testing::IsEmpty;
using ::testing::IsFalse;
using ::testing::IsTrue;
using ::testing::MockFunction;
using ::testing::NiceMock;
using ::testing::Pointwise;
using ::testing::Return;
using ::testing::SaveArg;
using ::testing::SizeIs;

// `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, "") {
  PDFEngine::Client::SearchStringResult l = std::get<0>(arg);
  PDFEngine::Client::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 == expected_text &&
         event.unmodified_text == expected_text;
}

base::Value::Dict ParseMessage(base::StringPiece 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;
}

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,
              (AccessibilityDocInfo),
              (override));
  MOCK_METHOD(void,
              SetAccessibilityPageInfo,
              (AccessibilityPageInfo,
               std::vector<AccessibilityTextRunInfo>,
               std::vector<AccessibilityCharInfo>,
               AccessibilityPageObjects),
              (override));
};

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/1322928): Must trigger callback to free
            // `UrlLoader`.
            client->DidReceiveResponse(blink::WebURLResponse());
            client->DidFinishLoading();
          });
      return associated_loader;
    });
    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,
              (PDFEngine::Client*, PDFiumFormFiller::ScriptOption),
              (override));

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

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

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

  MOCK_METHOD(void, PostMessage, (base::Value::Dict), (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*),
              (override));
};

class FakePdfService : public pdf::mojom::PdfService {
 public:
  MOCK_METHOD(void,
              SetListener,
              (mojo::PendingRemote<pdf::mojom::PdfListener>),
              (override));
  MOCK_METHOD(void, UpdateContentRestrictions, (int32_t), (override));
  MOCK_METHOD(void, HasUnsupportedFeature, (), (override));
  MOCK_METHOD(void,
              SaveUrlAs,
              (const GURL&, network::mojom::ReferrerPolicy),
              (override));
  MOCK_METHOD(void,
              SelectionChanged,
              (const gfx::PointF&, int32_t, const gfx::PointF&, int32_t),
              (override));
  MOCK_METHOD(void, SetPluginCanSave, (bool), (override));
};

}  // 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(base::StringPiece name,
                                base::StringPiece value,
                                blink::WebPluginParams& params) {
    params.attribute_names.push_back(
        blink::WebString::FromUTF8(name.data(), name.size()));
    params.attribute_values.push_back(
        blink::WebString::FromUTF8(value.data(), value.size()));
  }

  void SetUpPlugin(base::StringPiece document_url,
                   const 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](
                           PDFEngine::Client* 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::PdfService>(
                pdf_receiver_.BindNewEndpointAndPassDedicatedRemote()),
            params));
  }

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

    SetUpPlugin(url, 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::Value::Dict& message) {
    // New engine will be created making this unowned reference stale.
    engine_ptr_ = nullptr;
    plugin_->OnMessage(message);
  }

  NiceMock<FakePdfService> pdf_service_;
  mojo::AssociatedReceiver<pdf::mojom::PdfService> pdf_receiver_{&pdf_service_};

  // 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::Value::Dict 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();
  }

  // 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(PdfViewWebPluginTest, CreateUrlLoader) {
  EXPECT_CALL(*client_ptr_, DidStartLoading).Times(0);
  EXPECT_CALL(pdf_service_, 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_service_,
              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": false,
  })")));
  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_service_, 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": false,
  })")));
  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_service_, UpdateContentRestrictions(kContentRestrictionPrint |
                                                      kContentRestrictionPaste |
                                                      kContentRestrictionCut |
                                                      kContentRestrictionCopy));
  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);
  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"));

  // `HasUnsupportedFeature()` is not called if the viewer is not full-frame.
  EXPECT_CALL(pdf_service_, HasUnsupportedFeature).Times(0);

  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"));

  // `HasUnsupportedFeature()` is not called if the viewer is not full-frame.
  EXPECT_CALL(pdf_service_, HasUnsupportedFeature).Times(0);

  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"));

  // `HasUnsupportedFeature()` is called once for all features.
  EXPECT_CALL(pdf_service_, HasUnsupportedFeature);

  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"));

  // `HasUnsupportedFeature()` is called once for all features.
  EXPECT_CALL(pdf_service_, HasUnsupportedFeature);

  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, 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) {
  AccessibilityDocInfo doc_info = plugin_->GetAccessibilityDocInfoForTesting();

  EXPECT_EQ(TestPDFiumEngine::kPageNumber, doc_info.page_count);
  EXPECT_FALSE(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));

  AccessibilityDocInfo doc_info = plugin_->GetAccessibilityDocInfoForTesting();

  EXPECT_EQ(TestPDFiumEngine::kPageNumber, doc_info.page_count);
  EXPECT_FALSE(doc_info.text_accessible);
  EXPECT_TRUE(doc_info.text_copyable);
}

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

  AccessibilityDocInfo doc_info = plugin_->GetAccessibilityDocInfoForTesting();

  EXPECT_EQ(TestPDFiumEngine::kPageNumber, doc_info.page_count);
  EXPECT_TRUE(doc_info.text_accessible);
  EXPECT_FALSE(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));

  AccessibilityDocInfo doc_info = plugin_->GetAccessibilityDocInfoForTesting();

  EXPECT_EQ(TestPDFiumEngine::kPageNumber, doc_info.page_count);
  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());

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

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

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);

  ui::Cursor dummy_cursor;
  EXPECT_EQ(blink::WebInputEventResult::kHandledApplication,
            plugin_->HandleInputEvent(
                blink::WebCoalescedInputEvent(mouse_event, ui::LatencyInfo()),
                &dummy_cursor));
}

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) {
        base::StringPiece16 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) {
        base::StringPiece16 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_service_, 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_service_, 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_service_, 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::FromUTF8(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::FromUTF8(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, FormTextFieldFocusChangeUpdatesTextInputType) {
  ASSERT_EQ(blink::WebTextInputType::kWebTextInputTypeNone,
            plugin_->GetPluginTextInputType());

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

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

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

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

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

  {
    static constexpr PDFEngine::Client::SearchStringResult kExpectation[] = {
        {16, 3}};
    EXPECT_THAT(
        plugin_->SearchString(kTarget, kPattern, /*case_sensitive=*/true),
        Pointwise(SearchStringResultEq(), kExpectation));
  }
  {
    static constexpr PDFEngine::Client::SearchStringResult kExpectation[] = {
        {16, 3}, {41, 3}};
    EXPECT_THAT(
        plugin_->SearchString(kTarget, kPattern, /*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. `UpdateTextInputState` is called
    // twice because it also gets called due to
    // `PDFiumEngine::UpdateFocus(false)`.
    EXPECT_CALL(*client_ptr_, UpdateTextInputState).Times(2);
    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_service_, 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) {
  base::Value::Dict metadata;
  metadata.Set("fileSize", "0 B");
  metadata.Set("linearized", false);
  metadata.Set("pageSize", "Varies");
  metadata.Set("canSerializeDocument", true);

  base::Value::Dict message;
  message.Set("type", "metadata");
  message.Set("metadataData", std::move(metadata));

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

class PdfViewWebPluginWithDocInfoTest : public PdfViewWebPluginTest {
 protected:
  class TestPDFiumEngineWithDocInfo : public TestPDFiumEngine {
   public:
    explicit TestPDFiumEngineWithDocInfo(PDFEngine::Client* client)
        : TestPDFiumEngine(client) {
      InitializeDocumentAttachments();
      InitializeDocumentMetadata();
    }

    base::Value::List GetBookmarks() override {
      // Create `bookmark1` which navigates to an in-doc position. This bookmark
      // will be in the top-level bookmark list.
      base::Value::Dict bookmark1;
      bookmark1.Set("title", "Bookmark 1");
      bookmark1.Set("page", 2);
      bookmark1.Set("x", 10);
      bookmark1.Set("y", 20);
      bookmark1.Set("zoom", 2.0);

      // Create `bookmark2` which navigates to a web page. This bookmark will be
      // a child of `bookmark1`.
      base::Value::Dict bookmark2;
      bookmark2.Set("title", "Bookmark 2");
      bookmark2.Set("uri", "test.com");

      base::Value::List children_of_bookmark1;
      children_of_bookmark1.Append(std::move(bookmark2));
      bookmark1.Set("children", std::move(children_of_bookmark1));

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

    absl::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));
    }
  };

  static base::Value::Dict CreateExpectedAttachmentsResponse() {
    base::Value::List attachments;
    {
      base::Value::Dict attachment;
      attachment.Set("name", "attachment1.txt");
      attachment.Set("size", 13);
      attachment.Set("readable", true);
      attachments.Append(std::move(attachment));
    }
    {
      base::Value::Dict attachment;
      attachment.Set("name", "attachment2.pdf");
      attachment.Set("size", 0);
      attachment.Set("readable", false);
      attachments.Append(std::move(attachment));
    }
    {
      base::Value::Dict attachment;
      attachment.Set("name", "attachment3.mov");
      attachment.Set("size", -1);
      attachment.Set("readable", true);
      attachments.Append(std::move(attachment));
    }

    base::Value::Dict message;
    message.Set("type", "attachments");
    message.Set("attachmentsData", std::move(attachments));
    return message;
  }

  static base::Value::Dict CreateExpectedBookmarksResponse(
      base::Value::List bookmarks) {
    base::Value::Dict message;
    message.Set("type", "bookmarks");
    message.Set("bookmarksData", std::move(bookmarks));
    return message;
  }

  static base::Value::Dict CreateExpectedMetadataResponse() {
    base::Value::Dict metadata;
    metadata.Set("version", "1.7");
    metadata.Set("fileSize", "13 B");
    metadata.Set("linearized", true);

    metadata.Set("title", "Title");
    metadata.Set("author", "Author");
    metadata.Set("subject", "Subject");
    metadata.Set("keywords", "Keywords");
    metadata.Set("creator", "Creator");
    metadata.Set("producer", "Producer");
    metadata.Set("creationDate",
                 "5/4/21, 4:12:13\xE2\x80\xAF"
                 "AM");
    metadata.Set("modDate",
                 "6/4/21, 8:16:17\xE2\x80\xAF"
                 "AM");
    metadata.Set("pageSize", "13.89 × 16.67 in (portrait)");
    metadata.Set("canSerializeDocument", true);

    base::Value::Dict message;
    message.Set("type", "metadata");
    message.Set("metadataData", std::move(metadata));
    return message;
  }

  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;
    });
  }
};

TEST_F(PdfViewWebPluginWithDocInfoTest, OnDocumentLoadComplete) {
  const base::Value::Dict expect_attachments =
      CreateExpectedAttachmentsResponse();
  const base::Value::Dict expect_bookmarks =
      CreateExpectedBookmarksResponse(engine_ptr_->GetBookmarks());
  const base::Value::Dict expect_metadata = CreateExpectedMetadataResponse();
  EXPECT_CALL(*client_ptr_, PostMessage);
  EXPECT_CALL(*client_ptr_, PostMessage(Eq(std::ref(expect_attachments))));
  EXPECT_CALL(*client_ptr_, PostMessage(Eq(std::ref(expect_bookmarks))));
  EXPECT_CALL(*client_ptr_, PostMessage(Eq(std::ref(expect_metadata))));
  plugin_->DocumentLoadComplete();
}

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::Value::Dict& message) {
          EXPECT_NE("saveData", *message.FindString("type"));
        });
  }
};

#if BUILDFLAG(ENABLE_INK)
TEST_F(PdfViewWebPluginSaveTest, AnnotationInNonEditMode) {
  base::Value expected_response = base::test::ParseJson(R"({
    "type": "saveData",
    "token": "annotation-in-non-edit-mode",
    "fileName": "example.pdf",
    "editModeForTesting": false,
  })");
  AddDataToValue(base::make_span(TestPDFiumEngine::kLoadedData),
                 expected_response);

  EXPECT_CALL(pdf_service_, SetPluginCanSave(true));
  ExpectUpdateTextInputState(blink::WebTextInputType::kWebTextInputTypeNone);
  EXPECT_CALL(*client_ptr_, PostMessage(base::test::IsJson(expected_response)));

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

  pdf_receiver_.FlushForTesting();
}

TEST_F(PdfViewWebPluginSaveTest, AnnotationInEditMode) {
  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::make_span(TestPDFiumEngine::kSaveData),
                 expected_response);

  EXPECT_CALL(pdf_service_, SetPluginCanSave(true));
  ExpectUpdateTextInputState(blink::WebTextInputType::kWebTextInputTypeNone);
  EXPECT_CALL(*client_ptr_, PostMessage(base::test::IsJson(expected_response)));

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

  pdf_receiver_.FlushForTesting();
}
#endif  // BUILDFLAG(ENABLE_INK)

TEST_F(PdfViewWebPluginSaveTest, OriginalInNonEditMode) {
  {
    InSequence pdf_service_sequence;

    EXPECT_CALL(pdf_service_, SetPluginCanSave(false));
    EXPECT_CALL(
        pdf_service_,
        SaveUrlAs(GURL(kPdfUrl), network::mojom::ReferrerPolicy::kDefault));
    EXPECT_CALL(pdf_service_, SetPluginCanSave(false));
  }

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

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

  pdf_receiver_.FlushForTesting();
}

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

  {
    InSequence pdf_service_sequence;

    EXPECT_CALL(pdf_service_, SetPluginCanSave(false));
    EXPECT_CALL(
        pdf_service_,
        SaveUrlAs(GURL(kPdfUrl), network::mojom::ReferrerPolicy::kDefault));
    EXPECT_CALL(pdf_service_, SetPluginCanSave(true));
  }

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

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

  pdf_receiver_.FlushForTesting();
}

#if BUILDFLAG(ENABLE_INK)
TEST_F(PdfViewWebPluginSaveTest, EditedInNonEditMode) {
  base::Value expected_response = base::test::ParseJson(R"({
    "type": "saveData",
    "token": "edited-in-non-edit-mode",
    "fileName": "example.pdf",
    "editModeForTesting": false,
  })");
  AddDataToValue(base::make_span(TestPDFiumEngine::kLoadedData),
                 expected_response);

  ExpectUpdateTextInputState(blink::WebTextInputType::kWebTextInputTypeNone);
  EXPECT_CALL(*client_ptr_, PostMessage(base::test::IsJson(expected_response)));

  plugin_->OnMessage(ParseMessage(R"({
    "type": "save",
    "saveRequestType": 2,
    "token": "edited-in-non-edit-mode",
  })"));
}
#endif  // BUILDFLAG(ENABLE_INK)

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

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

  ExpectUpdateTextInputState(blink::WebTextInputType::kWebTextInputTypeNone);
  EXPECT_CALL(*client_ptr_, PostMessage(base::test::IsJson(expected_response)));

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

class PdfViewWebPluginSubmitFormTest
    : public PdfViewWebPluginWithoutInitializeTest {
 protected:
  void SubmitForm(const std::string& url,
                  base::StringPiece 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/1322928): 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, form_data.data(), form_data.size());
  }

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

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

    constexpr base::StringPiece kFormData = "form data";
    plugin_->SubmitForm(url, kFormData.data(), kFormData.size());
  }

  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 base::StringPiece 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/"));
  }
};

TEST_F(PdfViewWebPluginPrintPreviewTest, HandleResetPrintPreviewModeMessage) {
  EXPECT_CALL(*client_ptr_, CreateEngine)
      .WillOnce([](PDFEngine::Client* 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(ParseMessage(R"({
    "type": "resetPrintPreviewMode",
    "url": "chrome-untrusted://print/0/0/print.pdf",
    "grayscale": false,
    "pageCount": 1,
  })"));
}

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

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

  // The UI ID of 1 in the URL is arbitrary.
  // The page index value of -1, AKA `kCompletePDFIndex`, is required for PDFs.
  OnMessageWithEngineUpdate(ParseMessage(R"({
    "type": "resetPrintPreviewMode",
    "url": "chrome-untrusted://print/1/-1/print.pdf",
    "grayscale": false,
    "pageCount": 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([](PDFEngine::Client* client,
                   PDFiumFormFiller::ScriptOption /*script_option*/) {
        auto engine = std::make_unique<NiceMock<TestPDFiumEngine>>(client);
        EXPECT_CALL(*engine, SetGrayscale(true));
        return engine;
      });

  OnMessageWithEngineUpdate(ParseMessage(R"({
    "type": "resetPrintPreviewMode",
    "url": "chrome-untrusted://print/0/0/print.pdf",
    "grayscale": true,
    "pageCount": 1,
  })"));
}

TEST_F(PdfViewWebPluginPrintPreviewTest, DocumentLoadComplete) {
  OnMessageWithEngineUpdate(ParseMessage(R"({
    "type": "resetPrintPreviewMode",
    "url": "chrome-untrusted://print/0/0/print.pdf",
    "grayscale": false,
    "pageCount": 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": false,
  })")));
  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_service_, 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(ParseMessage(R"({
    "type": "resetPrintPreviewMode",
    "url": "chrome-untrusted://print/123/0/print.pdf",
    "grayscale": false,
    "pageCount": 2,
  })"));

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

TEST_F(PdfViewWebPluginPrintPreviewTest,
       DocumentLoadProgressNotResetByLoadPreviewPageMessage) {
  OnMessageWithEngineUpdate(ParseMessage(R"({
    "type": "resetPrintPreviewMode",
    "url": "chrome-untrusted://print/123/0/print.pdf",
    "grayscale": false,
    "pageCount": 2,
  })"));

  plugin_->DocumentLoadProgress(2, 100);

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

  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,
  })"));
}

}  // namespace chrome_pdf