// 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 <strstream>
#define FML_USED_ON_EMBEDDER

#include <algorithm>
#include <chrono>
#include <ctime>
#include <future>
#include <memory>
#include <thread>
#include <utility>
#include <vector>

#if SHELL_ENABLE_GL
#include <EGL/egl.h>
#endif  // SHELL_ENABLE_GL

#include "assets/directory_asset_bundle.h"
#include "common/graphics/persistent_cache.h"
#include "flutter/flow/layers/backdrop_filter_layer.h"
#include "flutter/flow/layers/clip_rect_layer.h"
#include "flutter/flow/layers/display_list_layer.h"
#include "flutter/flow/layers/layer_raster_cache_item.h"
#include "flutter/flow/layers/platform_view_layer.h"
#include "flutter/flow/layers/transform_layer.h"
#include "flutter/fml/backtrace.h"
#include "flutter/fml/command_line.h"
#include "flutter/fml/dart/dart_converter.h"
#include "flutter/fml/make_copyable.h"
#include "flutter/fml/message_loop.h"
#include "flutter/fml/synchronization/count_down_latch.h"
#include "flutter/fml/synchronization/waitable_event.h"
#include "flutter/runtime/dart_vm.h"
#include "flutter/shell/common/platform_view.h"
#include "flutter/shell/common/rasterizer.h"
#include "flutter/shell/common/shell_test.h"
#include "flutter/shell/common/shell_test_external_view_embedder.h"
#include "flutter/shell/common/shell_test_platform_view.h"
#include "flutter/shell/common/switches.h"
#include "flutter/shell/common/thread_host.h"
#include "flutter/shell/common/vsync_waiter_fallback.h"
#include "flutter/shell/version/version.h"
#include "flutter/testing/mock_canvas.h"
#include "flutter/testing/testing.h"
#include "gmock/gmock.h"
#include "third_party/rapidjson/include/rapidjson/writer.h"
#include "third_party/skia/include/codec/SkCodecAnimation.h"
#include "third_party/tonic/converter/dart_converter.h"

#ifdef SHELL_ENABLE_VULKAN
#include "flutter/vulkan/vulkan_application.h"  // nogncheck
#endif

// CREATE_NATIVE_ENTRY is leaky by design
// NOLINTBEGIN(clang-analyzer-core.StackAddressEscape)

namespace flutter {
namespace testing {

constexpr int64_t kImplicitViewId = 0ll;

using ::testing::_;
using ::testing::Return;

namespace {
class MockPlatformViewDelegate : public PlatformView::Delegate {
  MOCK_METHOD1(OnPlatformViewCreated, void(std::unique_ptr<Surface> surface));

  MOCK_METHOD0(OnPlatformViewDestroyed, void());

  MOCK_METHOD0(OnPlatformViewScheduleFrame, void());

  MOCK_METHOD1(OnPlatformViewSetNextFrameCallback,
               void(const fml::closure& closure));

  MOCK_METHOD2(OnPlatformViewSetViewportMetrics,
               void(int64_t view_id, const ViewportMetrics& metrics));

  MOCK_METHOD1(OnPlatformViewDispatchPlatformMessage,
               void(std::unique_ptr<PlatformMessage> message));

  MOCK_METHOD1(OnPlatformViewDispatchPointerDataPacket,
               void(std::unique_ptr<PointerDataPacket> packet));

  MOCK_METHOD3(OnPlatformViewDispatchSemanticsAction,
               void(int32_t id,
                    SemanticsAction action,
                    fml::MallocMapping args));

  MOCK_METHOD1(OnPlatformViewSetSemanticsEnabled, void(bool enabled));

  MOCK_METHOD1(OnPlatformViewSetAccessibilityFeatures, void(int32_t flags));

  MOCK_METHOD1(OnPlatformViewRegisterTexture,
               void(std::shared_ptr<Texture> texture));

  MOCK_METHOD1(OnPlatformViewUnregisterTexture, void(int64_t texture_id));

  MOCK_METHOD1(OnPlatformViewMarkTextureFrameAvailable,
               void(int64_t texture_id));

  MOCK_METHOD(const Settings&,
              OnPlatformViewGetSettings,
              (),
              (const, override));

  MOCK_METHOD3(LoadDartDeferredLibrary,
               void(intptr_t loading_unit_id,
                    std::unique_ptr<const fml::Mapping> snapshot_data,
                    std::unique_ptr<const fml::Mapping> snapshot_instructions));

  MOCK_METHOD3(LoadDartDeferredLibraryError,
               void(intptr_t loading_unit_id,
                    const std::string error_message,
                    bool transient));

  MOCK_METHOD2(UpdateAssetResolverByType,
               void(std::unique_ptr<AssetResolver> updated_asset_resolver,
                    AssetResolver::AssetResolverType type));
};

class MockSurface : public Surface {
 public:
  MOCK_METHOD0(IsValid, bool());

  MOCK_METHOD1(AcquireFrame,
               std::unique_ptr<SurfaceFrame>(const SkISize& size));

  MOCK_CONST_METHOD0(GetRootTransformation, SkMatrix());

  MOCK_METHOD0(GetContext, GrDirectContext*());

  MOCK_METHOD0(MakeRenderContextCurrent, std::unique_ptr<GLContextResult>());

  MOCK_METHOD0(ClearRenderContext, bool());
};

class MockPlatformView : public PlatformView {
 public:
  MockPlatformView(MockPlatformViewDelegate& delegate,
                   const TaskRunners& task_runners)
      : PlatformView(delegate, task_runners) {}
  MOCK_METHOD0(CreateRenderingSurface, std::unique_ptr<Surface>());
  MOCK_CONST_METHOD0(GetPlatformMessageHandler,
                     std::shared_ptr<PlatformMessageHandler>());
};

class TestPlatformView : public PlatformView {
 public:
  TestPlatformView(Shell& shell, const TaskRunners& task_runners)
      : PlatformView(shell, task_runners) {}
  MOCK_METHOD0(CreateRenderingSurface, std::unique_ptr<Surface>());
};

class MockPlatformMessageHandler : public PlatformMessageHandler {
 public:
  MOCK_METHOD1(HandlePlatformMessage,
               void(std::unique_ptr<PlatformMessage> message));
  MOCK_CONST_METHOD0(DoesHandlePlatformMessageOnPlatformThread, bool());
  MOCK_METHOD2(InvokePlatformMessageResponseCallback,
               void(int response_id, std::unique_ptr<fml::Mapping> mapping));
  MOCK_METHOD1(InvokePlatformMessageEmptyResponseCallback,
               void(int response_id));
};

class MockPlatformMessageResponse : public PlatformMessageResponse {
 public:
  static fml::RefPtr<MockPlatformMessageResponse> Create() {
    return fml::AdoptRef(new MockPlatformMessageResponse());
  }
  MOCK_METHOD1(Complete, void(std::unique_ptr<fml::Mapping> data));
  MOCK_METHOD0(CompleteEmpty, void());
};
}  // namespace

class TestAssetResolver : public AssetResolver {
 public:
  TestAssetResolver(bool valid, AssetResolver::AssetResolverType type)
      : valid_(valid), type_(type) {}

  bool IsValid() const override { return true; }

  // This is used to identify if replacement was made or not.
  bool IsValidAfterAssetManagerChange() const override { return valid_; }

  AssetResolver::AssetResolverType GetType() const override { return type_; }

  std::unique_ptr<fml::Mapping> GetAsMapping(
      const std::string& asset_name) const override {
    return nullptr;
  }

  std::vector<std::unique_ptr<fml::Mapping>> GetAsMappings(
      const std::string& asset_pattern,
      const std::optional<std::string>& subdir) const override {
    return {};
  };

 private:
  bool valid_;
  AssetResolver::AssetResolverType type_;
};

class ThreadCheckingAssetResolver : public AssetResolver {
 public:
  explicit ThreadCheckingAssetResolver(
      std::shared_ptr<fml::ConcurrentMessageLoop> concurrent_loop)
      : concurrent_loop_(std::move(concurrent_loop)) {}

  // |AssetResolver|
  bool IsValid() const override { return true; }

  // |AssetResolver|
  bool IsValidAfterAssetManagerChange() const override { return true; }

  // |AssetResolver|
  AssetResolverType GetType() const {
    return AssetResolverType::kApkAssetProvider;
  }

  // |AssetResolver|
  std::unique_ptr<fml::Mapping> GetAsMapping(
      const std::string& asset_name) const override {
    if (asset_name == "FontManifest.json") {
      // This file is loaded directly by the engine.
      return nullptr;
    }
    mapping_requests.push_back(asset_name);
    EXPECT_TRUE(concurrent_loop_->RunsTasksOnCurrentThread())
        << fml::BacktraceHere();
    return nullptr;
  }

  mutable std::vector<std::string> mapping_requests;

