// Copyright 2022 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/preloading/prerender/prerender_manager.h"

#include <string>

#include "base/test/scoped_feature_list.h"
#include "build/build_config.h"
#include "chrome/browser/after_startup_task_utils.h"
#include "chrome/browser/preloading/chrome_preloading.h"
#include "chrome/browser/preloading/preloading_features.h"
#include "chrome/browser/preloading/prerender/prerender_utils.h"
#include "chrome/browser/preloading/scoped_prewarm_feature_list.h"
#include "chrome/browser/search_engines/template_url_service_factory_test_util.h"
#include "chrome/test/base/chrome_render_view_host_test_harness.h"
#include "components/search_engines/template_url_data.h"
#include "components/search_engines/template_url_service.h"
#include "content/public/browser/render_frame_host.h"
#include "content/public/browser/web_contents.h"
#include "content/public/common/content_features.h"
#include "content/public/test/preloading_test_util.h"
#include "content/public/test/prerender_test_util.h"
#include "content/public/test/web_contents_tester.h"
#include "net/test/embedded_test_server/embedded_test_server.h"
#include "testing/gtest/include/gtest/gtest.h"

#if BUILDFLAG(IS_CHROMEOS)
#include "chrome/browser/ash/login/users/fake_chrome_user_manager.h"
#include "components/account_id/account_id.h"
#include "components/user_manager/scoped_user_manager.h"
#endif  // BUILDFLAG(IS_CHROMEOS)

namespace {

class PrerenderManagerTest : public ChromeRenderViewHostTestHarness {
 public:
  PrerenderManagerTest(const PrerenderManagerTest&) = delete;
  PrerenderManagerTest& operator=(const PrerenderManagerTest&) = delete;
  PrerenderManagerTest()
      : ChromeRenderViewHostTestHarness(
            content::BrowserTaskEnvironment::REAL_IO_THREAD),
        prerender_helper_(
            base::BindRepeating(&PrerenderManagerTest::GetActiveWebContents,
                                base::Unretained(this))) {
    reuse_feature_list_.InitAndEnableFeatureWithParameters(
        features::kPrerender2ReuseHost, {{"reuse_search_host", "true"}});
  }

  void SetUp() override {
    prerender_helper_.RegisterServerRequestMonitor(&test_server_);
    ASSERT_TRUE(test_server_.Start());
    ChromeRenderViewHostTestHarness::SetUp();

    // Set up TemplateURLService for search prerendering.
    TemplateURLServiceFactoryTestUtil factory_util(profile());
    factory_util.VerifyLoad();

    TemplateURLData template_url_data;
    template_url_data.SetURL(GetUrl(search_site() + "?q={searchTerms}").spec());
    factory_util.model()->SetUserSelectedDefaultSearchProvider(
        factory_util.model()->Add(
            std::make_unique<TemplateURL>(template_url_data)));

    PrerenderManager::CreateForWebContents(GetActiveWebContents());
    ASSERT_TRUE(PrerenderManager::FromWebContents(web_contents()));
    web_contents_delegate_ =
        std::make_unique<content::test::ScopedPrerenderWebContentsDelegate>(
            *web_contents());
  }

  content::WebContents* GetActiveWebContents() { return web_contents(); }

  GURL GetSearchSuggestionUrl(const std::string& original_query,
                              const std::string& search_terms) {
    return GetUrl(search_site() + "?q=" + search_terms +
                  "&oq=" + original_query);
  }

  GURL GetCanonicalSearchUrl(const GURL& search_suggestion_url) {
    GURL canonical_search_url;
    EXPECT_TRUE(HasCanonicalPreloadingOmniboxSearchURL(
        search_suggestion_url, profile(), &canonical_search_url));
    return canonical_search_url;
  }

  content::WebContentsTester* web_contents_tester() {
    return content::WebContentsTester::For(web_contents());
  }

 protected:
  GURL GetUrl(const std::string& path) { return test_server_.GetURL(path); }

  PrerenderManager* prerender_manager() {
    return PrerenderManager::FromWebContents(web_contents());
  }

  content::test::PrerenderTestHelper& prerender_helper() {
    return prerender_helper_;
  }

  content::PreloadingFailureReason ToPreloadingFailureReason(
      PrerenderPredictionStatus status) {
    return static_cast<content::PreloadingFailureReason>(
        static_cast<int>(status) +
        static_cast<int>(content::PreloadingFailureReason::
                             kPreloadingFailureReasonContentEnd));
  }

