// Copyright 2020 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/policy/messaging_layer/upload/upload_client.h"

#include <list>
#include <string>
#include <tuple>

#include "base/files/file_path.h"
#include "base/functional/bind.h"
#include "base/json/json_writer.h"
#include "base/strings/stringprintf.h"
#include "base/test/protobuf_matchers.h"
#include "base/test/task_environment.h"
#include "base/test/test_mock_time_task_runner.h"
#include "base/values.h"
#include "build/build_config.h"
#include "chrome/browser/policy/messaging_layer/util/reporting_server_connector.h"
#include "chrome/browser/policy/messaging_layer/util/reporting_server_connector_test_util.h"
#include "chrome/browser/policy/messaging_layer/util/test_request_payload.h"
#include "chrome/browser/policy/messaging_layer/util/test_response_payload.h"
#include "components/account_id/account_id.h"
#include "components/policy/core/common/cloud/mock_cloud_policy_client.h"
#include "components/reporting/proto/synced/record.pb.h"
#include "components/reporting/proto/synced/record_constants.pb.h"
#include "components/reporting/resources/resource_manager.h"
#include "components/reporting/util/status_macros.h"
#include "components/reporting/util/test_support_callbacks.h"
#include "content/public/test/browser_task_environment.h"
#include "google_apis/gaia/gaia_id.h"
#include "testing/gtest/include/gtest/gtest.h"

#if BUILDFLAG(IS_CHROMEOS)
#include "chrome/browser/ash/login/users/fake_chrome_user_manager.h"
#include "chrome/test/base/testing_profile.h"
#include "components/user_manager/scoped_user_manager.h"
#include "components/user_manager/test_helper.h"
#include "google_apis/gaia/core_account_id.h"
#endif  // BUILDFLAG(IS_CHROMEOS)