 private:
  std::shared_ptr<fml::ConcurrentMessageLoop> concurrent_loop_;
};

static bool ValidateShell(Shell* shell) {
  if (!shell) {
    return false;
  }

  if (!shell->IsSetup()) {
    return false;
  }

  ShellTest::PlatformViewNotifyCreated(shell);

  {
    fml::AutoResetWaitableEvent latch;
    fml::TaskRunner::RunNowOrPostTask(
        shell->GetTaskRunners().GetPlatformTaskRunner(), [shell, &latch]() {
          shell->GetPlatformView()->NotifyDestroyed();
          latch.Signal();
        });
    latch.Wait();
  }

  return true;
}

static bool RasterizerHasLayerTree(Shell* shell) {
  fml::AutoResetWaitableEvent latch;
  bool has_layer_tree = false;
  fml::TaskRunner::RunNowOrPostTask(
      shell->GetTaskRunners().GetRasterTaskRunner(),
      [shell, &latch, &has_layer_tree]() {
        has_layer_tree = shell->GetRasterizer()->GetLastLayerTree() != nullptr;
        latch.Signal();
      });
  latch.Wait();
  return has_layer_tree;
}

static void ValidateDestroyPlatformView(Shell* shell) {
  ASSERT_TRUE(shell != nullptr);
  ASSERT_TRUE(shell->IsSetup());

  // To validate destroy platform view, we must ensure the rasterizer has a
  // layer tree before the platform view is destroyed.
  ASSERT_TRUE(RasterizerHasLayerTree(shell));

  ShellTest::PlatformViewNotifyDestroyed(shell);
  // Validate the layer tree is destroyed
  ASSERT_FALSE(RasterizerHasLayerTree(shell));
}

static std::string CreateFlagsString(std::vector<const char*>& flags) {
  if (flags.empty()) {
    return "";
  }
  std::string flags_string = flags[0];
  for (size_t i = 1; i < flags.size(); ++i) {
    flags_string += ",";
    flags_string += flags[i];
  }
  return flags_string;
}

static void TestDartVmFlags(std::vector<const char*>& flags) {
  std::string flags_string = CreateFlagsString(flags);
  const std::vector<fml::CommandLine::Option> options = {
      fml::CommandLine::Option("dart-flags", flags_string)};
  fml::CommandLine command_line("", options, std::vector<std::string>());
  flutter::Settings settings = flutter::SettingsFromCommandLine(command_line);
  EXPECT_EQ(settings.dart_flags.size(), flags.size());
  for (size_t i = 0; i < flags.size(); ++i) {
    EXPECT_EQ(settings.dart_flags[i], flags[i]);
  }
}

static void PostSync(const fml::RefPtr<fml::TaskRunner>& task_runner,
                     const fml::closure& task) {
  fml::AutoResetWaitableEvent latch;
  fml::TaskRunner::RunNowOrPostTask(task_runner, [&latch, &task] {
    task();
    latch.Signal();
  });
  latch.Wait();
}

static sk_sp<DisplayList> MakeSizedDisplayList(int width, int height) {
  DisplayListBuilder builder(SkRect::MakeXYWH(0, 0, width, height));
  builder.DrawRect(SkRect::MakeXYWH(0, 0, width, height),
                   DlPaint(DlColor::kRed()));
  return builder.Build();
}

TEST_F(ShellTest, InitializeWithInvalidThreads) {
  ASSERT_FALSE(DartVMRef::IsInstanceRunning());
  Settings settings = CreateSettingsForFixture();
  TaskRunners task_runners("test", nullptr, nullptr, nullptr, nullptr);
  auto shell = CreateShell(settings, task_runners);
  ASSERT_FALSE(shell);
  ASSERT_FALSE(DartVMRef::IsInstanceRunning());
}

TEST_F(ShellTest, InitializeWithDifferentThreads) {
  ASSERT_FALSE(DartVMRef::IsInstanceRunning());
  Settings settings = CreateSettingsForFixture();
  std::string name_prefix = "io.flutter.test." + GetCurrentTestName() + ".";
  ThreadHost thread_host(ThreadHost::ThreadHostConfig(
      name_prefix, ThreadHost::Type::Platform | ThreadHost::Type::RASTER |
                       ThreadHost::Type::IO | ThreadHost::Type::UI));
  ASSERT_EQ(thread_host.name_prefix, name_prefix);

  TaskRunners task_runners("test", thread_host.platform_thread->GetTaskRunner(),
                           thread_host.raster_thread->GetTaskRunner(),
                           thread_host.ui_thread->GetTaskRunner(),
                           thread_host.io_thread->GetTaskRunner());
  auto shell = CreateShell(settings, task_runners);
  ASSERT_TRUE(ValidateShell(shell.get()));
  ASSERT_TRUE(DartVMRef::IsInstanceRunning());
  DestroyShell(std::move(shell), task_runners);
  ASSERT_FALSE(DartVMRef::IsInstanceRunning());
}

TEST_F(ShellTest, InitializeWithSingleThread) {
  ASSERT_FALSE(DartVMRef::IsInstanceRunning());
  Settings settings = CreateSettingsForFixture();
  ThreadHost thread_host("io.flutter.test." + GetCurrentTestName() + ".",
                         ThreadHost::Type::Platform);
  auto task_runner = thread_host.platform_thread->GetTaskRunner();
  TaskRunners task_runners("test", task_runner, task_runner, task_runner,
                           task_runner);
  auto shell = CreateShell(settings, task_runners);
  ASSERT_TRUE(DartVMRef::IsInstanceRunning());
  ASSERT_TRUE(ValidateShell(shell.get()));
  DestroyShell(std::move(shell), task_runners);
  ASSERT_FALSE(DartVMRef::IsInstanceRunning());
}

TEST_F(ShellTest, InitializeWithSingleThreadWhichIsTheCallingThread) {
  ASSERT_FALSE(DartVMRef::IsInstanceRunning());
  Settings settings = CreateSettingsForFixture();
  fml::MessageLoop::EnsureInitializedForCurrentThread();
  auto task_runner = fml::MessageLoop::GetCurrent().GetTaskRunner();
  TaskRunners task_runners("test", task_runner, task_runner, task_runner,
                           task_runner);
  auto shell = CreateShell(settings, task_runners);
  ASSERT_TRUE(ValidateShell(shell.get()));
  ASSERT_TRUE(DartVMRef::IsInstanceRunning());
  DestroyShell(std::move(shell), task_runners);
  ASSERT_FALSE(DartVMRef::IsInstanceRunning());
}

TEST_F(ShellTest,
       InitializeWithMultipleThreadButCallingThreadAsPlatformThread) {
  ASSERT_FALSE(DartVMRef::IsInstanceRunning());
  Settings settings = CreateSettingsForFixture();
  ThreadHost thread_host(
      "io.flutter.test." + GetCurrentTestName() + ".",
      ThreadHost::Type::RASTER | ThreadHost::Type::IO | ThreadHost::Type::UI);
  fml::MessageLoop::EnsureInitializedForCurrentThread();
  TaskRunners task_runners("test",
                           fml::MessageLoop::GetCurrent().GetTaskRunner(),
                           thread_host.raster_thread->GetTaskRunner(),
                           thread_host.ui_thread->GetTaskRunner(),
                           thread_host.io_thread->GetTaskRunner());
  auto shell = Shell::Create(
      flutter::PlatformData(), task_runners, settings,
      [](Shell& shell) {
        // This is unused in the platform view as we are not using the simulated
        // vsync mechanism. We should have better DI in the tests.
        const auto vsync_clock = std::make_shared<ShellTestVsyncClock>();
        return ShellTestPlatformView::Create(
            shell, shell.GetTaskRunners(), vsync_clock,
            [task_runners = shell.GetTaskRunners()]() {
              return static_cast<std::unique_ptr<VsyncWaiter>>(
                  std::make_unique<VsyncWaiterFallback>(task_runners));
            },
            ShellTestPlatformView::BackendType::kDefaultBackend, nullptr,
            shell.GetIsGpuDisabledSyncSwitch());
      },
      [](Shell& shell) { return std::make_unique<Rasterizer>(shell); });
  ASSERT_TRUE(ValidateShell(shell.get()));
  ASSERT_TRUE(DartVMRef::IsInstanceRunning());
  DestroyShell(std::move(shell), task_runners);
  ASSERT_FALSE(DartVMRef::IsInstanceRunning());
}

TEST_F(ShellTest, InitializeWithDisabledGpu) {
  ASSERT_FALSE(DartVMRef::IsInstanceRunning());
  Settings settings = CreateSettingsForFixture();
  ThreadHost thread_host("io.flutter.test." + GetCurrentTestName() + ".",
                         ThreadHost::Type::Platform);
  auto task_runner = thread_host.platform_thread->GetTaskRunner();
  TaskRunners task_runners("test", task_runner, task_runner, task_runner,
                           task_runner);
  auto shell = CreateShell({
      .settings = settings,
      .task_runners = task_runners,
      .is_gpu_disabled = true,
  });
  ASSERT_TRUE(DartVMRef::IsInstanceRunning());
  ASSERT_TRUE(ValidateShell(shell.get()));

  bool is_disabled = false;
  shell->GetIsGpuDisabledSyncSwitch()->Execute(
      fml::SyncSwitch::Handlers().SetIfTrue([&] { is_disabled = true; }));
  ASSERT_TRUE(is_disabled);

  DestroyShell(std::move(shell), task_runners);
  ASSERT_FALSE(DartVMRef::IsInstanceRunning());
}

TEST_F(ShellTest, InitializeWithGPUAndPlatformThreadsTheSame) {
  ASSERT_FALSE(DartVMRef::IsInstanceRunning());
  Settings settings = CreateSettingsForFixture();
  ThreadHost thread_host(
      "io.flutter.test." + GetCurrentTestName() + ".",
      ThreadHost::Type::Platform | ThreadHost::Type::IO | ThreadHost::Type::UI);
  TaskRunners task_runners(
      "test",
      thread_host.platform_thread->GetTaskRunner(),  // platform
      thread_host.platform_thread->GetTaskRunner(),  // raster
      thread_host.ui_thread->GetTaskRunner(),        // ui
      thread_host.io_thread->GetTaskRunner()         // io
  );
  auto shell = CreateShell(settings, task_runners);
  ASSERT_TRUE(DartVMRef::IsInstanceRunning());
  ASSERT_TRUE(ValidateShell(shell.get()));
  DestroyShell(std::move(shell), task_runners);
  ASSERT_FALSE(DartVMRef::IsInstanceRunning());
}

TEST_F(ShellTest, FixturesAreFunctional) {
  ASSERT_FALSE(DartVMRef::IsInstanceRunning());
  auto settings = CreateSettingsForFixture();
  auto shell = CreateShell(settings);
  ASSERT_TRUE(ValidateShell(shell.get()));

  auto configuration = RunConfiguration::InferFromSettings(settings);
  ASSERT_TRUE(configuration.IsValid());
  configuration.SetEntrypoint("fixturesAreFunctionalMain");

  fml::AutoResetWaitableEvent main_latch;
  AddNativeCallback(
      "SayHiFromFixturesAreFunctionalMain",
      CREATE_NATIVE_ENTRY([&main_latch](auto args) { main_latch.Signal(); }));

  RunEngine(shell.get(), std::move(configuration));
  main_latch.Wait();
  ASSERT_TRUE(DartVMRef::IsInstanceRunning());
  DestroyShell(std::move(shell));
  ASSERT_FALSE(DartVMRef::IsInstanceRunning());
}

TEST_F(ShellTest, SecondaryIsolateBindingsAreSetupViaShellSettings) {
  ASSERT_FALSE(DartVMRef::IsInstanceRunning());
  auto settings = CreateSettingsForFixture();
  auto shell = CreateShell(settings);
  ASSERT_TRUE(ValidateShell(shell.get()));

  auto configuration = RunConfiguration::InferFromSettings(settings);
  ASSERT_TRUE(configuration.IsValid());
  configuration.SetEntrypoint("testCanLaunchSecondaryIsolate");

  fml::CountDownLatch latch(2);
  AddNativeCallback("NotifyNative", CREATE_NATIVE_ENTRY([&latch](auto args) {
                      latch.CountDown();
                    }));

  RunEngine(shell.get(), std::move(configuration));

  latch.Wait();

  ASSERT_TRUE(DartVMRef::IsInstanceRunning());
  DestroyShell(std::move(shell));
  ASSERT_FALSE(DartVMRef::IsInstanceRunning());
}

TEST_F(ShellTest, LastEntrypoint) {
  ASSERT_FALSE(DartVMRef::IsInstanceRunning());
  auto settings = CreateSettingsForFixture();
  auto shell = CreateShell(settings);
  ASSERT_TRUE(ValidateShell(shell.get()));

  auto configuration = RunConfiguration::InferFromSettings(settings);
  ASSERT_TRUE(configuration.IsValid());
  std::string entry_point = "fixturesAreFunctionalMain";
  configuration.SetEntrypoint(entry_point);

  fml::AutoResetWaitableEvent main_latch;
  std::string last_entry_point;
  AddNativeCallback(
      "SayHiFromFixturesAreFunctionalMain", CREATE_NATIVE_ENTRY([&](auto args) {
        last_entry_point = shell->GetEngine()->GetLastEntrypoint();
        main_latch.Signal();
      }));

  RunEngine(shell.get(), std::move(configuration));
  main_latch.Wait();
  EXPECT_EQ(entry_point, last_entry_point);
  ASSERT_TRUE(DartVMRef::IsInstanceRunning());
  DestroyShell(std::move(shell));
  ASSERT_FALSE(DartVMRef::IsInstanceRunning());
}

TEST_F(ShellTest, LastEntrypointArgs) {
  ASSERT_FALSE(DartVMRef::IsInstanceRunning());
  auto settings = CreateSettingsForFixture();
  auto shell = CreateShell(settings);
  ASSERT_TRUE(ValidateShell(shell.get()));

  auto configuration = RunConfiguration::InferFromSettings(settings);
  ASSERT_TRUE(configuration.IsValid());
  std::string entry_point = "fixturesAreFunctionalMain";
  std::vector<std::string> entry_point_args = {"arg1"};
  configuration.SetEntrypoint(entry_point);
  configuration.SetEntrypointArgs(entry_point_args);

  fml::AutoResetWaitableEvent main_latch;
  std::vector<std::string> last_entry_point_args;
  AddNativeCallback(
      "SayHiFromFixturesAreFunctionalMain", CREATE_NATIVE_ENTRY([&](auto args) {
        last_entry_point_args = shell->GetEngine()->GetLastEntrypointArgs();
        main_latch.Signal();
      }));

  RunEngine(shell.get(), std::move(configuration));
  main_latch.Wait();
#if (FLUTTER_RUNTIME_MODE == FLUTTER_RUNTIME_MODE_DEBUG)
  EXPECT_EQ(last_entry_point_args, entry_point_args);
#else
  ASSERT_TRUE(last_entry_point_args.empty());
#endif
  ASSERT_TRUE(DartVMRef::IsInstanceRunning());
  DestroyShell(std::move(shell));
  ASSERT_FALSE(DartVMRef::IsInstanceRunning());
}

TEST_F(ShellTest, DisallowedDartVMFlag) {
#if defined(OS_FUCHSIA)
  GTEST_SKIP() << "This test flakes on Fuchsia. https://fxbug.dev/110006 ";
#else

  // Run this test in a thread-safe manner, otherwise gtest will complain.
  ::testing::FLAGS_gtest_death_test_style = "threadsafe";

  const std::vector<fml::CommandLine::Option> options = {
      fml::CommandLine::Option("dart-flags", "--verify_after_gc")};
  fml::CommandLine command_line("", options, std::vector<std::string>());

  // Upon encountering a disallowed Dart flag the process terminates.
  const char* expected =
      "Encountered disallowed Dart VM flag: --verify_after_gc";
  ASSERT_DEATH(flutter::SettingsFromCommandLine(command_line), expected);
#endif  // OS_FUCHSIA
}

TEST_F(ShellTest, AllowedDartVMFlag) {
  std::vector<const char*> flags = {
      "--enable-isolate-groups",
      "--no-enable-isolate-groups",
  };
#if !FLUTTER_RELEASE
  flags.push_back("--max_profile_depth 1");
  flags.push_back("--random_seed 42");
  flags.push_back("--max_subtype_cache_entries=22");
  if (!DartVM::IsRunningPrecompiledCode()) {
    flags.push_back("--enable_mirrors");
  }
#endif

  TestDartVmFlags(flags);
}

TEST_F(ShellTest, NoNeedToReportTimingsByDefault) {
  auto settings = CreateSettingsForFixture();
  std::unique_ptr<Shell> shell = CreateShell(settings);

  // Create the surface needed by rasterizer
  PlatformViewNotifyCreated(shell.get());

  auto configuration = RunConfiguration::InferFromSettings(settings);
  configuration.SetEntrypoint("emptyMain");

  RunEngine(shell.get(), std::move(configuration));
  PumpOneFrame(shell.get());
  ASSERT_FALSE(GetNeedsReportTimings(shell.get()));

  // This assertion may or may not be the direct result of needs_report_timings_
  // being false. The count could be 0 simply because we just cleared
  // unreported timings by reporting them. Hence this can't replace the
  // ASSERT_FALSE(GetNeedsReportTimings(shell.get())) check. We added
  // this assertion for an additional confidence that we're not pushing
  // back to unreported timings unnecessarily.
  //
  // Conversely, do not assert UnreportedTimingsCount(shell.get()) to be
  // positive in any tests. Otherwise those tests will be flaky as the clearing
  // of unreported timings is unpredictive.
  ASSERT_EQ(UnreportedTimingsCount(shell.get()), 0);
  DestroyShell(std::move(shell));
}

TEST_F(ShellTest, NeedsReportTimingsIsSetWithCallback) {
  auto settings = CreateSettingsForFixture();
  std::unique_ptr<Shell> shell = CreateShell(settings);

  // Create the surface needed by rasterizer
  PlatformViewNotifyCreated(shell.get());

  auto configuration = RunConfiguration::InferFromSettings(settings);
  configuration.SetEntrypoint("dummyReportTimingsMain");

  RunEngine(shell.get(), std::move(configuration));
  PumpOneFrame(shell.get());
  ASSERT_TRUE(GetNeedsReportTimings(shell.get()));
  DestroyShell(std::move(shell));
}

static void CheckFrameTimings(const std::vector<FrameTiming>& timings,
                              fml::TimePoint start,
                              fml::TimePoint finish) {
  fml::TimePoint last_frame_start;
  for (size_t i = 0; i < timings.size(); i += 1) {
    // Ensure that timings are sorted.
    ASSERT_TRUE(timings[i].Get(FrameTiming::kPhases[0]) >= last_frame_start);
    last_frame_start = timings[i].Get(FrameTiming::kPhases[0]);

    fml::TimePoint last_phase_time;
    for (auto phase : FrameTiming::kPhases) {
      // raster finish wall time doesn't use the same clock base
      // as rest of the frame timings.
      if (phase == FrameTiming::kRasterFinishWallTime) {
        continue;
      }

      ASSERT_TRUE(timings[i].Get(phase) >= start);
      ASSERT_TRUE(timings[i].Get(phase) <= finish);

      // phases should have weakly increasing time points
      ASSERT_TRUE(last_phase_time <= timings[i].Get(phase));
      last_phase_time = timings[i].Get(phase);
    }
  }
}

TEST_F(ShellTest, ReportTimingsIsCalled) {
  auto settings = CreateSettingsForFixture();
  std::unique_ptr<Shell> shell = CreateShell(settings);

  // We MUST put |start| after |CreateShell| because the clock source will be
  // reset through |TimePoint::SetClockSource()| in
  // |DartVMInitializer::Initialize()| within |CreateShell()|.
  fml::TimePoint start = fml::TimePoint::Now();

  // Create the surface needed by rasterizer
  PlatformViewNotifyCreated(shell.get());

  auto configuration = RunConfiguration::InferFromSettings(settings);
  ASSERT_TRUE(configuration.IsValid());
  configuration.SetEntrypoint("reportTimingsMain");
  fml::AutoResetWaitableEvent reportLatch;
  std::vector<int64_t> timestamps;
  auto nativeTimingCallback = [&reportLatch,
                               &timestamps](Dart_NativeArguments args) {
    Dart_Handle exception = nullptr;
    ASSERT_EQ(timestamps.size(), 0ul);
    timestamps = tonic::DartConverter<std::vector<int64_t>>::FromArguments(
        args, 0, exception);
    reportLatch.Signal();
  };
  AddNativeCallback("NativeReportTimingsCallback",
                    CREATE_NATIVE_ENTRY(nativeTimingCallback));
  RunEngine(shell.get(), std::move(configuration));

  // Pump many frames so we can trigger the report quickly instead of waiting
  // for the 1 second threshold.
  for (int i = 0; i < 200; i += 1) {
    PumpOneFrame(shell.get());
  }

  reportLatch.Wait();
  DestroyShell(std::move(shell));

  fml::TimePoint finish = fml::TimePoint::Now();
  ASSERT_TRUE(!timestamps.empty());
  ASSERT_TRUE(timestamps.size() % FrameTiming::kCount == 0);
  std::vector<FrameTiming> timings(timestamps.size() / FrameTiming::kCount);

  for (size_t i = 0; i * FrameTiming::kCount < timestamps.size(); i += 1) {
    for (auto phase : FrameTiming::kPhases) {
      timings[i].Set(
          phase,
          fml::TimePoint::FromEpochDelta(fml::TimeDelta::FromMicroseconds(
              timestamps[i * FrameTiming::kCount + phase])));
    }
  }
  CheckFrameTimings(timings, start, finish);
}

TEST_F(ShellTest, FrameRasterizedCallbackIsCalled) {
  auto settings = CreateSettingsForFixture();

  FrameTiming timing;
  fml::AutoResetWaitableEvent timingLatch;
  settings.frame_rasterized_callback = [&timing,
                                        &timingLatch](const FrameTiming& t) {
    timing = t;
    timingLatch.Signal();
  };

  std::unique_ptr<Shell> shell = CreateShell(settings);

  // Wait to make |start| bigger than zero
  using namespace std::chrono_literals;
  std::this_thread::sleep_for(1ms);

  // We MUST put |start| after |CreateShell()| because the clock source will be
  // reset through |TimePoint::SetClockSource()| in
  // |DartVMInitializer::Initialize()| within |CreateShell()|.
  fml::TimePoint start = fml::TimePoint::Now();

  for (auto phase : FrameTiming::kPhases) {
    timing.Set(phase, fml::TimePoint());
    // Check that the time points are initially smaller than start, so
    // CheckFrameTimings will fail if they're not properly set later.
    ASSERT_TRUE(timing.Get(phase) < start);
  }

  // Create the surface needed by rasterizer
  PlatformViewNotifyCreated(shell.get());

  auto configuration = RunConfiguration::InferFromSettings(settings);
  configuration.SetEntrypoint("onBeginFrameMain");

  int64_t frame_target_time;
  auto nativeOnBeginFrame = [&frame_target_time](Dart_NativeArguments args) {
    Dart_Handle exception = nullptr;
    frame_target_time =
        tonic::DartConverter<int64_t>::FromArguments(args, 0, exception);
  };
  AddNativeCallback("NativeOnBeginFrame",
                    CREATE_NATIVE_ENTRY(nativeOnBeginFrame));

  RunEngine(shell.get(), std::move(configuration));
  PumpOneFrame(shell.get());

  // Check that timing is properly set. This implies that
  // settings.frame_rasterized_callback is called.
  timingLatch.Wait();
  fml::TimePoint finish = fml::TimePoint::Now();
  std::vector<FrameTiming> timings = {timing};
  CheckFrameTimings(timings, start, finish);

  // Check that onBeginFrame, which is the frame_target_time, is after
  // FrameTiming's build start
  int64_t build_start =
      timing.Get(FrameTiming::kBuildStart).ToEpochDelta().ToMicroseconds();
  ASSERT_GT(frame_target_time, build_start);
  DestroyShell(std::move(shell));
}

TEST_F(ShellTest, ExternalEmbedderNoThreadMerger) {
  auto settings = CreateSettingsForFixture();
  fml::AutoResetWaitableEvent end_frame_latch;
  bool end_frame_called = false;
  auto end_frame_callback =
      [&](bool should_resubmit_frame,
          const fml::RefPtr<fml::RasterThreadMerger>& raster_thread_merger) {
        ASSERT_TRUE(raster_thread_merger.get() == nullptr);
        ASSERT_FALSE(should_resubmit_frame);
        end_frame_called = true;
        end_frame_latch.Signal();
      };
  auto external_view_embedder = std::make_shared<ShellTestExternalViewEmbedder>(
      end_frame_callback, PostPrerollResult::kResubmitFrame, false);
  auto shell = CreateShell({
      .settings = settings,
      .platform_view_create_callback = ShellTestPlatformViewBuilder({
          .shell_test_external_view_embedder = external_view_embedder,
      }),
  });

  // Create the surface needed by rasterizer
  PlatformViewNotifyCreated(shell.get());

  auto configuration = RunConfiguration::InferFromSettings(settings);
  configuration.SetEntrypoint("emptyMain");

  RunEngine(shell.get(), std::move(configuration));

  LayerTreeBuilder builder = [&](const std::shared_ptr<ContainerLayer>& root) {
    auto display_list_layer = std::make_shared<DisplayListLayer>(
        SkPoint::Make(10, 10), MakeSizedDisplayList(80, 80), false, false);
    root->Add(display_list_layer);
  };

  PumpOneFrame(shell.get(), 100, 100, builder);
  end_frame_latch.Wait();
  ASSERT_TRUE(end_frame_called);

  DestroyShell(std::move(shell));
}

TEST_F(ShellTest, PushBackdropFilterToVisitedPlatformViews) {
#if defined(OS_FUCHSIA)
  GTEST_SKIP() << "RasterThreadMerger flakes on Fuchsia. "
                  "https://github.com/flutter/flutter/issues/59816 ";
#else

  auto settings = CreateSettingsForFixture();

  std::shared_ptr<ShellTestExternalViewEmbedder> external_view_embedder;

  fml::AutoResetWaitableEvent end_frame_latch;
  bool end_frame_called = false;
  std::vector<int64_t> visited_platform_views;
  MutatorsStack stack_50;
  MutatorsStack stack_75;
  auto end_frame_callback =
      [&](bool should_resubmit_frame,
          const fml::RefPtr<fml::RasterThreadMerger>& raster_thread_merger) {
        if (end_frame_called) {
          return;
        }
        ASSERT_TRUE(raster_thread_merger.get() == nullptr);
        ASSERT_FALSE(should_resubmit_frame);
        end_frame_called = true;
        visited_platform_views =
            external_view_embedder->GetVisitedPlatformViews();
        stack_50 = external_view_embedder->GetStack(50);
        stack_75 = external_view_embedder->GetStack(75);
        end_frame_latch.Signal();
      };

  external_view_embedder = std::make_shared<ShellTestExternalViewEmbedder>(
      end_frame_callback, PostPrerollResult::kResubmitFrame, false);
  auto shell = CreateShell({
      .settings = settings,
      .platform_view_create_callback = ShellTestPlatformViewBuilder({
          .shell_test_external_view_embedder = external_view_embedder,
      }),
  });

  // Create the surface needed by rasterizer
  PlatformViewNotifyCreated(shell.get());

  auto configuration = RunConfiguration::InferFromSettings(settings);
  configuration.SetEntrypoint("emptyMain");

  RunEngine(shell.get(), std::move(configuration));

  LayerTreeBuilder builder = [&](const std::shared_ptr<ContainerLayer>& root) {
    auto platform_view_layer = std::make_shared<PlatformViewLayer>(
        SkPoint::Make(10, 10), SkSize::Make(10, 10), 50);
    root->Add(platform_view_layer);
    auto transform_layer =
        std::make_shared<TransformLayer>(SkMatrix::Translate(1, 1));
    root->Add(transform_layer);
    auto clip_rect_layer = std::make_shared<ClipRectLayer>(
        SkRect::MakeLTRB(0, 0, 30, 30), Clip::hardEdge);
    transform_layer->Add(clip_rect_layer);
    auto filter = std::make_shared<DlBlurImageFilter>(5, 5, DlTileMode::kClamp);
    auto backdrop_filter_layer =
        std::make_shared<BackdropFilterLayer>(filter, DlBlendMode::kSrcOver);
    clip_rect_layer->Add(backdrop_filter_layer);
    auto platform_view_layer2 = std::make_shared<PlatformViewLayer>(
        SkPoint::Make(10, 10), SkSize::Make(10, 10), 75);
    backdrop_filter_layer->Add(platform_view_layer2);
  };

  PumpOneFrame(shell.get(), 100, 100, builder);
  end_frame_latch.Wait();
  ASSERT_EQ(visited_platform_views, (std::vector<int64_t>{50, 75}));
  ASSERT_TRUE(stack_75.is_empty());
  ASSERT_FALSE(stack_50.is_empty());

  auto filter = DlBlurImageFilter(5, 5, DlTileMode::kClamp);
  auto mutator = *stack_50.Begin();
  ASSERT_EQ(mutator->GetType(), MutatorType::kBackdropFilter);
  ASSERT_EQ(mutator->GetFilterMutation().GetFilter(), filter);
  // Make sure the filterRect is in global coordinates (contains the (1,1)
  // translation).
  ASSERT_EQ(mutator->GetFilterMutation().GetFilterRect(),
            SkRect::MakeLTRB(1, 1, 31, 31));

  DestroyShell(std::move(shell));
#endif  // OS_FUCHSIA
}

// TODO(https://github.com/flutter/flutter/issues/59816): Enable on fuchsia.
TEST_F(ShellTest,
       ExternalEmbedderEndFrameIsCalledWhenPostPrerollResultIsResubmit) {
#if defined(OS_FUCHSIA)
  GTEST_SKIP() << "RasterThreadMerger flakes on Fuchsia. "
                  "https://github.com/flutter/flutter/issues/59816 ";
#else

  auto settings = CreateSettingsForFixture();
  fml::AutoResetWaitableEvent end_frame_latch;
  bool end_frame_called = false;
  auto end_frame_callback =
      [&](bool should_resubmit_frame,
          const fml::RefPtr<fml::RasterThreadMerger>& raster_thread_merger) {
        ASSERT_TRUE(raster_thread_merger.get() != nullptr);
        ASSERT_TRUE(should_resubmit_frame);
        end_frame_called = true;
        end_frame_latch.Signal();
      };
  auto external_view_embedder = std::make_shared<ShellTestExternalViewEmbedder>(
      end_frame_callback, PostPrerollResult::kResubmitFrame, true);
  auto shell = CreateShell({
      .settings = settings,
      .platform_view_create_callback = ShellTestPlatformViewBuilder({
          .shell_test_external_view_embedder = external_view_embedder,
      }),
  });

  // Create the surface needed by rasterizer
  PlatformViewNotifyCreated(shell.get());

  auto configuration = RunConfiguration::InferFromSettings(settings);
  configuration.SetEntrypoint("emptyMain");

  RunEngine(shell.get(), std::move(configuration));

  LayerTreeBuilder builder = [&](const std::shared_ptr<ContainerLayer>& root) {
    auto display_list_layer = std::make_shared<DisplayListLayer>(
        SkPoint::Make(10, 10), MakeSizedDisplayList(80, 80), false, false);
    root->Add(display_list_layer);
  };

  PumpOneFrame(shell.get(), 100, 100, builder);
  end_frame_latch.Wait();

  ASSERT_TRUE(end_frame_called);

  DestroyShell(std::move(shell));
#endif  // OS_FUCHSIA
}

TEST_F(ShellTest, OnPlatformViewDestroyDisablesThreadMerger) {
#if defined(OS_FUCHSIA)
  GTEST_SKIP() << "RasterThreadMerger flakes on Fuchsia. "
                  "https://github.com/flutter/flutter/issues/59816 ";
#else

  auto settings = CreateSettingsForFixture();
  fml::RefPtr<fml::RasterThreadMerger> raster_thread_merger;
  auto end_frame_callback =
      [&](bool should_resubmit_frame,
          fml::RefPtr<fml::RasterThreadMerger> thread_merger) {
        raster_thread_merger = std::move(thread_merger);
      };
  auto external_view_embedder = std::make_shared<ShellTestExternalViewEmbedder>(
      end_frame_callback, PostPrerollResult::kSuccess, true);

  auto shell = CreateShell({
      .settings = settings,
      .platform_view_create_callback = ShellTestPlatformViewBuilder({
          .shell_test_external_view_embedder = external_view_embedder,
      }),
  });

  // Create the surface needed by rasterizer
  PlatformViewNotifyCreated(shell.get());

  auto configuration = RunConfiguration::InferFromSettings(settings);
  configuration.SetEntrypoint("emptyMain");

  RunEngine(shell.get(), std::move(configuration));

  LayerTreeBuilder builder = [&](const std::shared_ptr<ContainerLayer>& root) {
    auto display_list_layer = std::make_shared<DisplayListLayer>(
        SkPoint::Make(10, 10), MakeSizedDisplayList(80, 80), false, false);
    root->Add(display_list_layer);
  };

  PumpOneFrame(shell.get(), 100, 100, builder);

  auto result = shell->WaitForFirstFrame(fml::TimeDelta::Max());
  ASSERT_TRUE(result.ok()) << "Result: " << static_cast<int>(result.code())
                           << ": " << result.message();

  ASSERT_TRUE(raster_thread_merger->IsEnabled());

  ValidateDestroyPlatformView(shell.get());
  ASSERT_TRUE(raster_thread_merger->IsEnabled());

  // Validate the platform view can be recreated and destroyed again
  ValidateShell(shell.get());
  ASSERT_TRUE(raster_thread_merger->IsEnabled());
  DestroyShell(std::move(shell));
#endif  // OS_FUCHSIA
}

TEST_F(ShellTest, OnPlatformViewDestroyAfterMergingThreads) {
#if defined(OS_FUCHSIA)
  GTEST_SKIP() << "RasterThreadMerger flakes on Fuchsia. "
                  "https://github.com/flutter/flutter/issues/59816 ";
#else

  const int ThreadMergingLease = 10;
  auto settings = CreateSettingsForFixture();
  fml::AutoResetWaitableEvent end_frame_latch;
  std::shared_ptr<ShellTestExternalViewEmbedder> external_view_embedder;

  auto end_frame_callback =
      [&](bool should_resubmit_frame,
          const fml::RefPtr<fml::RasterThreadMerger>& raster_thread_merger) {
        if (should_resubmit_frame && !raster_thread_merger->IsMerged()) {
          raster_thread_merger->MergeWithLease(ThreadMergingLease);

          ASSERT_TRUE(raster_thread_merger->IsMerged());
          external_view_embedder->UpdatePostPrerollResult(
              PostPrerollResult::kSuccess);
        }
        end_frame_latch.Signal();
      };
  external_view_embedder = std::make_shared<ShellTestExternalViewEmbedder>(
      end_frame_callback, PostPrerollResult::kSuccess, true);
  // Set resubmit once to trigger thread merging.
  external_view_embedder->UpdatePostPrerollResult(
      PostPrerollResult::kResubmitFrame);
  auto shell = CreateShell({
      .settings = settings,
      .platform_view_create_callback = ShellTestPlatformViewBuilder({
          .shell_test_external_view_embedder = external_view_embedder,
      }),
  });

  // Create the surface needed by rasterizer
  PlatformViewNotifyCreated(shell.get());

  auto configuration = RunConfiguration::InferFromSettings(settings);
  configuration.SetEntrypoint("emptyMain");

  RunEngine(shell.get(), std::move(configuration));

  LayerTreeBuilder builder = [&](const std::shared_ptr<ContainerLayer>& root) {
    auto display_list_layer = std::make_shared<DisplayListLayer>(
        SkPoint::Make(10, 10), MakeSizedDisplayList(80, 80), false, false);
    root->Add(display_list_layer);
  };

  PumpOneFrame(shell.get(), 100, 100, builder);
  // Pump one frame to trigger thread merging.
  end_frame_latch.Wait();
  // Pump another frame to ensure threads are merged and a regular layer tree is
  // submitted.
  PumpOneFrame(shell.get(), 100, 100, builder);
  // Threads are merged here. PlatformViewNotifyDestroy should be executed
  // successfully.
  ASSERT_TRUE(fml::TaskRunnerChecker::RunsOnTheSameThread(
      shell->GetTaskRunners().GetRasterTaskRunner()->GetTaskQueueId(),
      shell->GetTaskRunners().GetPlatformTaskRunner()->GetTaskQueueId()));
  ValidateDestroyPlatformView(shell.get());

  // Ensure threads are unmerged after platform view destroy
  ASSERT_FALSE(fml::TaskRunnerChecker::RunsOnTheSameThread(
      shell->GetTaskRunners().GetRasterTaskRunner()->GetTaskQueueId(),
      shell->GetTaskRunners().GetPlatformTaskRunner()->GetTaskQueueId()));

  // Validate the platform view can be recreated and destroyed again
  ValidateShell(shell.get());

  DestroyShell(std::move(shell));
#endif  // OS_FUCHSIA
}

TEST_F(ShellTest, OnPlatformViewDestroyWhenThreadsAreMerging) {
#if defined(OS_FUCHSIA)
  GTEST_SKIP() << "RasterThreadMerger flakes on Fuchsia. "
                  "https://github.com/flutter/flutter/issues/59816 ";
#else

  const int kThreadMergingLease = 10;
  auto settings = CreateSettingsForFixture();
  fml::AutoResetWaitableEvent end_frame_latch;
  auto end_frame_callback =
      [&](bool should_resubmit_frame,
          const fml::RefPtr<fml::RasterThreadMerger>& raster_thread_merger) {
        if (should_resubmit_frame && !raster_thread_merger->IsMerged()) {
          raster_thread_merger->MergeWithLease(kThreadMergingLease);
        }
        end_frame_latch.Signal();
      };
  // Start with a regular layer tree with `PostPrerollResult::kSuccess` so we
  // can later check if the rasterizer is tore down using
  // |ValidateDestroyPlatformView|
  auto external_view_embedder = std::make_shared<ShellTestExternalViewEmbedder>(
      end_frame_callback, PostPrerollResult::kSuccess, true);

  auto shell = CreateShell({
      .settings = settings,
      .platform_view_create_callback = ShellTestPlatformViewBuilder({
          .shell_test_external_view_embedder = external_view_embedder,
      }),
  });

  // Create the surface needed by rasterizer
  PlatformViewNotifyCreated(shell.get());

  auto configuration = RunConfiguration::InferFromSettings(settings);
  configuration.SetEntrypoint("emptyMain");

  RunEngine(shell.get(), std::move(configuration));

  LayerTreeBuilder builder = [&](const std::shared_ptr<ContainerLayer>& root) {
    auto display_list_layer = std::make_shared<DisplayListLayer>(
        SkPoint::Make(10, 10), MakeSizedDisplayList(80, 80), false, false);
    root->Add(display_list_layer);
  };

  PumpOneFrame(shell.get(), 100, 100, builder);
  // Pump one frame and threads aren't merged
  end_frame_latch.Wait();
  ASSERT_FALSE(fml::TaskRunnerChecker::RunsOnTheSameThread(
      shell->GetTaskRunners().GetRasterTaskRunner()->GetTaskQueueId(),
      shell->GetTaskRunners().GetPlatformTaskRunner()->GetTaskQueueId()));

  // Pump a frame with `PostPrerollResult::kResubmitFrame` to start merging
  // threads
  external_view_embedder->UpdatePostPrerollResult(
      PostPrerollResult::kResubmitFrame);
  PumpOneFrame(shell.get(), 100, 100, builder);

  // Now destroy the platform view immediately.
  // Two things can happen here:
  // 1. Threads haven't merged. 2. Threads has already merged.
  // |Shell:OnPlatformViewDestroy| should be able to handle both cases.
  ValidateDestroyPlatformView(shell.get());

  // Ensure threads are unmerged after platform view destroy
  ASSERT_FALSE(fml::TaskRunnerChecker::RunsOnTheSameThread(
      shell->GetTaskRunners().GetRasterTaskRunner()->GetTaskQueueId(),
      shell->GetTaskRunners().GetPlatformTaskRunner()->GetTaskQueueId()));

  // Validate the platform view can be recreated and destroyed again
  ValidateShell(shell.get());

  DestroyShell(std::move(shell));
#endif  // OS_FUCHSIA
}

TEST_F(ShellTest,
       OnPlatformViewDestroyWithThreadMergerWhileThreadsAreUnmerged) {
#if defined(OS_FUCHSIA)
  GTEST_SKIP() << "RasterThreadMerger flakes on Fuchsia. "
                  "https://github.com/flutter/flutter/issues/59816 ";
#else

  auto settings = CreateSettingsForFixture();
  fml::AutoResetWaitableEvent end_frame_latch;
  auto end_frame_callback =
      [&](bool should_resubmit_frame,
          const fml::RefPtr<fml::RasterThreadMerger>& raster_thread_merger) {
        end_frame_latch.Signal();
      };
  auto external_view_embedder = std::make_shared<ShellTestExternalViewEmbedder>(
      end_frame_callback, PostPrerollResult::kSuccess, true);
  auto shell = CreateShell({
      .settings = settings,
      .platform_view_create_callback = ShellTestPlatformViewBuilder({
          .shell_test_external_view_embedder = external_view_embedder,
      }),
  });

  // Create the surface needed by rasterizer
  PlatformViewNotifyCreated(shell.get());

  auto configuration = RunConfiguration::InferFromSettings(settings);
  configuration.SetEntrypoint("emptyMain");

  RunEngine(shell.get(), std::move(configuration));

  LayerTreeBuilder builder = [&](const std::shared_ptr<ContainerLayer>& root) {
    auto display_list_layer = std::make_shared<DisplayListLayer>(
        SkPoint::Make(10, 10), MakeSizedDisplayList(80, 80), false, false);
    root->Add(display_list_layer);
  };
  PumpOneFrame(shell.get(), 100, 100, builder);
  end_frame_latch.Wait();

  // Threads should not be merged.
  ASSERT_FALSE(fml::TaskRunnerChecker::RunsOnTheSameThread(
      shell->GetTaskRunners().GetRasterTaskRunner()->GetTaskQueueId(),
      shell->GetTaskRunners().GetPlatformTaskRunner()->GetTaskQueueId()));
  ValidateDestroyPlatformView(shell.get());

  // Ensure threads are unmerged after platform view destroy
  ASSERT_FALSE(fml::TaskRunnerChecker::RunsOnTheSameThread(
      shell->GetTaskRunners().GetRasterTaskRunner()->GetTaskQueueId(),
      shell->GetTaskRunners().GetPlatformTaskRunner()->GetTaskQueueId()));

  // Validate the platform view can be recreated and destroyed again
  ValidateShell(shell.get());

  DestroyShell(std::move(shell));
#endif  // OS_FUCHSIA
}

TEST_F(ShellTest, OnPlatformViewDestroyWithoutRasterThreadMerger) {
  auto settings = CreateSettingsForFixture();

  auto shell = CreateShell(settings);

  // Create the surface needed by rasterizer
  PlatformViewNotifyCreated(shell.get());

  auto configuration = RunConfiguration::InferFromSettings(settings);
  configuration.SetEntrypoint("emptyMain");

  RunEngine(shell.get(), std::move(configuration));

  LayerTreeBuilder builder = [&](const std::shared_ptr<ContainerLayer>& root) {
    auto display_list_layer = std::make_shared<DisplayListLayer>(
        SkPoint::Make(10, 10), MakeSizedDisplayList(80, 80), false, false);
    root->Add(display_list_layer);
  };
  PumpOneFrame(shell.get(), 100, 100, builder);

  // Threads should not be merged.
  ASSERT_FALSE(fml::TaskRunnerChecker::RunsOnTheSameThread(
      shell->GetTaskRunners().GetRasterTaskRunner()->GetTaskQueueId(),
      shell->GetTaskRunners().GetPlatformTaskRunner()->GetTaskQueueId()));
  ValidateDestroyPlatformView(shell.get());

  // Ensure threads are unmerged after platform view destroy
  ASSERT_FALSE(fml::TaskRunnerChecker::RunsOnTheSameThread(
      shell->GetTaskRunners().GetRasterTaskRunner()->GetTaskQueueId(),
      shell->GetTaskRunners().GetPlatformTaskRunner()->GetTaskQueueId()));

  // Validate the platform view can be recreated and destroyed again
  ValidateShell(shell.get());

  DestroyShell(std::move(shell));
}

// TODO(https://github.com/flutter/flutter/issues/59816): Enable on fuchsia.
TEST_F(ShellTest, OnPlatformViewDestroyWithStaticThreadMerging) {
#if defined(OS_FUCHSIA)
  GTEST_SKIP() << "RasterThreadMerger flakes on Fuchsia. "
                  "https://github.com/flutter/flutter/issues/59816 ";
#else

  auto settings = CreateSettingsForFixture();
  fml::AutoResetWaitableEvent end_frame_latch;
  auto end_frame_callback =
      [&](bool should_resubmit_frame,
          const fml::RefPtr<fml::RasterThreadMerger>& raster_thread_merger) {
        end_frame_latch.Signal();
      };
  auto external_view_embedder = std::make_shared<ShellTestExternalViewEmbedder>(
      end_frame_callback, PostPrerollResult::kSuccess, true);
  ThreadHost thread_host(
      "io.flutter.test." + GetCurrentTestName() + ".",
      ThreadHost::Type::Platform | ThreadHost::Type::IO | ThreadHost::Type::UI);
  TaskRunners task_runners(
      "test",
      thread_host.platform_thread->GetTaskRunner(),  // platform
      thread_host.platform_thread->GetTaskRunner(),  // raster
      thread_host.ui_thread->GetTaskRunner(),        // ui
      thread_host.io_thread->GetTaskRunner()         // io
  );
  auto shell = CreateShell({
      .settings = settings,
      .task_runners = task_runners,
      .platform_view_create_callback = ShellTestPlatformViewBuilder({
          .shell_test_external_view_embedder = external_view_embedder,
      }),
  });

  // Create the surface needed by rasterizer
  PlatformViewNotifyCreated(shell.get());

  auto configuration = RunConfiguration::InferFromSettings(settings);
  configuration.SetEntrypoint("emptyMain");

  RunEngine(shell.get(), std::move(configuration));

  LayerTreeBuilder builder = [&](const std::shared_ptr<ContainerLayer>& root) {
    auto display_list_layer = std::make_shared<DisplayListLayer>(
        SkPoint::Make(10, 10), MakeSizedDisplayList(80, 80), false, false);
    root->Add(display_list_layer);
  };
  PumpOneFrame(shell.get(), 100, 100, builder);
  end_frame_latch.Wait();

  ValidateDestroyPlatformView(shell.get());

  // Validate the platform view can be recreated and destroyed again
  ValidateShell(shell.get());

  DestroyShell(std::move(shell), task_runners);
#endif  // OS_FUCHSIA
}

TEST_F(ShellTest, GetUsedThisFrameShouldBeSetBeforeEndFrame) {
  auto settings = CreateSettingsForFixture();
  fml::AutoResetWaitableEvent end_frame_latch;
  std::shared_ptr<ShellTestExternalViewEmbedder> external_view_embedder;
  bool used_this_frame = true;
  auto end_frame_callback =
      [&](bool should_resubmit_frame,
          const fml::RefPtr<fml::RasterThreadMerger>& raster_thread_merger) {
        // We expect `used_this_frame` to be false.
        used_this_frame = external_view_embedder->GetUsedThisFrame();
        end_frame_latch.Signal();
      };
  external_view_embedder = std::make_shared<ShellTestExternalViewEmbedder>(
      end_frame_callback, PostPrerollResult::kSuccess, true);
  auto shell = CreateShell({
      .settings = settings,
      .platform_view_create_callback = ShellTestPlatformViewBuilder({
          .shell_test_external_view_embedder = external_view_embedder,
      }),
  });

  // Create the surface needed by rasterizer
  PlatformViewNotifyCreated(shell.get());

  auto configuration = RunConfiguration::InferFromSettings(settings);
  configuration.SetEntrypoint("emptyMain");

  RunEngine(shell.get(), std::move(configuration));

  LayerTreeBuilder builder = [&](const std::shared_ptr<ContainerLayer>& root) {
    auto display_list_layer = std::make_shared<DisplayListLayer>(
        SkPoint::Make(10, 10), MakeSizedDisplayList(80, 80), false, false);
    root->Add(display_list_layer);
  };
  PumpOneFrame(shell.get(), 100, 100, builder);
  end_frame_latch.Wait();
  ASSERT_FALSE(used_this_frame);

  // Validate the platform view can be recreated and destroyed again
  ValidateShell(shell.get());

  DestroyShell(std::move(shell));
}

// TODO(https://github.com/flutter/flutter/issues/66056): Deflake on all other
// platforms
TEST_F(ShellTest, DISABLED_SkipAndSubmitFrame) {
#if defined(OS_FUCHSIA)
  GTEST_SKIP() << "RasterThreadMerger flakes on Fuchsia. "
                  "https://github.com/flutter/flutter/issues/59816 ";
#else

  auto settings = CreateSettingsForFixture();
  fml::AutoResetWaitableEvent end_frame_latch;
  std::shared_ptr<ShellTestExternalViewEmbedder> external_view_embedder;

  auto end_frame_callback =
      [&](bool should_resubmit_frame,
          const fml::RefPtr<fml::RasterThreadMerger>& raster_thread_merger) {
        if (should_resubmit_frame && !raster_thread_merger->IsMerged()) {
          raster_thread_merger->MergeWithLease(10);
          external_view_embedder->UpdatePostPrerollResult(
              PostPrerollResult::kSuccess);
        }
        end_frame_latch.Signal();
      };
  external_view_embedder = std::make_shared<ShellTestExternalViewEmbedder>(
      end_frame_callback, PostPrerollResult::kSkipAndRetryFrame, true);

  auto shell = CreateShell({
      .settings = settings,
      .platform_view_create_callback = ShellTestPlatformViewBuilder({
          .shell_test_external_view_embedder = external_view_embedder,
      }),
  });

  PlatformViewNotifyCreated(shell.get());

  auto configuration = RunConfiguration::InferFromSettings(settings);
  configuration.SetEntrypoint("emptyMain");
  RunEngine(shell.get(), std::move(configuration));

  ASSERT_EQ(0, external_view_embedder->GetSubmittedFrameCount());

  PumpOneFrame(shell.get());

  // `EndFrame` changed the post preroll result to `kSuccess`.
  end_frame_latch.Wait();

  // Let the resubmitted frame to run and `GetSubmittedFrameCount` should be
  // called.
  end_frame_latch.Wait();
  // 2 frames are submitted because `kSkipAndRetryFrame`, but only the 2nd frame
  // should be submitted with `external_view_embedder`, hence the below check.
  ASSERT_EQ(1, external_view_embedder->GetSubmittedFrameCount());

  PlatformViewNotifyDestroyed(shell.get());
  DestroyShell(std::move(shell));
#endif  // OS_FUCHSIA
}

TEST(SettingsTest, FrameTimingSetsAndGetsProperly) {
  // Ensure that all phases are in kPhases.
  ASSERT_EQ(sizeof(FrameTiming::kPhases),
            FrameTiming::kCount * sizeof(FrameTiming::Phase));

  int lastPhaseIndex = -1;
  FrameTiming timing;
  for (auto phase : FrameTiming::kPhases) {
    ASSERT_TRUE(phase > lastPhaseIndex);  // Ensure that kPhases are in order.
    lastPhaseIndex = phase;
    auto fake_time =
        fml::TimePoint::FromEpochDelta(fml::TimeDelta::FromMicroseconds(phase));
    timing.Set(phase, fake_time);
    ASSERT_TRUE(timing.Get(phase) == fake_time);
  }
}

TEST_F(ShellTest, ReportTimingsIsCalledImmediatelyAfterTheFirstFrame) {
  auto settings = CreateSettingsForFixture();
  std::unique_ptr<Shell> shell = CreateShell(settings);

  // Create the surface needed by rasterizer
  PlatformViewNotifyCreated(shell.get());

  auto configuration = RunConfiguration::InferFromSettings(settings);
  ASSERT_TRUE(configuration.IsValid());
  configuration.SetEntrypoint("reportTimingsMain");
  fml::AutoResetWaitableEvent reportLatch;
  std::vector<int64_t> timestamps;
  auto nativeTimingCallback = [&reportLatch,
                               &timestamps](Dart_NativeArguments args) {
    Dart_Handle exception = nullptr;
    ASSERT_EQ(timestamps.size(), 0ul);
    timestamps = tonic::DartConverter<std::vector<int64_t>>::FromArguments(
        args, 0, exception);
    reportLatch.Signal();
  };
  AddNativeCallback("NativeReportTimingsCallback",
                    CREATE_NATIVE_ENTRY(nativeTimingCallback));
  ASSERT_TRUE(configuration.IsValid());
  RunEngine(shell.get(), std::move(configuration));

  for (int i = 0; i < 10; i += 1) {
    PumpOneFrame(shell.get());
  }

  reportLatch.Wait();
  DestroyShell(std::move(shell));

  // Check for the immediate callback of the first frame that doesn't wait for
  // the other 9 frames to be rasterized.
  ASSERT_EQ(timestamps.size(), FrameTiming::kCount);
}

TEST_F(ShellTest, WaitForFirstFrame) {
  auto settings = CreateSettingsForFixture();
  std::unique_ptr<Shell> shell = CreateShell(settings);

  // Create the surface needed by rasterizer
  PlatformViewNotifyCreated(shell.get());

  auto configuration = RunConfiguration::InferFromSettings(settings);
  configuration.SetEntrypoint("emptyMain");

  RunEngine(shell.get(), std::move(configuration));
  PumpOneFrame(shell.get());
  fml::Status result = shell->WaitForFirstFrame(fml::TimeDelta::Max());
  ASSERT_TRUE(result.ok());

  DestroyShell(std::move(shell));
}

TEST_F(ShellTest, WaitForFirstFrameZeroSizeFrame) {
  auto settings = CreateSettingsForFixture();
  std::unique_ptr<Shell> shell = CreateShell(settings);

  // Create the surface needed by rasterizer
  PlatformViewNotifyCreated(shell.get());

  auto configuration = RunConfiguration::InferFromSettings(settings);
  configuration.SetEntrypoint("emptyMain");

  RunEngine(shell.get(), std::move(configuration));
  PumpOneFrame(shell.get(), {1.0, 0.0, 0.0, 22, 0});
  fml::Status result = shell->WaitForFirstFrame(fml::TimeDelta::Zero());
  ASSERT_FALSE(result.ok());
  ASSERT_EQ(result.code(), fml::StatusCode::kDeadlineExceeded);

  DestroyShell(std::move(shell));
}

TEST_F(ShellTest, WaitForFirstFrameTimeout) {
  auto settings = CreateSettingsForFixture();
  std::unique_ptr<Shell> shell = CreateShell(settings);

  // Create the surface needed by rasterizer
  PlatformViewNotifyCreated(shell.get());

  auto configuration = RunConfiguration::InferFromSettings(settings);
  configuration.SetEntrypoint("emptyMain");

  RunEngine(shell.get(), std::move(configuration));
  fml::Status result = shell->WaitForFirstFrame(fml::TimeDelta::Zero());
  ASSERT_FALSE(result.ok());
  ASSERT_EQ(result.code(), fml::StatusCode::kDeadlineExceeded);

  DestroyShell(std::move(shell));
}

TEST_F(ShellTest, WaitForFirstFrameMultiple) {
  auto settings = CreateSettingsForFixture();
  std::unique_ptr<Shell> shell = CreateShell(settings);

  // Create the surface needed by rasterizer
  PlatformViewNotifyCreated(shell.get());

  auto configuration = RunConfiguration::InferFromSettings(settings);
  configuration.SetEntrypoint("emptyMain");

  RunEngine(shell.get(), std::move(configuration));
  PumpOneFrame(shell.get());
  fml::Status result = shell->WaitForFirstFrame(fml::TimeDelta::Max());
  ASSERT_TRUE(result.ok());
  for (int i = 0; i < 100; ++i) {
    result = shell->WaitForFirstFrame(fml::TimeDelta::Zero());
    ASSERT_TRUE(result.ok());
  }

  DestroyShell(std::move(shell));
}

/// Makes sure that WaitForFirstFrame works if we rendered a frame with the
/// single-thread setup.
TEST_F(ShellTest, WaitForFirstFrameInlined) {
  Settings settings = CreateSettingsForFixture();
  auto task_runner = CreateNewThread();
  TaskRunners task_runners("test", task_runner, task_runner, task_runner,
                           task_runner);
  std::unique_ptr<Shell> shell = CreateShell(settings, task_runners);

  // Create the surface needed by rasterizer
  PlatformViewNotifyCreated(shell.get());

  auto configuration = RunConfiguration::InferFromSettings(settings);
  configuration.SetEntrypoint("emptyMain");

  RunEngine(shell.get(), std::move(configuration));
  PumpOneFrame(shell.get());
  fml::AutoResetWaitableEvent event;
  task_runner->PostTask([&shell, &event] {
    fml::Status result = shell->WaitForFirstFrame(fml::TimeDelta::Max());
    ASSERT_FALSE(result.ok());
    ASSERT_EQ(result.code(), fml::StatusCode::kFailedPrecondition);
    event.Signal();
  });
  ASSERT_FALSE(event.WaitWithTimeout(fml::TimeDelta::Max()));

  DestroyShell(std::move(shell), task_runners);
}

static size_t GetRasterizerResourceCacheBytesSync(const Shell& shell) {
  size_t bytes = 0;
  fml::AutoResetWaitableEvent latch;
  fml::TaskRunner::RunNowOrPostTask(
      shell.GetTaskRunners().GetRasterTaskRunner(), [&]() {
        if (auto rasterizer = shell.GetRasterizer()) {
          bytes = rasterizer->GetResourceCacheMaxBytes().value_or(0U);
        }
        latch.Signal();
      });
  latch.Wait();
  return bytes;
}

TEST_F(ShellTest, MultipleFluttersSetResourceCacheBytes) {
  TaskRunners task_runners = GetTaskRunnersForFixture();
  auto settings = CreateSettingsForFixture();
  settings.resource_cache_max_bytes_threshold = 4000000U;
  GrMockOptions main_context_options;
  sk_sp<GrDirectContext> main_context =
      GrDirectContext::MakeMock(&main_context_options);
  Shell::CreateCallback<PlatformView> platform_view_create_callback =
      [task_runners, main_context](flutter::Shell& shell) {
        auto result = std::make_unique<TestPlatformView>(shell, task_runners);
        ON_CALL(*result, CreateRenderingSurface())
            .WillByDefault(::testing::Invoke([main_context] {
              auto surface = std::make_unique<MockSurface>();
              ON_CALL(*surface, GetContext())
                  .WillByDefault(Return(main_context.get()));
              ON_CALL(*surface, IsValid()).WillByDefault(Return(true));
              ON_CALL(*surface, MakeRenderContextCurrent())
                  .WillByDefault(::testing::Invoke([] {
                    return std::make_unique<GLContextDefaultResult>(true);
                  }));
              return surface;
            }));
        return result;
      };

  auto shell = CreateShell({
      .settings = settings,
      .task_runners = task_runners,
      .platform_view_create_callback = platform_view_create_callback,
  });

  // Create the surface needed by rasterizer
  PlatformViewNotifyCreated(shell.get());

  auto configuration = RunConfiguration::InferFromSettings(settings);
  configuration.SetEntrypoint("emptyMain");

  RunEngine(shell.get(), std::move(configuration));
  PostSync(shell->GetTaskRunners().GetPlatformTaskRunner(), [&shell]() {
    shell->GetPlatformView()->SetViewportMetrics(kImplicitViewId,
                                                 {1.0, 100, 100, 22, 0});
  });

  // first cache bytes
  EXPECT_EQ(GetRasterizerResourceCacheBytesSync(*shell),
            static_cast<size_t>(480000U));

  auto shell_spawn_callback = [&]() {
    std::unique_ptr<Shell> spawn;
    PostSync(
        shell->GetTaskRunners().GetPlatformTaskRunner(),
        [this, &spawn, &spawner = shell, platform_view_create_callback]() {
          auto configuration =
              RunConfiguration::InferFromSettings(CreateSettingsForFixture());
          configuration.SetEntrypoint("emptyMain");
          spawn = spawner->Spawn(
              std::move(configuration), "", platform_view_create_callback,
              [](Shell& shell) { return std::make_unique<Rasterizer>(shell); });
          ASSERT_NE(nullptr, spawn.get());
          ASSERT_TRUE(ValidateShell(spawn.get()));
        });
    return spawn;
  };

  std::unique_ptr<Shell> second_shell = shell_spawn_callback();
  PlatformViewNotifyCreated(second_shell.get());
  PostSync(second_shell->GetTaskRunners().GetPlatformTaskRunner(),
           [&second_shell]() {
             second_shell->GetPlatformView()->SetViewportMetrics(
                 kImplicitViewId, {1.0, 100, 100, 22, 0});
           });
  // first cache bytes + second cache bytes
  EXPECT_EQ(GetRasterizerResourceCacheBytesSync(*shell),
            static_cast<size_t>(960000U));

  PostSync(second_shell->GetTaskRunners().GetPlatformTaskRunner(),
           [&second_shell]() {
             second_shell->GetPlatformView()->SetViewportMetrics(
                 kImplicitViewId, {1.0, 100, 300, 22, 0});
           });
  // first cache bytes + second cache bytes
  EXPECT_EQ(GetRasterizerResourceCacheBytesSync(*shell),
            static_cast<size_t>(1920000U));

  std::unique_ptr<Shell> third_shell = shell_spawn_callback();
  PlatformViewNotifyCreated(third_shell.get());
  PostSync(third_shell->GetTaskRunners().GetPlatformTaskRunner(),
           [&third_shell]() {
             third_shell->GetPlatformView()->SetViewportMetrics(
                 kImplicitViewId, {1.0, 400, 100, 22, 0});
           });
  // first cache bytes + second cache bytes + third cache bytes
  EXPECT_EQ(GetRasterizerResourceCacheBytesSync(*shell),
            static_cast<size_t>(3840000U));

  PostSync(third_shell->GetTaskRunners().GetPlatformTaskRunner(),
           [&third_shell]() {
             third_shell->GetPlatformView()->SetViewportMetrics(
                 kImplicitViewId, {1.0, 800, 100, 22, 0});
           });
  // max bytes threshold
  EXPECT_EQ(GetRasterizerResourceCacheBytesSync(*shell),
            static_cast<size_t>(4000000U));
  DestroyShell(std::move(third_shell), task_runners);
  // max bytes threshold
  EXPECT_EQ(GetRasterizerResourceCacheBytesSync(*shell),
            static_cast<size_t>(4000000U));

  PostSync(second_shell->GetTaskRunners().GetPlatformTaskRunner(),
           [&second_shell]() {
             second_shell->GetPlatformView()->SetViewportMetrics(
                 kImplicitViewId, {1.0, 100, 100, 22, 0});
           });
  // first cache bytes + second cache bytes
  EXPECT_EQ(GetRasterizerResourceCacheBytesSync(*shell),
            static_cast<size_t>(960000U));

  DestroyShell(std::move(second_shell), task_runners);
  DestroyShell(std::move(shell), task_runners);
}

TEST_F(ShellTest, SetResourceCacheSize) {
  Settings settings = CreateSettingsForFixture();
  auto task_runner = CreateNewThread();
  TaskRunners task_runners("test", task_runner, task_runner, task_runner,
                           task_runner);
  std::unique_ptr<Shell> shell = CreateShell(settings, task_runners);

  // Create the surface needed by rasterizer
  PlatformViewNotifyCreated(shell.get());

  auto configuration = RunConfiguration::InferFromSettings(settings);
  configuration.SetEntrypoint("emptyMain");

  RunEngine(shell.get(), std::move(configuration));
  PumpOneFrame(shell.get());

  // The Vulkan and GL backends set different default values for the resource
  // cache size. The default backend (specified by the default param of
  // `CreateShell` in this test) will only resolve to Vulkan (in
  // `ShellTestPlatformView::Create`) if GL is disabled. This situation arises
  // when targeting the Fuchsia Emulator.
#if defined(SHELL_ENABLE_VULKAN) && !defined(SHELL_ENABLE_GL)
  EXPECT_EQ(GetRasterizerResourceCacheBytesSync(*shell),
            vulkan::kGrCacheMaxByteSize);
#elif defined(SHELL_ENABLE_METAL)
  EXPECT_EQ(GetRasterizerResourceCacheBytesSync(*shell),
            static_cast<size_t>(256 * (1 << 20)));
#else
  EXPECT_EQ(GetRasterizerResourceCacheBytesSync(*shell),
            static_cast<size_t>(24 * (1 << 20)));
#endif

  fml::TaskRunner::RunNowOrPostTask(
      shell->GetTaskRunners().GetPlatformTaskRunner(), [&shell]() {
        shell->GetPlatformView()->SetViewportMetrics(kImplicitViewId,
                                                     {1.0, 400, 200, 22, 0});
      });
  PumpOneFrame(shell.get());

  EXPECT_EQ(GetRasterizerResourceCacheBytesSync(*shell), 3840000U);

  std::string request_json = R"json({
                                "method": "Skia.setResourceCacheMaxBytes",
                                "args": 10000
                              })json";
  auto data =
      fml::MallocMapping::Copy(request_json.c_str(), request_json.length());
  auto platform_message = std::make_unique<PlatformMessage>(
      "flutter/skia", std::move(data), nullptr);
  SendEnginePlatformMessage(shell.get(), std::move(platform_message));
  PumpOneFrame(shell.get());
  EXPECT_EQ(GetRasterizerResourceCacheBytesSync(*shell), 10000U);