  content::PreloadingAttempt* TriggerDirectUrlInputPrerender(
      const GURL& prerendering_url) {
    auto* preloading_data = content::PreloadingData::GetOrCreateForWebContents(
        GetActiveWebContents());
    content::PreloadingURLMatchCallback same_url_matcher =
        content::PreloadingData::GetSameURLMatcher(prerendering_url);
    content::PreloadingAttempt* preloading_attempt =
        preloading_data->AddPreloadingAttempt(
            chrome_preloading_predictor::kOmniboxDirectURLInput,
            content::PreloadingType::kPrerender, same_url_matcher,
            GetActiveWebContents()
                ->GetPrimaryMainFrame()
                ->GetPageUkmSourceId());
    prerender_manager()->StartPrerenderDirectUrlInput(prerendering_url,
                                                      *preloading_attempt);
    return preloading_attempt;
  }

 private:
  static std::string search_site() { return "/title1.html"; }

  content::test::PrerenderTestHelper prerender_helper_;
  test::ScopedPrewarmFeatureList scoped_prewarm_feature_list_{
      test::ScopedPrewarmFeatureList::PrewarmState::kDisabled};
  base::test::ScopedFeatureList reuse_feature_list_;
  std::unique_ptr<content::test::ScopedPrerenderWebContentsDelegate>
      web_contents_delegate_;

