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

#include "chrome/browser/predictors/loading_predictor.h"

#include <array>
#include <map>
#include <memory>
#include <set>
#include <string>
#include <vector>

#include "base/base64.h"
#include "base/command_line.h"
#include "base/files/file_path.h"
#include "base/functional/bind.h"
#include "base/memory/raw_ptr.h"
#include "base/memory/scoped_refptr.h"
#include "base/strings/escape.h"
#include "base/strings/string_util.h"
#include "base/strings/stringprintf.h"
#include "base/strings/to_string.h"
#include "base/task/single_thread_task_runner.h"
#include "base/task/thread_pool/thread_pool_instance.h"
#include "base/test/metrics/histogram_tester.h"
#include "base/test/scoped_feature_list.h"
#include "base/test/task_environment.h"
#include "build/build_config.h"
#include "chrome/browser/browser_features.h"
#include "chrome/browser/browser_process.h"
#include "chrome/browser/navigation_predictor/navigation_predictor_features.h"
#include "chrome/browser/navigation_predictor/navigation_predictor_preconnect_client.h"
#include "chrome/browser/optimization_guide/optimization_guide_keyed_service.h"
#include "chrome/browser/optimization_guide/optimization_guide_keyed_service_factory.h"
#include "chrome/browser/page_load_metrics/observers/lcp_critical_path_predictor_page_load_metrics_observer.h"
#include "chrome/browser/predictors/lcp_critical_path_predictor/lcp_critical_path_predictor_util.h"
#include "chrome/browser/predictors/loading_predictor_factory.h"
#include "chrome/browser/predictors/loading_test_util.h"
#include "chrome/browser/predictors/predictors_enums.h"
#include "chrome/browser/predictors/predictors_features.h"
#include "chrome/browser/predictors/predictors_switches.h"
#include "chrome/browser/ui/browser_commands.h"
#include "chrome/browser/ui/browser_window/public/browser_window_interface.h"
#include "chrome/common/chrome_features.h"
#include "chrome/common/chrome_switches.h"
#include "chrome/test/base/in_process_browser_test.h"
#include "chrome/test/base/ui_test_utils.h"
#include "components/no_state_prefetch/browser/no_state_prefetch_handle.h"
#include "components/no_state_prefetch/browser/no_state_prefetch_manager.h"
#include "components/optimization_guide/core/optimization_guide_features.h"
#include "components/optimization_guide/proto/hints.pb.h"
#include "components/page_load_metrics/browser/page_load_metrics_test_waiter.h"
#include "content/public/browser/browser_task_traits.h"
#include "content/public/browser/browser_thread.h"
#include "content/public/browser/navigation_controller.h"
#include "content/public/browser/navigation_entry.h"
#include "content/public/browser/navigation_handle.h"
#include "content/public/browser/page_navigator.h"
#include "content/public/browser/preconnect_manager.h"
#include "content/public/browser/preconnect_request.h"
#include "content/public/browser/storage_partition.h"
#include "content/public/browser/web_contents.h"
#include "content/public/browser/web_contents_observer.h"
#include "content/public/common/content_features.h"
#include "content/public/common/page_type.h"
#include "content/public/common/referrer.h"
#include "content/public/test/back_forward_cache_util.h"
#include "content/public/test/browser_test.h"
#include "content/public/test/browser_test_utils.h"
#include "content/public/test/content_mock_cert_verifier.h"
#include "content/public/test/fenced_frame_test_util.h"
#include "content/public/test/preconnect_test_util.h"
#include "content/public/test/prerender_test_util.h"
#include "content/public/test/simple_url_loader_test_helper.h"
#include "content/public/test/test_frame_navigation_observer.h"
#include "content/public/test/test_navigation_observer.h"
#include "net/base/features.h"
#include "net/base/network_anonymization_key.h"
#include "net/dns/mock_host_resolver.h"
#include "net/test/embedded_test_server/connection_tracker.h"
#include "net/test/embedded_test_server/controllable_http_response.h"
#include "net/test/embedded_test_server/embedded_test_server.h"
#include "net/test/embedded_test_server/embedded_test_server_connection_listener.h"
#include "net/test/embedded_test_server/http_request.h"
#include "net/test/embedded_test_server/http_response.h"
#include "net/test/embedded_test_server/request_handler_util.h"
#include "net/traffic_annotation/network_traffic_annotation_test_helper.h"
#include "services/network/public/cpp/cors/cors_error_status.h"
#include "services/network/public/cpp/features.h"
#include "services/network/public/cpp/network_quality_tracker.h"
#include "services/network/public/cpp/network_switches.h"
#include "services/network/public/cpp/resource_request.h"
#include "services/network/public/cpp/simple_url_loader.h"
#include "services/network/public/mojom/clear_data_filter.mojom.h"
#include "services/network/public/mojom/cors.mojom.h"
#include "services/network/public/mojom/ip_address_space.mojom.h"
#include "services/network/public/mojom/network_context.mojom.h"
#include "testing/gmock/include/gmock/gmock.h"
#include "testing/gtest/include/gtest/gtest.h"
#include "third_party/blink/public/common/features.h"
#include "third_party/blink/public/common/navigation/preloading_headers.h"
#include "url/gurl.h"
#include "url/origin.h"

using content::BrowserThread;
using content::PreconnectRequest;
using testing::Optional;
using testing::SizeIs;

namespace predictors {

const char kChromiumUrl[] = "http://chromium.org";

const char kHtmlSubresourcesPath[] = "/predictors/html_subresources.html";
// The embedded test server runs on test.com.
// kHtmlSubresourcesPath contains high priority resources from baz.com and
// foo.com. kHtmlSubresourcesPath also contains a low priority resource from
// bar.com.
const char* const kHtmlSubresourcesHosts[] = {"test.com", "baz.com", "foo.com"};

std::string GetPathWithPortReplacement(const std::string& path, uint16_t port) {
  std::string string_port = base::StringPrintf("%d", port);
  return net::test_server::GetFilePathWithReplacements(
      path, {{"REPLACE_WITH_PORT", string_port}});
}

GURL GetDataURLWithContent(const std::string& content) {
  std::string encoded_content = base::Base64Encode(content);
  std::string data_uri_content = "data:text/html;base64," + encoded_content;
  return GURL(data_uri_content);
}

// Helper class to track and allow waiting for ResourcePrefetchPredictor
// initialization. WARNING: OnPredictorInitialized event will not be fired if
// ResourcePrefetchPredictor is initialized before the observer creation.
class PredictorInitializer : public TestObserver {
 public:
  explicit PredictorInitializer(ResourcePrefetchPredictor* predictor)
      : TestObserver(predictor), predictor_(predictor) {}

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

  void EnsurePredictorInitialized() {
    if (predictor_->initialization_state_ ==
        ResourcePrefetchPredictor::INITIALIZED) {
      return;
    }

    if (predictor_->initialization_state_ ==
        ResourcePrefetchPredictor::NOT_INITIALIZED) {
      predictor_->StartInitialization();
    }

    run_loop_.Run();
  }

  void OnPredictorInitialized() override { run_loop_.Quit(); }

 private:
  raw_ptr<ResourcePrefetchPredictor> predictor_ = nullptr;
  base::RunLoop run_loop_;
};

class LcpElementLearnWaiter : public TestObserver {
 public:
  explicit LcpElementLearnWaiter(ResourcePrefetchPredictor* predictor)
      : TestObserver(predictor) {}
  void Wait() { run_loop_.Run(); }

 private:
  void OnLcppLearned() override { run_loop_.Quit(); }
  base::RunLoop run_loop_;
};

class LcpUpdatedWaiter : public TestObserver {
 public:
  explicit LcpUpdatedWaiter(ResourcePrefetchPredictor* predictor,
                            size_t wait_lcp_count)
      : TestObserver(predictor), wait_lcp_count_(wait_lcp_count) {}
  const std::vector<std::optional<std::string>>& Wait() {
    run_loop_.Run();
    return element_locators_;
  }

 private:
  void OnLcpUpdated(
      const std::optional<std::string>& element_locator) override {
    element_locators_.push_back(element_locator);
    if (++lcp_count_ >= wait_lcp_count_) {
      run_loop_.Quit();
    }
  }
  base::RunLoop run_loop_;
  const size_t wait_lcp_count_;
  size_t lcp_count_ = 0u;
  std::vector<std::optional<std::string>> element_locators_;
};

class LcpTimingPredictedWaiter : public TestObserver {
 public:
  explicit LcpTimingPredictedWaiter(ResourcePrefetchPredictor* predictor)
      : TestObserver(predictor) {}
  std::optional<std::string>& Wait() {
    run_loop_.Run();
    return lcp_element_locator_;
  }

 private:
  void OnLcpTimingPredicted(
      const std::optional<std::string>& lcp_element_locator) override {
    lcp_element_locator_ = lcp_element_locator;
    run_loop_.Quit();
  }
  base::RunLoop run_loop_;
  std::optional<std::string> lcp_element_locator_;
};

class TestPreconnectManagerObserver
    : public content::PreconnectManager::Observer {
 public:
  explicit TestPreconnectManagerObserver(
      content::PreconnectManager* preconnect_manager) {
    preconnect_manager->SetObserverForTesting(this);
  }

  void OnPreconnectUrl(const GURL& url,
                       int num_sockets,
                       bool allow_credentials) override {
    preconnect_url_attempts_.insert(url.DeprecatedGetOriginAsURL());
    preconnect_url_attempts_history_.push_back(url.DeprecatedGetOriginAsURL());
  }

  void OnPreresolveFinished(
      const GURL& url,
      const net::NetworkAnonymizationKey& network_anonymization_key,
      mojo::PendingRemote<network::mojom::ConnectionChangeObserverClient>&
          observer,
      bool success) override {
    ResolveHostRequestInfo preconnect_info{url.GetHost(),
                                           network_anonymization_key};
    if (success) {
      successful_dns_lookups_.insert(preconnect_info);
    } else {
      unsuccessful_dns_lookups_.insert(preconnect_info);
    }
    CheckForWaitingLoop();
  }

  void OnProxyLookupFinished(
      const GURL& url,
      const net::NetworkAnonymizationKey& network_anonymization_key,
      bool success) override {
    ResolveProxyRequestInfo resolve_info{url::Origin::Create(url),
                                         network_anonymization_key};
    if (success) {
      successful_proxy_lookups_.insert(resolve_info);
    } else {
      unsuccessful_proxy_lookups_.insert(resolve_info);
    }
    CheckForWaitingLoop();
  }

  void WaitUntilHostLookedUp(
      const std::string& host,
      const net::NetworkAnonymizationKey& network_anonymization_key) {
    wait_event_ = WaitEvent::kDns;
    DCHECK(waiting_on_dns_.IsEmpty());
    waiting_on_dns_ = ResolveHostRequestInfo{host, network_anonymization_key};
    Wait();
  }

  void WaitUntilProxyLookedUp(
      const GURL& url,
      const net::NetworkAnonymizationKey& network_anonymization_key) {
    wait_event_ = WaitEvent::kProxy;
    DCHECK(waiting_on_proxy_.IsEmpty());
    waiting_on_proxy_ = ResolveProxyRequestInfo{url::Origin::Create(url),
                                                network_anonymization_key};
    Wait();
  }

  bool HasOriginAttemptedToPreconnect(const GURL& origin) {
    DCHECK_EQ(origin, origin.DeprecatedGetOriginAsURL());
    return preconnect_url_attempts_.contains(origin);
  }

  bool HasHostBeenLookedUp(
      const std::string& host,
      const net::NetworkAnonymizationKey& network_anonymization_key) {
    ResolveHostRequestInfo preconnect_info{host, network_anonymization_key};
    return successful_dns_lookups_.contains(preconnect_info) ||
           unsuccessful_dns_lookups_.contains(preconnect_info);
  }

  bool HostFound(
      const std::string& host,
      const net::NetworkAnonymizationKey& network_anonymization_key) {
    return successful_dns_lookups_.contains(
        ResolveHostRequestInfo{host, network_anonymization_key});
  }

  bool ProxyFound(
      const GURL& url,
      const net::NetworkAnonymizationKey& network_anonymization_key) {
    return successful_proxy_lookups_.contains(ResolveProxyRequestInfo{
        url::Origin::Create(url), network_anonymization_key});
  }

  const std::vector<GURL>& PreconnectUrlAttemptsHistory() const {
    return preconnect_url_attempts_history_;
  }

 private:
  enum class WaitEvent { kNone, kDns, kProxy };

  struct ResolveHostRequestInfo {
    bool operator<(const ResolveHostRequestInfo& other) const {
      return std::tie(hostname, network_anonymization_key) <
             std::tie(other.hostname, other.network_anonymization_key);
    }

    bool operator==(const ResolveHostRequestInfo& other) const {
      return std::tie(hostname, network_anonymization_key) ==
             std::tie(other.hostname, other.network_anonymization_key);
    }

    bool IsEmpty() const {
      return hostname.empty() && network_anonymization_key.IsEmpty();
    }

    std::string hostname;
    net::NetworkAnonymizationKey network_anonymization_key;
  };

  struct ResolveProxyRequestInfo {
    bool operator<(const ResolveProxyRequestInfo& other) const {
      return std::tie(origin, network_anonymization_key) <
             std::tie(other.origin, other.network_anonymization_key);
    }

    bool operator==(const ResolveProxyRequestInfo& other) const {
      return std::tie(origin, network_anonymization_key) ==
             std::tie(other.origin, other.network_anonymization_key);
    }

    bool IsEmpty() const {
      return origin.opaque() && network_anonymization_key.IsEmpty();
    }

    url::Origin origin;
    net::NetworkAnonymizationKey network_anonymization_key;
  };

  bool HasProxyBeenLookedUp(const ResolveProxyRequestInfo& resolve_proxy_info) {
    return successful_proxy_lookups_.contains(resolve_proxy_info) ||
           unsuccessful_proxy_lookups_.contains(resolve_proxy_info);
  }

  void Wait() {
    base::RunLoop run_loop;
    DCHECK(!run_loop_);
    run_loop_ = &run_loop;
    CheckForWaitingLoop();
    run_loop.Run();
  }

  void CheckForWaitingLoop() {
    switch (wait_event_) {
      case WaitEvent::kNone:
        return;
      case WaitEvent::kDns:
        if (!HasHostBeenLookedUp(waiting_on_dns_.hostname,
                                 waiting_on_dns_.network_anonymization_key)) {
          return;
        }
        waiting_on_dns_ = ResolveHostRequestInfo();
        break;
      case WaitEvent::kProxy:
        if (!HasProxyBeenLookedUp(waiting_on_proxy_)) {
          return;
        }
        waiting_on_proxy_ = ResolveProxyRequestInfo();
        break;
    }
    DCHECK(run_loop_);
    run_loop_->Quit();
    run_loop_ = nullptr;
    wait_event_ = WaitEvent::kNone;
  }

  WaitEvent wait_event_ = WaitEvent::kNone;
  raw_ptr<base::RunLoop> run_loop_ = nullptr;

  ResolveHostRequestInfo waiting_on_dns_;
  std::set<ResolveHostRequestInfo> successful_dns_lookups_;
  std::set<ResolveHostRequestInfo> unsuccessful_dns_lookups_;

  ResolveProxyRequestInfo waiting_on_proxy_;
  std::set<ResolveProxyRequestInfo> successful_proxy_lookups_;
  std::set<ResolveProxyRequestInfo> unsuccessful_proxy_lookups_;
  std::set<GURL> preconnect_url_attempts_;
  std::vector<GURL> preconnect_url_attempts_history_;
};

struct PrefetchResult {
  PrefetchResult(const GURL& prefetch_url,
                 const network::URLLoaderCompletionStatus& status)
      : prefetch_url(prefetch_url), status(status) {}

  GURL prefetch_url;
  network::URLLoaderCompletionStatus status;
};

class TestPrefetchManagerObserver : public PrefetchManager::Observer {
 public:
  explicit TestPrefetchManagerObserver(PrefetchManager& manager) {
    manager.set_observer_for_testing(this);
  }

  void OnPrefetchFinished(
      const GURL& url,
      const GURL& prefetch_url,
      const network::URLLoaderCompletionStatus& status) override {
    prefetches_.emplace_back(prefetch_url, status);
  }

  void OnAllPrefetchesFinished(const GURL& url) override {
    done_urls_.insert(url);
    if (waiting_url_ == url) {
      waiting_url_ = GURL();
      std::move(done_callback_).Run();
    }
  }

  void WaitForPrefetchesForNavigation(const GURL& url) {
    DCHECK(waiting_url_.is_empty());
    DCHECK(!url.is_empty());
    if (done_urls_.find(url) != done_urls_.end()) {
      return;
    }
    waiting_url_ = url;
    base::RunLoop loop;
    done_callback_ = loop.QuitClosure();
    loop.Run();
  }

  const std::vector<PrefetchResult>& results() const { return prefetches_; }

 private:
  std::vector<PrefetchResult> prefetches_;
  std::set<GURL> done_urls_;
  GURL waiting_url_;
  base::OnceClosure done_callback_;
};

class LoadingPredictorBrowserTest : public InProcessBrowserTest {
 public:
  LoadingPredictorBrowserTest() {
    scoped_feature_list_.InitWithFeatures(
        {features::kLoadingOnlyLearnHighPriorityResources,
         features::kLoadingPreconnectToRedirectTarget,
         features::kNavigationPredictorPreconnectHoldback},
        // TODO(crbug.com/40248833): Use HTTPS URLs in tests to avoid having to
        // disable this feature.
        {features::kHttpsUpgrades,
         // TODO(crbug.com/354087603): Update tests when this feature has
         // positive (or neutral) effect of loading performance.
         features::kLoadingPredictorLimitPreconnectSocketCount});
  }

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

  ~LoadingPredictorBrowserTest() override = default;

  void SetUp() override {
    ASSERT_TRUE(embedded_test_server()->InitializeAndListen());
    embedded_test_server()->RegisterRequestHandler(base::BindRepeating(
        &LoadingPredictorBrowserTest::HandleFaviconRequest));
    embedded_test_server()->RegisterRequestHandler(base::BindRepeating(
        &LoadingPredictorBrowserTest::HandleCacheRedirectRequest));

    ASSERT_TRUE(preconnecting_test_server_.InitializeAndListen());
    preconnecting_test_server_.RegisterRequestHandler(base::BindRepeating(
        &LoadingPredictorBrowserTest::HandleFaviconRequest));
    preconnecting_test_server_.AddDefaultHandlers(GetChromeTestDataDir());

    InProcessBrowserTest::SetUp();
  }

  void SetUpOnMainThread() override {
    host_resolver()->AddRule("*", "127.0.0.1");

    connection_tracker_ = std::make_unique<net::test_server::ConnectionTracker>(
        embedded_test_server());
    preconnecting_server_connection_tracker_ =
        std::make_unique<net::test_server::ConnectionTracker>(
            &preconnecting_test_server_);

    embedded_test_server()->StartAcceptingConnections();

    EXPECT_TRUE(preconnecting_test_server_.Started());
    preconnecting_test_server_.StartAcceptingConnections();

    loading_predictor_ =
        LoadingPredictorFactory::GetForProfile(browser()->GetProfile());
    ASSERT_TRUE(loading_predictor_);
    preconnect_manager_observer_ =
        std::make_unique<TestPreconnectManagerObserver>(
            loading_predictor_->preconnect_manager());
    if (base::FeatureList::IsEnabled(features::kLoadingPredictorPrefetch) ||
        base::FeatureList::IsEnabled(
            blink::features::kLCPPPrefetchSubresource)) {
      prefetch_manager_observer_ =
          std::make_unique<TestPrefetchManagerObserver>(
              *loading_predictor_->prefetch_manager());
    }
    PredictorInitializer initializer(
        loading_predictor_->resource_prefetch_predictor());
    initializer.EnsurePredictorInitialized();
  }

  void TearDownOnMainThread() override {
    ResetPredictorState();
    loading_predictor_ = nullptr;
  }

  // Navigates to an URL without blocking until the navigation finishes.
  // Returns an observer that can be used to wait for the navigation
  // completion.
  // This function creates a new tab for each navigation that allows multiple
  // simultaneous navigations and avoids triggering the reload behavior.
  std::unique_ptr<content::TestNavigationManager> NavigateToURLAsync(
      const GURL& url) {
    chrome::NewTab(browser(), NewTabTypes::kNoUserAction);
    content::WebContents* tab =
        browser()->GetTabStripModel()->GetActiveWebContents();
    DCHECK(tab);
    auto observer = std::make_unique<content::TestNavigationManager>(tab, url);
    tab->GetController().LoadURL(url, content::Referrer(),
                                 ui::PAGE_TRANSITION_TYPED, std::string());
    return observer;
  }

  void ResetNetworkState() {
    auto* network_context = browser()
                                ->GetProfile()
                                ->GetDefaultStoragePartition()
                                ->GetNetworkContext();
    base::RunLoop clear_host_cache_loop;
    base::RunLoop close_all_connections_loop;
    network_context->ClearHostCache(nullptr,
                                    clear_host_cache_loop.QuitClosure());
    network_context->CloseAllConnections(
        close_all_connections_loop.QuitClosure());
    clear_host_cache_loop.Run();
    close_all_connections_loop.Run();

    connection_tracker()->ResetCounts();
    preconnecting_server_connection_tracker_->ResetCounts();
  }

  void ResetPredictorState() {
    loading_predictor_->resource_prefetch_predictor()->DeleteAllUrls();
  }

