// Copyright 2019 The Chromium Authors
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.

#include "components/paint_preview/browser/paint_preview_client.h"

#include "base/files/file_path.h"
#include "base/files/file_util.h"
#include "base/files/scoped_temp_dir.h"
#include "base/functional/callback.h"
#include "base/notreached.h"
#include "base/strings/strcat.h"
#include "base/strings/utf_string_conversions.h"
#include "base/test/protobuf_matchers.h"
#include "base/test/test_future.h"
#include "base/unguessable_token.h"
#include "base/version.h"
#include "build/build_config.h"
#include "components/paint_preview/common/capture_result.h"
#include "components/paint_preview/common/file_stream.h"
#include "components/paint_preview/common/mock_paint_preview_recorder.h"
#include "components/paint_preview/common/mojom/paint_preview_recorder.mojom.h"
#include "components/paint_preview/common/proto/paint_preview.pb.h"
#include "components/paint_preview/common/redaction_params.h"
#include "components/paint_preview/common/serialized_recording.h"
#include "components/paint_preview/common/test_utils.h"
#include "components/paint_preview/common/version.h"
#include "components/version_info/version_info.h"
#include "content/public/browser/render_frame_host.h"
#include "content/public/browser/render_process_host.h"
#include "content/public/browser/web_contents.h"
#include "content/public/test/navigation_simulator.h"
#include "content/public/test/test_renderer_host.h"
#include "content/public/test/test_utils.h"
#include "mojo/public/cpp/base/big_buffer.h"
#include "mojo/public/cpp/bindings/associated_receiver.h"
#include "mojo/public/cpp/bindings/struct_ptr.h"
#include "testing/gmock/include/gmock/gmock.h"
#include "testing/gtest/include/gtest/gtest.h"
#include "third_party/blink/public/common/associated_interfaces/associated_interface_provider.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/SkSurfaceProps.h"