  fml::TaskRunner::RunNowOrPostTask(
      shell->GetTaskRunners().GetPlatformTaskRunner(), [&shell]() {
        shell->GetPlatformView()->SetViewportMetrics(kImplicitViewId,
                                                     {1.0, 800, 400, 22, 0});
      });
  PumpOneFrame(shell.get());

  EXPECT_EQ(GetRasterizerResourceCacheBytesSync(*shell), 10000U);
  DestroyShell(std::move(shell), task_runners);
}

TEST_F(ShellTest, SetResourceCacheSizeEarly) {
  Settings settings = CreateSettingsForFixture();
  auto task_runner = CreateNewThread();
  TaskRunners task_runners("test", task_runner, task_runner, task_runner,
                           task_runner);
  std::unique_ptr<Shell> shell = CreateShell(settings, task_runners);

  fml::TaskRunner::RunNowOrPostTask(
      shell->GetTaskRunners().GetPlatformTaskRunner(), [&shell]() {
        shell->GetPlatformView()->SetViewportMetrics(kImplicitViewId,
                                                     {1.0, 400, 200, 22, 0});
      });
  PumpOneFrame(shell.get());

  // Create the surface needed by rasterizer
  PlatformViewNotifyCreated(shell.get());

  auto configuration = RunConfiguration::InferFromSettings(settings);
  configuration.SetEntrypoint("emptyMain");

  RunEngine(shell.get(), std::move(configuration));
  PumpOneFrame(shell.get());

  EXPECT_EQ(GetRasterizerResourceCacheBytesSync(*shell),
            static_cast<size_t>(3840000U));
  DestroyShell(std::move(shell), task_runners);
}

