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

#include "components/page_load_metrics/browser/observers/prerender_page_load_metrics_observer.h"

#include "base/time/time.h"
#include "chrome/browser/page_load_metrics/integration_tests/metric_integration_test.h"
#include "chrome/browser/preloading/prerender/prerender_utils.h"
#include "chrome/test/base/ui_test_utils.h"
#include "components/page_load_metrics/browser/observers/core/uma_page_load_metrics_observer.h"
#include "components/page_load_metrics/browser/page_load_metrics_test_waiter.h"
#include "components/ukm/gmock_matchers.h"
#include "content/public/browser/preloading_trigger_type.h"
#include "content/public/browser/prerender_handle.h"
#include "content/public/browser/render_frame_host.h"
#include "content/public/test/browser_test.h"
#include "content/public/test/browser_test_utils.h"
#include "content/public/test/prerender_test_util.h"
#include "net/http/http_status_code.h"
#include "net/test/embedded_test_server/controllable_http_response.h"
#include "services/metrics/public/cpp/ukm_builders.h"
#include "testing/gmock/include/gmock/gmock.h"
#include "ui/base/page_transition_types.h"

namespace {
using testing::AllOf;
using testing::Not;
using ukm::builders::PageLoad;
using ukm::builders::PrerenderPageLoad;
using ukm::testing::HasMetric;
using ukm::testing::HasMetricWithValue;

const char kResponseWithNoStore[] =
    "HTTP/1.1 200 OK\r\n"
    "Content-Type: text/html; charset=utf-8\r\n"
    "Cache-Control: no-store\r\n"
    "\r\n"
    "The server speaks HTTP!";

class PrerenderPageLoadMetricsObserverBrowserTest
    : public MetricIntegrationTest {
 public:
  PrerenderPageLoadMetricsObserverBrowserTest()
      : prerender_helper_(base::BindRepeating(
            &PrerenderPageLoadMetricsObserverBrowserTest::web_contents,
            base::Unretained(this))) {
  }
  ~PrerenderPageLoadMetricsObserverBrowserTest() override = default;

  void SetUp() override {
    prerender_helper_.RegisterServerRequestMonitor(embedded_test_server());
    MetricIntegrationTest::SetUp();
  }

  // Similar to UkmRecorder::GetMergedEntriesByName(), but returned map is keyed
  // by source URL.
  std::map<GURL, ukm::mojom::UkmEntryPtr> GetMergedUkmEntries(
      const std::string& entry_name) {
    auto entries = ukm_recorder().GetMergedEntriesByName(entry_name);
    std::map<GURL, ukm::mojom::UkmEntryPtr> result;
    for (auto& kv : entries) {
      const ukm::mojom::UkmEntry* entry = kv.second.get();
      const ukm::UkmSource* source =
          ukm_recorder().GetSourceForSourceId(entry->source_id);
      if (!source) {
        continue;
      }
      EXPECT_TRUE(source->url().is_valid());
      EXPECT_TRUE(result.emplace(source->url(), std::move(kv.second)).second);
    }
    return result;
  }

 protected:
  void CheckFirstPaintMetrics(
      content::PreloadingTriggerType trigger_type =
          content::PreloadingTriggerType::kSpeculationRule,
      const std::string& embedder_suffix = "") {
    // FirstPaint should be recorded in the prerender PageLoad, not in the
    // regular PageLoad.
    histogram_tester().ExpectTotalCount(
        prerender_helper_.GenerateHistogramName(
            internal::kHistogramPrerenderActivationToFirstPaint, trigger_type,
            embedder_suffix),
        1);
    histogram_tester().ExpectTotalCount(internal::kHistogramFirstPaint, 0);
  }

  void CheckFirstContentfulPaintMetrics(
      content::PreloadingTriggerType trigger_type =
          content::PreloadingTriggerType::kSpeculationRule,
      const std::string& embedder_suffix = "") {
    // FirstContentfulPaint should be recorded in the prerender PageLoad, not in
    // the regular PageLoad.
    histogram_tester().ExpectTotalCount(
        prerender_helper_.GenerateHistogramName(
            internal::kHistogramPrerenderActivationToFirstContentfulPaint,
            trigger_type, embedder_suffix),
        1);
    histogram_tester().ExpectTotalCount(
        internal::kHistogramFirstContentfulPaint, 0);
  }

  void CheckFirstInputDelayMetrics(
      content::PreloadingTriggerType trigger_type =
          content::PreloadingTriggerType::kSpeculationRule,
      const std::string& embedder_suffix = "") {
    // FirstInputDelay should be recorded in the prerender PageLoad, not in the
    // regular PageLoad.
    histogram_tester().ExpectTotalCount(
        prerender_helper_.GenerateHistogramName(
            internal::kHistogramPrerenderFirstInputDelay4, trigger_type,
            embedder_suffix),
        1);
    histogram_tester().ExpectTotalCount(internal::kHistogramFirstInputDelay, 0);
  }

  void CheckLargestContentfulPaintMetrics(
      content::PreloadingTriggerType trigger_type =
          content::PreloadingTriggerType::kSpeculationRule,
      const std::string& embedder_suffix = "") {
    // LargestContentfulPaint should be recorded in the prerender PageLoad, not
    // in the regular PageLoad.
    histogram_tester().ExpectTotalCount(
        prerender_helper_.GenerateHistogramName(
            internal::kHistogramPrerenderActivationToLargestContentfulPaint2,
            trigger_type, embedder_suffix),
        1);
    histogram_tester().ExpectTotalCount(
        internal::kHistogramLargestContentfulPaint, 0);
  }

  void CheckResponsivenessMetrics(const GURL& url) {
    auto entries = GetMergedUkmEntries(PrerenderPageLoad::kEntryName);
    EXPECT_TRUE(entries.contains(url));
    const ukm::mojom::UkmEntry* entry = entries[url].get();
    EXPECT_TRUE(entry);
    EXPECT_THAT(
        entry,
        AllOf(
            HasMetric(
                PrerenderPageLoad::
                    kInteractiveTiming_WorstUserInteractionLatency_MaxEventDurationName),
            HasMetric(
                PrerenderPageLoad::
                    kInteractiveTiming_UserInteractionLatency_HighPercentile2_MaxEventDurationName),
            HasMetric(
                PrerenderPageLoad::kInteractiveTiming_NumInteractionsName)));

    std::vector<std::string> uma_list = {
        internal::kHistogramPrerenderNumInteractions,
        internal::
            kHistogramPrerenderUserInteractionLatencyHighPercentile2MaxEventDuration,
        internal::
            kHistogramPrerenderWorstUserInteractionLatencyMaxEventDuration};

    for (auto& uma : uma_list) {
      histogram_tester().ExpectTotalCount(uma, 1);
    }
  }

  content::test::PrerenderTestHelper prerender_helper_;
};

IN_PROC_BROWSER_TEST_F(PrerenderPageLoadMetricsObserverBrowserTest,
                       Activate_SpeculationRule) {
  ASSERT_TRUE(embedded_test_server()->Start());

  // Navigate to an initial page.
  auto initial_url = embedded_test_server()->GetURL("/empty.html");
  ASSERT_TRUE(ui_test_utils::NavigateToURL(browser(), initial_url));

  // Start a prerender.
  GURL prerender_url = embedded_test_server()->GetURL("/title2.html");
  prerender_helper_.AddPrerender(prerender_url);

  // Activate and wait for FCP.
  auto waiter = std::make_unique<page_load_metrics::PageLoadMetricsTestWaiter>(
      web_contents());
  waiter->AddPageExpectation(page_load_metrics::PageLoadMetricsTestWaiter::
                                 TimingField::kFirstContentfulPaint);
  prerender_helper_.NavigatePrimaryPage(prerender_url);
  waiter->Wait();

  histogram_tester().ExpectTotalCount(
      prerender_helper_.GenerateHistogramName(
          internal::kHistogramPrerenderNavigationToActivation,
          content::PreloadingTriggerType::kSpeculationRule, ""),
      1);

  // Expect only FP and FCP for prerender are recorded.
  CheckFirstPaintMetrics();
  CheckFirstContentfulPaintMetrics();

  // Simulate mouse click and wait for FirstInputDelay.
  content::SimulateMouseClick(web_contents(), 0,
                              blink::WebPointerProperties::Button::kLeft);
  waiter->AddPageExpectation(page_load_metrics::PageLoadMetricsTestWaiter::
                                 TimingField::kFirstInputDelay);
  waiter->Wait();

  // Expect only FID for prerender is recorded.
  CheckFirstInputDelayMetrics();

  // Force navigation to another page, which should force logging of histograms
  // persisted at the end of the page load lifetime.
  ASSERT_TRUE(
      ui_test_utils::NavigateToURL(browser(), GURL(url::kAboutBlankURL)));

  // Expect only LCP for prerender is recorded.
  CheckLargestContentfulPaintMetrics();

  histogram_tester().ExpectTotalCount(
      prerender_helper_.GenerateHistogramName(
          internal::kHistogramPrerenderCumulativeShiftScore,
          content::PreloadingTriggerType::kSpeculationRule, ""),
      1);
  histogram_tester().ExpectTotalCount(
      prerender_helper_.GenerateHistogramName(
          internal::kHistogramPrerenderCumulativeShiftScoreMainFrame,
          content::PreloadingTriggerType::kSpeculationRule, ""),
      1);

  auto entries = GetMergedUkmEntries(PrerenderPageLoad::kEntryName);
  EXPECT_EQ(2u, entries.size());

  const ukm::mojom::UkmEntry* prerendered_page_entry =
      entries[prerender_url].get();
  ASSERT_TRUE(prerendered_page_entry);
  EXPECT_THAT(
      prerendered_page_entry,
      AllOf(
          Not(HasMetric(PrerenderPageLoad::kTriggeredPrerenderName)),
          HasMetricWithValue(PrerenderPageLoad::kWasPrerenderedName, 1),
          HasMetricWithValue(PrerenderPageLoad::kNavigation_PageTransitionName,
                             ui::PAGE_TRANSITION_LINK)));
  EXPECT_THAT(
      prerendered_page_entry,
      AllOf(
          HasMetric(PrerenderPageLoad::kTiming_NavigationToActivationName),
          HasMetric(
              PrerenderPageLoad::kTiming_ActivationToFirstContentfulPaintName),
          HasMetric(PrerenderPageLoad::
                        kTiming_ActivationToLargestContentfulPaintName),
          HasMetric(PrerenderPageLoad::kInteractiveTiming_FirstInputDelay4Name),
          HasMetric(
              PrerenderPageLoad::
                  kLayoutInstability_MaxCumulativeShiftScore_SessionWindow_Gap1000ms_Max5000msName)));
  EXPECT_THAT(
      prerendered_page_entry,
      AllOf(Not(HasMetric(
                PageLoad::kPaintTiming_NavigationToFirstContentfulPaintName)),
            Not(HasMetric(
                PageLoad::
                    kPaintTiming_NavigationToLargestContentfulPaint2Name))));
  // Expect that when the response has no Cache-control:no-store we still record
  // the `kMainFrameResource_RequestHasNoStoreName` metric for prerender.
  EXPECT_THAT(
      prerendered_page_entry,
      HasMetric(PrerenderPageLoad::kMainFrameResource_RequestHasNoStoreName));

  const ukm::mojom::UkmEntry* initiator_page_entry = entries[initial_url].get();
  ASSERT_TRUE(initiator_page_entry);
  EXPECT_THAT(
      initiator_page_entry,
      HasMetricWithValue(PrerenderPageLoad::kTriggeredPrerenderName, 1));
  EXPECT_THAT(
      initiator_page_entry,
      AllOf(
          Not(HasMetric(PrerenderPageLoad::kWasPrerenderedName)),
          Not(HasMetric(PrerenderPageLoad::kTiming_NavigationToActivationName)),
          Not(HasMetric(
              PrerenderPageLoad::kTiming_ActivationToFirstContentfulPaintName)),
          Not(HasMetric(PrerenderPageLoad::
                            kTiming_ActivationToLargestContentfulPaintName))));

  CheckResponsivenessMetrics(prerender_url);
}

// Tests that metrics are not recorded if the page moves to background before
// recording metrics is completed.
IN_PROC_BROWSER_TEST_F(PrerenderPageLoadMetricsObserverBrowserTest,
                       ActivateAndMoveToBackground_SpeculationRule) {
  ASSERT_TRUE(embedded_test_server()->Start());

  // Navigate to an initial page.
  auto initial_url = embedded_test_server()->GetURL("/empty.html");
  ASSERT_TRUE(ui_test_utils::NavigateToURL(browser(), initial_url));

  // Start a prerender.
  GURL prerender_url = embedded_test_server()->GetURL("/title2.html");
  prerender_helper_.AddPrerender(prerender_url);

  // Start an activation.
  prerender_helper_.NavigatePrimaryPage(prerender_url);

  // Changing the visibility state to HIDDEN will prevent from recording metrics
  // such as LCP since they are supposed to be recorded only when the page is
  // foreground.
  web_contents()->WasHidden();

  // Force navigation to another page, which should force logging of metrics
  // persisted at the end of the page load lifetime.
  ASSERT_TRUE(
      ui_test_utils::NavigateToURL(browser(), GURL(url::kAboutBlankURL)));

  auto entries = GetMergedUkmEntries(PrerenderPageLoad::kEntryName);
  EXPECT_EQ(2u, entries.size());

  const ukm::mojom::UkmEntry* prerendered_page_entry =
      entries[prerender_url].get();
  ASSERT_TRUE(prerendered_page_entry);
  // `WasPrerendered` and `PageTransition` exist since they're recorded when the
  // activation starts.
  EXPECT_THAT(prerendered_page_entry,
              AllOf(HasMetric(PrerenderPageLoad::kWasPrerenderedName),
                    HasMetricWithValue(
                        PrerenderPageLoad::kNavigation_PageTransitionName,
                        ui::PAGE_TRANSITION_LINK)));

  // LCP for prerender shouldn't be recorded since the page is in the
  // background.
  EXPECT_THAT(
      prerendered_page_entry,
      Not(HasMetric(
          PrerenderPageLoad::kTiming_ActivationToLargestContentfulPaintName)));

  histogram_tester().ExpectTotalCount(
      prerender_helper_.GenerateHistogramName(
          internal::kHistogramPrerenderActivationToLargestContentfulPaint2,
          content::PreloadingTriggerType::kSpeculationRule, ""),
      0);
}

IN_PROC_BROWSER_TEST_F(PrerenderPageLoadMetricsObserverBrowserTest,
                       Activate_Embedder_DirectURLInput) {
  ASSERT_TRUE(embedded_test_server()->Start());

  GURL prerender_url = embedded_test_server()->GetURL("/title2.html");

  // Hold PrerenderHandle while the test is executed to avoid canceling a
  // prerender host in the destruction of PrerenderHandle.
  std::unique_ptr<content::PrerenderHandle> prerender_handle =
      prerender_helper_.AddEmbedderTriggeredPrerenderAsync(
          prerender_url, content::PreloadingTriggerType::kEmbedder,
          prerender_utils::kDirectUrlInputMetricSuffix,
          ui::PageTransitionFromInt(ui::PAGE_TRANSITION_TYPED |
                                    ui::PAGE_TRANSITION_FROM_ADDRESS_BAR));
  EXPECT_TRUE(prerender_handle);

  // Wait until the completion of prerendering navigation.
  prerender_helper_.WaitForPrerenderLoadCompletion(prerender_url);
  content::PrerenderHostId host_id =
      prerender_helper_.GetHostForUrl(prerender_url);
  EXPECT_TRUE(host_id);

  // Activate and wait for FCP.
  auto waiter = std::make_unique<page_load_metrics::PageLoadMetricsTestWaiter>(
      web_contents());
  waiter->AddPageExpectation(page_load_metrics::PageLoadMetricsTestWaiter::
                                 TimingField::kFirstContentfulPaint);
  // Simulate a browser-initiated navigation.
  prerender_helper_.NavigatePrimaryPageAsync(
      prerender_url,
      ui::PageTransitionFromInt(ui::PAGE_TRANSITION_TYPED |
                                ui::PAGE_TRANSITION_FROM_ADDRESS_BAR));
  waiter->Wait();

  histogram_tester().ExpectTotalCount(
      prerender_helper_.GenerateHistogramName(
          internal::kHistogramPrerenderNavigationToActivation,
          content::PreloadingTriggerType::kEmbedder,
          prerender_utils::kDirectUrlInputMetricSuffix),
      1);

  // Expect only FP and FCP for prerender are recorded.
  CheckFirstPaintMetrics(content::PreloadingTriggerType::kEmbedder,
                         prerender_utils::kDirectUrlInputMetricSuffix);
  CheckFirstContentfulPaintMetrics(
      content::PreloadingTriggerType::kEmbedder,
      prerender_utils::kDirectUrlInputMetricSuffix);

  // Simulate mouse click and wait for FirstInputDelay.
  content::SimulateMouseClick(web_contents(), 0,
                              blink::WebPointerProperties::Button::kLeft);
  waiter->AddPageExpectation(page_load_metrics::PageLoadMetricsTestWaiter::
                                 TimingField::kFirstInputDelay);
  waiter->Wait();

  // Expect only FID for prerender is recorded.
  CheckFirstInputDelayMetrics(content::PreloadingTriggerType::kEmbedder,
                              prerender_utils::kDirectUrlInputMetricSuffix);

  // Force navigation to another page, which should force logging of
  // histograms persisted at the end of the page load lifetime.
  ASSERT_TRUE(
      ui_test_utils::NavigateToURL(browser(), GURL(url::kAboutBlankURL)));

  // Expect only LCP for prerender is recorded.
  CheckLargestContentfulPaintMetrics(
      content::PreloadingTriggerType::kEmbedder,
      prerender_utils::kDirectUrlInputMetricSuffix);

  histogram_tester().ExpectTotalCount(
      prerender_helper_.GenerateHistogramName(
          internal::kHistogramPrerenderCumulativeShiftScore,
          content::PreloadingTriggerType::kEmbedder,
          prerender_utils::kDirectUrlInputMetricSuffix),
      1);
  histogram_tester().ExpectTotalCount(
      prerender_helper_.GenerateHistogramName(
          internal::kHistogramPrerenderCumulativeShiftScoreMainFrame,
          content::PreloadingTriggerType::kEmbedder,
          prerender_utils::kDirectUrlInputMetricSuffix),
      1);

  CheckResponsivenessMetrics(prerender_url);

  auto entries = GetMergedUkmEntries(PrerenderPageLoad::kEntryName);
  EXPECT_EQ(entries.size(), 1u);

  const ukm::mojom::UkmEntry* prerendered_page_entry =
      entries[prerender_url].get();
  ASSERT_TRUE(prerendered_page_entry);
  // `WasPrerendered` and `PageTransition` exist since they're recorded when the
  // activation starts.
  EXPECT_THAT(prerendered_page_entry,
              AllOf(HasMetric(PrerenderPageLoad::kWasPrerenderedName),
                    HasMetricWithValue(
                        PrerenderPageLoad::kNavigation_PageTransitionName,
                        ui::PAGE_TRANSITION_TYPED |
                            ui::PAGE_TRANSITION_FROM_ADDRESS_BAR)));
}

IN_PROC_BROWSER_TEST_F(PrerenderPageLoadMetricsObserverBrowserTest,
                       CancelledPrerender) {
  ASSERT_TRUE(embedded_test_server()->Start());

  // Navigate to an initial page.
  auto initial_url = embedded_test_server()->GetURL("/empty.html");
  ASSERT_TRUE(ui_test_utils::NavigateToURL(browser(), initial_url));

  // Start a prerender.
  GURL prerender_url = embedded_test_server()->GetURL("/title2.html");
  const content::PrerenderHostId host_id =
      prerender_helper_.AddPrerender(prerender_url);

  content::test::PrerenderHostObserver observer(*web_contents(), host_id);
  prerender_helper_.CancelPrerenderedPage(host_id);
  observer.WaitForDestroyed();

  // Force navigation to another page, which should force logging of histograms
  // persisted at the end of the page load lifetime.
  ASSERT_TRUE(
      ui_test_utils::NavigateToURL(browser(), GURL(url::kAboutBlankURL)));

  // As the prerender was cancelled, no prerendering metrics are recorded.
  EXPECT_EQ(0u, histogram_tester()
                    .GetTotalCountsForPrefix("PageLoad.Clients.Prerender.")
                    .size());

  auto entries = GetMergedUkmEntries(PrerenderPageLoad::kEntryName);
  EXPECT_FALSE(entries.contains(prerender_url));
}

IN_PROC_BROWSER_TEST_F(PrerenderPageLoadMetricsObserverBrowserTest,
                       Redirection) {
  ASSERT_TRUE(embedded_test_server()->Start());

  // Navigate to an initial page.
  auto initial_url = embedded_test_server()->GetURL("/empty.html");
  ASSERT_TRUE(ui_test_utils::NavigateToURL(browser(), initial_url));

  // Start a prerender.
  GURL redirected_url = embedded_test_server()->GetURL("/title2.html");
  GURL prerender_url = embedded_test_server()->GetURL("/server-redirect?" +
                                                      redirected_url.spec());
  prerender_helper_.AddPrerender(prerender_url);

  // Activate and wait for FCP.
  auto waiter = std::make_unique<page_load_metrics::PageLoadMetricsTestWaiter>(
      web_contents());
  waiter->AddPageExpectation(page_load_metrics::PageLoadMetricsTestWaiter::
                                 TimingField::kFirstContentfulPaint);
  prerender_helper_.NavigatePrimaryPage(prerender_url);
  waiter->Wait();

  CheckFirstPaintMetrics();
  CheckFirstContentfulPaintMetrics();

  // Force navigation to another page, which should force logging of histograms
  // persisted at the end of the page load lifetime.
  ASSERT_TRUE(
      ui_test_utils::NavigateToURL(browser(), GURL(url::kAboutBlankURL)));

  CheckLargestContentfulPaintMetrics();

  // Verify that UKM records the URL after the redirection, not the initial URL.
  auto entries = GetMergedUkmEntries(PrerenderPageLoad::kEntryName);
  ASSERT_FALSE(entries.contains(prerender_url));
  const ukm::mojom::UkmEntry* prerendered_page_entry =
      entries[redirected_url].get();
  ASSERT_TRUE(prerendered_page_entry);
  EXPECT_THAT(
      prerendered_page_entry,
      AllOf(Not(HasMetric(PrerenderPageLoad::kTriggeredPrerenderName)),
            HasMetricWithValue(PrerenderPageLoad::kWasPrerenderedName, 1)));
  EXPECT_THAT(
      prerendered_page_entry,
      AllOf(
          HasMetric(PrerenderPageLoad::kTiming_NavigationToActivationName),
          HasMetric(
              PrerenderPageLoad::kTiming_ActivationToFirstContentfulPaintName),
          HasMetric(PrerenderPageLoad::
                        kTiming_ActivationToLargestContentfulPaintName)));
}

// Tests that metrics are recoreded correctly with Cache-control:no store when
// prerender is activated.
IN_PROC_BROWSER_TEST_F(PrerenderPageLoadMetricsObserverBrowserTest,
                       ActivationWithNoStoreResponse) {
  // Create a HTTP response to control prerendering main-frame navigation.
  net::test_server::ControllableHttpResponse main_document_response(
      embedded_test_server(), "/main_document");

  ASSERT_TRUE(embedded_test_server()->Start());

  const GURL kInitialUrl = embedded_test_server()->GetURL("/empty.html");
  const GURL kPrerenderingUrl =
      embedded_test_server()->GetURL("/main_document");

  // Navigate to an initial page in primary frame tree.
  ASSERT_TRUE(ui_test_utils::NavigateToURL(browser(), kInitialUrl));

  // Start a prerender, and navigate to a page that doesn't commit navigation.
  {
    content::test::PrerenderHostRegistryObserver registry_observer(
        *web_contents());
    prerender_helper_.AddPrerenderAsync(kPrerenderingUrl);
    registry_observer.WaitForTrigger(kPrerenderingUrl);
  }

  content::PrerenderHostId host_id =
      prerender_helper_.GetHostForUrl(kPrerenderingUrl);
  content::test::PrerenderHostObserver prerender_observer(*web_contents(),
                                                          host_id);
  EXPECT_FALSE(prerender_observer.was_activated());

  // Complete the prerender response with no cache and finish ongoing prerender
  // main frame navigation. Start navigation in primary page to
  // kPrerenderingUrl.
  content::TestActivationManager primary_page_manager(web_contents(),
                                                      kPrerenderingUrl);
  ASSERT_TRUE(ExecJs(web_contents()->GetPrimaryMainFrame(),
                     content::JsReplace("location = $1", kPrerenderingUrl)));

  main_document_response.WaitForRequest();
  main_document_response.Send(kResponseWithNoStore);
  main_document_response.Done();

  auto waiter = std::make_unique<page_load_metrics::PageLoadMetricsTestWaiter>(
      web_contents());
  waiter->AddPageExpectation(page_load_metrics::PageLoadMetricsTestWaiter::
                                 TimingField::kFirstContentfulPaint);
  primary_page_manager.WaitForNavigationFinished();
  prerender_observer.WaitForActivation();
  waiter->Wait();

  // Force navigation to another page, which should force logging of metrics
  // persisted at the end of the page load lifetime.
  ASSERT_TRUE(
      ui_test_utils::NavigateToURL(browser(), GURL(url::kAboutBlankURL)));

  CheckFirstPaintMetrics();
  CheckFirstContentfulPaintMetrics();
  CheckLargestContentfulPaintMetrics();

  auto entries = GetMergedUkmEntries(PrerenderPageLoad::kEntryName);
  EXPECT_EQ(2u, entries.size());

  const ukm::mojom::UkmEntry* prerendered_page_entry =
      entries[kPrerenderingUrl].get();
  ASSERT_TRUE(prerendered_page_entry);

  // RequestHasNoStore should be recorded with value 1 as the response has
  // Cache-control no-store in it.
  EXPECT_THAT(
      prerendered_page_entry,
      AllOf(HasMetricWithValue(
                PrerenderPageLoad::kMainFrameResource_RequestHasNoStoreName, 1),
            HasMetric(PrerenderPageLoad::
                          kTiming_ActivationToLargestContentfulPaintName)));
}

// Tests that metrics are recoreded correctly for loading main resource of an
// activated page.
IN_PROC_BROWSER_TEST_F(PrerenderPageLoadMetricsObserverBrowserTest,
                       MainResourceLoadEvent) {
  ASSERT_TRUE(embedded_test_server()->Start());

  // Navigate to an initial page.
  auto initial_url = embedded_test_server()->GetURL("/empty.html");
  ASSERT_TRUE(content::NavigateToURL(web_contents(), initial_url));

  // Start a prerender.
  GURL prerender_url = embedded_test_server()->GetURL("/title2.html");
  GURL prerender_iframe_url = embedded_test_server()->GetURL("/title1.html");
  prerender_helper_.AddPrerender(prerender_url);
  prerender_helper_.WaitForPrerenderLoadCompletion(prerender_url);
  content::test::PrerenderHostObserver observer(*web_contents(), prerender_url);
  prerender_helper_.NavigatePrimaryPage(prerender_url);

  // Activate the page and add a new iframe.
  observer.WaitForActivation();
  std::string add_iframe_script = R"(
    function add_iframe(url) {
      const frame = document.createElement('iframe');
      frame.src = url;
      document.body.appendChild(frame);
      return new Promise(resolve => {
        frame.onload = e => resolve('LOADED');
      });
    }
    add_iframe($1);
  )";
  EXPECT_EQ("LOADED",
            EvalJs(web_contents(), content::JsReplace(add_iframe_script,
                                                      prerender_iframe_url)));