namespace paint_preview {

using base::test::EqualsProto;
using testing::AnyOf;
using testing::Key;
using testing::UnorderedElementsAre;

using CaptureResultFuture =
    base::test::TestFuture<base::UnguessableToken,
                           mojom::PaintPreviewStatus,
                           std::unique_ptr<CaptureResult>>;

namespace {

const std::string_view kMainFrameUrl = "https://www.chromium.org";
const std::string_view kOtherOriginUrl = "https://chromium.org";

// Convert |params| to the mojo::PaintPreviewServiceParams format. NOTE: this
// does not set the file parameter as the file is created in the client
// internals and should be treated as an opaque file (with an unknown path) in
// the render frame's service.
mojom::PaintPreviewCaptureParamsPtr ToMojoParams(
    const PaintPreviewClient::PaintPreviewParams& params) {
  mojom::PaintPreviewCaptureParamsPtr params_ptr =
      mojom::PaintPreviewCaptureParams::New();
  params_ptr->persistence = params.persistence;
  params_ptr->guid = params.inner.get_document_guid();
  params_ptr->is_main_frame = params.inner.is_main_frame;
  params_ptr->geometry_metadata_params = mojom::GeometryMetadataParams::New();
  params_ptr->geometry_metadata_params->clip_rect = params.inner.clip_rect;
  params_ptr->skip_accelerated_content = params.inner.skip_accelerated_content;
  return params_ptr;
}

base::FilePath ParseStringFilePath(std::string_view raw_path) {
  return base::FilePath(
#if BUILDFLAG(IS_WIN)
      base::UTF8ToWide(raw_path)
#else
      raw_path
#endif
  );
}

void VerifyFilePath(std::string_view raw_path,
                    const base::Location& location = FROM_HERE) {
  base::ScopedAllowBlockingForTesting scope;
  EXPECT_TRUE(base::PathExists(ParseStringFilePath(raw_path)))
      << "Expected at " << location.ToString();
}

base::UnguessableToken DeserializeFrameToken(
    const PaintPreviewFrameProto& frame) {
  auto token = base::UnguessableToken::Deserialize(frame.embedding_token_high(),
                                                   frame.embedding_token_low())
                   .value();
  CHECK_NE(token, base::UnguessableToken::Null());
  return token;
}

sk_sp<SkPicture> ReadPictureFromFile(std::string_view raw_path) {
  base::ScopedAllowBlockingForTesting scope;
  FileRStream rstream(
      base::File(ParseStringFilePath(raw_path),
                 base::File::FLAG_OPEN_ALWAYS | base::File::FLAG_READ));
  return SkPicture::MakeFromStream(&rstream, nullptr);
}

sk_sp<SkPicture> ReadPictureFromBuffer(mojo_base::BigBuffer buffer) {
  base::ScopedAllowBlockingForTesting scope;
  SkMemoryStream stream(buffer.data(), buffer.size(),
                        /*copyData=*/false);
  return SkPicture::MakeFromStream(&stream, nullptr);
}

SkBitmap MakeBitmapFromPicture(sk_sp<SkPicture> pic) {
  SkBitmap bitmap;
  CHECK(bitmap.tryAllocN32Pixels(pic->cullRect().width(),
                                 pic->cullRect().height()));
  SkCanvas canvas(bitmap, SkSurfaceProps{});
  canvas.drawPicture(pic);

  return bitmap;
}

}  // namespace

class PaintPreviewClientRenderViewHostTestBase
    : public content::RenderViewHostTestHarness {
 public:
  PaintPreviewClientRenderViewHostTestBase() = default;

  PaintPreviewClientRenderViewHostTestBase(
      const PaintPreviewClientRenderViewHostTestBase&) = delete;
  PaintPreviewClientRenderViewHostTestBase& operator=(
      const PaintPreviewClientRenderViewHostTestBase&) = delete;

 protected:
  void SetUp() override {
    ASSERT_TRUE(temp_dir_.CreateUniqueTempDir());
    RenderViewHostTestHarness::SetUp();
    content::RenderFrameHostTester::For(main_rfh())
        ->InitializeRenderFrameIfNeeded();
    content::NavigationSimulator::NavigateAndCommitFromBrowser(
        web_contents(), GURL(kMainFrameUrl));
  }

  void TearDown() override {
#if BUILDFLAG(IS_POSIX)
    base::SetPosixFilePermissions(temp_dir_.GetPath(), 0777);
#endif
    content::RenderViewHostTestHarness::TearDown();
  }

  void OverrideInterface(content::RenderFrameHost* rfh,
                         MockPaintPreviewRecorder* service) {
    ASSERT_TRUE(rfh);
    blink::AssociatedInterfaceProvider* remote_interfaces =
        rfh->GetRemoteAssociatedInterfaces();
    remote_interfaces->OverrideBinderForTesting(
        mojom::PaintPreviewRecorder::Name_,
        base::BindRepeating(&MockPaintPreviewRecorder::BindRequest,
                            base::Unretained(service)));
  }

  content::RenderFrameHost* AddChildRFH(content::RenderFrameHost* parent,
                                        std::string_view origin) {
    content::RenderFrameHost* result =
        content::RenderFrameHostTester::For(parent)->AppendChild("");
    content::RenderFrameHostTester::For(result)
        ->InitializeRenderFrameIfNeeded();
    SimulateNavigation(&result, GURL(origin));
    return result;
  }

  void SimulateNavigation(content::RenderFrameHost** rfh, const GURL& url) {
    auto navigation_simulator =
        content::NavigationSimulator::CreateRendererInitiated(url, *rfh);
    navigation_simulator->Commit();
    *rfh = navigation_simulator->GetFinalRenderFrameHost();
  }

  base::ScopedTempDir temp_dir_;

 private:
};

class PaintPreviewClientRenderViewHostTest
    : public PaintPreviewClientRenderViewHostTestBase,
      public testing::WithParamInterface<RecordingPersistence> {
 public:
  mojo::StructPtr<mojom::PaintPreviewCaptureResponse>
  NewMockPaintPreviewCaptureResponse() {
    auto response = mojom::PaintPreviewCaptureResponse::New();
    response->geometry_metadata = mojom::GeometryMetadataResponse::New();
    if (GetParam() == RecordingPersistence::kMemoryBuffer) {
      response->skp = {mojo_base::BigBuffer()};
    }
    return response;
  }

 private:
};

TEST_P(PaintPreviewClientRenderViewHostTest, CaptureMainFrameMock) {
  PaintPreviewClient::PaintPreviewParams params(GetParam());
  params.root_dir = temp_dir_.GetPath();
  params.inner.is_main_frame = true;

  content::RenderFrameHost* rfh = main_rfh();
  GURL expected_url = rfh->GetLastCommittedURL();

  auto response = NewMockPaintPreviewCaptureResponse();
  response->embedding_token = std::nullopt;
  response->geometry_metadata->scroll_offsets = gfx::Point(5, 10);
  response->geometry_metadata->frame_offsets = gfx::Point(20, 30);

  PaintPreviewProto expected_proto;
  auto* metadata = expected_proto.mutable_metadata();
  metadata->set_url(expected_url.spec());
  metadata->set_version(kPaintPreviewVersion);
  auto* chromeVersion = metadata->mutable_chrome_version();
  const auto& current_chrome_version = version_info::GetVersion();
  chromeVersion->set_major(current_chrome_version.components()[0]);
  chromeVersion->set_minor(current_chrome_version.components()[1]);
  chromeVersion->set_build(current_chrome_version.components()[2]);
  chromeVersion->set_patch(current_chrome_version.components()[3]);
  PaintPreviewFrameProto* main_frame = expected_proto.mutable_root_frame();
  main_frame->set_is_main_frame(true);
  main_frame->set_scroll_offset_x(5);
  main_frame->set_scroll_offset_y(10);
  main_frame->set_frame_offset_x(20);
  main_frame->set_frame_offset_y(30);

  MockPaintPreviewRecorder service;
  service.SetExpectedParams(ToMojoParams(params));
  service.SetResponse(std::move(response));
  OverrideInterface(rfh, &service);
  PaintPreviewClient::CreateForWebContents(web_contents());
  auto* client = PaintPreviewClient::FromWebContents(web_contents());
  ASSERT_NE(client, nullptr);
  CaptureResultFuture main_capture_future;
  client->CapturePaintPreview(params.Clone(), rfh,
                              main_capture_future.GetCallback());

  auto [returned_guid, status, result] = main_capture_future.Take();
  EXPECT_EQ(returned_guid, params.inner.get_document_guid());
  EXPECT_EQ(status, mojom::PaintPreviewStatus::kOk);

  auto token = DeserializeFrameToken(result->proto.root_frame());

  // The token for the main frame is set internally since the render frame
  // host won't have one. To simplify the proto comparison using
  // EqualsProto copy the generated one into |expected_proto|.
  main_frame->set_embedding_token_low(token.GetLowForSerialization());
  main_frame->set_embedding_token_high(token.GetHighForSerialization());
  if (GetParam() == RecordingPersistence::kFileSystem) {
    main_frame->set_file_path(
        temp_dir_.GetPath()
            .AppendASCII(base::StrCat({token.ToString(), ".skp"}))
            .AsUTF8Unsafe());
  }

  EXPECT_THAT(result->proto, EqualsProto(expected_proto));

  switch (GetParam()) {
    case RecordingPersistence::kFileSystem:
      VerifyFilePath(result->proto.root_frame().file_path());
      break;

    case RecordingPersistence::kMemoryBuffer: {
      EXPECT_EQ(result->serialized_skps.size(), 1u);
      EXPECT_TRUE(result->serialized_skps.contains(token));
    } break;

    default:
      NOTREACHED();
  }
}

TEST_P(PaintPreviewClientRenderViewHostTest, CaptureFailureMock) {
  PaintPreviewClient::PaintPreviewParams params(GetParam());
  params.root_dir = temp_dir_.GetPath();
  params.inner.is_main_frame = true;

  MockPaintPreviewRecorder recorder;
  recorder.SetExpectedParams(ToMojoParams(params));
  recorder.SetResponse(
      base::unexpected(mojom::PaintPreviewStatus::kCaptureFailed));
  OverrideInterface(main_rfh(), &recorder);
  PaintPreviewClient::CreateForWebContents(web_contents());
  auto* client = PaintPreviewClient::FromWebContents(web_contents());
  ASSERT_NE(client, nullptr);
  CaptureResultFuture main_capture_future;
  client->CapturePaintPreview(params.Clone(), main_rfh(),
                              main_capture_future.GetCallback());

  auto [returned_guid, status, result] = main_capture_future.Take();
  EXPECT_EQ(returned_guid, params.inner.get_document_guid());
  EXPECT_EQ(status, mojom::PaintPreviewStatus::kFailed);
  content::RunAllTasksUntilIdle();
  EXPECT_TRUE(base::IsDirectoryEmpty(temp_dir_.GetPath()));
}

// We can only mutate file permissions on POSIX systems.
#if BUILDFLAG(IS_POSIX)
TEST_F(PaintPreviewClientRenderViewHostTestBase, SubframeFileCreationFails) {
  auto* subframe = AddChildRFH(main_rfh(), "https://example.test");
  ASSERT_TRUE(subframe);

  PaintPreviewClient::PaintPreviewParams params(
      RecordingPersistence::kFileSystem);
  params.root_dir = temp_dir_.GetPath();
  params.inner.is_main_frame = true;

  MockPaintPreviewRecorder recorder;
  recorder.SetExpectedParams(ToMojoParams(params));
  recorder.SetResponse(
      base::unexpected(mojom::PaintPreviewStatus::kCaptureFailed));
  OverrideInterface(main_rfh(), &recorder);

  PaintPreviewClient::CreateForWebContents(web_contents());
  auto* client = PaintPreviewClient::FromWebContents(web_contents());
  ASSERT_NE(client, nullptr);

  base::test::TestFuture<void> recorder_received_request_future;
  recorder.SetReceivedRequestClosure(
      recorder_received_request_future.GetCallback());

  CaptureResultFuture main_capture_future;
  client->CapturePaintPreview(params.Clone(), main_rfh(),
                              main_capture_future.GetCallback());

  ASSERT_TRUE(recorder_received_request_future.Wait());
  // Before sending the main frame response, make the directory read-only to
  // cause a file creation error on subsequent writes, and issue a reentrant
  // call for a subframe, to see if the main frame's control flow mistakenly
  // invokes the callback even after the subframe failed the whole capture.

  base::SetPosixFilePermissions(temp_dir_.GetPath(), 0555);

  client->CaptureSubframePaintPreview(params.inner.get_document_guid(),
                                      gfx::Rect(), subframe);

  recorder.SendResponse();

  auto [returned_guid, status, result] = main_capture_future.Take();
  EXPECT_EQ(returned_guid, params.inner.get_document_guid());
  // The precise status from the client depends on thread scheduling.
  EXPECT_THAT(status, AnyOf(mojom::PaintPreviewStatus::kFileCreationError,
                            mojom::PaintPreviewStatus::kFailed));
  EXPECT_EQ(result, nullptr);
}
#endif

TEST_P(PaintPreviewClientRenderViewHostTest, RenderFrameDeletedNotCapturing) {
  // Test that a deleting a render frame doesn't cause any problems if not
  // capturing.
  PaintPreviewClient::CreateForWebContents(web_contents());
  auto* client = PaintPreviewClient::FromWebContents(web_contents());
  ASSERT_NE(client, nullptr);
  client->RenderFrameDeleted(main_rfh());
}

TEST_P(PaintPreviewClientRenderViewHostTest, RenderFrameDeletedDuringCapture) {
  PaintPreviewClient::PaintPreviewParams params(GetParam());
  params.root_dir = temp_dir_.GetPath();
  params.inner.is_main_frame = true;
  params.inner.skip_accelerated_content = true;

  content::RenderFrameHost* rfh = main_rfh();

  auto response = NewMockPaintPreviewCaptureResponse();
  response->geometry_metadata = mojom::GeometryMetadataResponse::New();
  response->embedding_token = std::nullopt;

  MockPaintPreviewRecorder service;
  service.SetExpectedParams(ToMojoParams(params));
  service.SetResponse(std::move(response));
  OverrideInterface(rfh, &service);
  PaintPreviewClient::CreateForWebContents(web_contents());
  auto* client = PaintPreviewClient::FromWebContents(web_contents());
  ASSERT_NE(client, nullptr);

  base::test::TestFuture<void> service_received_request_future;
  service.SetReceivedRequestClosure(
      service_received_request_future.GetCallback());

  CaptureResultFuture main_capture_future;
  client->CapturePaintPreview(params.Clone(), rfh,
                              main_capture_future.GetCallback());

  ASSERT_TRUE(service_received_request_future.Wait());
  client->RenderFrameDeleted(rfh);
  service.SendResponse();

  auto [guid, status, result] = main_capture_future.Take();
  EXPECT_EQ(status, mojom::PaintPreviewStatus::kFailed);
  EXPECT_EQ(result, nullptr);
}

INSTANTIATE_TEST_SUITE_P(
    All,
    PaintPreviewClientRenderViewHostTest,
    testing::Values(RecordingPersistence::kFileSystem,
                    RecordingPersistence::kMemoryBuffer),
    [](const testing::TestParamInfo<RecordingPersistence>& info) {
      return std::string(PersistenceToString(info.param));
    });

enum class ResponseOrdering { kMainFrameThenSubframe, kSubframeThenMainFrame };

std::string_view OrderingToString(ResponseOrdering ordering) {
  switch (ordering) {
    case ResponseOrdering::kMainFrameThenSubframe:
      return "kMainFrameThenSubframe";
    case ResponseOrdering::kSubframeThenMainFrame:
      return "kSubframeThenMainFrame";
  }
  NOTREACHED();
}

class PaintPreviewClientRenderViewHostResponseOrderingTest
    : public PaintPreviewClientRenderViewHostTestBase,
      public testing::WithParamInterface<
          std::tuple<RecordingPersistence, ResponseOrdering>> {
 public:
  mojo::StructPtr<mojom::PaintPreviewCaptureResponse>
  NewMockPaintPreviewCaptureResponse() {
    auto response = mojom::PaintPreviewCaptureResponse::New();
    response->geometry_metadata = mojom::GeometryMetadataResponse::New();
    if (persistence() == RecordingPersistence::kMemoryBuffer) {
      response->skp = {mojo_base::BigBuffer()};
    }
    return response;
  }

  RecordingPersistence persistence() const { return std::get<0>(GetParam()); }

  ResponseOrdering ordering() const { return std::get<1>(GetParam()); }

 private:
};

TEST_P(PaintPreviewClientRenderViewHostResponseOrderingTest,
       CaptureMainFrameWithSubframe) {
  content::RenderFrameHost* rfh = main_rfh();
  content::RenderFrameHost* subframe =
      AddChildRFH(rfh, "https://www.chromium.org");

  GURL expected_url = rfh->GetLastCommittedURL();

  PaintPreviewClient::PaintPreviewParams main_frame_params(persistence());
  main_frame_params.root_dir = temp_dir_.GetPath();
  main_frame_params.inner.is_main_frame = true;

  auto main_frame_response = NewMockPaintPreviewCaptureResponse();
  main_frame_response->embedding_token = std::nullopt;
  main_frame_response->geometry_metadata->scroll_offsets = gfx::Point(5, 10);
  main_frame_response->geometry_metadata->frame_offsets = gfx::Point(20, 30);

  auto subframe_response = NewMockPaintPreviewCaptureResponse();
  subframe_response->embedding_token = std::nullopt;
  subframe_response->geometry_metadata->scroll_offsets = gfx::Point(5, 10);
  subframe_response->geometry_metadata->frame_offsets = gfx::Point(20, 30);

  MockPaintPreviewRecorder main_frame_recorder;
  main_frame_recorder.SetExpectedParams(ToMojoParams(main_frame_params));
  main_frame_recorder.SetResponse(std::move(main_frame_response));
  base::test::TestFuture<void> main_frame_req;
  main_frame_recorder.SetReceivedRequestClosure(main_frame_req.GetCallback());
  OverrideInterface(rfh, &main_frame_recorder);

  const gfx::Rect subframe_rect(0, 0, 100, 200);
  PaintPreviewClient::PaintPreviewParams expected_subframe_params =
      PaintPreviewClient::PaintPreviewParams::CreateForTesting(
          persistence(), main_frame_params.inner.get_document_guid());
  expected_subframe_params.root_dir = temp_dir_.GetPath();
  expected_subframe_params.inner.is_main_frame = false;
  expected_subframe_params.inner.clip_rect = subframe_rect;
  MockPaintPreviewRecorder subframe_recorder;
  subframe_recorder.SetExpectedParams(ToMojoParams(expected_subframe_params));
  subframe_recorder.SetResponse(std::move(subframe_response));
  base::test::TestFuture<void> subframe_req;
  subframe_recorder.SetReceivedRequestClosure(subframe_req.GetCallback());
  OverrideInterface(subframe, &subframe_recorder);

  PaintPreviewClient::CreateForWebContents(web_contents());
  auto* client = PaintPreviewClient::FromWebContents(web_contents());
  ASSERT_NE(client, nullptr);

  CaptureResultFuture main_capture_future;
  client->CapturePaintPreview(main_frame_params.Clone(), rfh,
                              main_capture_future.GetCallback());
  ASSERT_TRUE(main_frame_req.Wait());

  client->CaptureSubframePaintPreview(
      main_frame_params.inner.get_document_guid(), subframe_rect, subframe);
  ASSERT_TRUE(subframe_req.Wait());

  switch (ordering()) {
    case ResponseOrdering::kMainFrameThenSubframe:
      main_frame_recorder.SendResponse();
      subframe_recorder.SendResponse();
      break;
    case ResponseOrdering::kSubframeThenMainFrame:
      subframe_recorder.SendResponse();
      main_frame_recorder.SendResponse();
      break;
  }

  auto [main_guid, main_status, result] = main_capture_future.Take();
  EXPECT_EQ(main_guid, main_frame_params.inner.get_document_guid());
  EXPECT_EQ(main_status, mojom::PaintPreviewStatus::kOk);

  auto token = DeserializeFrameToken(result->proto.root_frame());

  switch (persistence()) {
    case RecordingPersistence::kFileSystem:
      VerifyFilePath(result->proto.root_frame().file_path());
      ASSERT_EQ(result->proto.subframes().size(), 1);
      VerifyFilePath(result->proto.subframes()[0].file_path());
      break;

    case RecordingPersistence::kMemoryBuffer:
      EXPECT_EQ(result->serialized_skps.size(), 2u);
      EXPECT_TRUE(result->serialized_skps.contains(token));
      break;

    default:
      NOTREACHED();
  }
}

TEST_P(PaintPreviewClientRenderViewHostResponseOrderingTest,
       CaptureMainFrameWithSubframe_RedactedIframe) {
  content::RenderFrameHost* rfh = main_rfh();
  content::RenderFrameHost* subframe =
      AddChildRFH(rfh, "https://www.chromium.org");

  GURL expected_url = rfh->GetLastCommittedURL();

  PaintPreviewClient::PaintPreviewParams main_frame_params(persistence());
  main_frame_params.root_dir = temp_dir_.GetPath();
  main_frame_params.inner.is_main_frame = true;
  main_frame_params.inner.redaction_params =
      RedactionParams({url::Origin::Create(GURL(kOtherOriginUrl))}, {});

  auto main_frame_response = NewMockPaintPreviewCaptureResponse();
  main_frame_response->embedding_token = std::nullopt;
  main_frame_response->geometry_metadata->scroll_offsets = gfx::Point(0, 0);
  main_frame_response->geometry_metadata->frame_offsets = gfx::Point(0, 0);

  auto subframe_response = mojom::GeometryMetadataResponse::New();
  subframe_response->scroll_offsets = gfx::Point(0, 0);
  subframe_response->frame_offsets = gfx::Point(0, 0);

  MockPaintPreviewRecorder main_frame_recorder;
  main_frame_recorder.SetExpectedParams(ToMojoParams(main_frame_params));
  main_frame_recorder.SetResponse(std::move(main_frame_response));
  base::test::TestFuture<void> main_frame_req;
  main_frame_recorder.SetReceivedRequestClosure(main_frame_req.GetCallback());
  OverrideInterface(rfh, &main_frame_recorder);

  const gfx::Rect subframe_rect(0, 0, 100, 200);

  MockPaintPreviewRecorder subframe_recorder;
  auto subframe_geo_params = mojom::GeometryMetadataParams::New();
  subframe_geo_params->clip_rect = subframe_rect;
  subframe_recorder.SetExpectedGeometryParams(subframe_geo_params.Clone());
  subframe_recorder.SetGeometryResponse(subframe_response.Clone());
  base::test::TestFuture<void> subframe_req;
  subframe_recorder.SetReceivedRequestClosure(subframe_req.GetCallback());
  OverrideInterface(subframe, &subframe_recorder);

  PaintPreviewClient::CreateForWebContents(web_contents());
  auto* client = PaintPreviewClient::FromWebContents(web_contents());
  ASSERT_NE(client, nullptr);

  CaptureResultFuture main_capture_future;
  client->CapturePaintPreview(main_frame_params.Clone(), rfh,
                              main_capture_future.GetCallback());
  ASSERT_TRUE(main_frame_req.Wait());

  client->CaptureSubframePaintPreview(
      main_frame_params.inner.get_document_guid(), subframe_rect, subframe);
  ASSERT_TRUE(subframe_req.Wait());

  switch (ordering()) {
    case ResponseOrdering::kMainFrameThenSubframe:
      main_frame_recorder.SendResponse();
      subframe_recorder.SendGeometryResponse();
      break;
    case ResponseOrdering::kSubframeThenMainFrame:
      subframe_recorder.SendGeometryResponse();
      main_frame_recorder.SendResponse();
      break;
  }

  auto [main_guid, main_status, result] = main_capture_future.Take();
  EXPECT_EQ(main_guid, main_frame_params.inner.get_document_guid());
  EXPECT_EQ(main_status, mojom::PaintPreviewStatus::kOk);

  sk_sp<SkPicture> skp;
  switch (persistence()) {
    case RecordingPersistence::kFileSystem:
      VerifyFilePath(result->proto.root_frame().file_path());
      ASSERT_EQ(result->proto.subframes().size(), 1);
      VerifyFilePath(result->proto.subframes(0).file_path());
      skp = ReadPictureFromFile(result->proto.subframes(0).file_path());
      break;

    case RecordingPersistence::kMemoryBuffer: {
      auto subframe_token = DeserializeFrameToken(result->proto.subframes(0));
      ASSERT_THAT(result->serialized_skps,
                  UnorderedElementsAre(
                      Key(DeserializeFrameToken(result->proto.root_frame())),
                      Key(subframe_token)));
      skp = ReadPictureFromBuffer(
          std::move(result->serialized_skps.at(subframe_token)));
      break;
    }
  }

  SkBitmap subframe_bitmap = MakeBitmapFromPicture(skp);

  EXPECT_EQ(subframe_bitmap.width(), subframe_rect.width());
  EXPECT_EQ(subframe_bitmap.height(), subframe_rect.height());

  EXPECT_EQ(subframe_bitmap.getColor(0, 0), SK_ColorBLACK);
  EXPECT_EQ(subframe_bitmap.getColor(subframe_bitmap.width() - 1,
                                     subframe_bitmap.height() - 1),
            SK_ColorBLACK);
}

INSTANTIATE_TEST_SUITE_P(
    ,
    PaintPreviewClientRenderViewHostResponseOrderingTest,
    testing::Combine(testing::Values(RecordingPersistence::kFileSystem,
                                     RecordingPersistence::kMemoryBuffer),
                     testing::Values(ResponseOrdering::kMainFrameThenSubframe,
                                     ResponseOrdering::kSubframeThenMainFrame)),
    [](const testing::TestParamInfo<
        std::tuple<paint_preview::RecordingPersistence, ResponseOrdering>>&
           param) {
      return base::StrCat({
          PersistenceToString(std::get<0>(param.param)),
          "_",
          OrderingToString(std::get<1>(param.param)),
      });
    });

}  // namespace paint_preview