TEST_F(ShellTest, SetResourceCacheSizeNotifiesDart) {
  Settings settings = CreateSettingsForFixture();
  auto task_runner = CreateNewThread();
  TaskRunners task_runners("test", task_runner, task_runner, task_runner,
                           task_runner);
  std::unique_ptr<Shell> shell = CreateShell(settings, task_runners);

  fml::TaskRunner::RunNowOrPostTask(
      shell->GetTaskRunners().GetPlatformTaskRunner(), [&shell]() {
        shell->GetPlatformView()->SetViewportMetrics(kImplicitViewId,
                                                     {1.0, 400, 200, 22, 0});
      });
  PumpOneFrame(shell.get());

  // Create the surface needed by rasterizer
  PlatformViewNotifyCreated(shell.get());

  auto configuration = RunConfiguration::InferFromSettings(settings);
  configuration.SetEntrypoint("testSkiaResourceCacheSendsResponse");

  EXPECT_EQ(GetRasterizerResourceCacheBytesSync(*shell),
            static_cast<size_t>(3840000U));

  fml::AutoResetWaitableEvent latch;
  AddNativeCallback("NotifyNative", CREATE_NATIVE_ENTRY([&latch](auto args) {
                      latch.Signal();
                    }));

  RunEngine(shell.get(), std::move(configuration));
  PumpOneFrame(shell.get());

  latch.Wait();

  EXPECT_EQ(GetRasterizerResourceCacheBytesSync(*shell),
            static_cast<size_t>(10000U));
  DestroyShell(std::move(shell), task_runners);
}

TEST_F(ShellTest, CanCreateImagefromDecompressedBytes) {
  Settings settings = CreateSettingsForFixture();
  auto task_runner = CreateNewThread();

  TaskRunners task_runners("test", task_runner, task_runner, task_runner,
                           task_runner);

  std::unique_ptr<Shell> shell = CreateShell(settings, task_runners);

  // Create the surface needed by rasterizer
  PlatformViewNotifyCreated(shell.get());

  auto configuration = RunConfiguration::InferFromSettings(settings);
  configuration.SetEntrypoint("canCreateImageFromDecompressedData");

  fml::AutoResetWaitableEvent latch;
  AddNativeCallback("NotifyWidthHeight",
                    CREATE_NATIVE_ENTRY([&latch](auto args) {
                      auto width = tonic::DartConverter<int>::FromDart(
                          Dart_GetNativeArgument(args, 0));
                      auto height = tonic::DartConverter<int>::FromDart(
                          Dart_GetNativeArgument(args, 1));
                      ASSERT_EQ(width, 10);
                      ASSERT_EQ(height, 10);
                      latch.Signal();
                    }));

  RunEngine(shell.get(), std::move(configuration));

  latch.Wait();
  DestroyShell(std::move(shell), task_runners);
}

class MockTexture : public Texture {
 public:
  MockTexture(int64_t textureId,
              std::shared_ptr<fml::AutoResetWaitableEvent> latch)
      : Texture(textureId), latch_(std::move(latch)) {}

  ~MockTexture() override = default;

  // Called from raster thread.
  void Paint(PaintContext& context,
             const SkRect& bounds,
             bool freeze,
             const DlImageSampling) override {}

  void OnGrContextCreated() override {}

  void OnGrContextDestroyed() override {}

  void MarkNewFrameAvailable() override {
    frames_available_++;
    latch_->Signal();
  }

  void OnTextureUnregistered() override {
    unregistered_ = true;
    latch_->Signal();
  }

  bool unregistered() { return unregistered_; }
  int frames_available() { return frames_available_; }

 private:
  bool unregistered_ = false;
  int frames_available_ = 0;
  std::shared_ptr<fml::AutoResetWaitableEvent> latch_;
};

TEST_F(ShellTest, TextureFrameMarkedAvailableAndUnregister) {
  Settings settings = CreateSettingsForFixture();
  auto configuration = RunConfiguration::InferFromSettings(settings);
  auto task_runner = CreateNewThread();
  TaskRunners task_runners("test", task_runner, task_runner, task_runner,
                           task_runner);
  std::unique_ptr<Shell> shell = CreateShell(settings, task_runners);

  ASSERT_TRUE(ValidateShell(shell.get()));
  PlatformViewNotifyCreated(shell.get());

  RunEngine(shell.get(), std::move(configuration));

  std::shared_ptr<fml::AutoResetWaitableEvent> latch =
      std::make_shared<fml::AutoResetWaitableEvent>();

  std::shared_ptr<MockTexture> mockTexture =
      std::make_shared<MockTexture>(0, latch);

  fml::TaskRunner::RunNowOrPostTask(
      shell->GetTaskRunners().GetRasterTaskRunner(), [&]() {
        shell->GetPlatformView()->RegisterTexture(mockTexture);
        shell->GetPlatformView()->MarkTextureFrameAvailable(0);
      });
  latch->Wait();

  EXPECT_EQ(mockTexture->frames_available(), 1);

  fml::TaskRunner::RunNowOrPostTask(
      shell->GetTaskRunners().GetRasterTaskRunner(),
      [&]() { shell->GetPlatformView()->UnregisterTexture(0); });
  latch->Wait();

  EXPECT_EQ(mockTexture->unregistered(), true);
  DestroyShell(std::move(shell), task_runners);
}

TEST_F(ShellTest, IsolateCanAccessPersistentIsolateData) {
  const std::string message = "dummy isolate launch data.";

  Settings settings = CreateSettingsForFixture();
  settings.persistent_isolate_data =
      std::make_shared<fml::DataMapping>(message);
  TaskRunners task_runners("test",                  // label
                           GetCurrentTaskRunner(),  // platform
                           CreateNewThread(),       // raster
                           CreateNewThread(),       // ui
                           CreateNewThread()        // io
  );

  fml::AutoResetWaitableEvent message_latch;
  AddNativeCallback("NotifyMessage",
                    CREATE_NATIVE_ENTRY([&](Dart_NativeArguments args) {
                      const auto message_from_dart =
                          tonic::DartConverter<std::string>::FromDart(
                              Dart_GetNativeArgument(args, 0));
                      ASSERT_EQ(message, message_from_dart);
                      message_latch.Signal();
                    }));

  std::unique_ptr<Shell> shell = CreateShell(settings, task_runners);

  ASSERT_TRUE(shell->IsSetup());
  auto configuration = RunConfiguration::InferFromSettings(settings);
  configuration.SetEntrypoint("canAccessIsolateLaunchData");

  fml::AutoResetWaitableEvent event;
  shell->RunEngine(std::move(configuration), [&](auto result) {
    ASSERT_EQ(result, Engine::RunStatus::Success);
  });

  message_latch.Wait();
  DestroyShell(std::move(shell), task_runners);
}

TEST_F(ShellTest, CanScheduleFrameFromPlatform) {
  Settings settings = CreateSettingsForFixture();
  TaskRunners task_runners = GetTaskRunnersForFixture();
  fml::AutoResetWaitableEvent latch;
  AddNativeCallback(
      "NotifyNative",
      CREATE_NATIVE_ENTRY([&](Dart_NativeArguments args) { latch.Signal(); }));
  fml::AutoResetWaitableEvent check_latch;
  AddNativeCallback("NativeOnBeginFrame",
                    CREATE_NATIVE_ENTRY([&](Dart_NativeArguments args) {
                      check_latch.Signal();
                    }));
  std::unique_ptr<Shell> shell = CreateShell(settings, task_runners);
  ASSERT_TRUE(shell->IsSetup());

  auto configuration = RunConfiguration::InferFromSettings(settings);
  configuration.SetEntrypoint("onBeginFrameWithNotifyNativeMain");
  RunEngine(shell.get(), std::move(configuration));

  // Wait for the application to attach the listener.
  latch.Wait();

  fml::TaskRunner::RunNowOrPostTask(
      shell->GetTaskRunners().GetPlatformTaskRunner(),
      [&shell]() { shell->GetPlatformView()->ScheduleFrame(); });
  check_latch.Wait();
  DestroyShell(std::move(shell), task_runners);
}

TEST_F(ShellTest, SecondaryVsyncCallbackShouldBeCalledAfterVsyncCallback) {
  bool is_on_begin_frame_called = false;
  bool is_secondary_callback_called = false;
  Settings settings = CreateSettingsForFixture();
  TaskRunners task_runners = GetTaskRunnersForFixture();
  fml::AutoResetWaitableEvent latch;
  AddNativeCallback(
      "NotifyNative",
      CREATE_NATIVE_ENTRY([&](Dart_NativeArguments args) { latch.Signal(); }));
  fml::CountDownLatch count_down_latch(2);
  AddNativeCallback("NativeOnBeginFrame",
                    CREATE_NATIVE_ENTRY([&](Dart_NativeArguments args) {
                      EXPECT_FALSE(is_on_begin_frame_called);
                      EXPECT_FALSE(is_secondary_callback_called);
                      is_on_begin_frame_called = true;
                      count_down_latch.CountDown();
                    }));
  std::unique_ptr<Shell> shell = CreateShell(settings, task_runners);
  ASSERT_TRUE(shell->IsSetup());

  auto configuration = RunConfiguration::InferFromSettings(settings);
  configuration.SetEntrypoint("onBeginFrameWithNotifyNativeMain");
  RunEngine(shell.get(), std::move(configuration));

  // Wait for the application to attach the listener.
  latch.Wait();

  fml::TaskRunner::RunNowOrPostTask(
      shell->GetTaskRunners().GetUITaskRunner(), [&]() {
        shell->GetEngine()->ScheduleSecondaryVsyncCallback(0, [&]() {
          EXPECT_TRUE(is_on_begin_frame_called);
          EXPECT_FALSE(is_secondary_callback_called);
          is_secondary_callback_called = true;
          count_down_latch.CountDown();
        });
        shell->GetEngine()->ScheduleFrame();
      });
  count_down_latch.Wait();
  EXPECT_TRUE(is_on_begin_frame_called);
  EXPECT_TRUE(is_secondary_callback_called);
  DestroyShell(std::move(shell), task_runners);
}

static void LogSkData(const sk_sp<SkData>& data, const char* title) {
  FML_LOG(ERROR) << "---------- " << title;
  std::ostringstream ostr;
  for (size_t i = 0; i < data->size();) {
    ostr << std::hex << std::setfill('0') << std::setw(2)
         << static_cast<int>(data->bytes()[i]) << " ";
    i++;
    if (i % 16 == 0 || i == data->size()) {
      FML_LOG(ERROR) << ostr.str();
      ostr.str("");
      ostr.clear();
    }
  }
}

TEST_F(ShellTest, Screenshot) {
  auto settings = CreateSettingsForFixture();
  fml::AutoResetWaitableEvent firstFrameLatch;
  settings.frame_rasterized_callback =
      [&firstFrameLatch](const FrameTiming& t) { firstFrameLatch.Signal(); };

  std::unique_ptr<Shell> shell = CreateShell(settings);

  // Create the surface needed by rasterizer
  PlatformViewNotifyCreated(shell.get());

  auto configuration = RunConfiguration::InferFromSettings(settings);
  configuration.SetEntrypoint("emptyMain");

  RunEngine(shell.get(), std::move(configuration));

  LayerTreeBuilder builder = [&](const std::shared_ptr<ContainerLayer>& root) {
    auto display_list_layer = std::make_shared<DisplayListLayer>(
        SkPoint::Make(10, 10), MakeSizedDisplayList(80, 80), false, false);
    root->Add(display_list_layer);
  };

  PumpOneFrame(shell.get(), 100, 100, builder);
  firstFrameLatch.Wait();

  std::promise<Rasterizer::Screenshot> screenshot_promise;
  auto screenshot_future = screenshot_promise.get_future();

  fml::TaskRunner::RunNowOrPostTask(
      shell->GetTaskRunners().GetRasterTaskRunner(),
      [&screenshot_promise, &shell]() {
        auto rasterizer = shell->GetRasterizer();
        screenshot_promise.set_value(rasterizer->ScreenshotLastLayerTree(
            Rasterizer::ScreenshotType::CompressedImage, false));
      });

  auto fixtures_dir =
      fml::OpenDirectory(GetFixturesPath(), false, fml::FilePermission::kRead);

  auto reference_png = fml::FileMapping::CreateReadOnly(
      fixtures_dir, "shelltest_screenshot.png");

  // Use MakeWithoutCopy instead of MakeWithCString because we don't want to
  // encode the null sentinel
  sk_sp<SkData> reference_data = SkData::MakeWithoutCopy(
      reference_png->GetMapping(), reference_png->GetSize());

  sk_sp<SkData> screenshot_data = screenshot_future.get().data;
  if (!reference_data->equals(screenshot_data.get())) {
    LogSkData(reference_data, "reference");
    LogSkData(screenshot_data, "screenshot");
    ASSERT_TRUE(false);
  }

  DestroyShell(std::move(shell));
}

TEST_F(ShellTest, CanConvertToAndFromMappings) {
  const size_t buffer_size = 2 << 20;

  uint8_t* buffer = static_cast<uint8_t*>(::malloc(buffer_size));
  // NOLINTNEXTLINE(clang-analyzer-unix.Malloc)
  ASSERT_TRUE(buffer != nullptr);
  ASSERT_TRUE(MemsetPatternSetOrCheck(
      buffer, buffer_size, MemsetPatternOp::kMemsetPatternOpSetBuffer));

  std::unique_ptr<fml::Mapping> mapping =
      std::make_unique<fml::MallocMapping>(buffer, buffer_size);

  ASSERT_EQ(mapping->GetSize(), buffer_size);

  fml::AutoResetWaitableEvent latch;
  AddNativeCallback(
      "SendFixtureMapping", CREATE_NATIVE_ENTRY([&](auto args) {
        auto mapping_from_dart =
            tonic::DartConverter<std::unique_ptr<fml::Mapping>>::FromDart(
                Dart_GetNativeArgument(args, 0));
        ASSERT_NE(mapping_from_dart, nullptr);
        ASSERT_EQ(mapping_from_dart->GetSize(), buffer_size);
        ASSERT_TRUE(MemsetPatternSetOrCheck(
            const_cast<uint8_t*>(mapping_from_dart->GetMapping()),  // buffer
            mapping_from_dart->GetSize(),                           // size
            MemsetPatternOp::kMemsetPatternOpCheckBuffer            // op
            ));
        latch.Signal();
      }));

  AddNativeCallback(
      "GetFixtureMapping", CREATE_NATIVE_ENTRY([&](auto args) {
        tonic::DartConverter<tonic::DartConverterMapping>::SetReturnValue(
            args, mapping);
      }));

  auto settings = CreateSettingsForFixture();
  auto configuration = RunConfiguration::InferFromSettings(settings);
  configuration.SetEntrypoint("canConvertMappings");
  std::unique_ptr<Shell> shell = CreateShell(settings);
  ASSERT_NE(shell.get(), nullptr);
  RunEngine(shell.get(), std::move(configuration));
  latch.Wait();
  DestroyShell(std::move(shell));
}

