// 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/graph/frame_node_impl.h"

#include <optional>
#include <vector>

#include "base/memory/raw_ptr.h"
#include "base/test/bind.h"
#include "base/test/run_until.h"
#include "base/test/scoped_feature_list.h"
#include "chrome/browser/preloading/scoped_prewarm_feature_list.h"
#include "chrome/browser/ui/browser.h"
#include "chrome/test/base/in_process_browser_test.h"
#include "chrome/test/base/ui_test_utils.h"
#include "components/performance_manager/graph/frame_node_impl.h"
#include "components/performance_manager/performance_manager_impl.h"
#include "components/performance_manager/public/features.h"
#include "components/performance_manager/public/graph/frame_node.h"
#include "components/performance_manager/public/graph/graph.h"
#include "components/performance_manager/public/viewport_intersection.h"
#include "content/public/browser/render_process_host.h"
#include "content/public/common/result_codes.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/prerender_test_util.h"
#include "net/dns/mock_host_resolver.h"
#include "testing/gmock/include/gmock/gmock.h"
#include "testing/gtest/include/gtest/gtest.h"
#include "ui/gfx/geometry/rect.h"
#include "ui/gfx/geometry/rect_conversions.h"
#include "ui/gfx/geometry/rect_f.h"

namespace performance_manager {

using testing::AllOf;
using testing::Not;
using testing::UnorderedElementsAre;

MATCHER(IsMainFrame, "") {
  return arg->IsMainFrame();
}

MATCHER_P(HasViewportIntersection, viewport_intersection, "") {
  return arg->GetViewportIntersection() == viewport_intersection;
}

namespace {

// Returns true if the mojom::DocumentCoordinationUnit connection associated
// with `render_frame_host` is bound.
bool IsDocumentCoordinatorUnitBound(
    content::RenderFrameHost* render_frame_host) {
  base::WeakPtr<FrameNode> frame_node =
      PerformanceManager::GetFrameNodeForRenderFrameHost(render_frame_host);
  if (!frame_node) {
    return false;
  }

  FrameNodeImpl* frame_node_impl = FrameNodeImpl::FromNode(frame_node.get());
  return frame_node_impl->IsDocumentCoordinationUnitBoundForTesting();
}

std::vector<const FrameNode*> GetFrameNodesForWebContents(
    content::WebContents* web_contents) {
  const PageNode* page_node =
      PerformanceManager::GetPrimaryPageNodeForWebContents(web_contents).get();

  Graph* graph = PerformanceManager::GetGraph();
  std::vector<const FrameNode*> page_frame_nodes;
  for (const FrameNode* node : graph->GetAllFrameNodes()) {
    if (node->GetPageNode() == page_node) {
      page_frame_nodes.push_back(node);
    }
  }
  return page_frame_nodes;
}

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

