// 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.

#include "content/public/test/preloading_test_util.h"

#include "base/strings/string_util.h"
#include "base/strings/stringprintf.h"
#include "content/browser/preloading/preloading_attempt_impl.h"
#include "content/browser/preloading/preloading_config.h"
#include "content/browser/preloading/preloading_data_impl.h"
#include "preloading_test_util.h"
#include "services/metrics/public/cpp/metrics_utils.h"
#include "services/metrics/public/cpp/ukm_builders.h"
#include "testing/gmock/include/gmock/gmock.h"
#include "testing/gtest/include/gtest/gtest.h"

namespace content::test {
namespace {

constexpr char kAddSpeculationRuleScript[] = R"({
    const script = document.createElement('script');
    script.type = 'speculationrules';
    script.text = `{
      "$1": [{ $2 }]
    }`;
    document.head.appendChild(script);
  })";

constexpr char kAddSpeculationRuleWithRulesetTagScript[] = R"({
    const script = document.createElement('script');
    script.type = 'speculationrules';
    script.text = `{
      "tag": "$1",
      "$2": [{ $3 }]
    }`;
    document.head.appendChild(script);
  })";

}  // namespace

std::string ConvertEagernessToString(
    blink::mojom::SpeculationEagerness eagerness) {
  switch (eagerness) {
    case blink::mojom::SpeculationEagerness::kImmediate:
      return "immediate";
    case blink::mojom::SpeculationEagerness::kEager:
      return "eager";
    case blink::mojom::SpeculationEagerness::kModerate:
      return "moderate";
    case blink::mojom::SpeculationEagerness::kConservative:
      return "conservative";
  }
}

using UkmEntry = ukm::TestUkmRecorder::HumanReadableUkmEntry;
using Preloading_Attempt = ukm::builders::Preloading_Attempt;
using Preloading_Prediction = ukm::builders::Preloading_Prediction;

const std::vector<std::string> kPreloadingAttemptUkmMetrics{
    Preloading_Attempt::kPreloadingTypeName,
    Preloading_Attempt::kPreloadingPredictorName,
    Preloading_Attempt::kEligibilityName,
    Preloading_Attempt::kHoldbackStatusName,
    Preloading_Attempt::kTriggeringOutcomeName,
    Preloading_Attempt::kFailureReasonName,
    Preloading_Attempt::kAccurateTriggeringName,
    Preloading_Attempt::kReadyTimeName,
    Preloading_Attempt::kTimeToNextNavigationName,
    Preloading_Attempt::kSpeculationEagernessName,
};

const std::vector<std::string> kPreloadingPredictionUkmMetrics{
    Preloading_Prediction::kPreloadingPredictorName,
    Preloading_Prediction::kConfidenceName,
    Preloading_Prediction::kAccuratePredictionName,
    Preloading_Prediction::kTimeToNextNavigationName,
};

PreloadingAttemptUkmEntryBuilder::PreloadingAttemptUkmEntryBuilder(
    PreloadingPredictor predictor)
    : predictor_(predictor) {}

UkmEntry PreloadingAttemptUkmEntryBuilder::BuildEntry(
    ukm::SourceId source_id,
    PreloadingType preloading_type,
    PreloadingEligibility eligibility,
    PreloadingHoldbackStatus holdback_status,
    PreloadingTriggeringOutcome triggering_outcome,
    PreloadingFailureReason failure_reason,
    bool accurate,
    std::optional<base::TimeDelta> ready_time,
    std::optional<blink::mojom::SpeculationEagerness> eagerness) const {
  std::map<std::string, int64_t> metrics = {
      {Preloading_Attempt::kPreloadingTypeName,
       static_cast<int64_t>(preloading_type)},
      {Preloading_Attempt::kPreloadingPredictorName, predictor_.ukm_value()},
      {Preloading_Attempt::kEligibilityName, static_cast<int64_t>(eligibility)},
      {Preloading_Attempt::kHoldbackStatusName,
       static_cast<int64_t>(holdback_status)},
      {Preloading_Attempt::kTriggeringOutcomeName,
       static_cast<int64_t>(triggering_outcome)},
      {Preloading_Attempt::kFailureReasonName,
       static_cast<int64_t>(failure_reason)},
      {Preloading_Attempt::kAccurateTriggeringName, accurate ? 1 : 0},
      {Preloading_Attempt::kTimeToNextNavigationName,
       ukm::GetExponentialBucketMinForCounts1000(
           base::ScopedMockElapsedTimersForTest::kMockElapsedTime
               .InMilliseconds())}};
  if (ready_time) {
    metrics.insert({Preloading_Attempt::kReadyTimeName,
                    ukm::GetExponentialBucketMinForCounts1000(
                        ready_time->InMilliseconds())});
  }
  if (eagerness) {
    metrics.insert({Preloading_Attempt::kSpeculationEagernessName,
                    static_cast<int64_t>(eagerness.value())});
  }
  return UkmEntry{source_id, std::move(metrics)};
}

