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

#include "base/scoped_observation.h"
#include "base/task/task_traits.h"
#include "components/performance_manager/graph/frame_node_impl.h"
#include "components/performance_manager/graph/page_node_impl.h"
#include "components/performance_manager/graph/process_node_impl.h"
#include "components/performance_manager/public/execution_context_priority/execution_context_priority.h"
#include "components/performance_manager/test_support/graph/mock_worker_node_observer.h"
#include "components/performance_manager/test_support/graph_test_harness.h"
#include "testing/gmock/include/gmock/gmock.h"
#include "testing/gtest/include/gtest/gtest.h"
#include "url/gurl.h"
#include "url/origin.h"

namespace performance_manager {

namespace {

class WorkerNodeImplTest : public GraphTestHarness {
 public:
 protected:
};

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

 private:
  base::ScopedObservation<Graph, WorkerNodeObserver> scoped_observation_{this};
};

using ::testing::_;
using ::testing::ElementsAre;
using ::testing::InSequence;
using ::testing::InvokeWithoutArgs;

}  // namespace

TEST_F(WorkerNodeImplTest, SafeDowncast) {
  auto process = CreateNode<ProcessNodeImpl>();
  auto worker = CreateNode<WorkerNodeImpl>(WorkerNode::WorkerType::kDedicated,
                                           process.get());
  WorkerNode* node = worker.get();
  EXPECT_EQ(worker.get(), WorkerNodeImpl::FromNode(node));
  NodeBase* base = worker.get();
  EXPECT_EQ(base, NodeBase::FromNode(node));
  EXPECT_EQ(static_cast<Node*>(node), base->ToNode());
}

using WorkerNodeImplDeathTest = WorkerNodeImplTest;

TEST_F(WorkerNodeImplDeathTest, SafeDowncast) {
  auto process = CreateNode<ProcessNodeImpl>();
  auto worker = CreateNode<WorkerNodeImpl>(WorkerNode::WorkerType::kDedicated,
                                           process.get());
  ASSERT_DEATH_IF_SUPPORTED(FrameNodeImpl::FromNodeBase(worker.get()), "");
}

TEST_F(WorkerNodeImplTest, ConstProperties) {
  const WorkerNode::WorkerType kWorkerType = WorkerNode::WorkerType::kShared;
  const auto kTestBrowserContextId = base::UnguessableToken::Create();
  auto process = CreateNode<ProcessNodeImpl>();
  static const blink::WorkerToken kTestWorkerToken;
  static const auto kTestWorkerOrigin =
      url::Origin::Create(GURL("https://example.com"));

  auto worker_impl = CreateNode<WorkerNodeImpl>(
      kWorkerType, process.get(), kTestBrowserContextId, kTestWorkerToken,
      kTestWorkerOrigin);

  // Test private interface.
  EXPECT_EQ(worker_impl->process_node(), process.get());

  // Test public interface.
  const WorkerNode* worker = worker_impl.get();

  EXPECT_EQ(worker->GetBrowserContextID(), kTestBrowserContextId);
  EXPECT_EQ(worker->GetWorkerType(), kWorkerType);
  EXPECT_EQ(worker->GetProcessNode(), process.get());
  EXPECT_EQ(worker->GetWorkerToken(), kTestWorkerToken);
  EXPECT_EQ(worker->GetOrigin(), kTestWorkerOrigin);
}

TEST_F(WorkerNodeImplTest, OnFinalResponseURLDetermined) {
  auto process = CreateNode<ProcessNodeImpl>();
  static const GURL kTestUrl("testurl.com");

  auto worker_impl = CreateNode<WorkerNodeImpl>(WorkerNode::WorkerType::kShared,
                                                process.get());

  // Initially empty.
  EXPECT_TRUE(worker_impl->GetURL().is_empty());

  // Set when OnFinalResponseURLDetermined() is called.
  worker_impl->OnFinalResponseURLDetermined(kTestUrl);
  EXPECT_EQ(worker_impl->GetURL(), kTestUrl);
}

