// 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 "components/variations/variations_test_utils.h"

#include "base/base64.h"
#include "base/command_line.h"
#include "base/feature_list.h"
#include "base/files/file_path.h"
#include "base/files/file_util.h"
#include "base/json/json_file_value_serializer.h"
#include "base/no_destructor.h"
#include "base/time/time.h"
#include "base/values.h"
#include "components/metrics/clean_exit_beacon.h"
#include "components/metrics/metrics_pref_names.h"
#include "components/prefs/pref_service.h"
#include "components/variations/active_field_trials.h"
#include "components/variations/client_filterable_state.h"
#include "components/variations/field_trial_config/fieldtrial_testing_config.h"
#include "components/variations/hashing.h"
#include "components/variations/pref_names.h"
#include "components/variations/proto/client_variations.pb.h"
#include "components/variations/proto/stored_seed_info.pb.h"
#include "components/variations/seed_reader_writer.h"
#include "components/variations/synthetic_trial_registry.h"
#include "components/variations/variations_associated_data.h"
#include "components/variations/variations_switches.h"
#include "testing/gtest/include/gtest/gtest.h"
#include "third_party/zlib/google/compression_utils.h"

namespace variations {
namespace {

// Create mock testing config equivalent to:
// {
//   "UnitTest": [
//       {
//           "platforms": [
//               "android",
//               "android_webview",
//               "chromeos",
//               "fuchsia",
//               "ios",
//               "linux",
//               "mac",
//               "windows"
//           ],
//           "experiments": [
//               {
//                   "name": "Enabled",
//                   "params": {
//                       "x": "1"
//                   },
//                   "enable_features": [
//                       "UnitTestEnabled"
//                   ]
//               }
//           ]
//       }
//   ]
// }

const Study::Platform array_kFieldTrialConfig_platforms_0[] = {
    Study::PLATFORM_ANDROID,
    Study::PLATFORM_ANDROID_WEBVIEW,
    Study::PLATFORM_CHROMEOS,
    Study::PLATFORM_FUCHSIA,
    Study::PLATFORM_IOS,
    Study::PLATFORM_LINUX,
    Study::PLATFORM_MAC,
    Study::PLATFORM_WINDOWS,
};

const char* enable_features_0[] = {"UnitTestEnabled"};
const FieldTrialTestingExperimentParams array_kFieldTrialConfig_params_0[] = {
    {
        "x",
        "1",
    },
};

const FieldTrialTestingExperiment array_kFieldTrialConfig_experiments_0[] = {
    {/*name=*/"Enabled",
     /*platforms=*/array_kFieldTrialConfig_platforms_0,
     /*form_factors=*/{},
     /*is_low_end_device=*/std::nullopt,
     /*disable_benchmarking=*/std::nullopt,
     /*min_os_version=*/nullptr,
     /*params=*/array_kFieldTrialConfig_params_0,
     /*enable_features=*/enable_features_0,
     /*disable_features=*/{},
     /*forcing_flag=*/nullptr},
};

const FieldTrialTestingStudy array_kFieldTrialConfig_studies[] = {
    {/*name=*/"UnitTest",
     /*experiments=*/array_kFieldTrialConfig_experiments_0},
};

}  // namespace

// TestSeedData() is a simple VariationsSeed containing:
// serial_number: "test"
// study: {
//   name: "UMA-Uniformity-Trial-50-Percent"
//   consistency: PERMANENT
//   experiment: {
//     name: "default"
//     probability_weight: 1
//   }
//   experiment: {
//     name: "group_01"
//     probability_weight: 1
//   }
// }
const SignedSeedData& TestSeedData() {
  static const char* kTestSeed_StudyNames[] = {
      "UMA-Uniformity-Trial-50-Percent"};
  static const base::NoDestructor<SignedSeedData> kTestSeedData(
      /*study_names=*/kTestSeed_StudyNames,
      /*base64_uncompressed_data=*/
      "CgR0ZXN0Ej4KH1VNQS1Vbmlmb3JtaXR5LVRyaWFsLTUwLVBlcmNlbnQ4AUoLCgdkZWZhdWx0"
      "EAFKDAoIZ3JvdXBfMDEQAQ==",
      /*base64_compressed_data=*/
      "H4sIAAAAAAAA/+JiKUktLhGy45IP9XXUDc3LTMsvys0sqdQNKcpMzNE1NdANSC1KTs0rsWD0"
      "4uZiT0lNSyzNKRFg9OLh4kgvyi8tiDcwFGAEBAAA//90/JgERgAAAA==",
      /*base64_signature=*/
      "MEUCIQD5AEAzk5qEuE3xOZl+xSZR15Ac1RJpsXMiou7i5W0sMAIgRn++ngh03HaMGC+Pjl9N"
      "OuDoxf83qsSwycF2PSS1nYQ=");
  return *kTestSeedData;
}

// CrashingSeedData() is a VariationsSeed that triggers a crash for testing:
// serial_number:  "35ed2d9e354b414befdf930a734094019c0162f1"
// study:  {
//   name:  "CrashingStudy"
//   consistency:  PERMANENT
//   experiment:  {
//     name:  "EnabledLaunch"
//     probability_weight:  100
//     feature_association:  {
//       enable_feature:  "ForceFieldTrialSetupCrashForTesting"
//     }
//   }
//   experiment:  {
//     name:  "ForcedOn_ForceFieldTrialSetupCrashForTesting"
//     probability_weight:  0
//     feature_association:  {
//       forcing_feature_on:  "ForceFieldTrialSetupCrashForTesting"
//     }
//   }
//   experiment:  {
//     name:  "ForcedOff_ForceFieldTrialSetupCrashForTesting"
//     probability_weight:  0
//     feature_association:  {
//       forcing_feature_off:  "ForceFieldTrialSetupCrashForTesting"
//     }
//   }
//   filter:  {
//     min_version:  "91.*"
//     channel:  CANARY
//     channel:  DEV
//     channel:  BETA
//     channel:  STABLE
//     platform:  PLATFORM_ANDROID
//     platform:  PLATFORM_IOS
//     platform:  PLATFORM_ANDROID_WEBVIEW
//     platform:  PLATFORM_WINDOWS
//     platform:  PLATFORM_MAC
//     platform:  PLATFORM_LINUX
//     platform:  PLATFORM_CHROMEOS
//     platform:  PLATFORM_CHROMEOS_LACROS
//   }
// }
// version:  "hash/4aa56a1dc30dfc767615248d6fee29830198b276"
const SignedSeedData& CrashingSeedData() {
  static const char* kCrashingSeed_StudyNames[] = {"CrashingStudy"};
  static const base::NoDestructor<SignedSeedData> kCrashingSeedData(
      /*study_names=*/kCrashingSeed_StudyNames,
      /*base64_uncompressed_data=*/
      "CigzNWVkMmQ5ZTM1NGI0MTRiZWZkZjkzMGE3MzQwOTQwMTljMDE2MmYxEp4CCg1DcmFzaGlu"
      "Z1N0dWR5OAFKOAoNRW5hYmxlZExhdW5jaBBkYiUKI0ZvcmNlRmllbGRUcmlhbFNldHVwQ3Jh"
      "c2hGb3JUZXN0aW5nSlcKLEZvcmNlZE9uX0ZvcmNlRmllbGRUcmlhbFNldHVwQ3Jhc2hGb3JU"
      "ZXN0aW5nEABiJRojRm9yY2VGaWVsZFRyaWFsU2V0dXBDcmFzaEZvclRlc3RpbmdKWAotRm9y"
      "Y2VkT2ZmX0ZvcmNlRmllbGRUcmlhbFNldHVwQ3Jhc2hGb3JUZXN0aW5nEABiJSIjRm9yY2VG"
      "aWVsZFRyaWFsU2V0dXBDcmFzaEZvclRlc3RpbmdSHhIEOTEuKiAAIAEgAiADKAQoBSgGKAAo"
      "ASgCKAMoCSItaGFzaC80YWE1NmExZGMzMGRmYzc2NzYxNTI0OGQ2ZmVlMjk4MzAxOThiMjc"
      "2",
      /*base64_compressed_data=*/
      "H4sIAAAAAAAAAI3QwUvDMBTH8babwgKDsaMHKZNBEKdJk6bJWbbDEAQ30JskeS+2UKp07cF/"
      "Zn+rZfgH9Py+73P4ESpyhAwMilw6yaXDAMEIZgshmZGMG8+4ygJfnhMyf27tqayar0PXw6+"
      "O95rMt411NcKL7RtfLsCtyd3uu/W4q7CGY1vZ+oBd/"
      "3P5HA5HPHUDsH8nD5cMXpvPEf0icuubUfAH2fzDIYyVV2Pkt9vl1PDH+zRK4zRJJ3RKr+"
      "g1jWhMEzqhs9WmHPonaW2uLAcvGARfqELxPJMaVEDMjBbDotplhfoDs9NLbnoBAAA=",
      /*base64_signature=*/
      "MEQCIEn1+VsBfNA93dxzpk+BLhdO91kMQnofxfTK5Uo8vDi8AiAnTCFCIPgEGWNOKzuKfNWn"
      "6emB6pnGWjSTbI/pvfxHnw==");
  return *kCrashingSeedData;
}

const SignedSeedPrefKeys kSafeSeedPrefKeys{prefs::kVariationsSafeCompressedSeed,
                                           prefs::kVariationsSafeSeedSignature};

const SignedSeedPrefKeys kRegularSeedPrefKeys{prefs::kVariationsCompressedSeed,
                                              prefs::kVariationsSeedSignature};

SignedSeedData::SignedSeedData(base::raw_span<const char*> in_study_names,
                               const char* in_base64_uncompressed_data,
                               const char* in_base64_compressed_data,
                               const char* in_base64_signature)
    : study_names(std::move(in_study_names)),
      base64_uncompressed_data(in_base64_uncompressed_data),
      base64_compressed_data(in_base64_compressed_data),
      base64_signature(in_base64_signature) {}

SignedSeedData::~SignedSeedData() = default;

SignedSeedData::SignedSeedData(const SignedSeedData&) = default;
SignedSeedData::SignedSeedData(SignedSeedData&&) = default;
SignedSeedData& SignedSeedData::operator=(const SignedSeedData&) = default;
SignedSeedData& SignedSeedData::operator=(SignedSeedData&&) = default;

void DisableTestingConfig() {
  base::CommandLine::ForCurrentProcess()->AppendSwitch(
      switches::kDisableFieldTrialTestingConfig);
}

void EnableTestingConfig() {
  base::CommandLine::ForCurrentProcess()->AppendSwitch(
      switches::kEnableFieldTrialTestingConfig);
}

bool ExtractVariationIds(const std::string& variations,
                         std::set<VariationID>* variation_ids,
                         std::set<VariationID>* trigger_ids) {
  std::string serialized_proto;
  if (!base::Base64Decode(variations, &serialized_proto)) {
    return false;
  }
  ClientVariations proto;
  if (!proto.ParseFromString(serialized_proto)) {
    return false;
  }

  EXPECT_TRUE(IsSortedAndUnique(proto.variation_id()));
  EXPECT_TRUE(IsSortedAndUnique(proto.trigger_variation_id()));

  for (int i = 0; i < proto.variation_id_size(); ++i) {
    variation_ids->insert(proto.variation_id(i));
  }
  for (int i = 0; i < proto.trigger_variation_id_size(); ++i) {
    trigger_ids->insert(proto.trigger_variation_id(i));
  }
  return true;
}

scoped_refptr<base::FieldTrial> CreateInactiveTrialAndAssociateId(
    const std::string& trial_name,
    const std::string& group_name,
    IDCollectionKey key,
    VariationID id,
    TimeWindow time_window) {
  AssociateGoogleVariationIDForTesting(key, trial_name, group_name, id,
                                       time_window);
  scoped_refptr<base::FieldTrial> trial(
      base::FieldTrialList::CreateFieldTrial(trial_name, group_name));
  CHECK(trial);
  return trial;
}

scoped_refptr<base::FieldTrial> CreateTrialAndAssociateId(
    const std::string& trial_name,
    const std::string& group_name,
    IDCollectionKey key,
    VariationID id,
    TimeWindow time_window) {
  auto trial = CreateInactiveTrialAndAssociateId(trial_name, group_name, key,
                                                 id, time_window);
  if (trial) {
    // Ensure the trial is registered under the correct key so we can look it
    // up.
    trial->Activate();
  }

  return trial;
}

void SimulateCrash(PrefService* local_state) {
  local_state->SetBoolean(metrics::prefs::kStabilityExitedCleanly, false);
  metrics::CleanExitBeacon::SkipCleanShutdownStepsForTesting();
}

void WriteSignedSeedData(PrefService* local_state,
                         const SignedSeedData& seed_data,
                         const SignedSeedPrefKeys& pref_keys) {
  local_state->SetString(pref_keys.base64_compressed_data_key,
                         seed_data.base64_compressed_data);
  local_state->SetString(pref_keys.base64_signature_key,
                         seed_data.base64_signature);
  local_state->CommitPendingWrite();
}

void WriteSeedData(const base::FilePath& user_data_dir,
                   const VariationsSeed& seed) {
  const base::FilePath local_state_path =
      user_data_dir.Append(FILE_PATH_LITERAL("Local State"));
  const base::FilePath seed_file_path =
      user_data_dir.Append(FILE_PATH_LITERAL("VariationsSeedV2"));

  const std::string serialized_seed = seed.SerializeAsString();
  std::string compressed_seed;
  compression::GzipCompress(serialized_seed, &compressed_seed);

  // Write the Local-State-based seed.
  base::DictValue local_state;
  local_state.SetByDottedPath(prefs::kVariationsCompressedSeed,
                              base::Base64Encode(compressed_seed));
  CHECK(JSONFileValueSerializer(local_state_path).Serialize(local_state));

  // Write the seed-file-based seed.
  StoredSeedInfo seed_info;
  seed_info.set_data(serialized_seed);
  CHECK(base::WriteFile(seed_file_path,
                        SeedReaderWriter::CompressForSeedFileForTesting(
                            seed_info.SerializeAsString())));
}

bool FieldTrialListHasAllStudiesFrom(const SignedSeedData& seed_data) {
  return std::ranges::all_of(seed_data.study_names, [](const char* study) {
    return base::FieldTrialList::TrialExists(study);
  });
}

void ResetVariations() {
  test::ClearAllVariationIDs();
  test::ClearAllVariationParams();
}

const FieldTrialTestingConfig kTestingConfig = {
    array_kFieldTrialConfig_studies};

std::unique_ptr<ClientFilterableState> CreateDummyClientFilterableState() {
  auto client_state = std::make_unique<ClientFilterableState>();
  client_state->locale = "en-CA";
  client_state->reference_date = base::Time::Now();
  client_state->version = base::Version("20.0.0.0");
  client_state->channel = Study::STABLE;
  client_state->form_factor = Study::PHONE;
  client_state->platform = Study::PLATFORM_ANDROID;
  return client_state;
}

// Constructs mocked EntropyProviders.
MockEntropyProviders::MockEntropyProviders(
    MockEntropyProviders::Results results,
    uint32_t low_entropy_domain)
    : EntropyProviders(results.high_entropy.has_value() ? "client_id" : "",
                       {0, low_entropy_domain},
                       results.limited_entropy.has_value()
                           ? "limited_entropy_randomization_source"
                           : std::string_view()),
      low_provider_(results.low_entropy),
      high_provider_(results.high_entropy.value_or(0)),
      limited_provider_(results.limited_entropy.value_or(0)) {}

MockEntropyProviders::~MockEntropyProviders() = default;

const base::FieldTrial::EntropyProvider& MockEntropyProviders::low_entropy()
    const {
  return low_provider_;
}

const base::FieldTrial::EntropyProvider& MockEntropyProviders::default_entropy()
    const {
  if (default_entropy_is_high_entropy()) {
    return high_provider_;
  }
  return low_provider_;
}

const base::FieldTrial::EntropyProvider& MockEntropyProviders::limited_entropy()
    const {
  CHECK(has_limited_entropy());
  return limited_provider_;
}

std::string GZipAndB64EncodeToHexString(const VariationsSeed& seed) {
  auto serialized = seed.SerializeAsString();
  std::string compressed;
  compression::GzipCompress(serialized, &compressed);
  return base::Base64Encode(compressed);
}

bool ContainsTrialName(const std::vector<ActiveGroupId>& active_group_ids,
                       std::string_view trial_name) {
  auto hashed_name = HashName(trial_name);
  for (const auto& trial : active_group_ids) {
    if (trial.name == hashed_name) {
      return true;
    }
  }
  return false;
}

bool ContainsTrialAndGroupName(
    const std::vector<ActiveGroupId>& active_group_ids,
    std::string_view trial_name,
    std::string_view group_name) {
  auto hashed_trial_name = HashName(trial_name);
  auto hashed_group_name = HashName(group_name);
  for (const auto& trial : active_group_ids) {
    if (trial.name == hashed_trial_name && trial.group == hashed_group_name) {
      return true;
    }
  }
  return false;
}

void SetUpSeedFileTrial(std::string_view group_name) {
  if (group_name.empty()) {
    return;
  }
  base::MockEntropyProvider entropy_provider(0.9);
  scoped_refptr<base::FieldTrial> trial(
      base::FieldTrialList::FactoryGetFieldTrial(
          kSeedFileTrial, /*total_probability=*/100, kDefaultGroup,
          entropy_provider));

  trial->AppendGroup(group_name, /*group_probability=*/100);
  trial->SetForced();
}

}  // namespace variations