PreloadingPredictionUkmEntryBuilder::PreloadingPredictionUkmEntryBuilder(
    PreloadingPredictor predictor)
    : predictor_(predictor) {}

UkmEntry PreloadingPredictionUkmEntryBuilder::BuildEntry(
    ukm::SourceId source_id,
    int64_t confidence,
    bool accurate_prediction) const {
  return UkmEntry{source_id,
                  {{Preloading_Prediction::kPreloadingPredictorName,
                    predictor_.ukm_value()},
                   {Preloading_Prediction::kConfidenceName, confidence},
                   {Preloading_Prediction::kAccuratePredictionName,
                    accurate_prediction ? 1 : 0},
                   {Preloading_Prediction::kTimeToNextNavigationName,
                    ukm::GetExponentialBucketMinForCounts1000(
                        base::ScopedMockElapsedTimersForTest::kMockElapsedTime
                            .InMilliseconds())}}};
}

void ExpectPreloadingAttemptUkm(
    const ukm::TestAutoSetUkmRecorder& ukm_recorder,
    const std::vector<ukm::TestUkmRecorder::HumanReadableUkmEntry>&
        expected_attempt_entries) {
  auto attempt_entries = ukm_recorder.GetEntries(
      Preloading_Attempt::kEntryName, test::kPreloadingAttemptUkmMetrics);
  EXPECT_EQ(attempt_entries.size(), expected_attempt_entries.size());
  EXPECT_THAT(attempt_entries,
              testing::UnorderedElementsAreArray(expected_attempt_entries))
      << test::ActualVsExpectedUkmEntriesToString(attempt_entries,
                                                  expected_attempt_entries);
}

void ExpectPreloadingPredictionUkm(
    const ukm::TestAutoSetUkmRecorder& ukm_recorder,
    const std::vector<ukm::TestUkmRecorder::HumanReadableUkmEntry>&
        expected_prediction_entries) {
  auto prediction_entries = ukm_recorder.GetEntries(
      Preloading_Prediction::kEntryName, test::kPreloadingPredictionUkmMetrics);
  EXPECT_EQ(prediction_entries.size(), expected_prediction_entries.size());
  EXPECT_THAT(prediction_entries,
              testing::UnorderedElementsAreArray(expected_prediction_entries))
      << test::ActualVsExpectedUkmEntriesToString(prediction_entries,
                                                  expected_prediction_entries);
}

std::string UkmEntryToString(const UkmEntry& entry) {
  std::string result;
  result +=
      base::StringPrintf("Source ID: %d\n", static_cast<int>(entry.source_id));
  for (const auto& metric : entry.metrics) {
    result += base::StringPrintf("Metric '%s' = %d\n", metric.first.c_str(),
                                 static_cast<int>(metric.second));
  }
  result += "\n";
  return result;
}

std::string ActualVsExpectedUkmEntryToString(const UkmEntry& actual,
                                             const UkmEntry& expected) {
  std::string result = "Actual UKM entry:\n";
  result += UkmEntryToString(actual);
  result += "Expected UKM entry:\n";
  result += UkmEntryToString(expected);
  return result;
}

std::string ActualVsExpectedUkmEntriesToString(
    const std::vector<UkmEntry>& actual,
    const std::vector<UkmEntry>& expected) {
  std::string result = "Actual UKM entries:\n";
  for (auto entry : actual) {
    result += UkmEntryToString(entry);
  }
  result += "Expected UKM entries:\n";
  for (auto entry : expected) {
    result += UkmEntryToString(entry);
  }
  return result;
}

PreloadingAttemptAccessor::PreloadingAttemptAccessor(
    PreloadingAttempt* preloading_attempt)
    : preloading_attempt_(preloading_attempt) {}