// Create a worker of each type and register the frame as a client of each.
TEST_F(WorkerNodeImplTest, AddWorkerNodes) {
  auto process = CreateNode<ProcessNodeImpl>();
  auto page = CreateNode<PageNodeImpl>();
  auto frame = CreateFrameNodeAutoId(process.get(), page.get());
  auto dedicated_worker = CreateNode<WorkerNodeImpl>(
      WorkerNode::WorkerType::kDedicated, process.get());
  auto shared_worker = CreateNode<WorkerNodeImpl>(
      WorkerNode::WorkerType::kShared, process.get());
  auto service_worker = CreateNode<WorkerNodeImpl>(
      WorkerNode::WorkerType::kService, process.get());

  // Each workers have no clients.
  EXPECT_TRUE(dedicated_worker->client_frames().empty());
  EXPECT_TRUE(shared_worker->client_frames().empty());
  EXPECT_TRUE(service_worker->client_frames().empty());

  // The client frame doesn't have any child worker yet.
  EXPECT_TRUE(frame->child_worker_nodes().empty());

  dedicated_worker->AddClientFrame(frame.get());
  shared_worker->AddClientFrame(frame.get());
  service_worker->AddClientFrame(frame.get());

  // Each workers have one client frame.
  EXPECT_EQ(dedicated_worker->client_frames().size(), 1u);
  EXPECT_EQ(shared_worker->client_frames().size(), 1u);
  EXPECT_EQ(service_worker->client_frames().size(), 1u);

  // The client frame knows about the 3 workers.
  EXPECT_EQ(frame->child_worker_nodes().size(), 3u);

  // Remove client connections.
  service_worker->RemoveClientFrame(frame.get());
  shared_worker->RemoveClientFrame(frame.get());
  dedicated_worker->RemoveClientFrame(frame.get());
}

// Create a frame and a worker of each type that are all clients of the service
// worker.
TEST_F(WorkerNodeImplTest, ClientsOfServiceWorkers) {
  auto process = CreateNode<ProcessNodeImpl>();
  auto page = CreateNode<PageNodeImpl>();
  auto frame = CreateFrameNodeAutoId(process.get(), page.get());
  auto dedicated_worker = CreateNode<WorkerNodeImpl>(
      WorkerNode::WorkerType::kDedicated, process.get());
  auto shared_worker = CreateNode<WorkerNodeImpl>(
      WorkerNode::WorkerType::kShared, process.get());
  auto service_worker = CreateNode<WorkerNodeImpl>(
      WorkerNode::WorkerType::kService, process.get());

  // The service worker has no clients.
  EXPECT_TRUE(service_worker->client_frames().empty());
  EXPECT_TRUE(service_worker->client_workers().empty());

  // The frame and the other workers aren't connected to the service worker yet.
  EXPECT_TRUE(frame->child_worker_nodes().empty());
  EXPECT_TRUE(dedicated_worker->child_workers().empty());
  EXPECT_TRUE(shared_worker->child_workers().empty());

  service_worker->AddClientFrame(frame.get());
  service_worker->AddClientWorker(dedicated_worker.get());
  service_worker->AddClientWorker(shared_worker.get());

  EXPECT_EQ(service_worker->client_frames().size(), 1u);
  EXPECT_EQ(service_worker->client_workers().size(), 2u);

  EXPECT_EQ(frame->child_worker_nodes().size(), 1u);
  EXPECT_EQ(shared_worker->child_workers().size(), 1u);
  EXPECT_EQ(dedicated_worker->child_workers().size(), 1u);

  // Remove client connections.
  service_worker->RemoveClientWorker(shared_worker.get());
  service_worker->RemoveClientWorker(dedicated_worker.get());
  service_worker->RemoveClientFrame(frame.get());
}

