// Copyright 2021 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/reporting/client/report_queue_provider.h"

#include <memory>
#include <string>
#include <utility>

#include "base/functional/bind.h"
#include "base/task/bind_post_task.h"
#include "base/task/sequenced_task_runner.h"
#include "base/task/thread_pool.h"
#include "base/test/scoped_feature_list.h"
#include "base/test/task_environment.h"
#include "components/reporting/client/mock_report_queue.h"
#include "components/reporting/client/mock_report_queue_provider.h"
#include "components/reporting/client/report_queue.h"
#include "components/reporting/client/report_queue_configuration.h"
#include "components/reporting/client/report_queue_provider_test_helper.h"
#include "components/reporting/util/statusor.h"
#include "components/reporting/util/test_support_callbacks.h"
#include "testing/gmock/include/gmock/gmock.h"
#include "testing/gtest/include/gtest/gtest.h"

using ::testing::_;
using ::testing::Eq;
using ::testing::Invoke;
using ::testing::NiceMock;
using ::testing::StrEq;
using ::testing::WithArg;

namespace reporting {
namespace {

constexpr char kTestMessage[] = "TEST MESSAGE";

class ReportQueueProviderTest : public ::testing::Test {
 protected:
  void SetUp() override {
    helper_ = std::make_unique<test::ReportQueueProviderTestHelper>();
  }

  void TearDown() override { helper_.reset(); }

  base::test::TaskEnvironment task_environment_{
      base::test::TaskEnvironment::TimeSource::MOCK_TIME};