  std::unique_ptr<PreconnectPrediction> GetPreconnectPrediction(
      const GURL& url) {
    auto prediction = std::make_unique<PreconnectPrediction>();
    bool has_prediction = loading_predictor_->resource_prefetch_predictor()
                              ->PredictPreconnectOrigins(url, prediction.get());
    if (!has_prediction) {
      return nullptr;
    }
    return prediction;
  }

  LoadingPredictor* loading_predictor() { return loading_predictor_; }

  TestPreconnectManagerObserver* preconnect_manager_observer() {
    return preconnect_manager_observer_.get();
  }

  TestPrefetchManagerObserver* prefetch_manager_observer() {
    return prefetch_manager_observer_.get();
  }

  net::test_server::ConnectionTracker* connection_tracker() {
    return connection_tracker_.get();
  }

  net::test_server::ConnectionTracker*
  preconnecting_server_connection_tracker() {
    return preconnecting_server_connection_tracker_.get();
  }

  static std::unique_ptr<net::test_server::HttpResponse> HandleFaviconRequest(
      const net::test_server::HttpRequest& request) {
    if (request.relative_url != "/favicon.ico") {
      return nullptr;
    }

    auto http_response =
        std::make_unique<net::test_server::BasicHttpResponse>();
    http_response->set_code(net::HTTP_OK);
    http_response->AddCustomHeader("Cache-Control", "max-age=6000");
    return http_response;
  }

  static std::unique_ptr<net::test_server::HttpResponse>
  HandleCacheRedirectRequest(const net::test_server::HttpRequest& request) {
    if (!base::StartsWith(request.relative_url, "/cached-redirect?",
                          base::CompareCase::INSENSITIVE_ASCII)) {
      return nullptr;
    }

    GURL request_url = request.GetURL();
    std::string dest = base::UnescapeBinaryURLComponent(request_url.query());

    auto http_response =
        std::make_unique<net::test_server::BasicHttpResponse>();
    http_response->set_code(net::HTTP_MOVED_PERMANENTLY);
    http_response->AddCustomHeader("Location", dest);
    http_response->set_content_type("text/html");
    http_response->set_content(base::StringPrintf(
        "<html><head></head><body>Redirecting to %s</body></html>",
        dest.c_str()));
    http_response->AddCustomHeader("Cache-Control", "max-age=6000");
    return http_response;
  }

 protected:
  // Test server that initiates preconnect. Separate server from the one being
  // preconnected to separate preconnected connection count.
  net::EmbeddedTestServer preconnecting_test_server_;

 private:
  raw_ptr<LoadingPredictor> loading_predictor_ = nullptr;
  std::unique_ptr<net::test_server::ConnectionTracker> connection_tracker_;
  std::unique_ptr<net::test_server::ConnectionTracker>
      preconnecting_server_connection_tracker_;
  std::unique_ptr<TestPreconnectManagerObserver> preconnect_manager_observer_;
  std::unique_ptr<TestPrefetchManagerObserver> prefetch_manager_observer_;
  base::test::ScopedFeatureList scoped_feature_list_;
};

// Tests that a navigation triggers the LoadingPredictor.
IN_PROC_BROWSER_TEST_F(LoadingPredictorBrowserTest, SimpleNavigation) {
  GURL url = embedded_test_server()->GetURL("/nocontent");
  auto observer = NavigateToURLAsync(url);
  EXPECT_TRUE(observer->WaitForRequestStart());
  EXPECT_EQ(1u, loading_predictor()->GetActiveNavigationsSizeForTesting());
  // Checking GetActiveHintsSizeForTesting() is racy since the active hint
  // is removed after the preconnect finishes. Instead check for total
  // hints activated.
  EXPECT_LE(1u, loading_predictor()->GetTotalHintsActivatedForTesting());
  EXPECT_GE(2u, loading_predictor()->GetTotalHintsActivatedForTesting());
  ASSERT_TRUE(observer->WaitForNavigationFinished());
  EXPECT_EQ(0u, loading_predictor()->GetActiveNavigationsSizeForTesting());
  EXPECT_EQ(0u, loading_predictor()->GetActiveHintsSizeForTesting());
  EXPECT_LE(1u, loading_predictor()->GetTotalHintsActivatedForTesting());
  EXPECT_GE(2u, loading_predictor()->GetTotalHintsActivatedForTesting());
}

// Tests that two concurrenct navigations are recorded correctly by the
// predictor.
IN_PROC_BROWSER_TEST_F(LoadingPredictorBrowserTest, TwoConcurrentNavigations) {
  GURL url1 = embedded_test_server()->GetURL("/echo-raw?1");
  GURL url2 = embedded_test_server()->GetURL("/echo-raw?2");
  auto observer1 = NavigateToURLAsync(url1);
  auto observer2 = NavigateToURLAsync(url2);
  EXPECT_TRUE(observer1->WaitForRequestStart());
  EXPECT_TRUE(observer2->WaitForRequestStart());
  EXPECT_EQ(2u, loading_predictor()->GetActiveNavigationsSizeForTesting());
  // Checking GetActiveHintsSizeForTesting() is racy since the active hint
  // is removed after the preconnect finishes. Instead check for total
  // hints activated.
  EXPECT_LE(2u, loading_predictor()->GetTotalHintsActivatedForTesting());
  EXPECT_GE(4u, loading_predictor()->GetTotalHintsActivatedForTesting());
  ASSERT_TRUE(observer1->WaitForNavigationFinished());
  ASSERT_TRUE(observer2->WaitForNavigationFinished());
  EXPECT_EQ(0u, loading_predictor()->GetActiveNavigationsSizeForTesting());
  EXPECT_EQ(0u, loading_predictor()->GetActiveHintsSizeForTesting());
  EXPECT_LE(2u, loading_predictor()->GetTotalHintsActivatedForTesting());
  EXPECT_GE(4u, loading_predictor()->GetTotalHintsActivatedForTesting());
}

// Tests that two navigations to the same URL are deduplicated.
IN_PROC_BROWSER_TEST_F(LoadingPredictorBrowserTest,
                       TwoNavigationsToTheSameURL) {
  GURL url = embedded_test_server()->GetURL("/nocontent");
  auto observer1 = NavigateToURLAsync(url);
  auto observer2 = NavigateToURLAsync(url);
  EXPECT_TRUE(observer1->WaitForRequestStart());
  EXPECT_TRUE(observer2->WaitForRequestStart());
  EXPECT_EQ(2u, loading_predictor()->GetActiveNavigationsSizeForTesting());
  // Checking GetActiveHintsSizeForTesting() is racy since the active hint
  // is removed after the preconnect finishes. Instead check for total
  // hints activated. The total hints activated may be only 1 if the second
  // navigation arrives before the first preconnect finishes. However, if the
  // second navigation arrives later, then two hints may get activated.
  EXPECT_LE(1u, loading_predictor()->GetTotalHintsActivatedForTesting());
  EXPECT_GE(4u, loading_predictor()->GetTotalHintsActivatedForTesting());
  ASSERT_TRUE(observer1->WaitForNavigationFinished());
  ASSERT_TRUE(observer2->WaitForNavigationFinished());
  EXPECT_EQ(0u, loading_predictor()->GetActiveNavigationsSizeForTesting());
  EXPECT_EQ(0u, loading_predictor()->GetActiveHintsSizeForTesting());
  EXPECT_LE(1u, loading_predictor()->GetTotalHintsActivatedForTesting());
  EXPECT_GE(4u, loading_predictor()->GetTotalHintsActivatedForTesting());
}

// Tests that the LoadingPredictor doesn't record non-http(s) navigations.
IN_PROC_BROWSER_TEST_F(LoadingPredictorBrowserTest, NonHttpNavigation) {
  std::string content = "<body>Hello world!</body>";
  GURL url = GetDataURLWithContent(content);
  auto observer = NavigateToURLAsync(url);
  EXPECT_TRUE(observer->WaitForRequestStart());
  EXPECT_EQ(0u, loading_predictor()->GetActiveNavigationsSizeForTesting());
  EXPECT_EQ(0u, loading_predictor()->GetActiveHintsSizeForTesting());
}

// Tests that the LoadingPredictor doesn't preconnect to non-http(s) urls.
IN_PROC_BROWSER_TEST_F(LoadingPredictorBrowserTest,
                       PrepareForPageLoadNonHttpScheme) {
  std::string content = "<body>Hello world!</body>";
  GURL url = GetDataURLWithContent(content);
  ASSERT_TRUE(ui_test_utils::NavigateToURL(browser(), url));
  net::SchemefulSite site = net::SchemefulSite(url);
  auto network_anonymization_key =
      net::NetworkAnonymizationKey ::CreateSameSite(std::move(site));
  // Ensure that no backgound task would make a host lookup or attempt to
  // preconnect.
  base::RunLoop().RunUntilIdle();
  EXPECT_FALSE(preconnect_manager_observer()->HasHostBeenLookedUp(
      url.GetHost(), network_anonymization_key));
  EXPECT_FALSE(preconnect_manager_observer()->HasHostBeenLookedUp(
      "", network_anonymization_key));
  EXPECT_FALSE(preconnect_manager_observer()->HasOriginAttemptedToPreconnect(
      url.DeprecatedGetOriginAsURL()));
  EXPECT_FALSE(
      preconnect_manager_observer()->HasOriginAttemptedToPreconnect(GURL()));
}

namespace {
class TestPrerenderStopObserver
    : public prerender::NoStatePrefetchHandle::Observer {
 public:
  explicit TestPrerenderStopObserver(base::OnceClosure on_stop_closure)
      : on_stop_closure_(std::move(on_stop_closure)) {}
  ~TestPrerenderStopObserver() override = default;

  void OnPrefetchStop(prerender::NoStatePrefetchHandle* contents) override {
    if (on_stop_closure_) {
      std::move(on_stop_closure_).Run();
    }
  }

 private:
  base::OnceClosure on_stop_closure_;
};
}  // namespace

// Tests that the LoadingPredictor preconnects to the main frame origin even if
// it doesn't have any prediction for this origin.
IN_PROC_BROWSER_TEST_F(LoadingPredictorBrowserTest,
                       PrepareForPageLoadWithoutPrediction) {
  // Navigate the first time to fill the HTTP cache.
  GURL url = embedded_test_server()->GetURL(
      "test.com", GetPathWithPortReplacement(kHtmlSubresourcesPath,
                                             embedded_test_server()->port()));
  net::SchemefulSite site = net::SchemefulSite(url);
  auto network_anonymization_key =
      net::NetworkAnonymizationKey ::CreateSameSite(std::move(site));
  ASSERT_TRUE(ui_test_utils::NavigateToURL(browser(), url));
  ResetNetworkState();
  ResetPredictorState();

  auto observer = NavigateToURLAsync(url);
  EXPECT_TRUE(observer->WaitForRequestStart());
  preconnect_manager_observer()->WaitUntilHostLookedUp(
      url.GetHost(), network_anonymization_key);
  EXPECT_TRUE(preconnect_manager_observer()->HostFound(
      url.GetHost(), network_anonymization_key));
  // We should preconnect only 2 sockets for the main frame host.
  const size_t expected_connections = 2;
  connection_tracker()->WaitForAcceptedConnections(expected_connections);
  EXPECT_EQ(expected_connections,
            connection_tracker()->GetAcceptedSocketCount());
  // No reads since all resources should be cached.
  EXPECT_EQ(0u, connection_tracker()->GetReadSocketCount());
}

// Tests that the LoadingPredictor has a prediction for a host after navigating
// to it.
IN_PROC_BROWSER_TEST_F(LoadingPredictorBrowserTest, LearnFromNavigation) {
  GURL url = embedded_test_server()->GetURL(
      "test.com", GetPathWithPortReplacement(kHtmlSubresourcesPath,
                                             embedded_test_server()->port()));
  net::SchemefulSite site = net::SchemefulSite(url);
  std::vector<PreconnectRequest> requests;
  for (auto* const host : kHtmlSubresourcesHosts) {
    requests.emplace_back(
        url::Origin::Create(embedded_test_server()->GetURL(host, "/")), 1,
        net::NetworkAnonymizationKey::CreateSameSite(site));
  }

  ASSERT_TRUE(ui_test_utils::NavigateToURL(browser(), url));
  auto prediction = GetPreconnectPrediction(url);
  ASSERT_TRUE(prediction);
  EXPECT_EQ(prediction->is_redirected, false);
  EXPECT_EQ(prediction->host, url.GetHost());
  EXPECT_THAT(prediction->requests,
              testing::UnorderedElementsAreArray(requests));
}

class LoadingPredictorBrowserTestLearnAllResources
    : public LoadingPredictorBrowserTest {
 public:
  LoadingPredictorBrowserTestLearnAllResources() {
    feature_list_.InitAndDisableFeature(
        features::kLoadingOnlyLearnHighPriorityResources);
  }

 private:
  base::test::ScopedFeatureList feature_list_;
};

// Tests that the LoadingPredictor has a prediction for a host after navigating
// to it. Disables kLoadingOnlyLearnHighPriorityResources.
IN_PROC_BROWSER_TEST_F(LoadingPredictorBrowserTestLearnAllResources,
                       LearnFromNavigation) {
  GURL url = embedded_test_server()->GetURL(
      "test.com", GetPathWithPortReplacement(kHtmlSubresourcesPath,
                                             embedded_test_server()->port()));
  net::SchemefulSite site = net::SchemefulSite(url);
  std::vector<PreconnectRequest> requests;
  for (auto* const host : kHtmlSubresourcesHosts) {
    requests.emplace_back(
        url::Origin::Create(embedded_test_server()->GetURL(host, "/")), 1,
        net::NetworkAnonymizationKey::CreateSameSite(site));
  }

  // When kLoadingOnlyLearnHighPriorityResources is disabled, loading data
  // collector should learn the loading of low priority resources hosted on
  // bar.com as well.
  requests.emplace_back(
      url::Origin::Create(embedded_test_server()->GetURL("bar.com", "/")), 1,
      net::NetworkAnonymizationKey::CreateSameSite(site));

  ASSERT_TRUE(ui_test_utils::NavigateToURL(browser(), url));
  auto prediction = GetPreconnectPrediction(url);
  ASSERT_TRUE(prediction);
  EXPECT_EQ(prediction->is_redirected, false);
  EXPECT_EQ(prediction->host, url.GetHost());
  EXPECT_THAT(prediction->requests,
              testing::UnorderedElementsAreArray(requests));
}

// Tests that the LoadingPredictor correctly learns from navigations with
// redirect.
IN_PROC_BROWSER_TEST_F(LoadingPredictorBrowserTest,
                       LearnFromNavigationWithRedirect) {
  GURL redirect_url = embedded_test_server()->GetURL(
      "test.com", GetPathWithPortReplacement(kHtmlSubresourcesPath,
                                             embedded_test_server()->port()));
  GURL original_url = embedded_test_server()->GetURL(
      "redirect.com",
      base::StringPrintf("/server-redirect?%s", redirect_url.spec().c_str()));
  url::Origin origin = url::Origin::Create(redirect_url);
  net::SchemefulSite site = net::SchemefulSite(origin);
  std::vector<PreconnectRequest> expected_requests;
  for (auto* const host : kHtmlSubresourcesHosts) {
    expected_requests.emplace_back(
        url::Origin::Create(embedded_test_server()->GetURL(host, "/")), 1,
        net::NetworkAnonymizationKey::CreateSameSite(site));
  }

  ASSERT_TRUE(ui_test_utils::NavigateToURL(browser(), original_url));
  // The predictor can correctly predict hosts by |redirect_url|
  auto prediction = GetPreconnectPrediction(redirect_url);
  ASSERT_TRUE(prediction);
  EXPECT_EQ(prediction->is_redirected, false);
  EXPECT_EQ(prediction->host, redirect_url.GetHost());
  EXPECT_THAT(prediction->requests,
              testing::UnorderedElementsAreArray(expected_requests));
  // The predictor needs minimum two redirect hits to be confident in the
  // redirect to generate the origins for subresources. However, after the
  // first redirect, the predictor should learn the redirect origin for
  // preconnect.
  prediction = GetPreconnectPrediction(original_url);
  ASSERT_TRUE(prediction);
  EXPECT_FALSE(prediction->is_redirected);
  EXPECT_EQ(prediction->host, original_url.GetHost());
  std::vector<PreconnectRequest> expected_requests_1;
  url::Origin redirect_origin = url::Origin::Create(
      embedded_test_server()->GetURL(redirect_url.GetHost(), "/"));
  expected_requests_1.emplace_back(
      redirect_origin, 1, net::NetworkAnonymizationKey::CreateSameSite(site));
  EXPECT_THAT(prediction->requests,
              testing::UnorderedElementsAreArray(expected_requests_1));

  // The predictor will start predict for origins of subresources (based on
  // redirect) after the second navigation.
  ASSERT_TRUE(ui_test_utils::NavigateToURL(browser(), original_url));
  prediction = GetPreconnectPrediction(original_url);
  expected_requests.emplace_back(
      redirect_origin, 1, net::NetworkAnonymizationKey::CreateSameSite(site));
  ASSERT_TRUE(prediction);
  EXPECT_EQ(prediction->is_redirected, true);
  EXPECT_EQ(prediction->host, redirect_url.GetHost());
  EXPECT_THAT(prediction->requests,
              testing::UnorderedElementsAreArray(expected_requests));
}

// Tests that the LoadingPredictor performs preresolving/preconnecting for a
// navigation which it has a prediction for.
IN_PROC_BROWSER_TEST_F(LoadingPredictorBrowserTest,
                       PrepareForPageLoadWithPrediction) {
  // Navigate the first time to fill the predictor's database and the HTTP
  // cache.
  GURL url = embedded_test_server()->GetURL(
      "test.com", GetPathWithPortReplacement(kHtmlSubresourcesPath,
                                             embedded_test_server()->port()));
  net::SchemefulSite site = net::SchemefulSite(url);
  auto network_anonymization_key =
      net::NetworkAnonymizationKey::CreateSameSite(std::move(site));
  ASSERT_TRUE(ui_test_utils::NavigateToURL(browser(), url));
  ResetNetworkState();

  auto observer = NavigateToURLAsync(url);
  EXPECT_TRUE(observer->WaitForRequestStart());
  for (auto* const host : kHtmlSubresourcesHosts) {
    GURL host_url(base::StringPrintf("http://%s", host));
    preconnect_manager_observer()->WaitUntilHostLookedUp(
        host_url.GetHost(), network_anonymization_key);
    EXPECT_TRUE(preconnect_manager_observer()->HostFound(
        host_url.GetHost(), network_anonymization_key));
  }
  // 2 connections to the main frame host + 1 connection per host for others.
  const size_t expected_connections = std::size(kHtmlSubresourcesHosts) + 1;
  connection_tracker()->WaitForAcceptedConnections(expected_connections);
  EXPECT_EQ(expected_connections,
            connection_tracker()->GetAcceptedSocketCount());
  // No reads since all resources should be cached.
  EXPECT_EQ(0u, connection_tracker()->GetReadSocketCount());
}

// Tests that a host requested by <link rel="dns-prefetch"> is looked up.
IN_PROC_BROWSER_TEST_F(LoadingPredictorBrowserTest, DnsPrefetch) {
  GURL url = embedded_test_server()->GetURL("/predictor/dns_prefetch.html");
  net::SchemefulSite site = net::SchemefulSite(url);
  auto network_anonymization_key =
      net::NetworkAnonymizationKey::CreateSameSite(std::move(site));
  ASSERT_TRUE(ui_test_utils::NavigateToURL(browser(), url));
  preconnect_manager_observer()->WaitUntilHostLookedUp(
      GURL(kChromiumUrl).GetHost(), network_anonymization_key);
  EXPECT_TRUE(preconnect_manager_observer()->HostFound(
      GURL(kChromiumUrl).GetHost(), network_anonymization_key));
}

// Tests that preconnect warms up a socket connection to a test server.
// Note: This test uses a data URI to serve the preconnect hint, to make sure
// that the network stack doesn't just re-use its connection to the test server.
IN_PROC_BROWSER_TEST_F(LoadingPredictorBrowserTest, PreconnectNonCors) {
  GURL preconnect_url = embedded_test_server()->base_url();
  std::string preconnect_content =
      "<link rel=\"preconnect\" href=\"" + preconnect_url.spec() + "\">";
  ASSERT_TRUE(ui_test_utils::NavigateToURL(
      browser(), GetDataURLWithContent(preconnect_content)));
  connection_tracker()->WaitForAcceptedConnections(1u);
  EXPECT_EQ(1u, connection_tracker()->GetAcceptedSocketCount());
  EXPECT_EQ(0u, connection_tracker()->GetReadSocketCount());
}

class LCPPBrowserTestBase : public LoadingPredictorBrowserTest {
 public:
  void SetUp() override {
    embedded_https_test_server().SetCertHostnames(
        {"a.test", "p.com", "q.com", "exclude.test", "exclude2.test"});
    embedded_https_test_server().RegisterRequestHandler(base::BindRepeating(
        &LoadingPredictorBrowserTest::HandleFaviconRequest));
    LoadingPredictorBrowserTest::SetUp();
  }

  void SetUpOnMainThread() override {
    LoadingPredictorBrowserTest::SetUpOnMainThread();
    ASSERT_TRUE(embedded_https_test_server().Start());
  }