// Compares local times as seen by the dart isolate and as seen by this test
// fixture, to a resolution of 1 hour.
//
// This verifies that (1) the isolate is able to get a timezone (doesn't lock
// up for example), and (2) that the host and the isolate agree on what the
// timezone is.
TEST_F(ShellTest, LocaltimesMatch) {
  fml::AutoResetWaitableEvent latch;
  std::string dart_isolate_time_str;

  // See fixtures/shell_test.dart, the callback NotifyLocalTime is declared
  // there.
  AddNativeCallback("NotifyLocalTime", CREATE_NATIVE_ENTRY([&](auto args) {
                      dart_isolate_time_str =
                          tonic::DartConverter<std::string>::FromDart(
                              Dart_GetNativeArgument(args, 0));
                      latch.Signal();
                    }));

  auto settings = CreateSettingsForFixture();
  auto configuration = RunConfiguration::InferFromSettings(settings);
  configuration.SetEntrypoint("localtimesMatch");
  std::unique_ptr<Shell> shell = CreateShell(settings);
  ASSERT_NE(shell.get(), nullptr);
  RunEngine(shell.get(), std::move(configuration));
  latch.Wait();

  char timestr[200];
  const time_t timestamp = time(nullptr);
  const struct tm* local_time = localtime(&timestamp);
  ASSERT_NE(local_time, nullptr)
      << "Could not get local time: errno=" << errno << ": " << strerror(errno);
  // Example: "2020-02-26 14" for 2pm on February 26, 2020.
  const size_t format_size =
      strftime(timestr, sizeof(timestr), "%Y-%m-%d %H", local_time);
  ASSERT_NE(format_size, 0UL)
      << "strftime failed: host time: " << std::string(timestr)
      << " dart isolate time: " << dart_isolate_time_str;

  const std::string host_local_time_str = timestr;

  ASSERT_EQ(dart_isolate_time_str, host_local_time_str)
      << "Local times in the dart isolate and the local time seen by the test "
      << "differ by more than 1 hour, but are expected to be about equal";

  DestroyShell(std::move(shell));
}

TEST_F(ShellTest, CanDecompressImageFromAsset) {
  fml::AutoResetWaitableEvent latch;
  AddNativeCallback("NotifyWidthHeight", CREATE_NATIVE_ENTRY([&](auto args) {
                      auto width = tonic::DartConverter<int>::FromDart(
                          Dart_GetNativeArgument(args, 0));
                      auto height = tonic::DartConverter<int>::FromDart(
                          Dart_GetNativeArgument(args, 1));
                      ASSERT_EQ(width, 100);
                      ASSERT_EQ(height, 100);
                      latch.Signal();
                    }));

  AddNativeCallback(
      "GetFixtureImage", CREATE_NATIVE_ENTRY([](auto args) {
        auto fixture = OpenFixtureAsMapping("shelltest_screenshot.png");
        tonic::DartConverter<tonic::DartConverterMapping>::SetReturnValue(
            args, fixture);
      }));

  auto settings = CreateSettingsForFixture();
  auto configuration = RunConfiguration::InferFromSettings(settings);
  configuration.SetEntrypoint("canDecompressImageFromAsset");
  std::unique_ptr<Shell> shell = CreateShell(settings);
  ASSERT_NE(shell.get(), nullptr);
  RunEngine(shell.get(), std::move(configuration));
  latch.Wait();
  DestroyShell(std::move(shell));
}

/// An image generator that always creates a 1x1 single-frame green image.
class SinglePixelImageGenerator : public ImageGenerator {
 public:
  SinglePixelImageGenerator()
      : info_(SkImageInfo::MakeN32(1, 1, SkAlphaType::kOpaque_SkAlphaType)){};
  ~SinglePixelImageGenerator() = default;
  const SkImageInfo& GetInfo() { return info_; }

  unsigned int GetFrameCount() const { return 1; }

  unsigned int GetPlayCount() const { return 1; }

  const ImageGenerator::FrameInfo GetFrameInfo(unsigned int frame_index) {
    return {std::nullopt, 0, SkCodecAnimation::DisposalMethod::kKeep};
  }

  SkISize GetScaledDimensions(float scale) {
    return SkISize::Make(info_.width(), info_.height());
  }

  bool GetPixels(const SkImageInfo& info,
                 void* pixels,
                 size_t row_bytes,
                 unsigned int frame_index,
                 std::optional<unsigned int> prior_frame) {
    assert(info.width() == 1);
    assert(info.height() == 1);
    assert(row_bytes == 4);

    reinterpret_cast<uint32_t*>(pixels)[0] = 0x00ff00ff;
    return true;
  };

 private:
  SkImageInfo info_;
};

TEST_F(ShellTest, CanRegisterImageDecoders) {
  fml::AutoResetWaitableEvent latch;
  AddNativeCallback("NotifyWidthHeight", CREATE_NATIVE_ENTRY([&](auto args) {
                      auto width = tonic::DartConverter<int>::FromDart(
                          Dart_GetNativeArgument(args, 0));
                      auto height = tonic::DartConverter<int>::FromDart(
                          Dart_GetNativeArgument(args, 1));
                      ASSERT_EQ(width, 1);
                      ASSERT_EQ(height, 1);
                      latch.Signal();
                    }));

  auto settings = CreateSettingsForFixture();
  auto configuration = RunConfiguration::InferFromSettings(settings);
  configuration.SetEntrypoint("canRegisterImageDecoders");
  std::unique_ptr<Shell> shell = CreateShell(settings);
  ASSERT_NE(shell.get(), nullptr);

  fml::TaskRunner::RunNowOrPostTask(
      shell->GetTaskRunners().GetPlatformTaskRunner(), [&shell]() {
        shell->RegisterImageDecoder(
            [](const sk_sp<SkData>& buffer) {
              return std::make_unique<SinglePixelImageGenerator>();
            },
            100);
      });

  RunEngine(shell.get(), std::move(configuration));
  latch.Wait();
  DestroyShell(std::move(shell));
}

TEST_F(ShellTest, OnServiceProtocolGetSkSLsWorks) {
  fml::ScopedTemporaryDirectory base_dir;
  ASSERT_TRUE(base_dir.fd().is_valid());
  PersistentCache::SetCacheDirectoryPath(base_dir.path());
  PersistentCache::ResetCacheForProcess();

  // Create 2 dummy SkSL cache file IE (base32 encoding of A), II (base32
  // encoding of B) with content x and y.
  std::vector<std::string> components = {
      "flutter_engine", GetFlutterEngineVersion(), "skia", GetSkiaVersion(),
      PersistentCache::kSkSLSubdirName};
  auto sksl_dir = fml::CreateDirectory(base_dir.fd(), components,
                                       fml::FilePermission::kReadWrite);
  const std::string x_key_str = "A";
  const std::string x_value_str = "x";
  sk_sp<SkData> x_key =
      SkData::MakeWithCopy(x_key_str.data(), x_key_str.size());
  sk_sp<SkData> x_value =
      SkData::MakeWithCopy(x_value_str.data(), x_value_str.size());
  auto x_data = PersistentCache::BuildCacheObject(*x_key, *x_value);

  const std::string y_key_str = "B";
  const std::string y_value_str = "y";
  sk_sp<SkData> y_key =
      SkData::MakeWithCopy(y_key_str.data(), y_key_str.size());
  sk_sp<SkData> y_value =
      SkData::MakeWithCopy(y_value_str.data(), y_value_str.size());
  auto y_data = PersistentCache::BuildCacheObject(*y_key, *y_value);

  ASSERT_TRUE(fml::WriteAtomically(sksl_dir, "x_cache", *x_data));
  ASSERT_TRUE(fml::WriteAtomically(sksl_dir, "y_cache", *y_data));

  Settings settings = CreateSettingsForFixture();
  std::unique_ptr<Shell> shell = CreateShell(settings);
  ServiceProtocol::Handler::ServiceProtocolMap empty_params;
  rapidjson::Document document;
  OnServiceProtocol(shell.get(), ServiceProtocolEnum::kGetSkSLs,
                    shell->GetTaskRunners().GetIOTaskRunner(), empty_params,
                    &document);
  rapidjson::StringBuffer buffer;
  rapidjson::Writer<rapidjson::StringBuffer> writer(buffer);
  document.Accept(writer);
  DestroyShell(std::move(shell));

  const std::string expected_json1 =
      "{\"type\":\"GetSkSLs\",\"SkSLs\":{\"II\":\"eQ==\",\"IE\":\"eA==\"}}";
  const std::string expected_json2 =
      "{\"type\":\"GetSkSLs\",\"SkSLs\":{\"IE\":\"eA==\",\"II\":\"eQ==\"}}";
  bool json_is_expected = (expected_json1 == buffer.GetString()) ||
                          (expected_json2 == buffer.GetString());
  ASSERT_TRUE(json_is_expected) << buffer.GetString() << " is not equal to "
                                << expected_json1 << " or " << expected_json2;
}

TEST_F(ShellTest, RasterizerScreenshot) {
  Settings settings = CreateSettingsForFixture();
  auto configuration = RunConfiguration::InferFromSettings(settings);
  auto task_runner = CreateNewThread();
  TaskRunners task_runners("test", task_runner, task_runner, task_runner,
                           task_runner);
  std::unique_ptr<Shell> shell = CreateShell(settings, task_runners);

  ASSERT_TRUE(ValidateShell(shell.get()));
  PlatformViewNotifyCreated(shell.get());

  RunEngine(shell.get(), std::move(configuration));

  auto latch = std::make_shared<fml::AutoResetWaitableEvent>();

  PumpOneFrame(shell.get());

  fml::TaskRunner::RunNowOrPostTask(
      shell->GetTaskRunners().GetRasterTaskRunner(), [&shell, &latch]() {
        Rasterizer::Screenshot screenshot =
            shell->GetRasterizer()->ScreenshotLastLayerTree(
                Rasterizer::ScreenshotType::CompressedImage, true);
        EXPECT_NE(screenshot.data, nullptr);

        latch->Signal();
      });
  latch->Wait();
  DestroyShell(std::move(shell), task_runners);
}

TEST_F(ShellTest, RasterizerMakeRasterSnapshot) {
  Settings settings = CreateSettingsForFixture();
  auto configuration = RunConfiguration::InferFromSettings(settings);
  auto task_runner = CreateNewThread();
  TaskRunners task_runners("test", task_runner, task_runner, task_runner,
                           task_runner);
  std::unique_ptr<Shell> shell = CreateShell(settings, task_runners);

  ASSERT_TRUE(ValidateShell(shell.get()));
  PlatformViewNotifyCreated(shell.get());

  RunEngine(shell.get(), std::move(configuration));

  auto latch = std::make_shared<fml::AutoResetWaitableEvent>();

  PumpOneFrame(shell.get());

  fml::TaskRunner::RunNowOrPostTask(
      shell->GetTaskRunners().GetRasterTaskRunner(), [&shell, &latch]() {
        SnapshotDelegate* delegate =
            reinterpret_cast<Rasterizer*>(shell->GetRasterizer().get());
        sk_sp<DlImage> image = delegate->MakeRasterSnapshot(
            MakeSizedDisplayList(50, 50), SkISize::Make(50, 50));
        EXPECT_NE(image, nullptr);

        latch->Signal();
      });
  latch->Wait();
  DestroyShell(std::move(shell), task_runners);
}

TEST_F(ShellTest, OnServiceProtocolEstimateRasterCacheMemoryWorks) {
  Settings settings = CreateSettingsForFixture();
  std::unique_ptr<Shell> shell = CreateShell(settings);

  // 1. Construct a picture and a picture layer to be raster cached.
  sk_sp<DisplayList> display_list = MakeSizedDisplayList(10, 10);
  auto display_list_layer = std::make_shared<DisplayListLayer>(
      SkPoint::Make(0, 0), MakeSizedDisplayList(100, 100), false, false);
  display_list_layer->set_paint_bounds(SkRect::MakeWH(100, 100));

  // 2. Rasterize the picture and the picture layer in the raster cache.
  std::promise<bool> rasterized;

  shell->GetTaskRunners().GetRasterTaskRunner()->PostTask(
      [&shell, &rasterized, &display_list, &display_list_layer] {
        std::vector<RasterCacheItem*> raster_cache_items;
        auto* compositor_context = shell->GetRasterizer()->compositor_context();
        auto& raster_cache = compositor_context->raster_cache();

        LayerStateStack state_stack;
        FixedRefreshRateStopwatch raster_time;
        FixedRefreshRateStopwatch ui_time;
        PaintContext paint_context = {
            // clang-format off
            .state_stack                   = state_stack,
            .canvas                        = nullptr,
            .gr_context                    = nullptr,
            .dst_color_space               = nullptr,
            .view_embedder                 = nullptr,
            .raster_time                   = raster_time,
            .ui_time                       = ui_time,
            .texture_registry              = nullptr,
            .raster_cache                  = &raster_cache,
            // clang-format on
        };

        PrerollContext preroll_context = {
            // clang-format off
            .raster_cache                  = &raster_cache,
            .gr_context                    = nullptr,
            .view_embedder                 = nullptr,
            .state_stack                   = state_stack,
            .dst_color_space               = nullptr,
            .surface_needs_readback        = false,
            .raster_time                   = raster_time,
            .ui_time                       = ui_time,
            .texture_registry              = nullptr,
            .has_platform_view             = false,
            .has_texture_layer             = false,
            .raster_cached_entries         = &raster_cache_items,
            // clang-format on
        };

        // 2.1. Rasterize the picture. Call Draw multiple times to pass the
        // access threshold (default to 3) so a cache can be generated.
        MockCanvas dummy_canvas;
        DlPaint paint;
        bool picture_cache_generated;
        DisplayListRasterCacheItem display_list_raster_cache_item(
            display_list, SkPoint(), true, false);
        for (int i = 0; i < 4; i += 1) {
          SkMatrix matrix = SkMatrix::I();
          state_stack.set_preroll_delegate(matrix);
          display_list_raster_cache_item.PrerollSetup(&preroll_context, matrix);
          display_list_raster_cache_item.PrerollFinalize(&preroll_context,
                                                         matrix);
          picture_cache_generated =
              display_list_raster_cache_item.need_caching();
          state_stack.set_delegate(&dummy_canvas);
          display_list_raster_cache_item.TryToPrepareRasterCache(paint_context);
          display_list_raster_cache_item.Draw(paint_context, &dummy_canvas,
                                              &paint);
        }
        ASSERT_TRUE(picture_cache_generated);

        // 2.2. Rasterize the picture layer.
        LayerRasterCacheItem layer_raster_cache_item(display_list_layer.get());
        state_stack.set_preroll_delegate(SkMatrix::I());
        layer_raster_cache_item.PrerollSetup(&preroll_context, SkMatrix::I());
        layer_raster_cache_item.PrerollFinalize(&preroll_context,
                                                SkMatrix::I());
        state_stack.set_delegate(&dummy_canvas);
        layer_raster_cache_item.TryToPrepareRasterCache(paint_context);
        layer_raster_cache_item.Draw(paint_context, &dummy_canvas, &paint);
        rasterized.set_value(true);
      });
  rasterized.get_future().wait();

  // 3. Call the service protocol and check its output.
  ServiceProtocol::Handler::ServiceProtocolMap empty_params;
  rapidjson::Document document;
  OnServiceProtocol(
      shell.get(), ServiceProtocolEnum::kEstimateRasterCacheMemory,
      shell->GetTaskRunners().GetRasterTaskRunner(), empty_params, &document);
  rapidjson::StringBuffer buffer;
  rapidjson::Writer<rapidjson::StringBuffer> writer(buffer);
  document.Accept(writer);
  std::string expected_json =
      "{\"type\":\"EstimateRasterCacheMemory\",\"layerBytes\":40024,\"picture"
      "Bytes\":424}";
  std::string actual_json = buffer.GetString();
  ASSERT_EQ(actual_json, expected_json);

  DestroyShell(std::move(shell));
}

// ktz
TEST_F(ShellTest, OnServiceProtocolRenderFrameWithRasterStatsWorks) {
  auto settings = CreateSettingsForFixture();
  std::unique_ptr<Shell> shell = CreateShell(settings);

  // Create the surface needed by rasterizer
  PlatformViewNotifyCreated(shell.get());

  auto configuration = RunConfiguration::InferFromSettings(settings);
  configuration.SetEntrypoint("scene_with_red_box");

  RunEngine(shell.get(), std::move(configuration));
  PumpOneFrame(shell.get());

  ServiceProtocol::Handler::ServiceProtocolMap empty_params;
  rapidjson::Document document;
  OnServiceProtocol(
      shell.get(), ServiceProtocolEnum::kRenderFrameWithRasterStats,
      shell->GetTaskRunners().GetRasterTaskRunner(), empty_params, &document);
  rapidjson::StringBuffer buffer;
  rapidjson::Writer<rapidjson::StringBuffer> writer(buffer);
  document.Accept(writer);

  // It would be better to parse out the json and check for the validity of
  // fields. Below checks approximate what needs to be checked, this can not be
  // an exact check since duration will not exactly match.
#ifdef SHELL_ENABLE_METAL
  std::string expected_json =
      "\"snapshot\":[137,80,78,71,13,10,26,10,0,0,0,13,73,72,68,82,0,0,3,32,0,"
      "0,2,88,8,6,0,0,0,154,118,130,112,0,0,0,1,115,82,71,66,0,174,206,28,233,"
      "0,0,0,4,115,66,73,84,8,8,8,8,124,8,100,136,0,0,7,103,73,68,65,84,120,"
      "156,237,206,65,13,192,48,0,3,177,211,248,115,78,73,172,234,199,70,224,"
      "86,91,45,0,0,128,203,190,215,1,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,"
      "0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,"
      "0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,"
      "0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,"
      "0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,"
      "0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,"
      "0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,"
      "0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,"
      "0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,"
      "0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,"
      "0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,"
      "0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,"
      "0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,"
      "0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,"
      "0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,"
      "0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,"
      "0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,"
      "0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,"
      "0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,"
      "0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,"
      "0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,"
      "0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,"
      "0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,"
      "0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,"
      "0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,"
      "0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,"
      "0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,"
      "0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,"
      "0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,"
      "0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,"
      "0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,"
      "0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,"
      "0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,"
      "0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,"
      "0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,"
      "0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,"
      "0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,"
      "0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,"
      "0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,"
      "0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,"
      "0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,"
      "0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,"
      "0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,"
      "0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,"
      "0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,"
      "0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,"
      "0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,"
      "0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,"
      "0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,"
      "0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,"
      "0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,"
      "0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,"
      "0,224,47,7,195,182,3,255,101,111,186,90,0,0,0,0,73,69,78,68,174,66,96,"
      "130]";
#else
  std::string expected_json =
      "\"snapshot\":[137,80,78,71,13,10,26,10,0,"
      "0,0,13,73,72,68,82,0,0,0,1,0,0,0,1,8,6,0,0,0,31,21,196,137,0,0,0,1,115,"
      "82,71,66,0,174,206,28,233,0,0,0,4,115,66,73,84,8,8,8,8,124,8,100,136,0,"
      "0,0,13,73,68,65,84,8,153,99,248,207,192,240,31,0,5,0,1,255,171,206,54,"
      "137,0,0,0,0,73,69,78,68,174,66,96,130]";
#endif
  std::string actual_json = buffer.GetString();

  EXPECT_THAT(actual_json, ::testing::HasSubstr(expected_json));
  EXPECT_THAT(actual_json,
              ::testing::HasSubstr("{\"type\":\"RenderFrameWithRasterStats\""));
  EXPECT_THAT(actual_json, ::testing::HasSubstr("\"duration_micros\""));

  PlatformViewNotifyDestroyed(shell.get());
  DestroyShell(std::move(shell));
}

// TODO(https://github.com/flutter/flutter/issues/100273): Disabled due to
// flakiness.
// TODO(https://github.com/flutter/flutter/issues/100299): Fix it when
// re-enabling.
TEST_F(ShellTest, DISABLED_DiscardLayerTreeOnResize) {
  auto settings = CreateSettingsForFixture();

  SkISize wrong_size = SkISize::Make(400, 100);
  SkISize expected_size = SkISize::Make(400, 200);

  fml::AutoResetWaitableEvent end_frame_latch;
  auto end_frame_callback =
      [&](bool should_merge_thread,
          const fml::RefPtr<fml::RasterThreadMerger>& raster_thread_merger) {
        end_frame_latch.Signal();
      };
  auto external_view_embedder = std::make_shared<ShellTestExternalViewEmbedder>(
      std::move(end_frame_callback), PostPrerollResult::kSuccess, false);
  std::unique_ptr<Shell> shell = CreateShell({
      .settings = settings,
      .platform_view_create_callback = ShellTestPlatformViewBuilder({
          .shell_test_external_view_embedder = external_view_embedder,
      }),
  });

  // Create the surface needed by rasterizer
  PlatformViewNotifyCreated(shell.get());

  fml::TaskRunner::RunNowOrPostTask(
      shell->GetTaskRunners().GetPlatformTaskRunner(),
      [&shell, &expected_size]() {
        shell->GetPlatformView()->SetViewportMetrics(
            kImplicitViewId,
            {1.0, static_cast<double>(expected_size.width()),
             static_cast<double>(expected_size.height()), 22, 0});
      });

  auto configuration = RunConfiguration::InferFromSettings(settings);
  configuration.SetEntrypoint("emptyMain");

  RunEngine(shell.get(), std::move(configuration));

  PumpOneFrame(shell.get(), static_cast<double>(wrong_size.width()),
               static_cast<double>(wrong_size.height()));
  end_frame_latch.Wait();
  // Wrong size, no frames are submitted.
  ASSERT_EQ(0, external_view_embedder->GetSubmittedFrameCount());

  PumpOneFrame(shell.get(), static_cast<double>(expected_size.width()),
               static_cast<double>(expected_size.height()));
  end_frame_latch.Wait();
  // Expected size, 1 frame submitted.
  ASSERT_EQ(1, external_view_embedder->GetSubmittedFrameCount());
  ASSERT_EQ(expected_size, external_view_embedder->GetLastSubmittedFrameSize());

  PlatformViewNotifyDestroyed(shell.get());
  DestroyShell(std::move(shell));
}