// Create a hierarchy of nested dedicated workers where the parent one has 2
// children and one grandchildren.
TEST_F(WorkerNodeImplTest, NestedDedicatedWorkers) {
  auto process = CreateNode<ProcessNodeImpl>();
  auto page = CreateNode<PageNodeImpl>();
  auto frame = CreateFrameNodeAutoId(process.get(), page.get());
  auto parent_worker = CreateNode<WorkerNodeImpl>(
      WorkerNode::WorkerType::kDedicated, process.get());
  auto child_worker_1 = CreateNode<WorkerNodeImpl>(
      WorkerNode::WorkerType::kDedicated, process.get());
  auto child_worker_2 = CreateNode<WorkerNodeImpl>(
      WorkerNode::WorkerType::kDedicated, process.get());
  auto grandchild_worker = CreateNode<WorkerNodeImpl>(
      WorkerNode::WorkerType::kDedicated, process.get());

  parent_worker->AddClientFrame(frame.get());
  child_worker_1->AddClientWorker(parent_worker.get());
  child_worker_2->AddClientWorker(parent_worker.get());
  grandchild_worker->AddClientWorker(child_worker_1.get());

  EXPECT_EQ(parent_worker->client_frames().size(), 1u);
  EXPECT_EQ(parent_worker->client_workers().size(), 0u);

  EXPECT_EQ(parent_worker->child_workers().size(), 2u);

  grandchild_worker->RemoveClientWorker(child_worker_1.get());
  child_worker_2->RemoveClientWorker(parent_worker.get());
  child_worker_1->RemoveClientWorker(parent_worker.get());
  parent_worker->RemoveClientFrame(frame.get());
}

TEST_F(WorkerNodeImplTest, PriorityAndReason) {
  auto process = CreateNode<ProcessNodeImpl>();
  constexpr PriorityAndReason kTestPriorityAndReason(
      base::Process::Priority::kMaxValue, "Test reason");

  auto worker_impl = CreateNode<WorkerNodeImpl>(WorkerNode::WorkerType::kShared,
                                                process.get());

  // Initially the default priority.
  EXPECT_EQ(worker_impl->GetPriorityAndReason(),
            PriorityAndReason(base::Process::Priority::kMinValue,
                              WorkerNodeImpl::kDefaultPriorityReason));

  worker_impl->SetPriorityAndReason(kTestPriorityAndReason);

  EXPECT_EQ(worker_impl->GetPriorityAndReason(), kTestPriorityAndReason);
}

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

  MockObserver head_obs;
  MockObserver obs;
  MockObserver tail_obs;
  graph()->AddWorkerNodeObserver(&head_obs);
  graph()->AddWorkerNodeObserver(&obs);
  graph()->AddWorkerNodeObserver(&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, OnBeforeWorkerNodeAdded(_, _))
      .WillOnce(InvokeWithoutArgs([&] {
        graph()->RemoveWorkerNodeObserver(&head_obs);
        graph()->RemoveWorkerNodeObserver(&tail_obs);
      }));
  // `tail_obs` should not be notified as it was removed.
  EXPECT_CALL(tail_obs, OnBeforeWorkerNodeAdded(_, _)).Times(0);

  // Create a worker node and expect a matching call to both "OnBeforeWorkerNodeAdded" and
  // "OnWorkerNodeAdded".
  const WorkerNode* worker_node = nullptr;
  const ProcessNode* process_node = nullptr;
  EXPECT_CALL(obs, OnBeforeWorkerNodeAdded(_, _))
      .WillOnce(
          [&](const WorkerNode* node, const ProcessNode* pending_process_node) {
            worker_node = node;
            process_node = pending_process_node;

            // Node should be created without edges.
            EXPECT_FALSE(node->GetProcessNode());
            EXPECT_TRUE(node->GetClientFrames().empty());
            EXPECT_TRUE(node->GetClientWorkers().empty());
            EXPECT_TRUE(node->GetChildWorkers().empty());
          });
  EXPECT_CALL(obs, OnWorkerNodeAdded(_)).WillOnce([&](const WorkerNode* node) {
    EXPECT_EQ(worker_node, node);
    EXPECT_EQ(process_node, node->GetProcessNode());
  });
  auto dedicated_worker = CreateNode<WorkerNodeImpl>(
      WorkerNode::WorkerType::kDedicated, process.get());
  EXPECT_EQ(worker_node, dedicated_worker.get());

  // Re-entrant iteration should work.
  EXPECT_CALL(obs, OnFinalResponseURLDetermined(worker_node))
      .WillOnce(InvokeWithoutArgs([&] {
        dedicated_worker->SetPriorityAndReason(PriorityAndReason(
            base::Process::Priority::kUserBlocking, "test priority"));
      }));
  EXPECT_CALL(obs, OnPriorityAndReasonChanged(worker_node, _));
  dedicated_worker->OnFinalResponseURLDetermined(GURL("https://example.com"));

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