  void NavigateAndWaitForLcpElement(
      const GURL& url,
      const base::Location& from_here = FROM_HERE) {
    LcpElementLearnWaiter lcp_element_waiter(
        loading_predictor()->resource_prefetch_predictor());
    page_load_metrics::PageLoadMetricsTestWaiter waiter(
        browser()->GetTabStripModel()->GetActiveWebContents());
    waiter.AddPageExpectation(page_load_metrics::PageLoadMetricsTestWaiter::
                                  TimingField::kLargestContentfulPaint);
    ASSERT_TRUE(ui_test_utils::NavigateToURL(browser(), url))
        << from_here.ToString();
    waiter.Wait();
    // Navigate to about:blank to force recording a LCP element.
    ASSERT_TRUE(ui_test_utils::NavigateToURL(browser(), GURL("about:blank")))
        << from_here.ToString();
    lcp_element_waiter.Wait();
  }
};

// TODO(crbug.com/40063266): isolate test per feature.  Currently, it has
// test for script observer and fonts.
class LCPCriticalPathPredictorBrowserTest : public LCPPBrowserTestBase {
 public:
  LCPCriticalPathPredictorBrowserTest() {
    scoped_feature_list_.InitWithFeaturesAndParameters(
        {{blink::features::kLCPCriticalPathPredictor, {}},
         {blink::features::kLCPPFontURLPredictor,
          {{blink::features::kLCPPFontURLPredictorExcludedHosts.name,
            "exclude.test,exclude2.test"}}}},
        {});
  }

  std::vector<std::string> ExpectLcpElementLocatorsPrediction(
      const base::Location& from_here,
      const GURL& url,
      size_t expected_locator_count) {
    auto lcpp_stat =
        loading_predictor()->resource_prefetch_predictor()->GetLcppStat(
            /*initiator_origin=*/std::nullopt, url);
    std::vector<std::string> locators;
    if (lcpp_stat) {
      std::optional<blink::mojom::LCPCriticalPathPredictorNavigationTimeHint>
          hint = ConvertLcppStatToLCPCriticalPathPredictorNavigationTimeHint(
              *lcpp_stat);
      if (hint) {
        locators = hint->lcp_element_locators;
      }
    }
    EXPECT_EQ(expected_locator_count, locators.size()) << from_here.ToString();
    return locators;
  }


  std::vector<std::string> GetLCPPFonts(const GURL& url) {
    auto lcpp_stat =
        loading_predictor()->resource_prefetch_predictor()->GetLcppStat(
            /*initiator_origin=*/std::nullopt, url);
    if (!lcpp_stat) {
      return std::vector<std::string>();
    }
    std::vector<std::string> fonts;
    for (const auto& it : lcpp_stat->fetched_font_url_stat().main_buckets()) {
      fonts.push_back(it.first);
    }
    return fonts;
  }

 private:
  base::test::ScopedFeatureList scoped_feature_list_;
};

// Tests that the LoadingPredictor has a LCP critical path predictor
// (LCPP) prediction after navigating to it.
// LCPP:
// https://docs.google.com/document/d/18qTNRyv_9K2CtvVrl_ancLzPxiAnfAcbvrCNegU9IBM
// LCP: https://web.dev/lcp/
IN_PROC_BROWSER_TEST_F(LCPCriticalPathPredictorBrowserTest,
                       LearnLCPPFromNavigation) {
  const GURL kUrlA = embedded_https_test_server().GetURL(
      "p.com", "/predictors/load_image_a.html");
  const GURL kUrlB = embedded_https_test_server().GetURL(
      "p.com", "/predictors/load_image_b.html");
  const GURL kUrlC = embedded_https_test_server().GetURL(
      "q.com", "/predictors/load_image_a.html");

  // There is no knowledge in the beginning.
  ExpectLcpElementLocatorsPrediction(FROM_HERE, kUrlA,
                                     /*expected_locator_count=*/0);
  ExpectLcpElementLocatorsPrediction(FROM_HERE, kUrlB,
                                     /*expected_locator_count=*/0);
  ExpectLcpElementLocatorsPrediction(FROM_HERE, kUrlC,
                                     /*expected_locator_count=*/0);

  NavigateAndWaitForLcpElement(kUrlA);
  // The locators should contain [lcp_element_for_a].
  std::vector<std::string> locators_1 =
      ExpectLcpElementLocatorsPrediction(FROM_HERE, kUrlA,
                                         /*expected_locator_count=*/1);
  std::vector<std::string> locators_2 =
      ExpectLcpElementLocatorsPrediction(FROM_HERE, kUrlB,
                                         /*expected_locator_count=*/1);
  EXPECT_EQ(locators_1, locators_2);
  // The locator is encoded in a binary form. So storing the locator for a LCP
  // node for kUrlA to use later validation.
  const std::string& locator_for_a = locators_2[0];
  ExpectLcpElementLocatorsPrediction(FROM_HERE, kUrlC,
                                     /*expected_locator_count=*/0);

  NavigateAndWaitForLcpElement(kUrlB);
  // The locators should contain [lcp_element_for_a, lcp_element_for_b].
  std::vector<std::string> locators_3 =
      ExpectLcpElementLocatorsPrediction(FROM_HERE, kUrlA,
                                         /*expected_locator_count=*/2);
  std::vector<std::string> locators_4 =
      ExpectLcpElementLocatorsPrediction(FROM_HERE, kUrlB,
                                         /*expected_locator_count=*/2);
  EXPECT_EQ(locators_3, locators_4);
  ExpectLcpElementLocatorsPrediction(FROM_HERE, kUrlC,
                                     /*expected_locator_count=*/0);

  NavigateAndWaitForLcpElement(kUrlB);
  std::vector<std::string> locators_5 =
      ExpectLcpElementLocatorsPrediction(FROM_HERE, kUrlA,
                                         /*expected_locator_count=*/2);
  std::vector<std::string> locators_6 =
      ExpectLcpElementLocatorsPrediction(FROM_HERE, kUrlB,
                                         /*expected_locator_count=*/2);
  // The locators should contain [lcp_element_for_b, lcp_element_for_a].
  // lcp_element_for_b must come first because we navigated to kUrlB twice.
  EXPECT_EQ(locators_5, locators_6);
  ExpectLcpElementLocatorsPrediction(FROM_HERE, kUrlC,
                                     /*expected_locator_count=*/0);
  EXPECT_EQ(locator_for_a, locators_6[1]);
}

IN_PROC_BROWSER_TEST_F(LCPCriticalPathPredictorBrowserTest, LearnLCPPFont) {
  const GURL kUrlA = embedded_https_test_server().GetURL(
      "p.com", "/predictors/lcpp_font.html");
  const GURL kFontUrlA =
      embedded_https_test_server().GetURL("p.com", "/predictors/font.ttf");
  const GURL kUrlB = embedded_https_test_server().GetURL(
      "exclude.test", "/predictors/lcpp_font.html");
  const GURL kUrlC = embedded_https_test_server().GetURL(
      "exclude2.test", "/predictors/lcpp_font.html");

  EXPECT_EQ(std::vector<std::string>(), GetLCPPFonts(kUrlA));
  EXPECT_EQ(std::vector<std::string>(), GetLCPPFonts(kUrlB));

  std::vector<std::string> expected;
  expected.push_back(kFontUrlA.spec());
  NavigateAndWaitForLcpElement(kUrlA);
  EXPECT_EQ(expected, GetLCPPFonts(kUrlA));
  EXPECT_EQ(std::vector<std::string>(), GetLCPPFonts(kUrlB));
  EXPECT_EQ(std::vector<std::string>(), GetLCPPFonts(kUrlC));

  NavigateAndWaitForLcpElement(kUrlB);
  EXPECT_EQ(expected, GetLCPPFonts(kUrlA));
  EXPECT_EQ(std::vector<std::string>(), GetLCPPFonts(kUrlB));
  EXPECT_EQ(std::vector<std::string>(), GetLCPPFonts(kUrlC));

  NavigateAndWaitForLcpElement(kUrlC);
  EXPECT_EQ(expected, GetLCPPFonts(kUrlA));
  EXPECT_EQ(std::vector<std::string>(), GetLCPPFonts(kUrlB));
  EXPECT_EQ(std::vector<std::string>(), GetLCPPFonts(kUrlC));
}

class LCPPPrefetchSubresourceTest : public LCPPBrowserTestBase {
 public:
  LCPPPrefetchSubresourceTest() {
    scoped_feature_list_.InitWithFeatures(
        /*enabled_features=*/{blink::features::kLCPPPrefetchSubresource},
        /*disabled_features=*/{features::kLoadingPredictorPrefetch,
                               blink::features::kHttpDiskCachePrewarming});
  }

  void SetUpCommandLine(base::CommandLine* command_line) override {
    LCPPBrowserTestBase::SetUpCommandLine(command_line);
    command_line->AppendSwitch(
        switches::kLoadingPredictorAllowLocalRequestForTesting);
  }

  GURL GetURL(const std::string& path) {
    return embedded_https_test_server().GetURL("a.test", path);
  }

 private:
  base::test::ScopedFeatureList scoped_feature_list_;
};

// Tests that prefetch uses LCP-learning subresources.
IN_PROC_BROWSER_TEST_F(LCPPPrefetchSubresourceTest, Base) {
  CHECK(
      base::FeatureList::IsEnabled(blink::features::kLCPPPrefetchSubresource));
  CHECK(!base::FeatureList::IsEnabled(features::kLoadingPredictorPrefetch));
  CHECK(
      !base::FeatureList::IsEnabled(blink::features::kHttpDiskCachePrewarming));
  const GURL url = GetURL("/predictors/preload.html");
  NavigateAndWaitForLcpElement(url);
  EXPECT_TRUE(prefetch_manager_observer()->results().empty());
  auto lcpp_stat =
      loading_predictor()->resource_prefetch_predictor()->GetLcppStat(
          /*initiator_origin=*/std::nullopt, url);
  EXPECT_TRUE(lcpp_stat.has_value());
  const auto& subresource_urls = PredictFetchedSubresourceUrls(*lcpp_stat);
  // Check LCP has learnt the subresources.
  EXPECT_EQ(std::set<GURL>({GetURL("/predictors/red_rectangle.png"),
                            GetURL("/predictors/style.css"),
                            GetURL("/predictors/script.js"),
                            GetURL("/predictors/font.ttf")}),
            std::set<GURL>(subresource_urls.begin(), subresource_urls.end()));

  EXPECT_TRUE(prefetch_manager_observer()->results().empty());
  ASSERT_TRUE(ui_test_utils::NavigateToURL(browser(), url));
  prefetch_manager_observer()->WaitForPrefetchesForNavigation(url);
  const auto results = prefetch_manager_observer()->results();
  std::set<GURL> result_urls;
  for (const auto& result : results) {
    EXPECT_EQ(net::OK, result.status.error_code) << result.prefetch_url;
    result_urls.insert(result.prefetch_url);
  }
  // font.tts prefetch is handled by kLCPPFontURLPredictor feature.
  EXPECT_EQ(std::set<GURL>({GetURL("/predictors/style.css"),
                            GetURL("/predictors/script.js")}),
            result_urls);
}

IN_PROC_BROWSER_TEST_F(LCPPPrefetchSubresourceTest, UMA) {
  NavigateAndWaitForLcpElement(GetURL("/predictors/preload.html"));

  base::HistogramTester histogram_tester;
  NavigateAndWaitForLcpElement(GetURL("/predictors/preload2.html"));
  // predicted subresources:
  //   style.css
  //   script.js
  //   font.ttf
  //   red_rectangle.png
  // actual subresources:
  //   style.css
  //   script.js
  //   red_rectangle.png
  //   red_rectangle2.png
  //   red_rectangle3.png
  // hit = 3
  // precision = 3/4 = 75%
  // recall    = 3/5 = 60%
  EXPECT_THAT(
      histogram_tester.GetAllSamples(
          ::internal::kHistogramLCPPSubresourceCountType),
      base::BucketsAre(
          base::Bucket(network::mojom::RequestDestination::kImage, 3),
          base::Bucket(network::mojom::RequestDestination::kStyle, 1),
          base::Bucket(network::mojom::RequestDestination::kScript, 1)));
  histogram_tester.ExpectUniqueSample(
      ::internal::kHistogramLCPPSubresourceCountPrecision, 75, 1);
  histogram_tester.ExpectUniqueSample(
      ::internal::kHistogramLCPPSubresourceCountRecall, 60, 1);
  histogram_tester.ExpectUniqueSample(
      ::internal::kHistogramLCPPSubresourceCountSameSiteRatio, 100, 1);
}

class LCPPTimingPredictorTestBase : public InProcessBrowserTest {
 public:
  ~LCPPTimingPredictorTestBase() override = default;

  void SetUpOnMainThread() override {
    host_resolver()->AddRule("*", "127.0.0.1");

    embedded_https_test_server().SetCertHostnames({"a.test"});
    slow_response_manager_ =
        std::make_unique<net::test_server::ControllableHttpResponseManager>(
            &embedded_https_test_server(), "/image_slow.png");
    ASSERT_TRUE(embedded_https_test_server().Start());

    loading_predictor_ =
        LoadingPredictorFactory::GetForProfile(browser()->GetProfile());
    ASSERT_TRUE(loading_predictor_);
    loading_predictor_->EnableLCPPTesting();
    PredictorInitializer initializer(
        loading_predictor_->resource_prefetch_predictor());
    initializer.EnsurePredictorInitialized();
  }

  void TearDownOnMainThread() override { loading_predictor_ = nullptr; }

  void NavigateAndWaitForLcpElement(
      const GURL& url,
      const std::string& expected_events,
      size_t expected_lcp_count,
      const std::optional<size_t>& expected_lcp_index,
      const base::Location& from_here = FROM_HERE) {
    content::WebContents* web_contents =
        browser()->GetTabStripModel()->GetActiveWebContents();

    page_load_metrics::PageLoadMetricsTestWaiter onload_waiter(web_contents);
    onload_waiter.AddPageExpectation(
        page_load_metrics::PageLoadMetricsTestWaiter::TimingField::kLoadEvent);

    LcpTimingPredictedWaiter timing_predicted_waiter(
        loading_predictor()->resource_prefetch_predictor());
    LcpUpdatedWaiter lcp_updated_waiter(
        loading_predictor()->resource_prefetch_predictor(), expected_lcp_count);

    content::NavigationController::LoadURLParams params(url);
    web_contents->GetController().LoadURLWithParams(params);

    const std::vector<std::optional<std::string>>& lcp_element_locators =
        lcp_updated_waiter.Wait();

    std::unique_ptr<net::test_server::ControllableHttpResponse> slow_response =
        slow_response_manager_->WaitForRequest();
    slow_response->Send(net::HTTP_OK, "image/png", "image_body", /*cookies=*/{},
                        {"Cache-Control: no-store"});
    slow_response->Done();

    onload_waiter.Wait();

    const std::optional<std::string>& predicted_lcp_locator =
        timing_predicted_waiter.Wait();
    EXPECT_EQ(predicted_lcp_locator.has_value(),
              expected_lcp_index.has_value());
    if (expected_lcp_index) {
      EXPECT_EQ(*lcp_element_locators[*expected_lcp_index],
                *predicted_lcp_locator);
    }

    std::string actual_events = content::EvalJs(web_contents, R"(
      globalThis.events.join(", ")
    )")
                                    .ExtractString();

    std::vector<std::string> expected_vec = base::SplitString(
        expected_events, ",", base::TRIM_WHITESPACE, base::SPLIT_WANT_NONEMPTY);
    std::vector<std::string> actual_vec = base::SplitString(
        actual_events, ",", base::TRIM_WHITESPACE, base::SPLIT_WANT_NONEMPTY);

    EXPECT_THAT(actual_vec, testing::UnorderedElementsAreArray(expected_vec))
        << "Expected events: " << expected_events
        << "\nActual events: " << actual_events << "\nTimings: "
        << content::EvalJs(web_contents, R"(
      globalThis.timings.join(", ")
    )")
               .ExtractString();

    LcpElementLearnWaiter lcp_element_waiter(
        loading_predictor()->resource_prefetch_predictor());
    EXPECT_TRUE(ui_test_utils::NavigateToURL(browser(), GURL("about:blank")))
        << from_here.ToString();
    lcp_element_waiter.Wait();
  }

  LoadingPredictor* loading_predictor() { return loading_predictor_; }

 private:
  raw_ptr<LoadingPredictor> loading_predictor_ = nullptr;
  std::unique_ptr<net::test_server::ControllableHttpResponseManager>
      slow_response_manager_;
};

class LCPPTimingPredictorBrowserTest : public LCPPTimingPredictorTestBase {
 public:
  LCPPTimingPredictorBrowserTest() {
    scoped_feature_list_.InitWithFeatures(
        /*enabled_features=*/
        {blink::features::kLCPTimingPredictorPrerender2},
        /*disabled_features=*/{});
  }

  void TestPrediction(const base::Location& from_here = FROM_HERE) {
    const GURL kUrl = embedded_https_test_server().GetURL(
        "a.test", "/predictors/lcp_occur_twice.html");

    NavigateAndWaitForLcpElement(kUrl,
                                 /*expected_events=*/"LCP@IMG, LCP@DIV, Onload",
                                 /*expected_lcp_count=*/2u,
                                 /*expected_lcp_index=*/std::nullopt,
                                 from_here);
    NavigateAndWaitForLcpElement(kUrl,
                                 /*expected_events=*/"LCP@IMG, LCP@DIV, Onload",
                                 /*expected_lcp_count=*/2u,
                                 /*expected_lcp_index=*/1u,  // =DIV
                                 from_here);
  }

 private:
  base::test::ScopedFeatureList scoped_feature_list_;
};

// Confirm image element of the first LCP is predicted rather than div, or the
// actual LCP(current implementation)
// TODO(crbug.com/413192370): Flaky on win-rel.
#if BUILDFLAG(IS_WIN)
#define MAYBE_Base DISABLED_Base
#else
#define MAYBE_Base Base
#endif
IN_PROC_BROWSER_TEST_F(LCPPTimingPredictorBrowserTest, MAYBE_Base) {
  TestPrediction();
}

class LCPPTimingPredictorBrowserFlagTest
    : public LCPPTimingPredictorBrowserTest,
      public ::testing::WithParamInterface<std::string> {
 public:
  LCPPTimingPredictorBrowserFlagTest() {
    scoped_feature_list_.InitWithFeaturesAndParameters(
        /*enabled_features=*/
        {{blink::features::kLCPCriticalPathPredictor,
          {{blink::features::kLCPCriticalPathPredictorRecordedLcpElementTypes
                .name,
            GetParam()}}}},
        /*disabled_features=*/{});
  }

 private:
  base::test::ScopedFeatureList scoped_feature_list_;
};

// kLCPCriticalPathPredictorRecordedLcpElementTypes should not affect.
// TODO(crbug.com/413192370): Flaky on win-rel.
#if BUILDFLAG(IS_WIN)
#define MAYBE_WithKCriticalPathFlag DISABLED_WithKCriticalPathFlag
#else
#define MAYBE_WithKCriticalPathFlag WithKCriticalPathFlag
#endif
IN_PROC_BROWSER_TEST_P(LCPPTimingPredictorBrowserFlagTest,
                       MAYBE_WithKCriticalPathFlag) {
  TestPrediction();
}

INSTANTIATE_TEST_SUITE_P(Flags,
                         LCPPTimingPredictorBrowserFlagTest,
                         ::testing::Values("all", "image_only"));

class LCPPAutoPreconnectTest : public InProcessBrowserTest,
                               public ::testing::WithParamInterface<bool> {
 public:
  LCPPAutoPreconnectTest() {
    scoped_feature_list_.InitWithFeaturesAndParameters(
        /*enabled_features=*/
        {{blink::features::kLCPPAutoPreconnectLcpOrigin,
          {{blink::features::kLCPPAutoPreconnectRecordAllOrigins.name,
            GetParam() ? "true" : "false"}}}},
        /*disabled_features=*/{});
  }

  ~LCPPAutoPreconnectTest() override = default;

  void SetUpOnMainThread() override {
    host_resolver()->AddRule("*", "127.0.0.1");

    embedded_https_test_server().SetCertHostnames(
        {"a.test", "foo.com", "bar.com"});
    ASSERT_TRUE(embedded_https_test_server().Start());

    loading_predictor_ =
        LoadingPredictorFactory::GetForProfile(browser()->GetProfile());
    ASSERT_TRUE(loading_predictor_);

    PredictorInitializer initializer(
        loading_predictor_->resource_prefetch_predictor());
    initializer.EnsurePredictorInitialized();

    preconnect_manager_observer_ =
        std::make_unique<TestPreconnectManagerObserver>(
            loading_predictor_->preconnect_manager());
  }

  void TearDownOnMainThread() override { loading_predictor_ = nullptr; }

  void NavigateAndWaitForLcpElement(
      const GURL& url,
      const base::Location& from_here = FROM_HERE) {
    content::WebContents* web_contents =
        browser()->GetTabStripModel()->GetActiveWebContents();

    page_load_metrics::PageLoadMetricsTestWaiter waiter(web_contents);
    waiter.AddMinimumLargestContentfulPaintImageExpectation(1);
    ASSERT_TRUE(ui_test_utils::NavigateToURL(browser(), url))
        << from_here.ToString();
    waiter.Wait();

    waiter.AddMinimumLargestContentfulPaintImageExpectation(1);
    EXPECT_TRUE(content::ExecJs(
        web_contents, base::StringPrintf(R"(
    const img_bar = document.getElementById("bar");
    img_bar.src = "https://bar.com:%d/predictors/lcp-100x50.png";
        )",
                                         embedded_https_test_server().port())));
    waiter.Wait();

    // Navigate to about:blank to force recording a LCP element.
    LcpElementLearnWaiter lcp_element_waiter(
        loading_predictor()->resource_prefetch_predictor());
    ASSERT_TRUE(ui_test_utils::NavigateToURL(browser(), GURL("about:blank")))
        << from_here.ToString();
    lcp_element_waiter.Wait();
  }

