// Copyright 2019 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/performance_manager_tab_helper.h"

#include <optional>
#include <set>
#include <utility>

#include "base/test/scoped_feature_list.h"
#include "components/performance_manager/graph/frame_node_impl.h"
#include "components/performance_manager/graph/graph_impl.h"
#include "components/performance_manager/graph/page_node_impl.h"
#include "components/performance_manager/graph/process_node_impl.h"
#include "components/performance_manager/performance_manager_impl.h"
#include "components/performance_manager/public/features.h"
#include "components/performance_manager/public/graph/graph.h"
#include "components/performance_manager/public/graph/graph_operations.h"
#include "components/performance_manager/public/graph/page_node.h"
#include "components/performance_manager/render_process_user_data.h"
#include "components/performance_manager/test_support/graph/mock_page_node_observer.h"
#include "components/performance_manager/test_support/performance_manager_test_harness.h"
#include "content/public/browser/browser_context.h"
#include "content/public/browser/permission_descriptor_util.h"
#include "content/public/browser/permission_result.h"
#include "content/public/browser/render_process_host.h"
#include "content/public/common/content_features.h"
#include "content/public/common/process_type.h"
#include "content/public/test/mock_permission_controller.h"
#include "content/public/test/navigation_simulator.h"
#include "content/public/test/permissions_test_utils.h"
#include "content/public/test/render_frame_host_test_support.h"
#include "content/public/test/web_contents_tester.h"
#include "testing/gmock/include/gmock/gmock.h"
#include "testing/gtest/include/gtest/gtest.h"
#include "third_party/blink/public/common/permissions/permission_utils.h"
#include "third_party/blink/public/mojom/favicon/favicon_url.mojom.h"

