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

// Test support library for request payloads.

#include "chrome/browser/policy/messaging_layer/util/test_request_payload.h"

#include <algorithm>
#include <string>
#include <string_view>

#include "base/json/json_reader.h"
#include "base/strings/string_util.h"
#include "chrome/browser/policy/messaging_layer/upload/encrypted_reporting_client.h"
#include "chrome/browser/policy/messaging_layer/upload/record_upload_request_builder.h"
#include "components/reporting/util/encrypted_reporting_json_keys.h"
#include "third_party/abseil-cpp/absl/strings/ascii.h"

namespace reporting {

// Return true if s a properly formatted positive integer, i.e., is not empty,
// contains digits only and does not start with 0.
static bool IsPositiveInteger(std::string_view s) {
  if (s.empty()) {
    return false;
  }
  if (s.size() == 1) {
    return absl::ascii_isdigit(static_cast<unsigned char>(s[0]));
  }
  return s[0] != '0' && s.find_first_not_of("0123456789") == std::string::npos;
}

// Get the record list. If it can't, print the message to listener and return a
// null pointer.
static const base::ListValue* GetRecordList(const base::DictValue& arg,
                                            MatchResultListener* listener) {
  const auto* const record_list = arg.FindList(json_keys::kEncryptedRecordList);
  if (record_list == nullptr) {
    *listener << "No key named \"encryptedRecord\" in the argument or the "
                 "value is not a list.";
    return nullptr;
  }
  return record_list;
}

bool AttachEncryptionSettingsMatcher::MatchAndExplain(
    const base::DictValue& arg,
    MatchResultListener* listener) const {
  const auto attach_encryption_settings =
      arg.FindBool(json_keys::kAttachEncryptionSettings);
  if (!attach_encryption_settings) {
    *listener << "No key named \"attachEncryptionSettings\" in the argument or "
                 "the value is not of bool type.";
    return false;
  }
  if (!attach_encryption_settings.value()) {
    *listener << "The value of \"attachEncryptionSettings\" is false.";
    return false;
  }
  return true;
}

void AttachEncryptionSettingsMatcher::DescribeTo(std::ostream* os) const {
  *os << "has a valid attachEncryptionSettings field.";
}

void AttachEncryptionSettingsMatcher::DescribeNegationTo(
    std::ostream* os) const {
  *os << "has an invalid attachEncryptionSettings field.";
}

std::string AttachEncryptionSettingsMatcher::Name() const {
  return "attach-encryption-settings-matcher";
}

bool NoAttachEncryptionSettingsMatcher::MatchAndExplain(
    const base::DictValue& arg,
    MatchResultListener* listener) const {
  if (arg.Find(json_keys::kAttachEncryptionSettings) != nullptr) {
    *listener << "Found \"attachEncryptionSettings\" in the argument.";
    return false;
  }
  return true;
}

void NoAttachEncryptionSettingsMatcher::DescribeTo(std::ostream* os) const {
  *os << "expectedly has no attachEncryptionSettings field.";
}

void NoAttachEncryptionSettingsMatcher::DescribeNegationTo(
    std::ostream* os) const {
  *os << "unexpectedly has an attachEncryptionSettings field.";
}

std::string NoAttachEncryptionSettingsMatcher::Name() const {
  return "no-attach-encryption-settings-matcher";
}

bool ConfigurationFileVersionMatcher::MatchAndExplain(
    const base::DictValue& arg,
    MatchResultListener* listener) const {
  auto* attach_configuration_file =
      arg.Find(json_keys::kConfigurationFileVersion);
  if (!attach_configuration_file->GetIfInt().has_value()) {
    *listener << "No key named \"configurationFileVersion\" in the argument or "
                 "the value is not of int type.";
    return false;
  }
  return true;
}

void ConfigurationFileVersionMatcher::DescribeTo(std::ostream* os) const {
  *os << "has a valid configurationFileVersion field.";
}

void ConfigurationFileVersionMatcher::DescribeNegationTo(
    std::ostream* os) const {
  *os << "has an invalid configurationFileVersion field.";
}

std::string ConfigurationFileVersionMatcher::Name() const {
  return "configuration-file-version-matcher";
}

bool NoConfigurationFileVersionMatcher::MatchAndExplain(
    const base::DictValue& arg,
    MatchResultListener* listener) const {
  if (arg.Find(json_keys::kConfigurationFileVersion) != nullptr) {
    *listener << "Found \"configurationFileVersion\" in the argument.";
    return false;
  }
  return true;
}

void NoConfigurationFileVersionMatcher::DescribeTo(std::ostream* os) const {
  *os << "expectedly has no configurationFileVersion field.";
}

void NoConfigurationFileVersionMatcher::DescribeNegationTo(
    std::ostream* os) const {
  *os << "unexpectedly has an configurationFileVersion field.";
}

std::string NoConfigurationFileVersionMatcher::Name() const {
  return "no-configuration-file-version-matcher";
}

bool SourceMatcher::MatchAndExplain(const base::DictValue& arg,
                                    MatchResultListener* listener) const {
  if (arg.FindString(json_keys::kSource) == nullptr) {
    *listener << "No key named \"source\" or the value "
                 "is not a string in the argument.";
    return false;
  }
  return true;
}

void SourceMatcher::DescribeTo(std::ostream* os) const {
  *os << "has a valid source field.";
}

void SourceMatcher::DescribeNegationTo(std::ostream* os) const {
  *os << "has an invalid source field.";
}

std::string SourceMatcher::Name() const {
  return "source-test-matcher";
}

bool NoSourceMatcher::MatchAndExplain(const base::DictValue& arg,
                                      MatchResultListener* listener) const {
  if (arg.Find(json_keys::kSource) != nullptr) {
    *listener << "Found \"source\" in the argument.";
    return false;
  }
  return true;
}

void NoSourceMatcher::DescribeTo(std::ostream* os) const {
  *os << "expectedly has no source field.";
}

void NoSourceMatcher::DescribeNegationTo(std::ostream* os) const {
  *os << "unexpectedly has an Source field.";
}

std::string NoSourceMatcher::Name() const {
  return "source-test-matcher";
}

void CompressionInformationMatcher::DescribeTo(std::ostream* os) const {
  *os << "has a valid compression information field.";
}

void CompressionInformationMatcher::DescribeNegationTo(std::ostream* os) const {
  *os << "has an invalid compression information field.";
}

std::string CompressionInformationMatcher::Name() const {
  return "encrypted-record-matcher";
}

bool EncryptedRecordMatcher::MatchAndExplain(
    const base::DictValue& arg,
    MatchResultListener* listener) const {
  const auto* const record_list = GetRecordList(arg, listener);
  return record_list != nullptr;
}

void EncryptedRecordMatcher::DescribeTo(std::ostream* os) const {
  *os << "has a valid encryptedRecord field.";
}

void EncryptedRecordMatcher::DescribeNegationTo(std::ostream* os) const {
  *os << "has an invalid encryptedRecord field.";
}

std::string EncryptedRecordMatcher::Name() const {
  return "encrypted-record-matcher";
}

bool RequestIdMatcher::MatchAndExplain(const base::DictValue& arg,
                                       MatchResultListener* listener) const {
  const auto* const request_id = arg.FindString(json_keys::kRequestId);
  if (request_id == nullptr) {
    *listener << "No key named \"requestId\" in the argument or the value "
                 "is not a string.";
    return false;
  }
  if (request_id->empty()) {
    *listener << "Request ID is empty.";
    return false;
  }
  if (!std::ranges::all_of(*request_id, base::IsHexDigit<char>)) {
    *listener << "Request ID is not a hexadecimal number.";
    return false;
  }

  return true;
}

void RequestIdMatcher::DescribeTo(std::ostream* os) const {
  *os << "has a valid request ID.";
}

void RequestIdMatcher::DescribeNegationTo(std::ostream* os) const {
  *os << "has an invalid request ID.";
}

std::string RequestIdMatcher::Name() const {
  return "request-id-matcher";
}

bool RecordMatcher::MatchAndExplain(const base::DictValue& arg,
                                    MatchResultListener* listener) const {
  switch (mode_) {
    case Mode::FullRequest: {
      const auto* record_list = GetRecordList(arg, listener);
      if (record_list == nullptr) {
        return false;
      }

      for (const auto& record : *record_list) {
        const auto* record_dict = record.GetIfDict();
        if (record_dict == nullptr) {
          *listener << "Record " << record << " is not a dict.";
          return false;
        }
        if (!this->MatchAndExplainRecord(*record_dict, listener)) {
          return false;
        }
      }
      return true;
    }
    case Mode::RecordOnly: {
      return this->MatchAndExplainRecord(arg, listener);
    }
    default: {
      // Should never reach here.
      *listener << "Invalid mode of RecordMatcher encountered: "
                << static_cast<std::underlying_type_t<Mode>>(mode_);
      return false;
    }
  }
}

RecordMatcher& RecordMatcher::SetMode(RecordMatcher::Mode mode) {
  mode_ = mode;
  return *this;
}

bool CompressionInformationMatcher::MatchAndExplainRecord(
    const base::DictValue& record,
    MatchResultListener* listener) const {
  const auto* const compression_info =
      record.FindDict(json_keys::kCompressionInformation);
  if (compression_info == nullptr) {
    *listener << "No key named \"compressionInformation\" or the value is "
                 "not a dict in record "
              << record << '.';
    return false;
  }

  const auto compression_algorithm =
      compression_info->FindInt(json_keys::kCompressionAlgorithm);
  if (!compression_algorithm.has_value()) {
    *listener << "No key named \"compressionAlgorithm\" under "
                 "compressionInformation "
                 "or the value is not an integer in record "
              << record << '.';
    return false;
  }
  if (compression_algorithm.value() > 1 || compression_algorithm < 0) {
    *listener << "The value of \"compressionInformation/compressionAlgorithm\" "
                 "is neither 0 nor 1 in record "
              << record << '.';
    return false;
  }

  return true;
}

bool EncryptedWrappedRecordRecordMatcher::MatchAndExplainRecord(
    const base::DictValue& record,
    MatchResultListener* listener) const {
  if (record.FindString(json_keys::kEncryptedWrappedRecord) == nullptr) {
    *listener << "No key named \"encryptedWrappedRecord\" or the value "
                 "is not a string in record "
              << record << '.';
    return false;
  }
  return true;
}

void EncryptedWrappedRecordRecordMatcher::DescribeTo(std::ostream* os) const {
  *os << "has valid encrypted wrapped records.";
}

void EncryptedWrappedRecordRecordMatcher::DescribeNegationTo(
    std::ostream* os) const {
  *os << "has at least one invalid encrypted wrapped records or has missing "
         "encrypted wrapped records.";
}

std::string EncryptedWrappedRecordRecordMatcher::Name() const {
  return "encrypted-wrapped-record-record-matcher";
}

bool NoEncryptedWrappedRecordRecordMatcher::MatchAndExplainRecord(
    const base::DictValue& record,
    MatchResultListener* listener) const {
  if (record.Find(json_keys::kEncryptedWrappedRecord) != nullptr) {
    *listener << "Found \"encryptedWrappedRecord\" in record " << record << '.';
    return false;
  }
  return true;
}

void NoEncryptedWrappedRecordRecordMatcher::DescribeTo(std::ostream* os) const {
  *os << "expectedly has no encrypted wrapped record.";
}

void NoEncryptedWrappedRecordRecordMatcher::DescribeNegationTo(
    std::ostream* os) const {
  *os << "has at least one encrypted wrapped record that it should not have.";
}

std::string NoEncryptedWrappedRecordRecordMatcher::Name() const {
  return "no-encrypted-wrapped-record-record-matcher";
}

bool SequenceInformationRecordMatcher::MatchAndExplainRecord(
    const base::DictValue& record,
    MatchResultListener* listener) const {
  const auto* const sequence_information =
      record.FindDict(json_keys::kSequenceInformation);
  if (sequence_information == nullptr) {
    *listener << "No key named \"sequenceInformation\" or the value is "
                 "not a dict in record "
              << record << '.';
    return false;
  }

  if (!sequence_information->FindInt(json_keys::kPriority).has_value()) {
    *listener << "No key named \"sequenceInformation/priority\" or the "
                 "value is not an integer in record "
              << record << '.';
    return false;
  }

  for (const char* id : {json_keys::kSequencingId, json_keys::kGenerationId}) {
    const auto* const id_val = sequence_information->FindString(id);
    if (id_val == nullptr) {
      *listener << "No key named \"sequenceInformation/" << id
                << "\" or the value is not a string in record " << record
                << '.';
      return false;
    }
    if (!IsPositiveInteger(*id_val)) {
      *listener << "The value of \"sequenceInformation/" << id
                << "\" is not a properly formatted positive integer in record "
                << record << '.';
      return false;
    }
  }

#if BUILDFLAG(IS_CHROMEOS)
  if (EncryptedReportingClient::GenerationGuidIsRequired()) {
    const auto* generation_guid =
        sequence_information->FindString(json_keys::kGenerationGuid);
    if ((!generation_guid || generation_guid->empty())) {
      *listener << "No key named \"sequenceInformation/generationGuid\" or the "
                   "value is not a string in record "
                << record << '.';
      return false;
    }
  }
#endif  // BUILDFLAG(IS_CHROMEOS)
  return true;
}

void SequenceInformationRecordMatcher::DescribeTo(std::ostream* os) const {
  *os << "has valid sequence information.";
}

void SequenceInformationRecordMatcher::DescribeNegationTo(
    std::ostream* os) const {
  *os << "has invalid sequence information.";
}

std::string SequenceInformationRecordMatcher::Name() const {
  return "sequence-information-record-matcher";
}

bool RequestContainingRecordMatcher::IsSubDict(const base::DictValue& sub,
                                               const base::DictValue& super) {
  for (auto&& [key, sub_value] : sub) {
    const auto* super_value = super.Find(key);
    if (super_value == nullptr || *super_value != sub_value) {
      return false;
    }
  }
  return true;
}

RequestContainingRecordMatcher::RequestContainingRecordMatcher(
    std::string_view matched_record_json)
    : matched_record_json_(matched_record_json) {}

bool RequestContainingRecordMatcher::MatchAndExplain(
    const base::DictValue& arg,
    MatchResultListener* listener) const {
  const auto* record_list = GetRecordList(arg, listener);
  if (record_list == nullptr) {
    return false;
  }

  const auto matched_record = base::JSONReader::Read(
      matched_record_json_, base::JSON_PARSE_CHROMIUM_EXTENSIONS);
  if (!matched_record.has_value()) {
    *listener << "The specified record cannot be parsed as a JSON object.";
    return false;
  }
  const auto* matched_record_dict = matched_record->GetIfDict();
  if (matched_record_dict == nullptr) {
    *listener
        << "The specified record must be a Dict itself because each record "
           "is a Dict.";
    return false;
  }

  for (const auto& record : *record_list) {
    const auto* record_dict = record.GetIfDict();
    if (!record_dict) {
      continue;
    }

    // Match each key and value of matched_record with those of the iterated
    // record_dict. In this way, users can specify part of a record instead
    // of the full record.
    if (IsSubDict(*matched_record_dict, *record_dict)) {
      return true;
    }
  }

  *listener << "The specified record is not found in the argument.";
  return false;
}

void RequestContainingRecordMatcher::DescribeTo(std::ostream* os) const {
  *os << "contains the specified record.";
}

void RequestContainingRecordMatcher::DescribeNegationTo(
    std::ostream* os) const {
  *os << "does not contain the specified record or there are other failed "
         "conditions.";
}

}  // namespace reporting
