// Copyright 2011 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/about_flags.h"

#include <stddef.h>

#include <map>
#include <optional>
#include <set>
#include <string>
#include <utility>

#include "base/containers/span.h"
#include "base/strings/string_number_conversions.h"
#include "base/strings/string_util.h"
#include "base/strings/stringprintf.h"
#include "base/test/metrics/histogram_enum_reader.h"
#include "build/build_config.h"
#include "chrome/common/chrome_version.h"
#include "components/webui/flags/feature_entry.h"
#include "components/webui/flags/feature_entry_macros.h"
#include "components/webui/flags/flags_test_helpers.h"
#include "components/webui/flags/flags_ui_metrics.h"
#include "testing/gtest/include/gtest/gtest.h"

namespace about_flags {

namespace {

using Sample32 = base::HistogramBase::Sample32;
using SwitchToIdMap = std::map<std::string, Sample32>;

// Get all associated switches corresponding to defined about_flags.cc entries.
std::set<std::string> GetAllPublicSwitchesAndFeaturesForTesting() {
  std::set<std::string> result;

  for (const auto& entry : testing::GetFeatureEntries()) {
    // Skip over flags that are part of the flags system itself - they don't
    // have any of the usual metadata or histogram entries for flags, since they
    // are synthesized during the build process.
    // TODO(crbug.com/40125404): Remove the need for this by generating
    // histogram entries automatically.
    if (entry.supported_platforms & flags_ui::kFlagInfrastructure)
      continue;

    switch (entry.type) {
      case flags_ui::FeatureEntry::SINGLE_VALUE:
      case flags_ui::FeatureEntry::SINGLE_DISABLE_VALUE:
        result.insert(entry.switches.command_line_switch);
        break;
      case flags_ui::FeatureEntry::ORIGIN_LIST_VALUE:
      case flags_ui::FeatureEntry::STRING_VALUE:
        // Do nothing, origin list values and string values are not added as
        // feature flags.
        break;
      case flags_ui::FeatureEntry::MULTI_VALUE:
        for (int j = 0; j < entry.NumOptions(); ++j) {
          result.insert(entry.ChoiceForOption(j).command_line_switch);
        }
        break;
      case flags_ui::FeatureEntry::ENABLE_DISABLE_VALUE:
        result.insert(entry.switches.command_line_switch);
        result.insert(entry.switches.disable_command_line_switch);
        break;
      case flags_ui::FeatureEntry::FEATURE_VALUE:
      case flags_ui::FeatureEntry::FEATURE_WITH_PARAMS_VALUE:
        result.insert(std::string(entry.feature.feature->name) + ":enabled");
        result.insert(std::string(entry.feature.feature->name) + ":disabled");
        break;
#if BUILDFLAG(IS_CHROMEOS)
      case flags_ui::FeatureEntry::PLATFORM_FEATURE_NAME_VALUE:
      case flags_ui::FeatureEntry::PLATFORM_FEATURE_NAME_WITH_PARAMS_VALUE:
        std::string name(entry.platform_feature_name.name);
        result.insert(name + ":enabled");
        result.insert(name + ":disabled");
        break;
#endif  // BUILDFLAG(IS_CHROMEOS)
    }
  }
  return result;
}

// Returns all variation ids defined in flags entries.
std::vector<std::string> GetAllVariationIds() {
  std::vector<std::string> variation_ids;
  for (const auto& entry : testing::GetFeatureEntries()) {
    // Only FEATURE_WITH_PARAMS_VALUE or PLATFORM_FEATURE_NAME_WITH_PARAMS_VALUE
    // entries can have a variation id.
    if (entry.type != flags_ui::FeatureEntry::FEATURE_WITH_PARAMS_VALUE
#if BUILDFLAG(IS_CHROMEOS)
        && entry.type !=
               flags_ui::FeatureEntry::PLATFORM_FEATURE_NAME_WITH_PARAMS_VALUE
#endif  // BUILDFLAG(IS_CHROMEOS)
    ) {
      continue;
    }

    for (const auto& variation : entry.GetVariations()) {
      if (variation.variation_id)
        variation_ids.push_back(variation.variation_id);
    }
  }
  return variation_ids;
}

// Returns the parsed pair: <variation_id, is_triggering>.
std::pair<int, bool> ParseVariationId(const std::string& variation_str) {
  // Fail if an empty string has been supplied as variation_id.
  EXPECT_FALSE(variation_str.empty())
      << "Empty string used to denote variation ID. Use `nullptr` instead.";

  int variation_id{};
  bool is_triggering = variation_str[0] == 't';

  // Fail if we could not process the integer value.
  EXPECT_TRUE(
      base::StringToInt(&variation_str[is_triggering ? 1 : 0], &variation_id))
      << "Invalid variation string: \"" << variation_str
      << "\": must be either `#######` or `t#######`";

  return {variation_id, is_triggering};
}

}  // namespace

// Makes sure there are no separators in any of the entry names.
TEST(AboutFlagsTest, NoSeparators) {
  for (const auto& entry : testing::GetFeatureEntries()) {
    const std::string name(entry.internal_name);
    EXPECT_EQ(std::string::npos, name.find(flags_ui::testing::kMultiSeparator))
        << name;
  }
}

// Makes sure that every flag has an owner and an expiry entry in
// flag-metadata.json.
TEST(AboutFlagsTest, EveryFlagHasMetadata) {
  flags_ui::testing::EnsureEveryFlagHasMetadata(testing::GetFeatureEntries());
}

// Ensures that all flags marked as never expiring in flag-metadata.json is
// listed in flag-never-expire-list.json.
TEST(AboutFlagsTest, OnlyPermittedFlagsNeverExpire) {
  flags_ui::testing::EnsureOnlyPermittedFlagsNeverExpire();
}

// Ensures that every flag has an owner.
TEST(AboutFlagsTest, EveryFlagHasNonEmptyOwners) {
  flags_ui::testing::EnsureEveryFlagHasNonEmptyOwners();
}

// Ensures that owners conform to rules in flag-metadata.json.
TEST(AboutFlagsTest, OwnersLookValid) {
  flags_ui::testing::EnsureOwnersLookValid();
}

// Ensures that every flag in `flag-never-expire-list.json` has a matching entry
// in `flag-metadata.json`.
TEST(AboutFlagsTest, NeverExpireFlagsExist) {
  flags_ui::testing::EnsureNeverExpireFlagsExist();
}

// For some bizarre reason, far too many people see a file filled with
// alphabetically-ordered items and think "hey, let me drop this new item into a
// random location!" Prohibit such behavior in the flags files.
TEST(AboutFlagsTest, FlagsListedInAlphabeticalOrder) {
  flags_ui::testing::EnsureFlagsAreListedInAlphabeticalOrder();
}

TEST(AboutFlagsTest, EveryFlagIsValid) {
  for (const auto& entry : testing::GetFeatureEntries()) {
    EXPECT_TRUE(entry.IsValid()) << entry.internal_name;
  }
}

TEST(AboutFlagsTest, RecentUnexpireFlagsArePresent) {
  flags_ui::testing::EnsureRecentUnexpireFlagsArePresent(
      testing::GetFeatureEntries(), CHROME_VERSION_MAJOR);
}

// Ensures that all variation IDs specified are well-formed.
// - Variation IDs may be re-used, when multiple variants change client-side
//   behavior alone.
// - Variation IDs must be associated with the appropriate pool of valid numbers
TEST(AboutFlagsTest, VariationIdsAreValid) {
  std::set<int> nontriggering_variation_ids;
  std::set<int> triggering_variation_ids;

  struct VariationIdRange {
    int start;
    int end;

    bool Contains(int id) const { return id >= start && id <= end; }
    std::string ToString() const {
      return base::StringPrintf("[%d, %d]", start, end);
    }
  };
  // See: go/finch-allocating-gws-ids.
  const VariationIdRange kVariationIdRange1{3340000, 3399999};
  const VariationIdRange kVariationIdRange2{101000000, 101099999};

  for (const std::string& variation_str : GetAllVariationIds()) {
    auto [variation_id, is_triggering] = ParseVariationId(variation_str);
    // Reject variation IDs used both as triggering and non-triggering.
    // This is generally considered invalid.
    EXPECT_FALSE(
        // Triggering, but already recorded as visible.
        (is_triggering && nontriggering_variation_ids.contains(variation_id)) ||
        // Visible, but already recorded as triggering.
        (!is_triggering && triggering_variation_ids.contains(variation_id)))
        << "Variation ID \"" << variation_id
        << "\" used both as triggering and non-triggering.";

    EXPECT_TRUE(kVariationIdRange1.Contains(variation_id) ||
                kVariationIdRange2.Contains(variation_id))
        << "Variation ID \"" << variation_id << "\" falls outside of ranges of "
        << "valid variation IDs: " << kVariationIdRange1.ToString() << " and "
        << kVariationIdRange2.ToString() << ".";

    if (is_triggering) {
      triggering_variation_ids.insert(variation_id);
    } else {
      nontriggering_variation_ids.insert(variation_id);
    }
  }
}

// Test that ScopedFeatureEntries restores existing feature entries on
// destruction.
TEST(AboutFlagsTest, ScopedFeatureEntriesRestoresFeatureEntries) {
  const base::span<const flags_ui::FeatureEntry> old_entries =
      testing::GetFeatureEntries();
  EXPECT_FALSE(old_entries.empty());
  const char* first_feature_name = old_entries[0].internal_name;
  {
    static BASE_FEATURE(kTestFeature1, "FeatureName1",
                        base::FEATURE_ENABLED_BY_DEFAULT);
    testing::ScopedFeatureEntries feature_entries(
        {{"feature-1", "", "", flags_ui::FlagsState::GetCurrentPlatform(),
          FEATURE_VALUE_TYPE(kTestFeature1)}});
    EXPECT_EQ(testing::GetFeatureEntries().size(), 1U);
  }

  const base::span<const flags_ui::FeatureEntry> new_entries =
      testing::GetFeatureEntries();
  EXPECT_EQ(old_entries.size(), new_entries.size());
  EXPECT_TRUE(about_flags::GetCurrentFlagsState()->FindFeatureEntryByName(
      first_feature_name));
}

class AboutFlagsHistogramTest : public ::testing::Test {
 protected:
  // This is a helper function to check that all IDs in enum LoginCustomFlags in
  // histograms.xml are unique.
  void SetSwitchToHistogramIdMapping(const std::string& switch_name,
                                     const Sample32 switch_histogram_id,
                                     std::map<std::string, Sample32>* out_map) {
    const std::pair<std::map<std::string, Sample32>::iterator, bool> status =
        out_map->insert(std::make_pair(switch_name, switch_histogram_id));
    if (!status.second) {
      EXPECT_TRUE(status.first->second == switch_histogram_id)
          << "Duplicate switch '" << switch_name
          << "' found in enum 'LoginCustomFlags' in "
             "tools/metrics/histograms/enums.xml.";
    }
  }

