// Copyright 2013 The Flutter Authors. All rights reserved.
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.

#include "flutter/shell/platform/fuchsia/flutter/external_view_embedder.h"

#include <fuchsia/math/cpp/fidl.h>
#include <fuchsia/sysmem/cpp/fidl.h>
#include <fuchsia/ui/composition/cpp/fidl.h>
#include <fuchsia/ui/views/cpp/fidl.h>
#include <lib/async-testing/test_loop.h>
#include <lib/zx/event.h>
#include <lib/zx/eventpair.h>

#include <cstdint>
#include <functional>
#include <memory>
#include <optional>
#include <string>
#include <vector>

#include "flutter/flow/embedded_views.h"
#include "flutter/fml/logging.h"
#include "flutter/fml/time/time_delta.h"
#include "flutter/fml/time/time_point.h"
#include "third_party/skia/include/core/SkColor.h"
#include "third_party/skia/include/core/SkSize.h"
#include "third_party/skia/include/core/SkSurface.h"

#include "fakes/scenic/fake_flatland.h"
#include "fakes/scenic/fake_flatland_types.h"
#include "flutter/shell/platform/fuchsia/flutter/surface_producer.h"

#include "gmock/gmock.h"  // For EXPECT_THAT and matchers
#include "gtest/gtest.h"

using fuchsia::scenic::scheduling::FramePresentedInfo;
using fuchsia::scenic::scheduling::FuturePresentationTimes;
using fuchsia::scenic::scheduling::PresentReceivedInfo;
using ::testing::_;
using ::testing::AllOf;
using ::testing::Contains;
using ::testing::ElementsAre;
using ::testing::Eq;
using ::testing::Field;
using ::testing::FieldsAre;
using ::testing::IsEmpty;
using ::testing::Matcher;
using ::testing::Pointee;
using ::testing::Property;
using ::testing::SizeIs;
using ::testing::VariantWith;

namespace flutter_runner::testing {
namespace {

constexpr static fuchsia::ui::composition::BlendMode kFirstLayerBlendMode{
    fuchsia::ui::composition::BlendMode::SRC};
constexpr static fuchsia::ui::composition::BlendMode kUpperLayerBlendMode{
    fuchsia::ui::composition::BlendMode::SRC_OVER};

class FakeSurfaceProducerSurface : public SurfaceProducerSurface {
 public:
  explicit FakeSurfaceProducerSurface(
      fidl::InterfaceRequest<fuchsia::sysmem::BufferCollectionToken>
          sysmem_token_request,
      fuchsia::ui::composition::BufferCollectionImportToken buffer_import_token,
      const SkISize& size)
      : sysmem_token_request_(std::move(sysmem_token_request)),
        buffer_import_token_(std::move(buffer_import_token)),
        surface_(SkSurfaces::Null(size.width(), size.height())) {
    zx_status_t acquire_status = zx::event::create(0, &acquire_fence_);
    if (acquire_status != ZX_OK) {
      FML_LOG(ERROR)
          << "FakeSurfaceProducerSurface: Failed to create acquire event";
    }
    zx_status_t release_status = zx::event::create(0, &release_fence_);
    if (release_status != ZX_OK) {
      FML_LOG(ERROR)
          << "FakeSurfaceProducerSurface: Failed to create release event";
    }
  }
  ~FakeSurfaceProducerSurface() override {}

  bool IsValid() const override { return true; }

  SkISize GetSize() const override {
    return SkISize::Make(surface_->width(), surface_->height());
  }

  void SetImageId(uint32_t image_id) override { image_id_ = image_id; }
  uint32_t GetImageId() override { return image_id_; }

  sk_sp<SkSurface> GetSkiaSurface() const override { return surface_; }

  fuchsia::ui::composition::BufferCollectionImportToken
  GetBufferCollectionImportToken() override {
    return std::move(buffer_import_token_);
  }

  zx::event GetAcquireFence() override {
    zx::event fence;
    acquire_fence_.duplicate(ZX_RIGHT_SAME_RIGHTS, &fence);
    return fence;
  }

  zx::event GetReleaseFence() override {
    zx::event fence;
    release_fence_.duplicate(ZX_RIGHT_SAME_RIGHTS, &fence);
    return fence;
  }

  void SetReleaseImageCallback(
      ReleaseImageCallback release_image_callback) override {}

  size_t AdvanceAndGetAge() override { return 0; }
  bool FlushSessionAcquireAndReleaseEvents() override { return true; }
  void SignalWritesFinished(
      const std::function<void(void)>& on_writes_committed) override {}

 private:
  fidl::InterfaceRequest<fuchsia::sysmem::BufferCollectionToken>
      sysmem_token_request_;
  fuchsia::ui::composition::BufferCollectionImportToken buffer_import_token_;
  zx::event acquire_fence_;
  zx::event release_fence_;

  sk_sp<SkSurface> surface_;
  uint32_t image_id_{0};
};

class FakeSurfaceProducer : public SurfaceProducer {
 public:
  explicit FakeSurfaceProducer(
      fuchsia::ui::composition::AllocatorHandle flatland_allocator)
      : flatland_allocator_(flatland_allocator.Bind()) {}
  ~FakeSurfaceProducer() override = default;

  // |SurfaceProducer|
  GrDirectContext* gr_context() const override { return nullptr; }

  // |SurfaceProducer|
  std::unique_ptr<SurfaceProducerSurface> ProduceOffscreenSurface(
      const SkISize& size) override {
    return nullptr;
  }

  // |SurfaceProducer|
  std::unique_ptr<SurfaceProducerSurface> ProduceSurface(
      const SkISize& size) override {
    auto [buffer_export_token, buffer_import_token] =
        BufferCollectionTokenPair::New();
    fuchsia::sysmem::BufferCollectionTokenHandle sysmem_token;
    auto sysmem_token_request = sysmem_token.NewRequest();

    fuchsia::ui::composition::RegisterBufferCollectionArgs
        buffer_collection_args;
    buffer_collection_args.set_export_token(std::move(buffer_export_token));
    buffer_collection_args.set_buffer_collection_token(std::move(sysmem_token));
    buffer_collection_args.set_usage(
        fuchsia::ui::composition::RegisterBufferCollectionUsage::DEFAULT);
    flatland_allocator_->RegisterBufferCollection(
        std::move(buffer_collection_args),
        [](fuchsia::ui::composition::Allocator_RegisterBufferCollection_Result
               result) {
          if (result.is_err()) {
            FAIL()
                << "fuhsia::ui::composition::RegisterBufferCollection error: "
                << static_cast<uint32_t>(result.err());
          }
        });

    return std::make_unique<FakeSurfaceProducerSurface>(
        std::move(sysmem_token_request), std::move(buffer_import_token), size);
  }

  // |SurfaceProducer|
  void SubmitSurfaces(
      std::vector<std::unique_ptr<SurfaceProducerSurface>> surfaces) override {}