  std::vector<std::string> GetPreconnectedHosts() const {
    std::vector<std::string> hosts;
    for (auto& url :
         preconnect_manager_observer_->PreconnectUrlAttemptsHistory()) {
      hosts.push_back(url.GetHost());
    }
    return hosts;
  }

  LoadingPredictor* loading_predictor() { return loading_predictor_; }

 private:
  base::test::ScopedFeatureList scoped_feature_list_;
  raw_ptr<LoadingPredictor> loading_predictor_ = nullptr;
  std::unique_ptr<TestPreconnectManagerObserver> preconnect_manager_observer_;
};

// https://crbug.com/440185653: Test is flaky.
IN_PROC_BROWSER_TEST_P(LCPPAutoPreconnectTest, DISABLED_EnabledAllOrigins) {
  const bool kEnabledAllOrigins = GetParam();

  const GURL kUrl = embedded_https_test_server().GetURL(
      "a.test",
      GetPathWithPortReplacement("/predictors/preconnect.html",
                                 embedded_https_test_server().port()));
  base::HistogramTester histogram_tester;

  NavigateAndWaitForLcpElement(kUrl);
  EXPECT_EQ(std::vector<std::string>({"a.test"}), GetPreconnectedHosts());
  histogram_tester.ExpectUniqueSample("Blink.LCPP.PreconnectCount",
                                      kEnabledAllOrigins ? 2 : 1, 1);

  NavigateAndWaitForLcpElement(kUrl);
  EXPECT_EQ(
      kEnabledAllOrigins
          ? std::vector<std::string>({"a.test", "a.test", "foo.com", "bar.com"})
          : std::vector<std::string>({"a.test", "a.test", "bar.com"}),
      GetPreconnectedHosts());
}

INSTANTIATE_TEST_SUITE_P(Flags,
                         LCPPAutoPreconnectTest,
                         ::testing::Bool(),
                         ::testing::PrintToStringParamName());

class SuppressesLoadingPredictorOnSlowNetworkBrowserTest
    : public LoadingPredictorBrowserTest {
 public:
  SuppressesLoadingPredictorOnSlowNetworkBrowserTest() {
    scoped_feature_list_.InitWithFeaturesAndParameters(
        {{features::kSuppressesLoadingPredictorOnSlowNetwork,
          {{features::kSuppressesLoadingPredictorOnSlowNetworkThreshold.name,
            "500ms"}}}},
        {});
  }

  network::NetworkQualityTracker& GetNetworkQualityTracker() const {
    return *g_browser_process->network_quality_tracker();
  }

 private:
  base::test::ScopedFeatureList scoped_feature_list_;
};

// Tests that kSuppressesLoadingPredictorOnSlowNetwork feature suppresses
// LoadingPredictor on slow network.
IN_PROC_BROWSER_TEST_F(SuppressesLoadingPredictorOnSlowNetworkBrowserTest,
                       SuppressesOnSlowNetwork) {
  GURL url = embedded_test_server()->GetURL("/nocontent");
  base::TimeDelta http_rtt = GetNetworkQualityTracker().GetHttpRTT();
  int32_t downstream_throughput_kbps =
      GetNetworkQualityTracker().GetDownstreamThroughputKbps();

  {
    // LoadingPredictor will be suppressed on slow networks.
    GetNetworkQualityTracker().ReportRTTsAndThroughputForTesting(
        base::Milliseconds(501), downstream_throughput_kbps);
    auto observer = NavigateToURLAsync(url);
    ASSERT_TRUE(observer->WaitForNavigationFinished());
    EXPECT_EQ(0u, loading_predictor()->GetTotalHintsActivatedForTesting());
  }

  {
    // LoadingPredictor will not be suppressed on fast networks.
    GetNetworkQualityTracker().ReportRTTsAndThroughputForTesting(
        base::Milliseconds(500), downstream_throughput_kbps);
    auto observer = NavigateToURLAsync(url);
    ASSERT_TRUE(observer->WaitForNavigationFinished());
    EXPECT_EQ(1u, loading_predictor()->GetTotalHintsActivatedForTesting());
  }

  // Reset to the original values.
  GetNetworkQualityTracker().ReportRTTsAndThroughputForTesting(
      http_rtt, downstream_throughput_kbps);
}

enum class NetworkIsolationKeyMode {
  kDisabled,
  kEnabled,
};

class LoadingPredictorNetworkIsolationKeyBrowserTest
    : public LoadingPredictorBrowserTest,
      public testing::WithParamInterface<NetworkIsolationKeyMode> {
 public:
  LoadingPredictorNetworkIsolationKeyBrowserTest() {
    switch (GetParam()) {
      case NetworkIsolationKeyMode::kDisabled:
        scoped_feature_list2_.InitWithFeatures(
            // enabled_features
            {features::kLoadingPreconnectToRedirectTarget},
            // disabled_features
            {net::features::kPartitionConnectionsByNetworkIsolationKey,
             net::features::kSplitCacheByNetworkIsolationKey});
        break;
      case NetworkIsolationKeyMode::kEnabled:
        scoped_feature_list2_.InitWithFeatures(
            // enabled_features
            {net::features::kPartitionConnectionsByNetworkIsolationKey,
             net::features::kSplitCacheByNetworkIsolationKey,
             features::kLoadingPreconnectToRedirectTarget},
            // disabled_features
            {});
        break;
    }
  }

  ~LoadingPredictorNetworkIsolationKeyBrowserTest() override = default;

  // One server is used to initiate preconnects, and one is preconnected to.
  // This makes tracking preconnected sockets much easier, and removes all
  // worried about favicon fetches and other sources of preconnects.
  net::EmbeddedTestServer* preconnecting_test_server() {
    return &preconnecting_test_server_;
  }

  // Load the favicon into the cache, so that favicon requests won't create any
  // new connections. Can't just wait for it normally, because there's no easy
  // way to be sure that the favicon request associated with a page load has
  // completed, since it doesn't block navigation complete events.
  void CacheFavIcon() {
    CacheUrl(embedded_test_server()->GetURL("/favicon.ico"));
  }

  // Drives a request for the provided URL to completion, which will then be
  // stored in the HTTP cache, headers permitting.
  void CacheUrl(const GURL& url) {
    std::unique_ptr<network::ResourceRequest> request =
        std::make_unique<network::ResourceRequest>();
    request->url = url;
    content::SimpleURLLoaderTestHelper simple_loader_helper;
    url::Origin origin = url::Origin::Create(url);
    request->trusted_params = network::ResourceRequest::TrustedParams();
    request->trusted_params->isolation_info =
        net::IsolationInfo::CreateForInternalRequest(origin);
    request->site_for_cookies =
        request->trusted_params->isolation_info.site_for_cookies();
    std::unique_ptr<network::SimpleURLLoader> simple_loader =
        network::SimpleURLLoader::Create(std::move(request),
                                         TRAFFIC_ANNOTATION_FOR_TESTS);
    simple_loader->DownloadToStringOfUnboundedSizeUntilCrashAndDie(
        browser()->GetProfile()->GetURLLoaderFactory().get(),
        simple_loader_helper.GetCallback());
    simple_loader_helper.WaitForCallback();
    ASSERT_TRUE(simple_loader_helper.response_body());
    if (url.IntPort() == embedded_test_server()->port()) {
      EXPECT_EQ(1u, connection_tracker()->GetAcceptedSocketCount());
      EXPECT_EQ(1u, connection_tracker()->GetReadSocketCount());
    } else {
      EXPECT_EQ(url.IntPort(), preconnecting_test_server_.port());
      EXPECT_EQ(
          1u,
          preconnecting_server_connection_tracker()->GetAcceptedSocketCount());
      EXPECT_EQ(
          1u, preconnecting_server_connection_tracker()->GetReadSocketCount());
    }
    ResetNetworkState();
  }

  void RunCorsTest(bool use_cors_for_preconnect,
                   bool use_cors_for_resource_request) {
    auto kCrossOriginValue = std::to_array<const char*>({
        "anonymous",
        "use-credentials",
    });

    ASSERT_TRUE(ui_test_utils::NavigateToURL(
        browser(), preconnecting_test_server()->GetURL("/title1.html")));

    // Preconnect a socket.
    content::WebContents* tab =
        browser()->GetTabStripModel()->GetActiveWebContents();
    GURL preconnect_url = embedded_test_server()->base_url();
    std::string start_preconnect = base::StringPrintf(
        "var link = document.createElement('link');"
        "link.rel = 'preconnect';"
        "link.crossOrigin = '%s';"
        "link.href = '%s';"
        "document.head.appendChild(link);",
        kCrossOriginValue[use_cors_for_preconnect],
        preconnect_url.spec().c_str());
    content::ExecuteScriptAsync(tab, start_preconnect);
    connection_tracker()->WaitForAcceptedConnections(1u);
    EXPECT_EQ(1u, connection_tracker()->GetAcceptedSocketCount());
    EXPECT_EQ(0u, connection_tracker()->GetReadSocketCount());

    // Load an image.
    GURL image_url = embedded_test_server()->GetURL("/test.gif");
    std::string load_image = base::StringPrintf(
        "var image = document.createElement('img');"
        "image.crossOrigin = '%s';"
        "image.src = '%s';"
        "document.body.appendChild(image);",
        kCrossOriginValue[use_cors_for_resource_request],
        image_url.spec().c_str());
    content::ExecuteScriptAsync(tab, load_image);
    connection_tracker()->WaitUntilConnectionRead();

    // The preconnected socket should have been used by the image request if
    // the CORS behavior of the preconnect and the request were the same.
    if (use_cors_for_preconnect == use_cors_for_resource_request) {
      EXPECT_EQ(1u, connection_tracker()->GetAcceptedSocketCount());
      EXPECT_EQ(1u, connection_tracker()->GetReadSocketCount());
    } else {
      EXPECT_EQ(2u, connection_tracker()->GetAcceptedSocketCount());
      EXPECT_EQ(1u, connection_tracker()->GetReadSocketCount());
    }
  }

 private:
  base::test::ScopedFeatureList scoped_feature_list2_;
};

INSTANTIATE_TEST_SUITE_P(All,
                         LoadingPredictorNetworkIsolationKeyBrowserTest,
                         ::testing::Values(NetworkIsolationKeyMode::kDisabled,
                                           NetworkIsolationKeyMode::kEnabled));

// Make sure that the right NetworkAnonymizationKey is used by the
// LoadingPredictor, both when the predictor is populated and when it isn't.
//
// TODO(crbug.com/448862629): Disable flaky test on mac
#if BUILDFLAG(IS_MAC)
#define MAYBE_LoadingPredictorNoRedirects DISABLED_LoadingPredictorNoRedirects
#else
#define MAYBE_LoadingPredictorNoRedirects LoadingPredictorNoRedirects
#endif
IN_PROC_BROWSER_TEST_P(LoadingPredictorNetworkIsolationKeyBrowserTest,
                       MAYBE_LoadingPredictorNoRedirects) {
  // Cache resources needed by navigations, so the only sockets created
  // during navigations should be for the two preconnects.
  CacheFavIcon();
  GURL cacheable_url = embedded_test_server()->GetURL("/cachetime");
  CacheUrl(cacheable_url);

  // For the first loop iteration, the predictor has no state, and for the
  // second one it does.
  for (bool predictor_has_state : {false, true}) {
    SCOPED_TRACE(predictor_has_state);

    auto observer = NavigateToURLAsync(cacheable_url);
    ASSERT_TRUE(observer->WaitForNavigationFinished());
    connection_tracker()->WaitForAcceptedConnections(2);
    EXPECT_EQ(0u, connection_tracker()->GetReadSocketCount());

    // Have the page fetch a subresource, which should use one of the
    // preconnects triggered by the above navigation, due to the matching
    // NetworkAnonymizationKey. Do this instead of a navigation to a non-cached
    // URL to avoid triggering more preconnects.
    std::string fetch_resource = base::StringPrintf(
        "(async () => {"
        "  var resp = (await fetch('%s'));"
        "  return resp.status; })();",
        embedded_test_server()->GetURL("/echo").spec().c_str());
    EXPECT_EQ(200, EvalJs(browser()
                              ->GetTabStripModel()
                              ->GetActiveWebContents()
                              ->GetPrimaryMainFrame(),
                          fetch_resource));

    EXPECT_EQ(2u, connection_tracker()->GetAcceptedSocketCount());
    EXPECT_EQ(1u, connection_tracker()->GetReadSocketCount());

    ResetNetworkState();
  }
}

// Make sure that the right NetworkAnonymizationKey is used by the
// LoadingPredictor, both when the predictor is populated and when it isn't.
IN_PROC_BROWSER_TEST_P(LoadingPredictorNetworkIsolationKeyBrowserTest,
                       LoadingPredictorWithRedirects) {
  // Cache resources needed by navigations, so the only connections to the
  // tracked server created during navigations should be for preconnects.
  CacheFavIcon();
  GURL cacheable_url = embedded_test_server()->GetURL("/cachetime");
  CacheUrl(cacheable_url);

  GURL redirecting_url = preconnecting_test_server()->GetURL(
      "/server-redirect?" + cacheable_url.spec());

  // Learn the redirects from initial navigation.
  ASSERT_TRUE(ui_test_utils::NavigateToURL(browser(), redirecting_url));
  EXPECT_EQ(0u, connection_tracker()->GetAcceptedSocketCount());
  EXPECT_EQ(0u, connection_tracker()->GetReadSocketCount());

  // The next navigation should preconnect. It won't use the preconnected
  // socket, since the destination resource is still in the cache.
  auto observer = NavigateToURLAsync(redirecting_url);
  ASSERT_TRUE(observer->WaitForNavigationFinished());
  connection_tracker()->WaitForAcceptedConnections(1);
  EXPECT_EQ(0u, connection_tracker()->GetReadSocketCount());

  // Have the page fetch a subresource, which should use one of the
  // preconnects triggered by the above navigation, due to the matching
  // NetworkAnonymizationKey. Do this instead of a navigation to a non-cached
  // URL to avoid triggering more preconnects.
  std::string fetch_resource = base::StringPrintf(
      "(async () => {"
      "  var resp = (await fetch('%s'));"
      "  return resp.status; })();",
      embedded_test_server()->GetURL("/echo").spec().c_str());
  EXPECT_EQ(200, EvalJs(browser()
                            ->GetTabStripModel()
                            ->GetActiveWebContents()
                            ->GetPrimaryMainFrame(),
                        fetch_resource));

  EXPECT_EQ(1u, connection_tracker()->GetAcceptedSocketCount());
  EXPECT_EQ(1u, connection_tracker()->GetReadSocketCount());
}

// Checks the opposite of the above test - tests that even when a redirect is
// predicted, preconnects are still made to the original origin using the
// correct NetworkAnonymizationKey.
IN_PROC_BROWSER_TEST_P(LoadingPredictorNetworkIsolationKeyBrowserTest,
                       LoadingPredictorWithRedirects2) {
  // Cache the redirect, so the only connections to the tracked server created
  // during navigations should be for preconnects.
  GURL destination_url = preconnecting_test_server()->GetURL("/cachetime");
  GURL redirecting_url = embedded_test_server()->GetURL("/cached-redirect?" +
                                                        destination_url.spec());

  // Unlike other tests, the "preconnecting" server is actually the final
  // destination, so its favicon needs to be cached.
  CacheUrl(preconnecting_test_server()->GetURL("/favicon.ico"));

  CacheUrl(redirecting_url);

  // The first navigation learns to preconnect based on the redirect, and the
  // second actually preconnects to the untracked server. All navigations should
  // preconnect twice to the tracked server.
  for (int i = 0; i < 2; ++i) {
    if (i == 0) {
      // NavigateToURL waits long enough to ensure information from the
      // navigation is learned, while WaitForNavigationFinished() does not.
      ASSERT_TRUE(ui_test_utils::NavigateToURL(browser(), redirecting_url));
    } else {
      auto observer = NavigateToURLAsync(redirecting_url);
      ASSERT_TRUE(observer->WaitForNavigationFinished());
    }
    connection_tracker()->WaitForAcceptedConnections(2);
    EXPECT_EQ(0u, connection_tracker()->GetReadSocketCount());

    // Verify that the redirect from |redirecting_url| to |destination_url| was
    // learned and preconnected to.
    if (i == 1) {
      preconnecting_server_connection_tracker()->WaitForAcceptedConnections(1);
    }
    EXPECT_EQ(0u, connection_tracker()->GetReadSocketCount());

    // Verify that the preconnects to |embedded_test_server| were made using
    // the |redirecting_url|'s NetworkAnonymizationKey. To do this, make a
    // request using the tracked server's NetworkAnonymizationKey, and verify it
    // used one of the existing sockets.
    auto request = std::make_unique<network::ResourceRequest>();
    request->url = embedded_test_server()->GetURL("/echo");
    content::SimpleURLLoaderTestHelper simple_loader_helper;
    url::Origin origin = url::Origin::Create(request->url);
    request->trusted_params = network::ResourceRequest::TrustedParams();
    request->trusted_params->isolation_info =
        net::IsolationInfo::CreateForInternalRequest(origin);
    request->site_for_cookies =
        request->trusted_params->isolation_info.site_for_cookies();
    std::unique_ptr<network::SimpleURLLoader> simple_loader =
        network::SimpleURLLoader::Create(std::move(request),
                                         TRAFFIC_ANNOTATION_FOR_TESTS);
    simple_loader->DownloadToStringOfUnboundedSizeUntilCrashAndDie(
        browser()->GetProfile()->GetURLLoaderFactory().get(),
        simple_loader_helper.GetCallback());
    simple_loader_helper.WaitForCallback();
    ASSERT_TRUE(simple_loader_helper.response_body());
    EXPECT_EQ(2u, connection_tracker()->GetAcceptedSocketCount());
    EXPECT_EQ(1u, connection_tracker()->GetReadSocketCount());

    ResetNetworkState();
  }
}

IN_PROC_BROWSER_TEST_P(LoadingPredictorNetworkIsolationKeyBrowserTest,
                       LinkRelPreconnectMainFrame) {
  const char kHost1[] = "host1.test";
  const char kHost2[] = "host2.test";
  GURL preconnect_url = embedded_test_server()->GetURL("/echo");

  // Navigate two tabs, one to each host.

  content::WebContents* tab1 =
      browser()->GetTabStripModel()->GetActiveWebContents();
  ASSERT_TRUE(ui_test_utils::NavigateToURL(
      browser(), preconnecting_test_server()->GetURL(kHost1, "/title1.html")));

  chrome::NewTab(browser(), NewTabTypes::kNoUserAction);
  content::WebContents* tab2 =
      browser()->GetTabStripModel()->GetActiveWebContents();
  ASSERT_TRUE(ui_test_utils::NavigateToURL(
      browser(), preconnecting_test_server()->GetURL(kHost2, "/title1.html")));

  std::string start_preconnect = base::StringPrintf(
      "var link = document.createElement('link');"
      "link.rel = 'preconnect';"
      "link.crossOrigin = 'anonymous';"
      "link.href = '%s';"
      "document.head.appendChild(link);",
      preconnect_url.spec().c_str());
  content::ExecuteScriptAsync(tab1->GetPrimaryMainFrame(), start_preconnect);
  connection_tracker()->WaitForAcceptedConnections(1u);
  EXPECT_EQ(1u, connection_tracker()->GetAcceptedSocketCount());
  EXPECT_EQ(0u, connection_tracker()->GetReadSocketCount());

  std::string fetch_resource = base::StringPrintf(
      "(async () => {"
      "  var resp = (await fetch('%s',"
      "                          {credentials: 'omit',"
      "                           mode: 'no-cors'}));"
      "  return resp.status; })();",
      preconnect_url.spec().c_str());
  // Fetch a resource from the test server from tab 2, without CORS.
  EXPECT_EQ(0, EvalJs(tab2->GetPrimaryMainFrame(), fetch_resource));
  if (GetParam() == NetworkIsolationKeyMode::kDisabled) {
    // When not using NetworkAnonymizationKeys, the preconnected socket from a
    // tab at one site is usable by a request from another site.
    EXPECT_EQ(1u, connection_tracker()->GetAcceptedSocketCount());
    EXPECT_EQ(1u, connection_tracker()->GetReadSocketCount());
  } else {
    // When using NetworkAnonymizationKeys, the preconnected socket cannot be
    // used.
    EXPECT_EQ(2u, connection_tracker()->GetAcceptedSocketCount());
    EXPECT_EQ(1u, connection_tracker()->GetReadSocketCount());
  }

  // Now try fetching a resource from tab 1.
  EXPECT_EQ(0, EvalJs(tab1->GetPrimaryMainFrame(), fetch_resource));
  // If the preconnected socket was not used before, it should now be used. If
  // it was used before, a new socket will be used.
  EXPECT_EQ(2u, connection_tracker()->GetAcceptedSocketCount());
  EXPECT_EQ(2u, connection_tracker()->GetReadSocketCount());
}