  net::EmbeddedTestServer test_server_;
};

TEST_F(PrerenderManagerTest, StartCleanSearchSuggestionPrerender) {
  GURL prerendering_url = GetSearchSuggestionUrl("pre", "prerender");
  GURL canonical_url = GetCanonicalSearchUrl(prerendering_url);
  content::test::PrerenderHostRegistryObserver registry_observer(
      *GetActiveWebContents());
  prerender_manager()->StartPrerenderSearchResult(
      canonical_url, prerendering_url, /*attempt=*/nullptr);
  registry_observer.WaitForTrigger(prerendering_url);
  content::PrerenderHostId prerender_host_id =
      prerender_helper().GetHostForUrl(prerendering_url);
  EXPECT_TRUE(prerender_host_id);
}

// Tests that the old prerender will be destroyed when starting prerendering a
// different search result.
TEST_F(PrerenderManagerTest, StartNewSuggestionPrerender) {
  GURL prerendering_url1 = GetSearchSuggestionUrl("pre", "prefetch");
  GURL canonical_url1 = GetCanonicalSearchUrl(prerendering_url1);
  content::test::PrerenderHostRegistryObserver registry_observer(
      *GetActiveWebContents());
  prerender_manager()->StartPrerenderSearchResult(
      canonical_url1, prerendering_url1, /*attempt=*/nullptr);

  registry_observer.WaitForTrigger(prerendering_url1);
  content::PrerenderHostId prerender_host_id1 =
      prerender_helper().GetHostForUrl(prerendering_url1);
  ASSERT_TRUE(prerender_host_id1);
  content::FrameTreeNodeId prerender_frame_tree_node_id1 =
      prerender_helper()
          .GetPrerenderedMainFrameHost(prerender_host_id1)
          ->GetFrameTreeNodeId();
  ASSERT_TRUE(prerender_frame_tree_node_id1);
  content::test::PrerenderHostObserver host_observer(*GetActiveWebContents(),
                                                     prerender_host_id1);
  GURL prerendering_url2 = GetSearchSuggestionUrl("prer", "prerender");
  GURL canonical_url2 = GetCanonicalSearchUrl(prerendering_url2);
  prerender_manager()->StartPrerenderSearchResult(
      canonical_url2, prerendering_url2, /*attempt=*/nullptr);
  host_observer.WaitForDestroyed();
  registry_observer.WaitForTrigger(prerendering_url2);
  EXPECT_TRUE(prerender_manager()->HasSearchResultPagePrerendered());
  EXPECT_EQ(canonical_url2,
            prerender_manager()->GetPrerenderCanonicalSearchURLForTesting());
  content::PrerenderHostId prerender_host_id2 =
      prerender_helper().GetHostForUrl(prerendering_url2);
  content::FrameTreeNodeId prerender_frame_tree_node_id2 =
      prerender_helper()
          .GetPrerenderedMainFrameHost(prerender_host_id2)
          ->GetFrameTreeNodeId();
  // Search prerender reuses existing frames, so `FrameTreeNodeId`s should be
  // the same, while `PrerenderHostId`s should be different.
  EXPECT_EQ(prerender_frame_tree_node_id1, prerender_frame_tree_node_id2);
  EXPECT_NE(prerender_host_id1, prerender_host_id2);
}

// Tests that the old prerender is not destroyed when starting prerendering the
// same search suggestion.
TEST_F(PrerenderManagerTest, StartSameSuggestionPrerender) {
  GURL prerendering_url1 = GetSearchSuggestionUrl("pre", "prerender");
  GURL canonical_url = GetCanonicalSearchUrl(prerendering_url1);
  content::test::PrerenderHostRegistryObserver registry_observer(
      *GetActiveWebContents());
  prerender_manager()->StartPrerenderSearchResult(
      canonical_url, prerendering_url1, /*attempt=*/nullptr);
  registry_observer.WaitForTrigger(prerendering_url1);
  content::PrerenderHostId prerender_host_id1 =
      prerender_helper().GetHostForUrl(prerendering_url1);
  EXPECT_TRUE(prerender_host_id1);
  GURL prerendering_url2 = GetSearchSuggestionUrl("prer", "prerender");
  prerender_manager()->StartPrerenderSearchResult(
      canonical_url, prerendering_url2, /*attempt=*/nullptr);
  EXPECT_TRUE(prerender_manager()->HasSearchResultPagePrerendered());

  // The created prerender for `prerendering_url1` still exists, so the
  // prerender_host_id should be the same.
  content::PrerenderHostId prerender_host_id2 =
      prerender_helper().GetHostForUrl(prerendering_url2);
  EXPECT_EQ(prerender_host_id2, prerender_host_id1);
}

TEST_F(PrerenderManagerTest, StartCleanPrerenderDirectUrlInput) {
  GURL prerendering_url = GetUrl("/foo");
  content::test::PrerenderHostRegistryObserver registry_observer(
      *GetActiveWebContents());

  TriggerDirectUrlInputPrerender(prerendering_url);
  registry_observer.WaitForTrigger(prerendering_url);
  content::PrerenderHostId prerender_host_id =
      prerender_helper().GetHostForUrl(prerendering_url);
  EXPECT_TRUE(prerender_host_id);
}

TEST_F(PrerenderManagerTest, StartPrerenderDirectUrlInputDisabledByFeature) {
  base::test::ScopedFeatureList feature_list;
  feature_list.InitAndDisableFeature(features::kOmniboxDuiPrerendering);

  GURL prerendering_url = GetUrl("/foo");
  auto* preloading_data = content::PreloadingData::GetOrCreateForWebContents(
      GetActiveWebContents());
  content::PreloadingAttempt* preloading_attempt =
      preloading_data->AddPreloadingAttempt(
          chrome_preloading_predictor::kOmniboxDirectURLInput,
          content::PreloadingType::kPrerender,
          content::PreloadingData::GetSameURLMatcher(prerendering_url),
          GetActiveWebContents()->GetPrimaryMainFrame()->GetPageUkmSourceId());

  auto handle = prerender_manager()->StartPrerenderDirectUrlInput(
      prerendering_url, *preloading_attempt);
  EXPECT_FALSE(handle);
}

// Test that the PreloadingTriggeringOutcome is set to kFailure when the DUI
// predictor suggests a different URL.
TEST_F(PrerenderManagerTest, StartNewPrerenderDirectUrlInput) {
  GURL prerendering_url = GetUrl("/foo");
  content::test::PrerenderHostRegistryObserver registry_observer(
      *GetActiveWebContents());
  content::PreloadingAttempt* preloading_attempt =
      TriggerDirectUrlInputPrerender(prerendering_url);
  registry_observer.WaitForTrigger(prerendering_url);
  content::PrerenderHostId prerender_host_id =
      prerender_helper().GetHostForUrl(prerendering_url);
  EXPECT_TRUE(prerender_host_id);
  content::test::PrerenderHostObserver host_observer(*GetActiveWebContents(),
                                                     prerender_host_id);
  GURL prerendering_url2 = GetUrl("/bar");
  TriggerDirectUrlInputPrerender(prerendering_url2);
  host_observer.WaitForDestroyed();
  registry_observer.WaitForTrigger(prerendering_url2);
  EXPECT_EQ(ToPreloadingFailureReason(PrerenderPredictionStatus::kCancelled),
            content::test::PreloadingAttemptAccessor(preloading_attempt)
                .GetFailureReason());
}

// TODO(https://crbug.com/334988071): Add all embedder triggers and make it
// mandatory when adding new triggers to PrerenderManager.
enum TriggerType {
  kDirectUrlInput = 0,
  kSearchSuggestion = 1,
};

class PrerenderManagerBasicRequirementTest
    : public testing::WithParamInterface<TriggerType>,
      public PrerenderManagerTest {
 public:
  static std::string DescribeParams(
      const testing::TestParamInfo<ParamType>& info) {
    switch (info.param) {
      case kDirectUrlInput:
        return "DirectUrlInput";
      case kSearchSuggestion:
        return "SearchSuggestion";
    }
  }

  content::PrerenderHostId StartPrerender() {
    content::test::PrerenderHostRegistryObserver registry_observer(
        *GetActiveWebContents());
    GURL prerendering_url;
    switch (GetParam()) {
      case kDirectUrlInput:
        prerendering_url = GetUrl("/foo");
        TriggerDirectUrlInputPrerender(prerendering_url);
        break;
      case kSearchSuggestion:
        prerendering_url = GetSearchSuggestionUrl("pre", "prerender");
        GURL canonical_url = GetCanonicalSearchUrl(prerendering_url);
        prerender_manager()->StartPrerenderSearchResult(
            canonical_url, prerendering_url, /*attempt=*/nullptr);
        break;
    }
    registry_observer.WaitForTrigger(prerendering_url);
    return prerender_helper().GetHostForUrl(prerendering_url);
  }

  std::string GetMetricSuffix() {
    switch (GetParam()) {
      case kDirectUrlInput:
        return prerender_utils::kDirectUrlInputMetricSuffix;
      case kSearchSuggestion:
        return prerender_utils::kDefaultSearchEngineMetricSuffix;
    }
    NOTREACHED();
  }

  // Navigates to another page that cannot be prerendered.
  void NavigateAway() {
    web_contents_tester()->NavigateAndCommit(GetUrl("/empty.html"));
  }
};

INSTANTIATE_TEST_SUITE_P(
    /* no prefix */,
    PrerenderManagerBasicRequirementTest,
    testing::Values(TriggerType::kDirectUrlInput,
                    TriggerType::kSearchSuggestion),
    PrerenderManagerBasicRequirementTest::DescribeParams);

// Tests that the PrerenderHandle is destroyed when the primary page changed.
TEST_P(PrerenderManagerBasicRequirementTest, NavigateAway) {
  base::HistogramTester histogram_tester;
  content::PrerenderHostId prerender_host_id = StartPrerender();
  ASSERT_TRUE(prerender_host_id);
  content::test::PrerenderHostObserver host_observer(*GetActiveWebContents(),
                                                     prerender_host_id);
  NavigateAway();
  host_observer.WaitForDestroyed();

  histogram_tester.ExpectUniqueSample(
      "Prerender.Experimental.PrerenderHostFinalStatus.Embedder_" +
          GetMetricSuffix(),
      /*kTriggerDestroyed*/ 16, 1);

  switch (GetParam()) {
    case kSearchSuggestion:
      EXPECT_FALSE(prerender_manager()->HasSearchResultPagePrerendered());
      break;
    default:
      return;
  }
}

class PrerenderManagerPrewarmTest : public PrerenderManagerTest {
 public:
  PrerenderManagerPrewarmTest() = default;
  ~PrerenderManagerPrewarmTest() override = default;