  // Flush metrics.
  ASSERT_TRUE(
      content::NavigateToURL(web_contents(), GURL(url::kAboutBlankURL)));

  // Record the result for only once.
  histogram_tester().ExpectUniqueSample(
      prerender_helper_.GenerateHistogramName(
          "PageLoad.Internal.Prerender2.ActivatedPageLoaderStatus",
          content::PreloadingTriggerType::kSpeculationRule, ""),
      net::Error::OK, 1);
}

IN_PROC_BROWSER_TEST_F(PrerenderPageLoadMetricsObserverBrowserTest,
                       DomLoadedBeforeActivation) {
  ASSERT_TRUE(embedded_test_server()->Start());

  // Navigate to an initial page.
  auto initial_url = embedded_test_server()->GetURL("/empty.html");
  ASSERT_TRUE(content::NavigateToURL(web_contents(), initial_url));

  // Start a prerender.
  GURL prerender_url = embedded_test_server()->GetURL("/title2.html");
  prerender_helper_.AddPrerender(prerender_url);
  prerender_helper_.WaitForPrerenderLoadCompletion(prerender_url);
  content::test::PrerenderHostObserver observer(*web_contents(), prerender_url);
  prerender_helper_.NavigatePrimaryPage(prerender_url);
  // Flush metrics.
  ASSERT_TRUE(
      content::NavigateToURL(web_contents(), GURL(url::kAboutBlankURL)));

  std::string dom_content_loaded_histogram_name =
      prerender_helper_.GenerateHistogramName(
          "PageLoad.Internal.Prerender2.DomContentLoadedToActivation3",
          content::PreloadingTriggerType::kSpeculationRule, "");
  std::string parse_start_histogram_name =
      prerender_helper_.GenerateHistogramName(
          "PageLoad.Internal.Prerender2.MainResourceParseStartToActivation",
          content::PreloadingTriggerType::kSpeculationRule, "");
  histogram_tester().ExpectTotalCount(dom_content_loaded_histogram_name, 1);
  histogram_tester().ExpectTotalCount(parse_start_histogram_name, 1);
  // We shift the duration by the 1 minute when recording the metric.
  base::TimeDelta shifting_duration = base::Minutes(1);
  EXPECT_GE(histogram_tester().GetTotalSum(dom_content_loaded_histogram_name),
            shifting_duration.InMilliseconds());
  EXPECT_GE(histogram_tester().GetTotalSum(parse_start_histogram_name),
            shifting_duration.InMilliseconds());
}

