// Copyright 2013 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_seed_processor.h"

#include <stddef.h>

#include <map>
#include <optional>
#include <utility>
#include <vector>

#include "base/command_line.h"
#include "base/feature_list.h"
#include "base/metrics/field_trial.h"
#include "base/metrics/field_trial_params.h"
#include "base/metrics/histogram_functions.h"
#include "base/rand_util.h"
#include "base/strings/string_number_conversions.h"
#include "base/strings/string_util.h"
#include "base/strings/utf_string_conversions.h"
#include "base/types/optional_ref.h"
#include "components/variations/client_filterable_state.h"
#include "components/variations/entropy_provider.h"
#include "components/variations/processed_study.h"
#include "components/variations/study_filtering.h"
#include "components/variations/variations_associated_data.h"
#include "components/variations/variations_layers.h"

namespace variations {

namespace internal {

const char kFeatureConflictGroupName[] = "ClientSideFeatureConflict";
const char kGoogleGroupFeatureParamName[] = "__GGIDS";
const char kGoogleGroupFeatureParamSeparator[] = ",";

}  // namespace internal

namespace {

// Serializes the `google_groups` attribute of `filter`.
std::string SerializeGoogleGroupsFilter(const Study::Filter& filter) {
  std::string result;
  for (int64_t group_id : filter.google_group()) {
    if (!result.empty()) {
      result.append(internal::kGoogleGroupFeatureParamSeparator);
    }
    result.append(base::NumberToString(group_id));
  }
  return result;
}

// Converts `seconds` to a base::Time relative to the Unix epoch, or returns
// `default_value` if `seconds` is non-positive.
base::Time FromSecondsSinceUnixEpoch(int64_t seconds,
                                     base::Time default_value) {
  return (seconds <= 0) ? default_value
                        : base::Time::FromSecondsSinceUnixEpoch(seconds);
}

// Returns the time window during which the study may, upon becoming active, be
// visible to Google web properties. Note that the start and end proto fields
// default to 0, which are converted to base::Time::Min() and base::Time::Max()
// respectively, which is the desired default behavior for studies having no
// explicit start or end visibility dates.
TimeWindow GetWebVisibilityTimeWindow(const Study& study) {
  const base::Time start_time =
      FromSecondsSinceUnixEpoch(study.google_web_visibility_start_date(),
                                /*default_value=*/base::Time::Min());
  const base::Time end_time =
      FromSecondsSinceUnixEpoch(study.google_web_visibility_end_date(),
                                /*default_value=*/base::Time::Max());
  // TimeWindow gracefully maps invalid time windows (e.g. start >= end)
  // as empty (zero-duration) time spans.
  return {start_time, std::max(start_time, end_time)};
}

// Associates the variations params of `experiment`, if present.
void RegisterExperimentParams(const Study& study,
                              const Study::Experiment& experiment) {
  std::map<std::string, std::string> params;
  for (const auto& param : experiment.param()) {
    if (param.has_name() && param.has_value()) {
      params[param.name()] = param.value();
    }
  }
  // If the study has a filter with a `google_groups` attribute, we write those
  // Google Group ids into a feature parameter. This allows looking up which
  // Google Groups may influence a feature's state.
  if (study.filter().google_group_size() > 0) {
    params[internal::kGoogleGroupFeatureParamName] =
        SerializeGoogleGroupsFilter(study.filter());
  }
  if (!params.empty()) {
    base::AssociateFieldTrialParams(study.name(), experiment.name(),
                                    std::move(params));
  }
}

// Returns the IDCollectionKey with which |experiment| should be associated.
// Returns nullopt when |experiment| doesn't have a Google web or Google web
// trigger experiment ID.
std::optional<IDCollectionKey> GetKeyForWebExperiment(
    const Study::Experiment& experiment) {
  if (!VariationsSeedProcessor::HasGoogleWebExperimentId(experiment)) {
    return std::nullopt;
  }
  bool has_web_experiment_id = experiment.has_google_web_experiment_id();
  bool has_web_trigger_experiment_id =
      experiment.has_google_web_trigger_experiment_id();

  // An experiment cannot have both |google_web_experiment_id| and
  // |google_trigger_web_experiment_id|. This is enforced by
  // ValidateAndComputeTotalProbability() in processed_study.cc.
  CHECK(!(has_web_experiment_id && has_web_trigger_experiment_id));

  Study::GoogleWebVisibility visibility = experiment.google_web_visibility();
  if (visibility == Study::FIRST_PARTY) {
    return has_web_trigger_experiment_id
               ? GOOGLE_WEB_PROPERTIES_TRIGGER_FIRST_PARTY
               : GOOGLE_WEB_PROPERTIES_FIRST_PARTY;
  }
  return has_web_trigger_experiment_id
             ? GOOGLE_WEB_PROPERTIES_TRIGGER_ANY_CONTEXT
             : GOOGLE_WEB_PROPERTIES_ANY_CONTEXT;
}

// If there are VariationIDs associated with the |experiment| arm of |study|,
// register said VariationIDs unless `is_trial_overridden` is true.
void RegisterVariationIds(base::PassKey<VariationsSeedProcessor> pass_key,
                          const Study::Experiment& experiment,
                          const Study& study,
                          bool is_trial_overridden) {
  if (is_trial_overridden && experiment.has_google_web_experiment_id()) {
    Study::Experiment updated_experiment = experiment;
    updated_experiment.clear_google_web_experiment_id();
    RegisterVariationIds(pass_key, updated_experiment, study, false);
    return;
  }

  const std::string& trial_name = study.name();
  const auto web_visibility_time_window = GetWebVisibilityTimeWindow(study);
  if (experiment.has_google_app_experiment_id()) {
    const VariationID variation_id =
        static_cast<VariationID>(experiment.google_app_experiment_id());
    AssociateGoogleVariationID(pass_key, GOOGLE_APP,
                               MakeActiveGroupId(trial_name, experiment.name()),
                               variation_id, web_visibility_time_window);
  }

  std::optional<IDCollectionKey> key = GetKeyForWebExperiment(experiment);
  if (!key.has_value()) {
    return;
  }

  CHECK(VariationsSeedProcessor::HasGoogleWebExperimentId(experiment));
  // An experiment cannot have both |google_web_experiment_id| and
  // |google_trigger_web_experiment_id|. See GetKeyForWebExperiment() for more
  // details.
  const VariationID variation_id =
      experiment.has_google_web_trigger_experiment_id()
          ? static_cast<VariationID>(
                experiment.google_web_trigger_experiment_id())
          : static_cast<VariationID>(experiment.google_web_experiment_id());

  AssociateGoogleVariationID(pass_key, key.value(),
                             MakeActiveGroupId(trial_name, experiment.name()),
                             variation_id, web_visibility_time_window);
}

// Whether the given study should be activated on startup.
bool ShouldActivate(const Study& study,
                    const std::string group_name,
                    StickyActivationManager& sticky_activation_manager) {
  switch (study.activation_type()) {
    case Study::ACTIVATE_ON_STARTUP:
      return true;
    case Study::ACTIVATE_ON_QUERY:
      return false;
    case Study::STICKY_AFTER_QUERY:
      return sticky_activation_manager.ShouldActivate(study.name(), group_name);
    case Study_ActivationType_Study_ActivationType_INT_MIN_SENTINEL_DO_NOT_USE_:
    case Study_ActivationType_Study_ActivationType_INT_MAX_SENTINEL_DO_NOT_USE_:
      // Part of the enum but won't be seen in practice. See processed_study.cc.
      return false;
  }
}

// Forces the specified |experiment| to be enabled in |study|.
void ForceExperimentState(
    base::PassKey<VariationsSeedProcessor> pass_key,
    const Study& study,
    const Study::Experiment& experiment,
    StickyActivationManager& sticky_activation_manager,
    base::FieldTrial& trial) {
  RegisterExperimentParams(study, experiment);
  RegisterVariationIds(pass_key, experiment, study, trial.IsOverridden());
}

// Associates features for groups that do not specify them manually.
void AssociateDefaultFeatures(const Study& study,
                              base::FieldTrial* trial,
                              base::FeatureList* feature_list) {
  // Note: We only compute feature associations for ACTIVATE_ON_QUERY studies,
  // since these associations are only used to determine that the trial has
  // been queried when the feature is queried.
  // Note: We only compute feature associations for ACTIVATE_ON_QUERY and
  // STICKY_AFTER_QUERY studies, since these associations are only used to
  // ensure that the trial is activated when the feature is queried
  switch (study.activation_type()) {
    case Study::ACTIVATE_ON_STARTUP:
      return;
    case Study::ACTIVATE_ON_QUERY:
      // fall-through:
    case Study::STICKY_AFTER_QUERY:
      break;
    case Study_ActivationType_Study_ActivationType_INT_MIN_SENTINEL_DO_NOT_USE_:
    case Study_ActivationType_Study_ActivationType_INT_MAX_SENTINEL_DO_NOT_USE_:
      // Part of the enum but won't be seen in practice. See processed_study.cc.
      return;
  }

  std::set<std::string> features_to_associate;
  for (const auto& experiment : study.experiment()) {
    const auto& features = experiment.feature_association();
    for (const auto& feature : features.enable_feature()) {
      features_to_associate.insert(feature);
    }
    for (const auto& feature : features.disable_feature()) {
      features_to_associate.insert(feature);
    }
  }
  for (const auto& feature_name : features_to_associate) {
    feature_list->RegisterFieldTrialOverride(
        feature_name, base::FeatureList::OVERRIDE_USE_DEFAULT, trial);
  }
}

// Registers feature overrides `experiment` in the `study`.
void RegisterFeatureOverrides(const Study& study,
                              const Study::Experiment& experiment,
                              base::FieldTrial* trial,
                              base::FeatureList* feature_list) {
  // Process all the features to enable.
  int feature_count = experiment.feature_association().enable_feature_size();
  for (int i = 0; i < feature_count; ++i) {
    feature_list->RegisterFieldTrialOverride(
        experiment.feature_association().enable_feature(i),
        base::FeatureList::OVERRIDE_ENABLE_FEATURE, trial);
  }

  // Process all the features to disable.
  feature_count = experiment.feature_association().disable_feature_size();
  for (int i = 0; i < feature_count; ++i) {
    feature_list->RegisterFieldTrialOverride(
        experiment.feature_association().disable_feature(i),
        base::FeatureList::OVERRIDE_DISABLE_FEATURE, trial);
  }

  // Associate features for groups that do not specify them manually (e.g.
  // "Default" group), so that such groups are reported.
  if (!experiment.has_feature_association()) {
    AssociateDefaultFeatures(study, trial, feature_list);
  }
}

// Checks if |experiment| is associated with a forcing flag or feature and if
// it is, returns whether it should be forced enabled based on the
// |command_line| or |feature_list| state.
bool ShouldForceExperiment(const Study::Experiment& experiment,
                           const base::CommandLine& command_line,
                           const base::FeatureList& feature_list) {
  if (experiment.feature_association().has_forcing_feature_on()) {
    return feature_list.IsFeatureOverriddenFromCommandLine(
        experiment.feature_association().forcing_feature_on(),
        base::FeatureList::OVERRIDE_ENABLE_FEATURE);
  }
  if (experiment.feature_association().has_forcing_feature_off()) {
    return feature_list.IsFeatureOverriddenFromCommandLine(
        experiment.feature_association().forcing_feature_off(),
        base::FeatureList::OVERRIDE_DISABLE_FEATURE);
  }
  if (experiment.has_forcing_flag()) {
    return command_line.HasSwitch(experiment.forcing_flag());
  }
  return false;
}

bool StudyIsLowAnonymity(const Study& study) {
  // Studies which are set based on Google group membership are potentially
  // low anonymity (as the groups could in theory have a small number of
  // members).
  return study.filter().google_group_size() > 0 ||
         study.filter().enterprise_group_size() > 0;
}

// Creates a placeholder trial that indicates the feature conflict.
//
// This forcibly associates |trial_name| with the |kFeatureConflictGroupName|
// group, which indicates the trial was not applied due to a feature conflict.
// This group has no features, params, or variation IDs associated with it.
//
// Trials may be associated with this group due to toggling flags in
// chrome://flags that are associated with the trial's features, or if there
// are different trials associated with the same feature.
base::FieldTrial* CreateTrialWithFeatureConflictGroup(const Study& study) {
  base::FieldTrial* trial = base::FieldTrialList::CreateFieldTrial(
      study.name(), internal::kFeatureConflictGroupName,
      StudyIsLowAnonymity(study));
  DCHECK(trial);
  // Activate immediately to make the conflict obvious in metrics logs.
  trial->Activate();
  return trial;
}

}  // namespace

// static
bool VariationsSeedProcessor::HasGoogleWebExperimentId(
    const Study::Experiment& experiment) {
  return experiment.has_google_web_experiment_id() ||
         experiment.has_google_web_trigger_experiment_id();
}

VariationsSeedProcessor::VariationsSeedProcessor(
    StickyActivationManager& sticky_activation_manager)
    : sticky_activation_manager_(sticky_activation_manager) {}

VariationsSeedProcessor::~VariationsSeedProcessor() = default;

void VariationsSeedProcessor::CreateTrialsFromSeed(
    const VariationsSeed& seed,
    const ClientFilterableState& client_state,
    const EntropyProviders& entropy_providers,
    const VariationsLayers& layers,
    base::FeatureList* feature_list) {
  base::UmaHistogramCounts1000("Variations.AppliedSeed.StudyCount",
                               seed.study().size());
  std::vector<ProcessedStudy> filtered_studies =
      FilterAndValidateStudies(seed, client_state, layers);

  for (const ProcessedStudy& study : filtered_studies) {
    bool run_simulation = base::ShouldRecordSubsampledMetric(0.01) &&
                          !base::FieldTrialList::Find(study.study()->name());

    scoped_refptr<base::FieldTrial> actual_trial = CreateTrialFromStudyImpl(
        study, entropy_providers, layers, feature_list, /*simulated=*/false);

    if (run_simulation) {
      scoped_refptr<base::FieldTrial> simulated_trial =
          CreateTrialFromStudyImpl(study, entropy_providers, layers,
                                   feature_list, /*simulated=*/true);

      // Validate that the simulation returns the same result as the actual
      // trial. Note that `CreateTrialFromStudyImpl()` (`simulated = false`) can
      // return a trial with the group `kFeatureConflictGroupName` when a
      // feature conflict occurs. `CreateTrialFromStudyImpl()` (`simulated =
      // true`) skips this check (`if (!simulated)`) and simulates the group
      // assignment from scratch. We only validate when no feature conflict
      // occurs.
      if (!actual_trial || actual_trial->GetGroupNameWithoutActivation() !=
                               internal::kFeatureConflictGroupName) {
        bool matches = (simulated_trial && actual_trial &&
                        simulated_trial->GetGroupNameWithoutActivation() ==
                            actual_trial->GetGroupNameWithoutActivation()) ||
                       (!simulated_trial && !actual_trial);
        base::UmaHistogramBoolean("Variations.CreateTrial.SimulationMatches",
                                  matches);
      }
    }
  }
}

scoped_refptr<base::FieldTrial> VariationsSeedProcessor::CreateTrialFromStudy(
    base::PassKey<VariationsService>,
    const ProcessedStudy& processed_study,
    const EntropyProviders& entropy_providers,
    const VariationsLayers& layers,
    base::FeatureList* feature_list,
    bool simulated) {
  return CreateTrialFromStudyImpl(processed_study, entropy_providers, layers,
                                  feature_list, simulated);
}

scoped_refptr<base::FieldTrial>
VariationsSeedProcessor::CreateTrialFromStudyImpl(
    const ProcessedStudy& processed_study,
    const EntropyProviders& entropy_providers,
    const VariationsLayers& layers,
    base::FeatureList* feature_list,
    bool simulated) {
  // Since trials and features can come from many different sources (variations
  // seed, about://flags, and command line), there are special cases for when
  // they conflict with each other. See the following doc:
  // https://docs.google.com/document/d/1PAlx0KyjRwLJsmkIWlZMgZ-R422Oetgxa3ZPq0Q98aQ

  const Study& study = *processed_study.study();

  // If the trial already exists (e.g. was forced via a command line flag),
  // check if the selected group exists in the `processed_study`, and if so,
  // pick up its params, features, etc. If not, there is nothing to do here.
  // If the trial does not exist yet (most common case), then verify that it
  // does not specify features that have already been overridden (otherwise it's
  // a conflict).
  // Note: If this is a simulation, ignore these checks -- we don't want the
  // currently selected group to affect the simulation, as we may be trying to
  // simulate a new seed with groups that may differ.
  base::FieldTrial* existing_trial = base::FieldTrialList::Find(study.name());
  if (!simulated) {
    if (existing_trial) {
      int experiment_index = processed_study.GetExperimentIndexByName(
          existing_trial->GetGroupNameWithoutActivation());
      if (experiment_index == -1) {
        return nullptr;
      }

      // If the selected group exists in |processed_study|, then there may be
      // some variation ids, params, and features to pick up, so do not return
      // early. For example, if a user specifies the command line flag
      // "--force-fieldtrials=Study/Enabled" and the variations seed includes
      // a "Study" trial with an "Enabled" group that specifies features or
      // other details, then use those details, even though they were not
      // directly specified on the command line.
    } else {
      // If an experiment group in the study specifies a feature that is already
      // associated with another trial, forcibly select the
      // |kFeatureConflictGroupName| group to indicate a conflict. Usually, the
      // server-side enforces that no two studies enable/disable the same
      // feature, but this might happen from the client-side, such as through
      // flags or through the command line.
      //
      // Only check for this if the trial does not already exist. If it already
      // exists, then we cannot create the |kFeatureConflictGroupName| group for
      // it.
      for (const Study::Experiment& experiment : study.experiment()) {
        const auto& features = experiment.feature_association();
        for (const std::string& feature_name : features.enable_feature()) {
          if (feature_list->HasAssociatedFieldTrialByFeatureName(
                  feature_name)) {
            return CreateTrialWithFeatureConflictGroup(study);
          }
        }
        for (const std::string& feature_name : features.disable_feature()) {
          if (feature_list->HasAssociatedFieldTrialByFeatureName(
                  feature_name)) {
            return CreateTrialWithFeatureConflictGroup(study);
          }
        }
      }
    }
  }

  // Check if any experiments need to be forced due to a command line flag.
  // Force the first experiment with an existing flag.
  // For example, if the user disabled FeatureA via the command line, and
  // this study controls FeatureA, we should forcibly choose the group that
  // sets `forcing_feature_off`.
  base::CommandLine* command_line = base::CommandLine::ForCurrentProcess();
  for (const auto& experiment : study.experiment()) {
    if (ShouldForceExperiment(experiment, *command_line, *feature_list)) {
      base::FieldTrial* trial;
      if (!simulated) {
        // CreateFieldTrial() registers the trial it creates.
        trial = base::FieldTrialList::CreateFieldTrial(
            study.name(), experiment.name(), StudyIsLowAnonymity(study));

        // If `trial` is null, then a trial already exists and was already
        // forced to a different group (e.g. via --force-fieldtrials). In this
        // scenario, we can't set the forcing group. Break out of the loop, but
        // don't return, so that variation ids and params for the selected group
        // will still be picked up.
        if (!trial) {
          break;
        }

        if (experiment.feature_association().has_forcing_feature_on()) {
          feature_list->AssociateReportingFieldTrial(
              experiment.feature_association().forcing_feature_on(),
              base::FeatureList::OVERRIDE_ENABLE_FEATURE, trial);
        } else if (experiment.feature_association().has_forcing_feature_off()) {
          feature_list->AssociateReportingFieldTrial(
              experiment.feature_association().forcing_feature_off(),
              base::FeatureList::OVERRIDE_DISABLE_FEATURE, trial);
        }
        // Apply params and variation IDs from this forcing group (if any). Note
        // that we do not need to apply any feature state overrides, since
        // forcing groups are only forced when the feature state was already
        // overridden by e.g. the command line.
        ForceExperimentState(base::PassKey<VariationsSeedProcessor>(), study,
                             experiment, *sticky_activation_manager_, *trial);
      } else {
        // CreateSimulatedFieldTrial() does not register the trial it creates.
        // Note that we pass a total probability of 100 and an entropy value of
        // 0 (similarly to base::FieldTrialList::CreateFieldTrial()) because
        // we are not randomizing the group selection but rather forcing a
        // specific one.
        trial = base::FieldTrial::CreateSimulatedFieldTrial(
            study.name(), /*total_probability=*/100, experiment.name(),
            /*entropy_value=*/0);
      }

      if (ShouldActivate(study, experiment.name(),
                         *sticky_activation_manager_)) {
        // This call must happen after all params have been registered for the
        // trial. Otherwise, since we look up params by trial and group name,
        // the params won't be registered under the correct key.
        trial->Activate();
      }
      return trial;
    }
  }

  // This study has no randomized experiments and none of its experiments were
  // forced by flags so don't create a field trial.
  if (processed_study.total_probability() <= 0) {
    return nullptr;
  }

  base::optional_ref<const base::FieldTrial::EntropyProvider> entropy_provider =
      layers.SelectEntropyProviderForStudy(processed_study, entropy_providers);
  if (!entropy_provider.has_value()) {
    // Do not randomize because no suitable entropy provider can be applied to
    // the study.
    return nullptr;
  }

  scoped_refptr<base::FieldTrial> trial;
  if (!simulated) {
    // FactoryGetFieldTrial() registers the trial it creates.
    trial = base::FieldTrialList::FactoryGetFieldTrial(
        study.name(), processed_study.total_probability(),
        processed_study.GetDefaultExperimentName(), entropy_provider.value(),
        study.randomization_seed(), StudyIsLowAnonymity(study));
  } else {
    const double entropy_value = entropy_provider.value().GetEntropyForTrial(
        study.name(), study.randomization_seed());
    // CreateSimulatedFieldTrial() does not register the trial it creates.
    trial = base::FieldTrial::CreateSimulatedFieldTrial(
        study.name(), processed_study.total_probability(),
        processed_study.GetDefaultExperimentName(), entropy_value);
  }

  bool enables_or_disables_features = false;
  // Append the various experiment groups to the trial.
  for (const auto& experiment : study.experiment()) {
    // Groups with forcing flags have probability 0 and will never be selected.
    // Therefore, there's no need to add them to the field trial.
    if (experiment.has_forcing_flag() ||
        experiment.feature_association().has_forcing_feature_on() ||
        experiment.feature_association().has_forcing_feature_off()) {
      continue;
    }

    if (experiment.name() != study.default_experiment_name()) {
      trial->AppendGroup(experiment.name(), experiment.probability_weight());
    }

    // Register the variation IDs if this is not a simulation.
    if (!simulated) {
      bool is_trial_overridden =
          existing_trial && existing_trial->IsOverridden();
      RegisterVariationIds(base::PassKey<VariationsSeedProcessor>(), experiment,
                           study, is_trial_overridden);
    }

    if (experiment.feature_association().enable_feature_size() != 0 ||
        experiment.feature_association().disable_feature_size() != 0) {
      enables_or_disables_features = true;
    }
  }
  // Finalize the group choice.
  trial->SetForced();

  const std::string& group_name = trial->GetGroupNameWithoutActivation();
  // If this is not a simulation, then register params and feature overrides.
  if (!simulated) {
    int experiment_index = processed_study.GetExperimentIndexByName(group_name);
    // If the trial was forced on the command line, we may not be able to find
    // the experiment.
    if (experiment_index != -1) {
      const Study::Experiment& experiment = study.experiment(experiment_index);
      RegisterExperimentParams(study, experiment);
      if (enables_or_disables_features) {
        RegisterFeatureOverrides(study, experiment, trial.get(), feature_list);
      }
    }
  }

  if (ShouldActivate(study, group_name, *sticky_activation_manager_)) {
    // This call must happen after all params have been registered for the
    // trial. Otherwise, since we look up params by trial and group name, the
    // params won't be registered under the correct key.
    trial->Activate();
  }
  return trial;
}

}  // namespace variations
