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

#include "chrome/browser/task_manager/sampling/task_manager_impl.h"

#include <string>
#include <utility>
#include <vector>

#include "base/memory/raw_ptr.h"
#include "base/strings/utf_string_conversions.h"
#include "chrome/browser/task_manager/common/task_manager_features.h"
#include "chrome/browser/task_manager/providers/task.h"
#include "chrome/browser/task_manager/task_manager_observer.h"
#include "content/public/test/browser_task_environment.h"
#include "testing/gtest/include/gtest/gtest.h"

namespace task_manager {

namespace {

// A Task for unittests, not backed by a real process, that can report any given
// value.
class FakeTask : public Task {
 public:
  FakeTask(base::ProcessId process_id,
           Type type,
           const std::string& title,
           SessionID tab_id)
      : Task(base::ASCIIToUTF16(title),
             nullptr,
             base::kNullProcessHandle,
             process_id),
        type_(type),
        parent_(nullptr),
        tab_id_(tab_id) {
    TaskManagerImpl::GetInstance()->TaskAdded(this);
  }

  FakeTask(const FakeTask&) = delete;
  FakeTask& operator=(const FakeTask&) = delete;

  ~FakeTask() override { TaskManagerImpl::GetInstance()->TaskRemoved(this); }

  Type GetType() const override { return type_; }

  int GetChildProcessUniqueID() const override { return 0; }

  base::WeakPtr<Task> GetParentTask() const override { return parent_; }

  SessionID GetTabId() const override { return tab_id_; }

  void SetParent(base::WeakPtr<Task> parent) { parent_ = parent; }

  base::WeakPtr<FakeTask> AsWeakPtr() { return weak_ptr_factory_.GetWeakPtr(); }

 private:
  Type type_;
  base::WeakPtr<Task> parent_;
  SessionID tab_id_;
  base::WeakPtrFactory<FakeTask> weak_ptr_factory_{this};
};

}  // namespace

class TaskManagerImplTest : public testing::Test, public TaskManagerObserver {
 public:
  TaskManagerImplTest()
      : TaskManagerObserver(base::Seconds(1), REFRESH_TYPE_NONE) {
    TaskManagerImpl::GetInstance()->AddObserver(this);
  }
  TaskManagerImplTest(const TaskManagerImplTest&) = delete;
  TaskManagerImplTest& operator=(const TaskManagerImplTest&) = delete;
  ~TaskManagerImplTest() override {
    tasks_.clear();
    observed_task_manager()->RemoveObserver(this);
  }

  base::WeakPtr<FakeTask> AddTask(int pid_offset,
                                  Task::Type type,
                                  const std::string& title,
                                  SessionID tab_id) {
    // Offset based on the current process id, to avoid collisions with the
    // browser process task.
    base::ProcessId process_id = base::GetCurrentProcId() + pid_offset;
    tasks_.emplace_back(new FakeTask(process_id, type, title, tab_id));
    return tasks_.back()->AsWeakPtr();
  }

  std::string DumpSortedTasks() {
    std::string result;
    for (TaskId task_id : observed_task_manager()->GetTaskIdsList()) {
      result += base::UTF16ToUTF8(observed_task_manager()->GetTitle(task_id));
      result += "\n";
    }
    return result;
  }

