// Copyright 2015 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/providers/child_process_task.h"

#include <stdint.h>

#include "base/memory/raw_ptr.h"
#include "base/run_loop.h"
#include "base/strings/utf_string_conversions.h"
#include "chrome/browser/task_manager/providers/child_process_task_provider.h"
#include "chrome/browser/task_manager/task_manager_observer.h"
#include "chrome/grit/generated_resources.h"
#include "content/public/browser/child_process_data.h"
#include "content/public/common/process_type.h"
#include "content/public/test/browser_task_environment.h"
#include "content/public/test/test_utils.h"
#include "testing/gtest/include/gtest/gtest.h"
#include "ui/base/l10n/l10n_util.h"

using content::ChildProcessData;

namespace task_manager {

namespace {

// Will be used to test the translation from |content::ProcessType| to
// |task_manager::Task::Type|.
struct ProcessTypeTaskTypePair {
  int process_type_;
  Task::Type expected_task_type_;
} constexpr kProcessTaskTypesPairs[] = {
    {content::PROCESS_TYPE_UTILITY, Task::UTILITY},
    {content::PROCESS_TYPE_ZYGOTE, Task::ZYGOTE},
    {content::PROCESS_TYPE_SANDBOX_HELPER, Task::SANDBOX_HELPER},
    {content::PROCESS_TYPE_GPU, Task::GPU},
};

}  // namespace

// Defines a test for the child process task provider and the child process
// tasks themselves.
class ChildProcessTaskTest
    : public testing::Test,
      public TaskProviderObserver {
 public:
  ChildProcessTaskTest() = default;
  ChildProcessTaskTest(const ChildProcessTaskTest&) = delete;
  ChildProcessTaskTest& operator=(const ChildProcessTaskTest&) = delete;
  ~ChildProcessTaskTest() override = default;

  // task_manager::TaskProviderObserver:
  void TaskAdded(Task* task) override {
    DCHECK(task);
    if (provided_tasks_.find(task->process_handle()) != provided_tasks_.end())
      FAIL() << "ChildProcessTaskProvider must never provide duplicate tasks";

    provided_tasks_[task->process_handle()] = task;
  }

  void TaskRemoved(Task* task) override {
    DCHECK(task);
    provided_tasks_.erase(task->process_handle());
  }

  bool AreProviderContainersEmpty(
      const ChildProcessTaskProvider& provider) const {
    return provider.tasks_by_processid_.empty() &&
           provider.tasks_by_child_id_.empty();
  }

 protected:
  std::map<base::ProcessHandle, raw_ptr<Task, CtnExperimental>> provided_tasks_;

 private:
  content::BrowserTaskEnvironment task_environment_;
};

// Performs a basic test.
TEST_F(ChildProcessTaskTest, BasicTest) {
  ChildProcessTaskProvider provider;
  EXPECT_TRUE(provided_tasks_.empty());
  provider.SetObserver(this);
  content::RunAllPendingInMessageLoop();
  ASSERT_TRUE(provided_tasks_.empty()) <<
      "unit tests don't have any browser child processes";
  provider.ClearObserver();
  EXPECT_TRUE(provided_tasks_.empty());
  EXPECT_TRUE(AreProviderContainersEmpty(provider));
}

// Tests everything related to child process task providing.
TEST_F(ChildProcessTaskTest, TestAll) {
  ChildProcessTaskProvider provider;
  EXPECT_TRUE(provided_tasks_.empty());
  provider.SetObserver(this);
  content::RunAllPendingInMessageLoop();
  ASSERT_TRUE(provided_tasks_.empty());

  // The following process which has handle = base::kNullProcessHandle, won't be
  // added.
  ChildProcessData data1(0, content::ChildProcessId());
  ASSERT_FALSE(data1.GetProcess().IsValid());
  provider.BrowserChildProcessLaunchedAndConnected(data1);
  EXPECT_TRUE(provided_tasks_.empty());

  const content::ChildProcessId unique_id(245);
  const std::u16string name(u"Test Task");
  const std::u16string expected_name(
      l10n_util::GetStringFUTF16(IDS_TASK_MANAGER_UTILITY_PREFIX, name));

  ChildProcessData data2(content::PROCESS_TYPE_UTILITY, unique_id);
  data2.SetProcess(base::Process::Current());
  data2.name = name;
  provider.BrowserChildProcessLaunchedAndConnected(data2);
  ASSERT_EQ(1U, provided_tasks_.size());

  Task* task = provided_tasks_.begin()->second;
  // Process handles may not match, but process IDs must:
  EXPECT_EQ(base::GetCurrentProcId(), base::GetProcId(task->process_handle()));
  EXPECT_EQ(base::GetCurrentProcId(), task->process_id());
  EXPECT_EQ(expected_name, task->title());
  EXPECT_EQ(Task::UTILITY, task->GetType());
  // TODO(crbug.com/379869738): Remove GetUnsafeValue() usage.
  EXPECT_EQ(unique_id.GetUnsafeValue(), task->GetChildProcessUniqueID());
  EXPECT_EQ(std::u16string(), task->GetProfileName());
  EXPECT_FALSE(task->ReportsSqliteMemory());
  EXPECT_FALSE(task->ReportsWebCacheStats());

  // Make sure that indexing by child_id works properly.
  // TODO(crbug.com/379869738): Remove GetUnsafeValue() usage.
  ASSERT_EQ(task, provider.GetTaskOfUrlRequest(unique_id.GetUnsafeValue(), 0));
  ASSERT_EQ(task, provider.GetTaskOfUrlRequest(unique_id.GetUnsafeValue(), 1));

  const base::ByteSize bytes_read = base::KiB(1);
  task->OnNetworkBytesRead(bytes_read);
  task->Refresh(base::Seconds(1), REFRESH_TYPE_NETWORK_USAGE);

  EXPECT_EQ(bytes_read, task->GetNetworkUsageRate());

  // Clearing the observer won't notify us of any tasks removals even though
  // tasks will be actually deleted.
  provider.ClearObserver();
  EXPECT_FALSE(provided_tasks_.empty());
  EXPECT_TRUE(AreProviderContainersEmpty(provider));
}

// Tests the translation of |content::ProcessType| to
// |task_manager::Task::Type|.
TEST_F(ChildProcessTaskTest, ProcessTypeToTaskType) {
  ChildProcessTaskProvider provider;
  EXPECT_TRUE(provided_tasks_.empty());
  provider.SetObserver(this);
  content::RunAllPendingInMessageLoop();
  ASSERT_TRUE(provided_tasks_.empty());

  for (const auto& types_pair : kProcessTaskTypesPairs) {
    // Add the task.
    ChildProcessData data(types_pair.process_type_, content::ChildProcessId());
    data.SetProcess(base::Process::Current());
    provider.BrowserChildProcessLaunchedAndConnected(data);
    ASSERT_EQ(1U, provided_tasks_.size());
    Task* task = provided_tasks_.begin()->second;
    EXPECT_EQ(base::GetCurrentProcId(),
              base::GetProcId(task->process_handle()));
    EXPECT_EQ(types_pair.expected_task_type_, task->GetType());

    // Remove the task.
    provider.BrowserChildProcessHostDisconnected(data);
    EXPECT_TRUE(provided_tasks_.empty());
  }

  provider.ClearObserver();
  EXPECT_TRUE(AreProviderContainersEmpty(provider));
}

}  // namespace task_manager