 private:
  test::ScopedPrewarmFeatureList scoped_prewarm_feature_list_{
      test::ScopedPrewarmFeatureList::PrewarmState::kEnabledWithNoTrigger};
};

TEST_F(PrerenderManagerPrewarmTest, StartPrewarmSearchResult) {
  // Set default search provider to match `features::kPrewarmUrl`.
  TemplateURLServiceFactoryTestUtil factory_util(profile());
  TemplateURLService* model = factory_util.model();
  ASSERT_TRUE(model);
  TemplateURLData template_url_data;
  template_url_data.SetURL("https://search.example.com/?q={searchTerms}");
  model->SetUserSelectedDefaultSearchProvider(
      model->Add(std::make_unique<TemplateURL>(template_url_data)));

  const GURL prewarm_url(features::kPrewarmUrl.Get());
  ASSERT_TRUE(prewarm_url.is_valid());

  // Prerender the prewarm page.
  content::test::PrerenderHostRegistryObserver registry_observer(
      *GetActiveWebContents());
  ASSERT_TRUE(prerender_manager()->MaybeStartPrewarmSearchResult());
  registry_observer.WaitForTrigger(prewarm_url);

  // Prewarm page should not be found here as it's matcher was set as not
  // matching to any URL.
  content::PrerenderHostId prerender_host_id =
      prerender_helper().GetHostForUrl(prewarm_url);
  EXPECT_EQ(prerender_host_id, content::PrerenderHostId());
}

TEST_F(PrerenderManagerPrewarmTest, PrewarmPageRevalidatedAndNotCreatedAgain) {
  // Use same origin url as the test search URL so that it can be reused
  // by the search prerender.
  const GURL prewarm_url = GetUrl("/foo");
  ASSERT_TRUE(prewarm_url.is_valid());
  prerender_manager()->SetPrewarmUrlForTesting(prewarm_url);

  // Prerender the prewarm page.
  content::test::PrerenderHostRegistryObserver registry_observer(
      *GetActiveWebContents());
  EXPECT_TRUE(prerender_manager()->MaybeStartPrewarmSearchResult());
  registry_observer.WaitForTrigger(prewarm_url);

  // Trigger a new search prerender.
  GURL search_suggestion_url = GetSearchSuggestionUrl("pre", "prerender");
  GURL canonical_search_url = GetCanonicalSearchUrl(search_suggestion_url);

  prerender_manager()->StartPrerenderSearchResult(canonical_search_url,
                                                  search_suggestion_url,
                                                  /*attempt=*/nullptr);

  // Trying to prewarm again won't create a new prewarm page because we have a
  // prerender running right now.
  EXPECT_FALSE(prerender_manager()->MaybeStartPrewarmSearchResult());

  // Now cancel the search prerender.
  prerender_manager()->StopPrerenderSearchResult(canonical_search_url);

  // Trigger prewarm again and it should create a new prewarm page.
  EXPECT_TRUE(prerender_manager()->MaybeStartPrewarmSearchResult());
  registry_observer.WaitForTrigger(prewarm_url);
}

#if BUILDFLAG(IS_CHROMEOS)
TEST_F(PrerenderManagerPrewarmTest, StartPrewarmInKioskSessionForKioskMode) {
  base::HistogramTester histogram_tester;

  // Set up Kiosk user session.
  auto* user_manager = new ash::FakeChromeUserManager();
  user_manager::ScopedUserManager enabler{
      std::unique_ptr<user_manager::UserManager>(user_manager)};
  const AccountId account_id =
      AccountId::FromUserEmail("test-kiosk-app@localhost");
  user_manager->AddKioskWebAppUser(account_id);
  user_manager->LoginUser(account_id);

  const GURL prewarm_url(features::kPrewarmUrl.Get());
  ASSERT_TRUE(prewarm_url.is_valid());
  prerender_manager()->SetPrewarmUrlForTesting(prewarm_url);

  // Prerender the prewarm page.
  content::test::PrerenderHostRegistryObserver registry_observer(
      *GetActiveWebContents());
  EXPECT_FALSE(prerender_manager()->MaybeStartPrewarmSearchResult());

  // Verify that the correct decision was logged.
  // PrewarmDecision is a private enum, so we use its integer value.
  // kInKioskSession = 11.
  histogram_tester.ExpectUniqueSample("Prerender.Experimental.PrewarmDecision",
                                      /*kInKioskSession=*/11, 1);
}
#endif  // BUILDFLAG(IS_CHROMEOS)

TEST_F(PrerenderManagerTest, PrewarmDisableOnStartup) {
  base::HistogramTester histogram_tester;
  bool was_complete = AfterStartupTaskUtils::IsBrowserStartupComplete();

  // Configure feature to disable on startup.
  base::test::ScopedFeatureList feature_list;
  feature_list.InitWithFeaturesAndParameters(
      /*enabled_features=*/
      {{features::kPrewarm, {{"url", GetUrl("/prewarm.html").spec()}}},
       {features::kPrewarmDisableOnStartup, {}}},
      /*disabled_features=*/{});

  // Ensure startup is NOT complete.
  AfterStartupTaskUtils::UnsafeResetForTesting();
  ASSERT_FALSE(AfterStartupTaskUtils::IsBrowserStartupComplete());

  // Try to prewarm. It should be blocked.
  EXPECT_FALSE(prerender_manager()->MaybeStartPrewarmSearchResult());

  // Verify histogram.
  // kDisabledOnStartup = 14.
  histogram_tester.ExpectUniqueSample("Prerender.Experimental.PrewarmDecision",
                                      14, 1);

  // Now mark startup as complete.
  content::test::PrerenderHostRegistryObserver registry_observer(
      *GetActiveWebContents());
  AfterStartupTaskUtils::SetBrowserStartupIsCompleteForTesting();
  ASSERT_TRUE(AfterStartupTaskUtils::IsBrowserStartupComplete());

  // Verify that the prerender was triggered.
  registry_observer.WaitForTrigger(GetUrl("/prewarm.html"));

  histogram_tester.ExpectBucketCount("Prerender.Experimental.PrewarmDecision",
                                     0, 1);  // kReady = 0

  // Restore startup state.
  if (was_complete) {
    AfterStartupTaskUtils::SetBrowserStartupIsCompleteForTesting();
  } else {
    AfterStartupTaskUtils::UnsafeResetForTesting();
  }
}

}  // namespace
