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

#ifndef CONTENT_BROWSER_BACK_FORWARD_CACHE_BROWSERTEST_H_
#define CONTENT_BROWSER_BACK_FORWARD_CACHE_BROWSERTEST_H_

#include <memory>
#include <optional>

#include "base/compiler_specific.h"
#include "base/feature_list.h"
#include "base/hash/hash.h"
#include "base/test/scoped_feature_list.h"
#include "base/test/scoped_logging_settings.h"
#include "components/ukm/test_ukm_recorder.h"
#include "content/browser/back_forward_cache_test_util.h"
#include "content/browser/renderer_host/page_lifecycle_state_manager.h"
#include "content/browser/renderer_host/render_frame_host_impl.h"
#include "content/browser/renderer_host/render_frame_host_manager.h"
#include "content/public/browser/render_frame_host.h"
#include "content/public/browser/web_contents_observer.h"
#include "content/public/test/content_browser_test.h"
#include "content/public/test/content_mock_cert_verifier.h"
#include "content/test/content_browser_test_utils_internal.h"
#include "testing/gmock/include/gmock/gmock.h"
#include "third_party/blink/public/mojom/back_forward_cache_not_restored_reasons.mojom.h"

namespace content {

using NotRestoredReasons =
    BackForwardCacheCanStoreDocumentResult::NotRestoredReasons;
using NotRestoredReason = BackForwardCacheMetrics::NotRestoredReason;

using ReasonsMatcher = testing::Matcher<
    const blink::mojom::BackForwardCacheNotRestoredReasonsPtr&>;
using SameOriginMatcher = testing::Matcher<
    const blink::mojom::SameOriginBfcacheNotRestoredDetailsPtr&>;
using BlockingDetailsReasonsMatcher =
    testing::Matcher<const blink::mojom::BFCacheBlockingDetailedReasonPtr&>;
using SourceLocationMatcher =
    testing::Matcher<const blink::mojom::ScriptSourceLocationPtr&>;
using BlockingDetailsMatcher =
    testing::Matcher<const blink::mojom::BlockingDetailsPtr&>;

// Match RenderFrameHostImpl* that are in the BackForwardCache.
MATCHER(InBackForwardCache, "") {
  return arg->IsInBackForwardCache();
}

// Match RenderFrameDeletedObserver* which observed deletion of the RenderFrame.
MATCHER(Deleted, "") {
  return arg->deleted();
}

// Helper function to pass an initializer list to the EXPECT_THAT macro. This is
// indeed the identity function.
std::initializer_list<RenderFrameHostImpl*> Elements(
    std::initializer_list<RenderFrameHostImpl*> t);

enum class TestFrameType {
  kMainFrame,
  kSubFrame,
  kSubFrameOfSubframe,
};

// Test about the BackForwardCache.
class BackForwardCacheBrowserTest
    : public ContentBrowserTest,
      public WebContentsObserver,
      public BackForwardCacheMetrics::TestObserver,
      public BackForwardCacheMetricsTestMatcher {
 public:
  BackForwardCacheBrowserTest();
  ~BackForwardCacheBrowserTest() override;

  // TestObserver:
  void NotifyNotRestoredReasons(
      std::unique_ptr<BackForwardCacheCanStoreTreeResult> tree_result) override;

 protected:
  void SetUpCommandLine(base::CommandLine* command_line) override;

  void SetUpInProcessBrowserTestFixture() override;

  void TearDownInProcessBrowserTestFixture() override;

  void SetupFeaturesAndParameters();

  void EnableFeatureAndSetParams(const base::Feature& feature,
                                 std::string param_name,
                                 std::string param_value);

  void DisableFeature(const base::Feature& feature);

  void SetUpOnMainThread() override;

  void TearDownOnMainThread() override;

  WebContentsImpl* web_contents() const;

  RenderFrameHostImpl* current_frame_host();

  RenderFrameHostManager* render_frame_host_manager();

  std::string DepictFrameTree(FrameTreeNode* node);

  bool HistogramContainsIntValue(base::HistogramBase::Sample32 sample,
                                 std::vector<base::Bucket> histogram_values);

  void EvictByJavaScript(RenderFrameHostImpl* rfh);

  void StartRecordingEvents(RenderFrameHostImpl* rfh);

  void MatchEventList(RenderFrameHostImpl* rfh,
                      base::Value list,
                      base::Location location = base::Location::Current());

  // Creates a minimal HTTPS server, accessible through https_server().
  // Returns a pointer to the server.
  net::EmbeddedTestServer* CreateHttpsServer();

  net::EmbeddedTestServer* https_server();

  // Do not fail this test if a message from a renderer arrives at the browser
  // for a cached page.
  void DoNotFailForUnexpectedMessagesWhileCached();

  // Navigates to a page at |page_url| with an img element with src set to
  // "image.png".
  RenderFrameHostImpl* NavigateToPageWithImage(const GURL& page_url);

  void AcquireKeyboardLock(RenderFrameHostImpl* rfh);

  void ReleaseKeyboardLock(RenderFrameHostImpl* rfh);

  // Start a navigation to |url| but block it on an error. If |history_offset|
  // is not 0, then the navigation will be a history navigation and this will
  // assert that the URL after navigation is |url|.
  void NavigateAndBlock(GURL url, int history_offset);

  static testing::Matcher<BackForwardCacheCanStoreDocumentResult>
  MatchesDocumentResult(testing::Matcher<NotRestoredReasons> not_stored,
                        BlockListedFeatures block_listed);

  ReasonsMatcher MatchesNotRestoredReasons(
      const std::optional<testing::Matcher<std::string>>& id,
      const std::optional<testing::Matcher<std::string>>& name,
      const std::optional<testing::Matcher<std::string>>& src,
      const std::vector<BlockingDetailsReasonsMatcher>& reasons,
      const std::optional<SameOriginMatcher>& same_origin_details);

  SameOriginMatcher MatchesSameOriginDetails(
      const testing::Matcher<GURL>& url,
      const std::vector<ReasonsMatcher>& children);

  // Used in tests that ensure source location is sent to the renderer side from
  // the browser one
  BlockingDetailsReasonsMatcher MatchesDetailedReason(
      const testing::Matcher<std::string>& name,
      const std::optional<SourceLocationMatcher>& source);

  // Used in tests that ensure source location is sent to the browser side from
  // the renderer one.
  BlockingDetailsMatcher MatchesBlockingDetails(
      const std::optional<SourceLocationMatcher>& source);

  SourceLocationMatcher MatchesSourceLocation(
      const testing::Matcher<GURL>& url,
      const testing::Matcher<std::string>& function_name,
      const testing::Matcher<uint64_t>& line_number,
      const testing::Matcher<uint64_t>& column_number);

  // Access the tree result of NotRestoredReason for the last main frame
  // navigation.
  BackForwardCacheCanStoreTreeResult* GetTreeResult() {
    return tree_result_.get();
  }

  void InstallUnloadHandlerOnMainFrame();
  void InstallUnloadHandlerOnSubFrame();
  EvalJsResult GetUnloadRunCount();

  // Adds a blocklisted feature to the document to prevent caching. Currently
  // this means adding a plugin. We expect that plugins will never become
  // cacheable, so this should be stable (at least until plugins cease to
  // exist). If you need the feature to be sticky, then
  // `RenderFrameHostImpl::UseDummyStickyBackForwardCacheDisablingFeatureForTesting`
  // provides that.
  [[nodiscard]] bool AddBlocklistedFeature(RenderFrameHost* rfh);
  // Check that the document was not restored for the reason added by
  // `AddBlocklistedFeature`.
  void ExpectNotRestoredDueToBlocklistedFeature(base::Location location);

  const ukm::TestAutoSetUkmRecorder& ukm_recorder() override;
  const base::HistogramTester& histogram_tester() override;

 private:
  content::ContentMockCertVerifier mock_cert_verifier_;

  base::test::ScopedFeatureList feature_list_;
  logging::ScopedVmoduleSwitches vmodule_switches_;

  FrameTreeVisualizer visualizer_;
  std::unique_ptr<net::EmbeddedTestServer> https_server_;
  std::map<base::test::FeatureRef, std::map<std::string, std::string>>
      features_with_params_;
  std::vector<base::test::FeatureRef> disabled_features_;

  std::unique_ptr<ukm::TestAutoSetUkmRecorder> ukm_recorder_;
  std::unique_ptr<base::HistogramTester> histogram_tester_;

  // Store the tree result of NotRestoredReasons for the last main frame
  // navigation.
  std::unique_ptr<BackForwardCacheCanStoreTreeResult> tree_result_;

  // Whether we should fail the test if a message arrived at the browser from a
  // renderer for a bfcached page.
  bool fail_for_unexpected_messages_while_cached_ = true;
};

[[nodiscard]] bool WaitForDOMContentLoaded(RenderFrameHostImpl* rfh);

class HighCacheSizeBackForwardCacheBrowserTest
    : public BackForwardCacheBrowserTest {
 protected:
  void SetUpCommandLine(base::CommandLine* command_line) override;

  // The number of pages the BackForwardCache can hold per tab.
  // The number 5 was picked since Android ASAN trybot failed to keep more than
  // 6 pages in memory.
  const size_t kBackForwardCacheSize = 5;
};

// Test that enables the BackForwardCacheAllowUnload flag.
class BackForwardCacheUnloadBrowserTest : public BackForwardCacheBrowserTest {
 public:
  void SetUpCommandLine(base::CommandLine* command_line) override;

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

// An implementation of PageLifecycleStateManager::TestDelegate for testing.
class PageLifecycleStateManagerTestDelegate
    : public PageLifecycleStateManager::TestDelegate {
 public:
  explicit PageLifecycleStateManagerTestDelegate(
      PageLifecycleStateManager* manager);

  ~PageLifecycleStateManagerTestDelegate() override;

  // Waits for the renderer finishing to set the state of being in back/forward
  // cache.
  [[nodiscard]] bool WaitForInBackForwardCacheAck();

  void OnStoreInBackForwardCacheSent(base::OnceClosure cb);
  void OnDisableJsEvictionSent(base::OnceClosure cb);
  void OnRestoreFromBackForwardCacheSent(base::OnceClosure cb);

 private:
  // PageLifecycleStateManager::TestDelegate:
  void OnLastAcknowledgedStateChanged(
      const blink::mojom::PageLifecycleState& old_state,
      const blink::mojom::PageLifecycleState& new_state) override;
  void OnUpdateSentToRenderer(
      const blink::mojom::PageLifecycleState& new_state) override;
  void OnDeleted() override;

  raw_ptr<PageLifecycleStateManager, DanglingUntriaged> manager_;
  base::OnceClosure store_in_back_forward_cache_sent_;
  base::OnceClosure store_in_back_forward_cache_ack_received_;
  base::OnceClosure restore_from_back_forward_cache_sent_;
  base::OnceClosure disable_eviction_sent_;
};

// Gets the value of a key in local storage by evaluating JS. Use
// `WaitForLocalStorage` if you are dealing with multiple renderer processes.
EvalJsResult GetLocalStorage(RenderFrameHostImpl* rfh, std::string key);

// Because we are dealing with multiple renderer processes and the storage
// service, we sometimes need to wait for the storage changes to show up the
// renderer. See https://crbug.com/1494646.
// Returns whether the expected value was found (so timeouts can be recognized).
[[nodiscard]] bool WaitForLocalStorage(RenderFrameHostImpl* rfh,
                                       std::string key,
                                       std::string expected_value);

}  // namespace content

#endif  // CONTENT_BROWSER_BACK_FORWARD_CACHE_BROWSERTEST_H_