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

#include "components/performance_manager/v8_memory/v8_context_tracker.h"

#include <memory>

#include "base/command_line.h"
#include "base/location.h"
#include "base/strings/stringprintf.h"
#include "components/performance_manager/execution_context/execution_context_registry_impl.h"
#include "components/performance_manager/public/graph/graph.h"
#include "components/performance_manager/public/performance_manager.h"
#include "components/performance_manager/test_support/performance_manager_browsertest_harness.h"
#include "content/public/browser/render_frame_host.h"
#include "content/public/browser/web_contents.h"
#include "content/public/test/browser_test.h"
#include "content/public/test/browser_test_utils.h"
#include "content/public/test/content_browser_test_utils.h"
#include "content/public/test/test_utils.h"
#include "content/shell/browser/shell.h"
#include "testing/gmock/include/gmock/gmock.h"
#include "testing/gtest/include/gtest/gtest.h"
#include "url/gurl.h"

namespace performance_manager {
namespace v8_memory {

struct ContextCounts {
  size_t v8_context_count = 0;
  size_t execution_context_count = 0;
  size_t detached_v8_context_count = 0;
  size_t destroyed_execution_context_count = 0;
};

// TODO(crbug.com/40931300): Re-enable on Mac and Android.
// The whole theory behind this test suite is wrong, because it assumes that
// navigating to a page has a precise effect on the number of contexts. But the
// test doesn't control the environment enough to be sure of this -
// platform-specific policies, such as preloading, can change the number. Still,
// leaving it enabled on platforms that aren't currently flaking gives some
// coverage.
#if BUILDFLAG(IS_MAC) || BUILDFLAG(IS_ANDROID)
#define MAYBE_V8ContextTrackerTest DISABLED_V8ContextTrackerTest
#else
#define MAYBE_V8ContextTrackerTest V8ContextTrackerTest
#endif
class MAYBE_V8ContextTrackerTest : public PerformanceManagerBrowserTestHarness {
 public:
  using Super = PerformanceManagerBrowserTestHarness;

  MAYBE_V8ContextTrackerTest() = default;
  ~MAYBE_V8ContextTrackerTest() override = default;

  void SetUp() override {
    GetGraphFeatures().EnableV8ContextTracker();
    Super::SetUp();
  }

  void SetUpCommandLine(base::CommandLine* command_line) override {
    // Force site-per-process so the number of internal utility v8 contexts is
    // stable.
    content::IsolateAllSitesForTesting(command_line);
    Super::SetUpCommandLine(command_line);
  }

  void SetUpOnMainThread() override {
    Graph* graph = PerformanceManager::GetGraph();
    auto* v8ct = V8ContextTracker::GetFromGraph(graph);
    ASSERT_TRUE(v8ct);

    // The browser could start with execution contexts and/or v8 contexts (for
    // example if it creates a spare renderer loading about:blank, or
    // something preloads a utility context).
    current_counts_.v8_context_count = v8ct->GetV8ContextCountForTesting();
    current_counts_.execution_context_count =
        v8ct->GetExecutionContextCountForTesting();

    // There should not be any detached or destroyed contexts on start.
    EXPECT_EQ(v8ct->GetDetachedV8ContextCountForTesting(), 0u);
    EXPECT_EQ(v8ct->GetDestroyedExecutionContextCountForTesting(), 0u);
    Super::SetUpOnMainThread();
  }

  void ExpectCountIncrease(
      ContextCounts count_change,
      const base::Location& location = base::Location::Current()) {
    Graph* graph = PerformanceManager::GetGraph();
    SCOPED_TRACE(location.ToString());
    auto* v8ct = V8ContextTracker::GetFromGraph(graph);
    ASSERT_TRUE(v8ct);

    // There may be extra V8 contexts created, such as for lazily-created
    // utility contexts.
    EXPECT_GE(v8ct->GetV8ContextCountForTesting(),
              current_counts_.v8_context_count + count_change.v8_context_count)
        << "expected increase " << count_change.v8_context_count;
    current_counts_.v8_context_count = v8ct->GetV8ContextCountForTesting();

    EXPECT_EQ(v8ct->GetExecutionContextCountForTesting(),
              current_counts_.execution_context_count +
                  count_change.execution_context_count)
        << "expected increase " << count_change.execution_context_count;
    current_counts_.execution_context_count =
        v8ct->GetExecutionContextCountForTesting();

    EXPECT_EQ(v8ct->GetDetachedV8ContextCountForTesting(),
              current_counts_.detached_v8_context_count +
                  count_change.detached_v8_context_count)
        << "expected increase " << count_change.detached_v8_context_count;
    current_counts_.detached_v8_context_count =
        v8ct->GetDetachedV8ContextCountForTesting();

    EXPECT_EQ(v8ct->GetDestroyedExecutionContextCountForTesting(),
              current_counts_.destroyed_execution_context_count +
                  count_change.destroyed_execution_context_count)
        << "expected increase "
        << count_change.destroyed_execution_context_count;
    ;
    current_counts_.destroyed_execution_context_count =
        v8ct->GetDestroyedExecutionContextCountForTesting();
  }