IN_PROC_BROWSER_TEST_F(PrerenderPageLoadMetricsObserverBrowserTest,
                       DomLoadedAfterActivation) {
  net::test_server::ControllableHttpResponse response(
      embedded_test_server(), "/prerender/slow_layout.js");
  ASSERT_TRUE(embedded_test_server()->Start());

  // Navigate to an initial page.
  auto initial_url = embedded_test_server()->GetURL("/empty.html");
  ASSERT_TRUE(content::NavigateToURL(web_contents(), initial_url));

  // Start a prerender.
  GURL prerender_url = embedded_test_server()->GetURL(
      "/prerender/defer_domcontent_loaded_till_activation.html");
  content::test::PrerenderHostRegistryObserver registry_observer(
      *web_contents());
  prerender_helper_.AddPrerenderAsync(prerender_url);
  registry_observer.WaitForTrigger(prerender_url);
  response.WaitForRequest();
  content::RenderFrameHost* rfh =
      prerender_helper_.GetPrerenderedMainFrameHost(prerender_url);

  // The prerendered page is not loaded yet.
  ASSERT_TRUE(rfh);

  // Cannot use base::test::RunUntil due to an weird bug on macOS. See
  // http://crbug.com/360052666.
  // Wait the page to execute the script by checking whether an element has been
  // attached to the document by script. Otherwise, dom_content_loaded_called
  // may not be defined yet.
  while (true) {
    const bool new_element_was_attached_by_js =
        EvalJs(rfh,
               "document.getElementById('script_execution_watcher') !== null;")
            .ExtractBool();
    if (new_element_was_attached_by_js) {
      break;
    }
    base::RunLoop run_loop;
    run_loop.RunUntilIdle();
  }
  ASSERT_EQ(false, EvalJs(rfh, "dom_content_loaded_called"));

  // Activate the page, and then release the blocking script.
  content::test::PrerenderHostObserver observer(*web_contents(), prerender_url);
  auto waiter = std::make_unique<page_load_metrics::PageLoadMetricsTestWaiter>(
      web_contents());
  waiter->AddPageExpectation(
      page_load_metrics::PageLoadMetricsTestWaiter::TimingField::kLoadEvent);
  std::ignore = ExecJs(web_contents()->GetPrimaryMainFrame(),
                       content::JsReplace("location = $1", prerender_url));
  observer.WaitForActivation();
  response.Send(net::HTTP_OK, "text/javascript", "");
  response.Done();
  waiter->Wait();

  // Flush metrics.
  ASSERT_TRUE(
      content::NavigateToURL(web_contents(), GURL(url::kAboutBlankURL)));
  std::string histogram_name = prerender_helper_.GenerateHistogramName(
      "PageLoad.Internal.Prerender2.DomContentLoadedToActivation3",
      content::PreloadingTriggerType::kSpeculationRule, "");
  histogram_tester().ExpectTotalCount(histogram_name, 1);
  // We shift the duration by the 1 minute when recording the metric.
  base::TimeDelta shifting_duration = base::Minutes(1);
  EXPECT_LE(histogram_tester().GetTotalSum(histogram_name),
            shifting_duration.InMilliseconds());
}