IN_PROC_BROWSER_TEST_P(LoadingPredictorNetworkIsolationKeyBrowserTest,
                       LinkRelPreconnectSubFrame) {
  const char kHost1[] = "host1.test";
  const char kHost2[] = "host2.test";
  GURL preconnect_url = embedded_test_server()->GetURL("/echo");

  // Tab 1 has two iframes, one at kHost1, one at kHost2.
  content::WebContents* tab1 =
      browser()->GetTabStripModel()->GetActiveWebContents();
  ASSERT_TRUE(ui_test_utils::NavigateToURL(
      browser(), preconnecting_test_server()->GetURL(
                     kHost1, GetPathWithPortReplacement(
                                 "/predictors/two_iframes.html",
                                 preconnecting_test_server()->port()))));
  content::RenderFrameHost* main_frame = tab1->GetPrimaryMainFrame();
  ASSERT_EQ(kHost1, main_frame->GetLastCommittedOrigin().host());
  content::RenderFrameHost* iframe_1 = ChildFrameAt(main_frame, 0);
  ASSERT_TRUE(iframe_1);
  ASSERT_EQ(kHost1, iframe_1->GetLastCommittedOrigin().host());
  content::RenderFrameHost* iframe_2 = ChildFrameAt(main_frame, 1);
  ASSERT_TRUE(iframe_2);
  ASSERT_EQ(kHost2, iframe_2->GetLastCommittedOrigin().host());

  // Create another tab without an iframe, at kHost2.
  chrome::NewTab(browser(), NewTabTypes::kNoUserAction);
  content::WebContents* tab2 =
      browser()->GetTabStripModel()->GetActiveWebContents();
  ASSERT_TRUE(ui_test_utils::NavigateToURL(
      browser(), preconnecting_test_server()->GetURL(kHost2, "/title1.html")));

  // Preconnect a socket in the cross-origin iframe.
  std::string start_preconnect = base::StringPrintf(
      "var link = document.createElement('link');"
      "link.rel = 'preconnect';"
      "link.crossOrigin = 'anonymous';"
      "link.href = '%s';"
      "document.head.appendChild(link);",
      preconnect_url.spec().c_str());
  content::ExecuteScriptAsync(iframe_2, start_preconnect);
  connection_tracker()->WaitForAcceptedConnections(1u);
  EXPECT_EQ(1u, connection_tracker()->GetAcceptedSocketCount());
  EXPECT_EQ(0u, connection_tracker()->GetReadSocketCount());

  std::string fetch_resource = base::StringPrintf(
      "(async () => {"
      "  var resp = (await fetch('%s',"
      "                          {credentials: 'omit',"
      "                           mode: 'no-cors'}));"
      "  return resp.status; })();",
      preconnect_url.spec().c_str());

  // Fetch a resource from the test server from tab 2 iframe, without CORS.
  EXPECT_EQ(0, EvalJs(tab2->GetPrimaryMainFrame(), fetch_resource));
  if (GetParam() == NetworkIsolationKeyMode::kDisabled) {
    // When not using NetworkAnonymizationKeys, the preconnected socket from the
    // iframe from the first tab can be used.
    EXPECT_EQ(1u, connection_tracker()->GetAcceptedSocketCount());
    EXPECT_EQ(1u, connection_tracker()->GetReadSocketCount());
  } else {
    // Otherwise, the preconnected socket cannot be used.
    EXPECT_EQ(2u, connection_tracker()->GetAcceptedSocketCount());
    EXPECT_EQ(1u, connection_tracker()->GetReadSocketCount());
  }

  // Fetch a resource from the test server from the same-origin iframe, without
  // CORS.
  EXPECT_EQ(0, EvalJs(iframe_1, fetch_resource));
  if (GetParam() == NetworkIsolationKeyMode::kDisabled) {
    // When not using NetworkAnonymizationKeys, a new socket is created and
    // used.
    EXPECT_EQ(2u, connection_tracker()->GetAcceptedSocketCount());
    EXPECT_EQ(2u, connection_tracker()->GetReadSocketCount());
  } else {
    // Otherwise, the preconnected socket cannot be used.
    EXPECT_EQ(3u, connection_tracker()->GetAcceptedSocketCount());
    EXPECT_EQ(2u, connection_tracker()->GetReadSocketCount());
  }

  // Now try fetching a resource the cross-site iframe.
  EXPECT_EQ(0, EvalJs(iframe_2, fetch_resource));
  // If the preconnected socket was not used before, it should now be used. If
  // it was used before, a new socket will be used.
  EXPECT_EQ(3u, connection_tracker()->GetAcceptedSocketCount());
  EXPECT_EQ(3u, connection_tracker()->GetReadSocketCount());
}

// Tests that preconnect warms up a non-CORS connection to a test
// server, and that socket is used when fetching a non-CORS resource.
IN_PROC_BROWSER_TEST_P(LoadingPredictorNetworkIsolationKeyBrowserTest,
                       PreconnectNonCorsAndFetchNonCors) {
  RunCorsTest(false /* use_cors_for_preconnect */,
              false /* use_cors_for_resource_request */);
}

// Tests that preconnect warms up a non-CORS connection to a test
// server, but that socket is not used when fetching a CORS resource.
IN_PROC_BROWSER_TEST_P(LoadingPredictorNetworkIsolationKeyBrowserTest,
                       PreconnectNonCorsAndFetchCors) {
  RunCorsTest(false /* use_cors_for_preconnect */,
              true /* use_cors_for_resource_request */);
}

// Tests that preconnect warms up a CORS connection to a test server,
// but that socket is not used when fetching a non-CORS resource.
IN_PROC_BROWSER_TEST_P(LoadingPredictorNetworkIsolationKeyBrowserTest,
                       PreconnectCorsAndFetchNonCors) {
  RunCorsTest(true /* use_cors_for_preconnect */,
              false /* use_cors_for_resource_request */);
}

// Tests that preconnect warms up a CORS connection to a test server,
// that socket is used when fetching a CORS resource.
IN_PROC_BROWSER_TEST_P(LoadingPredictorNetworkIsolationKeyBrowserTest,
                       PreconnectCorsAndFetchCors) {
  RunCorsTest(true /* use_cors_for_preconnect */,
              true /* use_cors_for_resource_request */);
}

class LoadingPredictorBrowserTestWithProxy
    : public LoadingPredictorBrowserTest {
 public:
  void SetUp() override {
    pac_script_server_ = std::make_unique<net::EmbeddedTestServer>();
    pac_script_server_->AddDefaultHandlers(GetChromeTestDataDir());
    ASSERT_TRUE(pac_script_server_->InitializeAndListen());
    LoadingPredictorBrowserTest::SetUp();
  }

  void SetUpOnMainThread() override {
    LoadingPredictorBrowserTest::SetUpOnMainThread();
    // This will make all dns requests fail.
    host_resolver()->ClearRules();

    pac_script_server_->StartAcceptingConnections();
  }

  void SetUpCommandLine(base::CommandLine* command_line) override {
    GURL pac_url = pac_script_server_->GetURL(GetPathWithPortReplacement(
        "/predictors/proxy.pac", embedded_test_server()->port()));
    command_line->AppendSwitchASCII(switches::kProxyPacUrl, pac_url.spec());
  }

 private:
  std::unique_ptr<net::EmbeddedTestServer> pac_script_server_;
};

IN_PROC_BROWSER_TEST_F(LoadingPredictorBrowserTestWithProxy,
                       PrepareForPageLoadWithoutPrediction) {
  // Navigate the first time to fill the HTTP cache.
  GURL url = embedded_test_server()->GetURL(
      "test.com", GetPathWithPortReplacement(kHtmlSubresourcesPath,
                                             embedded_test_server()->port()));
  net::SchemefulSite site = net::SchemefulSite(url);
  auto network_anonymization_key =
      net::NetworkAnonymizationKey::CreateSameSite(std::move(site));
  ASSERT_TRUE(ui_test_utils::NavigateToURL(browser(), url));
  ResetNetworkState();
  ResetPredictorState();

  auto observer = NavigateToURLAsync(url);
  EXPECT_TRUE(observer->WaitForRequestStart());
  preconnect_manager_observer()->WaitUntilProxyLookedUp(
      url, network_anonymization_key);
  EXPECT_TRUE(preconnect_manager_observer()->ProxyFound(
      url, network_anonymization_key));
  // We should preconnect only 2 sockets for the main frame host.
  const size_t expected_connections = 2;
  connection_tracker()->WaitForAcceptedConnections(expected_connections);
  EXPECT_EQ(expected_connections,
            connection_tracker()->GetAcceptedSocketCount());
  // No reads since all resources should be cached.
  EXPECT_EQ(0u, connection_tracker()->GetReadSocketCount());
}

IN_PROC_BROWSER_TEST_F(LoadingPredictorBrowserTestWithProxy,
                       PrepareForPageLoadWithPrediction) {
  // Navigate the first time to fill the predictor's database and the HTTP
  // cache.
  GURL url = embedded_test_server()->GetURL(
      "test.com", GetPathWithPortReplacement(kHtmlSubresourcesPath,
                                             embedded_test_server()->port()));
  net::SchemefulSite site = net::SchemefulSite(url);
  auto network_anonymization_key =
      net::NetworkAnonymizationKey::CreateSameSite(std::move(site));
  ASSERT_TRUE(ui_test_utils::NavigateToURL(browser(), url));
  ResetNetworkState();

  auto observer = NavigateToURLAsync(url);
  EXPECT_TRUE(observer->WaitForRequestStart());
  for (auto* const host : kHtmlSubresourcesHosts) {
    GURL host_url = embedded_test_server()->GetURL(host, "/");
    preconnect_manager_observer()->WaitUntilProxyLookedUp(
        host_url, network_anonymization_key);
    EXPECT_TRUE(preconnect_manager_observer()->ProxyFound(
        host_url, network_anonymization_key));
  }
  // 2 connections to the main frame host + 1 connection per host for others.
  const size_t expected_connections = std::size(kHtmlSubresourcesHosts) + 1;
  connection_tracker()->WaitForAcceptedConnections(expected_connections);
  EXPECT_EQ(expected_connections,
            connection_tracker()->GetAcceptedSocketCount());
  // No reads since all resources should be cached.
  EXPECT_EQ(0u, connection_tracker()->GetReadSocketCount());
}

class LoadingPredictorBrowserTestWithOptimizationGuide
    : public ::testing::WithParamInterface<
          std::tuple<bool, bool, bool, std::string>>,
      public LoadingPredictorBrowserTest {
 public:
  LoadingPredictorBrowserTestWithOptimizationGuide() {
    feature_list_.InitWithFeaturesAndParameters(
        {{features::kLoadingPredictorUseOptimizationGuide,
          {{"use_predictions",
            base::ToString(ShouldUseOptimizationGuidePredictions())},
           {"always_retrieve_predictions", "true"}}},
         {optimization_guide::features::kOptimizationHints, {}}},
        {});

    if (IsLocalPredictionEnabled()) {
      local_predictions_feature_list_.InitAndEnableFeature(
          features::kLoadingPredictorUseLocalPredictions);
    } else {
      local_predictions_feature_list_.InitAndDisableFeature(
          features::kLoadingPredictorUseLocalPredictions);
    }

    if (IsPrefetchEnabled()) {
      prefetch_feature_list_.InitAndEnableFeatureWithParameters(
          features::kLoadingPredictorPrefetch,
          {{"subresource_type", GetSubresourceTypeParam()}});
    } else {
      prefetch_feature_list_.InitAndDisableFeature(
          features::kLoadingPredictorPrefetch);
    }
  }

  bool IsLocalPredictionEnabled() const { return std::get<0>(GetParam()); }

  bool ShouldUseOptimizationGuidePredictions() const {
    return std::get<1>(GetParam());
  }

  bool IsPrefetchEnabled() const { return std::get<2>(GetParam()); }

  std::string GetSubresourceTypeParam() const {
    return std::string(std::get<3>(GetParam()));
  }

  bool ShouldRetrieveOptimizationGuidePredictions() {
    return !IsLocalPredictionEnabled() ||
           features::ShouldAlwaysRetrieveOptimizationGuidePredictions();
  }

  // A predicted subresource.
  struct Subresource {
    explicit Subresource(std::string url)
        : url(url),
          type(optimization_guide::proto::RESOURCE_TYPE_UNKNOWN),
          preconnect_only(false) {}
    Subresource(std::string url, optimization_guide::proto::ResourceType type)
        : url(url), type(type), preconnect_only(false) {}
    Subresource(std::string url,
                optimization_guide::proto::ResourceType type,
                bool preconnect_only)
        : url(url), type(type), preconnect_only(preconnect_only) {}

    std::string url;
    optimization_guide::proto::ResourceType type;
    bool preconnect_only;
  };

  void SetUpOptimizationHint(
      const GURL& url,
      const std::vector<Subresource>& predicted_subresources) {
    auto* optimization_guide_keyed_service =
        OptimizationGuideKeyedServiceFactory::GetForProfile(
            browser()->GetProfile());
    optimization_guide::proto::LoadingPredictorMetadata
        loading_predictor_metadata;
    for (const auto& subresource : predicted_subresources) {
      auto* added = loading_predictor_metadata.add_subresources();
      added->set_url(subresource.url);
      added->set_resource_type(subresource.type);
      added->set_preconnect_only(subresource.preconnect_only);
    }

    optimization_guide::OptimizationMetadata optimization_metadata;
    optimization_metadata.set_loading_predictor_metadata(
        loading_predictor_metadata);
    optimization_guide_keyed_service->AddHintForTesting(
        url, optimization_guide::proto::LOADING_PREDICTOR,
        optimization_metadata);
  }

 private:
  base::test::ScopedFeatureList feature_list_;
  base::test::ScopedFeatureList local_predictions_feature_list_;
  base::test::ScopedFeatureList prefetch_feature_list_;
};

INSTANTIATE_TEST_SUITE_P(
    ,
    LoadingPredictorBrowserTestWithOptimizationGuide,
    testing::Combine(
        /*IsLocalPredictionEnabled()=*/testing::Bool(),
        /*ShouldUseOptimizationGuidePredictions()=*/testing::Bool(),
        /*IsPrefetchEnabled()=*/testing::Values(false),
        /*GetSubresourceType()=*/testing::Values("")));

IN_PROC_BROWSER_TEST_P(LoadingPredictorBrowserTestWithOptimizationGuide,
                       NavigationHasLocalPredictionNoOptimizationHint) {
  // Navigate the first time to fill the predictor's database and the HTTP
  // cache.
  GURL url = embedded_test_server()->GetURL(
      "test.com", GetPathWithPortReplacement(kHtmlSubresourcesPath,
                                             embedded_test_server()->port()));
  net::SchemefulSite site = net::SchemefulSite(url);
  auto network_anonymization_key =
      net::NetworkAnonymizationKey::CreateSameSite(std::move(site));
  ASSERT_TRUE(ui_test_utils::NavigateToURL(browser(), url));
  ResetNetworkState();

  auto observer = NavigateToURLAsync(url);
  EXPECT_TRUE(observer->WaitForRequestStart());
  for (auto* const host : kHtmlSubresourcesHosts) {
    if (!IsLocalPredictionEnabled() && host != url.GetHost()) {
      // We don't expect local predictions to be preconnected to.
      continue;
    }

    preconnect_manager_observer()->WaitUntilHostLookedUp(
        host, network_anonymization_key);
    EXPECT_TRUE(preconnect_manager_observer()->HostFound(
        host, network_anonymization_key));
  }
  size_t expected_connections;
  if (IsLocalPredictionEnabled()) {
    // 2 connections to the main frame host  + 1 connection per host for others.
    expected_connections = std::size(kHtmlSubresourcesHosts) + 1;
  } else {
    // There should always be 2 connections to the main frame host.
    expected_connections = 2;
  }
  connection_tracker()->WaitForAcceptedConnections(expected_connections);
  EXPECT_EQ(expected_connections,
            connection_tracker()->GetAcceptedSocketCount());
  // No reads since all resources should be cached.
  EXPECT_EQ(0u, connection_tracker()->GetReadSocketCount());
}

IN_PROC_BROWSER_TEST_P(LoadingPredictorBrowserTestWithOptimizationGuide,
                       NavigationWithBothLocalPredictionAndOptimizationHint) {
  // Navigate the first time to fill the predictor's database and the HTTP
  // cache.
  GURL url = embedded_test_server()->GetURL(
      "test.com", GetPathWithPortReplacement(kHtmlSubresourcesPath,
                                             embedded_test_server()->port()));
  url::Origin origin = url::Origin::Create(url);
  net::SchemefulSite site = net::SchemefulSite(origin);
  auto network_anonymization_key =
      net::NetworkAnonymizationKey::CreateSameSite(std::move(site));
  ASSERT_TRUE(ui_test_utils::NavigateToURL(browser(), url));
  ResetNetworkState();

  SetUpOptimizationHint(url, {Subresource("http://subresource.com/1"),
                              Subresource("http://subresource.com/2"),
                              Subresource("http://otherresource.com/2"),
                              Subresource("skipsoverinvalidurl/////")});

  auto observer = NavigateToURLAsync(url);
  EXPECT_TRUE(observer->WaitForRequestStart());

  // The initial URL should be preconnected to.
  preconnect_manager_observer()->WaitUntilHostLookedUp(
      url.GetHost(), network_anonymization_key);
  EXPECT_TRUE(preconnect_manager_observer()->HostFound(
      url.GetHost(), network_anonymization_key));
  EXPECT_TRUE(preconnect_manager_observer()->HasOriginAttemptedToPreconnect(
      origin.GetURL()));

  // Both subresource hosts should be preconnected to.
  std::vector<std::string> expected_subresource_hosts;
  if (IsLocalPredictionEnabled()) {
    // Should use subresources that were learned.
    expected_subresource_hosts = {"baz.com", "foo.com"};
  } else if (ShouldUseOptimizationGuidePredictions()) {
    // Should use subresources from optimization hint.
    expected_subresource_hosts = {"subresource.com", "otherresource.com"};
  }
  for (const auto& host : expected_subresource_hosts) {
    preconnect_manager_observer()->WaitUntilHostLookedUp(
        host, network_anonymization_key);
    EXPECT_TRUE(preconnect_manager_observer()->HostFound(
        host, network_anonymization_key));

    GURL expected_origin;
    if (IsLocalPredictionEnabled()) {
      // The locally learned origins are expected to have a port.
      expected_origin = embedded_test_server()->GetURL(host, "/");
    } else {
      // The optimization hints learned origins do not have a port.
      expected_origin = GURL(base::StringPrintf("http://%s", host.c_str()));
    }
    EXPECT_TRUE(preconnect_manager_observer()->HasOriginAttemptedToPreconnect(
        expected_origin));
  }
}

IN_PROC_BROWSER_TEST_P(LoadingPredictorBrowserTestWithOptimizationGuide,
                       NavigationWithNoLocalPredictionsButHasOptimizationHint) {
  base::HistogramTester histogram_tester;

  GURL url = embedded_test_server()->GetURL("m.hints.com", "/simple.html");
  SetUpOptimizationHint(url, {Subresource("http://subresource.com/1"),
                              Subresource("http://subresource.com/2"),
                              Subresource("http://otherresource.com/2"),
                              Subresource("skipsoverinvalidurl/////")});
  url::Origin origin = url::Origin::Create(url);
  net::SchemefulSite site = net::SchemefulSite(origin);
  auto network_anonymization_key =
      net::NetworkAnonymizationKey::CreateSameSite(std::move(site));

  auto observer = NavigateToURLAsync(url);
  EXPECT_TRUE(observer->WaitForRequestStart());

  // The initial URL should be preconnected to.
  preconnect_manager_observer()->WaitUntilHostLookedUp(
      url.GetHost(), network_anonymization_key);
  EXPECT_TRUE(preconnect_manager_observer()->HostFound(
      url.GetHost(), network_anonymization_key));
  EXPECT_TRUE(preconnect_manager_observer()->HasOriginAttemptedToPreconnect(
      origin.GetURL()));
  for (auto* const host : {"subresource.com", "otherresource.com"}) {
    if (ShouldUseOptimizationGuidePredictions()) {
      // Both subresource hosts should be preconnected to.
      preconnect_manager_observer()->WaitUntilHostLookedUp(
          host, network_anonymization_key);
    }
    EXPECT_EQ(preconnect_manager_observer()->HostFound(
                  host, network_anonymization_key),
              ShouldUseOptimizationGuidePredictions());

    EXPECT_EQ(preconnect_manager_observer()->HasOriginAttemptedToPreconnect(
                  GURL(base::StringPrintf("http://%s/", host))),
              ShouldUseOptimizationGuidePredictions());
  }

  EXPECT_TRUE(observer->WaitForResponse());
  observer->ResumeNavigation();
  content::AwaitDocumentOnLoadCompleted(observer->web_contents());
  ASSERT_TRUE(observer->WaitForNavigationFinished());

  // Navigate to another URL and wait until the previous RFH is destroyed (i.e.
  // until the optimization guide prediction is cleared and metrics are
  // recorded).
  content::RenderFrameHostWrapper rfh(
      observer->web_contents()->GetPrimaryMainFrame());
  // Disable BFCache to ensure the navigation below unloads |rfh|.
  content::DisableBackForwardCacheForTesting(
      browser()->GetTabStripModel()->GetActiveWebContents(),
      content::BackForwardCache::DisableForTestingReason::
          TEST_REQUIRES_NO_CACHING);
  ASSERT_TRUE(ui_test_utils::NavigateToURL(
      browser(), embedded_test_server()->GetURL("nohints.com", "/")));
  ASSERT_TRUE(rfh.WaitUntilRenderFrameDeleted());
}