// Same as the AddWorkerNodes test, but the graph is verified through the
// WorkerNodeObserver interface.
TEST_F(WorkerNodeImplTest, Observer_AddWorkerNodes) {
  InSequence s;

  MockObserver obs(graph());

  auto process = CreateNode<ProcessNodeImpl>();
  auto page = CreateNode<PageNodeImpl>();
  auto frame = CreateFrameNodeAutoId(process.get(), page.get());

  // Create workers.
  EXPECT_CALL(obs, OnBeforeWorkerNodeAdded(_, _));
  EXPECT_CALL(obs, OnWorkerNodeAdded(_));
  EXPECT_CALL(obs, OnBeforeWorkerNodeAdded(_, _));
  EXPECT_CALL(obs, OnWorkerNodeAdded(_));
  EXPECT_CALL(obs, OnBeforeWorkerNodeAdded(_, _));
  EXPECT_CALL(obs, OnWorkerNodeAdded(_));

  auto dedicated_worker = CreateNode<WorkerNodeImpl>(
      WorkerNode::WorkerType::kDedicated, process.get());
  auto shared_worker = CreateNode<WorkerNodeImpl>(
      WorkerNode::WorkerType::kShared, process.get());
  auto service_worker = CreateNode<WorkerNodeImpl>(
      WorkerNode::WorkerType::kService, process.get());

  // Add client connections.
  EXPECT_CALL(obs,
              OnBeforeClientFrameAdded(dedicated_worker.get(), frame.get()));
  EXPECT_CALL(obs, OnClientFrameAdded(dedicated_worker.get(), frame.get()));
  EXPECT_CALL(obs, OnBeforeClientFrameAdded(shared_worker.get(), frame.get()));
  EXPECT_CALL(obs, OnClientFrameAdded(shared_worker.get(), frame.get()));
  EXPECT_CALL(obs, OnBeforeClientFrameAdded(service_worker.get(), frame.get()));
  EXPECT_CALL(obs, OnClientFrameAdded(service_worker.get(), frame.get()));

  dedicated_worker->AddClientFrame(frame.get());
  shared_worker->AddClientFrame(frame.get());
  service_worker->AddClientFrame(frame.get());

  // Remove client connections.
  EXPECT_CALL(obs,
              OnBeforeClientFrameRemoved(service_worker.get(), frame.get()));
  EXPECT_CALL(obs,
              OnBeforeClientFrameRemoved(shared_worker.get(), frame.get()));
  EXPECT_CALL(obs,
              OnBeforeClientFrameRemoved(dedicated_worker.get(), frame.get()));

  service_worker->RemoveClientFrame(frame.get());
  shared_worker->RemoveClientFrame(frame.get());
  dedicated_worker->RemoveClientFrame(frame.get());

  const ProcessNode* saved_service_worker_process = nullptr;
  const ProcessNode* saved_shared_worker_process = nullptr;
  const ProcessNode* saved_dedicated_worker_process = nullptr;
  EXPECT_CALL(obs, OnBeforeWorkerNodeRemoved(service_worker.get()))
      .WillOnce([&](const WorkerNode* worker_node) {
        // Node should still be in graph.
        saved_service_worker_process = worker_node->GetProcessNode();
        EXPECT_TRUE(saved_service_worker_process);
      });
  EXPECT_CALL(obs, OnWorkerNodeRemoved(service_worker.get(), _))
      .WillOnce([&](const WorkerNode* worker_node,
                    const ProcessNode* previous_process_node) {
        EXPECT_EQ(saved_service_worker_process, previous_process_node);
        EXPECT_FALSE(worker_node->GetProcessNode());
      });
  EXPECT_CALL(obs, OnBeforeWorkerNodeRemoved(shared_worker.get()))
      .WillOnce([&](const WorkerNode* worker_node) {
        // Node should still be in graph.
        saved_shared_worker_process = worker_node->GetProcessNode();
        EXPECT_TRUE(saved_shared_worker_process);
      });
  EXPECT_CALL(obs, OnWorkerNodeRemoved(shared_worker.get(), _))
      .WillOnce([&](const WorkerNode* worker_node,
                    const ProcessNode* previous_process_node) {
        EXPECT_EQ(saved_shared_worker_process, previous_process_node);
        EXPECT_FALSE(worker_node->GetProcessNode());
      });
  EXPECT_CALL(obs, OnBeforeWorkerNodeRemoved(dedicated_worker.get()))
      .WillOnce([&](const WorkerNode* worker_node) {
        // Node should still be in graph.
        saved_dedicated_worker_process = worker_node->GetProcessNode();
        EXPECT_TRUE(saved_dedicated_worker_process);
      });
  EXPECT_CALL(obs, OnWorkerNodeRemoved(dedicated_worker.get(), _))
      .WillOnce([&](const WorkerNode* worker_node,
                    const ProcessNode* previous_process_node) {
        EXPECT_EQ(saved_dedicated_worker_process, previous_process_node);
        EXPECT_FALSE(worker_node->GetProcessNode());
      });

  // Clean up workers.
  service_worker.reset();
  shared_worker.reset();
  dedicated_worker.reset();
}

