// 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 "content/browser/scheduler/browser_task_queues.h"

#include <array>
#include <memory>

#include "base/functional/callback_helpers.h"
#include "base/message_loop/message_pump.h"
#include "base/message_loop/message_pump_type.h"
#include "base/run_loop.h"
#include "base/task/sequence_manager/sequence_manager.h"
#include "base/task/single_thread_task_runner.h"
#include "base/test/bind.h"
#include "base/test/mock_callback.h"
#include "base/test/scoped_feature_list.h"
#include "content/browser/scheduler/browser_task_priority.h"
#include "content/common/features.h"
#include "content/public/browser/browser_thread.h"
#include "testing/gmock/include/gmock/gmock.h"
#include "testing/gtest/include/gtest/gtest.h"

namespace content {
namespace {

using ::base::RunLoop;
using ::base::sequence_manager::CreateSequenceManagerOnCurrentThreadWithPump;
using ::base::sequence_manager::SequenceManager;
using ::testing::Mock;

using StrictMockTask =
    testing::StrictMock<base::MockCallback<base::RepeatingCallback<void()>>>;

using QueueType = BrowserTaskQueues::QueueType;

class BrowserTaskQueuesTest : public testing::Test {
 protected:
  BrowserTaskQueuesTest()
      : sequence_manager_(CreateSequenceManagerOnCurrentThreadWithPump(
            base::MessagePump::Create(base::MessagePumpType::DEFAULT),
            base::sequence_manager::SequenceManager::Settings::Builder()
                .SetPrioritySettings(
                    internal::CreateBrowserTaskPrioritySettings())
                .Build())) {}

  void SetUp() override {
    InitFeatureList();
    queues_ = std::make_unique<BrowserTaskQueues>(BrowserThread::UI,
                                                  sequence_manager_.get());
    handle_ = queues_->GetHandle();
    sequence_manager_->SetDefaultTaskQueue(queues_->GetDefaultTaskQueue());
  }

  virtual void InitFeatureList() {
    feature_list_.InitAndDisableFeature(
        features::kPrioritizeResizeTaskRunnerOnStartup);
  }

