// Copyright 2017 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/page_node_impl.h"

#include <algorithm>
#include <optional>
#include <string>

#include "base/memory/raw_ptr.h"
#include "base/scoped_observation.h"
#include "components/performance_manager/graph/frame_node_impl.h"
#include "components/performance_manager/graph/graph_impl_operations.h"
#include "components/performance_manager/graph/process_node_impl.h"
#include "components/performance_manager/public/freezing/freezing.h"
#include "components/performance_manager/public/graph/page_node.h"
#include "components/performance_manager/test_support/graph/mock_page_node_observer.h"
#include "components/performance_manager/test_support/graph_test_harness.h"
#include "components/performance_manager/test_support/mock_graphs.h"
#include "testing/gmock/include/gmock/gmock.h"
#include "testing/gtest/include/gtest/gtest.h"
#include "url/gurl.h"

namespace performance_manager {

namespace {

using PageNodeImplTest = GraphTestHarness;

const std::string kHtmlMimeType = "text/html";
const std::string kPdfMimeType = "application/pdf";
const blink::mojom::PermissionStatus kAskNotificationPermission =
    blink::mojom::PermissionStatus::ASK;

const PageNode* ToPublic(PageNodeImpl* page_node) {
  return page_node;
}

const FrameNode* ToPublic(FrameNodeImpl* frame_node) {
  return frame_node;
}

}  // namespace

TEST_F(PageNodeImplTest, SafeDowncast) {
  auto page = CreateNode<PageNodeImpl>();
  PageNode* node = page.get();
  EXPECT_EQ(page.get(), PageNodeImpl::FromNode(node));
  NodeBase* base = page.get();
  EXPECT_EQ(base, NodeBase::FromNode(node));
  EXPECT_EQ(static_cast<Node*>(node), base->ToNode());
}

using PageNodeImplDeathTest = PageNodeImplTest;

TEST_F(PageNodeImplDeathTest, SafeDowncast) {
  auto page = CreateNode<PageNodeImpl>();
  ASSERT_DEATH_IF_SUPPORTED(FrameNodeImpl::FromNodeBase(page.get()), "");
}

TEST_F(PageNodeImplTest, AddFrameBasic) {
  auto process_node = CreateNode<ProcessNodeImpl>();
  auto page_node = CreateNode<PageNodeImpl>();
  auto parent_frame =
      CreateFrameNodeAutoId(process_node.get(), page_node.get());
  auto child1_frame = CreateFrameNodeAutoId(process_node.get(), page_node.get(),
                                            parent_frame.get());
  auto child2_frame = CreateFrameNodeAutoId(process_node.get(), page_node.get(),
                                            parent_frame.get());

  // Validate that all frames are tallied to the page.
  EXPECT_EQ(3u, GraphImplOperations::GetFrameNodes(page_node.get()).size());
}

TEST_F(PageNodeImplTest, RemoveFrame) {
  auto process_node = CreateNode<ProcessNodeImpl>();
  auto page_node = CreateNode<PageNodeImpl>();
  auto frame_node =
      CreateFrameNodeAutoId(process_node.get(), page_node.get(), nullptr);

  // Ensure correct page-frame relationship has been established.
  auto frame_nodes = GraphImplOperations::GetFrameNodes(page_node.get());
  EXPECT_EQ(1u, frame_nodes.size());
  EXPECT_TRUE(std::ranges::contains(frame_nodes, frame_node.get()));
  EXPECT_EQ(page_node.get(), frame_node->page_node());

  frame_node.reset();

  // Parent-child relationships should no longer exist.
  EXPECT_EQ(0u, GraphImplOperations::GetFrameNodes(page_node.get()).size());
}

TEST_F(PageNodeImplTest, GetLastVisibilityChangeTime) {
  MockSinglePageInSingleProcessGraph mock_graph(graph());

  base::TimeTicks t0 = base::TimeTicks::Now();
  mock_graph.page->SetIsVisible(true);
  EXPECT_TRUE(mock_graph.page->IsVisible());
  AdvanceClock(base::Seconds(42));
  EXPECT_EQ(t0, mock_graph.page->GetLastVisibilityChangeTime());

  base::TimeTicks t1 = base::TimeTicks::Now();
  mock_graph.page->SetIsVisible(false);
  AdvanceClock(base::Seconds(23));
  EXPECT_EQ(t1, mock_graph.page->GetLastVisibilityChangeTime());
  EXPECT_FALSE(mock_graph.page->IsVisible());
}

TEST_F(PageNodeImplTest, GetTimeSinceLastAudibleChange) {
  MockSinglePageInSingleProcessGraph mock_graph(graph());
  EXPECT_FALSE(mock_graph.page->IsAudible());
  EXPECT_EQ(std::nullopt, mock_graph.page->GetTimeSinceLastAudibleChange());

  mock_graph.page->SetIsAudible(true);
  EXPECT_TRUE(mock_graph.page->IsAudible());
  AdvanceClock(base::Seconds(42));
  EXPECT_EQ(base::Seconds(42),
            mock_graph.page->GetTimeSinceLastAudibleChange());

  mock_graph.page->SetIsAudible(false);
  AdvanceClock(base::Seconds(23));
  EXPECT_EQ(base::Seconds(23),
            mock_graph.page->GetTimeSinceLastAudibleChange());
  EXPECT_FALSE(mock_graph.page->IsAudible());

  // Test a page that's audible at creation.
  auto audible_page = CreateNode<PageNodeImpl>(
      nullptr, /*browser_context_id=*/base::UnguessableToken(), GURL(),
      PagePropertyFlags{PagePropertyFlag::kIsAudible});
  AdvanceClock(base::Seconds(56));
  EXPECT_EQ(base::Seconds(56), audible_page->GetTimeSinceLastAudibleChange());
  EXPECT_TRUE(audible_page->IsAudible());
}

TEST_F(PageNodeImplTest, GetTimeSinceLastNavigation) {
  MockSinglePageInSingleProcessGraph mock_graph(graph());
  // Before any commit events, timedelta should be 0.
  EXPECT_TRUE(mock_graph.page->GetTimeSinceLastNavigation().is_zero());

  // 1st navigation.
  GURL url("http://www.example.org");
  mock_graph.page->OnMainFrameNavigationCommitted(false, base::TimeTicks::Now(),
                                                  10u, url, kHtmlMimeType,
                                                  kAskNotificationPermission);
  EXPECT_EQ(url, mock_graph.page->GetMainFrameUrl());
  EXPECT_EQ(10u, mock_graph.page->GetNavigationID());
  EXPECT_EQ(kHtmlMimeType, mock_graph.page->GetContentsMimeType());
  AdvanceClock(base::Seconds(11));
  EXPECT_EQ(base::Seconds(11), mock_graph.page->GetTimeSinceLastNavigation());

  // 2nd navigation.
  url = GURL("http://www.example.org/bobcat");
  mock_graph.page->OnMainFrameNavigationCommitted(false, base::TimeTicks::Now(),
                                                  20u, url, kHtmlMimeType,
                                                  kAskNotificationPermission);
  EXPECT_EQ(url, mock_graph.page->GetMainFrameUrl());
  EXPECT_EQ(20u, mock_graph.page->GetNavigationID());
  EXPECT_EQ(kHtmlMimeType, mock_graph.page->GetContentsMimeType());
  AdvanceClock(base::Seconds(17));
  EXPECT_EQ(base::Seconds(17), mock_graph.page->GetTimeSinceLastNavigation());

  // Test a same-document navigation.
  url = GURL("http://www.example.org/bobcat#fun");
  mock_graph.page->OnMainFrameNavigationCommitted(true, base::TimeTicks::Now(),
                                                  30u, url, kHtmlMimeType,
                                                  kAskNotificationPermission);
  EXPECT_EQ(url, mock_graph.page->GetMainFrameUrl());
  EXPECT_EQ(30u, mock_graph.page->GetNavigationID());
  EXPECT_EQ(kHtmlMimeType, mock_graph.page->GetContentsMimeType());
  AdvanceClock(base::Seconds(17));
  EXPECT_EQ(base::Seconds(17), mock_graph.page->GetTimeSinceLastNavigation());

  // Test a navigation to a page with a different MIME type.
  url = GURL("http://www.example.org/document.pdf");
  mock_graph.page->OnMainFrameNavigationCommitted(false, base::TimeTicks::Now(),
                                                  40u, url, kPdfMimeType,
                                                  kAskNotificationPermission);
  EXPECT_EQ(url, mock_graph.page->GetMainFrameUrl());
  EXPECT_EQ(40u, mock_graph.page->GetNavigationID());
  EXPECT_EQ(kPdfMimeType, mock_graph.page->GetContentsMimeType());
  AdvanceClock(base::Seconds(17));
  EXPECT_EQ(base::Seconds(17), mock_graph.page->GetTimeSinceLastNavigation());
}

TEST_F(PageNodeImplTest, BrowserContextID) {
  const auto kTestBrowserContextId = base::UnguessableToken::Create();
  auto page_node = CreateNode<PageNodeImpl>(nullptr, kTestBrowserContextId);

  EXPECT_EQ(page_node->GetBrowserContextID(), kTestBrowserContextId);
}

TEST_F(PageNodeImplTest, LoadingState) {
  MockSinglePageInSingleProcessGraph mock_graph(graph());
  auto* page_node = mock_graph.page.get();

  // This should start at kLoadingNotStarted.
  EXPECT_EQ(PageNode::LoadingState::kLoadingNotStarted,
            page_node->GetLoadingState());

  // Set to kLoadingNotStarted and the property should stay kLoadingNotStarted.
  page_node->SetLoadingState(PageNode::LoadingState::kLoadingNotStarted);
  EXPECT_EQ(PageNode::LoadingState::kLoadingNotStarted,
            page_node->GetLoadingState());

  // Set to kLoading and the property should switch to kLoading.
  page_node->SetLoadingState(PageNode::LoadingState::kLoading);
  EXPECT_EQ(PageNode::LoadingState::kLoading, page_node->GetLoadingState());

  // Set to kLoading again and the property should stay kLoading.
  page_node->SetLoadingState(PageNode::LoadingState::kLoading);
  EXPECT_EQ(PageNode::LoadingState::kLoading, page_node->GetLoadingState());
}

TEST_F(PageNodeImplTest, HadFormInteractions) {
  MockSinglePageInSingleProcessGraph mock_graph(graph());
  auto* page_node = mock_graph.page.get();

  // This should be initialized to false.
  EXPECT_FALSE(page_node->HadFormInteraction());

  page_node->SetHadFormInteractionForTesting(true);
  EXPECT_TRUE(page_node->HadFormInteraction());

  page_node->SetHadFormInteractionForTesting(false);
  EXPECT_FALSE(page_node->HadFormInteraction());
}

TEST_F(PageNodeImplTest, HadUserEdits) {
  MockSinglePageInSingleProcessGraph mock_graph(graph());
  auto* page_node = mock_graph.page.get();

  // This should be initialized to false.
  EXPECT_FALSE(page_node->HadUserEdits());

  page_node->SetHadUserEditsForTesting(true);
  EXPECT_TRUE(page_node->HadUserEdits());

  page_node->SetHadUserEditsForTesting(false);
  EXPECT_FALSE(page_node->HadUserEdits());
}

namespace {

class MockObserver : public MockPageNodeObserver {
 public:
  explicit MockObserver(Graph* graph = nullptr) {
    // If a `graph` is passed, automatically start observing it.
    if (graph) {
      scoped_observation_.Observe(graph);
    }
  }