// TODO(https://github.com/flutter/flutter/issues/100273): Disabled due to
// flakiness.
// TODO(https://github.com/flutter/flutter/issues/100299): Fix it when
// re-enabling.
TEST_F(ShellTest, DISABLED_DiscardResubmittedLayerTreeOnResize) {
  auto settings = CreateSettingsForFixture();

  SkISize origin_size = SkISize::Make(400, 100);
  SkISize new_size = SkISize::Make(400, 200);

  fml::AutoResetWaitableEvent end_frame_latch;

  fml::AutoResetWaitableEvent resize_latch;

  std::shared_ptr<ShellTestExternalViewEmbedder> external_view_embedder;
  fml::RefPtr<fml::RasterThreadMerger> raster_thread_merger_ref;
  auto end_frame_callback =
      [&](bool should_merge_thread,
          const fml::RefPtr<fml::RasterThreadMerger>& raster_thread_merger) {
        if (!raster_thread_merger_ref) {
          raster_thread_merger_ref = raster_thread_merger;
        }
        if (should_merge_thread) {
          raster_thread_merger->MergeWithLease(10);
          external_view_embedder->UpdatePostPrerollResult(
              PostPrerollResult::kSuccess);
        }
        end_frame_latch.Signal();

        if (should_merge_thread) {
          resize_latch.Wait();
        }
      };

  external_view_embedder = std::make_shared<ShellTestExternalViewEmbedder>(
      std::move(end_frame_callback), PostPrerollResult::kResubmitFrame, true);

  std::unique_ptr<Shell> shell = CreateShell({
      .settings = settings,
      .platform_view_create_callback = ShellTestPlatformViewBuilder({
          .shell_test_external_view_embedder = external_view_embedder,
      }),
  });

  // Create the surface needed by rasterizer
  PlatformViewNotifyCreated(shell.get());

  fml::TaskRunner::RunNowOrPostTask(
      shell->GetTaskRunners().GetPlatformTaskRunner(),
      [&shell, &origin_size]() {
        shell->GetPlatformView()->SetViewportMetrics(
            kImplicitViewId,
            {1.0, static_cast<double>(origin_size.width()),
             static_cast<double>(origin_size.height()), 22, 0});
      });

  auto configuration = RunConfiguration::InferFromSettings(settings);
  configuration.SetEntrypoint("emptyMain");

  RunEngine(shell.get(), std::move(configuration));

  PumpOneFrame(shell.get(), static_cast<double>(origin_size.width()),
               static_cast<double>(origin_size.height()));

  end_frame_latch.Wait();
  ASSERT_EQ(0, external_view_embedder->GetSubmittedFrameCount());

  fml::TaskRunner::RunNowOrPostTask(
      shell->GetTaskRunners().GetPlatformTaskRunner(),
      [&shell, &new_size, &resize_latch]() {
        shell->GetPlatformView()->SetViewportMetrics(
            kImplicitViewId, {1.0, static_cast<double>(new_size.width()),
                              static_cast<double>(new_size.height()), 22, 0});
        resize_latch.Signal();
      });

  end_frame_latch.Wait();

  // The frame resubmitted with origin size should be discarded after the
  // viewport metrics changed.
  ASSERT_EQ(0, external_view_embedder->GetSubmittedFrameCount());

  // Threads will be merged at the end of this frame.
  PumpOneFrame(shell.get(), static_cast<double>(new_size.width()),
               static_cast<double>(new_size.height()));

  end_frame_latch.Wait();
  ASSERT_TRUE(raster_thread_merger_ref->IsMerged());
  ASSERT_EQ(1, external_view_embedder->GetSubmittedFrameCount());
  ASSERT_EQ(new_size, external_view_embedder->GetLastSubmittedFrameSize());

  PlatformViewNotifyDestroyed(shell.get());
  DestroyShell(std::move(shell));
}

TEST_F(ShellTest, IgnoresInvalidMetrics) {
  fml::AutoResetWaitableEvent latch;
  double last_device_pixel_ratio;
  double last_width;
  double last_height;
  auto native_report_device_pixel_ratio = [&](Dart_NativeArguments args) {
    auto dpr_handle = Dart_GetNativeArgument(args, 0);
    ASSERT_TRUE(Dart_IsDouble(dpr_handle));
    Dart_DoubleValue(dpr_handle, &last_device_pixel_ratio);
    ASSERT_FALSE(last_device_pixel_ratio == 0.0);

    auto width_handle = Dart_GetNativeArgument(args, 1);
    ASSERT_TRUE(Dart_IsDouble(width_handle));
    Dart_DoubleValue(width_handle, &last_width);
    ASSERT_FALSE(last_width == 0.0);

    auto height_handle = Dart_GetNativeArgument(args, 2);
    ASSERT_TRUE(Dart_IsDouble(height_handle));
    Dart_DoubleValue(height_handle, &last_height);
    ASSERT_FALSE(last_height == 0.0);

    latch.Signal();
  };

  Settings settings = CreateSettingsForFixture();
  auto task_runner = CreateNewThread();
  TaskRunners task_runners("test", task_runner, task_runner, task_runner,
                           task_runner);

  AddNativeCallback("ReportMetrics",
                    CREATE_NATIVE_ENTRY(native_report_device_pixel_ratio));

  std::unique_ptr<Shell> shell = CreateShell(settings, task_runners);

  auto configuration = RunConfiguration::InferFromSettings(settings);
  configuration.SetEntrypoint("reportMetrics");

  RunEngine(shell.get(), std::move(configuration));

  task_runner->PostTask([&]() {
    // This one is invalid for having 0 pixel ratio.
    shell->GetPlatformView()->SetViewportMetrics(kImplicitViewId,
                                                 {0.0, 400, 200, 22, 0});
    task_runner->PostTask([&]() {
      // This one is invalid for having 0 width.
      shell->GetPlatformView()->SetViewportMetrics(kImplicitViewId,
                                                   {0.8, 0.0, 200, 22, 0});
      task_runner->PostTask([&]() {
        // This one is invalid for having 0 height.
        shell->GetPlatformView()->SetViewportMetrics(kImplicitViewId,
                                                     {0.8, 400, 0.0, 22, 0});
        task_runner->PostTask([&]() {
          // This one makes it through.
          shell->GetPlatformView()->SetViewportMetrics(
              kImplicitViewId, {0.8, 400, 200.0, 22, 0});
        });
      });
    });
  });
  latch.Wait();
  ASSERT_EQ(last_device_pixel_ratio, 0.8);
  ASSERT_EQ(last_width, 400.0);
  ASSERT_EQ(last_height, 200.0);
  latch.Reset();

  task_runner->PostTask([&]() {
    shell->GetPlatformView()->SetViewportMetrics(kImplicitViewId,
                                                 {1.2, 600, 300, 22, 0});
  });
  latch.Wait();
  ASSERT_EQ(last_device_pixel_ratio, 1.2);
  ASSERT_EQ(last_width, 600.0);
  ASSERT_EQ(last_height, 300.0);

  DestroyShell(std::move(shell), task_runners);
}

TEST_F(ShellTest, IgnoresMetricsUpdateToInvalidView) {
  fml::AutoResetWaitableEvent latch;
  double last_device_pixel_ratio;
  // This callback will be called whenever any view's metrics change.
  auto native_report_device_pixel_ratio = [&](Dart_NativeArguments args) {
    // The correct call will have a DPR of 3.
    auto dpr_handle = Dart_GetNativeArgument(args, 0);
    ASSERT_TRUE(Dart_IsDouble(dpr_handle));
    Dart_DoubleValue(dpr_handle, &last_device_pixel_ratio);
    ASSERT_TRUE(last_device_pixel_ratio > 2.5);

    latch.Signal();
  };

  Settings settings = CreateSettingsForFixture();
  auto task_runner = CreateNewThread();
  TaskRunners task_runners("test", task_runner, task_runner, task_runner,
                           task_runner);

  AddNativeCallback("ReportMetrics",
                    CREATE_NATIVE_ENTRY(native_report_device_pixel_ratio));

  std::unique_ptr<Shell> shell = CreateShell(settings, task_runners);

  auto configuration = RunConfiguration::InferFromSettings(settings);
  configuration.SetEntrypoint("reportMetrics");

  RunEngine(shell.get(), std::move(configuration));

  task_runner->PostTask([&]() {
    // This one is invalid for having an nonexistent view ID.
    // Also, it has a DPR of 2.0 for detection.
    shell->GetPlatformView()->SetViewportMetrics(2, {2.0, 400, 200, 22, 0});
    task_runner->PostTask([&]() {
      // This one is valid with DPR 3.0.
      shell->GetPlatformView()->SetViewportMetrics(kImplicitViewId,
                                                   {3.0, 400, 200, 22, 0});
    });
  });
  latch.Wait();
  ASSERT_EQ(last_device_pixel_ratio, 3.0);
  latch.Reset();

  DestroyShell(std::move(shell), task_runners);
}

TEST_F(ShellTest, OnServiceProtocolSetAssetBundlePathWorks) {
  Settings settings = CreateSettingsForFixture();
  std::unique_ptr<Shell> shell = CreateShell(settings);
  RunConfiguration configuration =
      RunConfiguration::InferFromSettings(settings);
  configuration.SetEntrypoint("canAccessResourceFromAssetDir");

  // Verify isolate can load a known resource with the
  // default asset directory - kernel_blob.bin
  fml::AutoResetWaitableEvent latch;

  // Callback used to signal whether the resource was loaded successfully.
  bool can_access_resource = false;
  auto native_can_access_resource = [&can_access_resource,
                                     &latch](Dart_NativeArguments args) {
    Dart_Handle exception = nullptr;
    can_access_resource =
        tonic::DartConverter<bool>::FromArguments(args, 0, exception);
    latch.Signal();
  };
  AddNativeCallback("NotifyCanAccessResource",
                    CREATE_NATIVE_ENTRY(native_can_access_resource));

  // Callback used to delay the asset load until after the service
  // protocol method has finished.
  auto native_notify_set_asset_bundle_path =
      [&shell](Dart_NativeArguments args) {
        // Update the asset directory to a bonus path.
        ServiceProtocol::Handler::ServiceProtocolMap params;
        params["assetDirectory"] = "assetDirectory";
        rapidjson::Document document;
        OnServiceProtocol(shell.get(), ServiceProtocolEnum::kSetAssetBundlePath,
                          shell->GetTaskRunners().GetUITaskRunner(), params,
                          &document);
        rapidjson::StringBuffer buffer;
        rapidjson::Writer<rapidjson::StringBuffer> writer(buffer);
        document.Accept(writer);
      };
  AddNativeCallback("NotifySetAssetBundlePath",
                    CREATE_NATIVE_ENTRY(native_notify_set_asset_bundle_path));

  RunEngine(shell.get(), std::move(configuration));

  latch.Wait();
  ASSERT_TRUE(can_access_resource);

  DestroyShell(std::move(shell));
}

TEST_F(ShellTest, EngineRootIsolateLaunchesDontTakeVMDataSettings) {
  ASSERT_FALSE(DartVMRef::IsInstanceRunning());
  // Make sure the shell launch does not kick off the creation of the VM
  // instance by already creating one upfront.
  auto vm_settings = CreateSettingsForFixture();
  auto vm_ref = DartVMRef::Create(vm_settings);
  ASSERT_TRUE(DartVMRef::IsInstanceRunning());

  auto settings = vm_settings;
  fml::AutoResetWaitableEvent isolate_create_latch;
  settings.root_isolate_create_callback = [&](const auto& isolate) {
    isolate_create_latch.Signal();
  };
  auto shell = CreateShell(settings);
  ASSERT_TRUE(ValidateShell(shell.get()));
  auto configuration = RunConfiguration::InferFromSettings(settings);
  ASSERT_TRUE(configuration.IsValid());
  RunEngine(shell.get(), std::move(configuration));
  ASSERT_TRUE(DartVMRef::IsInstanceRunning());
  DestroyShell(std::move(shell));
  isolate_create_latch.Wait();
}

TEST_F(ShellTest, AssetManagerSingle) {
  fml::ScopedTemporaryDirectory asset_dir;
  fml::UniqueFD asset_dir_fd = fml::OpenDirectory(
      asset_dir.path().c_str(), false, fml::FilePermission::kRead);

  std::string filename = "test_name";
  std::string content = "test_content";

  bool success = fml::WriteAtomically(asset_dir_fd, filename.c_str(),
                                      fml::DataMapping(content));
  ASSERT_TRUE(success);

  AssetManager asset_manager;
  asset_manager.PushBack(
      std::make_unique<DirectoryAssetBundle>(std::move(asset_dir_fd), false));

  auto mapping = asset_manager.GetAsMapping(filename);
  ASSERT_TRUE(mapping != nullptr);

  std::string result(reinterpret_cast<const char*>(mapping->GetMapping()),
                     mapping->GetSize());

  ASSERT_TRUE(result == content);
}

TEST_F(ShellTest, AssetManagerMulti) {
  fml::ScopedTemporaryDirectory asset_dir;
  fml::UniqueFD asset_dir_fd = fml::OpenDirectory(
      asset_dir.path().c_str(), false, fml::FilePermission::kRead);

  std::vector<std::string> filenames = {
      "good0",
      "bad0",
      "good1",
      "bad1",
  };

  for (auto filename : filenames) {
    bool success = fml::WriteAtomically(asset_dir_fd, filename.c_str(),
                                        fml::DataMapping(filename));
    ASSERT_TRUE(success);
  }

  AssetManager asset_manager;
  asset_manager.PushBack(
      std::make_unique<DirectoryAssetBundle>(std::move(asset_dir_fd), false));

  auto mappings = asset_manager.GetAsMappings("(.*)", std::nullopt);
  EXPECT_EQ(mappings.size(), 4u);

  std::vector<std::string> expected_results = {
      "good0",
      "good1",
  };

  mappings = asset_manager.GetAsMappings("(.*)good(.*)", std::nullopt);
  ASSERT_EQ(mappings.size(), expected_results.size());

  for (auto& mapping : mappings) {
    std::string result(reinterpret_cast<const char*>(mapping->GetMapping()),
                       mapping->GetSize());
    EXPECT_NE(
        std::find(expected_results.begin(), expected_results.end(), result),
        expected_results.end());
  }
}

#if defined(OS_FUCHSIA)
TEST_F(ShellTest, AssetManagerMultiSubdir) {
  std::string subdir_path = "subdir";

  fml::ScopedTemporaryDirectory asset_dir;
  fml::UniqueFD asset_dir_fd = fml::OpenDirectory(
      asset_dir.path().c_str(), false, fml::FilePermission::kRead);
  fml::UniqueFD subdir_fd =
      fml::OpenDirectory((asset_dir.path() + "/" + subdir_path).c_str(), true,
                         fml::FilePermission::kReadWrite);

  std::vector<std::string> filenames = {
      "bad0",
      "notgood",  // this is to make sure the pattern (.*)good(.*) only
                  // matches things in the subdirectory
  };

  std::vector<std::string> subdir_filenames = {
      "good0",
      "good1",
      "bad1",
  };

  for (auto filename : filenames) {
    bool success = fml::WriteAtomically(asset_dir_fd, filename.c_str(),
                                        fml::DataMapping(filename));
    ASSERT_TRUE(success);
  }

  for (auto filename : subdir_filenames) {
    bool success = fml::WriteAtomically(subdir_fd, filename.c_str(),
                                        fml::DataMapping(filename));
    ASSERT_TRUE(success);
  }

  AssetManager asset_manager;
  asset_manager.PushBack(
      std::make_unique<DirectoryAssetBundle>(std::move(asset_dir_fd), false));

  auto mappings = asset_manager.GetAsMappings("(.*)", std::nullopt);
  EXPECT_EQ(mappings.size(), 5u);

  mappings = asset_manager.GetAsMappings("(.*)", subdir_path);
  EXPECT_EQ(mappings.size(), 3u);

  std::vector<std::string> expected_results = {
      "good0",
      "good1",
  };

  mappings = asset_manager.GetAsMappings("(.*)good(.*)", subdir_path);
  ASSERT_EQ(mappings.size(), expected_results.size());

  for (auto& mapping : mappings) {
    std::string result(reinterpret_cast<const char*>(mapping->GetMapping()),
                       mapping->GetSize());
    ASSERT_NE(
        std::find(expected_results.begin(), expected_results.end(), result),
        expected_results.end());
  }
}
#endif  // OS_FUCHSIA

TEST_F(ShellTest, Spawn) {
  auto settings = CreateSettingsForFixture();
  auto shell = CreateShell(settings);
  ASSERT_TRUE(ValidateShell(shell.get()));

  auto configuration = RunConfiguration::InferFromSettings(settings);
  ASSERT_TRUE(configuration.IsValid());
  configuration.SetEntrypoint("fixturesAreFunctionalMain");

  auto second_configuration = RunConfiguration::InferFromSettings(settings);
  ASSERT_TRUE(second_configuration.IsValid());
  second_configuration.SetEntrypoint("testCanLaunchSecondaryIsolate");

  const std::string initial_route("/foo");

  fml::AutoResetWaitableEvent main_latch;
  std::string last_entry_point;
  // Fulfill native function for the first Shell's entrypoint.
  AddNativeCallback(
      "SayHiFromFixturesAreFunctionalMain", CREATE_NATIVE_ENTRY([&](auto args) {
        last_entry_point = shell->GetEngine()->GetLastEntrypoint();
        main_latch.Signal();
      }));
  // Fulfill native function for the second Shell's entrypoint.
  fml::CountDownLatch second_latch(2);
  AddNativeCallback(
      // The Dart native function names aren't very consistent but this is
      // just the native function name of the second vm entrypoint in the
      // fixture.
      "NotifyNative",
      CREATE_NATIVE_ENTRY([&](auto args) { second_latch.CountDown(); }));

  RunEngine(shell.get(), std::move(configuration));
  main_latch.Wait();
  ASSERT_TRUE(DartVMRef::IsInstanceRunning());
  // Check first Shell ran the first entrypoint.
  ASSERT_EQ("fixturesAreFunctionalMain", last_entry_point);

  PostSync(
      shell->GetTaskRunners().GetPlatformTaskRunner(),
      [this, &spawner = shell, &second_configuration, &second_latch,
       initial_route]() {
        MockPlatformViewDelegate platform_view_delegate;
        auto spawn = spawner->Spawn(
            std::move(second_configuration), initial_route,
            [&platform_view_delegate](Shell& shell) {
              auto result = std::make_unique<MockPlatformView>(
                  platform_view_delegate, shell.GetTaskRunners());
              ON_CALL(*result, CreateRenderingSurface())
                  .WillByDefault(::testing::Invoke(
                      [] { return std::make_unique<MockSurface>(); }));
              return result;
            },
            [](Shell& shell) { return std::make_unique<Rasterizer>(shell); });
        ASSERT_NE(nullptr, spawn.get());
        ASSERT_TRUE(ValidateShell(spawn.get()));

        PostSync(spawner->GetTaskRunners().GetUITaskRunner(), [&spawn, &spawner,
                                                               initial_route] {
          // Check second shell ran the second entrypoint.
          ASSERT_EQ("testCanLaunchSecondaryIsolate",
                    spawn->GetEngine()->GetLastEntrypoint());
          ASSERT_EQ(initial_route, spawn->GetEngine()->InitialRoute());

          ASSERT_NE(spawner->GetEngine()
                        ->GetRuntimeController()
                        ->GetRootIsolateGroup(),
                    0u);
          ASSERT_EQ(spawner->GetEngine()
                        ->GetRuntimeController()
                        ->GetRootIsolateGroup(),
                    spawn->GetEngine()
                        ->GetRuntimeController()
                        ->GetRootIsolateGroup());
          auto spawner_snapshot_delegate = spawner->GetEngine()
                                               ->GetRuntimeController()
                                               ->GetSnapshotDelegate();
          auto spawn_snapshot_delegate =
              spawn->GetEngine()->GetRuntimeController()->GetSnapshotDelegate();
          PostSync(spawner->GetTaskRunners().GetRasterTaskRunner(),
                   [spawner_snapshot_delegate, spawn_snapshot_delegate] {
                     ASSERT_NE(spawner_snapshot_delegate.get(),
                               spawn_snapshot_delegate.get());
                   });
        });
        PostSync(
            spawner->GetTaskRunners().GetIOTaskRunner(), [&spawner, &spawn] {
              ASSERT_EQ(spawner->GetIOManager()->GetResourceContext().get(),
                        spawn->GetIOManager()->GetResourceContext().get());
            });

        // Before destroying the shell, wait for expectations of the spawned
        // isolate to be met.
        second_latch.Wait();

        DestroyShell(std::move(spawn));
      });

  DestroyShell(std::move(shell));
  ASSERT_FALSE(DartVMRef::IsInstanceRunning());
}

TEST_F(ShellTest, SpawnWithDartEntrypointArgs) {
  auto settings = CreateSettingsForFixture();
  auto shell = CreateShell(settings);
  ASSERT_TRUE(ValidateShell(shell.get()));

  auto configuration = RunConfiguration::InferFromSettings(settings);
  ASSERT_TRUE(configuration.IsValid());
  configuration.SetEntrypoint("canReceiveArgumentsWhenEngineRun");
  const std::vector<std::string> entrypoint_args{"foo", "bar"};
  configuration.SetEntrypointArgs(entrypoint_args);

  auto second_configuration = RunConfiguration::InferFromSettings(settings);
  ASSERT_TRUE(second_configuration.IsValid());
  second_configuration.SetEntrypoint("canReceiveArgumentsWhenEngineSpawn");
  const std::vector<std::string> second_entrypoint_args{"arg1", "arg2"};
  second_configuration.SetEntrypointArgs(second_entrypoint_args);

  const std::string initial_route("/foo");

  fml::AutoResetWaitableEvent main_latch;
  std::string last_entry_point;
  // Fulfill native function for the first Shell's entrypoint.
  AddNativeCallback("NotifyNativeWhenEngineRun",
                    CREATE_NATIVE_ENTRY(([&](Dart_NativeArguments args) {
                      ASSERT_TRUE(tonic::DartConverter<bool>::FromDart(
                          Dart_GetNativeArgument(args, 0)));
                      last_entry_point =
                          shell->GetEngine()->GetLastEntrypoint();
                      main_latch.Signal();
                    })));

  fml::AutoResetWaitableEvent second_latch;
  // Fulfill native function for the second Shell's entrypoint.
  AddNativeCallback("NotifyNativeWhenEngineSpawn",
                    CREATE_NATIVE_ENTRY(([&](Dart_NativeArguments args) {
                      ASSERT_TRUE(tonic::DartConverter<bool>::FromDart(
                          Dart_GetNativeArgument(args, 0)));
                      last_entry_point =
                          shell->GetEngine()->GetLastEntrypoint();
                      second_latch.Signal();
                    })));

  RunEngine(shell.get(), std::move(configuration));
  main_latch.Wait();
  ASSERT_TRUE(DartVMRef::IsInstanceRunning());
  // Check first Shell ran the first entrypoint.
  ASSERT_EQ("canReceiveArgumentsWhenEngineRun", last_entry_point);

  PostSync(
      shell->GetTaskRunners().GetPlatformTaskRunner(),
      [this, &spawner = shell, &second_configuration, &second_latch,
       initial_route]() {
        MockPlatformViewDelegate platform_view_delegate;
        auto spawn = spawner->Spawn(
            std::move(second_configuration), initial_route,
            [&platform_view_delegate](Shell& shell) {
              auto result = std::make_unique<MockPlatformView>(
                  platform_view_delegate, shell.GetTaskRunners());
              ON_CALL(*result, CreateRenderingSurface())
                  .WillByDefault(::testing::Invoke(
                      [] { return std::make_unique<MockSurface>(); }));
              return result;
            },
            [](Shell& shell) { return std::make_unique<Rasterizer>(shell); });
        ASSERT_NE(nullptr, spawn.get());
        ASSERT_TRUE(ValidateShell(spawn.get()));

        PostSync(spawner->GetTaskRunners().GetUITaskRunner(),
                 [&spawn, &spawner, initial_route] {
                   // Check second shell ran the second entrypoint.
                   ASSERT_EQ("canReceiveArgumentsWhenEngineSpawn",
                             spawn->GetEngine()->GetLastEntrypoint());
                   ASSERT_EQ(initial_route, spawn->GetEngine()->InitialRoute());

                   ASSERT_NE(spawner->GetEngine()
                                 ->GetRuntimeController()
                                 ->GetRootIsolateGroup(),
                             0u);
                   ASSERT_EQ(spawner->GetEngine()
                                 ->GetRuntimeController()
                                 ->GetRootIsolateGroup(),
                             spawn->GetEngine()
                                 ->GetRuntimeController()
                                 ->GetRootIsolateGroup());
                 });

        PostSync(
            spawner->GetTaskRunners().GetIOTaskRunner(), [&spawner, &spawn] {
              ASSERT_EQ(spawner->GetIOManager()->GetResourceContext().get(),
                        spawn->GetIOManager()->GetResourceContext().get());
            });

        // Before destroying the shell, wait for expectations of the spawned
        // isolate to be met.
        second_latch.Wait();

        DestroyShell(std::move(spawn));
      });

  DestroyShell(std::move(shell));
  ASSERT_FALSE(DartVMRef::IsInstanceRunning());
}