  fuchsia::ui::composition::AllocatorPtr flatland_allocator_;
};

Matcher<fuchsia::ui::composition::ImageProperties> IsImageProperties(
    const fuchsia::math::SizeU& size) {
  return AllOf(
      Property("has_size", &fuchsia::ui::composition::ImageProperties::has_size,
               true),
      Property("size", &fuchsia::ui::composition::ImageProperties::size, size));
}

Matcher<fuchsia::ui::composition::ViewportProperties> IsViewportProperties(
    const fuchsia::math::SizeU& logical_size,
    const fuchsia::math::Inset& inset) {
  return AllOf(
      Property("has_logical_size",
               &fuchsia::ui::composition::ViewportProperties::has_logical_size,
               true),
      Property("logical_size",
               &fuchsia::ui::composition::ViewportProperties::logical_size,
               logical_size),
      Property("has_inset",
               &fuchsia::ui::composition::ViewportProperties::has_inset, true),
      Property("inset", &fuchsia::ui::composition::ViewportProperties::inset,
               inset));
}

Matcher<fuchsia::ui::composition::HitRegion> IsHitRegion(
    const float x,
    const float y,
    const float width,
    const float height,
    const fuchsia::ui::composition::HitTestInteraction hit_test) {
  return FieldsAre(FieldsAre(x, y, width, height), hit_test);
}

Matcher<FakeGraph> IsEmptyGraph() {
  return FieldsAre(IsEmpty(), IsEmpty(), Eq(nullptr), Eq(std::nullopt));
}

Matcher<FakeGraph> IsFlutterGraph(
    const fuchsia::ui::composition::ParentViewportWatcherPtr&
        parent_viewport_watcher,
    const fuchsia::ui::views::ViewportCreationToken& viewport_creation_token,
    const fuchsia::ui::views::ViewRef& view_ref,
    std::vector<Matcher<std::shared_ptr<FakeTransform>>> layer_matchers = {},
    fuchsia::math::VecF scale = FakeTransform::kDefaultScale) {
  auto viewport_token_koids = GetKoids(viewport_creation_token);
  auto view_ref_koids = GetKoids(view_ref);
  auto watcher_koids = GetKoids(parent_viewport_watcher);

  return FieldsAre(
      /*content_map*/ _, /*transform_map*/ _,
      Pointee(FieldsAre(
          /*id*/ _, FakeTransform::kDefaultTranslation,
          /*scale*/ scale, FakeTransform::kDefaultOrientation,
          /*clip_bounds*/ _, FakeTransform::kDefaultOpacity,
          /*children*/ ElementsAreArray(layer_matchers),
          /*content*/ Eq(nullptr), /*hit_regions*/ _)),
      Eq(FakeView{
          .view_token = viewport_token_koids.second,
          .view_ref = view_ref_koids.first,
          .view_ref_control = view_ref_koids.second,
          .view_ref_focused = ZX_KOID_INVALID,
          .focuser = ZX_KOID_INVALID,
          .touch_source = ZX_KOID_INVALID,
          .mouse_source = ZX_KOID_INVALID,
          .parent_viewport_watcher = watcher_koids.second,
      }));
}

Matcher<std::shared_ptr<FakeTransform>> IsImageLayer(
    const fuchsia::math::SizeU& layer_size,
    fuchsia::ui::composition::BlendMode blend_mode,
    std::vector<Matcher<fuchsia::ui::composition::HitRegion>>
        hit_region_matchers) {
  return Pointee(FieldsAre(
      /*id*/ _, FakeTransform::kDefaultTranslation,
      FakeTransform::kDefaultScale, FakeTransform::kDefaultOrientation,
      /*clip_bounds*/ _, FakeTransform::kDefaultOpacity,
      /*children*/ IsEmpty(),
      /*content*/
      Pointee(VariantWith<FakeImage>(FieldsAre(
          /*id*/ _, IsImageProperties(layer_size),
          FakeImage::kDefaultSampleRegion, layer_size,
          FakeImage::kDefaultOpacity, blend_mode,
          /*buffer_import_token*/ _, /*vmo_index*/ 0))),
      /* hit_regions*/ ElementsAreArray(hit_region_matchers)));
}

Matcher<std::shared_ptr<FakeTransform>> IsViewportLayer(
    const fuchsia::ui::views::ViewCreationToken& view_token,
    const fuchsia::math::SizeU& view_logical_size,
    const fuchsia::math::Inset& view_inset,
    const fuchsia::math::Vec& view_translation,
    const fuchsia::math::VecF& view_scale,
    const float view_opacity) {
  return Pointee(FieldsAre(
      /* id */ _, view_translation, view_scale,
      FakeTransform::kDefaultOrientation, /*clip_bounds*/ _, view_opacity,
      /*children*/ IsEmpty(),
      /*content*/
      Pointee(VariantWith<FakeViewport>(FieldsAre(
          /* id */ _, IsViewportProperties(view_logical_size, view_inset),
          /* viewport_token */ GetKoids(view_token).second,
          /* child_view_watcher */ _))),
      /*hit_regions*/ _));
}

Matcher<std::shared_ptr<FakeTransform>> IsClipTransformLayer(
    const fuchsia::math::Vec& transform_translation,
    const fuchsia::math::VecF& transform_scale,
    std::optional<fuchsia::math::Rect> clip_bounds,
    Matcher<std::shared_ptr<FakeTransform>> viewport_matcher) {
  return Pointee(FieldsAre(
      /* id */ _, transform_translation, transform_scale,
      FakeTransform::kDefaultOrientation, /*clip_bounds*/ clip_bounds,
      FakeTransform::kDefaultOpacity,
      /*children*/ ElementsAre(viewport_matcher),
      /*content*/ _,
      /*hit_regions*/ _));
}

Matcher<std::shared_ptr<FakeTransform>> IsInputShield() {
  return Pointee(AllOf(
      // Must not clip the hit region.
      Field("clip_bounds", &FakeTransform::clip_bounds, Eq(std::nullopt)),
      // Hit region must be "infinite".
      Field("hit_regions", &FakeTransform::hit_regions,
            Contains(flutter_runner::testing::kInfiniteHitRegion))));
}

fuchsia::ui::composition::OnNextFrameBeginValues WithPresentCredits(
    uint32_t additional_present_credits) {
  fuchsia::ui::composition::OnNextFrameBeginValues values;
  values.set_additional_present_credits(additional_present_credits);
  fuchsia::scenic::scheduling::PresentationInfo info_1;
  info_1.set_presentation_time(123);
  std::vector<fuchsia::scenic::scheduling::PresentationInfo> infos;
  infos.push_back(std::move(info_1));
  values.set_future_presentation_infos(std::move(infos));
  return values;
}

void DrawSimpleFrame(ExternalViewEmbedder& external_view_embedder,
                     SkISize frame_size,
                     float frame_dpr,
                     std::function<void(flutter::DlCanvas*)> draw_callback) {
  external_view_embedder.BeginFrame(frame_size, nullptr, frame_dpr, nullptr);
  {
    flutter::DlCanvas* root_canvas = external_view_embedder.GetRootCanvas();
    external_view_embedder.PostPrerollAction(nullptr);
    draw_callback(root_canvas);
  }
  external_view_embedder.EndFrame(false, nullptr);
  flutter::SurfaceFrame::FramebufferInfo framebuffer_info;
  framebuffer_info.supports_readback = true;
  external_view_embedder.SubmitFrame(
      nullptr, nullptr,
      std::make_unique<flutter::SurfaceFrame>(
          nullptr, std::move(framebuffer_info),
          [](const flutter::SurfaceFrame& surface_frame,
             flutter::DlCanvas* canvas) { return true; },
          frame_size));
}

void DrawFrameWithView(
    ExternalViewEmbedder& external_view_embedder,
    SkISize frame_size,
    float frame_dpr,
    int view_id,
    flutter::EmbeddedViewParams& view_params,
    std::function<void(flutter::DlCanvas*)> background_draw_callback,
    std::function<void(flutter::DlCanvas*)> overlay_draw_callback) {
  external_view_embedder.BeginFrame(frame_size, nullptr, frame_dpr, nullptr);
  {
    flutter::DlCanvas* root_canvas = external_view_embedder.GetRootCanvas();
    external_view_embedder.PrerollCompositeEmbeddedView(
        view_id, std::make_unique<flutter::EmbeddedViewParams>(view_params));
    external_view_embedder.PostPrerollAction(nullptr);
    background_draw_callback(root_canvas);
    flutter::DlCanvas* overlay_canvas =
        external_view_embedder.CompositeEmbeddedView(view_id);
    overlay_draw_callback(overlay_canvas);
  }
  external_view_embedder.EndFrame(false, nullptr);
  flutter::SurfaceFrame::FramebufferInfo framebuffer_info;
  framebuffer_info.supports_readback = true;
  external_view_embedder.SubmitFrame(
      nullptr, nullptr,
      std::make_unique<flutter::SurfaceFrame>(
          nullptr, std::move(framebuffer_info),
          [](const flutter::SurfaceFrame& surface_frame,
             flutter::DlCanvas* canvas) { return true; },
          frame_size));
}

};  // namespace

class ExternalViewEmbedderTest : public ::testing::Test {
 protected:
  ExternalViewEmbedderTest()
      : session_subloop_(loop_.StartNewLoop()),
        flatland_connection_(CreateFlatlandConnection()),
        fake_surface_producer_(
            std::make_shared<FakeSurfaceProducer>(CreateFlatlandAllocator())) {}
  ~ExternalViewEmbedderTest() override = default;

  async::TestLoop& loop() { return loop_; }

  std::shared_ptr<FakeSurfaceProducer> fake_surface_producer() {
    return fake_surface_producer_;
  }

  FakeFlatland& fake_flatland() { return fake_flatland_; }

  std::shared_ptr<FlatlandConnection> flatland_connection() {
    return flatland_connection_;
  }

 private:
  fuchsia::ui::composition::AllocatorHandle CreateFlatlandAllocator() {
    FML_CHECK(!fake_flatland_.is_allocator_connected());
    fuchsia::ui::composition::AllocatorHandle flatland_allocator =
        fake_flatland_.ConnectAllocator(session_subloop_->dispatcher());

    return flatland_allocator;
  }

  std::shared_ptr<FlatlandConnection> CreateFlatlandConnection() {
    FML_CHECK(!fake_flatland_.is_flatland_connected());
    fuchsia::ui::composition::FlatlandHandle flatland =
        fake_flatland_.ConnectFlatland(session_subloop_->dispatcher());

    const auto test_name =
        ::testing::UnitTest::GetInstance()->current_test_info()->name();
    const auto max_frames_in_flight = 1;
    const auto vsync_offset = fml::TimeDelta::Zero();
    return std::make_shared<FlatlandConnection>(
        std::move(test_name), std::move(flatland),
        /*error_callback=*/[] { FAIL(); }, /*ofpe_callback=*/[](auto...) {},
        max_frames_in_flight, vsync_offset);
  }

  // Primary loop and subloop for the FakeFlatland instance to process its
  // messages.  The subloop allocates its own zx_port_t, allowing us to use a
  // separate port for each end of the message channel, rather than sharing a
  // single one.  Dual ports allow messages and responses to be intermingled,
  // which is how production code behaves; this improves test realism.
  async::TestLoop loop_;
  std::unique_ptr<async::LoopInterface> session_subloop_;

  FakeFlatland fake_flatland_;