// TODO(crbug.com/438364202): Re-enable this test
#if BUILDFLAG(IS_LINUX) && defined(ADDRESS_SANITIZER)
#define MAYBE_ResponseBodyReceivedAfterActivation \
  DISABLED_ResponseBodyReceivedAfterActivation
#else
#define MAYBE_ResponseBodyReceivedAfterActivation \
  ResponseBodyReceivedAfterActivation
#endif
IN_PROC_BROWSER_TEST_F(PrerenderPageLoadMetricsObserverBrowserTest,
                       MAYBE_ResponseBodyReceivedAfterActivation) {
  net::test_server::ControllableHttpResponse response(embedded_test_server(),
                                                      "/title2.html");
  ASSERT_TRUE(embedded_test_server()->Start());

  // Navigate to an initial page.
  auto initial_url = embedded_test_server()->GetURL("/empty.html");
  ASSERT_TRUE(content::NavigateToURL(web_contents(), initial_url));

  // Start a prerender.
  GURL prerender_url = embedded_test_server()->GetURL("/title2.html");
  content::test::PrerenderHostRegistryObserver registry_observer(
      *web_contents());
  prerender_helper_.AddPrerenderAsync(prerender_url);
  registry_observer.WaitForTrigger(prerender_url);
  response.WaitForRequest();
  response.Send(net::HTTP_OK, "text/html");

  content::test::PrerenderHostObserver host_observer(*web_contents(),
                                                     prerender_url);
  auto waiter = std::make_unique<page_load_metrics::PageLoadMetricsTestWaiter>(
      web_contents());
  waiter->AddPageExpectation(
      page_load_metrics::PageLoadMetricsTestWaiter::TimingField::kLoadEvent);

  std::ignore = ExecJs(web_contents()->GetPrimaryMainFrame(),
                       content::JsReplace("location = $1", prerender_url));
  host_observer.WaitForActivation();

  // Send the body after activation.
  response.Send("<html>hello</html>");
  response.Done();
  waiter->Wait();

  // Flush metrics.
  ASSERT_TRUE(
      content::NavigateToURL(web_contents(), GURL(url::kAboutBlankURL)));
  std::string histogram_name = prerender_helper_.GenerateHistogramName(
      "PageLoad.Internal.Prerender2.MainResourceParseStartToActivation",
      content::PreloadingTriggerType::kSpeculationRule, "");
  histogram_tester().ExpectTotalCount(histogram_name, 1);
  // We shift the duration by the 1 minute when recording the metric.
  base::TimeDelta shifting_duration = base::Minutes(1);
  EXPECT_LE(histogram_tester().GetTotalSum(histogram_name),
            shifting_duration.InMilliseconds());
}