 private:
  ContextCounts current_counts_;
};

IN_PROC_BROWSER_TEST_F(MAYBE_V8ContextTrackerTest, AboutBlank) {
  ASSERT_TRUE(NavigateToURL(shell(), GURL("about:blank")));
  ExpectCountIncrease({.v8_context_count = 1, .execution_context_count = 1});
}

IN_PROC_BROWSER_TEST_F(MAYBE_V8ContextTrackerTest,
                       SameOriginIframeAttributionData) {
  GURL urla(embedded_test_server()->GetURL("a.com", "/a_embeds_a.html"));
  auto* contents = shell()->web_contents();
  ASSERT_TRUE(
      NavigateAndWaitForConsoleMessage(contents, urla, "a.html loaded"));

  // Get pointers to the RFHs for each frame.
  content::RenderFrameHost* main_rfh = contents->GetPrimaryMainFrame();
  content::RenderFrameHost* child_rfh = ChildFrameAt(main_rfh, 0);
  ASSERT_TRUE(child_rfh);

  auto frame_node =
      PerformanceManager::GetFrameNodeForRenderFrameHost(child_rfh);

  Graph* graph = PerformanceManager::GetGraph();
  ASSERT_TRUE(frame_node);
  auto* v8_context_tracker = V8ContextTracker::GetFromGraph(graph);
  ASSERT_TRUE(v8_context_tracker);
  auto* ec_state =
      v8_context_tracker->GetExecutionContextState(frame_node->GetFrameToken());
  ASSERT_TRUE(ec_state);
  ASSERT_TRUE(ec_state->iframe_attribution_data);
}

IN_PROC_BROWSER_TEST_F(MAYBE_V8ContextTrackerTest,
                       CrossOriginIframeAttributionData) {
  GURL urla(embedded_test_server()->GetURL("a.com", "/a_embeds_b.html"));
  auto* contents = shell()->web_contents();
  ASSERT_TRUE(
      NavigateAndWaitForConsoleMessage(contents, urla, "b.html loaded"));

  // Get pointers to the RFHs for each frame.
  content::RenderFrameHost* main_rfh = contents->GetPrimaryMainFrame();
  content::RenderFrameHost* child_rfh = ChildFrameAt(main_rfh, 0);
  ASSERT_TRUE(child_rfh);
  auto frame_node =
      PerformanceManager::GetFrameNodeForRenderFrameHost(child_rfh);

  Graph* graph = PerformanceManager::GetGraph();
  ASSERT_TRUE(frame_node);
  auto* v8_context_tracker = V8ContextTracker::GetFromGraph(graph);
  ASSERT_TRUE(v8_context_tracker);
  auto* ec_state =
      v8_context_tracker->GetExecutionContextState(frame_node->GetFrameToken());
  ASSERT_TRUE(ec_state);
  ASSERT_TRUE(ec_state->iframe_attribution_data)
      << "url " << frame_node->GetURL() << ", current "
      << frame_node->IsCurrent() << ", state "
      << frame_node->GetLifecycleState();
}

IN_PROC_BROWSER_TEST_F(MAYBE_V8ContextTrackerTest, SameSiteNavigation) {
  auto* contents = shell()->web_contents();
  GURL urla(embedded_test_server()->GetURL("a.com", "/a_embeds_b.html"));
  ASSERT_TRUE(
      NavigateAndWaitForConsoleMessage(contents, urla, "b.html loaded"));
  ExpectCountIncrease({.v8_context_count = 2, .execution_context_count = 2});

  // Get pointers to the RFHs for each frame.
  content::RenderFrameHost* rfha = contents->GetPrimaryMainFrame();
  content::RenderFrameHost* rfhb = ChildFrameAt(rfha, 0);
  bool rfh_should_change =
      rfhb->ShouldChangeRenderFrameHostOnSameSiteNavigation();

  // Execute a same site navigation in the child frame. This causes a
  // v8 context to be detached, and new context attached to the execution
  // context.
  GURL urlb(embedded_test_server()->GetURL("b.com", "/b.html?foo=bar"));
  ASSERT_TRUE(ExecJs(
      rfhb, base::StringPrintf("location.href = \"%s\"", urlb.spec().c_str())));
  WaitForLoad(contents);

  if (rfh_should_change) {
    // When RenderDocument is enabled, a new RenderFrameHost will be created for
    // the navigation to `urlb`. Both a new V8 context and ExecutionContext are
    // created, and the old ExecutionContext is destroyed..
    ExpectCountIncrease({.v8_context_count = 1,
                         .execution_context_count = 1,
                         .detached_v8_context_count = 1,
                         .destroyed_execution_context_count = 1});
  } else {
    // When RenderDocument is disabled, the same RenderFrameHost will be reused
    // for the navigation to `urlb`. So only a new V8 context will be created,
    // not a new ExecutionContext.
    ExpectCountIncrease(
        {.v8_context_count = 1, .detached_v8_context_count = 1});
  }
}

IN_PROC_BROWSER_TEST_F(MAYBE_V8ContextTrackerTest, DetachedContext) {
  auto* contents = shell()->web_contents();
  GURL urla(embedded_test_server()->GetURL("a.com", "/a_embeds_a.html"));
  ASSERT_TRUE(
      NavigateAndWaitForConsoleMessage(contents, urla, "a.html loaded"));
  ExpectCountIncrease({.v8_context_count = 2, .execution_context_count = 2});

  // Get pointers to the RFHs for each frame.
  content::RenderFrameHost* rfha = contents->GetPrimaryMainFrame();

  content::RenderFrameHost* iframe = ChildFrameAt(rfha, 0);
  content::RenderFrameDeletedObserver iframe_deleted_observer(iframe);

  // Keep a pointer to the window associated with the child iframe, but
  // unload it.
  ASSERT_TRUE(ExecJs(rfha,
                     "let iframe = document.getElementsByTagName('iframe')[0]; "
                     "document.body.leakyRef = iframe.contentWindow.window; "
                     "iframe.parentNode.removeChild(iframe); "
                     "console.log('detached and leaked iframe');"));

  iframe_deleted_observer.WaitUntilDeleted();

  ExpectCountIncrease({
      .detached_v8_context_count = 1,
      .destroyed_execution_context_count = 1,
  });
}

}  // namespace v8_memory
}  // namespace performance_manager
