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

#include "content/browser/preloading/prerender/prerender_host.h"

#include <memory>
#include <optional>

#include "base/check_is_test.h"
#include "base/feature_list.h"
#include "base/metrics/histogram_functions.h"
#include "base/notreached.h"
#include "base/observer_list.h"
#include "base/run_loop.h"
#include "base/strings/string_util.h"
#include "base/task/thread_pool.h"
#include "base/time/time.h"
#include "base/trace_event/named_trigger.h"
#include "base/trace_event/typed_macros.h"
#include "content/browser/client_hints/client_hints.h"
#include "content/browser/devtools/devtools_instrumentation.h"
#include "content/browser/preloading/prefetch/no_vary_search_helper.h"
#include "content/browser/preloading/preloading_attempt_impl.h"
#include "content/browser/preloading/preloading_trigger_type_impl.h"
#include "content/browser/preloading/prerender/devtools_prerender_attempt.h"
#include "content/browser/preloading/prerender/prerender_features.h"
#include "content/browser/preloading/prerender/prerender_final_status.h"
#include "content/browser/preloading/prerender/prerender_host_registry.h"
#include "content/browser/preloading/prerender/prerender_metrics.h"
#include "content/browser/preloading/prerender/prerender_navigation_utils.h"
#include "content/browser/preloading/speculation_rules/speculation_rules_util.h"
#include "content/browser/renderer_host/frame_tree.h"
#include "content/browser/renderer_host/frame_tree_node.h"
#include "content/browser/renderer_host/navigation_controller_impl.h"
#include "content/browser/renderer_host/navigation_entry_restore_context_impl.h"
#include "content/browser/renderer_host/navigation_request.h"
#include "content/browser/renderer_host/page_impl.h"
#include "content/browser/renderer_host/render_frame_host_impl.h"
#include "content/browser/web_contents/web_contents_impl.h"
#include "content/public/browser/browser_context.h"
#include "content/public/browser/navigation_controller.h"
#include "content/public/browser/preloading_trigger_type.h"
#include "content/public/browser/web_contents_delegate.h"
#if BUILDFLAG(ARKWEB_NETWORK_LOAD)
#include "content/public/common/content_switches.h"
#endif
#include "content/public/common/referrer.h"
#include "net/base/load_flags.h"
#include "net/http/http_request_headers.h"
#include "services/network/public/mojom/supports_loading_mode.mojom.h"
#include "third_party/blink/public/common/client_hints/enabled_client_hints.h"
#include "third_party/blink/public/common/navigation/preloading_headers.h"
#include "url/origin.h"

#if BUILDFLAG(IS_ANDROID)
#include "base/strings/stringprintf.h"
#endif