  void SetNotifiedPageNode(const PageNode* page_node) {
    notified_page_node_ = page_node;
  }

  void TestNotifiedPageNode(const PageNode* page_node) const {
    EXPECT_EQ(notified_page_node_, page_node);
  }

  void ExpectNoEdges(const PageNode* page_node) {
    // Node should be created without edges.
    EXPECT_FALSE(page_node->GetOpenerFrameNode());
    EXPECT_FALSE(page_node->GetEmbedderFrameNode());
    EXPECT_FALSE(page_node->GetPrimaryMainFrameNode());
    EXPECT_TRUE(page_node->GetMainFrameNodes().empty());
  }

  const PageNode* TakeNotifiedPageNode() {
    const PageNode* node = notified_page_node_;
    notified_page_node_ = nullptr;
    return node;
  }

 private:
  base::ScopedObservation<Graph, PageNodeObserver> scoped_observation_{this};
  raw_ptr<const PageNode, DanglingUntriaged> notified_page_node_ = nullptr;
};

using ::testing::_;
using ::testing::DoAll;
using ::testing::InSequence;
using ::testing::Invoke;
using ::testing::InvokeWithoutArgs;

}  // namespace

TEST_F(PageNodeImplTest, ObserverWorks) {
  auto process = CreateNode<ProcessNodeImpl>();

  MockObserver head_obs;
  MockObserver obs;
  MockObserver tail_obs;
  graph()->AddPageNodeObserver(&head_obs);
  graph()->AddPageNodeObserver(&obs);
  graph()->AddPageNodeObserver(&tail_obs);

  // Remove observers at the head and tail of the list inside a callback, and
  // expect that `obs` is still notified correctly.
  EXPECT_CALL(head_obs, OnBeforePageNodeAdded(_))
      .WillOnce(InvokeWithoutArgs([&] {
        graph()->RemovePageNodeObserver(&head_obs);
        graph()->RemovePageNodeObserver(&tail_obs);
      }));
  // `tail_obs` should not be notified as it was removed.
  EXPECT_CALL(tail_obs, OnBeforePageNodeAdded(_)).Times(0);

  // Create a page node and expect a matching call to both "OnBeforePageNodeAdded" and
  // "OnPageNodeAdded".
  {
    InSequence seq;
    EXPECT_CALL(obs, OnBeforePageNodeAdded(_))
        .WillOnce(DoAll(Invoke(&obs, &MockObserver::SetNotifiedPageNode),
                        Invoke(&obs, &MockObserver::ExpectNoEdges)));
    EXPECT_CALL(obs, OnPageNodeAdded(_))
        .WillOnce(Invoke(&obs, &MockObserver::TestNotifiedPageNode));
  }
  auto page_node = CreateNode<PageNodeImpl>();
  const PageNode* raw_page_node = page_node.get();
  EXPECT_EQ(raw_page_node, obs.TakeNotifiedPageNode());

  EXPECT_CALL(obs, OnIsFocusedChanged(_))
      .WillOnce(Invoke(&obs, &MockObserver::SetNotifiedPageNode));
  page_node->SetIsFocused(true);
  EXPECT_EQ(raw_page_node, obs.TakeNotifiedPageNode());

  EXPECT_CALL(obs, OnIsVisibleChanged(_))
      .WillOnce(Invoke(&obs, &MockObserver::SetNotifiedPageNode));
  page_node->SetIsVisible(true);
  EXPECT_EQ(raw_page_node, obs.TakeNotifiedPageNode());

  EXPECT_CALL(obs, OnIsAudibleChanged(_))
      .WillOnce(Invoke(&obs, &MockObserver::SetNotifiedPageNode));
  page_node->SetIsAudible(true);
  EXPECT_EQ(raw_page_node, obs.TakeNotifiedPageNode());

  EXPECT_CALL(obs, OnHasPictureInPictureChanged(_))
      .WillOnce(Invoke(&obs, &MockObserver::SetNotifiedPageNode));
  page_node->SetHasPictureInPicture(true);
  EXPECT_EQ(raw_page_node, obs.TakeNotifiedPageNode());

  EXPECT_CALL(obs, OnPageHasFreezingOriginTrialOptOutChanged(_))
      .WillOnce(Invoke(&obs, &MockObserver::SetNotifiedPageNode));
  page_node->SetHasFreezingOriginTrialOptOutForTesting(true);
  EXPECT_EQ(raw_page_node, obs.TakeNotifiedPageNode());

  EXPECT_CALL(obs, OnLoadingStateChanged(_, _))
      .WillOnce(testing::WithArg<0>(
          Invoke(&obs, &MockObserver::SetNotifiedPageNode)));
  page_node->SetLoadingState(PageNode::LoadingState::kLoading);
  EXPECT_EQ(raw_page_node, obs.TakeNotifiedPageNode());

  EXPECT_CALL(obs, OnUkmSourceIdChanged(_))
      .WillOnce(Invoke(&obs, &MockObserver::SetNotifiedPageNode));
  page_node->SetUkmSourceId(static_cast<ukm::SourceId>(0x1234));
  EXPECT_EQ(raw_page_node, obs.TakeNotifiedPageNode());

  EXPECT_CALL(obs, OnPageLifecycleStateChanged(_))
      .WillOnce(Invoke(&obs, &MockObserver::SetNotifiedPageNode));
  page_node->SetLifecycleStateForTesting(PageNodeImpl::LifecycleState::kFrozen);
  EXPECT_EQ(raw_page_node, obs.TakeNotifiedPageNode());

  EXPECT_CALL(obs, OnPageNotificationPermissionStatusChange(
                       _, std::optional<blink::mojom::PermissionStatus>()))
      .WillOnce(testing::WithArg<0>(
          Invoke(&obs, &MockObserver::SetNotifiedPageNode)));
  page_node->OnNotificationPermissionStatusChange(
      blink::mojom::PermissionStatus::GRANTED);
  EXPECT_EQ(raw_page_node, obs.TakeNotifiedPageNode());

  const GURL kTestUrl = GURL("https://foo.com/");
  int64_t navigation_id = 0x1234;
  EXPECT_CALL(obs, OnMainFrameUrlChanged(_, _))
      .WillOnce(testing::WithArg<0>(
          Invoke(&obs, &MockObserver::SetNotifiedPageNode)));
  EXPECT_CALL(
      obs, OnPageNotificationPermissionStatusChange(
               _, std::make_optional(blink::mojom::PermissionStatus::GRANTED)));
  // Expect no OnMainFrameDocumentChanged for same-document navigation
  page_node->OnMainFrameNavigationCommitted(
      true, base::TimeTicks::Now(), ++navigation_id, kTestUrl, kHtmlMimeType,
      kAskNotificationPermission);
  EXPECT_EQ(raw_page_node, obs.TakeNotifiedPageNode());

  EXPECT_CALL(obs, OnMainFrameDocumentChanged(_))
      .WillOnce(Invoke(&obs, &MockObserver::SetNotifiedPageNode));
  page_node->OnMainFrameNavigationCommitted(
      false, base::TimeTicks::Now(), ++navigation_id, kTestUrl, kHtmlMimeType,
      kAskNotificationPermission);
  EXPECT_EQ(raw_page_node, obs.TakeNotifiedPageNode());

  EXPECT_CALL(obs, OnTitleUpdated(_))
      .WillOnce(Invoke(&obs, &MockObserver::SetNotifiedPageNode));
  page_node->OnTitleUpdated();
  EXPECT_EQ(raw_page_node, obs.TakeNotifiedPageNode());

  EXPECT_CALL(obs, OnFaviconUpdated(_, _))
      .WillOnce(
          [&obs](const PageNode* node, blink::mojom::FaviconUpdateReason) {
            obs.SetNotifiedPageNode(node);
          });
  page_node->OnFaviconUpdated(
      blink::mojom::FaviconUpdateReason::kLinkElementChange);
  EXPECT_EQ(raw_page_node, obs.TakeNotifiedPageNode());

  // Re-entrant iteration should work.
  EXPECT_CALL(obs, OnIsFocusedChanged(raw_page_node))
      .WillOnce(InvokeWithoutArgs([&] { page_node->SetIsVisible(false); }));
  EXPECT_CALL(obs, OnIsVisibleChanged(raw_page_node));
  page_node->SetIsFocused(false);

  // Release the page node and expect a call to both "OnBeforePageNodeRemoved" and
  // "OnPageNodeRemoved".
  {
    InSequence seq;
    EXPECT_CALL(obs, OnBeforePageNodeRemoved(raw_page_node));
    EXPECT_CALL(obs, OnPageNodeRemoved(raw_page_node));
  }
  page_node.reset();

  graph()->RemovePageNodeObserver(&obs);
}

TEST_F(PageNodeImplTest, SetMainFrameRestoredState) {
  const GURL kUrl("http://www.example.org");
  auto page = CreateNode<PageNodeImpl>();
  const PageNode* raw_page_node = page.get();
  EXPECT_EQ(page->GetNotificationPermissionStatus(), std::nullopt);

  MockObserver obs(graph());

  EXPECT_CALL(obs, OnMainFrameUrlChanged(_, _))
      .WillOnce(testing::WithArg<0>(
          Invoke(&obs, &MockObserver::SetNotifiedPageNode)));
  EXPECT_CALL(obs, OnPageNotificationPermissionStatusChange(
                       _, std::optional<blink::mojom::PermissionStatus>()));
  page->SetMainFrameRestoredState(kUrl,
                                  /* notification_permission_status=*/blink::
                                      mojom::PermissionStatus::GRANTED);
  EXPECT_EQ(raw_page_node, obs.TakeNotifiedPageNode());

  EXPECT_EQ(page->GetMainFrameUrl(), kUrl);
  EXPECT_EQ(page->GetNotificationPermissionStatus(),
            blink::mojom::PermissionStatus::GRANTED);
}

TEST_F(PageNodeImplTest, PublicInterface) {
  auto process_node = CreateNode<ProcessNodeImpl>();
  auto page_node = CreateNode<PageNodeImpl>();
  const PageNode* public_page_node = page_node.get();
  auto frame_node = CreateFrameNodeAutoId(process_node.get(), page_node.get());

  // Simply test that the public interface impls yield the same result as their
  // private counterpart.

  EXPECT_EQ(page_node->primary_main_frame_node(),
            public_page_node->GetPrimaryMainFrameNode());
}

TEST_F(PageNodeImplTest, OpenerFrameNode) {
  auto process_node = CreateNode<ProcessNodeImpl>();
  auto page_node = CreateNode<PageNodeImpl>();
  auto opener_frame_node =
      CreateFrameNodeAutoId(process_node.get(), page_node.get());

  auto opened_page_node = CreateNode<PageNodeImpl>();
  const PageNode* public_opened_page_node = opened_page_node.get();

  opened_page_node->SetOpenerFrameNode(opener_frame_node.get());

  EXPECT_EQ(opened_page_node->opener_frame_node(), opener_frame_node.get());
  EXPECT_EQ(public_opened_page_node->GetOpenerFrameNode(),
            opener_frame_node.get());
}

TEST_F(PageNodeImplTest, EmbedderFrameNode) {
  auto process_node = CreateNode<ProcessNodeImpl>();
  auto page_node = CreateNode<PageNodeImpl>();
  auto embedder_frame_node =
      CreateFrameNodeAutoId(process_node.get(), page_node.get());

  auto embedded_page_node = CreateNode<PageNodeImpl>();
  const PageNode* public_embedded_page_node = embedded_page_node.get();

  embedded_page_node->SetEmbedderFrameNode(embedder_frame_node.get());

  EXPECT_EQ(embedded_page_node->embedder_frame_node(),
            embedder_frame_node.get());
  EXPECT_EQ(public_embedded_page_node->GetEmbedderFrameNode(),
            embedder_frame_node.get());
}

TEST_F(PageNodeImplTest, GetMainFrameNodes) {
  auto process = CreateNode<ProcessNodeImpl>();
  auto page = CreateNode<PageNodeImpl>();
  auto frame1 = CreateFrameNodeAutoId(process.get(), page.get());
  auto frame2 = CreateFrameNodeAutoId(process.get(), page.get());

  auto frames = ToPublic(page.get())->GetMainFrameNodes();
  EXPECT_THAT(frames, testing::UnorderedElementsAre(ToPublic(frame1.get()),
                                                    ToPublic(frame2.get())));
}

TEST_F(PageNodeImplTest, IsHoldingBlockingIndexedDBLock) {
  auto process = CreateNode<ProcessNodeImpl>();
  auto page_node = CreateNode<PageNodeImpl>();
  CreateFrameNodeAutoId(process.get(), page_node.get());
  const PageNode* raw_page_node = page_node.get();

  MockObserver obs(graph());

  EXPECT_CALL(obs, OnPageIsHoldingBlockingIndexedDBLockChanged(raw_page_node));
  page_node->SetIsHoldingBlockingIndexedDBLockForTesting(true);
  EXPECT_TRUE(page_node->IsHoldingBlockingIndexedDBLock());

  EXPECT_CALL(obs, OnPageIsHoldingBlockingIndexedDBLockChanged(raw_page_node));
  page_node->SetIsHoldingBlockingIndexedDBLockForTesting(false);
  EXPECT_FALSE(page_node->IsHoldingBlockingIndexedDBLock());
}

}  // namespace performance_manager