IN_PROC_BROWSER_TEST_F(PrerenderPageLoadMetricsObserverBrowserTest,
                       MainFrameNavigation) {
  ASSERT_TRUE(embedded_test_server()->Start());

  // Navigate to an initial page.
  auto initial_url = embedded_test_server()->GetURL("/empty.html");
  ASSERT_TRUE(ui_test_utils::NavigateToURL(browser(), initial_url));

  // Start a prerender and a main frame navigation in the prerendered page.
  GURL prerender_url = embedded_test_server()->GetURL("/title2.html");
  GURL navigation_url = embedded_test_server()->GetURL("/title3.html");
  content::PrerenderHostId host_id =
      prerender_helper_.AddPrerender(prerender_url);
  prerender_helper_.WaitForPrerenderLoadCompletion(host_id);
  prerender_helper_.NavigatePrerenderedPage(host_id, navigation_url);
  prerender_helper_.WaitForPrerenderLoadCompletion(host_id);

  // Activate and wait for FCP.
  auto waiter = std::make_unique<page_load_metrics::PageLoadMetricsTestWaiter>(
      web_contents());
  waiter->AddPageExpectation(page_load_metrics::PageLoadMetricsTestWaiter::
                                 TimingField::kFirstContentfulPaint);
  prerender_helper_.NavigatePrimaryPage(prerender_url);
  waiter->Wait();

  histogram_tester().ExpectTotalCount(
      prerender_helper_.GenerateHistogramName(
          internal::kHistogramPrerenderNavigationToActivation,
          content::PreloadingTriggerType::kSpeculationRule, ""),
      1);

  // Expect only FP and FCP for prerender are recorded.
  CheckFirstPaintMetrics();
  CheckFirstContentfulPaintMetrics();

  // Simulate mouse click and wait for FirstInputDelay.
  content::SimulateMouseClick(web_contents(), 0,
                              blink::WebPointerProperties::Button::kLeft);
  waiter->AddPageExpectation(page_load_metrics::PageLoadMetricsTestWaiter::
                                 TimingField::kFirstInputDelay);
  waiter->Wait();

  // Expect only FID for prerender is recorded.
  CheckFirstInputDelayMetrics();

  // Force navigation to another page, which should force logging of histograms
  // persisted at the end of the page load lifetime.
  ASSERT_TRUE(
      ui_test_utils::NavigateToURL(browser(), GURL(url::kAboutBlankURL)));

  // LargestContentfulPaint should be recorded in the prerender PageLoad, not
  // in the regular PageLoad.
  histogram_tester().ExpectTotalCount(
      prerender_helper_.GenerateHistogramName(
          internal::kHistogramPrerenderActivationToLargestContentfulPaint2,
          content::PreloadingTriggerType::kSpeculationRule,
          /*embedder_suffix=*/""),
      1);
  histogram_tester().ExpectTotalCount(
      internal::kHistogramLargestContentfulPaint, 0);

  // Expect CLS for prerender is recorded.
  histogram_tester().ExpectTotalCount(
      prerender_helper_.GenerateHistogramName(
          internal::kHistogramPrerenderCumulativeShiftScore,
          content::PreloadingTriggerType::kSpeculationRule, ""),
      1);
  histogram_tester().ExpectTotalCount(
      prerender_helper_.GenerateHistogramName(
          internal::kHistogramPrerenderCumulativeShiftScoreMainFrame,
          content::PreloadingTriggerType::kSpeculationRule, ""),
      1);

  // Verify that UKM records the URL (`navigation_url`) after the navigation,
  // not the initial URL.
  auto entries = GetMergedUkmEntries(PrerenderPageLoad::kEntryName);
  EXPECT_EQ(2u, entries.size());
  ASSERT_FALSE(entries.contains(prerender_url));
  const ukm::mojom::UkmEntry* prerendered_page_entry =
      entries[navigation_url].get();
  ASSERT_TRUE(prerendered_page_entry);

  EXPECT_THAT(
      prerendered_page_entry,
      AllOf(
          Not(HasMetric(PrerenderPageLoad::kTriggeredPrerenderName)),
          HasMetricWithValue(PrerenderPageLoad::kWasPrerenderedName, 1),
          HasMetric(PrerenderPageLoad::kTiming_NavigationToActivationName),
          HasMetric(
              PrerenderPageLoad::kTiming_ActivationToFirstContentfulPaintName),
          HasMetric(PrerenderPageLoad::
                        kTiming_ActivationToLargestContentfulPaintName),
          HasMetric(PrerenderPageLoad::kInteractiveTiming_FirstInputDelay4Name),
          HasMetric(
              PrerenderPageLoad::
                  kLayoutInstability_MaxCumulativeShiftScore_SessionWindow_Gap1000ms_Max5000msName)));

  EXPECT_THAT(
      prerendered_page_entry,
      AllOf(Not(HasMetric(
                PageLoad::kPaintTiming_NavigationToFirstContentfulPaintName)),
            Not(HasMetric(
                PageLoad::
                    kPaintTiming_NavigationToLargestContentfulPaint2Name))));

  const ukm::mojom::UkmEntry* initiator_page_entry = entries[initial_url].get();
  ASSERT_TRUE(initiator_page_entry);
  EXPECT_THAT(
      initiator_page_entry,
      HasMetricWithValue(PrerenderPageLoad::kTriggeredPrerenderName, 1));
  EXPECT_THAT(
      initiator_page_entry,
      AllOf(
          Not(HasMetric(PrerenderPageLoad::kWasPrerenderedName)),
          Not(HasMetric(PrerenderPageLoad::kTiming_NavigationToActivationName)),
          Not(HasMetric(
              PrerenderPageLoad::kTiming_ActivationToFirstContentfulPaintName)),
          Not(HasMetric(PrerenderPageLoad::
                            kTiming_ActivationToLargestContentfulPaintName))));

  CheckResponsivenessMetrics(navigation_url);
}

}  // namespace