// Same as the ClientsOfServiceWorkers test, but the graph is verified through
// the WorkerNodeObserver interface.
TEST_F(WorkerNodeImplTest, Observer_ClientsOfServiceWorkers) {
  InSequence s;

  MockObserver obs(graph());

  auto process = CreateNode<ProcessNodeImpl>();
  auto page = CreateNode<PageNodeImpl>();
  auto frame = CreateFrameNodeAutoId(process.get(), page.get());

  // Create workers.
  EXPECT_CALL(obs, OnBeforeWorkerNodeAdded(_, _));
  EXPECT_CALL(obs, OnWorkerNodeAdded(_));
  EXPECT_CALL(obs, OnBeforeWorkerNodeAdded(_, _));
  EXPECT_CALL(obs, OnWorkerNodeAdded(_));
  EXPECT_CALL(obs, OnBeforeWorkerNodeAdded(_, _));
  EXPECT_CALL(obs, OnWorkerNodeAdded(_));

  auto dedicated_worker = CreateNode<WorkerNodeImpl>(
      WorkerNode::WorkerType::kDedicated, process.get());
  auto shared_worker = CreateNode<WorkerNodeImpl>(
      WorkerNode::WorkerType::kShared, process.get());
  auto service_worker = CreateNode<WorkerNodeImpl>(
      WorkerNode::WorkerType::kService, process.get());

  // Add client connections.
  EXPECT_CALL(obs, OnBeforeClientFrameAdded(service_worker.get(), frame.get()));
  EXPECT_CALL(obs, OnClientFrameAdded(service_worker.get(), frame.get()));
  EXPECT_CALL(obs, OnBeforeClientWorkerAdded(service_worker.get(),
                                             dedicated_worker.get()));
  EXPECT_CALL(
      obs, OnClientWorkerAdded(service_worker.get(), dedicated_worker.get()));
  EXPECT_CALL(obs, OnBeforeClientWorkerAdded(service_worker.get(),
                                             shared_worker.get()));
  EXPECT_CALL(obs,
              OnClientWorkerAdded(service_worker.get(), shared_worker.get()));

  service_worker->AddClientFrame(frame.get());
  service_worker->AddClientWorker(dedicated_worker.get());
  service_worker->AddClientWorker(shared_worker.get());

  // Remove client connections.
  EXPECT_CALL(obs, OnBeforeClientWorkerRemoved(service_worker.get(),
                                               shared_worker.get()));
  EXPECT_CALL(obs, OnBeforeClientWorkerRemoved(service_worker.get(),
                                               dedicated_worker.get()));
  EXPECT_CALL(obs,
              OnBeforeClientFrameRemoved(service_worker.get(), frame.get()));

  service_worker->RemoveClientWorker(shared_worker.get());
  service_worker->RemoveClientWorker(dedicated_worker.get());
  service_worker->RemoveClientFrame(frame.get());

  // Clean up workers.
  EXPECT_CALL(obs, OnBeforeWorkerNodeRemoved(service_worker.get()));
  EXPECT_CALL(obs, OnWorkerNodeRemoved(service_worker.get(), _));
  EXPECT_CALL(obs, OnBeforeWorkerNodeRemoved(shared_worker.get()));
  EXPECT_CALL(obs, OnWorkerNodeRemoved(shared_worker.get(), _));
  EXPECT_CALL(obs, OnBeforeWorkerNodeRemoved(dedicated_worker.get()));
  EXPECT_CALL(obs, OnWorkerNodeRemoved(dedicated_worker.get(), _));

  service_worker.reset();
  shared_worker.reset();
  dedicated_worker.reset();
}