 private:
  content::BrowserTaskEnvironment task_environment_;
  std::vector<std::unique_ptr<FakeTask>> tasks_;
};

TEST_F(TaskManagerImplTest, SortingTypes) {
  constexpr SessionID kTabId1 = SessionID::FromSerializedValue(10);
  constexpr SessionID kTabId2 = SessionID::FromSerializedValue(20);

  AddTask(100, Task::GPU, "Gpu Process", /*tab_id=*/SessionID::InvalidValue());

  base::WeakPtr<Task> tab1 =
      AddTask(200, Task::RENDERER, "Tab One", kTabId1)->AsWeakPtr();
  AddTask(400, Task::EXTENSION, "Extension Subframe: Tab One", kTabId1)
      ->SetParent(tab1);
  AddTask(300, Task::RENDERER, "Subframe: Tab One", kTabId1)->SetParent(tab1);

  base::WeakPtr<Task> tab2 =
      AddTask(200, Task::RENDERER, "Tab Two: sharing process with Tab One",
              kTabId2)
          ->AsWeakPtr();

  AddTask(301, Task::RENDERER, "Subframe: Tab Two", kTabId2)->SetParent(tab2);
  AddTask(400, Task::EXTENSION, "Extension Subframe: Tab Two", kTabId2)
      ->SetParent(tab2);

  AddTask(600, Task::ARC, "ARC", /*tab_id=*/SessionID::InvalidValue());
  AddTask(650, Task::CROSTINI, "Crostini",
          /*tab_id=*/SessionID::InvalidValue());
  AddTask(800, Task::UTILITY, "Utility One",
          /*tab_id=*/SessionID::InvalidValue());
  AddTask(700, Task::UTILITY, "Utility Two",
          /*tab_id=*/SessionID::InvalidValue());
  AddTask(1000, Task::GUEST, "Guest", kTabId2);
  AddTask(900, Task::SERVICE_WORKER, "Service worker",
          /*tab_id=*/SessionID::InvalidValue());
  AddTask(900, Task::SHARED_WORKER, "Shared worker",
          /*tab_id=*/SessionID::InvalidValue());
  AddTask(900, Task::DEDICATED_WORKER, "Dedicated worker",
          /*tab_id=*/SessionID::InvalidValue());
  AddTask(500, Task::ZYGOTE, "Zygote", /*tab_id=*/SessionID::InvalidValue());

  AddTask(300, Task::RENDERER, "Subframe: Tab One (2)", kTabId1)
      ->SetParent(tab1);
  AddTask(300, Task::RENDERER, "Subframe: Tab One (third)", kTabId1)
      ->SetParent(tab1);
  AddTask(300, Task::RENDERER, "Subframe: Tab One (4)", kTabId1)
      ->SetParent(tab1);

  EXPECT_EQ(
      "Browser\n"
      "Gpu Process\n"
      "Crostini\n"
      "ARC\n"
      "Zygote\n"
      "Utility One\n"
      "Utility Two\n"
      "Tab One\n"
      "Tab Two: sharing process with Tab One\n"
      "Subframe: Tab One\n"
      "Subframe: Tab One (2)\n"
      "Subframe: Tab One (third)\n"
      "Subframe: Tab One (4)\n"
      "Extension Subframe: Tab One\n"
      "Extension Subframe: Tab Two\n"
      "Subframe: Tab Two\n"
      "Guest\n"
      "Dedicated worker\n"
      "Shared worker\n"
      "Service worker\n",
      DumpSortedTasks());
}

TEST_F(TaskManagerImplTest, SortingCycles) {
  constexpr SessionID kTabId1 = SessionID::FromSerializedValue(10);
  constexpr SessionID kTabId2 = SessionID::FromSerializedValue(20);
  constexpr SessionID kTabId3 = SessionID::FromSerializedValue(5);
  constexpr SessionID kTabId4 = SessionID::FromSerializedValue(30);

  // Two tabs, with subframes in the other's process. This induces a cycle in
  // the TaskGroup dependencies, without being a cycle in the Tasks. This can
  // happen in practice.
  base::WeakPtr<Task> tab1 =
      AddTask(200, Task::RENDERER, "Tab 1: Process 200", kTabId1)->AsWeakPtr();
  AddTask(300, Task::RENDERER, "Subframe in Tab 1: Process 300", kTabId1)
      ->SetParent(tab1);
  base::WeakPtr<Task> tab2 =
      AddTask(300, Task::RENDERER, "Tab 2: Process 300", kTabId2)->AsWeakPtr();
  AddTask(200, Task::RENDERER, "Subframe in Tab 2: Process 200", kTabId2)
      ->SetParent(tab2);

  // Simulated GPU process.
  AddTask(100, Task::GPU, "Gpu Process", /*tab_id=*/SessionID::InvalidValue());

  // Two subframes that list each other as a parent (a true cycle). This
  // shouldn't happen in practice, but we want the sorting code to handle it
  // gracefully.
  base::WeakPtr<FakeTask> cycle1 =
      AddTask(501, Task::SANDBOX_HELPER, "Cycle 1",
              /*tab_id=*/SessionID::InvalidValue());
  base::WeakPtr<FakeTask> cycle2 =
      AddTask(500, Task::ARC, "Cycle 2", /*tab_id=*/SessionID::InvalidValue());
  cycle1->SetParent(cycle2);
  cycle2->SetParent(cycle1);

  // A cycle where both elements are in the same group.
  base::WeakPtr<FakeTask> cycle3 =
      AddTask(600, Task::SANDBOX_HELPER, "Cycle 3",
              /*tab_id=*/SessionID::InvalidValue());
  base::WeakPtr<FakeTask> cycle4 =
      AddTask(600, Task::ARC, "Cycle 4", /*tab_id=*/SessionID::InvalidValue());
  cycle3->SetParent(cycle4);
  cycle4->SetParent(cycle3);

  // Tasks listing a cycle as their parent.
  base::WeakPtr<FakeTask> lollipop5 =
      AddTask(701, Task::EXTENSION, "Child of Cycle 3",
              /*tab_id=*/SessionID::InvalidValue());
  lollipop5->SetParent(cycle3);
  base::WeakPtr<FakeTask> lollipop6 =
      AddTask(700, Task::GUEST, "Child of Cycle 4",
              /*tab_id=*/SessionID::InvalidValue());
  lollipop6->SetParent(cycle4);

  // A task listing itself as parent.
  base::WeakPtr<FakeTask> self_cycle =
      AddTask(800, Task::RENDERER, "Self Cycle", kTabId3);
  self_cycle->SetParent(self_cycle);

  // Add a plugin child to tab1 and tab2.
  AddTask(900, Task::GUEST, "Guest: Tab 2", kTabId2)->SetParent(tab1);
  AddTask(901, Task::GUEST, "Guest: Tab 1", kTabId1)->SetParent(tab1);

  // Finish with a normal renderer task.
  AddTask(903, Task::RENDERER, "Tab: Normal Renderer", kTabId4);

  // Cycles should wind up on the bottom of the list.
  EXPECT_EQ(
      "Browser\n"
      "Gpu Process\n"
      "Tab 1: Process 200\n"
      "Subframe in Tab 2: Process 200\n"
      "Tab 2: Process 300\n"
      "Subframe in Tab 1: Process 300\n"
      "Guest: Tab 1\n"
      "Guest: Tab 2\n"
      "Tab: Normal Renderer\n"
      "Cycle 2\n"           // ARC
      "Cycle 1\n"           // Child of 2
      "Cycle 4\n"           // ARC; task_id > Cycle 2's
      "Cycle 3\n"           // Same-process child of 4 (SANDBOX_HELPER > ARC)
      "Child of Cycle 4\n"  // Child of 4
      "Child of Cycle 3\n"  // Child of 3
      "Self Cycle\n",       // RENDERER (> ARC)
      DumpSortedTasks());
}

}  // namespace task_manager