  base::test::ScopedFeatureList feature_list_;
  std::unique_ptr<SequenceManager> sequence_manager_;
  std::unique_ptr<BrowserTaskQueues> queues_;
  scoped_refptr<BrowserTaskQueues::Handle> handle_;
};

// Queues are disabled by default and only enabled by the BrowserTaskExecutor
// and so no task can be posted until the BrowserTaskExecutor is created. This
// allows an embedder to control when to enable the UI task queues. This state
// is required for WebView's async startup to work properly.
TEST_F(BrowserTaskQueuesTest, NoTaskRunsUntilQueuesAreEnabled) {
  StrictMockTask task;
  for (size_t i = 0; i < BrowserTaskQueues::kNumQueueTypes; ++i) {
    handle_->GetBrowserTaskRunner(static_cast<QueueType>(i))
        ->PostTask(FROM_HERE, task.Get());
  }

  {
    RunLoop run_loop;
    handle_->ScheduleRunAllPendingTasksForTesting(run_loop.QuitClosure());
    run_loop.Run();
  }

  handle_->OnStartupComplete();

  {
    RunLoop run_loop;
    handle_->ScheduleRunAllPendingTasksForTesting(run_loop.QuitClosure());
    EXPECT_CALL(task, Run).Times(BrowserTaskQueues::kNumQueueTypes);
    run_loop.Run();
  }
}

TEST_F(BrowserTaskQueuesTest, SimplePosting) {
  handle_->OnStartupComplete();
  scoped_refptr<base::SingleThreadTaskRunner> tq =
      handle_->GetBrowserTaskRunner(QueueType::kDefault);

  StrictMockTask task_1;
  StrictMockTask task_2;
  StrictMockTask task_3;

  {
    testing::InSequence s;
    EXPECT_CALL(task_1, Run);
    EXPECT_CALL(task_2, Run);
    EXPECT_CALL(task_3, Run);
  }

  tq->PostTask(FROM_HERE, task_1.Get());
  tq->PostTask(FROM_HERE, task_2.Get());
  tq->PostTask(FROM_HERE, task_3.Get());

  base::RunLoop().RunUntilIdle();
}

TEST_F(BrowserTaskQueuesTest, RunAllPendingTasksForTesting) {
  handle_->OnStartupComplete();

  StrictMockTask task;
  StrictMockTask followup_task;
  EXPECT_CALL(task, Run).WillOnce([&]() {
    for (size_t i = 0; i < BrowserTaskQueues::kNumQueueTypes; ++i) {
      handle_->GetBrowserTaskRunner(static_cast<QueueType>(i))
          ->PostTask(FROM_HERE, followup_task.Get());
    }
  });

  handle_->GetBrowserTaskRunner(QueueType::kDefault)
      ->PostTask(FROM_HERE, task.Get());

  {
    RunLoop run_loop;
    handle_->ScheduleRunAllPendingTasksForTesting(run_loop.QuitClosure());
    run_loop.Run();
  }

  Mock::VerifyAndClearExpectations(&task);
  EXPECT_CALL(followup_task, Run).Times(BrowserTaskQueues::kNumQueueTypes);

  RunLoop run_loop;
  handle_->ScheduleRunAllPendingTasksForTesting(run_loop.QuitClosure());
  run_loop.Run();
}

TEST_F(BrowserTaskQueuesTest, RunAllPendingTasksForTestingRunsAllTasks) {
  constexpr size_t kTasksPerPriority = 100;
  handle_->OnStartupComplete();

  StrictMockTask task;
  EXPECT_CALL(task, Run).Times(BrowserTaskQueues::kNumQueueTypes *
                               kTasksPerPriority);
  for (size_t i = 0; i < BrowserTaskQueues::kNumQueueTypes; ++i) {
    for (size_t j = 0; j < kTasksPerPriority; ++j) {
      handle_->GetBrowserTaskRunner(static_cast<QueueType>(i))
          ->PostTask(FROM_HERE, task.Get());
    }
  }

  RunLoop run_loop;
  handle_->ScheduleRunAllPendingTasksForTesting(run_loop.QuitClosure());
  run_loop.Run();
}

TEST_F(BrowserTaskQueuesTest, RunAllPendingTasksForTestingIsReentrant) {
  handle_->OnStartupComplete();
  StrictMockTask task_1;
  StrictMockTask task_2;
  StrictMockTask task_3;

  EXPECT_CALL(task_1, Run).WillOnce([&]() {
    handle_->GetBrowserTaskRunner(QueueType::kDefault)
        ->PostTask(FROM_HERE, task_2.Get());
    RunLoop run_loop(RunLoop::Type::kNestableTasksAllowed);
    handle_->ScheduleRunAllPendingTasksForTesting(run_loop.QuitClosure());
    run_loop.Run();
  });

  EXPECT_CALL(task_2, Run).WillOnce([&]() {
    handle_->GetBrowserTaskRunner(QueueType::kDefault)
        ->PostTask(FROM_HERE, task_3.Get());
  });

  handle_->GetBrowserTaskRunner(QueueType::kDefault)
      ->PostTask(FROM_HERE, task_1.Get());

  RunLoop run_loop;
  handle_->ScheduleRunAllPendingTasksForTesting(run_loop.QuitClosure());
  run_loop.Run();
}

TEST_F(BrowserTaskQueuesTest,
       RunAllPendingTasksForTestingIgnoresBestEffortIfNotEnabled) {
  handle_->EnableAllExceptBestEffortQueues();
  StrictMockTask best_effort_task;
  StrictMockTask default_task;

  handle_->GetBrowserTaskRunner(QueueType::kBestEffort)
      ->PostTask(FROM_HERE, best_effort_task.Get());
  handle_->GetBrowserTaskRunner(QueueType::kDefault)
      ->PostTask(FROM_HERE, default_task.Get());

  EXPECT_CALL(default_task, Run);

  RunLoop run_loop;
  handle_->ScheduleRunAllPendingTasksForTesting(run_loop.QuitClosure());
  run_loop.Run();
}

TEST_F(BrowserTaskQueuesTest,
       RunAllPendingTasksForTestingRunsBestEffortTasksWhenEnabled) {
  handle_->EnableAllExceptBestEffortQueues();
  StrictMockTask task_1;
  StrictMockTask task_2;
  StrictMockTask task_3;

  EXPECT_CALL(task_1, Run).WillOnce([&]() {
    // This task should not run as it is posted after the
    // RunAllPendingTasksForTesting() call
    handle_->GetBrowserTaskRunner(QueueType::kBestEffort)
        ->PostTask(FROM_HERE, task_3.Get());
    handle_->OnStartupComplete();
  });
  EXPECT_CALL(task_2, Run);

  handle_->GetBrowserTaskRunner(QueueType::kDefault)
      ->PostTask(FROM_HERE, task_1.Get());
  handle_->GetBrowserTaskRunner(QueueType::kBestEffort)
      ->PostTask(FROM_HERE, task_2.Get());

  RunLoop run_loop;
  handle_->ScheduleRunAllPendingTasksForTesting(run_loop.QuitClosure());
  run_loop.Run();
}

TEST_F(BrowserTaskQueuesTest, HandleStillWorksWhenQueuesDestroyed) {
  handle_->OnStartupComplete();
  StrictMockTask task;
  queues_.reset();

  for (size_t i = 0; i < BrowserTaskQueues::kNumQueueTypes; ++i) {
    EXPECT_FALSE(handle_->GetBrowserTaskRunner(static_cast<QueueType>(i))
                     ->PostTask(FROM_HERE, base::DoNothing()));
  }

  RunLoop run_loop;
  handle_->ScheduleRunAllPendingTasksForTesting(run_loop.QuitClosure());
  run_loop.Run();
}

TEST_F(BrowserTaskQueuesTest, TaskIsRunWhenEnableQueueIsCalled) {
  StrictMockTask task;
  StrictMockTask user_visible;
  for (size_t i = 0; i < BrowserTaskQueues::kNumQueueTypes; ++i) {
    auto queue_type = static_cast<QueueType>(i);
    if (queue_type != QueueType::kUserVisible) {
      handle_->GetBrowserTaskRunner(queue_type)
          ->PostTask(FROM_HERE, task.Get());
    }
  }
  handle_->GetBrowserTaskRunner(QueueType::kUserVisible)
      ->PostTask(FROM_HERE, user_visible.Get());

  {
    RunLoop run_loop;
    handle_->ScheduleRunAllPendingTasksForTesting(run_loop.QuitClosure());
    run_loop.Run();
  }

  handle_->EnableTaskQueue(QueueType::kUserVisible);

  {
    RunLoop run_loop;
    handle_->ScheduleRunAllPendingTasksForTesting(run_loop.QuitClosure());
    EXPECT_CALL(user_visible, Run).Times(1);
    run_loop.Run();
  }
}

TEST_F(BrowserTaskQueuesTest, StartupQueuePriorityDisabled) {
  using internal::BrowserTaskPriority;

  // Initially, kStartup queue is not prioritized.
  EXPECT_EQ(queues_->GetQueuePriorityForTesting(QueueType::kStartup),
            static_cast<base::sequence_manager::TaskQueue::QueuePriority>(
                BrowserTaskPriority::kNormalPriority));

  // Call OnStartupComplete and verify it remains normal priority.
  handle_->OnStartupComplete();
  {
    base::RunLoop run_loop;
    handle_->ScheduleRunAllPendingTasksForTesting(run_loop.QuitClosure());
    run_loop.Run();
  }

  EXPECT_EQ(queues_->GetQueuePriorityForTesting(QueueType::kStartup),
            static_cast<base::sequence_manager::TaskQueue::QueuePriority>(
                BrowserTaskPriority::kNormalPriority));
}

class BrowserTaskQueuesWithStartupPrioritizationTest
    : public BrowserTaskQueuesTest {
 protected:
  BrowserTaskQueuesWithStartupPrioritizationTest() = default;

  void InitFeatureList() override {
    feature_list_.InitAndEnableFeature(
        features::kPrioritizeResizeTaskRunnerOnStartup);
  }
};

TEST_F(BrowserTaskQueuesWithStartupPrioritizationTest,
       StartupQueuePriorityEnabled) {
  using internal::BrowserTaskPriority;

  // Initially, kStartup queue is prioritized immediately after construction.
  EXPECT_EQ(queues_->GetQueuePriorityForTesting(QueueType::kStartup),
            static_cast<base::sequence_manager::TaskQueue::QueuePriority>(
                BrowserTaskPriority::kHighestPriority));

  // Enable queues to verify it remains prioritized.
  handle_->EnableAllExceptBestEffortQueues();
  {
    base::RunLoop run_loop;
    handle_->ScheduleRunAllPendingTasksForTesting(run_loop.QuitClosure());
    run_loop.Run();
  }

  EXPECT_EQ(queues_->GetQueuePriorityForTesting(QueueType::kStartup),
            static_cast<base::sequence_manager::TaskQueue::QueuePriority>(
                BrowserTaskPriority::kHighestPriority));

  // Once startup is complete, it is set to normal priority.
  handle_->OnStartupComplete();
  {
    base::RunLoop run_loop;
    handle_->ScheduleRunAllPendingTasksForTesting(run_loop.QuitClosure());
    run_loop.Run();
  }

  EXPECT_EQ(queues_->GetQueuePriorityForTesting(QueueType::kStartup),
            static_cast<base::sequence_manager::TaskQueue::QueuePriority>(
                BrowserTaskPriority::kNormalPriority));
}

TEST_F(BrowserTaskQueuesTest, PostFeatureListInitDisabled) {
  using internal::BrowserTaskPriority;

  EXPECT_EQ(queues_->GetQueuePriorityForTesting(QueueType::kStartup),
            static_cast<base::sequence_manager::TaskQueue::QueuePriority>(
                BrowserTaskPriority::kNormalPriority));

  queues_->PostFeatureListInit();

  EXPECT_EQ(queues_->GetQueuePriorityForTesting(QueueType::kStartup),
            static_cast<base::sequence_manager::TaskQueue::QueuePriority>(
                BrowserTaskPriority::kNormalPriority));
}

TEST_F(BrowserTaskQueuesTest, PostFeatureListInitEnabled) {
  using internal::BrowserTaskPriority;

  EXPECT_EQ(queues_->GetQueuePriorityForTesting(QueueType::kStartup),
            static_cast<base::sequence_manager::TaskQueue::QueuePriority>(
                BrowserTaskPriority::kNormalPriority));

  base::test::ScopedFeatureList feature_list;
  feature_list.InitAndEnableFeature(
      features::kPrioritizeResizeTaskRunnerOnStartup);

  queues_->PostFeatureListInit();

  EXPECT_EQ(queues_->GetQueuePriorityForTesting(QueueType::kStartup),
            static_cast<base::sequence_manager::TaskQueue::QueuePriority>(
                BrowserTaskPriority::kHighestPriority));
}

}  // namespace
}  // namespace content