namespace performance_manager {

namespace {

const char kParentUrl[] = "https://parent.com/";
const char kChild1Url[] = "https://child1.com/";
const char kChild2Url[] = "https://child2.com/";
const char kGrandchildUrl[] = "https://grandchild.com/";
const char kNewGrandchildUrl[] = "https://newgrandchild.com/";
const char kCousinFreddyUrl[] = "https://cousinfreddy.com/";

class PerformanceManagerTabHelperTest
    : public PerformanceManagerTestHarness,
      public testing::WithParamInterface<bool> {
 public:
  PerformanceManagerTabHelperTest() {
    scoped_feature_list_.InitWithFeatureState(
        features::kSeamlessRenderFrameSwap, GetParam());
  }

  void TearDown() override {
    // Clean up the web contents, which should dispose of the page and frame
    // nodes involved.
    DeleteContents();

    PerformanceManagerTestHarness::TearDown();
  }

  // A helper function for checking that the graph matches the topology of
  // stuff in content. The graph should reflect the set of processes provided
  // by |hosts|, even though content may actually have other processes lying
  // around.
  void CheckGraphTopology(const std::set<content::RenderProcessHost*>& hosts,
                          const char* grandchild_url);

 protected:
  static size_t CountAllRenderProcessHosts() {
    size_t num_hosts = 0;
    for (auto it = content::RenderProcessHost::AllHostsIterator();
         !it.IsAtEnd(); it.Advance()) {
      ++num_hosts;
    }
    return num_hosts;
  }

  static size_t CountAllRenderProcessNodes(Graph* graph) {
    size_t num_hosts = 0;
    for (const ProcessNode* process_node : graph->GetAllProcessNodes()) {
      if (process_node->GetProcessType() == content::PROCESS_TYPE_RENDERER) {
        ++num_hosts;
      }
    }
    return num_hosts;
  }

  base::test::ScopedFeatureList scoped_feature_list_;
};

void PerformanceManagerTabHelperTest::CheckGraphTopology(
    const std::set<content::RenderProcessHost*>& hosts,
    const char* grandchild_url) {
  // There may be more RenderProcessHosts in existence than those used from
  // the RFHs above. The graph may not reflect all of them, as only those
  // observed through the TabHelper will have been reflected in the graph.
  size_t num_hosts = CountAllRenderProcessHosts();
  EXPECT_LE(hosts.size(), num_hosts);
  EXPECT_NE(0u, hosts.size());

  // Convert the RPHs to ProcessNodeImpls so we can check they match.
  std::set<const ProcessNode*> process_nodes;
  for (auto* host : hosts) {
    auto* data = RenderProcessUserData::GetForRenderProcessHost(host);
    EXPECT_TRUE(data);
    process_nodes.insert(data->process_node());
  }
  EXPECT_EQ(process_nodes.size(), hosts.size());

  // Check out the graph itself.
  auto* graph = PerformanceManager::GetGraph();

  EXPECT_GE(num_hosts, CountAllRenderProcessNodes(graph));
  EXPECT_EQ(4u, graph->GetAllFrameNodes().size());

  // Expect all frame nodes to be current. This fails if our
  // implementation of RenderFrameHostChanged is borked.
  for (auto* frame : graph->GetAllFrameNodes()) {
    EXPECT_TRUE(frame->IsCurrent());
  }

  ASSERT_EQ(1u, graph->GetAllPageNodes().size());
  auto* page = graph->GetAllPageNodes().AsVector()[0];

  // Extra RPHs can and most definitely do exist.
  auto associated_process_nodes =
      GraphOperations::GetAssociatedProcessNodes(page);
  EXPECT_GE(CountAllRenderProcessNodes(graph), associated_process_nodes.size());
  EXPECT_GE(num_hosts, associated_process_nodes.size());

  for (const ProcessNode* process_node : associated_process_nodes) {
    EXPECT_TRUE(process_nodes.contains(process_node));
  }

  EXPECT_EQ(4u, GraphOperations::GetFrameNodes(page).size());
  ASSERT_EQ(1u, page->GetMainFrameNodes().size());

  auto* main_frame = page->GetPrimaryMainFrameNode();
  EXPECT_EQ(kParentUrl, main_frame->GetURL().spec());
  EXPECT_EQ(2u, main_frame->GetChildFrameNodes().size());

  for (const FrameNode* child_frame : main_frame->GetChildFrameNodes()) {
    if (child_frame->GetURL().spec() == kChild1Url) {
      ASSERT_EQ(1u, child_frame->GetChildFrameNodes().size());
      auto* grandchild_frame = *child_frame->GetChildFrameNodes().begin();
      EXPECT_EQ(grandchild_url, grandchild_frame->GetURL().spec());
    } else if (child_frame->GetURL().spec() == kChild2Url) {
      EXPECT_TRUE(child_frame->GetChildFrameNodes().empty());
    } else {
      FAIL() << "Unexpected child frame: " << child_frame->GetURL().spec();
    }
  }
}

}  // namespace

INSTANTIATE_TEST_SUITE_P(All, PerformanceManagerTabHelperTest, testing::Bool());

TEST_P(PerformanceManagerTabHelperTest, FrameHierarchyReflectsToGraph) {
  SetContents(CreateTestWebContents());

  auto* parent = content::NavigationSimulator::NavigateAndCommitFromBrowser(
      web_contents(), GURL(kParentUrl));
  DCHECK(parent);

  auto* parent_tester = content::RenderFrameHostTester::For(parent);
  auto* child1 = content::NavigationSimulator::NavigateAndCommitFromDocument(
      GURL(kChild1Url), parent_tester->AppendChild("child1"));
  auto* grandchild =
      content::NavigationSimulator::NavigateAndCommitFromDocument(
          GURL(kGrandchildUrl),
          content::RenderFrameHostTester::For(child1)->AppendChild(
              "grandchild"));
  auto* child2 = content::NavigationSimulator::NavigateAndCommitFromDocument(
      GURL(kChild2Url), parent_tester->AppendChild("child2"));

  // Count the RFHs referenced.
  std::set<content::RenderProcessHost*> hosts;
  auto* grandchild_process = grandchild->GetProcess();
  hosts.insert(main_rfh()->GetProcess());
  hosts.insert(child1->GetProcess());
  hosts.insert(grandchild->GetProcess());
  hosts.insert(child2->GetProcess());

  CheckGraphTopology(hosts, kGrandchildUrl);

  // Navigate the grand-child frame. This tests that we accurately observe the
  // new RFH being created and marked current, with the old one being marked not
  // current and torn down. Note that the old RPH doesn't get torn down.
  auto* new_grandchild =
      content::NavigationSimulator::NavigateAndCommitFromDocument(
          GURL(kNewGrandchildUrl), grandchild);
  auto* new_grandchild_process = new_grandchild->GetProcess();

  // Update the set of processes we expect to be associated with the page.
  hosts.erase(grandchild_process);
  hosts.insert(new_grandchild_process);

  CheckGraphTopology(hosts, kNewGrandchildUrl);

  // Clean up the web contents, which should dispose of the page and frame nodes
  // involved.
  DeleteContents();

  // Allow content/ to settle.
  task_environment()->RunUntilIdle();

  size_t num_hosts = CountAllRenderProcessHosts();

  auto* graph = PerformanceManager::GetGraph();
  EXPECT_GE(num_hosts, CountAllRenderProcessNodes(graph));
  EXPECT_EQ(0u, graph->GetAllFrameNodes().size());
  ASSERT_EQ(0u, graph->GetAllPageNodes().size());
}

namespace {

void ExpectPageIsAudible(bool is_audible) {
  auto* graph = PerformanceManager::GetGraph();
  ASSERT_EQ(1u, graph->GetAllPageNodes().size());
  auto* page = graph->GetAllPageNodes().AsVector()[0];
  EXPECT_EQ(is_audible, page->IsAudible());
}

#if !BUILDFLAG(IS_ANDROID)
void ExpectNotificationPermissionStatus(
    std::optional<blink::mojom::PermissionStatus> status) {
  auto* graph = PerformanceManager::GetGraph();
  ASSERT_EQ(1u, graph->GetAllPageNodes().size());
  auto* page = graph->GetAllPageNodes().AsVector()[0];
  EXPECT_EQ(status, page->GetNotificationPermissionStatus());
}
#endif  // !BUILDFLAG(IS_ANDROID)

}  // namespace

TEST_P(PerformanceManagerTabHelperTest, PageIsAudible) {
  SetContents(CreateTestWebContents());

  ExpectPageIsAudible(false);
  content::WebContentsTester::For(web_contents())->SetIsCurrentlyAudible(true);
  ExpectPageIsAudible(true);
  content::WebContentsTester::For(web_contents())->SetIsCurrentlyAudible(false);
  ExpectPageIsAudible(false);
}

#if !BUILDFLAG(IS_ANDROID)
TEST_P(PerformanceManagerTabHelperTest, NotificationPermission) {
  auto owned_permission_controller =
      std::make_unique<testing::NiceMock<content::MockPermissionController>>();
  auto* permission_controller = owned_permission_controller.get();
  GetBrowserContext()->SetPermissionControllerForTesting(
      std::move(owned_permission_controller));
  content::PermissionController::SubscriptionId::Generator
      subscription_id_generator;
  const auto kFirstSubscriptionId = subscription_id_generator.GenerateNextId();
  const auto kSecondSubscriptionId = subscription_id_generator.GenerateNextId();

  SetContents(CreateTestWebContents());
  ExpectNotificationPermissionStatus(std::nullopt);

  // Navigate to an origin with `PermissionStatus::ASK`.
  {
    content::RenderFrameHost* rfh_arg = nullptr;
    content::RenderFrameHost* rfh_arg_2 = nullptr;
    blink::mojom::PermissionDescriptorPtr descriptor;

    EXPECT_CALL(*permission_controller, GetPermissionStatusForCurrentDocument)
        .WillOnce([&](const blink::mojom::PermissionDescriptorPtr&
                          permission_descriptor,
                      content::RenderFrameHost* render_frame_host) {
          descriptor = permission_descriptor->Clone();
          rfh_arg = render_frame_host;
          return blink::mojom::PermissionStatus::ASK;
        });
    EXPECT_CALL(*permission_controller,
                SubscribeToPermissionResultChange(
                    PermissionDescriptorToPermissionTypeMatcher(
                        blink::PermissionType::NOTIFICATIONS),
                    testing::_, testing::_, testing::_, testing::_, testing::_))
        .WillOnce(testing::DoAll(testing::SaveArg<2>(&rfh_arg_2),
                                 testing::Return(kFirstSubscriptionId)));

    content::NavigationSimulator::NavigateAndCommitFromBrowser(
        web_contents(), GURL(kParentUrl));
    testing::Mock::VerifyAndClear(permission_controller);
    EXPECT_EQ(blink::PermissionDescriptorToPermissionType(descriptor),
              blink::PermissionType::NOTIFICATIONS);
    EXPECT_EQ(rfh_arg, web_contents()->GetPrimaryMainFrame());
    ExpectNotificationPermissionStatus(blink::mojom::PermissionStatus::ASK);
  }

  base::RepeatingCallback<void(content::PermissionResult)> callback_arg;

  // Navigate to an origin with `PermissionStatus::GRANTED`.
  {
    content::RenderFrameHost* rfh_arg = nullptr;
    content::RenderProcessHost* rph_arg = nullptr;
    blink::mojom::PermissionDescriptorPtr descriptor;

    EXPECT_CALL(*permission_controller, GetPermissionStatusForCurrentDocument)
        .WillOnce([&](const blink::mojom::PermissionDescriptorPtr&
                          permission_descriptor,
                      content::RenderFrameHost* render_frame_host) {
          descriptor = permission_descriptor->Clone();
          rfh_arg = render_frame_host;
          return blink::mojom::PermissionStatus::GRANTED;
        });
    EXPECT_CALL(*permission_controller,
                UnsubscribeFromPermissionResultChange(kFirstSubscriptionId));
    EXPECT_CALL(*permission_controller,
                SubscribeToPermissionResultChange(
                    PermissionDescriptorToPermissionTypeMatcher(
                        blink::PermissionType::NOTIFICATIONS),
                    testing::_, testing::_, testing::_, testing::_, testing::_))
        .WillOnce(testing::DoAll(testing::SaveArg<1>(&rph_arg),
                                 testing::SaveArg<5>(&callback_arg),
                                 testing::Return(kSecondSubscriptionId)));

    content::NavigationSimulator::NavigateAndCommitFromBrowser(
        web_contents(), GURL(kCousinFreddyUrl));
    testing::Mock::VerifyAndClear(permission_controller);
    EXPECT_EQ(blink::PermissionDescriptorToPermissionType(descriptor),
              blink::PermissionType::NOTIFICATIONS);
    EXPECT_EQ(rfh_arg, web_contents()->GetPrimaryMainFrame());
    ExpectNotificationPermissionStatus(blink::mojom::PermissionStatus::GRANTED);
  }

  // Simulate a change of permission status independent from navigation.
  callback_arg.Run(
      content::PermissionResult(blink::mojom::PermissionStatus::DENIED));
  ExpectNotificationPermissionStatus(blink::mojom::PermissionStatus::DENIED);

  // The last subscription is removed when the tab helper is deleted.
  EXPECT_CALL(*permission_controller,
              UnsubscribeFromPermissionResultChange(kSecondSubscriptionId));
}
#endif  // BUILDFLAG(IS_ANDROID)

TEST_P(PerformanceManagerTabHelperTest, GetFrameNode) {
  SetContents(CreateTestWebContents());

  auto* tab_helper =
      PerformanceManagerTabHelper::FromWebContents(web_contents());
  ASSERT_TRUE(tab_helper);

  // GetFrameNode() can return nullptr. In this test, it is achieved by using an
  // empty RenderFrameHost.
  auto* empty_frame = web_contents()->GetPrimaryMainFrame();
  DCHECK(empty_frame);

  auto* empty_frame_node = tab_helper->GetFrameNode(empty_frame);
  EXPECT_FALSE(empty_frame_node);

  // This navigation will create a frame node.
  auto* new_frame = content::NavigationSimulator::NavigateAndCommitFromBrowser(
      web_contents(), GURL(kParentUrl));
  DCHECK(new_frame);

  auto* new_frame_node = tab_helper->GetFrameNode(new_frame);
  EXPECT_TRUE(new_frame_node);
}

TEST_P(PerformanceManagerTabHelperTest,
       NotificationsFromInactiveFrameTreeAreIgnored) {
  // When this feature is enabled, PerformanceManagerTabHelper does not ignore
  // the first favicon/title update.
  base::test::ScopedFeatureList feature_list;
  feature_list.InitAndEnableFeature(
      features::kUseLoadingStateToDetectBackgroundTitleOrFaviconUpdate);

  SetContents(CreateTestWebContents());

  content::NavigationSimulator::NavigateAndCommitFromBrowser(web_contents(),
                                                             GURL(kParentUrl));
  auto* first_nav_main_rfh = web_contents()->GetPrimaryMainFrame();

  content::LeaveInPendingDeletionState(first_nav_main_rfh);

  content::NavigationSimulator::NavigateAndCommitFromBrowser(
      web_contents(), GURL(kCousinFreddyUrl));
  EXPECT_NE(web_contents()->GetPrimaryMainFrame(), first_nav_main_rfh);

  // Mock observer.
  Graph* graph = PerformanceManager::GetGraph();

  MockPageNodeObserver observer;
  graph->AddPageNodeObserver(&observer);

  auto* tab_helper =
      PerformanceManagerTabHelper::FromWebContents(web_contents());
  ASSERT_TRUE(tab_helper);

  tab_helper->DidUpdateFaviconURL(
      first_nav_main_rfh, {},
      blink::mojom::FaviconUpdateReason::kLinkElementChange);

  // The observer shouldn't have been called at this point.
  testing::Mock::VerifyAndClear(&observer);
  // Set the expectation for the next check.
  EXPECT_CALL(observer, OnFaviconUpdated(::testing::_, ::testing::_));

  // Sanity check to ensure that notification sent to the active main frame are
  // forwarded.
  tab_helper->DidUpdateFaviconURL(
      web_contents()->GetPrimaryMainFrame(), {},
      blink::mojom::FaviconUpdateReason::kLinkElementChange);

  testing::Mock::VerifyAndClear(&observer);
  graph->RemovePageNodeObserver(&observer);
}

}  // namespace performance_manager