IN_PROC_BROWSER_TEST_P(
    LoadingPredictorBrowserTestWithOptimizationGuide,
    OptimizationGuidePredictionsNotAppliedForAlreadyCommittedNavigation) {
  GURL url = embedded_test_server()->GetURL("hints.com", "/simple.html");
  net::SchemefulSite site = net::SchemefulSite(url);
  auto network_anonymization_key =
      net::NetworkAnonymizationKey::CreateSameSite(site);
  // Navigate to URL with hints but only seed hints after navigation has
  // committed.
  auto observer = NavigateToURLAsync(url);
  EXPECT_TRUE(observer->WaitForResponse());
  observer->ResumeNavigation();
  SetUpOptimizationHint(url, {Subresource("http://subresource.com/1"),
                              Subresource("http://subresource.com/2"),
                              Subresource("http://otherresource.com/2"),
                              Subresource("skipsoverinvalidurl/////")});

  EXPECT_FALSE(preconnect_manager_observer()->HasHostBeenLookedUp(
      "subresource.com", network_anonymization_key));
  EXPECT_FALSE(preconnect_manager_observer()->HasHostBeenLookedUp(
      "otheresource.com", network_anonymization_key));
}

IN_PROC_BROWSER_TEST_P(LoadingPredictorBrowserTestWithOptimizationGuide,
                       OptimizationGuidePredictionsNotAppliedForRedirect) {
  GURL destination_url =
      embedded_test_server()->GetURL("otherhost.com", "/cachetime");
  GURL redirecting_url = embedded_test_server()->GetURL(
      "sometimesredirects.com", "/cached-redirect?" + destination_url.spec());
  SetUpOptimizationHint(destination_url,
                        {Subresource("http://subresource.com/1"),
                         Subresource("http://subresource.com/2"),
                         Subresource("http://otherresource.com/2"),
                         Subresource("skipsoverinvalidurl/////")});

  // Navigate the first time to something on redirecting origin to fill the
  // predictor's database and the HTTP cache.
  ASSERT_TRUE(ui_test_utils::NavigateToURL(
      browser(),
      embedded_test_server()->GetURL(
          "sometimesredirects.com",
          GetPathWithPortReplacement(kHtmlSubresourcesPath,
                                     embedded_test_server()->port()))));
  ResetNetworkState();

  net::SchemefulSite site = net::SchemefulSite(destination_url);
  auto network_anonymization_key =
      net::NetworkAnonymizationKey::CreateSameSite(site);
  // Navigate to URL with hints but is redirected, hints should not be
  // applied.
  auto observer = NavigateToURLAsync(redirecting_url);
  EXPECT_TRUE(observer->WaitForResponse());
  SetUpOptimizationHint(redirecting_url,
                        {Subresource("http://subresourceredirect.com/1"),
                         Subresource("http://subresourceredirect.com/2"),
                         Subresource("http://otherresourceredirect.com/2"),
                         Subresource("skipsoverinvalidurl/////")});
  observer->ResumeNavigation();

  std::vector<std::string> expected_opt_guide_subresource_hosts = {
      "subresource.com", "otherresource.com"};
  if (ShouldRetrieveOptimizationGuidePredictions() &&
      ShouldUseOptimizationGuidePredictions()) {
    // Should use subresources from optimization hint.
    for (const auto& host : expected_opt_guide_subresource_hosts) {
      preconnect_manager_observer()->WaitUntilHostLookedUp(
          host, network_anonymization_key);
      EXPECT_TRUE(preconnect_manager_observer()->HostFound(
          host, network_anonymization_key));

      // The origins from optimization hints do not have a port.
      GURL expected_origin =
          GURL(base::StringPrintf("http://%s", host.c_str()));
      EXPECT_TRUE(preconnect_manager_observer()->HasOriginAttemptedToPreconnect(
          expected_origin));
    }
  } else {
    for (const auto& host : expected_opt_guide_subresource_hosts) {
      EXPECT_FALSE(preconnect_manager_observer()->HasHostBeenLookedUp(
          host, network_anonymization_key));
    }
  }
}

class LoadingPredictorBrowserTestWithNoLocalPredictions
    : public LoadingPredictorBrowserTest {
 public:
  LoadingPredictorBrowserTestWithNoLocalPredictions() {
    feature_list_.InitAndDisableFeature(
        features::kLoadingPredictorUseLocalPredictions);
  }

 private:
  base::test::ScopedFeatureList feature_list_;
};

IN_PROC_BROWSER_TEST_F(LoadingPredictorBrowserTestWithNoLocalPredictions,
                       ShouldNotActOnLocalPrediction) {
  // Navigate the first time to fill the predictor's database and the HTTP
  // cache.
  GURL url = embedded_test_server()->GetURL(
      "test.com", GetPathWithPortReplacement(kHtmlSubresourcesPath,
                                             embedded_test_server()->port()));
  url::Origin origin = url::Origin::Create(url);
  net::SchemefulSite site = net::SchemefulSite(origin);
  auto network_anonymization_key =
      net::NetworkAnonymizationKey::CreateSameSite(std::move(site));
  ASSERT_TRUE(ui_test_utils::NavigateToURL(browser(), url));
  ResetNetworkState();

  auto observer = NavigateToURLAsync(url);
  EXPECT_TRUE(observer->WaitForRequestStart());
  // The initial URL should be preconnected to.
  preconnect_manager_observer()->WaitUntilHostLookedUp(
      url.GetHost(), network_anonymization_key);
  EXPECT_TRUE(preconnect_manager_observer()->HostFound(
      url.GetHost(), network_anonymization_key));
  EXPECT_TRUE(preconnect_manager_observer()->HasOriginAttemptedToPreconnect(
      origin.GetURL()));
  // 2 connections to the main frame host.
  const size_t expected_connections = 2;
  connection_tracker()->WaitForAcceptedConnections(expected_connections);
  EXPECT_EQ(expected_connections,
            connection_tracker()->GetAcceptedSocketCount());
  // No reads since all resources should be cached.
  EXPECT_EQ(0u, connection_tracker()->GetReadSocketCount());
}

// A fixture for testing prefetching with optimization guide hints.
class LoadingPredictorPrefetchBrowserTest
    : public LoadingPredictorBrowserTestWithOptimizationGuide {
 public:
  void SetUp() override {
    embedded_test_server()->RegisterRequestMonitor(base::BindRepeating(
        &LoadingPredictorPrefetchBrowserTest::MonitorRequest,
        base::Unretained(this)));

    LoadingPredictorBrowserTestWithOptimizationGuide::SetUp();
  }

  void SetUpCommandLine(base::CommandLine* command_line) override {
    LoadingPredictorBrowserTestWithOptimizationGuide::SetUpCommandLine(
        command_line);
    command_line->AppendSwitch(
        switches::kLoadingPredictorAllowLocalRequestForTesting);
  }

 protected:
  // Sets the requests to expect in WaitForRequests().
  void SetExpectedRequests(base::flat_set<GURL> requests) {
    expected_requests_ = std::move(requests);
  }

  // Returns once all expected requests have been received.
  void WaitForRequests() {
    if (expected_requests_.empty()) {
      return;
    }
    base::RunLoop loop;
    quit_ = loop.QuitClosure();
    loop.Run();
  }

 protected:
  void MonitorRequest(const net::test_server::HttpRequest& request) {
    // Monitor only prefetches.
    if (request.headers.find(blink::kSecPurposeHeaderName) ==
            request.headers.end() ||
        (request.headers.at(blink::kSecPurposeHeaderName) !=
         blink::kSecPurposePrefetchHeaderValue)) {
      return;
    }

    // |request.GetURL()| gives us the URL after it's already resolved to
    // 127.0.0.1, so reconstruct the requested host via the Host header
    // (which includes host+port).
    GURL url = request.GetURL();
    auto host_iter = request.headers.find("Host");
    if (host_iter != request.headers.end()) {
      url = GURL(std::string(request.GetURL().scheme()) + "://" +
                 host_iter->second + request.relative_url);
    }

    // Remove the expected request.
    auto it = expected_requests_.find(url);
    ASSERT_TRUE(it != expected_requests_.end())
        << "Got unexpected request: " << url;
    expected_requests_.erase(it);

    // Finish if done.
    if (expected_requests_.empty() && quit_) {
      std::move(quit_).Run();
    }
  }

  base::flat_set<GURL> expected_requests_;
  base::OnceClosure quit_;
};

// Tests that the LoadingPredictor performs prefetching
// for a navigation which it has a prediction for and there isn't a local
// prediction available.
IN_PROC_BROWSER_TEST_P(
    LoadingPredictorPrefetchBrowserTest,
    DISABLED_PrepareForPageLoadWithPredictionForPrefetchNoLocalHint) {
  GURL url = embedded_test_server()->GetURL(
      "test.com", GetPathWithPortReplacement(kHtmlSubresourcesPath,
                                             embedded_test_server()->port()));

  // Set up optimization hints.
  std::vector<Subresource> hints = {
      {"skipsoverinvalidurl/////",
       optimization_guide::proto::RESOURCE_TYPE_CSS},
      {embedded_test_server()->GetURL("subresource.com", "/css").spec(),
       optimization_guide::proto::RESOURCE_TYPE_CSS},
      {embedded_test_server()->GetURL("subresource.com", "/image").spec(),
       optimization_guide::proto::RESOURCE_TYPE_UNKNOWN},
      {embedded_test_server()->GetURL("otherresource.com", "/js").spec(),
       optimization_guide::proto::RESOURCE_TYPE_SCRIPT},
      {embedded_test_server()->GetURL("preconnect.com", "/other").spec(),
       optimization_guide::proto::RESOURCE_TYPE_UNKNOWN, true},
  };
  SetUpOptimizationHint(url, hints);

  // Expect these prefetches.
  std::vector<GURL> requests;
  if (GetSubresourceTypeParam() == "all") {
    requests = {embedded_test_server()->GetURL("subresource.com", "/css"),
                embedded_test_server()->GetURL("subresource.com", "/image"),
                embedded_test_server()->GetURL("otherresource.com", "/js")};
  } else if (GetSubresourceTypeParam() == "css") {
    requests = {embedded_test_server()->GetURL("subresource.com", "/css")};
  } else if (GetSubresourceTypeParam() == "js_css") {
    requests = {embedded_test_server()->GetURL("subresource.com", "/css"),
                embedded_test_server()->GetURL("otherresource.com", "/js")};
  }
  SetExpectedRequests(std::move(requests));

  // Start a navigation and observe these prefetches.
  auto observer = NavigateToURLAsync(url);
  EXPECT_TRUE(observer->WaitForRequestStart());
  WaitForRequests();

  // preconnect.com should be preconnected to.
  net::SchemefulSite site = net::SchemefulSite(url);
  auto network_anonymization_key =
      net::NetworkAnonymizationKey::CreateSameSite(site);
  preconnect_manager_observer()->WaitUntilHostLookedUp(
      "preconnect.com", network_anonymization_key);
  EXPECT_TRUE(preconnect_manager_observer()->HostFound(
      "preconnect.com", network_anonymization_key));
  EXPECT_TRUE(preconnect_manager_observer()->HasOriginAttemptedToPreconnect(
      embedded_test_server()->GetURL("preconnect.com", "/")));
}

// Tests that the LoadingPredictor performs prefetching
// for a navigation which it has a prediction for and there is a local
// prediction available.
IN_PROC_BROWSER_TEST_P(
    LoadingPredictorPrefetchBrowserTest,
    PrepareForPageLoadWithPredictionForPrefetchHasLocalHint) {
  // Navigate the first time to fill the predictor's database and the HTTP
  // cache.
  GURL url = embedded_test_server()->GetURL(
      "test.com", GetPathWithPortReplacement(kHtmlSubresourcesPath,
                                             embedded_test_server()->port()));
  ASSERT_TRUE(ui_test_utils::NavigateToURL(browser(), url));
  ResetNetworkState();

  // Set up optimization hints.
  std::vector<Subresource> hints = {
      {"skipsoverinvalidurl/////",
       optimization_guide::proto::RESOURCE_TYPE_CSS},
      {embedded_test_server()->GetURL("subresource.com", "/css").spec(),
       optimization_guide::proto::RESOURCE_TYPE_CSS},
      {embedded_test_server()->GetURL("subresource.com", "/image").spec(),
       optimization_guide::proto::RESOURCE_TYPE_UNKNOWN},
      {embedded_test_server()->GetURL("otherresource.com", "/js").spec(),
       optimization_guide::proto::RESOURCE_TYPE_SCRIPT},
      {embedded_test_server()->GetURL("preconnect.com", "/other").spec(),
       optimization_guide::proto::RESOURCE_TYPE_UNKNOWN, true},
  };
  SetUpOptimizationHint(url, hints);

  // Expect these prefetches.
  std::vector<GURL> requests;
  if (GetSubresourceTypeParam() == "all") {
    requests = {embedded_test_server()->GetURL("subresource.com", "/css"),
                embedded_test_server()->GetURL("subresource.com", "/image"),
                embedded_test_server()->GetURL("otherresource.com", "/js")};
  } else if (GetSubresourceTypeParam() == "css") {
    requests = {embedded_test_server()->GetURL("subresource.com", "/css")};
  } else if (GetSubresourceTypeParam() == "js_css") {
    requests = {embedded_test_server()->GetURL("subresource.com", "/css"),
                embedded_test_server()->GetURL("otherresource.com", "/js")};
  }
  SetExpectedRequests(std::move(requests));

  // Start a navigation and observe these prefetches.
  auto observer = NavigateToURLAsync(url);
  EXPECT_TRUE(observer->WaitForRequestStart());
  WaitForRequests();

  std::vector<std::string> expected_subresource_hosts;
  if (IsLocalPredictionEnabled()) {
    // Should use subresources that were learned.
    expected_subresource_hosts = {"baz.com", "foo.com"};
  } else {
    // Should use subresources from optimization hint.
    expected_subresource_hosts = {"preconnect.com"};
  }
  net::SchemefulSite site = net::SchemefulSite(url);
  auto network_anonymization_key =
      net::NetworkAnonymizationKey::CreateSameSite(site);
  for (const auto& host : expected_subresource_hosts) {
    preconnect_manager_observer()->WaitUntilHostLookedUp(
        host, network_anonymization_key);
    EXPECT_TRUE(preconnect_manager_observer()->HostFound(
        host, network_anonymization_key));
    EXPECT_TRUE(preconnect_manager_observer()->HasOriginAttemptedToPreconnect(
        embedded_test_server()->GetURL(host, "/")));
  }
}

// A fixture for testing prefetching with the local resource check not bypassed.
// The normal fixture bypasses the check so that the embedded test server can be
// used.
class LoadingPredictorPrefetchBrowserTestWithBlockedLocalRequest
    : public LoadingPredictorPrefetchBrowserTest {
 public:
  LoadingPredictorPrefetchBrowserTestWithBlockedLocalRequest() = default;

  // Override to prevent adding kLoadingPredictorAllowLocalRequestForTesting
  // here.
  void SetUpCommandLine(base::CommandLine* command_line) override {}
};

// Test that prefetches to local resources are blocked.
// Disabled for being flaky. crbug.com/40144804
IN_PROC_BROWSER_TEST_P(
    LoadingPredictorPrefetchBrowserTestWithBlockedLocalRequest,
    DISABLED_PrepareForPageLoadWithPredictionForPrefetch) {
  GURL url = embedded_test_server()->GetURL(
      "test.com", GetPathWithPortReplacement(kHtmlSubresourcesPath,
                                             embedded_test_server()->port()));

  GURL hint_url = embedded_test_server()->GetURL("subresource.com", "/css");

  // Set up one optimization hint.
  std::vector<Subresource> hints = {
      {hint_url.spec(), optimization_guide::proto::RESOURCE_TYPE_CSS},
  };
  SetUpOptimizationHint(url, hints);

  // Start a navigation which triggers prefetch.
  auto observer = NavigateToURLAsync(url);
  EXPECT_TRUE(observer->WaitForRequestStart());

  // The prefetch should have failed.
  prefetch_manager_observer()->WaitForPrefetchesForNavigation(url);
  auto results = prefetch_manager_observer()->results();
  ASSERT_THAT(results, SizeIs(1));

  const auto& status = results[0].status;
  EXPECT_EQ(status.error_code, net::ERR_FAILED);
  EXPECT_THAT(status.cors_error_status,
              Optional(network::CorsErrorStatus(
                  network::mojom::CorsError::kInsecureLocalNetwork,
                  network::mojom::IPAddressSpace::kLoopback)));
}

// This fixture is for disabling prefetching via test suite instantiation to
// test the counterfactual arm (|always_retrieve_predictions| is
// true but using the predictions is disabled).
class LoadingPredictorPrefetchCounterfactualBrowserTest
    : public LoadingPredictorPrefetchBrowserTest {};

IN_PROC_BROWSER_TEST_P(
    LoadingPredictorPrefetchCounterfactualBrowserTest,
    PrepareForPageLoadWithPredictionForPrefetchHasLocalHint) {
  // Assert that this tests the counterfactual arm.
  ASSERT_TRUE(features::ShouldAlwaysRetrieveOptimizationGuidePredictions());
  ASSERT_FALSE(features::ShouldUseOptimizationGuidePredictions());

  // Navigate the first time to fill the predictor's database and the HTTP
  // cache.
  GURL url = embedded_test_server()->GetURL(
      "test.com", GetPathWithPortReplacement(kHtmlSubresourcesPath,
                                             embedded_test_server()->port()));
  ASSERT_TRUE(ui_test_utils::NavigateToURL(browser(), url));
  ResetNetworkState();

  // Set up optimization hints.
  std::vector<Subresource> hints = {
      {"skipsoverinvalidurl/////",
       optimization_guide::proto::RESOURCE_TYPE_CSS},
      {embedded_test_server()->GetURL("subresource.com", "/css").spec(),
       optimization_guide::proto::RESOURCE_TYPE_CSS},
      {embedded_test_server()->GetURL("subresource.com", "/image").spec(),
       optimization_guide::proto::RESOURCE_TYPE_UNKNOWN},
      {embedded_test_server()->GetURL("otherresource.com", "/js").spec(),
       optimization_guide::proto::RESOURCE_TYPE_SCRIPT},
      {embedded_test_server()->GetURL("preconnect.com", "/other").spec(),
       optimization_guide::proto::RESOURCE_TYPE_UNKNOWN, true},
  };
  SetUpOptimizationHint(url, hints);

  // Expect no prefetches. The test will fail if any prefetch requests are
  // issued.
  SetExpectedRequests({});

  // Start a navigation.
  auto observer = NavigateToURLAsync(url);
  EXPECT_TRUE(observer->WaitForRequestStart());

  std::vector<std::string> expected_subresource_hosts;
  if (IsLocalPredictionEnabled()) {
    // Should use subresources that were learned.
    expected_subresource_hosts = {"baz.com", "foo.com"};
  } else {
    // Should not use subresources from optimization hint since
    // use_predictions is disabled.
  }
  net::SchemefulSite site = net::SchemefulSite(url);
  auto network_anonymization_key =
      net::NetworkAnonymizationKey::CreateSameSite(site);
  for (const auto& host : expected_subresource_hosts) {
    preconnect_manager_observer()->WaitUntilHostLookedUp(
        host, network_anonymization_key);
    EXPECT_TRUE(preconnect_manager_observer()->HostFound(
        host, network_anonymization_key));
    EXPECT_TRUE(preconnect_manager_observer()->HasOriginAttemptedToPreconnect(
        embedded_test_server()->GetURL(host, "/")));
  }

  // Run the run loop to give the test a chance to fail by issuing a prefetch.
  // We don't have an explicit signal for the prefetch manager *not* starting.
  base::RunLoop().RunUntilIdle();
  EXPECT_TRUE(prefetch_manager_observer()->results().empty());
}

INSTANTIATE_TEST_SUITE_P(
    ,
    LoadingPredictorPrefetchBrowserTest,
    testing::Combine(
        /*IsLocalPredictionEnabled()=*/testing::Values(true, false),
        /*ShouldUseOptimizationGuidePredictions()=*/
        testing::Values(true),
        /*IsPrefetchEnabled()=*/testing::Values(true),
        /*GetSubresourceType()=*/testing::Values("all", "css", "js_css")));

// For the "BlockedLocalRequest" test, the params largely don't matter. We just
// need to enable prefetching and test one configuration, since the test passes
// if the prefetch is blocked.
INSTANTIATE_TEST_SUITE_P(
    ,
    LoadingPredictorPrefetchBrowserTestWithBlockedLocalRequest,
    testing::Combine(
        /*IsLocalPredictionEnabled()=*/testing::Values(false),
        /*ShouldUseOptimizationGuidePredictions()=*/
        testing::Values(true),
        /*IsPrefetchEnabled()=*/testing::Values(true),
        /*GetSubresourceType()=*/testing::Values("all")));

// For the "prefetch counterfactual" test, we want to retrieve the optimization
// guide hints but not use them, so set ShouldUseOptimizationGuidePredictions()
// to false. It doesn't matter if IsPrefetchEnabled() is true or not, since
// prefetching only uses optimization guide predictions.
INSTANTIATE_TEST_SUITE_P(
    ,
    LoadingPredictorPrefetchCounterfactualBrowserTest,
    testing::Combine(
        /*IsLocalPredictionEnabled()=*/testing::Values(true, false),
        /*ShouldUseOptimizationGuidePredictions()=*/
        testing::Values(false),
        /*IsPrefetchEnabled()=*/testing::Values(true),
        /*GetSubresourceType()=*/testing::Values("all")));