namespace content {

namespace {

// When enabled, the SiteInstance used to initialize a prerender frame tree is
// associated with a SiteInfo derived from the prerendering URL, rather than an
// empty SiteInfo. This ensures that RenderProcessHost selection for a prerender
// navigation follows the same rules as a regular navigation to the same URL.
//
// Extra details:
//
// A PrerenderHost creates a 1st SiteInstance to init the frame tree. This is
// the SiteInstance for which this features changes the SiteInfo. This
// SiteInstance gets its RenderProcessHost in this stack:
//
//     content::SiteInstanceImpl::GetOrCreateProcess
//     content::RenderFrameHostManager::CreateRenderFrameHost
//     content::RenderFrameHostManager::InitRoot
//     content::FrameTree::Init
//     content::PrerenderHost::PrerenderHost
//
// Then, when the prerender navigation occurs, the SiteInstance for that
// navigation attempts to reuse the same process in this stack:
//
//     content::SiteInstanceImpl::ReuseExistingProcessIfPossible
//     content::RenderFrameHostManager::GetSiteInstanceForNavigation
//     content::RenderFrameHostManager::GetSiteInstanceForNavigationRequest
//     content::RenderFrameHostManager::GetFrameHostForNavigation
//     content::NavigationRequest::SelectFrameHostForOnResponseStarted
//     content::NavigationRequest::OnResponseStarted
//     content::NavigationRequest::OnResponseStarted
//     content::NavigationURLLoaderImpl::NotifyResponseStarted
//
// The fact that the 2nd SiteInstance attempts to reuse the same
// RenderProcessHost as the 1st SiteInstance is what makes it important to
// carefully choose the RenderProcessHost for the 1st SiteInstance.
BASE_FEATURE(kCreatePrerenderSiteInstanceWithURL,
             base::FEATURE_DISABLED_BY_DEFAULT);

base::OnceCallback<void(FrameTreeNodeId)>& GetHostCreationCallback() {
  static base::NoDestructor<base::OnceCallback<void(FrameTreeNodeId)>>
      host_creation_callback;
  return *host_creation_callback;
}

void CheckPrerenderAttributes(const PrerenderAttributes& attributes) {
  // If the prerendering is browser-initiated, it is expected to have no
  // initiator. All initiator related information should be null or invalid. On
  // the other hand, renderer-initiated prerendering should have valid initiator
  // information.
  if (attributes.IsBrowserInitiated()) {
    CHECK(!attributes.initiator_origin.has_value());
    CHECK(!attributes.initiator_frame_token.has_value());
    CHECK_EQ(attributes.initiator_process_id,
             ChildProcessHost::kInvalidUniqueID);
    CHECK_EQ(attributes.initiator_ukm_id, ukm::kInvalidSourceId);
    CHECK(attributes.initiator_frame_tree_node_id.is_null());
  } else {
    CHECK(attributes.initiator_origin.has_value());
    CHECK(attributes.initiator_frame_token.has_value());
    CHECK_NE(attributes.initiator_process_id,
             ChildProcessHost::kInvalidUniqueID);
    CHECK_NE(attributes.initiator_ukm_id, ukm::kInvalidSourceId);
    CHECK(attributes.initiator_frame_tree_node_id);
  }
}

#if BUILDFLAG(IS_ANDROID)
// This is similar to `HttpRequestHeaders::ToString()` but the headers are
// separated by "\n", not "\r\n", as
// `NavigationController::LoadURLParams::extra_headers` requires the format.
std::string SerializeHttpRequestHeaders(
    const net::HttpRequestHeaders& headers) {
  CHECK(!headers.IsEmpty());
  std::string output;
  for (const auto& header : headers.GetHeaderVector()) {
    base::StringAppendF(&output, "%s: %s\n", header.key.c_str(),
                        header.value.c_str());
  }
  // Add the trailing `\n`.
  output.append("\n");
  return output;
}
#endif  // BUILDFLAG(IS_ANDROID)

PrerenderHostId NextPrerenderHostId() {
  static PrerenderHostId::Generator generator;
  return generator.GenerateNextId();
}

}  // namespace

PrerenderHost::PrerenderFrameTreeDelegate::PrerenderFrameTreeDelegate(
    BrowserContext* browser_context,
    WebContentsImpl& web_contents,
    PrerenderHost& prerender_host)
    : prerender_host_(prerender_host),
      frame_tree_(
          std::make_unique<FrameTree>(browser_context,
                                      /*delegate=*/this,
                                      /*navigation_controller_delegate=*/this,
                                      /*navigator_delegate=*/&web_contents,
                                      /*render_frame_delegate=*/&web_contents,
                                      /*render_view_delegate=*/&web_contents,
                                      /*render_widget_delegate=*/&web_contents,
                                      /*manager_delegate=*/&web_contents,
                                      /*page_delegate=*/&web_contents,
                                      FrameTree::Type::kPrerender)) {}

void PrerenderHost::PrerenderFrameTreeDelegate::DidStopLoading() {
  if (on_wait_loading_finished_) {
    std::move(on_wait_loading_finished_).Run(LoadingOutcome::kLoadingCompleted);
  }
}

bool PrerenderHost::PrerenderFrameTreeDelegate::IsHidden() {
  return true;
}

FrameTree* PrerenderHost::PrerenderFrameTreeDelegate::LoadingTree() {
  // For prerendering loading tree is the same as its frame tree as loading is
  // done at a frame tree level in the background, unlike the loading visible
  // to the user where we account for nested frame tree loading state.
  return frame_tree_.get();
}

FrameTreeNodeId
PrerenderHost::PrerenderFrameTreeDelegate::GetOuterDelegateFrameTreeNodeId() {
  // A prerendered FrameTree is not "inner to" or "nested inside" another
  // FrameTree; it exists in parallel to the primary FrameTree of the current
  // WebContents. Therefore, it must not attempt to access the primary
  // FrameTree in the sense of an "outer delegate" relationship, so we return
  // the invalid ID here.
  return FrameTreeNodeId();
}

RenderFrameHostImpl*
PrerenderHost::PrerenderFrameTreeDelegate::GetProspectiveOuterDocument() {
  // A prerendered FrameTree never has an outer document.
  return nullptr;
}

void PrerenderHost::PrerenderFrameTreeDelegate::SetFocusedFrame(
    FrameTreeNode* node,
    SiteInstanceGroup* source) {
  // `node` can only become focused when `node`'s current RenderFrameHost is
  // active.
  NOTREACHED();
}

FrameTree* PrerenderHost::PrerenderFrameTreeDelegate::
    GetOwnedDocumentPictureInPictureFrameTree() {
  return nullptr;
}

FrameTree* PrerenderHost::PrerenderFrameTreeDelegate::
    GetDocumentPictureInPictureOpenerFrameTree() {
  return nullptr;
}

bool PrerenderHost::PrerenderFrameTreeDelegate::
    OnRenderFrameProxyVisibilityChanged(
        RenderFrameProxyHost* render_frame_proxy_host,
        blink::mojom::FrameVisibility visibility) {
  return false;
}

void PrerenderHost::PrerenderFrameTreeDelegate::
    ActivateAndShowRepostFormWarningDialog() {
  // Not supported, cancel pending reload.
  frame_tree_->controller().CancelPendingReload();
}

bool PrerenderHost::PrerenderFrameTreeDelegate::ShouldPreserveAbortedURLs() {
  return false;
}

#if BUILDFLAG(IS_ANDROID)

scoped_refptr<viz::RasterContextProvider>
PrerenderHost::PrerenderFrameTreeDelegate::GetRasterContextProvider() {
  NOTREACHED();
}

gfx::ColorSpace PrerenderHost::PrerenderFrameTreeDelegate::GetOutputColorSpace(
    gfx::ContentColorUsage color_usage,
    bool needs_alpha) {
  NOTREACHED();
}

#endif  // BUILDFLAG(IS_ANDROID)

PrerenderHost::LoadingOutcome
PrerenderHost::PrerenderFrameTreeDelegate::WaitForLoadStopForTesting() {
  LoadingOutcome status = LoadingOutcome::kLoadingCompleted;

  if (!frame_tree_->IsLoadingIncludingInnerFrameTrees() &&
      prerender_host_->GetInitialNavigationId().has_value()) {
    return status;
  }

  base::RunLoop loop;
  on_wait_loading_finished_ = base::BindOnce(
      [](base::OnceClosure on_close, LoadingOutcome* result,
         LoadingOutcome status) {
        *result = status;
        std::move(on_close).Run();
      },
      loop.QuitClosure(), &status);
  loop.Run();
  // Reset callback to null in case if loop is quit by timeout.
  //
  // This `if` body causes SEGV for `kPrerenderingCancelled` case because the
  // callback is called in dtor and `this` is already destructed here.
  //
  // TODO(crbug.com/372691377): Split setup and wait parts and make the wait
  // part `static`.
  if (status != PrerenderHost::LoadingOutcome::kPrerenderingCancelled) {
    on_wait_loading_finished_.Reset();
  }
  return status;
}

PrerenderHost::PrerenderFrameTreeDelegate::~PrerenderFrameTreeDelegate() {
  if (frame_tree_) {
    frame_tree_->Shutdown();
  }
  // If we are still waiting on test loop, we can assume the page loading step
  // has been cancelled and the PrerenderHost is being discarded without
  // completing loading the page.
  if (on_wait_loading_finished_) {
    std::move(on_wait_loading_finished_)
        .Run(PrerenderHost::LoadingOutcome::kPrerenderingCancelled);
  }
}

// static
PrerenderHost* PrerenderHost::GetFromFrameTreeNodeIfPrerendering(
    FrameTreeNode& frame_tree_node) {
  if (!frame_tree_node.frame_tree().is_prerendering()) {
    return nullptr;
  }
  return &GetFromFrameTreeNode(frame_tree_node);
}

// static
PrerenderHost& PrerenderHost::GetFromFrameTreeNode(
    FrameTreeNode& frame_tree_node) {
  return GetFromFrameTree(&frame_tree_node.frame_tree());
}

// static
PrerenderHost& PrerenderHost::GetFromFrameTree(FrameTree* frame_tree) {
  CHECK(frame_tree);
  CHECK(frame_tree->is_prerendering());
  return *(static_cast<PrerenderHost::PrerenderFrameTreeDelegate*>(
               frame_tree->delegate())
               ->prerender_host_);
}

// static
bool PrerenderHost::AreHttpRequestHeadersCompatible(
    const std::string& potential_activation_headers_str,
#if BUILDFLAG(IS_ANDROID)
    const std::string& potential_activation_additional_headers_str,
#endif  // BUILDFLAG(IS_ANDROID)
    const std::string& prerender_headers_str,
    PreloadingTriggerType trigger_type,
    const std::string& histogram_suffix,
    bool allow_x_header_mismatch,
    PrerenderCancellationReason& reason) {
  net::HttpRequestHeaders prerender_headers;
  prerender_headers.AddHeadersFromString(prerender_headers_str);

  net::HttpRequestHeaders potential_activation_headers;
  potential_activation_headers.AddHeadersFromString(
      potential_activation_headers_str);
#if BUILDFLAG(IS_ANDROID)
  potential_activation_headers.AddHeadersFromString(
      potential_activation_additional_headers_str);
#endif  // BUILDFLAG(IS_ANDROID)

  // `prerender_headers` contains the "Purpose: prefetch" and "Sec-Purpose:
  // prefetch;prerender" to notify servers of prerender requests, while
  // `potential_activation_headers` doesn't contain it. Remove "Purpose" and
  // "Sec-Purpose" matching from consideration so that activation works with the
  // header.
  prerender_headers.RemoveHeader(blink::kPurposeHeaderName);
  potential_activation_headers.RemoveHeader(blink::kPurposeHeaderName);
  prerender_headers.RemoveHeader(blink::kSecPurposeHeaderName);
  potential_activation_headers.RemoveHeader(blink::kSecPurposeHeaderName);
  // Ditto for "Sec-Speculation-Tags".
  prerender_headers.RemoveHeader(blink::kSecSpeculationTagsHeaderName);
  CHECK(!potential_activation_headers.HasHeader(
      blink::kSecSpeculationTagsHeaderName));

  prerender_headers.RemoveHeader("RTT");
  potential_activation_headers.RemoveHeader("RTT");
  prerender_headers.RemoveHeader("Downlink");
  potential_activation_headers.RemoveHeader("Downlink");

  // TODO(crbug.com/40244149): Instead of handling headers added by
  // embedders specifically, prerender should expose an interface to embedders
  // to set url parameters.
#if BUILDFLAG(IS_ANDROID)
  // Used by Android devices only.
  if (trigger_type == PreloadingTriggerType::kEmbedder) {
    prerender_headers.RemoveHeader("X-Geo");
    potential_activation_headers.RemoveHeader("X-Geo");
  }
#endif  // BUILDFLAG(IS_ANDROID)

  // Remove the viewport headers as the viewport size of the initiator page can
  // be changed during prerendering. See also https://crbug.com/1401244.
  prerender_headers.RemoveHeader("viewport-width");
  potential_activation_headers.RemoveHeader("viewport-width");
  prerender_headers.RemoveHeader("sec-ch-viewport-width");
  potential_activation_headers.RemoveHeader("sec-ch-viewport-width");
  // Don't need to handle "viewport-height" as it is not defined in the specs.
  prerender_headers.RemoveHeader("sec-ch-viewport-height");
  potential_activation_headers.RemoveHeader("sec-ch-viewport-height");

  // Allow mismatches on `X-` headers. Currently this is allowed only on the
  // WebView.
  // TODO(crbug.com/40244149): Expand this to other platforms and non-x-headers.
  if (allow_x_header_mismatch) {
    std::set<std::string> headers_to_be_removed;
    for (net::HttpRequestHeaders::Iterator it(prerender_headers);
         it.GetNext();) {
      if (it.name().starts_with("X-") || it.name().starts_with("x-")) {
        headers_to_be_removed.insert(it.name());
      }
    }
    for (net::HttpRequestHeaders::Iterator it(potential_activation_headers);
         it.GetNext();) {
      if (it.name().starts_with("X-") || it.name().starts_with("x-")) {
        headers_to_be_removed.insert(it.name());
      }
    }
    for (const std::string& name : headers_to_be_removed) {
      prerender_headers.RemoveHeader(name);
      potential_activation_headers.RemoveHeader(name);
    }
  }

  return PrerenderHost::IsActivationHeaderMatch(potential_activation_headers,
                                                prerender_headers, reason);
}

// static
void PrerenderHost::SetHostCreationCallbackForTesting(
    base::OnceCallback<void(FrameTreeNodeId host_id)> callback) {
  GetHostCreationCallback() = std::move(callback);
}

PrerenderHost::PrerenderHost(
    std::unique_ptr<PrerenderHost> reuse_host,
    const PrerenderAttributes& attributes,
    WebContentsImpl& web_contents,
    base::WeakPtr<PreloadingAttempt> attempt,
    std::unique_ptr<DevToolsPrerenderAttempt> devtools_attempt)
    : attributes_(attributes),
      prerender_host_id_(NextPrerenderHostId()),
      metric_suffix_(
          GeneratePrerenderHistogramSuffix(trigger_type(),
                                           embedder_histogram_suffix())),
      attempt_(std::move(attempt)),
      devtools_attempt_(std::move(devtools_attempt)),
      web_contents_(web_contents),
      host_reused_(reuse_host) {
#if BUILDFLAG(IS_ANDROID)
  if (trigger_type() == PreloadingTriggerType::kSpeculationRule) {
    base::trace_event::EmitNamedTrigger("sp-prerender-start");
  }
#endif  // BUILDFLAG(IS_ANDROID)

  CheckPrerenderAttributes(attributes_);
  SetTriggeringOutcome(PreloadingTriggeringOutcome::kTriggeredButPending);

  if (reuse_host) {
    reuse_host->RecordFailedFinalStatusImpl(PrerenderCancellationReason(
        PrerenderFinalStatus::kPrerenderHostReused));
    if (reuse_host->frame_tree_delegate_->on_wait_loading_finished_) {
      std::move(reuse_host->frame_tree_delegate_->on_wait_loading_finished_)
          .Run(PrerenderHost::LoadingOutcome::kPrerenderingCancelled);
    }
    frame_tree_delegate_ = std::move(reuse_host->frame_tree_delegate_);
    // Reset the NavigationRequest if there is an on-going one in the frame tree
    // since the navigation is no longer needed. If there is no on-going
    // NavigationRequest, the function call will be no-op.
    GetFrameTree()->root()->ResetNavigationRequest(
        NavigationDiscardReason::kExplicitCancellation);
    frame_tree_delegate_->prerender_host_ = *this;
  } else {
    frame_tree_delegate_ = std::make_unique<PrerenderFrameTreeDelegate>(
        web_contents.GetBrowserContext(), web_contents, *this);
    scoped_refptr<SiteInstanceImpl> site_instance =
        base::FeatureList::IsEnabled(kCreatePrerenderSiteInstanceWithURL)
            ? SiteInstanceImpl::CreateForURL(web_contents.GetBrowserContext(),
                                             attributes.prerendering_url)
            : SiteInstanceImpl::Create(web_contents.GetBrowserContext());
    GetFrameTree()->Init(site_instance.get(),
                         /*renderer_initiated_creation=*/false,
                         /*main_frame_name=*/"", /*opener_for_origin=*/nullptr,
                         /*frame_policy=*/blink::FramePolicy(),
                         base::UnguessableToken::Create());

    // Use the same SessionStorageNamespace as the primary page for the
    // prerendering page.
    GetFrameTree()->controller().SetSessionStorageNamespace(
        site_instance->GetStoragePartitionConfig(),
        web_contents_->GetPrimaryFrameTree()
            .controller()
            .GetSessionStorageNamespace(
                site_instance->GetStoragePartitionConfig()));

    // TODO(crbug.com/40177940): This should be moved to FrameTree::Init
    web_contents_->NotifySwappedFromRenderManager(
        /*old_frame=*/nullptr,
        GetFrameTree()->root()->render_manager()->current_frame_host());
  }

  frame_tree_node_id_ = GetFrameTree()->root()->frame_tree_node_id();

  if (GetHostCreationCallback()) {
    CHECK_IS_TEST();
    std::move(GetHostCreationCallback()).Run(frame_tree_node_id_);
  }
}

// static
bool PrerenderHost::IsActivationHeaderMatch(
    const net::HttpRequestHeaders& potential_activation_headers,
    const net::HttpRequestHeaders& prerender_headers,
    PrerenderCancellationReason& reason) {
  // Normalize the headers.
  using HeaderPair = net::HttpRequestHeaders::HeaderKeyValuePair;
  auto cmp = [](const HeaderPair& a, const HeaderPair& b) {
    return a.key < b.key;
  };
  auto lower_case = [](HeaderPair& x) { x.key = base::ToLowerASCII(x.key); };
  auto same_predicate = [](const HeaderPair& a, const HeaderPair& b) {
    return a.key == b.key && base::EqualsCaseInsensitiveASCII(a.value, b.value);
  };

  std::vector<HeaderPair> potential_header_list(
      potential_activation_headers.GetHeaderVector());
  std::vector<HeaderPair> prerender_header_list(
      prerender_headers.GetHeaderVector());
  std::for_each(potential_header_list.begin(), potential_header_list.end(),
                lower_case);
  std::for_each(prerender_header_list.begin(), prerender_header_list.end(),
                lower_case);
  std::sort(potential_header_list.begin(), potential_header_list.end(), cmp);
  std::sort(prerender_header_list.begin(), prerender_header_list.end(), cmp);

  std::unique_ptr<std::vector<PrerenderMismatchedHeaders>> mismatched_headers =
      std::make_unique<std::vector<PrerenderMismatchedHeaders>>();

  auto prerender_header_list_it = prerender_header_list.begin();
  auto potential_header_list_it = potential_header_list.begin();

  while (prerender_header_list_it != prerender_header_list.end() &&
         potential_header_list_it != potential_header_list.end()) {
    if (same_predicate(*prerender_header_list_it, *potential_header_list_it)) {
      prerender_header_list_it++;
      potential_header_list_it++;
    } else if (prerender_header_list_it->key == potential_header_list_it->key) {
      mismatched_headers->emplace_back(prerender_header_list_it->key,
                                       prerender_header_list_it->value,
                                       potential_header_list_it->value);
      prerender_header_list_it++;
      potential_header_list_it++;
    } else if (prerender_header_list_it->key < potential_header_list_it->key) {
      mismatched_headers->emplace_back(prerender_header_list_it->key,
                                       prerender_header_list_it->value,
                                       std::nullopt);
      prerender_header_list_it++;
    } else {
      mismatched_headers->emplace_back(potential_header_list_it->key,
                                       std::nullopt,
                                       potential_header_list_it->value);
      potential_header_list_it++;
    }
  }

  while (prerender_header_list_it != prerender_header_list.end()) {
    mismatched_headers->emplace_back(prerender_header_list_it->key,
                                     prerender_header_list_it->value,
                                     std::nullopt);
    prerender_header_list_it++;
  }

  while (potential_header_list_it != potential_header_list.end()) {
    mismatched_headers->emplace_back(potential_header_list_it->key,
                                     std::nullopt,
                                     potential_header_list_it->value);
    potential_header_list_it++;
  }
  if (mismatched_headers->empty()) {
    return true;
  }
  reason.SetPrerenderMismatchedHeaders(std::move(mismatched_headers));
  return false;
}

PrerenderHost::~PrerenderHost() {
  if (!final_status_.has_value()) {
    RecordFailedFinalStatusImpl(
        PrerenderCancellationReason(PrerenderFinalStatus::kDestroyed));
  }
  for (auto& observer : observers_) {
    observer.OnHostDestroyed(final_status_.value());
  }
}

// TODO(crbug.com/40150744): Inspect diffs from the current
// no-state-prefetch implementation. See PrerenderContents::StartPrerendering()
// for example.
bool PrerenderHost::StartPrerendering() {
  TRACE_EVENT("navigation", "PrerenderHost::StartPrerendering");

  // Since prerender started we mark it as eligible and set it to running.
  SetTriggeringOutcome(PreloadingTriggeringOutcome::kRunning);

  // Start prerendering navigation.
  NavigationController::LoadURLParams load_url_params(
      attributes_.prerendering_url);
  load_url_params.initiator_origin = attributes_.initiator_origin;
  load_url_params.initiator_process_id = attributes_.initiator_process_id;
  load_url_params.initiator_frame_token = attributes_.initiator_frame_token;
#if BUILDFLAG(IS_ANDROID)
  if (!attributes_.additional_headers.IsEmpty()) {
    load_url_params.extra_headers =
        SerializeHttpRequestHeaders(attributes_.additional_headers);
  }
#endif  // BUILDFLAG(IS_ANDROID)
  load_url_params.is_renderer_initiated = !attributes_.IsBrowserInitiated();
  load_url_params.transition_type =
      ui::PageTransitionFromInt(attributes_.transition_type);
#if BUILDFLAG(ARKWEB_NETWORK_LOAD)
  load_url_params.extra_headers = attributes_.extra_headers;
#endif

  // Just use the referrer from attributes, as NoStatePrefetch does.
  load_url_params.referrer = attributes_.referrer;

  load_url_params.override_user_agent =
      web_contents_->GetDelegate()->ShouldOverrideUserAgentForPreloading(
          attributes_.prerendering_url);

  // TODO(https://crbug.com/1406149, https://crbug.com/1378921): Set
  // `override_user_agent` for Android. This field is determined on the Java
  // side based on the URL and we should mimic Java code and set it to the
  // correct value. After fixing this, we can remove the check for UA headers
  // upon activation.

  // TODO(crbug.com/40150744): Set up other fields of `load_url_params`
  // as well, and add tests for them.
  base::WeakPtr<NavigationHandle> created_navigation_handle =
      GetNavigationController().LoadURLWithParams(load_url_params);

  if (!created_navigation_handle) {
    return false;
  }

  if (attributes_.prerender_navigation_handle_callback) {
    attributes_.prerender_navigation_handle_callback.Run(
        *created_navigation_handle);
  }

  // Even when LoadURLWithParams() returns a valid navigation handle, navigation
  // can fail during navigation start, for example, due to prerendering a
  // non-supported URL scheme that is filtered out in
  // PrerenderNavigationThrottle.
  if (final_status_.has_value()) {
    return false;
  }

  NavigationRequest* navigation_request =
      NavigationRequest::From(created_navigation_handle.get());

  // In usual code path, `initial_navigation_id_` should be set by
  // PrerenderNavigationThrottle during `LoadURLWithParams` above.
  if (initial_navigation_id_.has_value()) {
    CHECK_EQ(*initial_navigation_id_,
             created_navigation_handle->GetNavigationId());
    CHECK(begin_params_);
    CHECK(common_params_);
  } else if (navigation_request->state() ==
             NavigationRequest::WAITING_FOR_RENDERER_RESPONSE) {
    // If a same-site prerender host is reused, the BeforeUnload handler maybe
    // called and thus to cause the NavigationRequest::BeginNavigation to be
    // delayed. The NavigationRequest state will be
    // NavigationRequest::WAITING_FOR_RENDERER_RESPONSE in this case. This is
    // not an error so we will wait for the SetInitialNavigation to be called
    // afterwards.
    CHECK(!begin_params_);
    CHECK(!common_params_);
  } else {
    // Prerender navigation failed before reaching the throttle for some reason.
    // For example, `ContentBrowserClient::ShouldOverrideUrlLoading()` may block
    // navigation.
    return false;
  }

  CHECK_GE(navigation_request->state(),
           NavigationRequest::WAITING_FOR_RENDERER_RESPONSE);

#if BUILDFLAG(ARKWEB_NETWORK_LOAD)
  // Update headers in begin_params_,
  // which is used to be compared before prerenderring page is activated.
  if (begin_params_ &&
    base::CommandLine::ForCurrentProcess()->HasSwitch(::switches::kEnableNwebEx)) {
    begin_params_->headers = navigation_request->begin_params().headers;
  }
#endif

  return true;
}

void PrerenderHost::DidStartNavigation(NavigationHandle* navigation_handle) {
  auto* navigation_request = NavigationRequest::From(navigation_handle);
  CHECK(navigation_request->IsInPrerenderedMainFrame());

  // Do nothing for the initial navigation.
  if (IsInitialNavigation(*navigation_request)) {
    return;
  }

  // Reset `is_ready_for_activation_` since it can be set to true more than once
  // and CHECK will fail when the main frame navigation happens in a
  // prerendered page and PrerenderHost::DidFinishNavigation is called multiple
  // times.
  is_ready_for_activation_ = false;
}

void PrerenderHost::ReadyToCommitNavigation(
    NavigationHandle* navigation_handle) {
  CHECK(navigation_handle);
  // For the initial navigation, set No-Vary-Search if there is a
  // No-Vary-Search header.
  auto* navigation_request = NavigationRequest::From(navigation_handle);
  CHECK(navigation_request->IsInPrerenderedMainFrame());
  // Prerender frame tree node is alive, see:
  // `PrerenderHostRegistry::ReadyToCommitNavigation`.
  CHECK(GetFrameTree());
  CHECK_EQ(GetFrameTree(),
           &navigation_request->frame_tree_node()->frame_tree());

  if (!IsInitialNavigation(*navigation_request)) {
    return;
  }

  bool has_no_vary_search_with_parse_error_header = false;
  if (navigation_request->response() &&
      navigation_request->response()->parsed_headers) {
    const network::mojom::ParsedHeadersPtr& parsed_headers =
        navigation_request->response()->parsed_headers;
    if (parsed_headers->no_vary_search_with_parse_error) {
      has_no_vary_search_with_parse_error_header = true;
      MaybeSetNoVarySearch(*parsed_headers->no_vary_search_with_parse_error);
    }

    const bool is_prerender_2_cross_origin_iframes_enabled =
        attributes_.enable_cross_origin_prerender_iframes ||
        base::FeatureList::IsEnabled(
            blink::features::kPrerender2CrossOriginIframes);
    if (is_prerender_2_cross_origin_iframes_enabled &&
        base::Contains(
            parsed_headers->supports_loading_mode,
            network::mojom::LoadingMode::kPrerenderCrossOriginFrames)) {
      allow_cross_origin_subframe_navigation_ = true;
    }
  }
  if (!has_no_vary_search_with_parse_error_header) {
    CHECK(!no_vary_search_.has_value());
    CHECK(!no_vary_search_parse_error_.has_value());
  }

  // ReadyToCommitNavigation is called when the headers are received.
  were_headers_received_ = true;
  for (auto& observer : observers_) {
    observer.OnHeadersReceived(*navigation_handle);
  }
}

void PrerenderHost::DidFinishNavigation(NavigationHandle* navigation_handle) {
  auto* navigation_request = NavigationRequest::From(navigation_handle);

  // Observe navigation only in the prerendering frame tree.
  CHECK_EQ(&(navigation_request->frame_tree_node()->frame_tree()),
           GetFrameTree());
  CHECK(navigation_request->GetPrerenderHostId());

  // TODO(crbug.com/434826191): Remove after fully migrating to PrerenderHostId.
  // The if clause is added for filtering the DidCommitNavigation
  // callback called after reusing a PrerenderHost. Currently the
  // PrerenderHostRegistry finds the PrerenderHost by the FrameTreeNodeId, this
  // will cause the DidFinishNavigation callback of the previous PrerenderHost
  // to be sent to the new PrerenderHost.
  if (prerender_host_id_ != navigation_request->GetPrerenderHostId()) {
    return;
  }

  if (PreloadServingMetricsCapsule::IsFeatureEnabled()) {
    // If `DidFinishNavigation()` is called multiple times, ignore
    // `PreloadServingMetrics` of that navigation and keep the first one.
    if (!prerender_initial_preload_serving_metrics_) {
      // Take `PreloadServingMetrics` of prerender initial navigation.
      auto& initial_preload_serving_metrics_holder =
          *PreloadServingMetricsHolder::GetOrCreateForNavigationHandle(
              *navigation_handle);
      prerender_initial_preload_serving_metrics_ =
          initial_preload_serving_metrics_holder.Take();
    }
  }

  const bool is_prerender_main_frame =
      navigation_request->GetFrameTreeNodeId() == frame_tree_node_id_;

  // Cancel prerendering on navigation request failure.
  //
  // Check net::Error here rather than PrerenderNavigationThrottle as CSP
  // blocking occurs before NavigationThrottles so cannot be observed in
  // NavigationThrottle::WillFailRequest().
  net::Error net_error = navigation_request->GetNetErrorCode();

  std::optional<PrerenderFinalStatus> status;
  if (net_error == net::Error::ERR_BLOCKED_BY_CSP) {
    status = PrerenderFinalStatus::kNavigationRequestBlockedByCsp;
  } else if (net_error == net::Error::ERR_BLOCKED_BY_CLIENT) {
    status = PrerenderFinalStatus::kBlockedByClient;
  } else if (is_prerender_main_frame && net_error != net::Error::OK) {
    status = PrerenderFinalStatus::kNavigationRequestNetworkError;
  } else if (is_prerender_main_frame && !navigation_request->HasCommitted()) {
    status = PrerenderFinalStatus::kNavigationNotCommitted;
  }
  if (status.has_value()) {
    Cancel(*status);
    return;
  }

  // The prerendered contents are considered ready for activation when the
  // main frame navigation reaches DidFinishNavigation and the prerender host
  // has not been canceled yet.
  if (is_prerender_main_frame && !final_status_) {
    CHECK(!is_ready_for_activation_);
    is_ready_for_activation_ = true;

    // Prerender is ready to activate. Set the status to kReady.
    SetTriggeringOutcome(PreloadingTriggeringOutcome::kReady);
  }
}

std::unique_ptr<StoredPage> PrerenderHost::Activate(
    NavigationRequest& navigation_request) {
  TRACE_EVENT("navigation", "PrerenderHost::Activate", "navigation_request",
              &navigation_request);

  CHECK(is_ready_for_activation_);
  is_ready_for_activation_ = false;

  FrameTree& target_frame_tree = web_contents_->GetPrimaryFrameTree();

  // There should be no ongoing main-frame navigation during activation.
  // TODO(crbug.com/40174232): Make sure sub-frame navigations are
  // fine.
  CHECK(!GetFrameTree()->root()->HasNavigation());

  // Before the root's current_frame_host is cleared, collect the subframes of
  // `frame_tree_` whose FrameTree will need to be updated.
  FrameTree::NodeRange node_range = GetFrameTree()->Nodes();
  std::vector<FrameTreeNode*> subframe_nodes(std::next(node_range.begin()),
                                             node_range.end());

  // Before the root's current_frame_host is cleared, collect the replication
  // state so that it can be used for post-activation validation.
  blink::mojom::FrameReplicationState prior_replication_state =
      GetFrameTree()->root()->current_replication_state();

  // Update FrameReplicationState::has_received_user_gesture_before_nav of the
  // prerendered page.
  //
  // On regular navigation, it is updated via a renderer => browser IPC
  // (RenderFrameHostImpl::HadStickyUserActivationBeforeNavigationChanged),
  // which is sent from blink::DocumentLoader::CommitNavigation. However,
  // this doesn't happen on prerender page activation, so the value is not
  // correctly updated without this treatment.
  //
  // The updated value will be sent to the renderer on
  // blink::mojom::Page::ActivatePrerenderedPage.
  prior_replication_state.has_received_user_gesture_before_nav =
      navigation_request.frame_tree_node()
          ->has_received_user_gesture_before_nav();

  // frame_tree_->root(). Do not add any code between here and
  // frame_tree_.reset() that calls into observer functions to minimize the
  // duration of current_frame_host being null.
  std::unique_ptr<StoredPage> page =
      GetFrameTree()->root()->render_manager()->TakePrerenderedPage();
  CHECK(page);
  if (allow_cross_origin_subframe_navigation_) {
    page->render_frame_host()
        ->GetPage()
        .NotifyCrossOriginSubframePrerenderIsAllowed();
  }

  NavigationEntryRestoreContextImpl context;
  std::unique_ptr<NavigationEntryImpl> nav_entry =
      GetNavigationController()
          .GetEntryWithUniqueID(page->render_frame_host()->nav_entry_id())
          ->CloneWithoutSharing(&context);

  navigation_request.SetPrerenderActivationNavigationState(
      std::move(nav_entry), prior_replication_state);

  CHECK_EQ(&target_frame_tree,
           &navigation_request.frame_tree_node()->frame_tree());

  // We support activating the prerendered page only to the topmost
  // RenderFrameHost.
  CHECK(!page->render_frame_host()->GetParentOrOuterDocumentOrEmbedder());

  page->render_frame_host()->SetFrameTreeNode(*(target_frame_tree.root()));
  page->render_frame_host()->SetRenderFrameHostOwner(target_frame_tree.root());

  // Copy frame name into the replication state of the primary main frame to
  // ensure that the replication state of the primary main frame after
  // activation matches the replication state stored in the renderer.
  // TODO(crbug.com/40192974): Copying frame name here is suboptimal
  // and ideally we'd do this at the same time when transferring the proxies
  // from the StoredPage into RenderFrameHostManager. However, this is a
  // temporary solution until we move this into BrowsingContextState,
  // along with RenderFrameProxyHost.
  page->render_frame_host()->frame_tree_node()->set_frame_name_for_activation(
      prior_replication_state.unique_name, prior_replication_state.name);
  for (auto& it : page->proxy_hosts()) {
    it.second->set_frame_tree_node(*(target_frame_tree.root()));
  }

  // Iterate over the root RenderFrameHost's subframes and update the
  // associated frame tree. Note that subframe proxies don't need their
  // FrameTrees independently updated, since their FrameTreeNodes don't
  // change, and FrameTree references in those FrameTreeNodes will be updated
  // through RenderFrameHosts.
  //
  // TODO(crbug.com/40177949): Need to investigate if and how
  // pending delete RenderFrameHost objects should be handled if prerendering
  // runs all of the unload handlers; they are not currently handled here.
  // This is because pending delete RenderFrameHosts can still receive and
  // process some messages while the RenderFrameHost FrameTree and
  // FrameTreeNode are stale.
  for (FrameTreeNode* subframe_node : subframe_nodes) {
    subframe_node->SetFrameTree(target_frame_tree);
  }

  frame_tree_delegate_.reset();

  page->render_frame_host()->ForEachRenderFrameHostImplIncludingSpeculative(
      [this](RenderFrameHostImpl* rfh) {
        // The visibility state of the prerendering page has not been
        // updated by
        // WebContentsImpl::UpdateVisibilityAndNotifyPageAndView(). So
        // updates the visibility state using the PageVisibilityState of
        // `web_contents`.
        rfh->render_view_host()->SetFrameTreeVisibility(
            web_contents_->GetPageVisibilityState());
      });

  for (auto& observer : observers_) {
    observer.OnActivated();
  }

  // The activated page is on the primary tree now. It can propagate the client
  // hints to the global settings.
  BrowserContext* browser_context =
      target_frame_tree.controller().GetBrowserContext();
  ClientHintsControllerDelegate* client_hints_delegate =
      browser_context->GetClientHintsControllerDelegate();
  if (client_hints_delegate) {
    for (auto& [origin, client_hint] : client_hints_type_) {
      PersistAcceptCH(origin, *(target_frame_tree.root()),
                      client_hints_delegate, client_hint);
    }
  }

  // Associate `PreloadServingMetrics` of prerender initial navigation to ones
  // of activation.
  if (PreloadServingMetricsCapsule::IsFeatureEnabled()) {
    auto& activation_preload_serving_metrics_holder =
        *PreloadServingMetricsHolder::GetOrCreateForNavigationHandle(
            navigation_request);
    activation_preload_serving_metrics_holder
        .SetPrerenderInitialPreloadServingMetrics(
            std::move(prerender_initial_preload_serving_metrics_));
  }

  RecordActivation(navigation_request);

  // Prerender is activated. Set the status to kSuccess.
  SetTriggeringOutcome(PreloadingTriggeringOutcome::kSuccess);
  devtools_instrumentation::DidActivatePrerender(
      navigation_request, initiator_devtools_navigation_token());
  return page;
}

// Ensure that the frame policies are compatible between primary main frame and
// prerendering main frame:
// a) primary main frame's pending_frame_policy would normally apply to the new
// document during its creation. However, for prerendering we can't apply it as
// the document is already created.
// b) prerender main frame's pending_frame_policy can't be transferred to the
// primary main frame, we should not activate if it's non-zero.
// c) Existing  document can't change the frame_policy it is affected by, so we
// can't transfer RenderFrameHosts between FrameTreeNodes with different frame
// policies.
//
// Usually frame policy for the main frame is empty as in the most common case a
// parent document sets a policy on the child iframe.
bool PrerenderHost::IsFramePolicyCompatibleWithPrimaryFrameTree() {
  FrameTreeNode* prerender_root_ftn = GetFrameTree()->root();
  FrameTreeNode* primary_root_ftn = web_contents_->GetPrimaryFrameTree().root();

  // Ensure that the pending frame policy is not set on the main frames, as it
  // is usually set on frames by their parent frames.
  if (prerender_root_ftn->pending_frame_policy() != blink::FramePolicy()) {
    return false;
  }

  if (primary_root_ftn->pending_frame_policy() != blink::FramePolicy()) {
    return false;
  }

  if (prerender_root_ftn->current_replication_state().frame_policy !=
      primary_root_ftn->current_replication_state().frame_policy) {
    return false;
  }

  return true;
}

bool PrerenderHost::AreInitialPrerenderNavigationParamsCompatibleWithNavigation(
    NavigationRequest& navigation_request,
    PrerenderCancellationReason& reason) {
  // TODO(crbug.com/40170513): compare the rest of the navigation parameters. We
  // should introduce compile-time parameter checks as well, to ensure how new
  // fields should be compared for compatibility.

  // As the initial prerender navigation is a) limited to HTTP(s) URLs and b)
  // initiated by the PrerenderHost, we do not expect some navigation parameters
  // connected to certain navigation types to be set and the CHECKS below
  // enforce that.
  // The parameters of the potential activation, however, are coming from the
  // renderer and we mostly don't have any guarantees what they are, so we
  // should not CHECK them. Instead, by default we compare them with initial
  // prerender activation parameters and fail to activate when they differ.
  // Note: some of those parameters should be never set (or should be ignored)
  // for main-frame / HTTP(s) navigations, but we still compare them here as a
  // defence-in-depth measure.
  CHECK(navigation_request.IsInPrimaryMainFrame());

  // Check `common_params_` and `begin_params_` here as these can be nullptr
  // if LoadURLWithParams failed without running PrerenderNavigationThrottle.
  if (!common_params_ || !begin_params_) {
    return false;
  }

  // Relaxes checks for initiator, transition type, and headers. This logic is
  // intended to be used for WebView, as WebView is intended to host
  // embedder-trusted contests.
  bool allow_partial_mismatch =
      web_contents_->GetDelegate()->ShouldAllowPartialParamMismatchOfPrerender2(
          navigation_request);
#if BUILDFLAG(ARKWEB_NETWORK_LOAD)
  // Allow prerender page by API(embedder) and activate the page by the address
  // bar. Relaxes checks in this situation.
  allow_partial_mismatch =
      (allow_partial_mismatch ||
       (((navigation_request.common_params().transition &
          ~ui::PAGE_TRANSITION_CLIENT_REDIRECT) ==
         (ui::PAGE_TRANSITION_TYPED | ui::PAGE_TRANSITION_FROM_ADDRESS_BAR)) &&
        common_params_->transition ==
            (ui::PAGE_TRANSITION_TYPED | ui::PAGE_TRANSITION_FROM_API)));
#endif
  // Compare BeginNavigationParams.
  ActivationNavigationParamsMatch result =
      AreBeginNavigationParamsCompatibleWithNavigation(
          navigation_request.common_params().url,
          navigation_request.begin_params(), allow_partial_mismatch, reason);
  if (result != ActivationNavigationParamsMatch::kOk) {
    RecordPrerenderActivationNavigationParamsMatch(result,
                                                   GetHistogramSuffix());
    return false;
  }

  // Compare CommonNavigationParams.
  result = AreCommonNavigationParamsCompatibleWithNavigation(
      navigation_request.common_params(), allow_partial_mismatch);
  if (result != ActivationNavigationParamsMatch::kOk) {
    RecordPrerenderActivationNavigationParamsMatch(result,
                                                   GetHistogramSuffix());
    return false;
  }

  RecordPrerenderActivationNavigationParamsMatch(
      ActivationNavigationParamsMatch::kOk, GetHistogramSuffix());
  return true;
}

#if BUILDFLAG(IS_ANDROID)
// The flag below is provided in case the workaround had a bug. Use the flag to
// revert back to the previous behavior.
// TODO(crbug.com/399478939): Remove the workaround and this flag.
BASE_FEATURE(kPrerenderActivationMismatchWebViewWorkaround,
             base::FEATURE_ENABLED_BY_DEFAULT);
#endif

PrerenderHost::ActivationNavigationParamsMatch
PrerenderHost::AreBeginNavigationParamsCompatibleWithNavigation(
    const GURL& potential_activation_url,
    const blink::mojom::BeginNavigationParams& potential_activation,
    bool allow_partial_mismatch,
    PrerenderCancellationReason& reason) {
  CHECK(begin_params_);

  // TODO(https://crbug.com/340416082): Check details of security properties,
  // update the check to appropriate form and remove differences among all
  // platforms.
  if (!allow_partial_mismatch && (potential_activation.initiator_frame_token !=
                                  begin_params_->initiator_frame_token)) {
    return ActivationNavigationParamsMatch::kInitiatorFrameToken;
  }

#if BUILDFLAG(IS_ANDROID)
  std::string activation_additional_headers_str;
  bool workaround_enabled = base::FeatureList::IsEnabled(
      kPrerenderActivationMismatchWebViewWorkaround);
  if (!workaround_enabled || !IsSpeculationRuleType(trigger_type())) {
    activation_additional_headers_str =
        web_contents_->GetBrowserContext()->GetExtraHeadersForUrl(
            potential_activation_url);
  }
#endif  // BUILDFLAG(IS_ANDROID)
  if (!AreHttpRequestHeadersCompatible(potential_activation.headers,
#if BUILDFLAG(IS_ANDROID)
                                       activation_additional_headers_str,
#endif  // BUILDFLAG(IS_ANDROID)
                                       begin_params_->headers, trigger_type(),
                                       GetHistogramSuffix(),
                                       allow_partial_mismatch, reason)) {
    return ActivationNavigationParamsMatch::kHttpRequestHeader;
  }

  // Don't activate a prerendered page if the potential activation request
  // requires validation or bypass of the browser cache, as the prerendered page
  // is a kind of caches.
  // TODO(crbug.com/40183588): Instead of checking the load flags on
  // activation, we should cancel prerendering when the prerender initial
  // navigation has the flags.
  int cache_load_flags = net::LOAD_VALIDATE_CACHE | net::LOAD_BYPASS_CACHE |
                         net::LOAD_DISABLE_CACHE;
  if (potential_activation.load_flags & cache_load_flags) {
    return ActivationNavigationParamsMatch::kCacheLoadFlags;
  }
  if (potential_activation.load_flags != begin_params_->load_flags) {
    return ActivationNavigationParamsMatch::kLoadFlags;
  }

  if (potential_activation.skip_service_worker !=
      begin_params_->skip_service_worker) {
    return ActivationNavigationParamsMatch::kSkipServiceWorker;
  }

  if (potential_activation.mixed_content_context_type !=
      begin_params_->mixed_content_context_type) {
    return ActivationNavigationParamsMatch::kMixedContentContextType;
  }

  // Initial prerender navigation cannot be a form submission.
  CHECK(!begin_params_->is_form_submission);
  if (potential_activation.is_form_submission !=
      begin_params_->is_form_submission) {
    return ActivationNavigationParamsMatch::kIsFormSubmission;
  }

  if (potential_activation.searchable_form_url !=
      begin_params_->searchable_form_url) {
    return ActivationNavigationParamsMatch::kSearchableFormUrl;
  }

  if (potential_activation.searchable_form_encoding !=
      begin_params_->searchable_form_encoding) {
    return ActivationNavigationParamsMatch::kSearchableFormEncoding;
  }

  // Trust token params can be set only on subframe navigations, so both values
  // should be null here.
  CHECK(!begin_params_->trust_token_params);
  if (potential_activation.trust_token_params !=
      begin_params_->trust_token_params) {
    return ActivationNavigationParamsMatch::kTrustTokenParams;
  }

  // Don't require equality for request_context_type because link clicks
  // (HYPERLINK) should be allowed for activation, whereas prerender always has
  // type LOCATION.
  CHECK_EQ(begin_params_->request_context_type,
           blink::mojom::RequestContextType::LOCATION);
  switch (potential_activation.request_context_type) {
    case blink::mojom::RequestContextType::HYPERLINK:
    case blink::mojom::RequestContextType::LOCATION:
      break;
    default:
      return ActivationNavigationParamsMatch::kRequestContextType;
  }

  // Since impression should not be set, no need to compare contents.
  CHECK(!begin_params_->impression);
  if (potential_activation.impression.has_value()) {
    return ActivationNavigationParamsMatch::kImpressionHasValue;
  }

  // No need to test for devtools_initiator because this field is used for
  // tracking what triggered a network request, and prerender activation will
  // not use network requests.

  return ActivationNavigationParamsMatch::kOk;
}

PrerenderHost::ActivationNavigationParamsMatch
PrerenderHost::AreCommonNavigationParamsCompatibleWithNavigation(
    const blink::mojom::CommonNavigationParams& potential_activation,
    bool allow_partial_mismatch) {
  // The CommonNavigationParams::url field is expected to match both initial and
  // activation prerender navigations, as the PrerenderHost selection would have
  // already checked for matching values. Adding a CHECK here to be safe.
  CHECK(common_params_);
  if (attributes_.url_match_predicate) {
    // TODO(crbug.com/41494389): Figure out what we need to pass here as a
    // web_url_match result instead of std::nullopt.
    CHECK(attributes_.url_match_predicate.Run(potential_activation.url,
                                              std::nullopt));
  } else if (no_vary_search_.has_value()) {
    CHECK(no_vary_search_->AreEquivalent(potential_activation.url,
                                         common_params_->url));
  } else if (no_vary_search_hint().has_value()) {
    CHECK(no_vary_search_hint()->AreEquivalent(potential_activation.url,
                                               common_params_->url));
  } else {
    CHECK_EQ(potential_activation.url, common_params_->url);
  }

  // TODO(https://crbug.com/340416082): Check details of security properties,
  // update the check to appropriate form and remove differences among all
  // platforms.
  if (!allow_partial_mismatch && (potential_activation.initiator_origin !=
                                  common_params_->initiator_origin)) {
    return ActivationNavigationParamsMatch::kInitiatorOrigin;
  }

  // The transition must match with the exception of the client redirect flag.
  // The renderer may add the client redirect flag when it has enough
  // information to be certain that this navigation would replace the current
  // history entry (e.g., a renderer-initiated navigation to the current URL).
  int32_t potential_activation_transition =
      potential_activation.transition & ~ui::PAGE_TRANSITION_CLIENT_REDIRECT;
  if (!allow_partial_mismatch &&
      (potential_activation_transition != common_params_->transition)) {
    RecordPrerenderActivationTransition(potential_activation_transition,
                                        GetHistogramSuffix());
    return ActivationNavigationParamsMatch::kTransition;
  }

  CHECK_EQ(common_params_->navigation_type,
           blink::mojom::NavigationType::DIFFERENT_DOCUMENT);
  if (potential_activation.navigation_type != common_params_->navigation_type) {
    return ActivationNavigationParamsMatch::kNavigationType;
  }

  // We don't check download_policy as it affects whether the download triggered
  // by the NavigationRequest is allowed to proceed (or logs metrics) and
  // doesn't affect the behaviour of the document created by a non-download
  // navigation after commit (e.g. it doesn't affect future downloads in child
  // frames). PrerenderNavigationThrottle has already ensured that the initial
  // prerendering navigation isn't a download and as prerendering activation
  // won't reach out to the network, it won't turn into a navigation as well.

  CHECK(common_params_->base_url_for_data_url.is_empty());
  if (potential_activation.base_url_for_data_url !=
      common_params_->base_url_for_data_url) {
    return ActivationNavigationParamsMatch::kBaseUrlForDataUrl;
  }

  // The method parameter is compared only by CHECK_EQ because that change is
  // detected earlier by checking the HTTP request headers changes.
  CHECK_EQ(potential_activation.method, common_params_->method);

  // Initial prerender navigation can't be a form submission.
  CHECK(!common_params_->post_data);
  if (potential_activation.post_data != common_params_->post_data) {
    return ActivationNavigationParamsMatch::kPostData;
  }

  // No need to compare source_location, as it's only passed to the DevTools for
  // debugging purposes and does not impact the properties of the document
  // created by this navigation.

  CHECK(!common_params_->started_from_context_menu);
  if (potential_activation.started_from_context_menu !=
      common_params_->started_from_context_menu) {
    return ActivationNavigationParamsMatch::kStartedFromContextMenu;
  }

  // has_user_gesture doesn't affect any of the security properties of the
  // document created by navigation, so equality of the values is not required.
  // TODO(crbug.com/40191309): ensure that the user activation status is
  // propagated to the activated document.

  // text_fragment_token doesn't affect any of the security properties of the
  // document created by navigation, so equality of the values is not required.
  // TODO(crbug.com/40191311): ensure the activated document consumes
  // text_fragment_token and scrolls to the corresponding viewport.

  // No need to compare should_check_main_world_csp, as if the CSP blocks the
  // initial navigation, it cancels prerendering, and we don't reach here for
  // matching. So regardless of the activation's capability to bypass the main
  // world CSP, the prerendered page is eligible for the activation. This also
  // permits content scripts to activate the page.

  if (potential_activation.initiator_origin_trial_features !=
      common_params_->initiator_origin_trial_features) {
    return ActivationNavigationParamsMatch::kInitiatorOriginTrialFeature;
  }

  if (potential_activation.href_translate != common_params_->href_translate) {
    return ActivationNavigationParamsMatch::kHrefTranslate;
  }

  // Initial prerender navigation can't be a history navigation.
  CHECK(!common_params_->is_history_navigation_in_new_child_frame);
  if (potential_activation.is_history_navigation_in_new_child_frame !=
      common_params_->is_history_navigation_in_new_child_frame) {
    return ActivationNavigationParamsMatch::kIsHistoryNavigationInNewChildFrame;
  }

  // We intentionally don't check referrer or referrer->policy. See spec
  // discussion at https://github.com/WICG/nav-speculation/issues/18.

  if (potential_activation.request_destination !=
      common_params_->request_destination) {
    return ActivationNavigationParamsMatch::kRequestDestination;
  }

  return ActivationNavigationParamsMatch::kOk;
}

RenderFrameHostImpl* PrerenderHost::GetPrerenderedMainFrameHost() {
  CHECK(GetFrameTree());
  CHECK(GetFrameTree()->root()->current_frame_host());
  return GetFrameTree()->root()->current_frame_host();
}

FrameTree& PrerenderHost::GetPrerenderFrameTree() {
  CHECK(GetFrameTree());
  return *GetFrameTree();
}

void PrerenderHost::RecordFailedFinalStatus(
    base::PassKey<PrerenderHostRegistry>,
    const PrerenderCancellationReason& reason) {
  RecordFailedFinalStatusImpl(reason);
}

void PrerenderHost::RecordFailedFinalStatusImpl(
    const PrerenderCancellationReason& reason) {
  CHECK(!final_status_);
  CHECK_NE(reason.final_status(), PrerenderFinalStatus::kActivated);
  final_status_ = reason.final_status();
  RecordFailedPrerenderFinalStatus(reason, attributes_);

  // Set failure reason for this PreloadingAttempt specific to the
  // FinalStatus.
  SetFailureReason(reason);

  for (auto& observer : observers_) {
    observer.OnFailed(final_status_.value());
  }
}

void PrerenderHost::RecordActivation(NavigationRequest& navigation_request) {
  CHECK(!final_status_);
  final_status_ = PrerenderFinalStatus::kActivated;

  // TODO(crbug.com/40215894): Replace
  // `navigation_request.GetNextPageUkmSourceId()` with prerendered page's UKM
  // source ID.
  ReportSuccessActivation(attributes_,
                          navigation_request.GetNextPageUkmSourceId());
}

PrerenderHost::LoadingOutcome PrerenderHost::WaitForLoadStopForTesting() {
  return frame_tree_delegate_->WaitForLoadStopForTesting();  // IN-TEST
}

const GURL& PrerenderHost::GetInitialUrl() const {
  return attributes_.prerendering_url;
}

void PrerenderHost::AddObserver(Observer* observer) {
  observers_.AddObserver(observer);
}

void PrerenderHost::RemoveObserver(Observer* observer) {
  observers_.RemoveObserver(observer);
}

std::optional<int64_t> PrerenderHost::GetInitialNavigationId() const {
  return initial_navigation_id_;
}

void PrerenderHost::SetInitialNavigation(NavigationRequest* navigation) {
  CHECK(!initial_navigation_id_.has_value());
  initial_navigation_id_ = navigation->GetNavigationId();
  begin_params_ = navigation->begin_params().Clone();
  common_params_ = navigation->common_params().Clone();

  // The prerendered page should be checked by the main world CSP. See also
  // relevant comments in AreCommonNavigationParamsCompatibleWithNavigation().
  CHECK_EQ(common_params_->should_check_main_world_csp,
           network::mojom::CSPDisposition::CHECK);
}

void PrerenderHost::SetTriggeringOutcome(PreloadingTriggeringOutcome outcome) {
  if (attempt_) {
    attempt_->SetTriggeringOutcome(outcome);
  }

  if (devtools_attempt_) {
    devtools_attempt_->SetTriggeringOutcome(attributes_, outcome);
  }
}

void PrerenderHost::SetFailureReason(
    const PrerenderCancellationReason& reason) {
  switch (reason.final_status()) {
    // When adding a new failure reason, consider whether it should be
    // propagated to `attempt_`. Most values should be propagated, but we
    // explicitly do not propagate failure reasons if:
    // 1. prerender was successfully prepared but then destroyed because it
    //    wasn't needed for a subsequent navigation (kTriggerDestroyed and
    //    kPrerenderHostReused).
    // 2. the prerender was still pending for its initial navigation when it was
    //    activated (kActivatedBeforeStarted).
    case PrerenderFinalStatus::kTriggerDestroyed:
    case PrerenderFinalStatus::kActivatedBeforeStarted:
    case PrerenderFinalStatus::kTabClosedByUserGesture:
    case PrerenderFinalStatus::kTabClosedWithoutUserGesture:
    case PrerenderFinalStatus::kSpeculationRuleRemoved:
    case PrerenderFinalStatus::kOtherPrerenderedPageActivated:
    case PrerenderFinalStatus::kPrerenderHostReused:
      return;
    case PrerenderFinalStatus::kDestroyed:
    case PrerenderFinalStatus::kLowEndDevice:
    case PrerenderFinalStatus::kInvalidSchemeRedirect:
    case PrerenderFinalStatus::kInvalidSchemeNavigation:
    case PrerenderFinalStatus::kNavigationRequestBlockedByCsp:
    case PrerenderFinalStatus::kMojoBinderPolicy:
    case PrerenderFinalStatus::kRendererProcessCrashed:
    case PrerenderFinalStatus::kRendererProcessKilled:
    case PrerenderFinalStatus::kDownload:
    case PrerenderFinalStatus::kNavigationNotCommitted:
    case PrerenderFinalStatus::kNavigationBadHttpStatus:
    case PrerenderFinalStatus::kClientCertRequested:
    case PrerenderFinalStatus::kNavigationRequestNetworkError:
    case PrerenderFinalStatus::kCancelAllHostsForTesting:
    case PrerenderFinalStatus::kDidFailLoad:
    case PrerenderFinalStatus::kStop:
    case PrerenderFinalStatus::kSslCertificateError:
    case PrerenderFinalStatus::kLoginAuthRequested:
    case PrerenderFinalStatus::kUaChangeRequiresReload:
    case PrerenderFinalStatus::kBlockedByClient:
    case PrerenderFinalStatus::kMixedContent:
    case PrerenderFinalStatus::kTriggerBackgrounded:
    case PrerenderFinalStatus::kMemoryLimitExceeded:
    case PrerenderFinalStatus::kDataSaverEnabled:
    case PrerenderFinalStatus::kTriggerUrlHasEffectiveUrl:
    case PrerenderFinalStatus::kInactivePageRestriction:
    case PrerenderFinalStatus::kStartFailed:
    case PrerenderFinalStatus::kTimeoutBackgrounded:
    case PrerenderFinalStatus::kCrossSiteNavigationInInitialNavigation:
    case PrerenderFinalStatus::kCrossSiteRedirectInInitialNavigation:
    case PrerenderFinalStatus::
        kSameSiteCrossOriginRedirectNotOptInInInitialNavigation:
    case PrerenderFinalStatus::
        kSameSiteCrossOriginNavigationNotOptInInInitialNavigation:
    case PrerenderFinalStatus::kActivationNavigationParameterMismatch:
    case PrerenderFinalStatus::kActivatedInBackground:
    case PrerenderFinalStatus::kActivationNavigationDestroyedBeforeSuccess:
    case PrerenderFinalStatus::kPrimaryMainFrameRendererProcessCrashed:
    case PrerenderFinalStatus::kPrimaryMainFrameRendererProcessKilled:
    case PrerenderFinalStatus::kActivationFramePolicyNotCompatible:
    case PrerenderFinalStatus::kPreloadingDisabled:
    case PrerenderFinalStatus::kBatterySaverEnabled:
    case PrerenderFinalStatus::kActivatedDuringMainFrameNavigation:
    case PrerenderFinalStatus::kPreloadingUnsupportedByWebContents:
    case PrerenderFinalStatus::
        kSameSiteCrossOriginNavigationNotOptInInMainFrameNavigation:
    case PrerenderFinalStatus::
        kSameSiteCrossOriginRedirectNotOptInInMainFrameNavigation:
    case PrerenderFinalStatus::kCrossSiteNavigationInMainFrameNavigation:
    case PrerenderFinalStatus::kCrossSiteRedirectInMainFrameNavigation:
    case PrerenderFinalStatus::kMemoryPressureOnTrigger:
    case PrerenderFinalStatus::kMemoryPressureAfterTriggered:
    case PrerenderFinalStatus::kPrerenderingDisabledByDevTools:
    case PrerenderFinalStatus::kActivatedWithAuxiliaryBrowsingContexts:
    case PrerenderFinalStatus::kMaxNumOfRunningImmediatePrerendersExceeded:
    case PrerenderFinalStatus::kMaxNumOfRunningNonImmediatePrerendersExceeded:
    case PrerenderFinalStatus::kMaxNumOfRunningEmbedderPrerendersExceeded:
    case PrerenderFinalStatus::kPrerenderingUrlHasEffectiveUrl:
    case PrerenderFinalStatus::kRedirectedPrerenderingUrlHasEffectiveUrl:
    case PrerenderFinalStatus::kActivationUrlHasEffectiveUrl:
    case PrerenderFinalStatus::kJavaScriptInterfaceAdded:
    case PrerenderFinalStatus::kJavaScriptInterfaceRemoved:
    case PrerenderFinalStatus::kAllPrerenderingCanceled:
    case PrerenderFinalStatus::kWindowClosed:
    case PrerenderFinalStatus::kSlowNetwork:
    case PrerenderFinalStatus::kPrerenderFailedDuringPrefetch:
    case PrerenderFinalStatus::kBrowsingDataRemoved:
      if (attempt_) {
        attempt_->SetFailureReason(
            ToPreloadingFailureReason(reason.final_status()));
        // We reset the attempt to ensure we don't update once we have reported
        // it as failure or accidentally use it for any other prerender attempts
        // as PrerenderHost deletion is async.
        attempt_.reset();
      }

      if (devtools_attempt_) {
        devtools_attempt_->SetFailureReason(attributes_, reason);
        devtools_attempt_.reset();
      }

      return;
    case PrerenderFinalStatus::kActivated:
      // The activation path does not call this method, so it should never reach
      // this case.
      NOTREACHED();
  }
}

std::optional<UrlMatchType> PrerenderHost::IsUrlMatch(const GURL& url) const {
  // Triggers are not allowed to treat a cross-origin url as a matched url. It
  // would cause security risks.
  if (!url::IsSameOriginWith(attributes_.prerendering_url, url)) {
    return std::nullopt;
  }

  std::optional<UrlMatchType> result;

  if (GetInitialUrl() == url) {
    result = UrlMatchType::kExact;
  }

  // Check No-Vary-Search header and try and match.
  if (!result && no_vary_search_.has_value() &&
      no_vary_search_->AreEquivalent(GetInitialUrl(), url)) {
    result = UrlMatchType::kNoVarySearch;
  }

  if (!attributes_.url_match_predicate) {
    return result;
  }

  // Override the result of default url match logic with the result
  // from the custom url matching predicate call.
  if (attributes_.url_match_predicate.Run(url, result)) {
    return UrlMatchType::kURLPredicateMatch;
  }

  return std::nullopt;
}

bool PrerenderHost::IsNoVarySearchHintUrlMatch(const GURL& url) const {
  // Triggers are not allowed to treat a cross-origin url as a matched url. It
  // would cause security risks.
  if (!url::IsSameOriginWith(attributes_.prerendering_url, url)) {
    return false;
  }

  // We don't care about url_match_predicate here because it is applied only
  // if we know for sure url is a match. This is a "potential"
  // match depending on the No-Vary-Search header that will be received.
  if (attributes_.url_match_predicate) {
    return false;
  }
  // The same as above. We also don't care about the exact match.
  if (GetInitialUrl() == url) {
    return false;
  }

  // Let's check if this PrerenderHost would match by
  // No-Vary-Search hint. We need to check if the headers were already received.
  if (!were_headers_received()) {
    if (no_vary_search_hint().has_value() &&
        no_vary_search_hint()->AreEquivalent(GetInitialUrl(), url)) {
      return true;
    }
  }

  return false;
}

bool PrerenderHost::IsUrlSameOrigin(const GURL& url) const {
  return url::IsSameOriginWith(GetInitialUrl(), url);
}

bool PrerenderHost::IsUrlSameSite(const GURL& url) const {
  return prerender_navigation_utils::IsSameSite(
      url, url::Origin::Create(GetInitialUrl()));
}

void PrerenderHost::OnAcceptClientHintChanged(
    const url::Origin& origin,
    const std::vector<network::mojom::WebClientHintsType>& client_hints_type) {
  client_hints_type_[origin] = client_hints_type;
}

void PrerenderHost::GetAllowedClientHintsOnPage(
    const url::Origin& origin,
    blink::EnabledClientHints* client_hints) const {
  auto it = client_hints_type_.find(origin);
  if (it == client_hints_type_.end()) {
    return;
  }
  for (const auto& hint : it->second) {
    client_hints->SetIsEnabled(hint, true);
  }
}

std::string PrerenderHost::GetHistogramSuffix() const {
  return metric_suffix_;
}

void PrerenderHost::Cancel(PrerenderFinalStatus status) {
  TRACE_EVENT("navigation", "PrerenderHost::Cancel", "final_status", status);
  // Already cancelled.
  if (final_status_) {
    return;
  }

  RenderFrameHostImpl* host = PrerenderHost::GetPrerenderedMainFrameHost();
  CHECK(host);
  PrerenderHostRegistry* registry =
      host->delegate()->GetPrerenderHostRegistry();
  CHECK(registry);
  registry->CancelHost(frame_tree_node_id_, status);
}

void PrerenderHost::MaybeSetNoVarySearch(
    network::mojom::NoVarySearchWithParseError&
        no_vary_search_with_parse_error) {
  CHECK(!no_vary_search_);
  CHECK(!no_vary_search_parse_error_);
  if (no_vary_search_with_parse_error.is_parse_error()) {
    no_vary_search_parse_error_ =
        no_vary_search_with_parse_error.get_parse_error();
    return;
  }
  CHECK(no_vary_search_with_parse_error.is_no_vary_search());
  net::HttpNoVarySearchData no_vary_search =
      no_vary_search::ParseHttpNoVarySearchDataFromMojom(
          no_vary_search_with_parse_error.get_no_vary_search());
  if (attempt_) {
    static_cast<PreloadingAttemptImpl*>(attempt_.get())
        ->SetNoVarySearchMatchPredicate(base::BindRepeating(
            [](net::HttpNoVarySearchData no_vary_search, const GURL& a,
               const GURL& b) { return no_vary_search.AreEquivalent(a, b); },
            no_vary_search, GetInitialUrl()));
  }
  no_vary_search_ = std::move(no_vary_search);
}

bool PrerenderHost::IsInitialNavigation(
    const NavigationRequest& navigation_request) const {
  return GetInitialNavigationId() == navigation_request.GetNavigationId();
}

base::TimeDelta PrerenderHost::WaitUntilHeadTimeout() {
  int timeout_in_milliseconds = 0;
  if (IsSpeculationRuleType(attributes_.trigger_type)) {
    CHECK(eagerness().has_value());
    switch (eagerness().value()) {
      // Currently, `kImmediate` and `kEager` behaves the same.
      // TODO(crbug.com/40287486): Separate these behaviors.
      case blink::mojom::SpeculationEagerness::kImmediate:
      case blink::mojom::SpeculationEagerness::kEager:
        timeout_in_milliseconds =
            features::kPrerender2NoVarySearchWaitForHeadersTimeoutEagerPrerender
                .Get();
        break;
      case blink::mojom::SpeculationEagerness::kModerate:
        timeout_in_milliseconds =
            features::
                kPrerender2NoVarySearchWaitForHeadersTimeoutModeratePrerender
                    .Get();
        break;
      case blink::mojom::SpeculationEagerness::kConservative:
        timeout_in_milliseconds =
            features::
                kPrerender2NoVarySearchWaitForHeadersTimeoutConservativePrerender
                    .Get();
        break;
    }
  } else {
    timeout_in_milliseconds =
        features::kPrerender2NoVarySearchWaitForHeadersTimeoutForEmbedders
            .Get();
  }
  return base::Milliseconds(timeout_in_milliseconds);
}

void PrerenderHost::OnWaitingForHeadersStarted(
    NavigationHandle& navigation_handle,
    WaitingForHeadersStartedReason reason) {
  // Prerender frame tree is alive. This check is also done by the caller.
  CHECK(GetFrameTree());
  for (auto& observer : observers_) {
    observer.OnWaitingForHeadersStarted(navigation_handle, reason);
  }
}

void PrerenderHost::OnWaitingForHeadersFinished(
    WaitingForHeadersFinishedReason reason) {
  // Prerender frame tree is alive. This check is also done by the caller.
  CHECK(GetFrameTree());

  base::UmaHistogramEnumeration(
      "Prerender.Experimental.WaitingForHeadersFinishedReason" +
          GetHistogramSuffix(),
      reason);

  for (auto& observer : observers_) {
    observer.OnWaitingForHeadersFinished(reason);
  }
}

bool PrerenderHost::ShouldAbortNavigationBecausePrefetchUnavailable() const {
  CHECK(features::UsePrefetchPrerenderIntegration());

  auto is_prefetch_used =
      [](const std::optional<PrefetchStatus>& prefetch_status) -> bool {
    if (!prefetch_status.has_value()) {
      return false;
    }

    switch (prefetch_status.value()) {
      case PrefetchStatus::kPrefetchResponseUsed:
        return true;
      case PrefetchStatus::kPrefetchNotUsedProbeFailed:
      case PrefetchStatus::kPrefetchNotStarted:
      case PrefetchStatus::kPrefetchIneligibleUserHasCookies:
      case PrefetchStatus::kPrefetchIneligibleUserHasServiceWorker:
      case PrefetchStatus::
          kPrefetchIneligibleUserHasServiceWorkerNoFetchHandler:
      case PrefetchStatus::kPrefetchIneligibleRedirectFromServiceWorker:
      case PrefetchStatus::kPrefetchIneligibleRedirectToServiceWorker:
      case PrefetchStatus::kPrefetchIneligibleSchemeIsNotHttps:
      case PrefetchStatus::kPrefetchIneligibleNonDefaultStoragePartition:
      case PrefetchStatus::kPrefetchNotFinishedInTime:
      case PrefetchStatus::kPrefetchFailedNetError:
      case PrefetchStatus::kPrefetchFailedNon2XX:
      case PrefetchStatus::kPrefetchFailedMIMENotSupported:
      case PrefetchStatus::kPrefetchSuccessful:
      case PrefetchStatus::kPrefetchIneligibleRetryAfter:
      case PrefetchStatus::kPrefetchIneligiblePrefetchProxyNotAvailable:
      case PrefetchStatus::kPrefetchIsPrivacyDecoy:
      case PrefetchStatus::kPrefetchIsStale:
      case PrefetchStatus::kPrefetchNotUsedCookiesChanged:
      case PrefetchStatus::kPrefetchIneligibleHostIsNonUnique:
      case PrefetchStatus::kPrefetchIneligibleDataSaverEnabled:
      case PrefetchStatus::kPrefetchIneligibleExistingProxy:
      case PrefetchStatus::kPrefetchHeldback:
      case PrefetchStatus::kPrefetchFailedInvalidRedirect:
      case PrefetchStatus::kPrefetchFailedIneligibleRedirect:
      case PrefetchStatus::
          kPrefetchIneligibleSameSiteCrossOriginPrefetchRequiredProxy:
      case PrefetchStatus::kPrefetchIneligibleBatterySaverEnabled:
      case PrefetchStatus::kPrefetchIneligiblePreloadingDisabled:
      case PrefetchStatus::kPrefetchEvictedAfterCandidateRemoved:
      case PrefetchStatus::kPrefetchEvictedForNewerPrefetch:
      case PrefetchStatus::kPrefetchEvictedAfterBrowsingDataRemoved:
        return false;
    }
  };
  auto is_ineligibility_admissible =
      [](PreloadingEligibility prefetch_eligibility) -> bool {
    switch (prefetch_eligibility) {
      // Prefetch is not available if SW exists, but prerender is.
      case PreloadingEligibility::kUserHasServiceWorker:
      case PreloadingEligibility::kUserHasServiceWorkerNoFetchHandler:
      case PreloadingEligibility::kRedirectFromServiceWorker:
      case PreloadingEligibility::kRedirectToServiceWorker:
        // Prefetch is not available for HTTP, but prerender is available
        // for HTTPS/HTTP.
      case PreloadingEligibility::kSchemeIsNotHttps:
        return true;
      case PreloadingEligibility::kEligible:
      case PreloadingEligibility::kUnspecified:
      case PreloadingEligibility::kPreloadingDisabled:
      case PreloadingEligibility::kHidden:
      case PreloadingEligibility::kCrossOrigin:
      case PreloadingEligibility::kLowMemory:
      case PreloadingEligibility::kJavascriptDisabled:
      case PreloadingEligibility::kDataSaverEnabled:
      case PreloadingEligibility::kHasEffectiveUrl:
      case PreloadingEligibility::kSingleProcess:
      case PreloadingEligibility::kLinkRelNext:
      case PreloadingEligibility::kThirdPartyCookies:
      case PreloadingEligibility::kPreloadingInvokedWithinTimelimit:
      case PreloadingEligibility::kRendererProcessLimitExceeded:
      case PreloadingEligibility::kBatterySaverEnabled:
      case PreloadingEligibility::kPreloadingUnsupportedByWebContents:
      case PreloadingEligibility::kMemoryPressure:
      case PreloadingEligibility::kPreloadingDisabledByDevTools:
      case PreloadingEligibility::kHttpsOnly:
      case PreloadingEligibility::kHttpOrHttpsOnly:
      case PreloadingEligibility::kSlowNetwork:
      case PreloadingEligibility::kUserHasCookies:
      case PreloadingEligibility::kNonDefaultStoragePartition:
      case PreloadingEligibility::kRetryAfter:
      case PreloadingEligibility::kPrefetchProxyNotAvailable:
      case PreloadingEligibility::kHostIsNonUnique:
      case PreloadingEligibility::kExistingProxy:
      case PreloadingEligibility::kSameSiteCrossOriginPrefetchRequiredProxy:
      case PreloadingEligibility::kPreloadingEligibilityContentEnd:
      case PreloadingEligibility::kPreloadingEligibilityContentStart2:
      case PreloadingEligibility::kPreloadingEligibilityContentEnd2:
        return false;
    }
  };
  // If a prerender navigation reached to `PrefetchURLLoaderInterceptor`, it is
  // blocked by `PrefetchMatchResolver` and prefetch ahead of prerender. So, we
  // should've got prefetch eligibility when it reached to
  // `PrerenderURLLoaderThrottle`. Therefore, if prefetch eligibility is
  // `PreloadingEligibility::kUnspecified`, it implies that the navigation is
  // handled by other `NavigationLoaderInterceptor` earlier than
  // `PrefetchURLLoaderInterceptor`. In this case, the interceptor already
  // decided to serve a resource for the navigation. So, we don't need to abort
  // this prerender.
  //
  // For example, if a service worker is installed to the given URL,
  // `ServiceWorkerMainResourceLoaderInterceptor` intercepts the navigation.
  // `PrefetchService` may or may not report prefetch eligibility at this
  // timing, but eventually reports
  // `PreloadingEligibility::kUserHasServiceWorker`. So, continuing the
  // navigation is reasonable.
  auto nav_is_likely_handled_by_earlier_interceptor =
      [](PreloadingEligibility prefetch_eligibility) -> bool {
    return prefetch_eligibility == PreloadingEligibility::kUnspecified;
  };

  // Use a prefetch (in many cases, aheaf of prerender) if it is about to be
  // used.
  if (is_prefetch_used(attributes_.preload_pipeline_info->prefetch_status())) {
    return false;
  }

  // Fallback to normal navigation if the prefetch was ineligible that is
  // admissible.
  if (is_ineligibility_admissible(
          attributes_.preload_pipeline_info->prefetch_eligibility())) {
    return false;
  }

  // Continue if the navigation is handled by an earlier interceptor.
  if (nav_is_likely_handled_by_earlier_interceptor(
          attributes_.preload_pipeline_info->prefetch_eligibility())) {
    return false;
  }

  // Otherwise, abort this prerender.
  return true;
}

void PrerenderHost::AddAdditionalRequestHeaders(
    net::HttpRequestHeaders& headers,
    FrameTreeNode& navigating_frame_tree_node) {
  // The given FrameTreeNode should be in the same prerendering FrameTree.
  CHECK_EQ(&navigating_frame_tree_node.frame_tree(), &GetPrerenderFrameTree());

  // Add the "Sec-Purpose: prefetch;prerender" header to prerender navigations
  // including subframe navigations. Add "Purpose: prefetch" as well for
  // compatibility concerns (See
  // https://github.com/WICG/nav-speculation/issues/133).
  headers.SetHeader(blink::kSecPurposeHeaderName,
                    blink::kSecPurposePrefetchPrerenderHeaderValue);
  if (!base::FeatureList::IsEnabled(
          blink::features::kRemovePurposeHeaderForPrefetch)) {
    headers.SetHeader(blink::kPurposeHeaderName,
                      blink::kSecPurposePrefetchHeaderValue);
  }

  // Add the "Sec-Speculation-Tags" header to main frame initial prerender
  // navigation.
  // https://wicg.github.io/nav-speculation/prefetch.html#sec-speculation-tags-header
  std::optional<SpeculationRulesTags> tags = attributes_.GetTags();
  if (navigating_frame_tree_node.IsMainFrame() &&
      !GetInitialNavigationId().has_value() && tags.has_value()) {
    headers.SetHeader(blink::kSecSpeculationTagsHeaderName,
                      tags->ConvertStringToHeaderString().value());
  }
}

void PrerenderHost::NotifyReused() {
  for (auto& observer : observers_) {
    observer.OnHostReused();
  }
}

void PrerenderHost::OnWillBeCancelled(
    const PrerenderCancellationReason& reason) {
  if (!PreloadServingMetricsCapsule::IsFeatureEnabled()) {
    return;
  }

  [&]() {
    // There are two cases:
    //
    // 1. `DidFinishNavigation()` is already called and then prerender is
    //    cancelled.
    // 2. Cancelled before `DidFinishNavigation()`. (E.g.
    //    `PrerenderURLLoaderThrottle`.)
    //
    // In the case 1, `prerender_initial_preload_serving_metrics_` is already
    // set. So, nothing to do and return.
    if (prerender_initial_preload_serving_metrics_) {
      return;
    }

    // We believe that `NavigationRequest` exist in this case, but some tests
    // fails If we use `CHECK` for `frame_tree_node` and `navigation_request`.
    // (We don't check which causes the failure.) Give up to record metrics in
    // such case.
    //
    // TODO(crbug.com/360094997): Investigate why and Use `CHECK` instead if
    // possible.
    auto* frame_tree_node =
        FrameTreeNode::GloballyFindByID(frame_tree_node_id_);
    if (!frame_tree_node) {
      return;
    }

    NavigationRequest* navigation_request =
        frame_tree_node->navigation_request();
    if (!navigation_request) {
      // Cancellation can be occur in prerender initial navigation or after
      // `DidFinishNavigation()`. `!navigation_request` implies that the latter
      // case, but it is handled in `DidFinishNavigation()` and already taken
      // the log.
      //
      // TODO(crbug.com/360094997): Ditto. Investigate test failure. Use
      // `DUMP_WILL_BE_NOTREACHED` instead.
      // TODO(crbug.com/360094997): Use `CHECK` instead once we checked the
      // safety by `DUMP_WILL_BE_NOTREACHED`.
      return;
    }

    // Take `PreloadServingMetrics` of prerender initial navigation.
    auto& initial_preload_serving_metrics_holder =
        *PreloadServingMetricsHolder::GetOrCreateForNavigationHandle(
            *navigation_request);
    prerender_initial_preload_serving_metrics_ =
        initial_preload_serving_metrics_holder.Take();
  }();

  if (prerender_initial_preload_serving_metrics_) {
    prerender_initial_preload_serving_metrics_
        ->RecordMetricsForPrerenderInitialNavigationFailed();
  }
}

bool PrerenderHost::IsInitiatorOverridingUserAgent() {
  // The initiator FrameTreeNode can be unavailable in browser-initiated
  // prerender. In such cases, we use the primary main frame of the initiator
  // `WebContents` as a workaround.
  // TODO(crbug.com/445992576): Support prerender in new tab by looking into
  // `should_override_user_agent_in_new_tab_` in `WebContentsImpl`.
  NavigationEntry* last_entry = nullptr;
  if (initiator_frame_tree_node_id()) {
    last_entry = FrameTreeNode::GloballyFindByID(initiator_frame_tree_node_id())
                     ->frame_tree()
                     .controller()
                     .GetLastCommittedEntry();
  } else if (initiator_web_contents()) {
    last_entry = initiator_web_contents()
                     ->GetPrimaryMainFrame()
                     ->GetController()
                     .GetLastCommittedEntry();
  }
  return last_entry && last_entry->GetIsOverridingUserAgent();
}

base::WeakPtr<PrerenderHost> PrerenderHost::GetWeakPtr() {
  return weak_factory_.GetWeakPtr();
}

}  // namespace content