TEST_F(ShellTest, IOManagerIsSharedBetweenParentAndSpawnedShell) {
  auto settings = CreateSettingsForFixture();
  auto shell = CreateShell(settings);
  ASSERT_TRUE(ValidateShell(shell.get()));

  PostSync(shell->GetTaskRunners().GetPlatformTaskRunner(), [this,
                                                             &spawner = shell,
                                                             &settings] {
    auto second_configuration = RunConfiguration::InferFromSettings(settings);
    ASSERT_TRUE(second_configuration.IsValid());
    second_configuration.SetEntrypoint("emptyMain");
    const std::string initial_route("/foo");
    MockPlatformViewDelegate platform_view_delegate;
    auto spawn = spawner->Spawn(
        std::move(second_configuration), initial_route,
        [&platform_view_delegate](Shell& shell) {
          auto result = std::make_unique<MockPlatformView>(
              platform_view_delegate, shell.GetTaskRunners());
          ON_CALL(*result, CreateRenderingSurface())
              .WillByDefault(::testing::Invoke(
                  [] { return std::make_unique<MockSurface>(); }));
          return result;
        },
        [](Shell& shell) { return std::make_unique<Rasterizer>(shell); });
    ASSERT_TRUE(ValidateShell(spawn.get()));

    PostSync(spawner->GetTaskRunners().GetIOTaskRunner(), [&spawner, &spawn] {
      ASSERT_NE(spawner->GetIOManager().get(), nullptr);
      ASSERT_EQ(spawner->GetIOManager().get(), spawn->GetIOManager().get());
    });

    // Destroy the child shell.
    DestroyShell(std::move(spawn));
  });
  // Destroy the parent shell.
  DestroyShell(std::move(shell));
}

TEST_F(ShellTest, IOManagerInSpawnedShellIsNotNullAfterParentShellDestroyed) {
  auto settings = CreateSettingsForFixture();
  auto shell = CreateShell(settings);
  ASSERT_TRUE(ValidateShell(shell.get()));

  PostSync(shell->GetTaskRunners().GetUITaskRunner(), [&shell] {
    // We must get engine on UI thread.
    auto runtime_controller = shell->GetEngine()->GetRuntimeController();
    PostSync(shell->GetTaskRunners().GetIOTaskRunner(),
             [&shell, &runtime_controller] {
               // We must get io_manager on IO thread.
               auto io_manager = runtime_controller->GetIOManager();
               // Check io_manager existence.
               ASSERT_NE(io_manager.get(), nullptr);
               ASSERT_NE(io_manager->GetSkiaUnrefQueue().get(), nullptr);
               // Get io_manager directly from shell and check its existence.
               ASSERT_NE(shell->GetIOManager().get(), nullptr);
             });
  });

  std::unique_ptr<Shell> spawn;

  PostSync(shell->GetTaskRunners().GetPlatformTaskRunner(), [&shell, &settings,
                                                             &spawn] {
    auto second_configuration = RunConfiguration::InferFromSettings(settings);
    ASSERT_TRUE(second_configuration.IsValid());
    second_configuration.SetEntrypoint("emptyMain");
    const std::string initial_route("/foo");
    MockPlatformViewDelegate platform_view_delegate;
    auto child = shell->Spawn(
        std::move(second_configuration), initial_route,
        [&platform_view_delegate](Shell& shell) {
          auto result = std::make_unique<MockPlatformView>(
              platform_view_delegate, shell.GetTaskRunners());
          ON_CALL(*result, CreateRenderingSurface())
              .WillByDefault(::testing::Invoke(
                  [] { return std::make_unique<MockSurface>(); }));
          return result;
        },
        [](Shell& shell) { return std::make_unique<Rasterizer>(shell); });
    spawn = std::move(child);
  });
  // Destroy the parent shell.
  DestroyShell(std::move(shell));

  PostSync(spawn->GetTaskRunners().GetUITaskRunner(), [&spawn] {
    // We must get engine on UI thread.
    auto runtime_controller = spawn->GetEngine()->GetRuntimeController();
    PostSync(spawn->GetTaskRunners().GetIOTaskRunner(),
             [&spawn, &runtime_controller] {
               // We must get io_manager on IO thread.
               auto io_manager = runtime_controller->GetIOManager();
               // Check io_manager existence here.
               ASSERT_NE(io_manager.get(), nullptr);
               ASSERT_NE(io_manager->GetSkiaUnrefQueue().get(), nullptr);
               // Get io_manager directly from shell and check its existence.
               ASSERT_NE(spawn->GetIOManager().get(), nullptr);
             });
  });
  // Destroy the child shell.
  DestroyShell(std::move(spawn));
}

TEST_F(ShellTest, ImageGeneratorRegistryNotNullAfterParentShellDestroyed) {
  auto settings = CreateSettingsForFixture();
  auto shell = CreateShell(settings);
  ASSERT_TRUE(ValidateShell(shell.get()));

  std::unique_ptr<Shell> spawn;

  PostSync(shell->GetTaskRunners().GetPlatformTaskRunner(), [&shell, &settings,
                                                             &spawn] {
    auto second_configuration = RunConfiguration::InferFromSettings(settings);
    ASSERT_TRUE(second_configuration.IsValid());
    second_configuration.SetEntrypoint("emptyMain");
    const std::string initial_route("/foo");
    MockPlatformViewDelegate platform_view_delegate;
    auto child = shell->Spawn(
        std::move(second_configuration), initial_route,
        [&platform_view_delegate](Shell& shell) {
          auto result = std::make_unique<MockPlatformView>(
              platform_view_delegate, shell.GetTaskRunners());
          ON_CALL(*result, CreateRenderingSurface())
              .WillByDefault(::testing::Invoke(
                  [] { return std::make_unique<MockSurface>(); }));
          return result;
        },
        [](Shell& shell) { return std::make_unique<Rasterizer>(shell); });
    spawn = std::move(child);
  });

  PostSync(spawn->GetTaskRunners().GetUITaskRunner(), [&spawn] {
    std::shared_ptr<const DartIsolate> isolate =
        spawn->GetEngine()->GetRuntimeController()->GetRootIsolate().lock();
    ASSERT_TRUE(isolate);
    ASSERT_TRUE(isolate->GetImageGeneratorRegistry());
  });

  // Destroy the parent shell.
  DestroyShell(std::move(shell));

  PostSync(spawn->GetTaskRunners().GetUITaskRunner(), [&spawn] {
    std::shared_ptr<const DartIsolate> isolate =
        spawn->GetEngine()->GetRuntimeController()->GetRootIsolate().lock();
    ASSERT_TRUE(isolate);
    ASSERT_TRUE(isolate->GetImageGeneratorRegistry());
  });
  // Destroy the child shell.
  DestroyShell(std::move(spawn));
}

TEST_F(ShellTest, UpdateAssetResolverByTypeReplaces) {
  ASSERT_FALSE(DartVMRef::IsInstanceRunning());
  Settings settings = CreateSettingsForFixture();

  fml::MessageLoop::EnsureInitializedForCurrentThread();
  auto task_runner = fml::MessageLoop::GetCurrent().GetTaskRunner();
  TaskRunners task_runners("test", task_runner, task_runner, task_runner,
                           task_runner);
  auto shell = CreateShell(settings, task_runners);
  ASSERT_TRUE(DartVMRef::IsInstanceRunning());
  ASSERT_TRUE(ValidateShell(shell.get()));

  auto configuration = RunConfiguration::InferFromSettings(settings);
  configuration.SetEntrypoint("emptyMain");
  auto asset_manager = configuration.GetAssetManager();

  shell->RunEngine(std::move(configuration), [&](auto result) {
    ASSERT_EQ(result, Engine::RunStatus::Success);
  });

  auto platform_view =
      std::make_unique<PlatformView>(*shell.get(), task_runners);

  auto old_resolver = std::make_unique<TestAssetResolver>(
      true, AssetResolver::AssetResolverType::kApkAssetProvider);
  ASSERT_TRUE(old_resolver->IsValid());
  asset_manager->PushBack(std::move(old_resolver));

  auto updated_resolver = std::make_unique<TestAssetResolver>(
      false, AssetResolver::AssetResolverType::kApkAssetProvider);
  ASSERT_FALSE(updated_resolver->IsValidAfterAssetManagerChange());
  platform_view->UpdateAssetResolverByType(
      std::move(updated_resolver),
      AssetResolver::AssetResolverType::kApkAssetProvider);

  auto resolvers = asset_manager->TakeResolvers();
  ASSERT_EQ(resolvers.size(), 2ull);
  ASSERT_TRUE(resolvers[0]->IsValidAfterAssetManagerChange());

  ASSERT_FALSE(resolvers[1]->IsValidAfterAssetManagerChange());

  DestroyShell(std::move(shell), task_runners);
  ASSERT_FALSE(DartVMRef::IsInstanceRunning());
}

TEST_F(ShellTest, UpdateAssetResolverByTypeAppends) {
  ASSERT_FALSE(DartVMRef::IsInstanceRunning());
  Settings settings = CreateSettingsForFixture();

  fml::MessageLoop::EnsureInitializedForCurrentThread();
  auto task_runner = fml::MessageLoop::GetCurrent().GetTaskRunner();
  TaskRunners task_runners("test", task_runner, task_runner, task_runner,
                           task_runner);
  auto shell = CreateShell(settings, task_runners);
  ASSERT_TRUE(DartVMRef::IsInstanceRunning());
  ASSERT_TRUE(ValidateShell(shell.get()));

  auto configuration = RunConfiguration::InferFromSettings(settings);
  configuration.SetEntrypoint("emptyMain");
  auto asset_manager = configuration.GetAssetManager();

  shell->RunEngine(std::move(configuration), [&](auto result) {
    ASSERT_EQ(result, Engine::RunStatus::Success);
  });

  auto platform_view =
      std::make_unique<PlatformView>(*shell.get(), task_runners);

  auto updated_resolver = std::make_unique<TestAssetResolver>(
      false, AssetResolver::AssetResolverType::kApkAssetProvider);
  ASSERT_FALSE(updated_resolver->IsValidAfterAssetManagerChange());
  platform_view->UpdateAssetResolverByType(
      std::move(updated_resolver),
      AssetResolver::AssetResolverType::kApkAssetProvider);

  auto resolvers = asset_manager->TakeResolvers();
  ASSERT_EQ(resolvers.size(), 2ull);
  ASSERT_TRUE(resolvers[0]->IsValidAfterAssetManagerChange());

  ASSERT_FALSE(resolvers[1]->IsValidAfterAssetManagerChange());

  DestroyShell(std::move(shell), task_runners);
  ASSERT_FALSE(DartVMRef::IsInstanceRunning());
}

TEST_F(ShellTest, UpdateAssetResolverByTypeNull) {
  ASSERT_FALSE(DartVMRef::IsInstanceRunning());
  Settings settings = CreateSettingsForFixture();
  ThreadHost thread_host(ThreadHost::ThreadHostConfig(
      "io.flutter.test." + GetCurrentTestName() + ".",
      ThreadHost::Type::Platform));
  auto task_runner = thread_host.platform_thread->GetTaskRunner();
  TaskRunners task_runners("test", task_runner, task_runner, task_runner,
                           task_runner);
  auto shell = CreateShell(settings, task_runners);
  ASSERT_TRUE(DartVMRef::IsInstanceRunning());
  ASSERT_TRUE(ValidateShell(shell.get()));

  auto configuration = RunConfiguration::InferFromSettings(settings);
  configuration.SetEntrypoint("emptyMain");
  auto asset_manager = configuration.GetAssetManager();
  RunEngine(shell.get(), std::move(configuration));

  auto platform_view =
      std::make_unique<PlatformView>(*shell.get(), task_runners);

  auto old_resolver = std::make_unique<TestAssetResolver>(
      true, AssetResolver::AssetResolverType::kApkAssetProvider);
  ASSERT_TRUE(old_resolver->IsValid());
  asset_manager->PushBack(std::move(old_resolver));

  platform_view->UpdateAssetResolverByType(
      nullptr, AssetResolver::AssetResolverType::kApkAssetProvider);

  auto resolvers = asset_manager->TakeResolvers();
  ASSERT_EQ(resolvers.size(), 2ull);
  ASSERT_TRUE(resolvers[0]->IsValidAfterAssetManagerChange());
  ASSERT_TRUE(resolvers[1]->IsValidAfterAssetManagerChange());

  DestroyShell(std::move(shell), task_runners);
  ASSERT_FALSE(DartVMRef::IsInstanceRunning());
}

TEST_F(ShellTest, UpdateAssetResolverByTypeDoesNotReplaceMismatchType) {
  ASSERT_FALSE(DartVMRef::IsInstanceRunning());
  Settings settings = CreateSettingsForFixture();

  fml::MessageLoop::EnsureInitializedForCurrentThread();
  auto task_runner = fml::MessageLoop::GetCurrent().GetTaskRunner();
  TaskRunners task_runners("test", task_runner, task_runner, task_runner,
                           task_runner);
  auto shell = CreateShell(settings, task_runners);
  ASSERT_TRUE(DartVMRef::IsInstanceRunning());
  ASSERT_TRUE(ValidateShell(shell.get()));

  auto configuration = RunConfiguration::InferFromSettings(settings);
  configuration.SetEntrypoint("emptyMain");
  auto asset_manager = configuration.GetAssetManager();

  shell->RunEngine(std::move(configuration), [&](auto result) {
    ASSERT_EQ(result, Engine::RunStatus::Success);
  });

  auto platform_view =
      std::make_unique<PlatformView>(*shell.get(), task_runners);

  auto old_resolver = std::make_unique<TestAssetResolver>(
      true, AssetResolver::AssetResolverType::kAssetManager);
  ASSERT_TRUE(old_resolver->IsValid());
  asset_manager->PushBack(std::move(old_resolver));

  auto updated_resolver = std::make_unique<TestAssetResolver>(
      false, AssetResolver::AssetResolverType::kApkAssetProvider);
  ASSERT_FALSE(updated_resolver->IsValidAfterAssetManagerChange());
  platform_view->UpdateAssetResolverByType(
      std::move(updated_resolver),
      AssetResolver::AssetResolverType::kApkAssetProvider);

  auto resolvers = asset_manager->TakeResolvers();
  ASSERT_EQ(resolvers.size(), 3ull);
  ASSERT_TRUE(resolvers[0]->IsValidAfterAssetManagerChange());

  ASSERT_TRUE(resolvers[1]->IsValidAfterAssetManagerChange());

  ASSERT_FALSE(resolvers[2]->IsValidAfterAssetManagerChange());

  DestroyShell(std::move(shell), task_runners);
  ASSERT_FALSE(DartVMRef::IsInstanceRunning());
}

TEST_F(ShellTest, CanCreateShellsWithGLBackend) {
#if !SHELL_ENABLE_GL
  // GL emulation does not exist on Fuchsia.
  GTEST_SKIP();
#else
  auto settings = CreateSettingsForFixture();
  std::unique_ptr<Shell> shell = CreateShell({
      .settings = settings,
      .platform_view_create_callback = ShellTestPlatformViewBuilder({
          .rendering_backend = ShellTestPlatformView::BackendType::kGLBackend,
      }),
  });
  ASSERT_NE(shell, nullptr);
  ASSERT_TRUE(shell->IsSetup());
  auto configuration = RunConfiguration::InferFromSettings(settings);
  PlatformViewNotifyCreated(shell.get());
  configuration.SetEntrypoint("emptyMain");
  RunEngine(shell.get(), std::move(configuration));
  PumpOneFrame(shell.get());
  PlatformViewNotifyDestroyed(shell.get());
  DestroyShell(std::move(shell));
#endif  // !SHELL_ENABLE_GL
}

TEST_F(ShellTest, CanCreateShellsWithVulkanBackend) {
#if !SHELL_ENABLE_VULKAN
  GTEST_SKIP();
#else
  auto settings = CreateSettingsForFixture();
  std::unique_ptr<Shell> shell = CreateShell({
      .settings = settings,
      .platform_view_create_callback = ShellTestPlatformViewBuilder({
          .rendering_backend =
              ShellTestPlatformView::BackendType::kVulkanBackend,
      }),
  });
  ASSERT_NE(shell, nullptr);
  ASSERT_TRUE(shell->IsSetup());
  auto configuration = RunConfiguration::InferFromSettings(settings);
  PlatformViewNotifyCreated(shell.get());
  configuration.SetEntrypoint("emptyMain");
  RunEngine(shell.get(), std::move(configuration));
  PumpOneFrame(shell.get());
  PlatformViewNotifyDestroyed(shell.get());
  DestroyShell(std::move(shell));
#endif  // !SHELL_ENABLE_VULKAN
}

TEST_F(ShellTest, CanCreateShellsWithMetalBackend) {
#if !SHELL_ENABLE_METAL
  GTEST_SKIP();
#else
  auto settings = CreateSettingsForFixture();
  std::unique_ptr<Shell> shell = CreateShell({
      .settings = settings,
      .platform_view_create_callback = ShellTestPlatformViewBuilder({
          .rendering_backend =
              ShellTestPlatformView::BackendType::kMetalBackend,
      }),
  });
  ASSERT_NE(shell, nullptr);
  ASSERT_TRUE(shell->IsSetup());
  auto configuration = RunConfiguration::InferFromSettings(settings);
  PlatformViewNotifyCreated(shell.get());
  configuration.SetEntrypoint("emptyMain");
  RunEngine(shell.get(), std::move(configuration));
  PumpOneFrame(shell.get());
  PlatformViewNotifyDestroyed(shell.get());
  DestroyShell(std::move(shell));
#endif  // !SHELL_ENABLE_METAL
}

TEST_F(ShellTest, UserTagSetOnStartup) {
  ASSERT_FALSE(DartVMRef::IsInstanceRunning());
  // Make sure the shell launch does not kick off the creation of the VM
  // instance by already creating one upfront.
  auto vm_settings = CreateSettingsForFixture();
  auto vm_ref = DartVMRef::Create(vm_settings);
  ASSERT_TRUE(DartVMRef::IsInstanceRunning());

  auto settings = vm_settings;
  fml::AutoResetWaitableEvent isolate_create_latch;

  // ensure that "AppStartUpTag" is set during isolate creation.
  settings.root_isolate_create_callback = [&](const DartIsolate& isolate) {
    Dart_Handle current_tag = Dart_GetCurrentUserTag();
    Dart_Handle startup_tag = Dart_NewUserTag("AppStartUp");
    EXPECT_TRUE(Dart_IdentityEquals(current_tag, startup_tag));

    isolate_create_latch.Signal();
  };

  auto shell = CreateShell(settings);
  ASSERT_TRUE(ValidateShell(shell.get()));

  auto configuration = RunConfiguration::InferFromSettings(settings);
  ASSERT_TRUE(configuration.IsValid());

  RunEngine(shell.get(), std::move(configuration));
  ASSERT_TRUE(DartVMRef::IsInstanceRunning());

  DestroyShell(std::move(shell));
  isolate_create_latch.Wait();
}

TEST_F(ShellTest, PrefetchDefaultFontManager) {
  auto settings = CreateSettingsForFixture();
  settings.prefetched_default_font_manager = true;
  std::unique_ptr<Shell> shell;

  auto get_font_manager_count = [&] {
    fml::AutoResetWaitableEvent latch;
    size_t font_manager_count;
    fml::TaskRunner::RunNowOrPostTask(
        shell->GetTaskRunners().GetUITaskRunner(),
        [this, &shell, &latch, &font_manager_count]() {
          font_manager_count =
              GetFontCollection(shell.get())->GetFontManagersCount();
          latch.Signal();
        });
    latch.Wait();
    return font_manager_count;
  };
  size_t initial_font_manager_count = 0;
  settings.root_isolate_create_callback = [&](const auto& isolate) {
    ASSERT_GT(initial_font_manager_count, 0ul);
    // Should not have fetched the default font manager yet, since the root
    // isolate was only just created.
    ASSERT_EQ(get_font_manager_count(), initial_font_manager_count);
  };

  shell = CreateShell(settings);

  initial_font_manager_count = get_font_manager_count();

  auto configuration = RunConfiguration::InferFromSettings(settings);
  configuration.SetEntrypoint("emptyMain");
  RunEngine(shell.get(), std::move(configuration));

  // If the prefetched_default_font_manager flag is set, then the default font
  // manager will not be added until the engine starts running.
  ASSERT_EQ(get_font_manager_count(), initial_font_manager_count + 1);

  DestroyShell(std::move(shell));
}

TEST_F(ShellTest, OnPlatformViewCreatedWhenUIThreadIsBusy) {
  // This test will deadlock if the threading logic in
  // Shell::OnCreatePlatformView is wrong.
  auto settings = CreateSettingsForFixture();
  auto shell = CreateShell(settings);

  fml::AutoResetWaitableEvent latch;
  fml::TaskRunner::RunNowOrPostTask(shell->GetTaskRunners().GetUITaskRunner(),
                                    [&latch]() { latch.Wait(); });

  ShellTest::PlatformViewNotifyCreated(shell.get());
  latch.Signal();

  DestroyShell(std::move(shell));
}