// Tests that LoadingPredictorTabHelper ignores prerender navigations and
// page activations.
class LoadingPredictorMultiplePageBrowserTest
    : public LoadingPredictorBrowserTest {
 public:
  LoadingPredictorMultiplePageBrowserTest()
      : prerender_test_helper_(base::BindRepeating(
            &LoadingPredictorMultiplePageBrowserTest::GetWebContents,
            base::Unretained(this))) {}

  content::test::PrerenderTestHelper& prerender_test_helper() {
    return prerender_test_helper_;
  }

  content::WebContents* GetWebContents() {
    return browser()->GetTabStripModel()->GetActiveWebContents();
  }

 protected:
  void SetUp() override {
    prerender_test_helper_.RegisterServerRequestMonitor(embedded_test_server());
    LoadingPredictorBrowserTest::SetUp();
  }

 private:
  content::test::PrerenderTestHelper prerender_test_helper_;
};

IN_PROC_BROWSER_TEST_F(LoadingPredictorMultiplePageBrowserTest,
                       PrerenderNavigationNotObserved) {
  GURL first_main = embedded_test_server()->GetURL("/title1.html");
  GURL prerender = embedded_test_server()->GetURL("/title2.html");
  GURL second_main = embedded_test_server()->GetURL("/title3.html");
  auto* loading_predictor = predictors::LoadingPredictorFactory::GetForProfile(
      browser()->GetProfile());

  // Start navigation in the primary main frame.
  auto first_main_observer = std::make_unique<content::TestNavigationManager>(
      GetWebContents(), first_main);
  GetWebContents()->GetController().LoadURL(first_main, content::Referrer(),
                                            ui::PAGE_TRANSITION_TYPED,
                                            std::string());
  ASSERT_TRUE(first_main_observer->WaitForRequestStart());
  EXPECT_EQ(1u, loading_predictor->GetActiveNavigationsSizeForTesting());
  ASSERT_TRUE(first_main_observer->WaitForNavigationFinished());
  EXPECT_EQ(0u, loading_predictor->GetActiveNavigationsSizeForTesting());
  content::WaitForLoadStop(GetWebContents());
  EXPECT_EQ(1u, loading_predictor->GetTotalHintsActivatedForTesting());

  // Start a prerender and a navigation in the primary main frame so we have 2
  // concurrent navigations.
  auto prerender_observer = std::make_unique<content::TestNavigationManager>(
      GetWebContents(), prerender);
  auto second_main_observer = std::make_unique<content::TestNavigationManager>(
      GetWebContents(), second_main);
  prerender_test_helper().AddPrerenderAsync(prerender);
  GetWebContents()->GetController().LoadURL(second_main, content::Referrer(),
                                            ui::PAGE_TRANSITION_TYPED,
                                            std::string());
  ASSERT_TRUE(prerender_observer->WaitForRequestStart());
  ASSERT_TRUE(second_main_observer->WaitForRequestStart());
  EXPECT_EQ(1u, loading_predictor->GetActiveNavigationsSizeForTesting());
  ASSERT_TRUE(prerender_observer->WaitForNavigationFinished());
  EXPECT_EQ(1u, loading_predictor->GetActiveNavigationsSizeForTesting());
  ASSERT_TRUE(second_main_observer->WaitForNavigationFinished());
  EXPECT_EQ(0u, loading_predictor->GetActiveNavigationsSizeForTesting());

  content::WaitForLoadStop(GetWebContents());
  EXPECT_EQ(2u, loading_predictor->GetTotalHintsActivatedForTesting());
}

IN_PROC_BROWSER_TEST_F(LoadingPredictorMultiplePageBrowserTest,
                       PrerenderActivationNotObserved) {
  GURL main_url = embedded_test_server()->GetURL("/title1.html");
  GURL prerender_url = embedded_test_server()->GetURL("/title2.html");
  auto* loading_predictor = predictors::LoadingPredictorFactory::GetForProfile(
      browser()->GetProfile());

  // Navigate primary main frame.
  GetWebContents()->GetController().LoadURL(
      main_url, content::Referrer(), ui::PAGE_TRANSITION_TYPED, std::string());
  content::WaitForLoadStop(GetWebContents());
  EXPECT_EQ(1u, loading_predictor->GetTotalHintsActivatedForTesting());

  // Start a prerender.
  prerender_test_helper().AddPrerender(prerender_url);
  EXPECT_EQ(1u, loading_predictor->GetTotalHintsActivatedForTesting());

  // Activate the prerender.
  prerender_test_helper().NavigatePrimaryPage(prerender_url);
  EXPECT_EQ(1u, loading_predictor->GetTotalHintsActivatedForTesting());
}

// TODO(crbug.com/325336071): Re-enable this test
#if BUILDFLAG(IS_LINUX)
#define MAYBE_BackForwardCacheNavigationNotObserved \
  DISABLED_BackForwardCacheNavigationNotObserved
#else
#define MAYBE_BackForwardCacheNavigationNotObserved \
  BackForwardCacheNavigationNotObserved
#endif
IN_PROC_BROWSER_TEST_F(LoadingPredictorMultiplePageBrowserTest,
                       MAYBE_BackForwardCacheNavigationNotObserved) {
  GURL url_1 = embedded_test_server()->GetURL("a.com", "/title1.html");
  GURL url_2 = embedded_test_server()->GetURL("b.com", "/title2.html");
  auto* loading_predictor = predictors::LoadingPredictorFactory::GetForProfile(
      browser()->GetProfile());

  // Navigate primary main frame twice.
  ASSERT_TRUE(content::NavigateToURL(GetWebContents(), url_1));
  content::RenderFrameHostWrapper rfh_1(
      GetWebContents()->GetPrimaryMainFrame());
  ASSERT_TRUE(content::NavigateToURL(GetWebContents(), url_2));
  ASSERT_EQ(rfh_1->GetLifecycleState(),
            content::RenderFrameHost::LifecycleState::kInBackForwardCache);
  EXPECT_EQ(2u, loading_predictor->GetTotalHintsActivatedForTesting());

  // Go back (using BackForwardCache).
  ASSERT_TRUE(content::HistoryGoBack(GetWebContents()));
  ASSERT_EQ(GetWebContents()->GetPrimaryMainFrame(), rfh_1.get());
  EXPECT_EQ(2u, loading_predictor->GetTotalHintsActivatedForTesting());
}

// Test interaction with fenced frames.
class FencedFrameLoadingPredictorBrowserTest
    : public LoadingPredictorBrowserTest {
 public:
  void SetUpOnMainThread() override {
    LoadingPredictorBrowserTest::SetUpOnMainThread();

    // Set up the embedded https test server for fenced frame which requires a
    // secure context to load.
    embedded_https_test_server().SetSSLConfig(
        net::EmbeddedTestServer::CERT_TEST_NAMES);

    // Add content/test/data for cross_site_iframe_factory.html.
    embedded_https_test_server().ServeFilesFromSourceDirectory(
        "content/test/data");
    embedded_https_test_server().ServeFilesFromDirectory(
        GetChromeTestDataDir());
  }

  content::test::FencedFrameTestHelper& fenced_frame_test_helper() {
    return fenced_frame_test_helper_;
  }

  content::WebContents* GetWebContents() {
    return browser()->GetTabStripModel()->GetActiveWebContents();
  }

 private:
  content::test::FencedFrameTestHelper fenced_frame_test_helper_;
};

// Verify DNS prefetch is working in fenced frame.
IN_PROC_BROWSER_TEST_F(FencedFrameLoadingPredictorBrowserTest, DnsPrefetch) {
  ASSERT_TRUE(embedded_https_test_server().Start());

  // Navigate to a page that contains a fenced frame.
  const GURL main_url = embedded_https_test_server().GetURL(
      "a.test", "/cross_site_iframe_factory.html?a.test(a.test{fenced})");
  EXPECT_TRUE(ui_test_utils::NavigateToURL(browser(), main_url));

  // Get fenced frame render frame host.
  std::vector<content::RenderFrameHost*> child_frames =
      fenced_frame_test_helper().GetChildFencedFrameHosts(
          GetWebContents()->GetPrimaryMainFrame());
  ASSERT_EQ(child_frames.size(), 1u);
  content::RenderFrameHost* fenced_frame_rfh = child_frames[0];

  // Get fenced frame NetworkAnonymizationKey.
  const net::NetworkAnonymizationKey& network_anonymization_key =
      fenced_frame_rfh->GetIsolationInfoForSubresources()
          .network_anonymization_key();

  GURL dns_prefetch_url("https://chromium.org");

  // Add a link element in fenced frame that does a DNS prefetch.
  EXPECT_TRUE(ExecJs(fenced_frame_rfh, content::JsReplace(R"(
                    var link_element = document.createElement('link');
                    link_element.href = $1;
                    link_element.rel = 'dns-prefetch';
                    document.body.appendChild(link_element);
          )",
                                                          dns_prefetch_url)));

  // The observer should observe a DNS prefetch which succeeds.
  preconnect_manager_observer()->WaitUntilHostLookedUp(
      dns_prefetch_url.GetHost(), network_anonymization_key);
  EXPECT_TRUE(preconnect_manager_observer()->HasHostBeenLookedUp(
      dns_prefetch_url.GetHost(), network_anonymization_key));
  EXPECT_TRUE(preconnect_manager_observer()->HostFound(
      dns_prefetch_url.GetHost(), network_anonymization_key));
}

// TODO(crbug.com/489349560): Allow `LoadingPredictorBrowserTest` to specify the
// type of EmbeddedTestServer, instead of the subclass to construct its own.
class ConnectionAllowlistLoadingPredictorBrowserTest
    : public LoadingPredictorBrowserTest {
 public:
  ConnectionAllowlistLoadingPredictorBrowserTest() {
    feature_list_.InitWithFeatures(
        /*enabled_features=*/{network::features::kConnectionAllowlists},
        /*disabled_features=*/{});
  }

  // Note: `LoadingPredictorBrowserTest::SetUpOnMainThread()` sets up the
  // ConnectionTracker on `embedded_test_server()`. If the tests need to
  // use ConnectionTracker, it should construct a separate one on
  // `embedded_https_test_server()`, instead of using `connection_tracker()`.
  void SetUpOnMainThread() override {
    LoadingPredictorBrowserTest::SetUpOnMainThread();

    mock_cert_verifier_.mock_cert_verifier()->set_default_result(net::OK);
    embedded_https_test_server().SetSSLConfig(
        net::EmbeddedTestServer::CERT_TEST_NAMES);
    embedded_https_test_server().AddDefaultHandlers();
    embedded_https_test_server().RegisterRequestHandler(
        base::BindRepeating(&ConnectionAllowlistLoadingPredictorBrowserTest::
                                HandleMainFrameRequest));
    embedded_https_test_server().RegisterRequestHandler(base::BindRepeating(
        &ConnectionAllowlistLoadingPredictorBrowserTest::
            HandleMainFrameRequestWithDNSPrefetchLinkHeader));
    ASSERT_TRUE(embedded_https_test_server().Start());
  }

  void SetUpCommandLine(base::CommandLine* command_line) override {
    LoadingPredictorBrowserTest::SetUpCommandLine(command_line);
    mock_cert_verifier_.SetUpCommandLine(command_line);
  }

  void SetUpInProcessBrowserTestFixture() override {
    LoadingPredictorBrowserTest::SetUpInProcessBrowserTestFixture();
    mock_cert_verifier_.SetUpInProcessBrowserTestFixture();
  }

  void TearDownInProcessBrowserTestFixture() override {
    LoadingPredictorBrowserTest::TearDownInProcessBrowserTestFixture();
    mock_cert_verifier_.TearDownInProcessBrowserTestFixture();
  }

  static std::unique_ptr<net::test_server::HttpResponse> HandleMainFrameRequest(
      const net::test_server::HttpRequest& request) {
    if (request.relative_url != "/connection-allowlist") {
      return nullptr;
    }

    auto http_response =
        std::make_unique<net::test_server::BasicHttpResponse>();
    http_response->set_code(net::HTTP_OK);
    http_response->AddCustomHeader("Connection-Allowlist",
                                   "(\"https://a.test\" \"https://b.test\")");
    return http_response;
  }

  static std::unique_ptr<net::test_server::HttpResponse>
  HandleMainFrameRequestWithDNSPrefetchLinkHeader(
      const net::test_server::HttpRequest& request) {
    if (request.relative_url != "/connection-allowlist-dns-prefetch") {
      return nullptr;
    }

    auto http_response =
        std::make_unique<net::test_server::BasicHttpResponse>();
    http_response->set_code(net::HTTP_OK);
    http_response->AddCustomHeader("Connection-Allowlist",
                                   "(\"https://a.test\" \"https://b.test\")");
    http_response->AddCustomHeader("Link",
                                   "<https://b.test>; rel=dns-prefetch");
    http_response->AddCustomHeader("Link",
                                   "<https://c.test>; rel=dns-prefetch");
    return http_response;
  }

 private:
  base::test::ScopedFeatureList feature_list_;
  content::ContentMockCertVerifier mock_cert_verifier_;
};

// Verify that DNS prefetch fails when requests to the host are prevented by the
// connection allowlist.
// TODO(crbug.com/447954811): This test only applies to DNS prefetch initiated
// by a <link>, and does not test HTTP 103 Early Hints headers. Once we
// resolve how to handle them in
// https://github.com/WICG/connection-allowlists/issues/3, we can add the
// appropriate test as well.
IN_PROC_BROWSER_TEST_F(ConnectionAllowlistLoadingPredictorBrowserTest,
                       ConnectionAllowlistDnsPrefetchFails) {
  // Navigate the main frame to a page with a connection allowlist.
  const GURL main_url =
      embedded_https_test_server().GetURL("a.test", "/connection-allowlist");
  EXPECT_TRUE(ui_test_utils::NavigateToURL(browser(), main_url));

  GURL dns_prefetch_url("https://c.test");

  content::RenderFrameHost* main_frame_rfh = browser()
                                                 ->GetTabStripModel()
                                                 ->GetActiveWebContents()
                                                 ->GetPrimaryMainFrame();
  // Add a link element that does a DNS prefetch.
  EXPECT_TRUE(ExecJs(main_frame_rfh, content::JsReplace(R"(
            var link_element = document.createElement('link');
            link_element.href = $1;
            link_element.rel = 'dns-prefetch';
            document.body.appendChild(link_element);
          )",
                                                        dns_prefetch_url)));

  net::NetworkAnonymizationKey network_anonymization_key =
      main_frame_rfh->GetIsolationInfoForSubresources()
          .network_anonymization_key();
  // The observer should observe a DNS prefetch which is cancelled.
  preconnect_manager_observer()->WaitUntilHostLookedUp(
      dns_prefetch_url.GetHost(), network_anonymization_key);

  // The host is looked up, but the lookup is eventually cancelled because
  // `dns_prefetch_url` does not match the allowlist.
  EXPECT_TRUE(preconnect_manager_observer()->HasHostBeenLookedUp(
      dns_prefetch_url.GetHost(), network_anonymization_key));
  EXPECT_FALSE(preconnect_manager_observer()->HostFound(
      dns_prefetch_url.GetHost(), network_anonymization_key));
}

// Verify that DNS prefetch succeeds when requests to the host are allowed by
// connection allowlist.
IN_PROC_BROWSER_TEST_F(ConnectionAllowlistLoadingPredictorBrowserTest,
                       ConnectionAllowlistDnsPrefetchSucceeds) {
  // Navigate the main frame to a page with a connection allowlist.
  const GURL main_url =
      embedded_https_test_server().GetURL("a.test", "/connection-allowlist");
  EXPECT_TRUE(ui_test_utils::NavigateToURL(browser(), main_url));

  // This URL is allowed by the Connection-Allowlist header.
  GURL dns_prefetch_url("https://b.test");

  content::RenderFrameHost* main_frame_rfh = browser()
                                                 ->GetTabStripModel()
                                                 ->GetActiveWebContents()
                                                 ->GetPrimaryMainFrame();
  // Add a link element that does a DNS prefetch.
  EXPECT_TRUE(ExecJs(main_frame_rfh, content::JsReplace(R"(
            var link_element = document.createElement('link');
            link_element.href = $1;
            link_element.rel = 'dns-prefetch';
            document.body.appendChild(link_element);
          )",
                                                        dns_prefetch_url)));

  net::NetworkAnonymizationKey network_anonymization_key =
      main_frame_rfh->GetIsolationInfoForSubresources()
          .network_anonymization_key();
  // The observer should observe a DNS prefetch.
  preconnect_manager_observer()->WaitUntilHostLookedUp(
      dns_prefetch_url.GetHost(), network_anonymization_key);

  // The host is looked up successfully because dns_prefetch_url is in the
  // connection allowlist.
  EXPECT_TRUE(preconnect_manager_observer()->HasHostBeenLookedUp(
      dns_prefetch_url.GetHost(), network_anonymization_key));
  EXPECT_TRUE(preconnect_manager_observer()->HostFound(
      dns_prefetch_url.GetHost(), network_anonymization_key));
}

// Verify that window.open() to a host disallowed by the initiator's
// Connection-Allowlist does not leak the destination host's DNS via the
// speculative navigation preconnect. The navigation is blocked, so no
// speculative network activity (which would carry no enforcing
// network_restrictions_id) must reach the destination host.
// Regression test for https://github.com/WICG/connection-allowlists/issues/11.
IN_PROC_BROWSER_TEST_F(ConnectionAllowlistLoadingPredictorBrowserTest,
                       ConnectionAllowlistWindowOpenNoTargetNoDnsLeak) {
  // Navigate the main frame to a page that enforces a Connection-Allowlist of
  // {a.test, b.test}.
  const GURL main_url =
      embedded_https_test_server().GetURL("a.test", "/connection-allowlist");
  ASSERT_TRUE(ui_test_utils::NavigateToURL(browser(), main_url));

  content::RenderFrameHost* main_frame_rfh = browser()
                                                 ->GetTabStripModel()
                                                 ->GetActiveWebContents()
                                                 ->GetPrimaryMainFrame();
  ASSERT_FALSE(main_frame_rfh->GetNetworkRestrictionsID().is_empty());

  // c.test is not on the allowlist, so opening it must be blocked. The
  // speculative navigation preconnect must not resolve its DNS.
  const GURL disallowed_url("https://c.test");
  const net::NetworkAnonymizationKey disallowed_nak =
      net::NetworkAnonymizationKey::CreateSameSite(
          net::SchemefulSite(disallowed_url));

  content::WebContentsAddedObserver popup_observer;
  ASSERT_TRUE(content::ExecJs(
      main_frame_rfh, content::JsReplace("window.open($1);", disallowed_url)));
  content::WebContents* popup = popup_observer.GetWebContents();
  ASSERT_TRUE(popup);
  // The popup navigation is blocked by the Connection-Allowlist; wait for it to
  // finish so DidStartNavigation() (and any speculative preconnect it would
  // trigger) has run. The blocked navigation commits an error page, so
  // WaitForLoadStop() reports failure.
  EXPECT_FALSE(content::WaitForLoadStop(popup));
  // Confirm the navigation really was blocked (committed an error page) so the
  // assertions below test the blocked path rather than passing vacuously.
  ASSERT_TRUE(popup->GetController().GetLastCommittedEntry());
  EXPECT_EQ(content::PAGE_TYPE_ERROR,
            popup->GetController().GetLastCommittedEntry()->GetPageType());

  // Synchronize on an allowed dns-prefetch from the opener. Preconnect jobs run
  // concurrently (up to PreconnectManagerImpl::kMaxInflightPreresolves) and
  // their lookups can finish in any order, so this is a run-loop barrier, not an
  // in-order guarantee. The speculative preconnect for the blocked
  // window.open() navigation, if it were attempted, has its DNS resolve started
  // while that navigation is handled -- before this dns-prefetch is even issued
  // -- and these localhost lookups settle during the run-loop spins here. So
  // once this allowed lookup is observed (and pending callbacks are drained
  // below), a disallowed-host lookup/preconnect would already have been recorded
  // if the leak were present.
  const GURL allowed_url("https://b.test");
  const net::NetworkAnonymizationKey allowed_nak =
      main_frame_rfh->GetIsolationInfoForSubresources()
          .network_anonymization_key();
  ASSERT_TRUE(content::ExecJs(main_frame_rfh, content::JsReplace(R"(
            var link = document.createElement('link');
            link.rel = 'dns-prefetch';
            link.href = $1;
            document.body.appendChild(link);
          )",
                                                                 allowed_url)));
  preconnect_manager_observer()->WaitUntilHostLookedUp(allowed_url.GetHost(),
                                                       allowed_nak);
  ASSERT_TRUE(preconnect_manager_observer()->HostFound(allowed_url.GetHost(),
                                                       allowed_nak));

  // Drain any pending preconnect-manager completion callbacks so the checks
  // below observe fully-settled state.
  base::RunLoop().RunUntilIdle();

  // The HasHostBeenLookedUp call triggers because the preresolve reaches the
  // network service, but the host is never found and the preconnect never
  // occurs due to the Connection Allowlist.
  EXPECT_FALSE(preconnect_manager_observer()->HasHostBeenLookedUp(
      disallowed_url.GetHost(), disallowed_nak));
  EXPECT_FALSE(preconnect_manager_observer()->HostFound(
      disallowed_url.GetHost(), disallowed_nak));
  EXPECT_FALSE(preconnect_manager_observer()->HasOriginAttemptedToPreconnect(
      GURL("https://c.test/")));
}