 private:
  // TODO(https://crbug.com/423465927): Explore a better approach to make the
  // existing tests run with the prewarm feature enabled.
  test::ScopedPrewarmFeatureList scoped_prewarm_feature_list_{
      test::ScopedPrewarmFeatureList::PrewarmState::kDisabled};
};

class ParameterizedFrameNodeImplBrowserTest
    : public FrameNodeImplBrowserTest,
      public testing::WithParamInterface<bool> {
 public:
  ParameterizedFrameNodeImplBrowserTest() {
    base::FieldTrialParams params = {
        {features::kRenderedOutOfViewIsNotVisible.name,
         GetParam() ? "true" : "false"}};
    scoped_feature_list_.InitAndEnableFeatureWithParameters(
        features::kPMProcessPriorityPolicy, params);
  }
  ~ParameterizedFrameNodeImplBrowserTest() override = default;

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

}  // namespace

IN_PROC_BROWSER_TEST_P(ParameterizedFrameNodeImplBrowserTest,
                       ViewportIntersection_OutOfView) {
  ASSERT_TRUE(embedded_test_server()->Start());
  EXPECT_EQ(1, browser()->tab_strip_model()->count());

  const bool expects_intersects_viewport =
      !performance_manager::features::kRenderedOutOfViewIsNotVisible.Get();
  testing::Matcher<const FrameNode*> viewport_intersection_matcher =
      expects_intersects_viewport
          ? HasViewportIntersection(ViewportIntersection::kIntersecting)
          : HasViewportIntersection(ViewportIntersection::kNotIntersecting);

  const GURL main_frame_url(
      embedded_test_server()->GetURL("/iframe_out_of_view.html"));
  ASSERT_TRUE(ui_test_utils::NavigateToURL(browser(), main_frame_url));

  ASSERT_TRUE(base::test::RunUntil([&]() {
    return testing::Matches(UnorderedElementsAre(
        // One main frame, intersects with the viewport.
        AllOf(IsMainFrame(),
              HasViewportIntersection(ViewportIntersection::kIntersecting)),
        // One child frame, intersects with the viewport depending on the
        // value of the kRenderedOutOfViewIsNotVisible feature.
        AllOf(Not(IsMainFrame()), viewport_intersection_matcher)))(
        GetFrameNodesForWebContents(
            browser()->tab_strip_model()->GetActiveWebContents()));
  }));
}

INSTANTIATE_TEST_SUITE_P(,
                         ParameterizedFrameNodeImplBrowserTest,
                         testing::Bool());

IN_PROC_BROWSER_TEST_F(FrameNodeImplBrowserTest, ViewportIntersection_Hidden) {
  ASSERT_TRUE(embedded_test_server()->Start());
  EXPECT_EQ(1, browser()->tab_strip_model()->count());

  const GURL main_frame_url(
      embedded_test_server()->GetURL("/iframe_hidden.html"));
  ASSERT_TRUE(ui_test_utils::NavigateToURL(browser(), main_frame_url));

  ASSERT_TRUE(base::test::RunUntil([&]() {
    return testing::Matches(UnorderedElementsAre(
        // One main frame, intersects with the viewport.
        AllOf(IsMainFrame(),
              HasViewportIntersection(ViewportIntersection::kIntersecting)),
        // One child frame, does not intersect with the viewport.
        AllOf(
            Not(IsMainFrame()),
            HasViewportIntersection(ViewportIntersection::kNotIntersecting))))(
        GetFrameNodesForWebContents(
            browser()->tab_strip_model()->GetActiveWebContents()));
  }));
}

IN_PROC_BROWSER_TEST_F(FrameNodeImplBrowserTest,
                       ViewportIntersection_PartiallyVisible) {
  ASSERT_TRUE(embedded_test_server()->Start());
  EXPECT_EQ(1, browser()->tab_strip_model()->count());

  const GURL main_frame_url(
      embedded_test_server()->GetURL("/iframe_partially_visible.html"));
  ASSERT_TRUE(ui_test_utils::NavigateToURL(browser(), main_frame_url));

  ASSERT_TRUE(base::test::RunUntil([&]() {
    return testing::Matches(UnorderedElementsAre(
        // One main frame, intersects with the viewport.
        AllOf(IsMainFrame(),
              HasViewportIntersection(ViewportIntersection::kIntersecting)),
        // One child frame, also intersects with the viewport.
        AllOf(Not(IsMainFrame()),
              HasViewportIntersection(ViewportIntersection::kIntersecting))))(
        GetFrameNodesForWebContents(
            browser()->tab_strip_model()->GetActiveWebContents()));
  }));
}

IN_PROC_BROWSER_TEST_F(FrameNodeImplBrowserTest, ViewportIntersection_Scaled) {
  ASSERT_TRUE(embedded_test_server()->Start());
  EXPECT_EQ(1, browser()->tab_strip_model()->count());

  const GURL main_frame_url(
      embedded_test_server()->GetURL("/iframe_scaled.html"));
  ASSERT_TRUE(ui_test_utils::NavigateToURL(browser(), main_frame_url));

  ASSERT_TRUE(base::test::RunUntil([&]() {
    return testing::Matches(UnorderedElementsAre(
        // One main frame, intersects with the viewport.
        AllOf(IsMainFrame(),
              HasViewportIntersection(ViewportIntersection::kIntersecting)),
        // One child frame, also intersects with the viewport.
        AllOf(Not(IsMainFrame()),
              HasViewportIntersection(ViewportIntersection::kIntersecting))))(
        GetFrameNodesForWebContents(
            browser()->tab_strip_model()->GetActiveWebContents()));
  }));
}

IN_PROC_BROWSER_TEST_F(FrameNodeImplBrowserTest, ViewportIntersection_Rotated) {
  ASSERT_TRUE(embedded_test_server()->Start());
  EXPECT_EQ(1, browser()->tab_strip_model()->count());

  const GURL main_frame_url(
      embedded_test_server()->GetURL("/iframe_rotated.html"));
  ASSERT_TRUE(ui_test_utils::NavigateToURL(browser(), main_frame_url));

  ASSERT_TRUE(base::test::RunUntil([&]() {
    return testing::Matches(UnorderedElementsAre(
        // One main frame, intersects with the viewport.
        AllOf(IsMainFrame(),
              HasViewportIntersection(ViewportIntersection::kIntersecting)),
        // One child frame, also intersects with the viewport.
        AllOf(Not(IsMainFrame()),
              HasViewportIntersection(ViewportIntersection::kIntersecting))))(
        GetFrameNodesForWebContents(
            browser()->tab_strip_model()->GetActiveWebContents()));
  }));
}

IN_PROC_BROWSER_TEST_F(FrameNodeImplBrowserTest, Bind_SimpleNavigation) {
  ASSERT_TRUE(embedded_test_server()->Start());
  EXPECT_EQ(1, browser()->tab_strip_model()->count());

  const GURL kTestUrl = embedded_test_server()->GetURL("/title1.html");

  content::RenderFrameHost* rfh =
      ui_test_utils::NavigateToURL(browser(), kTestUrl);
  ASSERT_TRUE(rfh);
  EXPECT_EQ(rfh->GetLifecycleState(),
            content::RenderFrameHost::LifecycleState::kActive);

  ASSERT_TRUE(base::test::RunUntil(
      [&]() { return IsDocumentCoordinatorUnitBound(rfh); }));
}

class FrameNodeImplBackForwardCacheBrowserTest
    : public FrameNodeImplBrowserTest {
 public:
  FrameNodeImplBackForwardCacheBrowserTest() {
    content::InitBackForwardCacheFeature(&scoped_feature_list_,
                                         /*enable_back_forward_cache=*/true);
  }

 public:
  base::test::ScopedFeatureList scoped_feature_list_;
};

IN_PROC_BROWSER_TEST_F(FrameNodeImplBackForwardCacheBrowserTest,
                       Bind_BackForwardCache) {
  ASSERT_TRUE(embedded_test_server()->Start());
  EXPECT_EQ(1, browser()->tab_strip_model()->count());

  const GURL kTestUrl = embedded_test_server()->GetURL("/title1.html");
  const GURL kOtherUrl = embedded_test_server()->GetURL("/title2.html");

  // Navigation to the test URL.
  content::RenderFrameHost* rfh =
      ui_test_utils::NavigateToURL(browser(), kTestUrl);
  ASSERT_TRUE(rfh);
  EXPECT_EQ(rfh->GetLifecycleState(),
            content::RenderFrameHost::LifecycleState::kActive);

  // Navigate to some other URL.
  ASSERT_TRUE(ui_test_utils::NavigateToURL(browser(), kOtherUrl));
  EXPECT_EQ(rfh->GetLifecycleState(),
            content::RenderFrameHost::LifecycleState::kInBackForwardCache);

  // Back to the test URL.
  EXPECT_TRUE(content::HistoryGoBack(
      browser()->tab_strip_model()->GetActiveWebContents()));
  EXPECT_EQ(rfh->GetLifecycleState(),
            content::RenderFrameHost::LifecycleState::kActive);

  ASSERT_TRUE(base::test::RunUntil(
      [&]() { return IsDocumentCoordinatorUnitBound(rfh); }));
}

class FrameNodeImplPrerenderBrowserTest : public FrameNodeImplBrowserTest {
 public:
  FrameNodeImplPrerenderBrowserTest()
      : prerender_test_helper_(base::BindRepeating(
            &FrameNodeImplPrerenderBrowserTest::GetWebContents,
            base::Unretained(this))) {}

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