  std::unique_ptr<test::ReportQueueProviderTestHelper> helper_;
  const Destination destination_ = Destination::UPLOAD_EVENTS;
};

// Asynchronously creates ReportingQueue and enqueue message.
// Returns result with callback.
void CreateQueuePostData(
    std::string data,
    Priority priority,
    std::unique_ptr<ReportQueueConfiguration> config,
    scoped_refptr<base::SequencedTaskRunner> sequenced_task_runner,
    ReportQueue::EnqueueCallback done_cb) {
  base::OnceCallback<void(StatusOr<std::unique_ptr<ReportQueue>>)> queue_cb =
      base::BindOnce(
          [](std::string data, Priority priority,
             ReportQueue::EnqueueCallback done_cb,
             StatusOr<std::unique_ptr<ReportQueue>> report_queue_result) {
            // Bail out if queue failed to create.
            if (!report_queue_result.has_value()) {
              std::move(done_cb).Run(report_queue_result.error());
              return;
            }
            // Queue created successfully, enqueue the message on a random
            // thread and verify.
            EXPECT_CALL(*static_cast<MockReportQueue*>(
                            report_queue_result.value().get()),
                        AddRecord(StrEq(data), Eq(priority), _))
                .WillOnce(WithArg<2>([](ReportQueue::EnqueueCallback cb) {
                  std::move(cb).Run(Status::StatusOK());
                }));
            base::ThreadPool::PostTask(
                FROM_HERE,
                base::BindOnce(
                    [](std::unique_ptr<ReportQueue> queue, std::string data,
                       Priority priority,
                       ReportQueue::EnqueueCallback done_cb) {
                      queue->Enqueue(data, priority, std::move(done_cb));
                    },
                    std::move(report_queue_result.value()), data, priority,
                    std::move(done_cb)));
          },
          std::string(data), priority, std::move(done_cb));
  ReportQueueProvider::CreateQueue(
      std::move(config),
      // Verification callback needs to be serialized, because EXPECT_... do not
      // support multithreading.
      base::BindPostTask(sequenced_task_runner, std::move(queue_cb)));
}

// Asynchronously creates ReportingQueue and enqueue message.
// Returns result with callback.
void CreateSpeculativeQueuePostData(
    std::string data,
    Priority priority,
    std::unique_ptr<ReportQueueConfiguration> config,
    scoped_refptr<base::SequencedTaskRunner> sequenced_task_runner,
    ReportQueue::EnqueueCallback done_cb) {
  auto report_queue_result =
      ReportQueueProvider::CreateSpeculativeQueue(std::move(config));
  // Bail out if queue failed to create.
  if (!report_queue_result.has_value()) {
    std::move(done_cb).Run(report_queue_result.error());
    return;
  }
  // Queue created successfully, enqueue the message on a random thread and
  // verify.
  EXPECT_CALL(*static_cast<MockReportQueue*>(report_queue_result.value().get()),
              AddRecord(StrEq(data), Eq(priority), _))
      .WillOnce(WithArg<2>([](ReportQueue::EnqueueCallback cb) {
        std::move(cb).Run(Status::StatusOK());
      }));
  // Enqueue on a random thread again.
  base::ThreadPool::PostTask(
      FROM_HERE,
      base::BindOnce(
          [](std::unique_ptr<ReportQueue, base::OnTaskRunnerDeleter> queue,
             std::string data, Priority priority,
             ReportQueue::EnqueueCallback done_cb) {
            queue->Enqueue(data, priority, std::move(done_cb));
          },
          std::move(report_queue_result.value()), data, priority,
          // Verification callback needs to be serialized, because EXPECT_... do
          // not support multithreading.
          base::BindPostTask(sequenced_task_runner, std::move(done_cb))));
}

TEST_F(ReportQueueProviderTest, CreateAndGetQueue) {
  // Create configuration.
  auto config_result =
      ReportQueueConfiguration::Create(
          {.event_type = EventType::kDevice, .destination = destination_})
          .Build();
  ASSERT_TRUE(config_result.has_value());
  EXPECT_CALL(*helper_->mock_provider(), OnInitCompletedMock()).Times(1);
  helper_->mock_provider()->ExpectCreateNewQueueAndReturnNewMockQueue(1);
  // Use it to asynchronously create ReportingQueue and then asynchronously
  // send the message.
  test::TestEvent<Status> e;
  base::ThreadPool::PostTask(
      FROM_HERE,
      base::BindOnce(&CreateQueuePostData, kTestMessage, FAST_BATCH,
                     std::move(config_result.value()),
                     base::SequencedTaskRunner::GetCurrentDefault(), e.cb()));
  const auto res = e.result();
  EXPECT_OK(res) << res;
}

TEST_F(ReportQueueProviderTest, CreateMultipleQueues) {
  static constexpr std::array<std::pair<Priority, Destination>, 9> send_as{
      std::make_pair(FAST_BATCH, HEARTBEAT_EVENTS),
      std::make_pair(SLOW_BATCH, CRD_EVENTS),
      std::make_pair(IMMEDIATE, LOGIN_LOGOUT_EVENTS),
      std::make_pair(SLOW_BATCH, HEARTBEAT_EVENTS),
      std::make_pair(FAST_BATCH, CRD_EVENTS),
      std::make_pair(IMMEDIATE, LOGIN_LOGOUT_EVENTS),
      std::make_pair(FAST_BATCH, HEARTBEAT_EVENTS),
      std::make_pair(IMMEDIATE, CRD_EVENTS),
      std::make_pair(SLOW_BATCH, LOGIN_LOGOUT_EVENTS),
  };
  test::TestCallbackAutoWaiter waiter;
  waiter.Attach(send_as.size() + 1);
  // Expect only one InitCompleted callback.
  EXPECT_CALL(*helper_->mock_provider(), OnInitCompletedMock())
      .WillOnce(Invoke(&waiter, &test::TestCallbackAutoWaiter::Signal));
  // ... even though we create multiple queues.
  helper_->mock_provider()->ExpectCreateNewQueueAndReturnNewMockQueue(
      send_as.size());
  for (const auto& s : send_as) {
    // Create configuration.
    auto config_result =
        ReportQueueConfiguration::Create(
            {.event_type = EventType::kDevice, .destination = s.second})
            .Build();
    ASSERT_TRUE(config_result.has_value());
    // Compose the message.
    std::string message = std::string(kTestMessage)
                              .append(" priority=")
                              .append(Priority_Name(s.first))
                              .append(" destination=")
                              .append(Destination_Name(s.second));
    // Asynchronously create ReportingQueue and then asynchronously send the
    // message.
    auto done_cb = base::BindOnce(
        [](test::TestCallbackWaiter* waiter, Status status) {
          EXPECT_OK(status) << status;
          waiter->Signal();
        },
        &waiter);
    base::ThreadPool::PostTask(
        FROM_HERE,
        base::BindOnce(&CreateQueuePostData, std::move(message),
                       /*priority=*/s.first, std::move(config_result.value()),
                       base::SequencedTaskRunner::GetCurrentDefault(),
                       std::move(done_cb)));
  }
  waiter.Signal();  // Release the waiter
}

TEST_F(ReportQueueProviderTest, CreateMultipleSpeculativeQueues) {
  static constexpr std::array<std::pair<Priority, Destination>, 9> send_as{
      std::make_pair(FAST_BATCH, HEARTBEAT_EVENTS),
      std::make_pair(SLOW_BATCH, CRD_EVENTS),
      std::make_pair(IMMEDIATE, LOGIN_LOGOUT_EVENTS),
      std::make_pair(SLOW_BATCH, HEARTBEAT_EVENTS),
      std::make_pair(FAST_BATCH, CRD_EVENTS),
      std::make_pair(IMMEDIATE, LOGIN_LOGOUT_EVENTS),
      std::make_pair(FAST_BATCH, HEARTBEAT_EVENTS),
      std::make_pair(IMMEDIATE, CRD_EVENTS),
      std::make_pair(SLOW_BATCH, LOGIN_LOGOUT_EVENTS),
  };
  test::TestCallbackAutoWaiter waiter;
  waiter.Attach(send_as.size() + 1);
  // Expect only one InitCompleted callback.
  EXPECT_CALL(*helper_->mock_provider(), OnInitCompletedMock())
      .WillOnce(Invoke(&waiter, &test::TestCallbackAutoWaiter::Signal));
  // ... even though we create multiple queues.
  helper_->mock_provider()
      ->ExpectCreateNewSpeculativeQueueAndReturnNewMockQueue(send_as.size());
  for (const auto& s : send_as) {
    // Create configuration.
    auto config_result =
        ReportQueueConfiguration::Create(
            {.event_type = EventType::kDevice, .destination = s.second})
            .Build();
    ASSERT_TRUE(config_result.has_value());
    // Compose the message.
    std::string message = std::string(kTestMessage)
                              .append(" priority=")
                              .append(Priority_Name(s.first))
                              .append(" destination=")
                              .append(Destination_Name(s.second));
    // Create SpeculativeReportingQueue and then asynchronously send the
    // message.
    auto done_cb = base::BindOnce(
        [](test::TestCallbackWaiter* waiter, Status status) {
          EXPECT_OK(status) << status;
          waiter->Signal();
        },
        &waiter);
    CreateSpeculativeQueuePostData(
        std::move(message),
        /*priority=*/s.first, std::move(config_result.value()),
        base::SequencedTaskRunner::GetCurrentDefault(), std::move(done_cb));
  }
  waiter.Signal();  // Release the waiter
}

TEST_F(ReportQueueProviderTest,
       CreateReportQueueWithEncryptedReportingPipelineDisabled) {
  base::test::ScopedFeatureList feature_list;
  feature_list.InitAndDisableFeature(kEncryptedReportingPipeline);

  // Create configuration
  auto config_result =
      ReportQueueConfiguration::Create(
          {.event_type = EventType::kDevice, .destination = destination_})
          .Build();
  ASSERT_TRUE(config_result.has_value());

  test::TestEvent<ReportQueueProvider::CreateReportQueueResponse> event;
  ReportQueueProvider::CreateQueue(std::move(config_result.value()),
                                   event.cb());
  const auto result = event.result();

  ASSERT_FALSE(result.has_value());
  EXPECT_EQ(result.error().code(), error::FAILED_PRECONDITION);
}

TEST_F(ReportQueueProviderTest,
       CreateSpeculativeReportQueueWithEncryptedReportingPipelineDisabled) {
  base::test::ScopedFeatureList feature_list;
  feature_list.InitAndDisableFeature(kEncryptedReportingPipeline);

  // Create configuration
  auto config_result =
      ReportQueueConfiguration::Create(
          {.event_type = EventType::kDevice, .destination = destination_})
          .Build();
  ASSERT_TRUE(config_result.has_value());

  const auto result = ReportQueueProvider::CreateSpeculativeQueue(
      std::move(config_result.value()));
  ASSERT_FALSE(result.has_value());
  EXPECT_EQ(result.error().code(), error::FAILED_PRECONDITION);
}
}  // namespace
}  // namespace reporting