IN_PROC_BROWSER_TEST_F(ConnectionAllowlistLoadingPredictorBrowserTest,
                       ConnectionAllowlistLinkHeaderDnsPrefetch) {
  // Navigate the main frame to a page with a Connection Allowlist and "Link
  // rel=dns-prefetch" headers.
  const GURL main_url = embedded_https_test_server().GetURL(
      "a.test", "/connection-allowlist-dns-prefetch");
  EXPECT_TRUE(ui_test_utils::NavigateToURL(browser(), main_url));

  // Create the NAK that should be used for DNS prefetch, which should just be
  // the same as the key for a.test
  net::NetworkAnonymizationKey network_anonymization_key =
      net::NetworkAnonymizationKey::CreateSameSite(
          net::SchemefulSite(main_url));

  // Host lookup for the allowed host should complete successfully.
  GURL allowed_prefetch_url("https://b.test");
  preconnect_manager_observer()->WaitUntilHostLookedUp(
      allowed_prefetch_url.GetHost(), network_anonymization_key);
  EXPECT_TRUE(preconnect_manager_observer()->HasHostBeenLookedUp(
      allowed_prefetch_url.GetHost(), network_anonymization_key));
  EXPECT_TRUE(preconnect_manager_observer()->HostFound(
      allowed_prefetch_url.GetHost(), network_anonymization_key));

  // Host lookup for the disallowed host should fail, because it is not in the
  // allowlist.
  GURL denied_prefetch_url("https://c.test");
  preconnect_manager_observer()->WaitUntilHostLookedUp(
      denied_prefetch_url.GetHost(), network_anonymization_key);
  EXPECT_TRUE(preconnect_manager_observer()->HasHostBeenLookedUp(
      denied_prefetch_url.GetHost(), network_anonymization_key));
  EXPECT_FALSE(preconnect_manager_observer()->HostFound(
      denied_prefetch_url.GetHost(), network_anonymization_key));
}

// Verify that Connection-Allowlist policy is inherited by about:blank
// iframes. When a parent document has Connection-Allowlist active, hints
// injected into an about:blank iframe's contentDocument should be blocked
// for non-allowlisted hosts.
// Bug: crbug.com/496096539, crbug.com/496907108
IN_PROC_BROWSER_TEST_F(ConnectionAllowlistLoadingPredictorBrowserTest,
                       ConnectionAllowlistAboutBlankIframePolicyInheritance) {
  const GURL main_url =
      embedded_https_test_server().GetURL("a.test", "/connection-allowlist");
  EXPECT_TRUE(ui_test_utils::NavigateToURL(browser(), main_url));

  GURL dns_prefetch_url("https://c.test");

  content::RenderFrameHost* main_frame_rfh = browser()
                                                 ->GetTabStripModel()
                                                 ->GetActiveWebContents()
                                                 ->GetPrimaryMainFrame();
  EXPECT_FALSE(main_frame_rfh->GetNetworkRestrictionsID().is_empty());

  // Create an iframe and wait for it to load. The initial about:blank
  // document should now correctly inherit policies and get a
  // network_restrictions_id.
  content::TestNavigationObserver observer(
      browser()->GetTabStripModel()->GetActiveWebContents());
  EXPECT_TRUE(ExecJs(main_frame_rfh, R"(
            var iframe = document.createElement('iframe');
            document.body.appendChild(iframe);
          )"));
  observer.Wait();

  content::RenderFrameHost* child_frame_rfh =
      content::ChildFrameAt(main_frame_rfh, 0);
  ASSERT_TRUE(child_frame_rfh);
  EXPECT_FALSE(child_frame_rfh->GetNetworkRestrictionsID().is_empty());

  net::NetworkAnonymizationKey network_anonymization_key =
      child_frame_rfh->GetIsolationInfoForSubresources()
          .network_anonymization_key();

  // Inject a dns-prefetch link into the iframe's contentDocument.
  EXPECT_TRUE(ExecJs(child_frame_rfh, content::JsReplace(R"(
                var link = document.createElement('link');
                link.rel = 'dns-prefetch';
                link.href = $1;
                document.head.appendChild(link);
          )",
                                                         dns_prefetch_url)));

  preconnect_manager_observer()->WaitUntilHostLookedUp(
      dns_prefetch_url.GetHost(), network_anonymization_key);

  EXPECT_TRUE(preconnect_manager_observer()->HasHostBeenLookedUp(
      dns_prefetch_url.GetHost(), network_anonymization_key));
  EXPECT_FALSE(preconnect_manager_observer()->HostFound(
      dns_prefetch_url.GetHost(), network_anonymization_key))
      << "DNS lookup via initial about:blank iframe contentWindow injection "
         "should be blocked for non-allowlisted host.";
}

// Verify that Connection-Allowlist policy is inherited by space-prefixed
// " about:" iframes. The space before "about:" should not prevent policy
// inheritance from the parent document.
IN_PROC_BROWSER_TEST_F(
    ConnectionAllowlistLoadingPredictorBrowserTest,
    ConnectionAllowlistSpacePrefixedAboutIframePolicyInheritance) {
  const GURL main_url =
      embedded_https_test_server().GetURL("a.test", "/connection-allowlist");
  EXPECT_TRUE(ui_test_utils::NavigateToURL(browser(), main_url));

  GURL dns_prefetch_url("https://c.test");

  content::RenderFrameHost* main_frame_rfh = browser()
                                                 ->GetTabStripModel()
                                                 ->GetActiveWebContents()
                                                 ->GetPrimaryMainFrame();
  content::TestNavigationObserver observer(
      browser()->GetTabStripModel()->GetActiveWebContents());
  EXPECT_TRUE(ExecJs(main_frame_rfh, R"(
            var iframe = document.createElement('iframe');
            iframe.src = ' about:';
            document.body.appendChild(iframe);
          )"));
  observer.Wait();

  content::RenderFrameHost* child_frame_rfh =
      content::ChildFrameAt(main_frame_rfh, 0);
  ASSERT_TRUE(child_frame_rfh);
  net::NetworkAnonymizationKey network_anonymization_key =
      child_frame_rfh->GetIsolationInfoForSubresources()
          .network_anonymization_key();

  // Inject a dns-prefetch link into the iframe's contentDocument.
  EXPECT_TRUE(ExecJs(child_frame_rfh, content::JsReplace(R"(
              var link = document.createElement('link');
              link.rel = 'dns-prefetch';
              link.href = $1;
              document.head.appendChild(link);
            )",
                                                         dns_prefetch_url)));

  preconnect_manager_observer()->WaitUntilHostLookedUp(
      dns_prefetch_url.GetHost(), network_anonymization_key);

  EXPECT_TRUE(preconnect_manager_observer()->HasHostBeenLookedUp(
      dns_prefetch_url.GetHost(), network_anonymization_key));
  EXPECT_FALSE(preconnect_manager_observer()->HostFound(
      dns_prefetch_url.GetHost(), network_anonymization_key))
      << "DNS lookup via space-prefixed about: iframe contentWindow "
         "injection should be blocked for non-allowlisted host.";
}

// Verify that entity-encoded rel attributes (e.g. rel="preco&#110;&#110;ect"
// decoded by the HTML parser to "preconnect") are subject to
// Connection-Allowlist enforcement when injected into an iframe's
// contentDocument via innerHTML.
IN_PROC_BROWSER_TEST_F(
    ConnectionAllowlistLoadingPredictorBrowserTest,
    ConnectionAllowlistEntityEncodedRelIframePolicyInheritance) {
  const GURL main_url =
      embedded_https_test_server().GetURL("a.test", "/connection-allowlist");
  EXPECT_TRUE(ui_test_utils::NavigateToURL(browser(), main_url));

  GURL dns_prefetch_url("https://c.test");

  content::RenderFrameHost* main_frame_rfh = browser()
                                                 ->GetTabStripModel()
                                                 ->GetActiveWebContents()
                                                 ->GetPrimaryMainFrame();
  // Create about:blank iframe and wait for it to load.
  content::TestNavigationObserver observer(
      browser()->GetTabStripModel()->GetActiveWebContents());
  EXPECT_TRUE(ExecJs(main_frame_rfh, R"(
            var iframe = document.createElement('iframe');
            iframe.src = 'about:blank';
            document.body.appendChild(iframe);
          )"));
  observer.Wait();

  content::RenderFrameHost* child_frame_rfh =
      content::ChildFrameAt(main_frame_rfh, 0);
  ASSERT_TRUE(child_frame_rfh);
  net::NetworkAnonymizationKey network_anonymization_key =
      child_frame_rfh->GetIsolationInfoForSubresources()
          .network_anonymization_key();

  // Inject entity-encoded rel via innerHTML. The HTML parser decodes
  // "preco&#110;&#110;ect" to "preconnect".
  EXPECT_TRUE(ExecJs(child_frame_rfh, content::JsReplace(R"(
              document.head.innerHTML =
                  '<link rel="preco&#110;&#110;ect" href="' + $1 + '">';
            )",
                                                         dns_prefetch_url)));

  preconnect_manager_observer()->WaitUntilHostLookedUp(
      dns_prefetch_url.GetHost(), network_anonymization_key);

  EXPECT_TRUE(preconnect_manager_observer()->HasHostBeenLookedUp(
      dns_prefetch_url.GetHost(), network_anonymization_key));
  EXPECT_FALSE(preconnect_manager_observer()->HostFound(
      dns_prefetch_url.GetHost(), network_anonymization_key))
      << "Entity-encoded rel in iframe contentWindow should be blocked "
         "for non-allowlisted host.";
}

class LoadingPredictorBrowserTestWithOptimizationGuideAndConnectionAllowlist
    : public LoadingPredictorBrowserTestWithOptimizationGuide {
 public:
  LoadingPredictorBrowserTestWithOptimizationGuideAndConnectionAllowlist() {
    connection_allowlist_feature_list_.InitWithFeatures(
        {network::features::kConnectionAllowlists}, {});
  }

 private:
  base::test::ScopedFeatureList connection_allowlist_feature_list_;
};

INSTANTIATE_TEST_SUITE_P(
    ,
    LoadingPredictorBrowserTestWithOptimizationGuideAndConnectionAllowlist,
    testing::Combine(
        /*IsLocalPredictionEnabled()=*/testing::Bool(),
        /*ShouldUseOptimizationGuidePredictions()=*/testing::Bool(),
        /*IsPrefetchEnabled()=*/testing::Values(false),
        /*GetSubresourceType()=*/testing::Values("")));

IN_PROC_BROWSER_TEST_P(
    LoadingPredictorBrowserTestWithOptimizationGuideAndConnectionAllowlist,
    PreconnectBlockedByConnectionAllowlist) {
  GURL url = embedded_test_server()->GetURL(
      "test.com", "/preload/connection_allowlist_redirect_allowed.html");
  net::SchemefulSite site = net::SchemefulSite(url);
  auto network_anonymization_key =
      net::NetworkAnonymizationKey::CreateSameSite(std::move(site));

  // 1. Navigate to first document and wait for it to complete.
  ASSERT_TRUE(ui_test_utils::NavigateToURL(browser(), url));

  ASSERT_TRUE(browser()
                  ->GetTabStripModel()
                  ->GetActiveWebContents()
                  ->GetPrimaryMainFrame()
                  ->GetConnectionAllowlists()
                  .enforced.has_value());

  // 2. Set up optimization hints for the second navigation to secondnav.com.
  GURL url2 = embedded_test_server()->GetURL("secondnav.com", "/title1.html");
  net::SchemefulSite site2 = net::SchemefulSite(url2);
  auto network_anonymization_key2 =
      net::NetworkAnonymizationKey::CreateSameSite(std::move(site2));

  // Set up optimization hints.
  // 1. Allowed cross-origin preconnect (https://example.com)
  // 2. Blocked cross-origin preconnect (https://blocked.com)
  SetUpOptimizationHint(
      url2, {Subresource("https://example.com",
                         optimization_guide::proto::RESOURCE_TYPE_CSS,
                         /*preconnect_only=*/true),
             Subresource("https://blocked.com",
                         optimization_guide::proto::RESOURCE_TYPE_CSS,
                         /*preconnect_only=*/true)});

  content::WebContents* tab =
      browser()->GetTabStripModel()->GetActiveWebContents();
  EXPECT_TRUE(
      content::ExecJs(tab, content::JsReplace("window.location = $1", url2)));

  // Same-origin preconnect for url2 navigation (to secondnav.com) should
  // succeed because secondnav.com is also in the allowlist of url.
  preconnect_manager_observer()->WaitUntilHostLookedUp(
      url2.GetHost(), network_anonymization_key2);
  EXPECT_TRUE(preconnect_manager_observer()->HostFound(
      url2.GetHost(), network_anonymization_key2));

  if (ShouldUseOptimizationGuidePredictions()) {
    // Optimization Guide preconnect to https://example.com should succeed.
    preconnect_manager_observer()->WaitUntilHostLookedUp(
        "example.com", network_anonymization_key2);
    EXPECT_TRUE(preconnect_manager_observer()->HostFound(
        "example.com", network_anonymization_key2));

    // Optimization Guide preconnect to https://blocked.com should fail.
    preconnect_manager_observer()->WaitUntilHostLookedUp(
        "blocked.com", network_anonymization_key2);
    EXPECT_FALSE(preconnect_manager_observer()->HostFound(
        "blocked.com", network_anonymization_key2));
  } else {
    // Optimization guide predictions are not used. So no preconnects to
    // example.com or blocked.com should happen.
    base::RunLoop().RunUntilIdle();
    EXPECT_FALSE(preconnect_manager_observer()->HasHostBeenLookedUp(
        "example.com", network_anonymization_key2));
    EXPECT_FALSE(preconnect_manager_observer()->HasHostBeenLookedUp(
        "blocked.com", network_anonymization_key2));
  }
}

class LoadingPredictorPrefetchBrowserTestWithConnectionAllowlist
    : public LoadingPredictorPrefetchBrowserTest {
 public:
  LoadingPredictorPrefetchBrowserTestWithConnectionAllowlist() {
    connection_allowlist_feature_list_.InitWithFeatures(
        {network::features::kConnectionAllowlists}, {});
  }

  void SetUpCommandLine(base::CommandLine* command_line) override {
    LoadingPredictorPrefetchBrowserTest::SetUpCommandLine(command_line);
    mock_cert_verifier_.SetUpCommandLine(command_line);
    command_line->AppendSwitchASCII(network::switches::kIpAddressSpaceOverrides,
                                    "127.0.0.1:0=public");
  }

  void SetUpInProcessBrowserTestFixture() override {
    LoadingPredictorPrefetchBrowserTest::SetUpInProcessBrowserTestFixture();
    mock_cert_verifier_.SetUpInProcessBrowserTestFixture();
  }

  void TearDownInProcessBrowserTestFixture() override {
    LoadingPredictorPrefetchBrowserTest::TearDownInProcessBrowserTestFixture();
    mock_cert_verifier_.TearDownInProcessBrowserTestFixture();
  }

  void SetUpOnMainThread() override {
    LoadingPredictorPrefetchBrowserTest::SetUpOnMainThread();
    // Note: Some prefetch behavior may rely on HTTPS. We configure the test
    // server here to avoid any issues in this class and subclasses.
    mock_cert_verifier_.mock_cert_verifier()->set_default_result(net::OK);
    embedded_https_test_server().SetSSLConfig(
        net::EmbeddedTestServer::CERT_TEST_NAMES);
    embedded_https_test_server().AddDefaultHandlers();
    embedded_https_test_server().RegisterRequestMonitor(base::BindRepeating(
        &LoadingPredictorPrefetchBrowserTestWithConnectionAllowlist::
            MonitorRequest,
        base::Unretained(this)));
    ASSERT_TRUE(embedded_https_test_server().Start());
  }

 protected:
  virtual bool ShouldCheckObserver() const { return true; }
  void RunTest() {
    // Navigate to a document that has a connection allowlist only accepting
    // example.com and secondnav.com. We use
    // `connection_allowlist_redirect_allowed.html` because
    // NetworkContext::Prefetch cancels the entire prefetch if the allowlist has
    // a redirect policy of "block" (which is the default).
    GURL url = embedded_https_test_server().GetURL(
        "test.com", "/preload/connection_allowlist_redirect_allowed.html");

    // 1. Navigate to first document and wait for it to complete.
    ASSERT_TRUE(ui_test_utils::NavigateToURL(browser(), url));

    ASSERT_TRUE(browser()
                    ->GetTabStripModel()
                    ->GetActiveWebContents()
                    ->GetPrimaryMainFrame()
                    ->GetConnectionAllowlists()
                    .enforced.has_value());

    // 2. Set up optimization hints for the second navigation to secondnav.com.
    GURL url2 =
        embedded_https_test_server().GetURL("secondnav.com", "/title1.html");

    // Set up optimization hints.
    // 1. Allowed cross-origin prefetch (https://example.com:<port>/css)
    // 2. Blocked cross-origin prefetch (https://blocked.com:<port>/css)
    GURL allowed_prefetch_url =
        embedded_https_test_server().GetURL("example.com", "/css");
    GURL blocked_prefetch_url =
        embedded_https_test_server().GetURL("blocked.com", "/css");

    SetUpOptimizationHint(
        url2, {Subresource(allowed_prefetch_url.spec(),
                           optimization_guide::proto::RESOURCE_TYPE_CSS,
                           /*preconnect_only=*/false),
               Subresource(blocked_prefetch_url.spec(),
                           optimization_guide::proto::RESOURCE_TYPE_CSS,
                           /*preconnect_only=*/false)});

    // 3. Set expected requests.
    // We only expect the allowed prefetch url.
    std::vector<GURL> expected_requests = {allowed_prefetch_url};
    SetExpectedRequests(std::move(expected_requests));

    // 4. Initiate second navigation.
    content::WebContents* tab =
        browser()->GetTabStripModel()->GetActiveWebContents();
    EXPECT_TRUE(
        content::ExecJs(tab, content::JsReplace("window.location = $1", url2)));

    // 5. Wait for the expected prefetch.
    WaitForRequests();

    if (ShouldCheckObserver()) {
      // 6. Verify that the blocked prefetch failed.
      prefetch_manager_observer()->WaitForPrefetchesForNavigation(url2);
      const auto results = prefetch_manager_observer()->results();

      bool found_allowed = false;
      bool found_blocked = false;
      for (const auto& result : results) {
        if (result.prefetch_url == allowed_prefetch_url) {
          EXPECT_EQ(result.status.error_code, net::OK);
          found_allowed = true;
        } else if (result.prefetch_url == blocked_prefetch_url) {
          found_blocked = true;
        }
      }
      EXPECT_TRUE(found_allowed);
      EXPECT_FALSE(found_blocked);
    } else {
      // For NetworkContextPrefetch, we can't observe completion via
      // PrefetchManager::Observer. Just wait a bit to ensure no unexpected
      // requests (like the blocked one) arrive. We will still assert if any
      // unexpected request is found in MonitorRequest;
      base::RunLoop().RunUntilIdle();
    }
  }

 private:
  content::ContentMockCertVerifier mock_cert_verifier_;
  base::test::ScopedFeatureList connection_allowlist_feature_list_;
};

INSTANTIATE_TEST_SUITE_P(
    ,
    LoadingPredictorPrefetchBrowserTestWithConnectionAllowlist,
    testing::Combine(
        /*IsLocalPredictionEnabled()=*/testing::Values(true, false),
        /*ShouldUseOptimizationGuidePredictions()=*/
        testing::Values(true),
        /*IsPrefetchEnabled()=*/testing::Values(true),
        /*GetSubresourceType()=*/testing::Values("all", "css", "js_css")));

IN_PROC_BROWSER_TEST_P(
    LoadingPredictorPrefetchBrowserTestWithConnectionAllowlist,
    PrefetchBlockedByConnectionAllowlist) {
  RunTest();
}

class
    LoadingPredictorPrefetchBrowserTestWithConnectionAllowlistAndNetworkContextPrefetch
    : public LoadingPredictorPrefetchBrowserTestWithConnectionAllowlist {
 public:
  LoadingPredictorPrefetchBrowserTestWithConnectionAllowlistAndNetworkContextPrefetch() {
    network_context_prefetch_feature_list_.InitWithFeatures(
        {features::kPrefetchManagerUseNetworkContextPrefetch,
         network::features::kNetworkContextPrefetch},
        {});
  }

  bool ShouldCheckObserver() const override { return false; }

 private:
  base::test::ScopedFeatureList network_context_prefetch_feature_list_;
};

INSTANTIATE_TEST_SUITE_P(
    ,
    LoadingPredictorPrefetchBrowserTestWithConnectionAllowlistAndNetworkContextPrefetch,
    testing::Combine(
        /*IsLocalPredictionEnabled()=*/testing::Values(true, false),
        /*ShouldUseOptimizationGuidePredictions()=*/
        testing::Values(true),
        /*IsPrefetchEnabled()=*/testing::Values(true),
        /*GetSubresourceType()=*/testing::Values("all", "css", "js_css")));

IN_PROC_BROWSER_TEST_P(
    LoadingPredictorPrefetchBrowserTestWithConnectionAllowlistAndNetworkContextPrefetch,
    PrefetchBlockedByConnectionAllowlist) {
  RunTest();
}

}  // namespace predictors