PreloadingTriggeringOutcome PreloadingAttemptAccessor::GetTriggeringOutcome() {
  return static_cast<PreloadingAttemptImpl*>(preloading_attempt_)
      ->triggering_outcome_;
}

PreloadingFailureReason PreloadingAttemptAccessor::GetFailureReason() {
  return static_cast<PreloadingAttemptImpl*>(preloading_attempt_)
      ->failure_reason_;
}

PreloadingConfigOverride::PreloadingConfigOverride() {
  preloading_config_ = std::make_unique<PreloadingConfig>();
  overridden_config_ =
      PreloadingConfig::OverrideForTesting(preloading_config_.get());
}

PreloadingConfigOverride::~PreloadingConfigOverride() {
  raw_ptr<PreloadingConfig> uninstalled_override =
      PreloadingConfig::OverrideForTesting(overridden_config_);
  // Make sure the override we uninstalled is the one we installed in the
  // constructor.
  CHECK_EQ(uninstalled_override.get(), preloading_config_.get());
}

void PreloadingConfigOverride::SetHoldback(PreloadingType preloading_type,
                                           PreloadingPredictor predictor,
                                           bool holdback) {
  preloading_config_->SetHoldbackForTesting(preloading_type, predictor,
                                            holdback);
}

void PreloadingConfigOverride::SetHoldback(std::string_view preloading_type,
                                           std::string_view predictor,
                                           bool holdback) {
  preloading_config_->SetHoldbackForTesting(preloading_type, predictor,
                                            holdback);
}

void SetHasSpeculationRulesPrerender(PreloadingData* preloading_data) {
  static_cast<PreloadingDataImpl*>(preloading_data)
      ->SetHasSpeculationRulesPrerender();
}

std::string BuildScriptElementSpeculationRules(
    const std::string& action,
    const std::vector<GURL>& urls,
    std::optional<blink::mojom::SpeculationEagerness> eagerness,
    std::optional<std::string> no_vary_search_hint,
    const std::string& target_hint,
    std::optional<std::string> ruleset_tag,
    std::optional<bool> form_submission) {
  if (action == "prefetch") {
    CHECK(target_hint.empty()) << "prefetch rules should not have target_hint";
    CHECK(!form_submission.has_value())
        << "prefetch rules should not have form_submission";
  }

  std::stringstream ss;

  // Add source field.
  ss << R"("source": "list", )";

  // Concatenate the given URLs with a comma separator.
  std::stringstream urls_ss;
  for (size_t i = 0; i < urls.size(); i++) {
    // Wrap the url with double quotes.
    urls_ss << base::StringPrintf(R"("%s")", urls[i].spec().c_str());
    if (i + 1 < urls.size()) {
      urls_ss << ", ";
    }
  }
  // Add urls fields.
  ss << base::StringPrintf(R"("urls": [ %s ])", urls_ss.str().c_str());

  // Add eagerness field.
  if (eagerness.has_value()) {
    ss << base::StringPrintf(
        R"(, "eagerness": "%s")",
        ConvertEagernessToString(eagerness.value()).c_str());
  }
  if (no_vary_search_hint.has_value()) {
    ss << base::StringPrintf(R"(, "expects_no_vary_search": "%s")",
                             no_vary_search_hint.value().c_str());
  }

  // Add target_hint field.
  if (!target_hint.empty() && action != "prefetch") {
    ss << base::StringPrintf(R"(, "target_hint": "%s")", target_hint.c_str());
  }

  if (form_submission.has_value() && action != "prefetch") {
    if (form_submission.value()) {
      ss << R"(, "form_submission": true)";
    } else {
      ss << R"(, "form_submission": false)";
    }
  }

  return ruleset_tag.has_value()
             ? base::ReplaceStringPlaceholders(
                   kAddSpeculationRuleWithRulesetTagScript,
                   {ruleset_tag.value(), action, ss.str()}, nullptr)
             : base::ReplaceStringPlaceholders(kAddSpeculationRuleScript,
                                               {action, ss.str()}, nullptr);
}

size_t GetPreloadingAttemptsCount(WebContents* web_contents) {
  PreloadingDataImpl* preloading_data =
      PreloadingDataImpl::FromWebContents(web_contents);
  return preloading_data ? preloading_data->GetAttemptsSizeForTesting() : 0;
}

}  // namespace content::test