TEST_F(WorkerNodeImplTest, Observer_OnFinalResponseURLDetermined) {
  InSequence s;

  MockObserver obs(graph());

  auto process = CreateNode<ProcessNodeImpl>();

  // Create the worker.
  EXPECT_CALL(obs, OnBeforeWorkerNodeAdded(_, _));
  EXPECT_CALL(obs, OnWorkerNodeAdded(_));
  auto worker = CreateNode<WorkerNodeImpl>(WorkerNode::WorkerType::kDedicated,
                                           process.get());

  // Set the final response URL.
  EXPECT_CALL(obs, OnFinalResponseURLDetermined(worker.get()));
  worker->OnFinalResponseURLDetermined(GURL("testurl.com"));

  // `worker` goes out of scope before observer.
  EXPECT_CALL(obs, OnBeforeWorkerNodeRemoved(worker.get()));
  EXPECT_CALL(obs, OnWorkerNodeRemoved(worker.get(), _));
}

TEST_F(WorkerNodeImplTest, Observer_OnPriorityAndReasonChanged) {
  InSequence s;

  MockObserver obs(graph());

  auto process = CreateNode<ProcessNodeImpl>();

  // Create the worker.
  EXPECT_CALL(obs, OnBeforeWorkerNodeAdded(_, _));
  EXPECT_CALL(obs, OnWorkerNodeAdded(_));
  auto worker = CreateNode<WorkerNodeImpl>(WorkerNode::WorkerType::kDedicated,
                                           process.get());

  static const PriorityAndReason kPriorityAndReason(
      base::Process::Priority::kMaxValue, "this is a reason!");
  EXPECT_CALL(obs, OnPriorityAndReasonChanged(worker.get(), _));
  worker->SetPriorityAndReason(kPriorityAndReason);

  // `worker` goes out of scope before observer.
  EXPECT_CALL(obs, OnBeforeWorkerNodeRemoved(worker.get()));
  EXPECT_CALL(obs, OnWorkerNodeRemoved(worker.get(), _));
}

}  // namespace performance_manager