TEST_F(ShellTest, UIWorkAfterOnPlatformViewDestroyed) {
  auto settings = CreateSettingsForFixture();
  auto shell = CreateShell(settings);
  auto configuration = RunConfiguration::InferFromSettings(settings);
  configuration.SetEntrypoint("drawFrames");

  fml::AutoResetWaitableEvent latch;
  fml::AutoResetWaitableEvent notify_native_latch;
  AddNativeCallback("NotifyNative", CREATE_NATIVE_ENTRY([&](auto args) {
                      notify_native_latch.Signal();
                      latch.Wait();
                    }));

  RunEngine(shell.get(), std::move(configuration));
  // Wait to make sure we get called back from Dart and thus have latched
  // the UI thread before we create/destroy the platform view.
  notify_native_latch.Wait();

  ShellTest::PlatformViewNotifyCreated(shell.get());

  fml::AutoResetWaitableEvent destroy_latch;
  fml::TaskRunner::RunNowOrPostTask(
      shell->GetTaskRunners().GetPlatformTaskRunner(),
      [&shell, &destroy_latch]() {
        shell->GetPlatformView()->NotifyDestroyed();
        destroy_latch.Signal();
      });

  destroy_latch.Wait();

  // Unlatch the UI thread and let it send us a scene to render.
  latch.Signal();

  // Flush the UI task runner to make sure we process the render/scheduleFrame
  // request.
  fml::AutoResetWaitableEvent ui_flush_latch;
  fml::TaskRunner::RunNowOrPostTask(
      shell->GetTaskRunners().GetUITaskRunner(),
      [&ui_flush_latch]() { ui_flush_latch.Signal(); });
  ui_flush_latch.Wait();
  DestroyShell(std::move(shell));
}

TEST_F(ShellTest, UsesPlatformMessageHandler) {
  TaskRunners task_runners = GetTaskRunnersForFixture();
  auto settings = CreateSettingsForFixture();
  MockPlatformViewDelegate platform_view_delegate;
  auto platform_message_handler =
      std::make_shared<MockPlatformMessageHandler>();
  int message_id = 1;
  EXPECT_CALL(*platform_message_handler, HandlePlatformMessage(_));
  EXPECT_CALL(*platform_message_handler,
              InvokePlatformMessageEmptyResponseCallback(message_id));
  Shell::CreateCallback<PlatformView> platform_view_create_callback =
      [&platform_view_delegate, task_runners,
       platform_message_handler](flutter::Shell& shell) {
        auto result = std::make_unique<MockPlatformView>(platform_view_delegate,
                                                         task_runners);
        EXPECT_CALL(*result, GetPlatformMessageHandler())
            .WillOnce(Return(platform_message_handler));
        return result;
      };
  auto shell = CreateShell({
      .settings = settings,
      .task_runners = task_runners,
      .platform_view_create_callback = platform_view_create_callback,
  });

  EXPECT_EQ(platform_message_handler, shell->GetPlatformMessageHandler());
  PostSync(task_runners.GetUITaskRunner(), [&shell]() {
    size_t data_size = 4;
    fml::MallocMapping bytes =
        fml::MallocMapping(static_cast<uint8_t*>(malloc(data_size)), data_size);
    fml::RefPtr<MockPlatformMessageResponse> response =
        MockPlatformMessageResponse::Create();
    auto message = std::make_unique<PlatformMessage>(
        /*channel=*/"foo", /*data=*/std::move(bytes), /*response=*/response);
    (static_cast<Engine::Delegate*>(shell.get()))
        ->OnEngineHandlePlatformMessage(std::move(message));
  });
  shell->GetPlatformMessageHandler()
      ->InvokePlatformMessageEmptyResponseCallback(message_id);
  DestroyShell(std::move(shell));
}

TEST_F(ShellTest, SpawnWorksWithOnError) {
  auto settings = CreateSettingsForFixture();
  auto shell = CreateShell(settings);
  ASSERT_TRUE(ValidateShell(shell.get()));

  auto configuration = RunConfiguration::InferFromSettings(settings);
  ASSERT_TRUE(configuration.IsValid());
  configuration.SetEntrypoint("onErrorA");

  auto second_configuration = RunConfiguration::InferFromSettings(settings);
  ASSERT_TRUE(second_configuration.IsValid());
  second_configuration.SetEntrypoint("onErrorB");

  fml::CountDownLatch latch(2);

  AddNativeCallback(
      "NotifyErrorA", CREATE_NATIVE_ENTRY([&](Dart_NativeArguments args) {
        auto string_handle = Dart_GetNativeArgument(args, 0);
        const char* c_str;
        Dart_StringToCString(string_handle, &c_str);
        EXPECT_STREQ(c_str, "Exception: I should be coming from A");
        latch.CountDown();
      }));

  AddNativeCallback(
      "NotifyErrorB", CREATE_NATIVE_ENTRY([&](Dart_NativeArguments args) {
        auto string_handle = Dart_GetNativeArgument(args, 0);
        const char* c_str;
        Dart_StringToCString(string_handle, &c_str);
        EXPECT_STREQ(c_str, "Exception: I should be coming from B");
        latch.CountDown();
      }));

  RunEngine(shell.get(), std::move(configuration));

  ASSERT_TRUE(DartVMRef::IsInstanceRunning());

  PostSync(
      shell->GetTaskRunners().GetPlatformTaskRunner(),
      [this, &spawner = shell, &second_configuration, &latch]() {
        ::testing::NiceMock<MockPlatformViewDelegate> platform_view_delegate;
        auto spawn = spawner->Spawn(
            std::move(second_configuration), "",
            [&platform_view_delegate](Shell& shell) {
              auto result =
                  std::make_unique<::testing::NiceMock<MockPlatformView>>(
                      platform_view_delegate, shell.GetTaskRunners());
              ON_CALL(*result, CreateRenderingSurface())
                  .WillByDefault(::testing::Invoke([] {
                    return std::make_unique<::testing::NiceMock<MockSurface>>();
                  }));
              return result;
            },
            [](Shell& shell) { return std::make_unique<Rasterizer>(shell); });
        ASSERT_NE(nullptr, spawn.get());
        ASSERT_TRUE(ValidateShell(spawn.get()));

        // Before destroying the shell, wait for expectations of the spawned
        // isolate to be met.
        latch.Wait();

        DestroyShell(std::move(spawn));
      });

  DestroyShell(std::move(shell));
  ASSERT_FALSE(DartVMRef::IsInstanceRunning());
}

TEST_F(ShellTest, ImmutableBufferLoadsAssetOnBackgroundThread) {
  Settings settings = CreateSettingsForFixture();
  auto task_runner = CreateNewThread();
  TaskRunners task_runners("test", task_runner, task_runner, task_runner,
                           task_runner);
  std::unique_ptr<Shell> shell = CreateShell(settings, task_runners);

  fml::CountDownLatch latch(1);
  AddNativeCallback("NotifyNative",
                    CREATE_NATIVE_ENTRY([&](auto args) { latch.CountDown(); }));

  // Create the surface needed by rasterizer
  PlatformViewNotifyCreated(shell.get());

  auto configuration = RunConfiguration::InferFromSettings(settings);
  configuration.SetEntrypoint("testThatAssetLoadingHappensOnWorkerThread");
  auto asset_manager = configuration.GetAssetManager();
  auto test_resolver = std::make_unique<ThreadCheckingAssetResolver>(
      shell->GetDartVM()->GetConcurrentMessageLoop());
  auto leaked_resolver = test_resolver.get();
  asset_manager->PushBack(std::move(test_resolver));

  RunEngine(shell.get(), std::move(configuration));
  PumpOneFrame(shell.get());

  latch.Wait();

  EXPECT_EQ(leaked_resolver->mapping_requests[0], "DoesNotExist");

  PlatformViewNotifyDestroyed(shell.get());
  DestroyShell(std::move(shell), task_runners);
}

TEST_F(ShellTest, PictureToImageSync) {
#if !SHELL_ENABLE_GL
  // This test uses the GL backend.
  GTEST_SKIP();
#else
  auto settings = CreateSettingsForFixture();
  std::unique_ptr<Shell> shell = CreateShell({
      .settings = settings,
      .platform_view_create_callback = ShellTestPlatformViewBuilder({
          .rendering_backend = ShellTestPlatformView::BackendType::kGLBackend,
      }),
  });

  AddNativeCallback("NativeOnBeforeToImageSync",
                    CREATE_NATIVE_ENTRY([&](auto args) {
                      // nop
                    }));

  fml::CountDownLatch latch(2);
  AddNativeCallback("NotifyNative", CREATE_NATIVE_ENTRY([&](auto args) {
                      // Teardown and set up rasterizer again.
                      PlatformViewNotifyDestroyed(shell.get());
                      PlatformViewNotifyCreated(shell.get());
                      latch.CountDown();
                    }));

  ASSERT_NE(shell, nullptr);
  ASSERT_TRUE(shell->IsSetup());
  auto configuration = RunConfiguration::InferFromSettings(settings);
  PlatformViewNotifyCreated(shell.get());
  configuration.SetEntrypoint("toImageSync");
  RunEngine(shell.get(), std::move(configuration));
  PumpOneFrame(shell.get());

  latch.Wait();

  PlatformViewNotifyDestroyed(shell.get());
  DestroyShell(std::move(shell));
#endif  // !SHELL_ENABLE_GL
}

TEST_F(ShellTest, PictureToImageSyncImpellerNoSurface) {
#if !SHELL_ENABLE_METAL
  // This test uses the Metal backend.
  GTEST_SKIP();
#else
  auto settings = CreateSettingsForFixture();
  settings.enable_impeller = true;
  std::unique_ptr<Shell> shell = CreateShell({
      .settings = settings,
      .platform_view_create_callback = ShellTestPlatformViewBuilder({
          .rendering_backend =
              ShellTestPlatformView::BackendType::kMetalBackend,
      }),
  });

  AddNativeCallback("NativeOnBeforeToImageSync",
                    CREATE_NATIVE_ENTRY([&](auto args) {
                      // nop
                    }));

  fml::CountDownLatch latch(2);
  AddNativeCallback("NotifyNative", CREATE_NATIVE_ENTRY([&](auto args) {
                      // Teardown and set up rasterizer again.
                      PlatformViewNotifyDestroyed(shell.get());
                      PlatformViewNotifyCreated(shell.get());
                      latch.CountDown();
                    }));

  ASSERT_NE(shell, nullptr);
  ASSERT_TRUE(shell->IsSetup());
  auto configuration = RunConfiguration::InferFromSettings(settings);

  // Important: Do not create the platform view yet!
  // This test is making sure that the rasterizer can create the texture
  // as expected without a surface.

  configuration.SetEntrypoint("toImageSync");
  RunEngine(shell.get(), std::move(configuration));
  PumpOneFrame(shell.get());

  latch.Wait();

  PlatformViewNotifyDestroyed(shell.get());
  DestroyShell(std::move(shell));
#endif  // !SHELL_ENABLE_METAL
}

#if SHELL_ENABLE_GL
// This test uses the GL backend and refers to symbols in egl.h
TEST_F(ShellTest, PictureToImageSyncWithTrampledContext) {
  // make it easier to trample the GL context by running on a single task
  // runner.
  ThreadHost thread_host("io.flutter.test." + GetCurrentTestName() + ".",
                         ThreadHost::Type::Platform);
  auto task_runner = thread_host.platform_thread->GetTaskRunner();
  TaskRunners task_runners("test", task_runner, task_runner, task_runner,
                           task_runner);

  auto settings = CreateSettingsForFixture();
  std::unique_ptr<Shell> shell = CreateShell({
      .settings = settings,
      .task_runners = task_runners,
      .platform_view_create_callback = ShellTestPlatformViewBuilder({
          .rendering_backend = ShellTestPlatformView::BackendType::kGLBackend,
      }),
  });

  AddNativeCallback(
      "NativeOnBeforeToImageSync", CREATE_NATIVE_ENTRY([&](auto args) {
        // Trample the GL context. If the rasterizer fails
        // to make the right one current again, test will
        // fail.
        ::eglMakeCurrent(::eglGetCurrentDisplay(), NULL, NULL, NULL);
      }));

  fml::CountDownLatch latch(2);
  AddNativeCallback("NotifyNative", CREATE_NATIVE_ENTRY([&](auto args) {
                      // Teardown and set up rasterizer again.
                      PlatformViewNotifyDestroyed(shell.get());
                      PlatformViewNotifyCreated(shell.get());
                      latch.CountDown();
                    }));

  ASSERT_NE(shell, nullptr);
  ASSERT_TRUE(shell->IsSetup());
  auto configuration = RunConfiguration::InferFromSettings(settings);
  PlatformViewNotifyCreated(shell.get());
  configuration.SetEntrypoint("toImageSync");
  RunEngine(shell.get(), std::move(configuration));
  PumpOneFrame(shell.get());

  latch.Wait();

  PlatformViewNotifyDestroyed(shell.get());
  DestroyShell(std::move(shell), task_runners);
}
#endif  // SHELL_ENABLE_GL

TEST_F(ShellTest, PluginUtilitiesCallbackHandleErrorHandling) {
  auto settings = CreateSettingsForFixture();
  std::unique_ptr<Shell> shell = CreateShell(settings);

  fml::AutoResetWaitableEvent latch;
  bool test_passed;
  AddNativeCallback("NotifyNativeBool", CREATE_NATIVE_ENTRY([&](auto args) {
                      Dart_Handle exception = nullptr;
                      test_passed = tonic::DartConverter<bool>::FromArguments(
                          args, 0, exception);
                      latch.Signal();
                    }));

  ASSERT_NE(shell, nullptr);
  ASSERT_TRUE(shell->IsSetup());
  auto configuration = RunConfiguration::InferFromSettings(settings);
  PlatformViewNotifyCreated(shell.get());
  configuration.SetEntrypoint("testPluginUtilitiesCallbackHandle");
  RunEngine(shell.get(), std::move(configuration));
  PumpOneFrame(shell.get());

  latch.Wait();

  ASSERT_TRUE(test_passed);

  PlatformViewNotifyDestroyed(shell.get());
  DestroyShell(std::move(shell));
}

TEST_F(ShellTest, NotifyIdleRejectsPastAndNearFuture) {
  ASSERT_FALSE(DartVMRef::IsInstanceRunning());
  Settings settings = CreateSettingsForFixture();
  ThreadHost thread_host("io.flutter.test." + GetCurrentTestName() + ".",
                         ThreadHost::Type::Platform | ThreadHost::UI |
                             ThreadHost::IO | ThreadHost::RASTER);
  auto platform_task_runner = thread_host.platform_thread->GetTaskRunner();
  TaskRunners task_runners("test", thread_host.platform_thread->GetTaskRunner(),
                           thread_host.raster_thread->GetTaskRunner(),
                           thread_host.ui_thread->GetTaskRunner(),
                           thread_host.io_thread->GetTaskRunner());
  auto shell = CreateShell(settings, task_runners);
  ASSERT_TRUE(DartVMRef::IsInstanceRunning());
  ASSERT_TRUE(ValidateShell(shell.get()));

  fml::AutoResetWaitableEvent latch;

  auto configuration = RunConfiguration::InferFromSettings(settings);
  configuration.SetEntrypoint("emptyMain");
  RunEngine(shell.get(), std::move(configuration));

  fml::TaskRunner::RunNowOrPostTask(
      task_runners.GetUITaskRunner(), [&latch, &shell]() {
        auto runtime_controller = const_cast<RuntimeController*>(
            shell->GetEngine()->GetRuntimeController());

        auto now = fml::TimeDelta::FromMicroseconds(Dart_TimelineGetMicros());

        EXPECT_FALSE(runtime_controller->NotifyIdle(
            now - fml::TimeDelta::FromMilliseconds(10)));
        EXPECT_FALSE(runtime_controller->NotifyIdle(now));
        EXPECT_FALSE(runtime_controller->NotifyIdle(
            now + fml::TimeDelta::FromNanoseconds(100)));

        EXPECT_TRUE(runtime_controller->NotifyIdle(
            now + fml::TimeDelta::FromMilliseconds(100)));
        latch.Signal();
      });

  latch.Wait();

  DestroyShell(std::move(shell), task_runners);
  ASSERT_FALSE(DartVMRef::IsInstanceRunning());
}

TEST_F(ShellTest, NotifyIdleNotCalledInLatencyMode) {
  ASSERT_FALSE(DartVMRef::IsInstanceRunning());
  Settings settings = CreateSettingsForFixture();
  ThreadHost thread_host("io.flutter.test." + GetCurrentTestName() + ".",
                         ThreadHost::Type::Platform | ThreadHost::UI |
                             ThreadHost::IO | ThreadHost::RASTER);
  auto platform_task_runner = thread_host.platform_thread->GetTaskRunner();
  TaskRunners task_runners("test", thread_host.platform_thread->GetTaskRunner(),
                           thread_host.raster_thread->GetTaskRunner(),
                           thread_host.ui_thread->GetTaskRunner(),
                           thread_host.io_thread->GetTaskRunner());
  auto shell = CreateShell(settings, task_runners);
  ASSERT_TRUE(DartVMRef::IsInstanceRunning());
  ASSERT_TRUE(ValidateShell(shell.get()));

  // we start off in balanced mode, where we expect idle notifications to
  // succeed. After the first `NotifyNativeBool` we expect to be in latency
  // mode, where we expect idle notifications to fail.
  fml::CountDownLatch latch(2);
  AddNativeCallback(
      "NotifyNativeBool", CREATE_NATIVE_ENTRY([&](auto args) {
        Dart_Handle exception = nullptr;
        bool is_in_latency_mode =
            tonic::DartConverter<bool>::FromArguments(args, 0, exception);
        auto runtime_controller = const_cast<RuntimeController*>(
            shell->GetEngine()->GetRuntimeController());
        bool success =
            runtime_controller->NotifyIdle(fml::TimeDelta::FromMicroseconds(
                Dart_TimelineGetMicros() + 100000));
        EXPECT_EQ(success, !is_in_latency_mode);
        latch.CountDown();
      }));

  auto configuration = RunConfiguration::InferFromSettings(settings);
  configuration.SetEntrypoint("performanceModeImpactsNotifyIdle");
  RunEngine(shell.get(), std::move(configuration));

  latch.Wait();

  DestroyShell(std::move(shell), task_runners);
  ASSERT_FALSE(DartVMRef::IsInstanceRunning());
}

TEST_F(ShellTest, NotifyDestroyed) {
  ASSERT_FALSE(DartVMRef::IsInstanceRunning());
  Settings settings = CreateSettingsForFixture();
  ThreadHost thread_host("io.flutter.test." + GetCurrentTestName() + ".",
                         ThreadHost::Type::Platform | ThreadHost::UI |
                             ThreadHost::IO | ThreadHost::RASTER);
  auto platform_task_runner = thread_host.platform_thread->GetTaskRunner();
  TaskRunners task_runners("test", thread_host.platform_thread->GetTaskRunner(),
                           thread_host.raster_thread->GetTaskRunner(),
                           thread_host.ui_thread->GetTaskRunner(),
                           thread_host.io_thread->GetTaskRunner());
  auto shell = CreateShell(settings, task_runners);
  ASSERT_TRUE(DartVMRef::IsInstanceRunning());
  ASSERT_TRUE(ValidateShell(shell.get()));

  fml::CountDownLatch latch(1);
  AddNativeCallback("NotifyDestroyed", CREATE_NATIVE_ENTRY([&](auto args) {
                      auto runtime_controller = const_cast<RuntimeController*>(
                          shell->GetEngine()->GetRuntimeController());
                      bool success = runtime_controller->NotifyDestroyed();
                      EXPECT_TRUE(success);
                      latch.CountDown();
                    }));

  auto configuration = RunConfiguration::InferFromSettings(settings);
  configuration.SetEntrypoint("callNotifyDestroyed");
  RunEngine(shell.get(), std::move(configuration));

  latch.Wait();

  DestroyShell(std::move(shell), task_runners);
  ASSERT_FALSE(DartVMRef::IsInstanceRunning());
}

TEST_F(ShellTest, PrintsErrorWhenPlatformMessageSentFromWrongThread) {
#if FLUTTER_RUNTIME_MODE != FLUTTER_RUNTIME_MODE_DEBUG || OS_FUCHSIA
  GTEST_SKIP() << "Test is for debug mode only on non-fuchsia targets.";
#else
  Settings settings = CreateSettingsForFixture();
  ThreadHost thread_host("io.flutter.test." + GetCurrentTestName() + ".",
                         ThreadHost::Type::Platform);
  auto task_runner = thread_host.platform_thread->GetTaskRunner();
  TaskRunners task_runners("test", task_runner, task_runner, task_runner,
                           task_runner);
  auto shell = CreateShell(settings, task_runners);

  {
    fml::testing::LogCapture log_capture;

    // The next call will result in a thread checker violation.
    fml::ThreadChecker::DisableNextThreadCheckFailure();
    SendPlatformMessage(shell.get(), std::make_unique<PlatformMessage>(
                                         "com.test.plugin", nullptr));

    EXPECT_THAT(
        log_capture.str(),
        ::testing::EndsWith(
            "The 'com.test.plugin' channel sent a message from native to "
            "Flutter on a non-platform thread. Platform channel messages "
            "must be sent on the platform thread. Failure to do so may "
            "result in data loss or crashes, and must be fixed in the "
            "plugin or application code creating that channel.\nSee "
            "https://docs.flutter.dev/platform-integration/"
            "platform-channels#channels-and-platform-threading for more "
            "information.\n"));
  }

  {
    fml::testing::LogCapture log_capture;

    // The next call will result in a thread checker violation.
    fml::ThreadChecker::DisableNextThreadCheckFailure();
    SendPlatformMessage(shell.get(), std::make_unique<PlatformMessage>(
                                         "com.test.plugin", nullptr));

    EXPECT_EQ(log_capture.str(), "");
  }

  DestroyShell(std::move(shell), task_runners);
  ASSERT_FALSE(DartVMRef::IsInstanceRunning());
#endif
}

TEST_F(ShellTest, DiesIfSoftwareRenderingAndImpellerAreEnabledDeathTest) {
#if defined(OS_FUCHSIA)
  GTEST_SKIP() << "Fuchsia";
#else
  ::testing::FLAGS_gtest_death_test_style = "threadsafe";
  Settings settings = CreateSettingsForFixture();
  settings.enable_impeller = true;
  settings.enable_software_rendering = true;
  ThreadHost thread_host("io.flutter.test." + GetCurrentTestName() + ".",
                         ThreadHost::Type::Platform);
  auto task_runner = thread_host.platform_thread->GetTaskRunner();
  TaskRunners task_runners("test", task_runner, task_runner, task_runner,
                           task_runner);
  EXPECT_DEATH_IF_SUPPORTED(
      CreateShell(settings, task_runners),
      "Software rendering is incompatible with Impeller.");
#endif  // OS_FUCHSIA
}

}  // namespace testing
}  // namespace flutter

// NOLINTEND(clang-analyzer-core.StackAddressEscape)