  // This method generates a hint for the user for what string should be added
  // to the enum LoginCustomFlags to make in consistent.
  std::string GetHistogramEnumEntryText(const std::string& switch_name,
                                        Sample32 value) {
    return base::StringPrintf(
        "<int value=\"%d\" label=\"%s\"/>", value, switch_name.c_str());
  }
};

TEST_F(AboutFlagsHistogramTest, CheckHistograms) {
  std::optional<base::HistogramEnumEntryMap> login_custom_flags =
      base::ReadEnumFromEnumsXml("LoginCustomFlags");
  ASSERT_TRUE(login_custom_flags)
      << "Error reading enum 'LoginCustomFlags' from "
         "tools/metrics/histograms/enums.xml.";

  // Build reverse map {switch_name => id} from login_custom_flags.
  SwitchToIdMap metadata_switches_ids;

  EXPECT_TRUE(
      login_custom_flags->count(flags_ui::testing::kBadSwitchFormatHistogramId))
      << "Entry for UMA ID of incorrect command-line flag is not found in "
         "tools/metrics/histograms/enums.xml enum LoginCustomFlags. "
         "Consider adding entry:\n"
      << "  " << GetHistogramEnumEntryText("BAD_FLAG_FORMAT", 0);
  // Check that all LoginCustomFlags entries have correct values.
  for (const auto& entry : *login_custom_flags) {
    if (entry.first == flags_ui::testing::kBadSwitchFormatHistogramId) {
      // Add error value with empty name.
      SetSwitchToHistogramIdMapping(std::string(), entry.first,
                                    &metadata_switches_ids);
      continue;
    }
    const Sample32 uma_id = flags_ui::GetSwitchUMAId(entry.second);
    EXPECT_EQ(uma_id, entry.first)
        << "tools/metrics/histograms/enums.xml enum LoginCustomFlags "
           "entry '"
        << entry.second << "' has incorrect value=" << entry.first << ", but "
        << uma_id << " is expected. Consider changing entry to:\n"
        << "  " << GetHistogramEnumEntryText(entry.second, uma_id);
    SetSwitchToHistogramIdMapping(entry.second, entry.first,
                                  &metadata_switches_ids);
  }

  // Check that all flags in about_flags.cc have entries in login_custom_flags.
  std::set<std::string> all_flags = GetAllPublicSwitchesAndFeaturesForTesting();
  for (const std::string& flag : all_flags) {
    // Skip empty placeholders.
    if (flag.empty())
      continue;
    const Sample32 uma_id = flags_ui::GetSwitchUMAId(flag);
    EXPECT_NE(flags_ui::testing::kBadSwitchFormatHistogramId, uma_id)
        << "Command-line switch '" << flag
        << "' from about_flags.cc has UMA ID equal to reserved value "
           "kBadSwitchFormatHistogramId="
        << flags_ui::testing::kBadSwitchFormatHistogramId
        << ". Please modify switch name.";
    auto enum_entry = metadata_switches_ids.lower_bound(flag);

    // Ignore case here when switch ID is incorrect - it has already been
    // reported in the previous loop.
    EXPECT_TRUE(enum_entry != metadata_switches_ids.end() &&
                enum_entry->first == flag)
        << "tools/metrics/histograms/enums.xml enum LoginCustomFlags doesn't "
           "contain switch '"
        << flag << "' (value=" << uma_id << " expected). Consider running:\n"
        << "  tools/metrics/histograms/generate_flag_enums.py --feature "
        << flag.substr(0, flag.find(":")) << "\nOr manually adding the entry:\n"
        << "  " << GetHistogramEnumEntryText(flag, uma_id);
  }
}

}  // namespace about_flags