namespace reporting {
namespace {

using base::test::EqualsProto;
using ::policy::MockCloudPolicyClient;
using ::testing::_;
using ::testing::AllOf;
using ::testing::ContainerEq;
using ::testing::ElementsAre;
using ::testing::Eq;
using ::testing::Gt;
using ::testing::InvokeArgument;
using ::testing::IsEmpty;
using ::testing::MockFunction;
using ::testing::Not;
using ::testing::Property;
using testing::SizeIs;
using ::testing::StrictMock;
using ::testing::WithArgs;

class UploadClientTest : public ::testing::TestWithParam<
                             ::testing::tuple</*need_encryption_key*/ bool,
                                              /*force_confirm*/ bool>> {
 public:
  UploadClientTest() = default;

 protected:
  void SetUp() override {
    memory_resource_ =
        base::MakeRefCounted<ResourceManager>(4u * 1024LLu * 1024LLu);  // 4 MiB

#if BUILDFLAG(IS_CHROMEOS)
    // Set up fake primary profile.
    auto fake_user_manager = std::make_unique<ash::FakeChromeUserManager>();
    profile_ = std::make_unique<TestingProfile>(
        base::FilePath(FILE_PATH_LITERAL("/home/chronos/u-0123456789abcdef")));
    const AccountId account_id(AccountId::FromUserEmailGaiaId(
        profile_->GetProfileUserName(), GaiaId("12345")));
    fake_user_manager->AddPublicAccountUser(account_id);
    fake_user_manager->UserLoggedIn(
        account_id, user_manager::TestHelper::GetFakeUsernameHash(account_id));
    user_manager_ = std::make_unique<user_manager::ScopedUserManager>(
        std::move(fake_user_manager));
#endif  // BUILDFLAG(IS_CHROMEOS)
  }

  void TearDown() override {
#if BUILDFLAG(IS_CHROMEOS)
    user_manager_.reset();
    profile_.reset();
#endif  // BUILDFLAG(IS_CHROMEOS)
    EXPECT_THAT(memory_resource_->GetUsed(), Eq(0uL));
  }

  bool need_encryption_key() const { return std::get<0>(GetParam()); }

  bool force_confirm() const { return std::get<1>(GetParam()); }

  content::BrowserTaskEnvironment task_environment_;

#if BUILDFLAG(IS_CHROMEOS)
  std::unique_ptr<TestingProfile> profile_;
  std::unique_ptr<user_manager::ScopedUserManager> user_manager_;
#endif  // BUILDFLAG(IS_CHROMEOS)
  scoped_refptr<ResourceManager> memory_resource_;
};

using TestEncryptionKeyAttached = MockFunction<void(SignedEncryptionInfo)>;
using TestConfigFileAttached = MockFunction<void(ConfigFile)>;

TEST_P(UploadClientTest, CreateUploadClientAndUploadRecords) {
  static constexpr int64_t kExpectedCallTimes = 10;
  static constexpr int64_t kGenerationId = 1234;
#if BUILDFLAG(IS_CHROMEOS)
  static constexpr char kGenerationGuid[] =
      "c947e7e9-b87d-4592-9fe7-407792544e53";
#endif  // BUILDFLAG(IS_CHROMEOS)

  base::DictValue data;
  data.Set("TEST_KEY", "TEST_VALUE");

  std::string json_data;
  ASSERT_TRUE(base::JSONWriter::Write(data, &json_data));

  WrappedRecord wrapped_record;
  Record* record = wrapped_record.mutable_record();
  record->set_data(json_data);
  record->set_destination(Destination::UPLOAD_EVENTS);

  std::list<int64_t> expected_cached_seq_ids;
  ScopedReservation total_reservation(0uL, memory_resource_);
  std::string serialized_record;
  wrapped_record.SerializeToString(&serialized_record);
  std::vector<EncryptedRecord> records;
  for (int64_t i = 0; i < kExpectedCallTimes; i++) {
    EncryptedRecord encrypted_record;
    encrypted_record.set_encrypted_wrapped_record(serialized_record);
    SequenceInformation* sequence_information =
        encrypted_record.mutable_sequence_information();
    sequence_information->set_sequencing_id(i);
    sequence_information->set_generation_id(kGenerationId);
#if BUILDFLAG(IS_CHROMEOS)
    sequence_information->set_generation_guid(kGenerationGuid);
#endif  // BUILDFLAG(IS_CHROMEOS)
    sequence_information->set_priority(Priority::IMMEDIATE);
    ScopedReservation record_reservation(encrypted_record.ByteSizeLong(),
                                         memory_resource_);
    EXPECT_TRUE(record_reservation.reserved());
    total_reservation.HandOver(record_reservation);
    records.push_back(encrypted_record);
    expected_cached_seq_ids.push_back(i);
  }

  StrictMock<TestEncryptionKeyAttached> encryption_key_attached;
  EXPECT_CALL(
      encryption_key_attached,
      Call(AllOf(Property(&SignedEncryptionInfo::public_asymmetric_key,
                          Not(IsEmpty())),
                 Property(&SignedEncryptionInfo::public_key_id, Gt(0)),
                 Property(&SignedEncryptionInfo::signature, Not(IsEmpty())))))
      .Times(need_encryption_key() ? 1 : 0);
  auto encryption_key_attached_cb =
      base::BindRepeating(&TestEncryptionKeyAttached::Call,
                          base::Unretained(&encryption_key_attached));

  StrictMock<TestConfigFileAttached> config_file_attached;
  EXPECT_CALL(
      config_file_attached,
      Call(AllOf(Property(&ConfigFile::blocked_event_configs, Not(IsEmpty())),
                 Property(&ConfigFile::version, Gt(4444)),
                 Property(&ConfigFile::config_file_signature, Not(IsEmpty())))))
      .Times(0);
  auto config_file_attached_cb = base::BindRepeating(
      &TestConfigFileAttached::Call, base::Unretained(&config_file_attached));

  auto test_env = std::make_unique<ReportingServerConnector::TestEnvironment>();

  static constexpr char matched_record_template[] =
#if BUILDFLAG(IS_CHROMEOS)
      R"JSON(
{
  "sequenceInformation": {
    "generationId": "1234",
    "generationGuid": "c947e7e9-b87d-4592-9fe7-407792544e53",
    "priority": 1,
    "sequencingId": "%d"
  }
}
)JSON"
#else   // BUILDFLAG(IS_CHROMEOS)
      R"JSON(
{
  "sequenceInformation": {
    "generationId": "1234",
    "priority": 1,
    "sequencingId": "%d"
  }
}
)JSON"
#endif  // BUILDFLAG(IS_CHROMEOS)
      ;