 public:
  content::test::ScopedPrerenderFeatureList scoped_prerender_feature_list_;
  content::test::PrerenderTestHelper prerender_test_helper_;
};

IN_PROC_BROWSER_TEST_F(FrameNodeImplPrerenderBrowserTest,
                       Bind_PrerenderNavigation) {
  ASSERT_TRUE(embedded_test_server()->Start());
  EXPECT_EQ(1, browser()->tab_strip_model()->count());

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

  // Initial navigation. Needed so we can add prerendered frames.
  content::RenderFrameHost* rfh =
      ui_test_utils::NavigateToURL(browser(), kInitialUrl);
  ASSERT_TRUE(rfh);
  EXPECT_EQ(rfh->GetLifecycleState(),
            content::RenderFrameHost::LifecycleState::kActive);

  // Create the prerendered frame.
  content::PrerenderHostId host_id =
      prerender_test_helper_.AddPrerender(kPrerenderUrl);
  content::RenderFrameHost* prerender_rfh =
      prerender_test_helper_.GetPrerenderedMainFrameHost(host_id);
  ASSERT_TRUE(prerender_rfh);
  EXPECT_EQ(prerender_rfh->GetLifecycleState(),
            content::RenderFrameHost::LifecycleState::kPrerendering);

  // Navigate to the prerendered frame.
  prerender_test_helper_.NavigatePrimaryPage(kPrerenderUrl);
  EXPECT_EQ(prerender_rfh->GetLifecycleState(),
            content::RenderFrameHost::LifecycleState::kActive);

  ASSERT_TRUE(base::test::RunUntil(
      [&]() { return IsDocumentCoordinatorUnitBound(prerender_rfh); }));
}

class FrameNodeImplIsActiveBrowserTest : public FrameNodeImplBrowserTest {
 public:
  void SetUpOnMainThread() override {
    FrameNodeImplBrowserTest::SetUpOnMainThread();
    host_resolver()->AddRule("*", "127.0.0.1");
    ASSERT_TRUE(embedded_test_server()->Start());
  }
};

// Tests that FrameNode::IsActive() is correctly set for speculative frames
// that go through a standard (non-early-commit) cross-site navigation.
IN_PROC_BROWSER_TEST_F(FrameNodeImplIsActiveBrowserTest,
                       IsActiveSpeculativeFrame_StandardCommit) {
  const GURL kUrl1 = embedded_test_server()->GetURL("a.com", "/title1.html");
  const GURL kUrl2 = embedded_test_server()->GetURL("b.com", "/title1.html");

  // Initial navigation.
  ASSERT_TRUE(ui_test_utils::NavigateToURL(browser(), kUrl1));

  content::WebContents* web_contents =
      browser()->tab_strip_model()->GetActiveWebContents();

  content::RenderFrameHost* initial_rfh = web_contents->GetPrimaryMainFrame();
  base::WeakPtr<FrameNode> initial_frame_ptr =
      PerformanceManager::GetFrameNodeForRenderFrameHost(initial_rfh);
  ASSERT_TRUE(initial_frame_ptr);
  EXPECT_TRUE(initial_frame_ptr->IsActive());

  // Navigate to a different site to trigger a speculative RenderFrameHost that
  // commits normally (standard commit).
  ASSERT_TRUE(ui_test_utils::NavigateToURL(browser(), kUrl2));

  // Ask content for the new primary main frame, and independently assert its
  // states.
  content::RenderFrameHost* new_rfh = web_contents->GetPrimaryMainFrame();
  EXPECT_NE(initial_rfh, new_rfh);

  base::WeakPtr<FrameNode> new_frame_ptr =
      PerformanceManager::GetFrameNodeForRenderFrameHost(new_rfh);
  ASSERT_TRUE(new_frame_ptr);

  EXPECT_NE(initial_frame_ptr.get(), new_frame_ptr.get());
  EXPECT_TRUE(new_frame_ptr->IsActive());
  EXPECT_TRUE(new_frame_ptr->IsCurrent());
}

// Tests that FrameNode::IsActive() is correctly set for speculative frames
// that go through an early-commit cross-site navigation (triggered here by
// navigating from a crashed RFH).
IN_PROC_BROWSER_TEST_F(FrameNodeImplIsActiveBrowserTest,
                       IsActiveSpeculativeFrame_EarlyCommit) {
  const GURL kUrl1 = embedded_test_server()->GetURL("a.com", "/title1.html");
  const GURL kUrl2 = embedded_test_server()->GetURL("b.com", "/title1.html");

  // Initial navigation.
  ASSERT_TRUE(ui_test_utils::NavigateToURL(browser(), kUrl1));

  content::WebContents* web_contents =
      browser()->tab_strip_model()->GetActiveWebContents();

  content::RenderFrameHost* initial_rfh = web_contents->GetPrimaryMainFrame();
  base::WeakPtr<FrameNode> initial_frame_ptr =
      PerformanceManager::GetFrameNodeForRenderFrameHost(initial_rfh);
  ASSERT_TRUE(initial_frame_ptr);
  EXPECT_TRUE(initial_frame_ptr->IsActive());

  // Crash the current renderer process to trigger an early-commit path on the
  // next navigation.
  content::RenderProcessHost* process =
      web_contents->GetPrimaryMainFrame()->GetProcess();
  ASSERT_TRUE(process);
  content::RenderProcessHostWatcher process_exit_observer(
      process, content::RenderProcessHostWatcher::WATCH_FOR_PROCESS_EXIT);
  process->Shutdown(content::RESULT_CODE_KILLED);
  process_exit_observer.Wait();

  // Navigate to a different site. Because the previous process was crashed,
  // this will trigger an early-commit of the speculative RenderFrameHost.
  ASSERT_TRUE(ui_test_utils::NavigateToURL(browser(), kUrl2));

  // Ask content for the new primary main frame, and independently assert its
  // states.
  content::RenderFrameHost* new_rfh = web_contents->GetPrimaryMainFrame();
  EXPECT_NE(initial_rfh, new_rfh);

  base::WeakPtr<FrameNode> new_frame_ptr =
      PerformanceManager::GetFrameNodeForRenderFrameHost(new_rfh);
  ASSERT_TRUE(new_frame_ptr);

  EXPECT_NE(initial_frame_ptr.get(), new_frame_ptr.get());
  EXPECT_TRUE(new_frame_ptr->IsActive());
  EXPECT_TRUE(new_frame_ptr->IsCurrent());
}

}  // namespace performance_manager
