// Copyright 2017 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/page_load_metrics/browser/page_load_metrics_util.h"

#include <optional>

#include "base/test/metrics/histogram_tester.h"
#include "base/test/scoped_feature_list.h"
#include "components/page_load_metrics/browser/fake_page_load_metrics_observer_delegate.h"
#include "components/page_load_metrics/browser/features.h"
#include "components/page_load_metrics/common/page_load_metrics.mojom.h"
#include "testing/gmock/include/gmock/gmock.h"
#include "testing/gtest/include/gtest/gtest.h"
#include "url/gurl.h"

namespace page_load_metrics {

class PageLoadMetricsUtilTest : public testing::Test {};

TEST_F(PageLoadMetricsUtilTest, QueryContainsComponent) {
  struct {
    bool expected_result;
    const char* query;
    const char* component;
  } test_cases[] = {
      {true, "a=b", "a=b"},
      {true, "a=b&c=d", "a=b"},
      {true, "a=b&c=d", "c=d"},
      {true, "a=b&c=d&e=f", "c=d"},
      {true, "za=b&a=b", "a=b"},
      {true, "a=bz&a=b", "a=b"},
      {true, "a=ba=b&a=b", "a=b"},
      {true, "a=a=a&a=a", "a=a"},
      {true, "source=web", "source=web"},
      {true, "a=b&source=web", "source=web"},
      {true, "a=b&source=web&c=d", "source=web"},
      {false, "a=a=a", "a=a"},
      {false, "", ""},
      {false, "a=b", ""},
      {false, "", "a=b"},
      {false, "za=b", "a=b"},
      {false, "za=bz", "a=b"},
      {false, "a=bz", "a=b"},
      {false, "za=b&c=d", "a=b"},
      {false, "a=b&c=dz", "c=d"},
      {false, "a=b&zc=d&e=f", "c=d"},
      {false, "a=b&c=dz&e=f", "c=d"},
      {false, "a=b&zc=dz&e=f", "c=d"},
      {false, "a=b&foosource=web&c=d", "source=web"},
      {false, "a=b&source=webbar&c=d", "source=web"},
      {false, "a=b&foosource=webbar&c=d", "source=web"},
      // Correctly handle cases where there is a leading "?" or "#" character.
      {true, "?a=b&source=web", "a=b"},
      {false, "a=b&?source=web", "source=web"},
  };
  for (const auto& test : test_cases) {
    EXPECT_EQ(test.expected_result, page_load_metrics::QueryContainsComponent(
                                        test.query, test.component))
        << "For query: " << test.query << " with component: " << test.component;
  }
}

TEST_F(PageLoadMetricsUtilTest, QueryContainsComponentPrefix) {
  struct {
    bool expected_result;
    const char* query;
    const char* component;
  } test_cases[] = {
      {true, "a=b", "a="},
      {true, "a=b&c=d", "a="},
      {true, "a=b&c=d", "c="},
      {true, "a=b&c=d&e=f", "c="},
      {true, "za=b&a=b", "a="},
      {true, "ba=a=b&a=b", "a="},
      {true, "q=test", "q="},
      {true, "a=b&q=test", "q="},
      {true, "q=test&c=d", "q="},
      {true, "a=b&q=test&c=d", "q="},
      {false, "", ""},
      {false, "za=b", "a="},
      {false, "za=b&c=d", "a="},
      {false, "a=b&zc=d", "c="},
      {false, "a=b&zc=d&e=f", "c="},
      {false, "a=b&zq=test&c=d", "q="},
      {false, "ba=a=b", "a="},
  };
  for (const auto& test : test_cases) {
    EXPECT_EQ(test.expected_result,
              page_load_metrics::QueryContainsComponentPrefix(test.query,
                                                              test.component))
        << "For query: " << test.query << " with component: " << test.component;
  }
}

TEST_F(PageLoadMetricsUtilTest, UmaMaxCumulativeShiftScoreHistogram) {
  constexpr char kTestMaxCumulativeShiftScoreSessionWindow[] = "Test";
  const page_load_metrics::NormalizedCLSData normalized_cls_data{0.5, false};
  base::HistogramTester histogram_tester;
  page_load_metrics::UmaMaxCumulativeShiftScoreHistogram10000x(
      kTestMaxCumulativeShiftScoreSessionWindow, normalized_cls_data);
  histogram_tester.ExpectTotalCount(kTestMaxCumulativeShiftScoreSessionWindow,
                                    1);
  histogram_tester.ExpectBucketCount(kTestMaxCumulativeShiftScoreSessionWindow,
                                     5000, 1);
}

TEST_F(PageLoadMetricsUtilTest, GetNonPrerenderingBackgroundStartTiming) {
  struct {
    PrerenderingState prerendering_state;
    std::optional<base::TimeDelta> activation_start;
    PageVisibility visibility_at_start_or_activation_;
    std::optional<base::TimeDelta> time_to_first_background;
    std::optional<base::TimeDelta> expected_result;
  } test_cases[] = {
      {PrerenderingState::kNoPrerendering, std::nullopt,
       PageVisibility::kForeground, std::nullopt, std::nullopt},
      {PrerenderingState::kNoPrerendering, std::nullopt,
       PageVisibility::kForeground, base::Seconds(2), base::Seconds(2)},
      {PrerenderingState::kNoPrerendering, std::nullopt,
       PageVisibility::kBackground, std::nullopt, base::Seconds(0)},
      {PrerenderingState::kNoPrerendering, std::nullopt,
       PageVisibility::kBackground, base::Seconds(2), base::Seconds(0)},
      {PrerenderingState::kInPrerendering, std::nullopt,
       PageVisibility::kForeground, std::nullopt, std::nullopt},
      {PrerenderingState::kInPrerendering, std::nullopt,
       PageVisibility::kForeground, base::Seconds(10), std::nullopt},
      {PrerenderingState::kActivatedNoActivationStart, std::nullopt,
       PageVisibility::kForeground, base::Seconds(12), std::nullopt},
      {PrerenderingState::kActivated, base::Seconds(10),
       PageVisibility::kForeground, std::nullopt, std::nullopt},
      {PrerenderingState::kActivated, base::Seconds(10),
       PageVisibility::kForeground, base::Seconds(12), base::Seconds(12)},
      // Invalid time_to_first_background. Not checked and may return invalid
      // value.
      {PrerenderingState::kActivated, base::Seconds(10),
       PageVisibility::kForeground, base::Seconds(2), base::Seconds(2)},
      {PrerenderingState::kActivated, base::Seconds(10),
       PageVisibility::kBackground, std::nullopt, base::Seconds(10)},
      {PrerenderingState::kActivated, base::Seconds(10),
       PageVisibility::kBackground, base::Seconds(12), base::Seconds(10)},
      // Invalid time_to_first_background. Not checked and may return invalid
      // value.
      {PrerenderingState::kActivated, base::Seconds(10),
       PageVisibility::kBackground, base::Seconds(2), base::Seconds(10)},
  };
  for (const auto& test_case : test_cases) {
    page_load_metrics::FakePageLoadMetricsObserverDelegate delegate;
    delegate.prerendering_state_ = test_case.prerendering_state;
    delegate.activation_start_ = test_case.activation_start;
    if (test_case.time_to_first_background.has_value()) {
      delegate.first_background_time_ =
          delegate.navigation_start_ +
          test_case.time_to_first_background.value();
    } else {
      delegate.first_background_time_ = std::nullopt;
    }

    switch (test_case.prerendering_state) {
      case PrerenderingState::kNoPrerendering:
        DCHECK_NE(test_case.visibility_at_start_or_activation_,
                  PageVisibility::kNotInitialized);
        delegate.started_in_foreground_ =
            (test_case.visibility_at_start_or_activation_ ==
             PageVisibility::kForeground);
        delegate.visibility_at_activation_ = PageVisibility::kNotInitialized;
        break;
      case PrerenderingState::kInPrerendering:
        delegate.started_in_foreground_ = false;
        delegate.visibility_at_activation_ = PageVisibility::kNotInitialized;
        break;
      case PrerenderingState::kActivatedNoActivationStart:
        delegate.started_in_foreground_ = false;
        delegate.visibility_at_activation_ =
            test_case.visibility_at_start_or_activation_;
        break;
      case PrerenderingState::kActivated:
        delegate.started_in_foreground_ = false;
        delegate.visibility_at_activation_ =
            test_case.visibility_at_start_or_activation_;
        break;
    }

    std::optional<base::TimeDelta> got =
        GetNonPrerenderingBackgroundStartTiming(delegate);
    EXPECT_EQ(test_case.expected_result, got);
  }
}

TEST_F(PageLoadMetricsUtilTest, CorrectEventAsNavigationOrActivationOrigined) {
  struct {
    std::string description;
    PrerenderingState prerendering_state;
    std::optional<base::TimeDelta> activation_start;
    base::TimeDelta event;
    std::optional<base::TimeDelta> expected_result;
  } test_cases[] = {
      {"Not modified", PrerenderingState::kNoPrerendering, std::nullopt,
       base::Seconds(2), base::Seconds(2)},
      {"max(0, 2 - x), where x is time of activation start that may come in "
       "the future and should be greater than an already occurred event.",
       PrerenderingState::kInPrerendering, std::nullopt, base::Seconds(2),
       base::Seconds(0)},
      {"activation start not yet available in browser, otherwise same as above",
       PrerenderingState::kActivatedNoActivationStart, std::nullopt,
       base::Seconds(2), base::Seconds(0)},
      {"crash due to incorrect data", PrerenderingState::kActivated,
       base::Seconds(10), base::Seconds(2), base::Seconds(0)},
      {"max(0, 12 - 10)", PrerenderingState::kActivated, base::Seconds(10),
       base::Seconds(12), base::Seconds(2)},
  };

  for (const auto& test_case : test_cases) {
    SCOPED_TRACE(test_case.description);
    page_load_metrics::FakePageLoadMetricsObserverDelegate delegate;
    delegate.prerendering_state_ = test_case.prerendering_state;
    delegate.activation_start_ = test_case.activation_start;

    EXPECT_EQ(test_case.expected_result,
              CorrectEventAsNavigationOrActivationOrigined(delegate,
                                                           test_case.event));
  }
}

TEST_F(PageLoadMetricsUtilTest, CalculateLCPEntropyBucket) {
  EXPECT_EQ(0, CalculateLCPEntropyBucket(0));
  EXPECT_EQ(1, CalculateLCPEntropyBucket(0.000005));
  EXPECT_EQ(17, CalculateLCPEntropyBucket(0.42));
  EXPECT_EQ(35, CalculateLCPEntropyBucket(42.0));
  EXPECT_EQ(42, CalculateLCPEntropyBucket(4200.0));
  EXPECT_EQ(43, CalculateLCPEntropyBucket(42000.0));
  EXPECT_EQ(43, CalculateLCPEntropyBucket(42000000.0));
  // These are not expected, we're just testing them for robustness.
  EXPECT_EQ(0, CalculateLCPEntropyBucket(-1));
  EXPECT_EQ(43,
            CalculateLCPEntropyBucket(std::numeric_limits<double>::infinity()));
  EXPECT_EQ(
      0, CalculateLCPEntropyBucket(std::numeric_limits<double>::quiet_NaN()));
}

// A type to support parameterized testing for the category of the request.
struct UrlCategoryTestCase {
  std::string test_case;
  std::string url_string;
  std::optional<uint32_t> expected;
};

class GetCategoryIdFromUrlTest
    : public testing::Test,
      public testing::WithParamInterface<UrlCategoryTestCase> {
 protected:
  using FeaturesType = std::vector<base::test::FeatureRefAndParams>;

  GetCategoryIdFromUrlTest() {
    static const FeaturesType enabled_features = {
        {features::kBeaconLeakageLogging,
         {{"category_prefix", "test-prefix"}}}};
    scoped_feature_list_.InitWithFeaturesAndParameters(enabled_features, {});
  }
  ~GetCategoryIdFromUrlTest() override = default;

 private:
  base::test::ScopedFeatureList scoped_feature_list_;
};

INSTANTIATE_TEST_SUITE_P(
    All,
    GetCategoryIdFromUrlTest,
    testing::ValuesIn<UrlCategoryTestCase>({
        {"EmptyCategory", "", std::nullopt},
        {"InvalidCategory", "https://a.com?category=invalid-category",
         std::nullopt},
        {"ValidCategory0", "https://a.com?category=test-prefix0",
         std::make_optional(0u)},
        {"ValidCategory1", "https://a.com?param1=true&category=test-prefix1",
         std::make_optional(1u)},
        {"ValidCategory200", "https://a.com?category=test-prefix200",
         std::make_optional(200u)},
    }),
    [](const testing::TestParamInfo<UrlCategoryTestCase>& info) {
      return info.param.test_case;
    });

TEST_P(GetCategoryIdFromUrlTest, GetCategoryIdFromUrl) {
  EXPECT_THAT(GetCategoryIdFromUrl(GURL(GetParam().url_string)),
              testing::Eq(GetParam().expected));
}

}  // namespace page_load_metrics