  test::TestMultiEvent<SequenceInformation, bool> upload_success_event;

  // Save last record seq info for verification.
  const SequenceInformation last_record_seq_info =
      records.back().sequence_information();

  test::TestEvent<StatusOr<std::unique_ptr<UploadClient>>> e;
  UploadClient::Create(e.cb());
  StatusOr<std::unique_ptr<UploadClient>> upload_client_result = e.result();
  ASSERT_TRUE(upload_client_result.has_value()) << upload_client_result.error();

  auto upload_client = std::move(upload_client_result.value());
  // config_file_version is set to 0 for testing. The default value is -1 and we
  // want to override it.
  test::TestEvent<StatusOr<std::list<int64_t>>> enqueued_event;
  upload_client->EnqueueUpload(
      need_encryption_key(), /*config_file_version=*/0, std::move(records),
      std::move(total_reservation), enqueued_event.cb(),
      upload_success_event.repeating_cb(), encryption_key_attached_cb,
      std::move(config_file_attached_cb));
  const auto& enqueued_result = enqueued_event.result();
  ASSERT_OK(enqueued_result) << enqueued_result.error();
  EXPECT_THAT(enqueued_result.value(), ContainerEq(expected_cached_seq_ids));
  task_environment_.RunUntilIdle();

  ASSERT_THAT(*test_env->url_loader_factory()->pending_requests(), SizeIs(1));
  base::DictValue request_body = test_env->request_body(0);
  EXPECT_THAT(request_body, AllOf(IsDataUploadRequestValid(),
                                  DoesRequestContainRecord(base::StringPrintf(
                                      matched_record_template, 0)),
                                  DoesRequestContainRecord(base::StringPrintf(
                                      matched_record_template, 1)),
                                  DoesRequestContainRecord(base::StringPrintf(
                                      matched_record_template, 2)),
                                  DoesRequestContainRecord(base::StringPrintf(
                                      matched_record_template, 3)),
                                  DoesRequestContainRecord(base::StringPrintf(
                                      matched_record_template, 4)),
                                  DoesRequestContainRecord(base::StringPrintf(
                                      matched_record_template, 5)),
                                  DoesRequestContainRecord(base::StringPrintf(
                                      matched_record_template, 6)),
                                  DoesRequestContainRecord(base::StringPrintf(
                                      matched_record_template, 7)),
                                  DoesRequestContainRecord(base::StringPrintf(
                                      matched_record_template, 8)),
                                  DoesRequestContainRecord(base::StringPrintf(
                                      matched_record_template, 9))));

  auto response = ResponseBuilder(std::move(request_body))
                      .SetForceConfirm(force_confirm())
                      .Build();
  ASSERT_OK(response) << response.error();
  test_env->SimulateCustomResponseForRequest(0, std::move(*response));

  auto upload_success_result = upload_success_event.result();
  EXPECT_THAT(std::get<0>(upload_success_result),
              EqualsProto(last_record_seq_info));
  EXPECT_THAT(std::get<1>(upload_success_result), Eq(force_confirm()));
}

INSTANTIATE_TEST_SUITE_P(
    NeedOrNoNeedKey,
    UploadClientTest,
    ::testing::Combine(/*need_encryption_key*/ ::testing::Bool(),
                       /*force_confirm*/ ::testing::Bool()));

}  // namespace
}  // namespace reporting