  std::shared_ptr<FlatlandConnection> flatland_connection_;
  std::shared_ptr<FakeSurfaceProducer> fake_surface_producer_;
};

TEST_F(ExternalViewEmbedderTest, RootScene) {
  fuchsia::ui::composition::ParentViewportWatcherPtr parent_viewport_watcher;
  fuchsia::ui::views::ViewportCreationToken viewport_creation_token;
  fuchsia::ui::views::ViewCreationToken view_creation_token;
  fuchsia::ui::views::ViewRef view_ref_clone;
  auto view_creation_token_status = zx::channel::create(
      0u, &viewport_creation_token.value, &view_creation_token.value);
  ASSERT_EQ(view_creation_token_status, ZX_OK);

  fuchsia::ui::views::ViewRefControl view_ref_control;
  fuchsia::ui::views::ViewRef view_ref;
  auto status = zx::eventpair::create(
      /*options*/ 0u, &view_ref_control.reference, &view_ref.reference);
  ASSERT_EQ(status, ZX_OK);
  view_ref_control.reference.replace(
      ZX_DEFAULT_EVENTPAIR_RIGHTS & (~ZX_RIGHT_DUPLICATE),
      &view_ref_control.reference);
  view_ref.reference.replace(ZX_RIGHTS_BASIC, &view_ref.reference);
  view_ref.Clone(&view_ref_clone);

  ExternalViewEmbedder external_view_embedder(
      std::move(view_creation_token),
      fuchsia::ui::views::ViewIdentityOnCreation{
          .view_ref = std::move(view_ref),
          .view_ref_control = std::move(view_ref_control),
      },
      fuchsia::ui::composition::ViewBoundProtocols{},
      parent_viewport_watcher.NewRequest(), flatland_connection(),
      fake_surface_producer());
  EXPECT_THAT(fake_flatland().graph(), IsEmptyGraph());

  // Pump the loop; the graph should still be empty because nothing called
  // `Present` yet.
  loop().RunUntilIdle();
  EXPECT_THAT(fake_flatland().graph(), IsEmptyGraph());

  // Pump the loop; the contents of the initial `Present` should be processed.
  flatland_connection()->Present();
  loop().RunUntilIdle();
  EXPECT_THAT(fake_flatland().graph(),
              IsFlutterGraph(parent_viewport_watcher, viewport_creation_token,
                             view_ref_clone));
}

TEST_F(ExternalViewEmbedderTest, SimpleScene) {
  fuchsia::ui::composition::ParentViewportWatcherPtr parent_viewport_watcher;
  fuchsia::ui::views::ViewportCreationToken viewport_creation_token;
  fuchsia::ui::views::ViewCreationToken view_creation_token;
  fuchsia::ui::views::ViewRef view_ref_clone;
  auto view_creation_token_status = zx::channel::create(
      0u, &viewport_creation_token.value, &view_creation_token.value);
  ASSERT_EQ(view_creation_token_status, ZX_OK);

  fuchsia::ui::views::ViewRefControl view_ref_control;
  fuchsia::ui::views::ViewRef view_ref;
  auto status = zx::eventpair::create(
      /*options*/ 0u, &view_ref_control.reference, &view_ref.reference);
  ASSERT_EQ(status, ZX_OK);
  view_ref_control.reference.replace(
      ZX_DEFAULT_EVENTPAIR_RIGHTS & (~ZX_RIGHT_DUPLICATE),
      &view_ref_control.reference);
  view_ref.reference.replace(ZX_RIGHTS_BASIC, &view_ref.reference);
  view_ref.Clone(&view_ref_clone);

  // Create the `ExternalViewEmbedder` and pump the message loop until
  // the initial scene graph is setup.
  ExternalViewEmbedder external_view_embedder(
      std::move(view_creation_token),
      fuchsia::ui::views::ViewIdentityOnCreation{
          .view_ref = std::move(view_ref),
          .view_ref_control = std::move(view_ref_control),
      },
      fuchsia::ui::composition::ViewBoundProtocols{},
      parent_viewport_watcher.NewRequest(), flatland_connection(),
      fake_surface_producer());
  flatland_connection()->Present();
  loop().RunUntilIdle();
  fake_flatland().FireOnNextFrameBeginEvent(WithPresentCredits(1u));
  loop().RunUntilIdle();
  EXPECT_THAT(fake_flatland().graph(),
              IsFlutterGraph(parent_viewport_watcher, viewport_creation_token,
                             view_ref_clone));

  // Draw the scene.  The scene graph shouldn't change yet.
  const SkISize frame_size_signed = SkISize::Make(512, 512);
  const fuchsia::math::SizeU frame_size{
      static_cast<uint32_t>(frame_size_signed.width()),
      static_cast<uint32_t>(frame_size_signed.height())};
  DrawSimpleFrame(external_view_embedder, frame_size_signed, 1.f,
                  [](flutter::DlCanvas* canvas) {
                    const SkISize layer_size = canvas->GetBaseLayerSize();
                    const SkSize canvas_size =
                        SkSize::Make(layer_size.width(), layer_size.height());
                    flutter::DlPaint rect_paint;
                    rect_paint.setColor(flutter::DlColor::kGreen());
                    canvas->Translate(canvas_size.width() / 4.f,
                                      canvas_size.height() / 2.f);
                    canvas->DrawRect(
                        SkRect::MakeWH(canvas_size.width() / 32.f,
                                       canvas_size.height() / 32.f),
                        rect_paint);
                  });
  EXPECT_THAT(fake_flatland().graph(),
              IsFlutterGraph(parent_viewport_watcher, viewport_creation_token,
                             view_ref_clone));

  // Pump the message loop. The scene updates should propagate to flatland.
  loop().RunUntilIdle();

  EXPECT_THAT(
      fake_flatland().graph(),
      IsFlutterGraph(
          parent_viewport_watcher, viewport_creation_token, view_ref_clone,
          /*layers*/
          {IsImageLayer(
              frame_size, kFirstLayerBlendMode,
              {IsHitRegion(
                  /* x */ 128.f,
                  /* y */ 256.f,
                  /* width */ 16.f,
                  /* height */ 16.f,
                  /* hit_test */
                  fuchsia::ui::composition::HitTestInteraction::DEFAULT)})}));
}

TEST_F(ExternalViewEmbedderTest, SceneWithOneView) {
  fuchsia::ui::composition::ParentViewportWatcherPtr parent_viewport_watcher;
  fuchsia::ui::views::ViewportCreationToken viewport_creation_token;
  fuchsia::ui::views::ViewCreationToken view_creation_token;
  fuchsia::ui::views::ViewRef view_ref_clone;
  auto view_creation_token_status = zx::channel::create(
      0u, &viewport_creation_token.value, &view_creation_token.value);
  ASSERT_EQ(view_creation_token_status, ZX_OK);

  fuchsia::ui::views::ViewRefControl view_ref_control;
  fuchsia::ui::views::ViewRef view_ref;
  auto status = zx::eventpair::create(
      /*options*/ 0u, &view_ref_control.reference, &view_ref.reference);
  ASSERT_EQ(status, ZX_OK);
  view_ref_control.reference.replace(
      ZX_DEFAULT_EVENTPAIR_RIGHTS & (~ZX_RIGHT_DUPLICATE),
      &view_ref_control.reference);
  view_ref.reference.replace(ZX_RIGHTS_BASIC, &view_ref.reference);
  view_ref.Clone(&view_ref_clone);

  // Create the `ExternalViewEmbedder` and pump the message loop until
  // the initial scene graph is setup.
  ExternalViewEmbedder external_view_embedder(
      std::move(view_creation_token),
      fuchsia::ui::views::ViewIdentityOnCreation{
          .view_ref = std::move(view_ref),
          .view_ref_control = std::move(view_ref_control),
      },
      fuchsia::ui::composition::ViewBoundProtocols{},
      parent_viewport_watcher.NewRequest(), flatland_connection(),
      fake_surface_producer());
  flatland_connection()->Present();
  loop().RunUntilIdle();
  fake_flatland().FireOnNextFrameBeginEvent(WithPresentCredits(1u));
  loop().RunUntilIdle();
  EXPECT_THAT(fake_flatland().graph(),
              IsFlutterGraph(parent_viewport_watcher, viewport_creation_token,
                             view_ref_clone));

  // Create the view before drawing the scene.
  const SkSize child_view_size_signed = SkSize::Make(256.f, 512.f);
  const fuchsia::math::SizeU child_view_size{
      static_cast<uint32_t>(child_view_size_signed.width()),
      static_cast<uint32_t>(child_view_size_signed.height())};
  auto [child_view_token, child_viewport_token] = ViewTokenPair::New();
  const uint32_t child_view_id = child_viewport_token.value.get();

  const int kOpacity = 200;
  const float kOpacityFloat = 200 / 255.0f;
  const fuchsia::math::VecF kScale{3.0f, 4.0f};

  auto matrix = SkMatrix::I();
  matrix.setScaleX(kScale.x);
  matrix.setScaleY(kScale.y);

  auto mutators_stack = flutter::MutatorsStack();
  mutators_stack.PushOpacity(kOpacity);
  mutators_stack.PushTransform(matrix);

  flutter::EmbeddedViewParams child_view_params(matrix, child_view_size_signed,
                                                mutators_stack);
  external_view_embedder.CreateView(
      child_view_id, []() {},
      [](fuchsia::ui::composition::ContentId,
         fuchsia::ui::composition::ChildViewWatcherHandle) {});
  const SkRect child_view_occlusion_hint = SkRect::MakeLTRB(1, 2, 3, 4);
  const fuchsia::math::Inset child_view_inset{
      static_cast<int32_t>(child_view_occlusion_hint.top()),
      static_cast<int32_t>(child_view_occlusion_hint.right()),
      static_cast<int32_t>(child_view_occlusion_hint.bottom()),
      static_cast<int32_t>(child_view_occlusion_hint.left())};
  external_view_embedder.SetViewProperties(
      child_view_id, child_view_occlusion_hint, /*hit_testable=*/false,
      /*focusable=*/false);

  // We must take into account the effect of DPR on the view scale.
  const float kDPR = 2.0f;
  const float kInvDPR = 1.f / kDPR;

  // Draw the scene. The scene graph shouldn't change yet.
  const SkISize frame_size_signed = SkISize::Make(512, 512);
  const fuchsia::math::SizeU frame_size{
      static_cast<uint32_t>(frame_size_signed.width()),
      static_cast<uint32_t>(frame_size_signed.height())};
  DrawFrameWithView(
      external_view_embedder, frame_size_signed, kDPR, child_view_id,
      child_view_params,
      [](flutter::DlCanvas* canvas) {
        const SkISize layer_size = canvas->GetBaseLayerSize();
        const SkSize canvas_size =
            SkSize::Make(layer_size.width(), layer_size.height());
        flutter::DlPaint rect_paint;
        rect_paint.setColor(flutter::DlColor::kGreen());
        canvas->Translate(canvas_size.width() / 4.f,
                          canvas_size.height() / 2.f);
        canvas->DrawRect(SkRect::MakeWH(canvas_size.width() / 32.f,
                                        canvas_size.height() / 32.f),
                         rect_paint);
      },
      [](flutter::DlCanvas* canvas) {
        const SkISize layer_size = canvas->GetBaseLayerSize();
        const SkSize canvas_size =
            SkSize::Make(layer_size.width(), layer_size.height());
        flutter::DlPaint rect_paint;
        rect_paint.setColor(flutter::DlColor::kRed());
        canvas->Translate(canvas_size.width() * 3.f / 4.f,
                          canvas_size.height() / 2.f);
        canvas->DrawRect(SkRect::MakeWH(canvas_size.width() / 32.f,
                                        canvas_size.height() / 32.f),
                         rect_paint);
      });
  EXPECT_THAT(fake_flatland().graph(),
              IsFlutterGraph(parent_viewport_watcher, viewport_creation_token,
                             view_ref_clone));

  // Pump the message loop.  The scene updates should propagate to flatland.
  loop().RunUntilIdle();
  fake_flatland().FireOnNextFrameBeginEvent(WithPresentCredits(1u));
  loop().RunUntilIdle();

  EXPECT_THAT(
      fake_flatland().graph(),
      IsFlutterGraph(
          parent_viewport_watcher, viewport_creation_token,
          view_ref_clone, /*layers*/
          {IsImageLayer(
               frame_size, kFirstLayerBlendMode,
               {IsHitRegion(
                   /* x */ 128.f,
                   /* y */ 256.f,
                   /* width */ 16.f,
                   /* height */ 16.f,
                   /* hit_test */
                   fuchsia::ui::composition::HitTestInteraction::DEFAULT)}),
           IsViewportLayer(child_view_token, child_view_size, child_view_inset,
                           {0, 0}, kScale, kOpacityFloat),
           IsImageLayer(
               frame_size, kUpperLayerBlendMode,
               {IsHitRegion(
                   /* x */ 384.f,
                   /* y */ 256.f,
                   /* width */ 16.f,
                   /* height */ 16.f,
                   /* hit_test */
                   fuchsia::ui::composition::HitTestInteraction::DEFAULT)})},
          {kInvDPR, kInvDPR}));

  // Destroy the view.  The scene graph shouldn't change yet.
  external_view_embedder.DestroyView(
      child_view_id, [](fuchsia::ui::composition::ContentId) {});
  EXPECT_THAT(
      fake_flatland().graph(),
      IsFlutterGraph(
          parent_viewport_watcher, viewport_creation_token,
          view_ref_clone, /*layers*/
          {IsImageLayer(
               frame_size, kFirstLayerBlendMode,
               {IsHitRegion(
                   /* x */ 128.f,
                   /* y */ 256.f,
                   /* width */ 16.f,
                   /* height */ 16.f,
                   /* hit_test */
                   fuchsia::ui::composition::HitTestInteraction::DEFAULT)}),
           IsViewportLayer(child_view_token, child_view_size, child_view_inset,
                           {0, 0}, kScale, kOpacityFloat),
           IsImageLayer(
               frame_size, kUpperLayerBlendMode,
               {IsHitRegion(
                   /* x */ 384.f,
                   /* y */ 256.f,
                   /* width */ 16.f,
                   /* height */ 16.f,
                   /* hit_test */
                   fuchsia::ui::composition::HitTestInteraction::DEFAULT)})},
          {kInvDPR, kInvDPR}));

  // Draw another frame without the view.  The scene graph shouldn't change yet.
  DrawSimpleFrame(external_view_embedder, frame_size_signed, 1.f,
                  [](flutter::DlCanvas* canvas) {
                    const SkISize layer_size = canvas->GetBaseLayerSize();
                    const SkSize canvas_size =
                        SkSize::Make(layer_size.width(), layer_size.height());
                    flutter::DlPaint rect_paint;
                    rect_paint.setColor(flutter::DlColor::kGreen());
                    canvas->Translate(canvas_size.width() / 4.f,
                                      canvas_size.height() / 2.f);
                    canvas->DrawRect(
                        SkRect::MakeWH(canvas_size.width() / 32.f,
                                       canvas_size.height() / 32.f),
                        rect_paint);
                  });
  EXPECT_THAT(
      fake_flatland().graph(),
      IsFlutterGraph(
          parent_viewport_watcher, viewport_creation_token,
          view_ref_clone, /*layers*/
          {IsImageLayer(
               frame_size, kFirstLayerBlendMode,
               {IsHitRegion(
                   /* x */ 128.f,
                   /* y */ 256.f,
                   /* width */ 16.f,
                   /* height */ 16.f,
                   /* hit_test */
                   fuchsia::ui::composition::HitTestInteraction::DEFAULT)}),
           IsViewportLayer(child_view_token, child_view_size, child_view_inset,
                           {0, 0}, kScale, kOpacityFloat),
           IsImageLayer(
               frame_size, kUpperLayerBlendMode,
               {IsHitRegion(
                   /* x */ 384.f,
                   /* y */ 256.f,
                   /* width */ 16.f,
                   /* height */ 16.f,
                   /* hit_test */
                   fuchsia::ui::composition::HitTestInteraction::DEFAULT)})},
          {kInvDPR, kInvDPR}));

  // Pump the message loop.  The scene updates should propagate to flatland.
  loop().RunUntilIdle();
  EXPECT_THAT(
      fake_flatland().graph(),
      IsFlutterGraph(
          parent_viewport_watcher, viewport_creation_token,
          view_ref_clone, /*layers*/
          {IsImageLayer(
              frame_size, kFirstLayerBlendMode,
              {IsHitRegion(
                  /* x */ 128.f,
                  /* y */ 256.f,
                  /* width */ 16.f,
                  /* height */ 16.f,
                  /* hit_test */
                  fuchsia::ui::composition::HitTestInteraction::DEFAULT)})}));
}

TEST_F(ExternalViewEmbedderTest, SceneWithOneClippedView) {
  fuchsia::ui::composition::ParentViewportWatcherPtr parent_viewport_watcher;
  fuchsia::ui::views::ViewportCreationToken viewport_creation_token;
  fuchsia::ui::views::ViewCreationToken view_creation_token;
  fuchsia::ui::views::ViewRef view_ref_clone;
  auto view_creation_token_status = zx::channel::create(
      0u, &viewport_creation_token.value, &view_creation_token.value);
  ASSERT_EQ(view_creation_token_status, ZX_OK);

  fuchsia::ui::views::ViewRefControl view_ref_control;
  fuchsia::ui::views::ViewRef view_ref;
  auto status = zx::eventpair::create(
      /*options*/ 0u, &view_ref_control.reference, &view_ref.reference);
  ASSERT_EQ(status, ZX_OK);
  view_ref_control.reference.replace(
      ZX_DEFAULT_EVENTPAIR_RIGHTS & (~ZX_RIGHT_DUPLICATE),
      &view_ref_control.reference);
  view_ref.reference.replace(ZX_RIGHTS_BASIC, &view_ref.reference);
  view_ref.Clone(&view_ref_clone);

  // Create the `ExternalViewEmbedder` and pump the message loop until
  // the initial scene graph is setup.
  ExternalViewEmbedder external_view_embedder(
      std::move(view_creation_token),
      fuchsia::ui::views::ViewIdentityOnCreation{
          .view_ref = std::move(view_ref),
          .view_ref_control = std::move(view_ref_control),
      },
      fuchsia::ui::composition::ViewBoundProtocols{},
      parent_viewport_watcher.NewRequest(), flatland_connection(),
      fake_surface_producer());
  flatland_connection()->Present();
  loop().RunUntilIdle();
  fake_flatland().FireOnNextFrameBeginEvent(WithPresentCredits(1u));
  loop().RunUntilIdle();
  EXPECT_THAT(fake_flatland().graph(),
              IsFlutterGraph(parent_viewport_watcher, viewport_creation_token,
                             view_ref_clone));

  // Create the view before drawing the scene.
  const SkSize child_view_size_signed = SkSize::Make(256.f, 512.f);
  const fuchsia::math::SizeU child_view_size{
      static_cast<uint32_t>(child_view_size_signed.width()),
      static_cast<uint32_t>(child_view_size_signed.height())};
  auto [child_view_token, child_viewport_token] = ViewTokenPair::New();
  const uint32_t child_view_id = child_viewport_token.value.get();

  const int kOpacity = 200;
  const float kOpacityFloat = 200 / 255.0f;
  const fuchsia::math::VecF kScale{3.0f, 4.0f};
  const int kTranslateX = 10;
  const int kTranslateY = 20;

  auto matrix = SkMatrix::I();
  matrix.setScaleX(kScale.x);
  matrix.setScaleY(kScale.y);
  matrix.setTranslateX(kTranslateX);
  matrix.setTranslateY(kTranslateY);

  SkRect kClipRect =
      SkRect::MakeXYWH(30, 40, child_view_size_signed.width() - 50,
                       child_view_size_signed.height() - 60);
  fuchsia::math::Rect kClipInMathRect = {
      static_cast<int32_t>(kClipRect.x()), static_cast<int32_t>(kClipRect.y()),
      static_cast<int32_t>(kClipRect.width()),
      static_cast<int32_t>(kClipRect.height())};

  auto mutators_stack = flutter::MutatorsStack();
  mutators_stack.PushOpacity(kOpacity);
  mutators_stack.PushTransform(matrix);
  mutators_stack.PushClipRect(kClipRect);

  flutter::EmbeddedViewParams child_view_params(matrix, child_view_size_signed,
                                                mutators_stack);
  external_view_embedder.CreateView(
      child_view_id, []() {},
      [](fuchsia::ui::composition::ContentId,
         fuchsia::ui::composition::ChildViewWatcherHandle) {});
  const SkRect child_view_occlusion_hint = SkRect::MakeLTRB(1, 2, 3, 4);
  const fuchsia::math::Inset child_view_inset{
      static_cast<int32_t>(child_view_occlusion_hint.top()),
      static_cast<int32_t>(child_view_occlusion_hint.right()),
      static_cast<int32_t>(child_view_occlusion_hint.bottom()),
      static_cast<int32_t>(child_view_occlusion_hint.left())};
  external_view_embedder.SetViewProperties(
      child_view_id, child_view_occlusion_hint, /*hit_testable=*/false,
      /*focusable=*/false);

  // We must take into account the effect of DPR on the view scale.
  const float kDPR = 2.0f;
  const float kInvDPR = 1.f / kDPR;

  // Draw the scene. The scene graph shouldn't change yet.
  const SkISize frame_size_signed = SkISize::Make(512, 512);
  const fuchsia::math::SizeU frame_size{
      static_cast<uint32_t>(frame_size_signed.width()),
      static_cast<uint32_t>(frame_size_signed.height())};
  DrawFrameWithView(
      external_view_embedder, frame_size_signed, kDPR, child_view_id,
      child_view_params,
      [](flutter::DlCanvas* canvas) {
        const SkISize layer_size = canvas->GetBaseLayerSize();
        const SkSize canvas_size =
            SkSize::Make(layer_size.width(), layer_size.height());
        flutter::DlPaint rect_paint;
        rect_paint.setColor(flutter::DlColor::kGreen());
        canvas->Translate(canvas_size.width() / 4.f,
                          canvas_size.height() / 2.f);
        canvas->DrawRect(SkRect::MakeWH(canvas_size.width() / 32.f,
                                        canvas_size.height() / 32.f),
                         rect_paint);
      },
      [](flutter::DlCanvas* canvas) {
        const SkISize layer_size = canvas->GetBaseLayerSize();
        const SkSize canvas_size =
            SkSize::Make(layer_size.width(), layer_size.height());
        flutter::DlPaint rect_paint;
        rect_paint.setColor(flutter::DlColor::kRed());
        canvas->Translate(canvas_size.width() * 3.f / 4.f,
                          canvas_size.height() / 2.f);
        canvas->DrawRect(SkRect::MakeWH(canvas_size.width() / 32.f,
                                        canvas_size.height() / 32.f),
                         rect_paint);
      });
  EXPECT_THAT(fake_flatland().graph(),
              IsFlutterGraph(parent_viewport_watcher, viewport_creation_token,
                             view_ref_clone));

  // Pump the message loop.  The scene updates should propagate to flatland.
  loop().RunUntilIdle();
  fake_flatland().FireOnNextFrameBeginEvent(WithPresentCredits(1u));
  loop().RunUntilIdle();

  EXPECT_THAT(
      fake_flatland().graph(),
      IsFlutterGraph(
          parent_viewport_watcher, viewport_creation_token,
          view_ref_clone, /*layers*/
          {IsImageLayer(
               frame_size, kFirstLayerBlendMode,
               {IsHitRegion(
                   /* x */ 128.f,
                   /* y */ 256.f,
                   /* width */ 16.f,
                   /* height */ 16.f,
                   /* hit_test */
                   fuchsia::ui::composition::HitTestInteraction::DEFAULT)}),
           IsClipTransformLayer(
               {kTranslateX, kTranslateY}, kScale, kClipInMathRect,
               IsViewportLayer(child_view_token, child_view_size,
                               child_view_inset, {0, 0},
                               FakeTransform::kDefaultScale, kOpacityFloat)),
           IsImageLayer(
               frame_size, kUpperLayerBlendMode,
               {IsHitRegion(
                   /* x */ 384.f,
                   /* y */ 256.f,
                   /* width */ 16.f,
                   /* height */ 16.f,
                   /* hit_test */
                   fuchsia::ui::composition::HitTestInteraction::DEFAULT)})},
          {kInvDPR, kInvDPR}));

  // Draw another frame with view, but get rid of the clips this time. This
  // should remove all ClipTransformLayer instances.
  auto new_matrix = SkMatrix::I();
  new_matrix.setScaleX(kScale.x);
  new_matrix.setScaleY(kScale.y);
  auto new_mutators_stack = flutter::MutatorsStack();
  new_mutators_stack.PushOpacity(kOpacity);
  new_mutators_stack.PushTransform(new_matrix);
  flutter::EmbeddedViewParams new_child_view_params(
      new_matrix, child_view_size_signed, new_mutators_stack);
  DrawFrameWithView(
      external_view_embedder, frame_size_signed, kDPR, child_view_id,
      new_child_view_params,
      [](flutter::DlCanvas* canvas) {
        const SkISize layer_size = canvas->GetBaseLayerSize();
        const SkSize canvas_size =
            SkSize::Make(layer_size.width(), layer_size.height());
        flutter::DlPaint rect_paint;
        rect_paint.setColor(flutter::DlColor::kGreen());
        canvas->Translate(canvas_size.width() / 4.f,
                          canvas_size.height() / 2.f);
        canvas->DrawRect(SkRect::MakeWH(canvas_size.width() / 32.f,
                                        canvas_size.height() / 32.f),
                         rect_paint);
      },
      [](flutter::DlCanvas* canvas) {
        const SkISize layer_size = canvas->GetBaseLayerSize();
        const SkSize canvas_size =
            SkSize::Make(layer_size.width(), layer_size.height());
        flutter::DlPaint rect_paint;
        rect_paint.setColor(flutter::DlColor::kRed());
        canvas->Translate(canvas_size.width() * 3.f / 4.f,
                          canvas_size.height() / 2.f);
        canvas->DrawRect(SkRect::MakeWH(canvas_size.width() / 32.f,
                                        canvas_size.height() / 32.f),
                         rect_paint);
      });
  loop().RunUntilIdle();
  fake_flatland().FireOnNextFrameBeginEvent(WithPresentCredits(1u));
  loop().RunUntilIdle();
  EXPECT_THAT(
      fake_flatland().graph(),
      IsFlutterGraph(
          parent_viewport_watcher, viewport_creation_token,
          view_ref_clone, /*layers*/
          {IsImageLayer(
               frame_size, kFirstLayerBlendMode,
               {IsHitRegion(
                   /* x */ 128.f,
                   /* y */ 256.f,
                   /* width */ 16.f,
                   /* height */ 16.f,
                   /* hit_test */
                   fuchsia::ui::composition::HitTestInteraction::DEFAULT)}),
           IsViewportLayer(child_view_token, child_view_size, child_view_inset,
                           {0, 0}, kScale, kOpacityFloat),
           IsImageLayer(
               frame_size, kUpperLayerBlendMode,
               {IsHitRegion(
                   /* x */ 384.f,
                   /* y */ 256.f,
                   /* width */ 16.f,
                   /* height */ 16.f,
                   /* hit_test */
                   fuchsia::ui::composition::HitTestInteraction::DEFAULT)})},
          {kInvDPR, kInvDPR}));

  // Destroy the view and draw another frame without the view.
  external_view_embedder.DestroyView(
      child_view_id, [](fuchsia::ui::composition::ContentId) {});
  DrawSimpleFrame(external_view_embedder, frame_size_signed, 1.f,
                  [](flutter::DlCanvas* canvas) {
                    const SkISize layer_size = canvas->GetBaseLayerSize();
                    const SkSize canvas_size =
                        SkSize::Make(layer_size.width(), layer_size.height());
                    flutter::DlPaint rect_paint;
                    rect_paint.setColor(flutter::DlColor::kGreen());
                    canvas->Translate(canvas_size.width() / 4.f,
                                      canvas_size.height() / 2.f);
                    canvas->DrawRect(
                        SkRect::MakeWH(canvas_size.width() / 32.f,
                                       canvas_size.height() / 32.f),
                        rect_paint);
                  });
  loop().RunUntilIdle();
  EXPECT_THAT(
      fake_flatland().graph(),
      IsFlutterGraph(
          parent_viewport_watcher, viewport_creation_token,
          view_ref_clone, /*layers*/
          {IsImageLayer(
              frame_size, kFirstLayerBlendMode,
              {IsHitRegion(
                  /* x */ 128.f,
                  /* y */ 256.f,
                  /* width */ 16.f,
                  /* height */ 16.f,
                  /* hit_test */
                  fuchsia::ui::composition::HitTestInteraction::DEFAULT)})}));
}

TEST_F(ExternalViewEmbedderTest, SceneWithOneView_NoOverlay) {
  fuchsia::ui::composition::ParentViewportWatcherPtr parent_viewport_watcher;
  fuchsia::ui::views::ViewportCreationToken viewport_creation_token;
  fuchsia::ui::views::ViewCreationToken view_creation_token;
  fuchsia::ui::views::ViewRef view_ref_clone;
  auto view_creation_token_status = zx::channel::create(
      0u, &viewport_creation_token.value, &view_creation_token.value);
  ASSERT_EQ(view_creation_token_status, ZX_OK);

  fuchsia::ui::views::ViewRefControl view_ref_control;
  fuchsia::ui::views::ViewRef view_ref;
  auto status = zx::eventpair::create(
      /*options*/ 0u, &view_ref_control.reference, &view_ref.reference);
  ASSERT_EQ(status, ZX_OK);
  view_ref_control.reference.replace(
      ZX_DEFAULT_EVENTPAIR_RIGHTS & (~ZX_RIGHT_DUPLICATE),
      &view_ref_control.reference);
  view_ref.reference.replace(ZX_RIGHTS_BASIC, &view_ref.reference);
  view_ref.Clone(&view_ref_clone);

  // Create the `ExternalViewEmbedder` and pump the message loop until
  // the initial scene graph is setup.
  ExternalViewEmbedder external_view_embedder(
      std::move(view_creation_token),
      fuchsia::ui::views::ViewIdentityOnCreation{
          .view_ref = std::move(view_ref),
          .view_ref_control = std::move(view_ref_control),
      },
      fuchsia::ui::composition::ViewBoundProtocols{},
      parent_viewport_watcher.NewRequest(), flatland_connection(),
      fake_surface_producer());
  flatland_connection()->Present();
  loop().RunUntilIdle();
  fake_flatland().FireOnNextFrameBeginEvent(WithPresentCredits(1u));
  loop().RunUntilIdle();
  EXPECT_THAT(fake_flatland().graph(),
              IsFlutterGraph(parent_viewport_watcher, viewport_creation_token,
                             view_ref_clone));

  // Create the view before drawing the scene.
  const SkSize child_view_size_signed = SkSize::Make(256.f, 512.f);
  const fuchsia::math::SizeU child_view_size{
      static_cast<uint32_t>(child_view_size_signed.width()),
      static_cast<uint32_t>(child_view_size_signed.height())};
  auto [child_view_token, child_viewport_token] = ViewTokenPair::New();
  const uint32_t child_view_id = child_viewport_token.value.get();

  const int kOpacity = 125;
  const float kOpacityFloat = 125 / 255.0f;
  const fuchsia::math::VecF kScale{2.f, 3.0f};

  auto matrix = SkMatrix::I();
  matrix.setScaleX(kScale.x);
  matrix.setScaleY(kScale.y);

  auto mutators_stack = flutter::MutatorsStack();
  mutators_stack.PushOpacity(kOpacity);
  mutators_stack.PushTransform(matrix);

  flutter::EmbeddedViewParams child_view_params(matrix, child_view_size_signed,
                                                mutators_stack);
  external_view_embedder.CreateView(
      child_view_id, []() {},
      [](fuchsia::ui::composition::ContentId,
         fuchsia::ui::composition::ChildViewWatcherHandle) {});

  // Draw the scene.  The scene graph shouldn't change yet.
  const SkISize frame_size_signed = SkISize::Make(512, 512);
  const fuchsia::math::SizeU frame_size{
      static_cast<uint32_t>(frame_size_signed.width()),
      static_cast<uint32_t>(frame_size_signed.height())};
  DrawFrameWithView(
      external_view_embedder, frame_size_signed, 1.f, child_view_id,
      child_view_params,
      [](flutter::DlCanvas* canvas) {
        const SkISize layer_size = canvas->GetBaseLayerSize();
        const SkSize canvas_size =
            SkSize::Make(layer_size.width(), layer_size.height());
        flutter::DlPaint rect_paint;
        rect_paint.setColor(flutter::DlColor::kGreen());
        canvas->Translate(canvas_size.width() / 4.f,
                          canvas_size.height() / 2.f);
        canvas->DrawRect(SkRect::MakeWH(canvas_size.width() / 32.f,
                                        canvas_size.height() / 32.f),
                         rect_paint);
      },
      [](flutter::DlCanvas* canvas) {});
  EXPECT_THAT(fake_flatland().graph(),
              IsFlutterGraph(parent_viewport_watcher, viewport_creation_token,
                             view_ref_clone));

  // Pump the message loop.  The scene updates should propagate to flatland.
  loop().RunUntilIdle();
  fake_flatland().FireOnNextFrameBeginEvent(WithPresentCredits(1u));
  loop().RunUntilIdle();
  EXPECT_THAT(
      fake_flatland().graph(),
      IsFlutterGraph(
          parent_viewport_watcher, viewport_creation_token,
          view_ref_clone, /*layers*/
          {IsImageLayer(
               frame_size, kFirstLayerBlendMode,
               {IsHitRegion(
                   /* x */ 128.f,
                   /* y */ 256.f,
                   /* width */ 16.f,
                   /* height */ 16.f,
                   /* hit_test */
                   fuchsia::ui::composition::HitTestInteraction::DEFAULT)}),
           IsViewportLayer(child_view_token, child_view_size,
                           FakeViewport::kDefaultViewportInset, {0, 0}, kScale,
                           kOpacityFloat)}));

  // Destroy the view.  The scene graph shouldn't change yet.
  external_view_embedder.DestroyView(
      child_view_id, [](fuchsia::ui::composition::ContentId) {});
  EXPECT_THAT(
      fake_flatland().graph(),
      IsFlutterGraph(
          parent_viewport_watcher, viewport_creation_token,
          view_ref_clone, /*layers*/
          {IsImageLayer(
               frame_size, kFirstLayerBlendMode,
               {IsHitRegion(
                   /* x */ 128.f,
                   /* y */ 256.f,
                   /* width */ 16.f,
                   /* height */ 16.f,
                   /* hit_test */
                   fuchsia::ui::composition::HitTestInteraction::DEFAULT)}),
           IsViewportLayer(child_view_token, child_view_size,
                           FakeViewport::kDefaultViewportInset, {0, 0}, kScale,
                           kOpacityFloat)}));

  // Draw another frame without the view.  The scene graph shouldn't change yet.
  DrawSimpleFrame(external_view_embedder, frame_size_signed, 1.f,
                  [](flutter::DlCanvas* canvas) {
                    const SkISize layer_size = canvas->GetBaseLayerSize();
                    const SkSize canvas_size =
                        SkSize::Make(layer_size.width(), layer_size.height());
                    flutter::DlPaint rect_paint;
                    rect_paint.setColor(flutter::DlColor::kGreen());
                    canvas->Translate(canvas_size.width() / 4.f,
                                      canvas_size.height() / 2.f);
                    canvas->DrawRect(
                        SkRect::MakeWH(canvas_size.width() / 32.f,
                                       canvas_size.height() / 32.f),
                        rect_paint);
                  });

  EXPECT_THAT(
      fake_flatland().graph(),
      IsFlutterGraph(
          parent_viewport_watcher, viewport_creation_token,
          view_ref_clone, /*layers*/
          {IsImageLayer(
               frame_size, kFirstLayerBlendMode,
               {IsHitRegion(
                   /* x */ 128.f,
                   /* y */ 256.f,
                   /* width */ 16.f,
                   /* height */ 16.f,
                   /* hit_test */
                   fuchsia::ui::composition::HitTestInteraction::DEFAULT)}),
           IsViewportLayer(child_view_token, child_view_size,
                           FakeViewport::kDefaultViewportInset, {0, 0}, kScale,
                           kOpacityFloat)}));

  // Pump the message loop.  The scene updates should propagate to flatland.
  loop().RunUntilIdle();
  EXPECT_THAT(
      fake_flatland().graph(),
      IsFlutterGraph(
          parent_viewport_watcher, viewport_creation_token,
          view_ref_clone, /*layers*/
          {IsImageLayer(
              frame_size, kFirstLayerBlendMode,
              {IsHitRegion(
                  /* x */ 128.f,
                  /* y */ 256.f,
                  /* width */ 16.f,
                  /* height */ 16.f,
                  /* hit_test */
                  fuchsia::ui::composition::HitTestInteraction::DEFAULT)})}));
}

TEST_F(ExternalViewEmbedderTest, SceneWithOneView_DestroyBeforeDrawing) {
  fuchsia::ui::composition::ParentViewportWatcherPtr parent_viewport_watcher;
  fuchsia::ui::views::ViewportCreationToken viewport_creation_token;
  fuchsia::ui::views::ViewCreationToken view_creation_token;
  fuchsia::ui::views::ViewRef view_ref_clone;
  auto view_creation_token_status = zx::channel::create(
      0u, &viewport_creation_token.value, &view_creation_token.value);
  ASSERT_EQ(view_creation_token_status, ZX_OK);

  fuchsia::ui::views::ViewRefControl view_ref_control;
  fuchsia::ui::views::ViewRef view_ref;
  auto status = zx::eventpair::create(
      /*options*/ 0u, &view_ref_control.reference, &view_ref.reference);
  ASSERT_EQ(status, ZX_OK);
  view_ref_control.reference.replace(
      ZX_DEFAULT_EVENTPAIR_RIGHTS & (~ZX_RIGHT_DUPLICATE),
      &view_ref_control.reference);
  view_ref.reference.replace(ZX_RIGHTS_BASIC, &view_ref.reference);
  view_ref.Clone(&view_ref_clone);

  // Create the `ExternalViewEmbedder` and pump the message loop until
  // the initial scene graph is setup.
  ExternalViewEmbedder external_view_embedder(
      std::move(view_creation_token),
      fuchsia::ui::views::ViewIdentityOnCreation{
          .view_ref = std::move(view_ref),
          .view_ref_control = std::move(view_ref_control),
      },
      fuchsia::ui::composition::ViewBoundProtocols{},
      parent_viewport_watcher.NewRequest(), flatland_connection(),
      fake_surface_producer());
  flatland_connection()->Present();
  loop().RunUntilIdle();
  fake_flatland().FireOnNextFrameBeginEvent(WithPresentCredits(1u));
  loop().RunUntilIdle();
  EXPECT_THAT(fake_flatland().graph(),
              IsFlutterGraph(parent_viewport_watcher, viewport_creation_token,
                             view_ref_clone));

  // Create the view before drawing the scene.
  auto [child_view_token, child_viewport_token] = ViewTokenPair::New();
  const uint32_t child_view_id = child_viewport_token.value.get();
  external_view_embedder.CreateView(
      child_view_id, []() {},
      [](fuchsia::ui::composition::ContentId,
         fuchsia::ui::composition::ChildViewWatcherHandle) {});

  // Draw the scene without the view. The scene graph shouldn't change yet.
  const SkISize frame_size_signed = SkISize::Make(512, 512);
  const fuchsia::math::SizeU frame_size{
      static_cast<uint32_t>(frame_size_signed.width()),
      static_cast<uint32_t>(frame_size_signed.height())};
  DrawSimpleFrame(external_view_embedder, frame_size_signed, 1.f,
                  [](flutter::DlCanvas* canvas) {
                    const SkISize layer_size = canvas->GetBaseLayerSize();
                    const SkSize canvas_size =
                        SkSize::Make(layer_size.width(), layer_size.height());
                    flutter::DlPaint rect_paint;
                    rect_paint.setColor(flutter::DlColor().kGreen());
                    canvas->Translate(canvas_size.width() / 4.f,
                                      canvas_size.height() / 2.f);
                    canvas->DrawRect(
                        SkRect::MakeWH(canvas_size.width() / 32.f,
                                       canvas_size.height() / 32.f),
                        rect_paint);
                  });

  // Pump the message loop. The scene updates should propagate to flatland.
  loop().RunUntilIdle();
  fake_flatland().FireOnNextFrameBeginEvent(WithPresentCredits(1u));
  loop().RunUntilIdle();
  EXPECT_THAT(
      fake_flatland().graph(),
      IsFlutterGraph(
          parent_viewport_watcher, viewport_creation_token, view_ref_clone,
          /*layers*/
          {IsImageLayer(
              frame_size, kFirstLayerBlendMode,
              {IsHitRegion(
                  /* x */ 128.f,
                  /* y */ 256.f,
                  /* width */ 16.f,
                  /* height */ 16.f,
                  /* hit_test */
                  fuchsia::ui::composition::HitTestInteraction::DEFAULT)})}));

  // Destroy the view.  The scene graph shouldn't change yet.
  external_view_embedder.DestroyView(
      child_view_id, [](fuchsia::ui::composition::ContentId) {});
  EXPECT_THAT(
      fake_flatland().graph(),
      IsFlutterGraph(
          parent_viewport_watcher, viewport_creation_token, view_ref_clone,
          /*layers*/
          {IsImageLayer(
              frame_size, kFirstLayerBlendMode,
              {IsHitRegion(
                  /* x */ 128.f,
                  /* y */ 256.f,
                  /* width */ 16.f,
                  /* height */ 16.f,
                  /* hit_test */
                  fuchsia::ui::composition::HitTestInteraction::DEFAULT)})}));

  // Draw another frame without the view and change the size. The scene graph
  // shouldn't change yet.
  const SkISize new_frame_size_signed = SkISize::Make(256, 256);
  const fuchsia::math::SizeU new_frame_size{
      static_cast<uint32_t>(new_frame_size_signed.width()),
      static_cast<uint32_t>(new_frame_size_signed.height())};
  DrawSimpleFrame(external_view_embedder, new_frame_size_signed, 1.f,
                  [](flutter::DlCanvas* canvas) {
                    const SkISize layer_size = canvas->GetBaseLayerSize();
                    const SkSize canvas_size =
                        SkSize::Make(layer_size.width(), layer_size.height());
                    flutter::DlPaint rect_paint;
                    rect_paint.setColor(flutter::DlColor::kGreen());
                    canvas->Translate(canvas_size.width() / 4.f,
                                      canvas_size.height() / 2.f);
                    canvas->DrawRect(
                        SkRect::MakeWH(canvas_size.width() / 32.f,
                                       canvas_size.height() / 32.f),
                        rect_paint);
                  });
  EXPECT_THAT(
      fake_flatland().graph(),
      IsFlutterGraph(
          parent_viewport_watcher, viewport_creation_token, view_ref_clone,
          /*layers*/
          {IsImageLayer(
              frame_size, kFirstLayerBlendMode,
              {IsHitRegion(
                  /* x */ 128.f,
                  /* y */ 256.f,
                  /* width */ 16.f,
                  /* height */ 16.f,
                  /* hit_test */
                  fuchsia::ui::composition::HitTestInteraction::DEFAULT)})}));

  // Pump the message loop.  The scene updates should propagate to flatland.
  loop().RunUntilIdle();
  EXPECT_THAT(
      fake_flatland().graph(),
      IsFlutterGraph(
          parent_viewport_watcher, viewport_creation_token,
          view_ref_clone, /*layers*/
          {IsImageLayer(
              new_frame_size, kFirstLayerBlendMode,
              {IsHitRegion(
                  /* x */ 64.f,
                  /* y */ 128.f,
                  /* width */ 8.f,
                  /* height */ 8.f,
                  /* hit_test */
                  fuchsia::ui::composition::HitTestInteraction::DEFAULT)})}));
}

// This test case exercises the scenario in which the view contains two disjoint
// regions with painted content; we should generate two separate hit regions
// matching the bounds of the painted regions in this case.
TEST_F(ExternalViewEmbedderTest, SimpleScene_DisjointHitRegions) {
  fuchsia::ui::composition::ParentViewportWatcherPtr parent_viewport_watcher;
  fuchsia::ui::views::ViewportCreationToken viewport_creation_token;
  fuchsia::ui::views::ViewCreationToken view_creation_token;
  fuchsia::ui::views::ViewRef view_ref_clone;
  auto view_creation_token_status = zx::channel::create(
      0u, &viewport_creation_token.value, &view_creation_token.value);
  ASSERT_EQ(view_creation_token_status, ZX_OK);

  fuchsia::ui::views::ViewRefControl view_ref_control;
  fuchsia::ui::views::ViewRef view_ref;
  auto status = zx::eventpair::create(
      /*options*/ 0u, &view_ref_control.reference, &view_ref.reference);
  ASSERT_EQ(status, ZX_OK);
  view_ref_control.reference.replace(
      ZX_DEFAULT_EVENTPAIR_RIGHTS & (~ZX_RIGHT_DUPLICATE),
      &view_ref_control.reference);
  view_ref.reference.replace(ZX_RIGHTS_BASIC, &view_ref.reference);
  view_ref.Clone(&view_ref_clone);

  // Create the `ExternalViewEmbedder` and pump the message loop until
  // the initial scene graph is setup.
  ExternalViewEmbedder external_view_embedder(
      std::move(view_creation_token),
      fuchsia::ui::views::ViewIdentityOnCreation{
          .view_ref = std::move(view_ref),
          .view_ref_control = std::move(view_ref_control),
      },
      fuchsia::ui::composition::ViewBoundProtocols{},
      parent_viewport_watcher.NewRequest(), flatland_connection(),
      fake_surface_producer());
  flatland_connection()->Present();
  loop().RunUntilIdle();
  fake_flatland().FireOnNextFrameBeginEvent(WithPresentCredits(1u));
  loop().RunUntilIdle();
  EXPECT_THAT(fake_flatland().graph(),
              IsFlutterGraph(parent_viewport_watcher, viewport_creation_token,
                             view_ref_clone));

  // Draw the scene.  The scene graph shouldn't change yet.
  const SkISize frame_size_signed = SkISize::Make(512, 512);
  const fuchsia::math::SizeU frame_size{
      static_cast<uint32_t>(frame_size_signed.width()),
      static_cast<uint32_t>(frame_size_signed.height())};
  DrawSimpleFrame(
      external_view_embedder, frame_size_signed, 1.f,
      [](flutter::DlCanvas* canvas) {
        const SkISize layer_size = canvas->GetBaseLayerSize();
        const SkSize canvas_size =
            SkSize::Make(layer_size.width(), layer_size.height());

        SkRect paint_region_1, paint_region_2;

        paint_region_1 = SkRect::MakeXYWH(
            canvas_size.width() / 4.f, canvas_size.height() / 2.f,
            canvas_size.width() / 32.f, canvas_size.height() / 32.f);

        flutter::DlPaint rect_paint;
        rect_paint.setColor(flutter::DlColor::kGreen());
        canvas->DrawRect(paint_region_1, rect_paint);

        paint_region_2 = SkRect::MakeXYWH(
            canvas_size.width() * 3.f / 4.f, canvas_size.height() / 2.f,
            canvas_size.width() / 32.f, canvas_size.height() / 32.f);

        rect_paint.setColor(flutter::DlColor::kRed());
        canvas->DrawRect(paint_region_2, rect_paint);
      });
  EXPECT_THAT(fake_flatland().graph(),
              IsFlutterGraph(parent_viewport_watcher, viewport_creation_token,
                             view_ref_clone));

  // Pump the message loop. The scene updates should propagate to flatland.
  loop().RunUntilIdle();

  EXPECT_THAT(
      fake_flatland().graph(),
      IsFlutterGraph(
          parent_viewport_watcher, viewport_creation_token, view_ref_clone,
          /*layers*/
          {IsImageLayer(
              frame_size, kFirstLayerBlendMode,
              {IsHitRegion(
                   /* x */ 128.f,
                   /* y */ 256.f,
                   /* width */ 16.f,
                   /* height */ 16.f,
                   /* hit_test */
                   fuchsia::ui::composition::HitTestInteraction::DEFAULT),
               IsHitRegion(
                   /* x */ 384.f,
                   /* y */ 256.f,
                   /* width */ 16.f,
                   /* height */ 16.f,
                   /* hit_test */
                   fuchsia::ui::composition::HitTestInteraction::DEFAULT)})}));
}

// This test case exercises the scenario in which the view contains two
// overlapping regions with painted content; we should generate one hit
// region matching the union of the bounds of the two painted regions in
// this case.
TEST_F(ExternalViewEmbedderTest, SimpleScene_OverlappingHitRegions) {
  fuchsia::ui::composition::ParentViewportWatcherPtr parent_viewport_watcher;
  fuchsia::ui::views::ViewportCreationToken viewport_creation_token;
  fuchsia::ui::views::ViewCreationToken view_creation_token;
  fuchsia::ui::views::ViewRef view_ref_clone;
  auto view_creation_token_status = zx::channel::create(
      0u, &viewport_creation_token.value, &view_creation_token.value);
  ASSERT_EQ(view_creation_token_status, ZX_OK);

  fuchsia::ui::views::ViewRefControl view_ref_control;
  fuchsia::ui::views::ViewRef view_ref;
  auto status = zx::eventpair::create(
      /*options*/ 0u, &view_ref_control.reference, &view_ref.reference);
  ASSERT_EQ(status, ZX_OK);
  view_ref_control.reference.replace(
      ZX_DEFAULT_EVENTPAIR_RIGHTS & (~ZX_RIGHT_DUPLICATE),
      &view_ref_control.reference);
  view_ref.reference.replace(ZX_RIGHTS_BASIC, &view_ref.reference);
  view_ref.Clone(&view_ref_clone);

  // Create the `ExternalViewEmbedder` and pump the message loop until
  // the initial scene graph is setup.
  ExternalViewEmbedder external_view_embedder(
      std::move(view_creation_token),
      fuchsia::ui::views::ViewIdentityOnCreation{
          .view_ref = std::move(view_ref),
          .view_ref_control = std::move(view_ref_control),
      },
      fuchsia::ui::composition::ViewBoundProtocols{},
      parent_viewport_watcher.NewRequest(), flatland_connection(),
      fake_surface_producer());
  flatland_connection()->Present();
  loop().RunUntilIdle();
  fake_flatland().FireOnNextFrameBeginEvent(WithPresentCredits(1u));
  loop().RunUntilIdle();
  EXPECT_THAT(fake_flatland().graph(),
              IsFlutterGraph(parent_viewport_watcher, viewport_creation_token,
                             view_ref_clone));

  // Draw the scene.  The scene graph shouldn't change yet.
  const SkISize frame_size_signed = SkISize::Make(512, 512);
  const fuchsia::math::SizeU frame_size{
      static_cast<uint32_t>(frame_size_signed.width()),
      static_cast<uint32_t>(frame_size_signed.height())};
  DrawSimpleFrame(
      external_view_embedder, frame_size_signed, 1.f,
      [](flutter::DlCanvas* canvas) {
        const SkISize layer_size = canvas->GetBaseLayerSize();
        const SkSize canvas_size =
            SkSize::Make(layer_size.width(), layer_size.height());

        SkRect paint_region_1, paint_region_2;

        paint_region_1 = SkRect::MakeXYWH(
            canvas_size.width() / 4.f, canvas_size.height() / 2.f,
            3.f * canvas_size.width() / 8.f, canvas_size.height() / 4.f);

        flutter::DlPaint rect_paint;
        rect_paint.setColor(flutter::DlColor::kGreen());
        canvas->DrawRect(paint_region_1, rect_paint);

        paint_region_2 = SkRect::MakeXYWH(
            canvas_size.width() * 3.f / 8.f, canvas_size.height() / 2.f,
            3.f * canvas_size.width() / 8.f, canvas_size.height() / 4.f);

        rect_paint.setColor(flutter::DlColor::kRed());
        canvas->DrawRect(paint_region_2, rect_paint);
      });
  EXPECT_THAT(fake_flatland().graph(),
              IsFlutterGraph(parent_viewport_watcher, viewport_creation_token,
                             view_ref_clone));

  // Pump the message loop. The scene updates should propagate to flatland.
  loop().RunUntilIdle();

  EXPECT_THAT(
      fake_flatland().graph(),
      IsFlutterGraph(
          parent_viewport_watcher, viewport_creation_token, view_ref_clone,
          /*layers*/
          {IsImageLayer(
              frame_size, kFirstLayerBlendMode,
              {IsHitRegion(
                  /* x */ 128.f,
                  /* y */ 256.f,
                  /* width */ 256.f,
                  /* height */ 128.f,
                  /* hit_test */
                  fuchsia::ui::composition::HitTestInteraction::DEFAULT)})}));
}

TEST_F(ExternalViewEmbedderTest, ViewportCoveredWithInputInterceptor) {
  fuchsia::ui::composition::ParentViewportWatcherPtr parent_viewport_watcher;
  fuchsia::ui::views::ViewportCreationToken viewport_creation_token;
  fuchsia::ui::views::ViewCreationToken view_creation_token;
  fuchsia::ui::views::ViewRef view_ref_clone;
  auto view_creation_token_status = zx::channel::create(
      0u, &viewport_creation_token.value, &view_creation_token.value);
  ASSERT_EQ(view_creation_token_status, ZX_OK);

  fuchsia::ui::views::ViewRefControl view_ref_control;
  fuchsia::ui::views::ViewRef view_ref;
  auto status = zx::eventpair::create(
      /*options*/ 0u, &view_ref_control.reference, &view_ref.reference);
  ASSERT_EQ(status, ZX_OK);
  view_ref_control.reference.replace(
      ZX_DEFAULT_EVENTPAIR_RIGHTS & (~ZX_RIGHT_DUPLICATE),
      &view_ref_control.reference);
  view_ref.reference.replace(ZX_RIGHTS_BASIC, &view_ref.reference);
  view_ref.Clone(&view_ref_clone);

  // Create the `ExternalViewEmbedder` and pump the message loop until
  // the initial scene graph is setup.
  ExternalViewEmbedder external_view_embedder(
      std::move(view_creation_token),
      fuchsia::ui::views::ViewIdentityOnCreation{
          .view_ref = std::move(view_ref),
          .view_ref_control = std::move(view_ref_control),
      },
      fuchsia::ui::composition::ViewBoundProtocols{},
      parent_viewport_watcher.NewRequest(), flatland_connection(),
      fake_surface_producer(),
      /*intercept_all_input=*/true  // Enables the interceptor.
  );
  flatland_connection()->Present();
  loop().RunUntilIdle();
  fake_flatland().FireOnNextFrameBeginEvent(WithPresentCredits(1u));
  loop().RunUntilIdle();
  EXPECT_THAT(fake_flatland().graph(),
              IsFlutterGraph(parent_viewport_watcher, viewport_creation_token,
                             view_ref_clone, {IsInputShield()}));

  // Create the view before drawing the scene.
  const SkSize child_view_size_signed = SkSize::Make(256.f, 512.f);
  const fuchsia::math::SizeU child_view_size{
      static_cast<uint32_t>(child_view_size_signed.width()),
      static_cast<uint32_t>(child_view_size_signed.height())};
  auto [child_view_token, child_viewport_token] = ViewTokenPair::New();
  const uint32_t child_view_id = child_viewport_token.value.get();

  const int kOpacity = 200;
  const float kOpacityFloat = 200 / 255.0f;
  const fuchsia::math::VecF kScale{3.0f, 4.0f};

  auto matrix = SkMatrix::I();
  matrix.setScaleX(kScale.x);
  matrix.setScaleY(kScale.y);

  auto mutators_stack = flutter::MutatorsStack();
  mutators_stack.PushOpacity(kOpacity);
  mutators_stack.PushTransform(matrix);

  flutter::EmbeddedViewParams child_view_params(matrix, child_view_size_signed,
                                                mutators_stack);
  external_view_embedder.CreateView(
      child_view_id, []() {},
      [](fuchsia::ui::composition::ContentId,
         fuchsia::ui::composition::ChildViewWatcherHandle) {});
  const SkRect child_view_occlusion_hint = SkRect::MakeLTRB(1, 2, 3, 4);
  const fuchsia::math::Inset child_view_inset{
      static_cast<int32_t>(child_view_occlusion_hint.top()),
      static_cast<int32_t>(child_view_occlusion_hint.right()),
      static_cast<int32_t>(child_view_occlusion_hint.bottom()),
      static_cast<int32_t>(child_view_occlusion_hint.left())};
  external_view_embedder.SetViewProperties(
      child_view_id, child_view_occlusion_hint, /*hit_testable=*/false,
      /*focusable=*/false);

  // We must take into account the effect of DPR on the view scale.
  const float kDPR = 2.0f;
  const float kInvDPR = 1.f / kDPR;

  // Draw the scene. The scene graph shouldn't change yet.
  const SkISize frame_size_signed = SkISize::Make(512, 512);
  const fuchsia::math::SizeU frame_size{
      static_cast<uint32_t>(frame_size_signed.width()),
      static_cast<uint32_t>(frame_size_signed.height())};
  DrawFrameWithView(
      external_view_embedder, frame_size_signed, kDPR, child_view_id,
      child_view_params,
      [](flutter::DlCanvas* canvas) {
        const SkISize layer_size = canvas->GetBaseLayerSize();
        const SkSize canvas_size =
            SkSize::Make(layer_size.width(), layer_size.height());
        flutter::DlPaint rect_paint;
        rect_paint.setColor(flutter::DlColor::kGreen());
        canvas->Translate(canvas_size.width() / 4.f,
                          canvas_size.height() / 2.f);
        canvas->DrawRect(SkRect::MakeWH(canvas_size.width() / 32.f,
                                        canvas_size.height() / 32.f),
                         rect_paint);
      },
      [](flutter::DlCanvas* canvas) {
        const SkISize layer_size = canvas->GetBaseLayerSize();
        const SkSize canvas_size =
            SkSize::Make(layer_size.width(), layer_size.height());
        flutter::DlPaint rect_paint;
        rect_paint.setColor(flutter::DlColor::kRed());
        canvas->Translate(canvas_size.width() * 3.f / 4.f,
                          canvas_size.height() / 2.f);
        canvas->DrawRect(SkRect::MakeWH(canvas_size.width() / 32.f,
                                        canvas_size.height() / 32.f),
                         rect_paint);
      });
  EXPECT_THAT(fake_flatland().graph(),
              IsFlutterGraph(parent_viewport_watcher, viewport_creation_token,
                             view_ref_clone, {IsInputShield()}));

  // Pump the message loop.  The scene updates should propagate to flatland.
  loop().RunUntilIdle();
  fake_flatland().FireOnNextFrameBeginEvent(WithPresentCredits(1u));
  loop().RunUntilIdle();

  EXPECT_THAT(
      fake_flatland().graph(),
      IsFlutterGraph(
          parent_viewport_watcher, viewport_creation_token,
          view_ref_clone, /*layers*/
          {IsImageLayer(
               frame_size, kFirstLayerBlendMode,
               {IsHitRegion(
                   /* x */ 128.f,
                   /* y */ 256.f,
                   /* width */ 16.f,
                   /* height */ 16.f,
                   /* hit_test */
                   fuchsia::ui::composition::HitTestInteraction::DEFAULT)}),
           IsViewportLayer(child_view_token, child_view_size, child_view_inset,
                           {0, 0}, kScale, kOpacityFloat),
           IsImageLayer(
               frame_size, kUpperLayerBlendMode,
               {IsHitRegion(
                   /* x */ 384.f,
                   /* y */ 256.f,
                   /* width */ 16.f,
                   /* height */ 16.f,
                   /* hit_test */
                   fuchsia::ui::composition::HitTestInteraction::DEFAULT)}),
           IsInputShield()},
          {kInvDPR, kInvDPR}));
}

}  // namespace flutter_runner::testing
