// Copyright 2025 The Chromium Authors
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.

#include "net/http/no_vary_search_cache.h"

#include <algorithm>
#include <array>
#include <optional>
#include <string>
#include <string_view>
#include <utility>

#include "base/memory/raw_ptr.h"
#include "base/memory/raw_ref.h"
#include "base/pickle.h"
#include "base/strings/strcat.h"
#include "base/strings/string_number_conversions.h"
#include "base/strings/string_util.h"
#include "base/test/scoped_feature_list.h"
#include "base/test/task_environment.h"
#include "base/threading/platform_thread.h"
#include "base/time/time.h"
#include "net/base/features.h"
#include "net/base/load_flags.h"
#include "net/base/network_isolation_key.h"
#include "net/base/pickle.h"
#include "net/base/pickle_traits.h"
#include "net/base/schemeful_site.h"
#include "net/http/http_cache.h"
#include "net/http/http_response_headers.h"
#include "net/http/no_vary_search_cache_test_utils.h"
#include "testing/gmock/include/gmock/gmock.h"
#include "testing/gtest/include/gtest/gtest.h"
#include "url/gurl.h"

namespace net {

namespace {

namespace nvs_test = no_vary_search_cache_test_utils;

using ::testing::_;
using ::testing::AllOf;
using ::testing::EndsWith;
using ::testing::Eq;
using ::testing::Ge;
using ::testing::InSequence;
using ::testing::Le;
using ::testing::Optional;

constexpr size_t kMaxSize = 5;

class NoVarySearchCacheTest : public ::testing::TestWithParam<bool> {
 public:
  NoVarySearchCacheTest() {
    feature_list_.InitWithFeatureState(
        features::kSplitCacheByNetworkIsolationKey, GetParam());
  }

 protected:
  NoVarySearchCache& cache() { return cache_; }

  // Generates a URL with the query `query`.
  static GURL TestURL(std::string_view query = {}) {
    return nvs_test::CreateTestURL(query);
  }

  // Generates an HttpRequestInfo object containing a URL that has the query
  // `query`.
  static HttpRequestInfo TestRequest(std::string_view query = {}) {
    return nvs_test::CreateTestRequest(query);
  }

  // Generates an HttpRequestInfo object with the URL `url`.
  static HttpRequestInfo TestRequest(const GURL& url) {
    return nvs_test::CreateTestRequest(url);
  }

  // Generates an HttpRequestInfo object with the given `url` and `nik`.
  static HttpRequestInfo TestRequest(const GURL& url,
                                     const NetworkIsolationKey& nik) {
    return nvs_test::CreateTestRequest(url, nik);
  }

  // Returns a reference to an HttpResponseHeaders object with a No-Vary-Search
  // header with the value `no_vary_search`.
  const HttpResponseHeaders& TestHeaders(std::string_view no_vary_search) {
    response_header_storage_.push_back(
        nvs_test::CreateTestHeaders(no_vary_search));
    return *response_header_storage_.back();
  }

  // Inserts a URL with query `query` into cache with a No-Vary-Search header
  // value of `no_vary_search`.
  void Insert(std::string_view query, std::string_view no_vary_search) {
    nvs_test::Insert(cache_, query, no_vary_search);
  }

  // Returns true if TestURL(query) exists in cache.
  bool Exists(std::string_view query) {
    return nvs_test::Exists(cache_, query);
  }

  // Returns true if inserting a request with query `insert` results in a lookup
  // for query `lookup` succeeding, assuming a No-Vary-Search header value of
  // `no_vary_search`.
  bool Matches(std::string_view insert,
               std::string_view lookup,
               std::string_view no_vary_search) {
    NoVarySearchCache cache(kMaxSize);

    cache.MaybeInsert(TestRequest(insert), TestHeaders(no_vary_search));
    EXPECT_EQ(cache.size(), 1u);

    const auto exists = [&cache](std::string_view query) {
      return cache.Lookup(TestRequest(query)).has_value();
    };

    // It would be bad if the query didn't match itself.
    EXPECT_TRUE(exists(insert));

    return exists(lookup);
  }

  // Returns true if inserting a request with query `insert2` after a request
  // for `insert1` results in the new request replacing the old one, meaning
  // they were match according to the No-Vary-Search header value of
  // `no_vary_search`.
  bool InsertMatches(std::string_view insert1,
                     std::string_view insert2,
                     std::string_view no_vary_search) {
    NoVarySearchCache cache(kMaxSize);
    const auto insert = [&cache, no_vary_search, this](std::string_view query) {
      cache.MaybeInsert(TestRequest(query), TestHeaders(no_vary_search));
    };

    insert(insert1);
    EXPECT_EQ(cache.size(), 1u);
    insert(insert2);
    return cache.size() == 1u;
  }

  std::string GenerateCachePartitionKey(std::string_view url) {
    const auto request = TestRequest(GURL(url));
    std::optional<std::string> maybe_cache_key =
        HttpCache::GenerateCachePartitionKeyForRequest(request);
    EXPECT_TRUE(maybe_cache_key);
    return maybe_cache_key.value_or(std::string());
  }

 private:
  base::test::ScopedFeatureList feature_list_;
  NoVarySearchCache cache_{kMaxSize};

  // Scratch space for HttpResponseHeaders objects so that TestHeaders() can
  // return a const reference for convenience. This is thrown away at the end of
  // each test.
  std::vector<scoped_refptr<HttpResponseHeaders>> response_header_storage_;
};

INSTANTIATE_TEST_SUITE_P(All,
                         NoVarySearchCacheTest,
                         ::testing::Bool(),
                         [](const testing::TestParamInfo<bool>& info) {
                           return info.param ? "SplitCacheEnabled"
                                             : "SplitCacheDisabled";
                         });

TEST_P(NoVarySearchCacheTest, NewlyConstructedCacheIsEmpty) {
  EXPECT_EQ(cache().size(), 0u);
}

TEST_P(NoVarySearchCacheTest, LookupOnEmptyCache) {
  EXPECT_EQ(cache().Lookup(TestRequest()), std::nullopt);
}

TEST_P(NoVarySearchCacheTest, InsertLookupErase) {
  Insert("", "key-order");

  auto result = cache().Lookup(TestRequest());
  ASSERT_TRUE(result);
  EXPECT_EQ(result->original_url, TestURL());

  EXPECT_EQ(cache().size(), 1u);

  cache().Erase(std::move(result->erase_handle));
  EXPECT_EQ(cache().size(), 0u);
  EXPECT_TRUE(cache().IsTopLevelMapEmptyForTesting());
}

TEST_P(NoVarySearchCacheTest, MoveConstruct) {
  Insert("a=b", "key-order");

  NoVarySearchCache new_cache = std::move(cache());

  EXPECT_TRUE(new_cache.Lookup(TestRequest("a=b")));

  // NOLINTNEXTLINE(bugprone-use-after-move)
  EXPECT_EQ(cache().size(), 0u);
}

// An asan build will find leaks, but this test works on any build.
TEST_P(NoVarySearchCacheTest, QueryNotLeaked) {
  std::optional<NoVarySearchCache::LookupResult> result;
  {
    NoVarySearchCache cache(kMaxSize);

    cache.MaybeInsert(TestRequest(), TestHeaders("params"));
    result = cache.Lookup(TestRequest());
    ASSERT_TRUE(result);
    EXPECT_FALSE(result->erase_handle.IsGoneForTesting());
  }
  EXPECT_TRUE(result->erase_handle.IsGoneForTesting());
}

std::string QueryWithIParameter(size_t i) {
  return "i=" + base::NumberToString(i);
}

constexpr std::string_view kVaryOnIParameter = "params, except=(\"i\")";

TEST_P(NoVarySearchCacheTest, OldestItemIsEvicted) {
  for (size_t i = 0; i < kMaxSize + 1; ++i) {
    std::string query = QueryWithIParameter(i);
    Insert(query, kVaryOnIParameter);
    EXPECT_TRUE(Exists(query));
  }

  EXPECT_EQ(cache().size(), kMaxSize);

  EXPECT_FALSE(Exists("i=0"));
}

TEST_P(NoVarySearchCacheTest, RecentlyUsedItemIsNotEvicted) {
  for (size_t i = 0; i < kMaxSize + 1; ++i) {
    std::string query = QueryWithIParameter(i);
    Insert(query, kVaryOnIParameter);
    EXPECT_TRUE(Exists(query));
    // Exists() calls Lookup(), which makes an entry "used".
    EXPECT_TRUE(Exists("i=0"));
  }

  EXPECT_EQ(cache().size(), kMaxSize);

  EXPECT_TRUE(Exists("i=0"));
  EXPECT_FALSE(Exists("i=1"));
}

TEST_P(NoVarySearchCacheTest, MostRecentlyUsedItemIsNotEvicted) {
  const auto query = QueryWithIParameter;
  // Fill the cache.
  for (size_t i = 0; i < kMaxSize; ++i) {
    Insert(query(i), kVaryOnIParameter);
  }
  EXPECT_EQ(cache().size(), kMaxSize);

  // Make "i=3" be the most recently used item.
  EXPECT_TRUE(Exists("i=3"));

  // Evict kMaxSize - 1 items.
  for (size_t i = kMaxSize; i < kMaxSize * 2 - 1; ++i) {
    Insert(query(i), kVaryOnIParameter);
    EXPECT_TRUE(Exists(query(i)));
  }

  EXPECT_EQ(cache().size(), kMaxSize);

  EXPECT_TRUE(Exists("i=3"));
}

TEST_P(NoVarySearchCacheTest, LeastRecentlyUsedItemIsEvicted) {
  const auto query = QueryWithIParameter;
  // Fill the cache.
  for (size_t i = 0; i < kMaxSize; ++i) {
    Insert(query(i), kVaryOnIParameter);
  }
  EXPECT_EQ(cache().size(), kMaxSize);

  // Make "i=kMaxSize-1" be the least recently used item.
  for (size_t i = 0; i < kMaxSize - 1; ++i) {
    EXPECT_TRUE(Exists(query(i)));
  }

  // Evict one item.
  Insert(query(kMaxSize), kVaryOnIParameter);

  // Verify it was the least-recently-used item.
  EXPECT_FALSE(Exists(query(kMaxSize - 1)));
}

TEST_P(NoVarySearchCacheTest, InsertUpdatesIdenticalItem) {
  Insert("a=b", "params=(\"c\")");
  auto original_result = cache().Lookup(TestRequest("a=b"));
  ASSERT_TRUE(original_result);
  Insert("a=b", "params=(\"c\")");
  auto new_result = cache().Lookup(TestRequest("a=b"));
  ASSERT_TRUE(new_result);
  EXPECT_TRUE(
      original_result->erase_handle.EqualsForTesting(new_result->erase_handle));
}

TEST_P(NoVarySearchCacheTest, InsertRemovesMatchingItem) {
  Insert("a=b&c=1", "params=(\"c\")");
  auto original_result = cache().Lookup(TestRequest("a=b"));
  ASSERT_TRUE(original_result);
  EXPECT_EQ(original_result->original_url, TestURL("a=b&c=1"));
  Insert("a=b&c=2", "params=(\"c\")");
  EXPECT_TRUE(original_result->erase_handle.IsGoneForTesting());
  EXPECT_EQ(cache().size(), 1u);
  auto new_result = cache().Lookup(TestRequest("a=b"));
  EXPECT_EQ(new_result->original_url, TestURL("a=b&c=2"));
}

TEST_P(NoVarySearchCacheTest, MaybeInsertDoesNothingWithNoNoVarySearchHeader) {
  auto headers = HttpResponseHeaders::Builder({1, 1}, "200 OK").Build();
  cache().MaybeInsert(TestRequest(), *headers);
  EXPECT_EQ(cache().size(), 0u);
  EXPECT_TRUE(cache().IsTopLevelMapEmptyForTesting());
}

TEST_P(NoVarySearchCacheTest, MaybeInsertDoesNothingForDefaultBehavior) {
  // The following header values are all equivalent to default behavior.
  static constexpr auto kDefaultCases = std::to_array<std::string_view>(
      {"", " ", "params=?0", "params=()", "key-order=?0", "nonsense"});

  for (auto no_vary_search : kDefaultCases) {
    NoVarySearchCache cache(kMaxSize);

    Insert("a=b", no_vary_search);
    EXPECT_EQ(cache.size(), 0u) << no_vary_search;
  }
}

// A size 1 cache is a special case, because eviction results in an empty cache.
TEST_P(NoVarySearchCacheTest, EvictWithSize1Cache) {
  NoVarySearchCache cache(1u);
  cache.MaybeInsert(TestRequest("a=1"), TestHeaders("key-order"));
  cache.MaybeInsert(TestRequest("a=2"), TestHeaders("key-order"));
  EXPECT_TRUE(cache.Lookup(TestRequest("a=2")).has_value());
  EXPECT_EQ(cache.size(), 1u);
}

// This is a regression test for a bug where insertion led to eviction of the
// only cache entry with the same base URL.
TEST_P(NoVarySearchCacheTest, InsertWithBaseURLMatchingEvicted) {
  static constexpr auto my_test_request = [](std::string_view query) {
    constexpr std::string_view kBaseURL = "https://a.example/?";
    return TestRequest(GURL(base::StrCat({kBaseURL, query})));
  };

  cache().MaybeInsert(my_test_request("will-be-evicted"),
                      TestHeaders("key-order"));
  for (size_t i = 1; i < kMaxSize; ++i) {
    Insert(QueryWithIParameter(i), kVaryOnIParameter);
  }
  EXPECT_EQ(cache().size(), kMaxSize);

  cache().MaybeInsert(my_test_request("same-base-url"),
                      TestHeaders("key-order"));
  EXPECT_TRUE(cache().Lookup(my_test_request("same-base-url")).has_value());
}

// This is a regression test for a bug where insertion led to eviction of the
// only cache entry with the same base URL & No-Vary-Search header value.
// The difference from the InsertWithBaseURLMatchingEvicted test is that in this
// test there are other entries with the same base URL.
TEST_P(NoVarySearchCacheTest, InsertWithNoVarySearchValueMatchingEvicted) {
  Insert("will-be-evicted", "params=(\"ignored\")");
  for (size_t i = 1; i < kMaxSize; ++i) {
    Insert(QueryWithIParameter(i), kVaryOnIParameter);
  }
  EXPECT_EQ(cache().size(), kMaxSize);

  Insert("same-nvs", "params=(\"ignored\")");
  EXPECT_TRUE(Exists("same-nvs"));
}

TEST_P(NoVarySearchCacheTest, MatchesWithoutQueryString) {
  auto url_with_query = GURL("https://example.com/foo?");
  cache().MaybeInsert(TestRequest(url_with_query), TestHeaders("key-order"));
  auto result = cache().Lookup(TestRequest(GURL("https://example.com/foo")));
  ASSERT_TRUE(result);
  EXPECT_EQ(result->original_url, url_with_query);
}

TEST_P(NoVarySearchCacheTest, InsertInvalidURLIsIgnored) {
  auto invalid_url = GURL("???");
  ASSERT_FALSE(invalid_url.is_valid());
  cache().MaybeInsert(TestRequest(invalid_url), TestHeaders("key-order"));
  EXPECT_EQ(cache().size(), 0u);
}

// There's no way to insert an invalid URL into the cache. There's also no way
// to add a query to a valid URL to make it invalid. So this test just verifies
// that we don't crash.
TEST_P(NoVarySearchCacheTest, LookupInvalidURLReturnsNullopt) {
  GURL invalid_url = GURL("???");
  ASSERT_FALSE(invalid_url.is_valid());
  auto result = cache().Lookup(TestRequest(invalid_url));
  EXPECT_FALSE(result);
}

TEST_P(NoVarySearchCacheTest, MatchCases) {
  struct Case {
    std::string_view description;
    std::string_view query1;
    std::string_view query2;
    std::string_view no_vary_search;
  };

  static constexpr Case cases[] = {
      {"Encoded & in key", "%26=a&b=c", "b=c", "params=(\"&\")"},
      {"Encoded & in value", "a=b%26&c=d", "c=d&a=b%26", "key-order"},
      {"Encoded =", "%3d=a", "%3D=a", "key-order"},
      {"Encoded and unencoded =", "a=%3d", "a==", "key-order"},
      {"Embedded null in key", "a%00b=c", "", "params=(\"a%00b\")"},
      {"Embedded null in value", "a=b%00c", "a=b%00c", "key-order"},
      {"Encoded space in key", "+=a", "%20=b", "params=(\" \")"},
      {"Encoded space in value", "a=b&c=+", "c=%20&a=b", "key-order"},
      {"Key is ?", "?=1", "", "params=(\"?\")"},
      {"Empty key", "=7&c=d", "c=d", "params=(\"\")"},
      {"Empty value", "a=&c=d", "c=d&a", "key-order"},
      {"Bad UTF8", "%fe=%ff", "\xfe=\xff", "key-order"},
      {"Two params removed", "a=b&c=d&e", "e", R"(params=("a" "c"))"},
  };

  for (const auto& [description, query1, query2, no_vary_search] : cases) {
    EXPECT_TRUE(Matches(query1, query2, no_vary_search))
        << "Testing forwards: " << description;
    EXPECT_TRUE(Matches(query2, query1, no_vary_search))
        << "Testing backwards: " << description;
    EXPECT_TRUE(InsertMatches(query1, query2, no_vary_search))
        << "Testing double insert: " << description;
  }
}

TEST_P(NoVarySearchCacheTest, NoMatchCases) {
  struct Case {
    std::string_view description;
    std::string_view query1;
    std::string_view query2;
  };

  static constexpr Case cases[] = {
      {"Encoded &", "a&b", "a%26b"},
      {"Encoded =", "a=b", "a%3db"},
  };

  static constexpr std::string_view kKeyOrder = "key-order";

  for (const auto& [description, query1, query2] : cases) {
    EXPECT_FALSE(Matches(query1, query2, kKeyOrder))
        << "Testing forwards: " << description;
    EXPECT_FALSE(Matches(query2, query1, kKeyOrder))
        << "Testing backwards: " << description;
    EXPECT_FALSE(InsertMatches(query1, query2, kKeyOrder))
        << "Testing double insert: " << description;
  }
}

// Different representations of No-Very-Search headers that should compare
// equal.
TEST_P(NoVarySearchCacheTest, NoVarySearchVariants) {
  struct Case {
    std::string_view description;
    std::string_view variant1;
    std::string_view variant2;
  };

  static constexpr Case cases[] = {
      {"Extra space", R"(params=("a" "b"))", R"(params=( "a"  "b" ))"},
      {"Bool or omitted params", "params", "params=?1"},
      {"Bool or omitted key-order", "key-order", "key-order=?1"},
      {"Absent or false key-order", "params, key-order=?0", "params"},
      {"Ignored entry", "params, ignored", "params"},
      {"Empty except", "params, except=()", "except=(), params"},
      {"Different order", R"(params, except=("a"), key-order)",
       R"(key-order, except=("a"), params)"},
  };

  static constexpr std::string_view kQuery = "a=b&b=7&c=d";

  for (const auto& [description, variant1, variant2] : cases) {
    NoVarySearchCache cache(kMaxSize);

    cache.MaybeInsert(TestRequest(kQuery), TestHeaders(variant1));
    EXPECT_EQ(cache.size(), 1u);
    cache.MaybeInsert(TestRequest(kQuery), TestHeaders(variant2));
    EXPECT_EQ(cache.size(), 1u)
        << "Failing: " << description << "; variant1='" << variant1
        << "'; variant2 = '" << variant2 << "'";
  }
}

// Items with a transient NIK will not be stored in the disk cache, and so they
// shouldn't be stored in the NoVarySearchCache either.
TEST_P(NoVarySearchCacheTest, TransientNIK) {
  const auto transient = NetworkIsolationKey::CreateTransientForTesting();

  cache().MaybeInsert(TestRequest(TestURL(), transient), TestHeaders("params"));
  if (HttpCache::IsSplitCacheEnabled()) {
    EXPECT_EQ(cache().size(), 0u);
    EXPECT_FALSE(cache().Lookup(TestRequest(TestURL(), transient)));
  } else {
    EXPECT_EQ(cache().size(), 1u);
    EXPECT_TRUE(cache().Lookup(TestRequest(TestURL(), transient)));
  }
}

TEST_P(NoVarySearchCacheTest, DifferentNIK) {
  const NetworkIsolationKey different_nik(
      SchemefulSite(TestURL()),
      SchemefulSite(GURL("https://thirdparty.example/")));

  cache().MaybeInsert(TestRequest(), TestHeaders("params"));
  cache().MaybeInsert(TestRequest(TestURL(), different_nik),
                      TestHeaders("params"));

  const auto result1 = cache().Lookup(TestRequest());
  const auto result2 = cache().Lookup(TestRequest(TestURL(), different_nik));
  ASSERT_TRUE(result1);
  ASSERT_TRUE(result2);

  const size_t cache_size = cache().size();
  const bool handles_are_equal =
      result1->erase_handle.EqualsForTesting(result2->erase_handle);

  if (HttpCache::IsSplitCacheEnabled()) {
    EXPECT_EQ(cache_size, 2u);
    EXPECT_FALSE(handles_are_equal);
  } else {
    EXPECT_EQ(cache_size, 1u);
    EXPECT_TRUE(handles_are_equal);
  }
}

TEST_P(NoVarySearchCacheTest, PerPartitionSizeLimitEnforced) {
  if (!HttpCache::IsSplitCacheEnabled()) {
    GTEST_SKIP() << "Requires distinct cache partitions.";
  }

  base::test::ScopedFeatureList feature_list;
  feature_list.InitWithFeaturesAndParameters(
      {{features::kHttpCacheNoVarySearch,
        {{features::kHttpCacheNoVarySearchCacheMaxEntries.name, "10"},
         {features::kHttpCacheNoVarySearchCacheMaxPartitionEntries.name, "2"},
         {features::kHttpCacheNoVarySearchCacheMaxPartitions.name, "10"}}}},
      {});

  NoVarySearchCache custom_cache(10);
  const SchemefulSite site_a(GURL("https://a.test/"));
  const SchemefulSite site_b(GURL("https://b.test/"));
  const NetworkIsolationKey nik_a(site_a, site_a);
  const NetworkIsolationKey nik_b(site_b, site_b);

  const auto url_a = [](size_t i) {
    return GURL("https://a.test/" + base::NumberToString(i));
  };
  const auto url_b = [](size_t i) {
    return GURL("https://b.test/" + base::NumberToString(i));
  };

  for (size_t i = 0; i < 3; ++i) {
    custom_cache.MaybeInsert(TestRequest(url_a(i), nik_a),
                             TestHeaders("params"));
  }
  EXPECT_EQ(custom_cache.size(), 2u);
  EXPECT_FALSE(custom_cache.Lookup(TestRequest(url_a(0), nik_a)));
  EXPECT_TRUE(custom_cache.Lookup(TestRequest(url_a(1), nik_a)));
  EXPECT_TRUE(custom_cache.Lookup(TestRequest(url_a(2), nik_a)));

  for (size_t i = 0; i < 2; ++i) {
    custom_cache.MaybeInsert(TestRequest(url_b(i), nik_b),
                             TestHeaders("params"));
  }
  EXPECT_EQ(custom_cache.size(), 4u);
  EXPECT_TRUE(custom_cache.Lookup(TestRequest(url_b(0), nik_b)));
  EXPECT_TRUE(custom_cache.Lookup(TestRequest(url_b(1), nik_b)));
}

TEST_P(NoVarySearchCacheTest, MaxPartitionsLimitEnforced) {
  if (!HttpCache::IsSplitCacheEnabled()) {
    GTEST_SKIP() << "Requires distinct cache partitions.";
  }

  base::test::ScopedFeatureList feature_list;
  feature_list.InitWithFeaturesAndParameters(
      {{features::kHttpCacheNoVarySearch,
        {{features::kHttpCacheNoVarySearchCacheMaxEntries.name, "100"},
         {features::kHttpCacheNoVarySearchCacheMaxPartitionEntries.name, "10"},
         {features::kHttpCacheNoVarySearchCacheMaxPartitions.name, "2"}}}},
      {});

  NoVarySearchCache custom_cache(100);
  const auto make_nik = [](size_t i) {
    const SchemefulSite site(
        GURL("https://site" + base::NumberToString(i) + ".test/"));
    return NetworkIsolationKey(site, site);
  };
  const auto make_url = [](size_t i) {
    return GURL("https://site" + base::NumberToString(i) + ".test/res");
  };

  custom_cache.MaybeInsert(TestRequest(make_url(0), make_nik(0)),
                           TestHeaders("key-order"));
  custom_cache.MaybeInsert(TestRequest(make_url(1), make_nik(1)),
                           TestHeaders("key-order"));
  EXPECT_EQ(custom_cache.size(), 2u);

  custom_cache.MaybeInsert(TestRequest(make_url(2), make_nik(2)),
                           TestHeaders("key-order"));
  EXPECT_EQ(custom_cache.size(), 2u);
  EXPECT_FALSE(custom_cache.Lookup(TestRequest(make_url(0), make_nik(0))));
  EXPECT_TRUE(custom_cache.Lookup(TestRequest(make_url(1), make_nik(1))));
  EXPECT_TRUE(custom_cache.Lookup(TestRequest(make_url(2), make_nik(2))));
}

TEST_P(NoVarySearchCacheTest, LookupUpdatesPartitionLru) {
  if (!HttpCache::IsSplitCacheEnabled()) {
    GTEST_SKIP() << "Requires distinct cache partitions.";
  }

  base::test::ScopedFeatureList feature_list;
  feature_list.InitWithFeaturesAndParameters(
      {{features::kHttpCacheNoVarySearch,
        {{features::kHttpCacheNoVarySearchCacheMaxEntries.name, "100"},
         {features::kHttpCacheNoVarySearchCacheMaxPartitionEntries.name, "10"},
         {features::kHttpCacheNoVarySearchCacheMaxPartitions.name, "2"}}}},
      {});

  NoVarySearchCache custom_cache(100);
  const auto make_nik = [](size_t i) {
    const SchemefulSite site(
        GURL("https://site" + base::NumberToString(i) + ".test/"));
    return NetworkIsolationKey(site, site);
  };
  const auto make_url = [](size_t i) {
    return GURL("https://site" + base::NumberToString(i) + ".test/res");
  };

  custom_cache.MaybeInsert(TestRequest(make_url(0), make_nik(0)),
                           TestHeaders("key-order"));
  custom_cache.MaybeInsert(TestRequest(make_url(1), make_nik(1)),
                           TestHeaders("key-order"));
  EXPECT_EQ(custom_cache.size(), 2u);

  // Lookup partition 0 to make it the most recently used.
  EXPECT_TRUE(custom_cache.Lookup(TestRequest(make_url(0), make_nik(0))));

  // Insert into partition 2, which should evict partition 1 instead of 0.
  custom_cache.MaybeInsert(TestRequest(make_url(2), make_nik(2)),
                           TestHeaders("key-order"));
  EXPECT_EQ(custom_cache.size(), 2u);

  EXPECT_TRUE(custom_cache.Lookup(TestRequest(make_url(0), make_nik(0))));
  EXPECT_FALSE(custom_cache.Lookup(TestRequest(make_url(1), make_nik(1))));
  EXPECT_TRUE(custom_cache.Lookup(TestRequest(make_url(2), make_nik(2))));
}

TEST_P(NoVarySearchCacheTest, LeastRecentlyUsedPartitionEvictedWhenFull) {
  if (!HttpCache::IsSplitCacheEnabled()) {
    GTEST_SKIP() << "Requires distinct cache partitions.";
  }

  base::test::ScopedFeatureList feature_list;
  feature_list.InitWithFeaturesAndParameters(
      {{features::kHttpCacheNoVarySearch,
        {{features::kHttpCacheNoVarySearchCacheMaxEntries.name, "10"},
         {features::kHttpCacheNoVarySearchCacheMaxPartitionEntries.name, "2"},
         {features::kHttpCacheNoVarySearchCacheMaxPartitions.name, "5"}}}},
      {});

  NoVarySearchCache custom_cache(10);

  const auto make_nik = [](size_t i) {
    const SchemefulSite site(
        GURL("https://site" + base::NumberToString(i) + ".test/"));
    return NetworkIsolationKey(site, site);
  };
  const auto make_url = [](size_t i, size_t j) {
    return GURL("https://site" + base::NumberToString(i) + ".test/res" +
                base::NumberToString(j));
  };

  for (size_t i = 0; i < 5; ++i) {
    custom_cache.MaybeInsert(TestRequest(make_url(i, 0), make_nik(i)),
                             TestHeaders("key-order"));
    custom_cache.MaybeInsert(TestRequest(make_url(i, 1), make_nik(i)),
                             TestHeaders("key-order"));
  }
  EXPECT_EQ(custom_cache.size(), 10u);

  custom_cache.MaybeInsert(TestRequest(make_url(5, 0), make_nik(5)),
                           TestHeaders("key-order"));

  EXPECT_FALSE(custom_cache.Lookup(TestRequest(make_url(0, 0), make_nik(0))));
  EXPECT_FALSE(custom_cache.Lookup(TestRequest(make_url(0, 1), make_nik(0))));
  EXPECT_TRUE(custom_cache.Lookup(TestRequest(make_url(1, 0), make_nik(1))));
  EXPECT_TRUE(custom_cache.Lookup(TestRequest(make_url(5, 0), make_nik(5))));
}

TEST_P(NoVarySearchCacheTest, DifferentURL) {
  const GURL url1("https://example.com/a?a=b");
  const GURL url2("https://example.com/b?a=b");

  cache().MaybeInsert(TestRequest(url1), TestHeaders("key-order"));
  cache().MaybeInsert(TestRequest(url2), TestHeaders("key-order"));
  EXPECT_EQ(cache().size(), 2u);
  const auto result1 = cache().Lookup(TestRequest(url1));
  const auto result2 = cache().Lookup(TestRequest(url2));
  ASSERT_TRUE(result1);
  ASSERT_TRUE(result2);
  EXPECT_FALSE(result1->erase_handle.EqualsForTesting(result2->erase_handle));
}

void SpinUntilCurrentTimeChanges() {
  const auto start = base::Time::Now();
  while (start == base::Time::Now()) {
    base::PlatformThread::YieldCurrentThread();
  }
}

TEST_P(NoVarySearchCacheTest, DifferentNoVarySearch) {
  Insert("a=b&c=d", "params, except=(\"a\")");
  // Make sure that the two inserts reliably get a different `inserted`
  // timestamp so that the ordering is deterministic.
  SpinUntilCurrentTimeChanges();
  Insert("a=b", "key-order");

  EXPECT_EQ(cache().size(), 2u);
  const auto result = cache().Lookup(TestRequest("a=b"));
  ASSERT_TRUE(result);
  // If time goes backwards this test will flake.
  EXPECT_EQ(result->original_url, TestURL("a=b"));
}

// The winner of the lookup should depend only on insertion order and not on the
// order of iteration of the map. To ensure this works for any iteration order,
// we perform the same test in the opposite direction.
TEST_P(NoVarySearchCacheTest, DifferentNoVarySearchReverseOrder) {
  Insert("a=b", "key-order");
  SpinUntilCurrentTimeChanges();
  Insert("a=b&c=d", "params, except=(\"a\")");

  EXPECT_EQ(cache().size(), 2u);
  const auto result = cache().Lookup(TestRequest("a=b"));
  ASSERT_TRUE(result);
  // If time goes backwards this test will flake.
  EXPECT_EQ(result->original_url, TestURL("a=b&c=d"));
}

TEST_P(NoVarySearchCacheTest, EraseInDifferentOrder) {
  // Insert in order a, b, c.
  Insert("a", "key-order");
  Insert("b", "key-order");
  Insert("c", "key-order");

  // Look up in order b, c, a.
  const auto lookup = [this](std::string_view query) {
    return cache().Lookup(TestRequest(query));
  };

  auto result_b = lookup("b");
  auto result_c = lookup("c");
  auto result_a = lookup("a");

  ASSERT_TRUE(result_a);
  ASSERT_TRUE(result_b);
  ASSERT_TRUE(result_c);

  // Erase in order c, a, b.
  cache().Erase(std::move(result_c->erase_handle));
  EXPECT_TRUE(Exists("a"));
  EXPECT_TRUE(Exists("b"));
  EXPECT_FALSE(Exists("c"));

  cache().Erase(std::move(result_a->erase_handle));
  EXPECT_FALSE(Exists("a"));
  EXPECT_TRUE(Exists("b"));

  cache().Erase(std::move(result_b->erase_handle));
  EXPECT_FALSE(Exists("b"));

  EXPECT_EQ(cache().size(), 0u);
  EXPECT_TRUE(cache().IsTopLevelMapEmptyForTesting());
}

// The URL "ref", also known as the "fragment", also known as the "hash", is
// ignored for matching and not stored in the cache.
TEST_P(NoVarySearchCacheTest, URLRefIsIgnored) {
  cache().MaybeInsert(TestRequest(GURL("https://example.com/?a=b#foo")),
                      TestHeaders("key-order"));
  cache().MaybeInsert(TestRequest(GURL("https://example.com/?a=b#bar")),
                      TestHeaders("key-order"));
  EXPECT_EQ(cache().size(), 1u);
  auto result =
      cache().Lookup(TestRequest(GURL("https://example.com/?a=b#baz")));
  EXPECT_TRUE(result);
  EXPECT_EQ(result->original_url, GURL("https://example.com/?a=b"));
}

TEST_P(NoVarySearchCacheTest, URLWithUsernameIsRejected) {
  const GURL url_with_username("https://me@example.com/?a=b");
  cache().MaybeInsert(TestRequest(url_with_username), TestHeaders("key-order"));
  EXPECT_EQ(cache().size(), 0u);

  // See if it matches against the URL without the username.
  cache().MaybeInsert(TestRequest(GURL("https://example.com/?a=b")),
                      TestHeaders("key-order"));
  EXPECT_FALSE(cache().Lookup(TestRequest(url_with_username)));
}

TEST_P(NoVarySearchCacheTest, URLWithPasswordIsRejected) {
  const GURL url_with_password("https://:hunter123@example.com/?a=b");
  cache().MaybeInsert(TestRequest(url_with_password), TestHeaders("key-order"));
  EXPECT_EQ(cache().size(), 0u);

  // See if it matches against the URL without the password.
  cache().MaybeInsert(TestRequest(GURL("https://example.com/?a=b")),
                      TestHeaders("key-order"));
  EXPECT_FALSE(cache().Lookup(TestRequest(url_with_password)));
}

TEST_P(NoVarySearchCacheTest, ClearDataEverything) {
  cache().MaybeInsert(TestRequest(GURL("http://example/q?a=b")),
                      TestHeaders("key-order"));
  const NetworkIsolationKey different_nik(
      SchemefulSite(TestURL()),
      SchemefulSite(GURL("https://thirdparty.example/")));
  cache().MaybeInsert(TestRequest(GURL("http://example/q?a=b"), different_nik),
                      TestHeaders("key-order"));
  cache().MaybeInsert(TestRequest(GURL("https://example.com/search?q=z")),
                      TestHeaders("key-order"));

  const bool cleared = cache().ClearData(UrlFilterType::kFalseIfMatches, {}, {},
                                         base::Time(), base::Time::Max());

  EXPECT_TRUE(cleared);
  EXPECT_EQ(cache().size(), 0u);
  EXPECT_TRUE(cache().IsTopLevelMapEmptyForTesting());
}

TEST_P(NoVarySearchCacheTest, ClearDataMatchOrigin) {
  // Scheme differs.
  cache().MaybeInsert(TestRequest(GURL("https://example.com/q?a=b")),
                      TestHeaders("key-order"));
  cache().MaybeInsert(TestRequest(GURL("http://example.com/q?a=b")),
                      TestHeaders("key-order"));

  const bool cleared =
      cache().ClearData(UrlFilterType::kTrueIfMatches,
                        {url::Origin::Create(GURL("https://example.com/"))}, {},
                        base::Time(), base::Time::Max());

  EXPECT_TRUE(cleared);
  EXPECT_EQ(cache().size(), 1u);
  EXPECT_TRUE(cache()
                  .Lookup(TestRequest(GURL("http://example.com/q?a=b")))
                  .has_value());
}

TEST_P(NoVarySearchCacheTest, ClearDataMatchDomain) {
  // Scheme differs.
  cache().MaybeInsert(TestRequest(GURL("http://example.com:80/q?a=b")),
                      TestHeaders("key-order"));
  cache().MaybeInsert(TestRequest(GURL("https://www.example.com:8080/q?a=b")),
                      TestHeaders("key-order"));
  cache().MaybeInsert(TestRequest(GURL("https://other.example/q?a=b")),
                      TestHeaders("key-order"));

  const bool cleared =
      cache().ClearData(UrlFilterType::kTrueIfMatches, {}, {"example.com"},
                        base::Time(), base::Time::Max());

  EXPECT_TRUE(cleared);
  EXPECT_EQ(cache().size(), 1u);
  EXPECT_TRUE(cache()
                  .Lookup(TestRequest(GURL("https://other.example/q?a=b")))
                  .has_value());
}

TEST_P(NoVarySearchCacheTest, ClearDataMatchTime) {
  base::test::TaskEnvironment task_environment(
      base::test::SingleThreadTaskEnvironment::TimeSource::MOCK_TIME);
  // Function to convert a time string in the format HH:MM:SS to a base::Time.
  static constexpr auto time = [](std::string_view time) {
    base::Time output;
    const bool parsed = base::Time::FromUTCString(
        base::StrCat({"Tue, 4 Mar 2025 ", time, " GMT"}).c_str(), &output);
    CHECK(parsed);
    return output;
  };

  // Advance the mock time to `when`, specified in the format HH:MM:SS.
  const auto advance_time_to = [&](std::string_view when) {
    base::Time target_time = time(when);
    auto mock_now = base::Time::Now();
    CHECK_GT(target_time, mock_now);
    task_environment.FastForwardBy(target_time - mock_now);
  };

  advance_time_to("12:00:00");
  Insert("a=1", "key-order");

  advance_time_to("13:00:00");
  Insert("a=2", "key-order");

  advance_time_to("14:00:00");
  Insert("a=3", "key-order");

  const bool cleared = cache().ClearData(UrlFilterType::kFalseIfMatches, {}, {},
                                         time("12:30:00"), time("13:30:00"));

  EXPECT_TRUE(cleared);
  EXPECT_EQ(cache().size(), 2u);
  EXPECT_TRUE(Exists("a=1"));
  EXPECT_FALSE(Exists("a=2"));
  EXPECT_TRUE(Exists("a=3"));
}

TEST_P(NoVarySearchCacheTest, ClearDataEmptyCache) {
  const bool cleared =
      cache().ClearData(UrlFilterType::kTrueIfMatches,
                        {url::Origin::Create(GURL("https://example.com/"))}, {},
                        base::Time(), base::Time::Max());

  EXPECT_FALSE(cleared);
  EXPECT_EQ(cache().size(), 0u);
}

TEST_P(NoVarySearchCacheTest, ClearDataNoMatch) {
  Insert("a=1", "key-order");

  const bool cleared = cache().ClearData(
      UrlFilterType::kTrueIfMatches,
      {url::Origin::Create(GURL("https://nomatch.com:9999/"))}, {},
      base::Time(), base::Time::Max());

  EXPECT_FALSE(cleared);
  EXPECT_EQ(cache().size(), 1u);
  EXPECT_TRUE(Exists("a=1"));
}

std::optional<NoVarySearchCache> TestPickleRoundTrip(
    const NoVarySearchCache& cache) {
  base::Pickle pickle;
  WriteToPickle(pickle, cache);
  // The estimate of PickleSize should always be correct.
  EXPECT_EQ(EstimatePickleSize(cache), pickle.payload_size());
  auto maybe_cache = ReadValueFromPickle<NoVarySearchCache>(pickle);
  if (!maybe_cache) {
    return std::nullopt;
  }

  EXPECT_EQ(cache.size(), maybe_cache->size());
  return maybe_cache;
}

TEST_P(NoVarySearchCacheTest, SerializeDeserializeEmpty) {
  EXPECT_TRUE(TestPickleRoundTrip(cache()));
}

TEST_P(NoVarySearchCacheTest, SerializeDeserializeSimple) {
  Insert("b=1", "key-order");
  Insert("c&d", "key-order");
  Insert("f=3", "params=(\"a\")");

  auto new_cache = TestPickleRoundTrip(cache());
  ASSERT_TRUE(new_cache);

  const auto lookup = [&](std::string_view params) {
    return new_cache->Lookup(TestRequest(params));
  };

  auto maybe_handle1 = lookup("b=1");
  auto maybe_handle2 = lookup("d&c");
  auto maybe_handle3 = lookup("f=3&a=7");

  ASSERT_TRUE(maybe_handle1);
  ASSERT_TRUE(maybe_handle2);
  ASSERT_TRUE(maybe_handle3);

  new_cache->Erase(std::move(maybe_handle1->erase_handle));
  new_cache->Erase(std::move(maybe_handle2->erase_handle));
  new_cache->Erase(std::move(maybe_handle3->erase_handle));

  EXPECT_EQ(new_cache->size(), 0u);
  EXPECT_TRUE(new_cache->IsTopLevelMapEmptyForTesting());
}

TEST_P(NoVarySearchCacheTest, SerializeDeserializeFull) {
  for (size_t i = 0; i < kMaxSize; ++i) {
    Insert(QueryWithIParameter(i), kVaryOnIParameter);
  }

  auto new_cache = TestPickleRoundTrip(cache());
  ASSERT_TRUE(new_cache);

  for (size_t i = 0; i < kMaxSize; ++i) {
    EXPECT_TRUE(new_cache->Lookup(TestRequest(QueryWithIParameter(i))));
  }
}

TEST_P(NoVarySearchCacheTest, DeserializeBadSizes) {
  struct TestCase {
    std::string_view test_description;
    int size;
    int max_size;
    int map_size;
  };
  static constexpr auto kTestCases = std::to_array<TestCase>({
      {"Negative size", -1, 1, 0},
      {"Size larger than max_size", 2, 1, 0},
      {"Size bigger than map contents", 1, 1, 0},
      {"Negative max_size", 0, -1, 0},
      {"Zero max_size", 0, 0, 0},
      {"Negative map size", 0, 1, -1},
      {"Map size larger than map contents", 0, 1, 1},
  });

  for (const auto& test_case : kTestCases) {
    SCOPED_TRACE(test_case.test_description);
    base::Pickle pickle;
    // This uses the fact that containers use an integer for size.
    WriteToPickle(pickle, test_case.size, test_case.max_size,
                  test_case.map_size);
    EXPECT_FALSE(ReadValueFromPickle<NoVarySearchCache>(pickle));
  }
}

// A truncated Pickle should never deserialize to a NoVarySearchCache object.
// This tests covers many different checks for bad data during deserialization.
TEST_P(NoVarySearchCacheTest, TruncatedPickle) {
  Insert("a=9&b=1", "params=(\"a\")");
  Insert("a=8&b=2", "params=(\"a\")");
  Insert("f=3", "params, except=(\"f\")");
  Insert("", "params, except=(\"f\")");

  base::Pickle pickle;
  WriteToPickle(pickle, cache());

  // Go up in increments of 4 bytes because a Pickle with a size that is not a
  // multiple of 4 is invalid in a way that is not interesting to this test.
  for (size_t bytes = 4u; bytes < pickle.payload_size(); bytes += 4) {
    SCOPED_TRACE(bytes);
    base::Pickle truncated;
    truncated.WriteBytes(pickle.payload_bytes().first(bytes));
    EXPECT_FALSE(ReadValueFromPickle<NoVarySearchCache>(truncated));
  }
}

// A Journal that registers and deregisters itself automatically.
class ScopedJournal : public NoVarySearchCache::Journal {
 public:
  explicit ScopedJournal(NoVarySearchCache& cache) : cache_(cache) {
    cache.SetJournal(this);
  }

  ~ScopedJournal() override { cache_->SetJournal(nullptr); }

 private:
  raw_ref<NoVarySearchCache> cache_;
};

// A Journal object implemented by GoogleMock.
class ScopedMockJournal : public ScopedJournal {
 public:
  using ScopedJournal::ScopedJournal;

  MOCK_METHOD(void,
              OnInsert,
              (const std::string&,
               const std::string&,
               const HttpNoVarySearchData&,
               const std::optional<std::string>&,
               base::Time),
              (override));
  MOCK_METHOD(void,
              OnErase,
              (const std::string&,
               const std::string&,
               const HttpNoVarySearchData&,
               const std::optional<std::string>&),
              (override));
};

// A matcher which matches No-Vary-Search: key-order
const auto IsKeyOrder =
    Eq(HttpNoVarySearchData::CreateFromNoVaryParams({}, false));

TEST_P(NoVarySearchCacheTest, JournalNewInsert) {
  ScopedMockJournal journal(cache());

  const base::Time now = base::Time::Now();

  EXPECT_CALL(journal, OnInsert(_, "https://example.com/", IsKeyOrder,
                                Optional(Eq("a=0")), Ge(now)));

  Insert("a=0", "key-order");
}

TEST_P(NoVarySearchCacheTest, JournalRefresh) {
  Insert("a=1", "key-order");

  // Start journalling now.
  ScopedMockJournal journal(cache());

  const base::Time now = base::Time::Now();

  EXPECT_CALL(journal, OnInsert(_, "https://example.com/", IsKeyOrder,
                                Optional(Eq("a=1")), Ge(now)));

  Insert("a=1", "key-order");
}

TEST_P(NoVarySearchCacheTest, JournalReplacement) {
  Insert("a=2&k=1", "params=(\"k\")");

  ScopedMockJournal journal(cache());

  const auto params_k =
      HttpNoVarySearchData::CreateFromNoVaryParams({"k"}, true);

  const base::Time now = base::Time::Now();

  EXPECT_CALL(journal, OnInsert(_, "https://example.com/", Eq(params_k),
                                Optional(Eq("a=2&k=2")), Ge(now)));
  EXPECT_CALL(journal, OnErase).Times(0);

  // This one replaces the one inserted earlier, but OnErase() is not called to
  // reflect that the old one was removed.
  Insert("a=2&k=2", "params=(\"k\")");
}

TEST_P(NoVarySearchCacheTest, JournalErase) {
  Insert("a=3", "key-order");

  auto [original_url, erase_handle] =
      cache().Lookup(TestRequest("a=3")).value();

  ScopedMockJournal journal(cache());

  EXPECT_CALL(journal, OnErase(_, "https://example.com/", IsKeyOrder,
                               Optional(Eq("a=3"))));

  cache().Erase(std::move(erase_handle));
}

TEST_P(NoVarySearchCacheTest, DontJournalEviction) {
  ScopedMockJournal journal(cache());

  EXPECT_CALL(journal, OnInsert(_, "https://example.com/", _, _, _))
      .Times(kMaxSize + 1);

  // Eviction does not result in a call to OnErase().
  EXPECT_CALL(journal, OnErase).Times(0);

  for (size_t i = 0; i < kMaxSize + 1; ++i) {
    Insert(QueryWithIParameter(i), "key-order");
  }
}

TEST_P(NoVarySearchCacheTest, DontJournalNonInsertion) {
  ScopedMockJournal journal(cache());

  EXPECT_CALL(journal, OnInsert).Times(0);

  // This No-Vary-Search value is equivalent to the default, so doesn't get
  // inserted into the cache.
  Insert("a=5", "params=()");
}

TEST_P(NoVarySearchCacheTest, DontJournalClearData) {
  Insert("a=6", "key-order");

  ScopedMockJournal journal(cache());

  EXPECT_CALL(journal, OnErase).Times(0);

  // Matches everything.
  cache().ClearData(UrlFilterType::kFalseIfMatches, {}, {}, base::Time(),
                    base::Time::Max());
}

TEST_P(NoVarySearchCacheTest, DontJournalLookup) {
  Insert("a=6", "key-order");

  ScopedMockJournal journal(cache());

  EXPECT_CALL(journal, OnInsert).Times(0);
  EXPECT_CALL(journal, OnErase).Times(0);

  cache().Lookup(TestRequest("a=6"));
}

// A Journal that clones all changes into a target NoVarySearchCache object.
class CloningJournal : public ScopedJournal {
 public:
  CloningJournal(NoVarySearchCache& source, NoVarySearchCache& target)
      : ScopedJournal(source), target_(target) {}

  void OnInsert(const std::string& partition_key,
                const std::string& base_url,
                const HttpNoVarySearchData& nvs_data,
                const std::optional<std::string>& query,
                base::Time update_time) override {
    target_->ReplayInsert(partition_key, base_url, nvs_data, query,
                          update_time);
  }

  // Called when an entry is erased by the Erase() method.
  void OnErase(const std::string& partition_key,
               const std::string& base_url,
               const HttpNoVarySearchData& nvs_data,
               const std::optional<std::string>& query) override {
    target_->ReplayErase(partition_key, base_url, nvs_data, query);
  }

 private:
  raw_ref<NoVarySearchCache> target_;
};

struct CloneMaker {
  NoVarySearchCache clone;
  CloningJournal journal;

  explicit CloneMaker(NoVarySearchCache& source)
      : clone(kMaxSize), journal(source, clone) {}
};

class NoVarySearchCacheReplayTest : public NoVarySearchCacheTest {
 protected:
  struct ReplayTestCase {
    std::string_view description;
    HttpRequestInfo to_insert;
    std::string_view no_vary_search_value;
    HttpRequestInfo to_lookup;
  };

  auto ReplayTestCases() {
    return std::to_array<ReplayTestCase>({
        {
            "Simple key-order",
            TestRequest("c=2&a=1"),
            "key-order",
            TestRequest("a=1&c=2"),
        },
        {
            "Different no-vary-search",
            TestRequest("d=3&e=5"),
            "params=(\"d\")",
            TestRequest("e=5"),
        },
        {
            "Different base URL",
            TestRequest(GURL("https://www.example.com/other?c=2&a=1")),
            "key-order",
            TestRequest(GURL("https://www.example.com/other?a=1&c=2")),
        },
        {
            "Different site",
            TestRequest(GURL("https://example.example/?c=2&a=1")),
            "key-order",
            TestRequest(GURL("https://example.example/?a=1&c=2")),
        },
        {
            "No question mark",
            TestRequest(GURL("https://example2.example/")),
            "params",
            TestRequest(GURL("https://example2.example/?q=ignored")),
        },
    });
  }
};

INSTANTIATE_TEST_SUITE_P(All,
                         NoVarySearchCacheReplayTest,
                         ::testing::Bool(),
                         [](const testing::TestParamInfo<bool>& info) {
                           return info.param ? "SplitCacheEnabled"
                                             : "SplitCacheDisabled";
                         });

TEST_P(NoVarySearchCacheReplayTest, Inserts) {
  const auto test_cases = ReplayTestCases();
  CloneMaker maker(cache());

  for (const auto& [description, to_insert, no_vary_search_value, to_lookup] :
       test_cases) {
    cache().MaybeInsert(to_insert, TestHeaders(no_vary_search_value));
  }

  EXPECT_EQ(maker.clone.size(), test_cases.size());

  for (const auto& [description, to_insert, no_vary_search_value, to_lookup] :
       test_cases) {
    SCOPED_TRACE(description);
    auto source_lookup_result = cache().Lookup(to_lookup);
    auto target_lookup_result = maker.clone.Lookup(to_lookup);

    ASSERT_TRUE(source_lookup_result);
    ASSERT_TRUE(target_lookup_result);
    EXPECT_EQ(source_lookup_result->original_url,
              target_lookup_result->original_url);
  }
}

TEST_P(NoVarySearchCacheReplayTest, Erases) {
  const auto test_cases = ReplayTestCases();
  CloneMaker maker(cache());

  for (const auto& [description, to_insert, no_vary_search_value, to_lookup] :
       test_cases) {
    cache().MaybeInsert(to_insert, TestHeaders(no_vary_search_value));
  }

  for (const auto& [description, to_insert, no_vary_search_value, to_lookup] :
       test_cases) {
    SCOPED_TRACE(description);
    auto source_lookup_result = cache().Lookup(to_lookup);
    ASSERT_TRUE(source_lookup_result);

    cache().Erase(std::move(source_lookup_result->erase_handle));

    EXPECT_FALSE(maker.clone.Lookup(to_lookup));
  }

  EXPECT_EQ(maker.clone.size(), 0u);
  EXPECT_TRUE(maker.clone.IsTopLevelMapEmptyForTesting());
}

TEST_P(NoVarySearchCacheTest, ReplayInsertBadURLs) {
  struct TestCase {
    std::string_view description;
    std::string_view bad_url;
  };
  static constexpr auto kBadURLs = std::to_array<TestCase>({
      {"Invalid URL", ":"},
      {"Not canonical", "https://example%2Eexample/"},
      {"No path", "what:"},
      {"Has username", "https://bob@example.example/"},
      {"Has password", "https://:pass@example.example/"},
      {"Has query", "https://example.example/?"},
      {"Has ref", "https://example.example/#water"},
  });
  static constexpr std::string_view kRealURL = "https://example.example/test";
  const std::string partition_key = GenerateCachePartitionKey(kRealURL);
  const auto nvs_data = HttpNoVarySearchData::CreateFromNoVaryParams({}, false);
  const std::optional<std::string> query = "t=1";
  const base::Time update_time;
  for (const auto& [description, bad_url] : kBadURLs) {
    SCOPED_TRACE(description);
    cache().ReplayInsert(partition_key, std::string(bad_url), nvs_data, query,
                         update_time);
    EXPECT_EQ(cache().size(), 0u);
  }
}

TEST_P(NoVarySearchCacheTest, ReplayInsertBadQuery) {
  static constexpr std::string_view kUrl = "https://example.example/";
  const std::string partition_key = GenerateCachePartitionKey(kUrl);
  const auto nvs_data = HttpNoVarySearchData::CreateFromNoVaryParams({}, false);
  const base::Time update_time;
  cache().ReplayInsert(partition_key, std::string(kUrl), nvs_data, "t=1#what",
                       update_time);
  EXPECT_EQ(cache().size(), 0u);
}

TEST_P(NoVarySearchCacheTest, ReplayEraseSuccess) {
  static constexpr std::string_view kUrl = "https://example.example/";
  const std::string partition_key = GenerateCachePartitionKey(kUrl);
  const auto nvs_data = HttpNoVarySearchData::CreateFromNoVaryParams({}, false);
  const std::optional<std::string> query = "t=1";
  const base::Time update_time;
  cache().ReplayInsert(partition_key, std::string(kUrl), nvs_data, query,
                       update_time);

  cache().ReplayErase(partition_key, std::string(kUrl), nvs_data, query);
  EXPECT_EQ(cache().size(), 0u);
}

TEST_P(NoVarySearchCacheTest, ReplayEraseOnEmptyCache) {
  static constexpr std::string_view kUrl = "https://example.example/";
  const std::string partition_key = GenerateCachePartitionKey(kUrl);
  const auto nvs_data = HttpNoVarySearchData::CreateFromNoVaryParams({}, false);
  cache().ReplayErase(partition_key, std::string(kUrl), nvs_data, "t=1");
  EXPECT_EQ(cache().size(), 0u);
}

TEST_P(NoVarySearchCacheTest, ReplayEraseMismatchedPartition) {
  static constexpr std::string_view kUrl = "https://example.example/";
  const std::string partition_key = GenerateCachePartitionKey(kUrl);
  const auto nvs_data = HttpNoVarySearchData::CreateFromNoVaryParams({}, false);
  const std::optional<std::string> query = "t=1";
  const base::Time update_time;
  cache().ReplayInsert(partition_key, std::string(kUrl), nvs_data, query,
                       update_time);

  cache().ReplayErase(partition_key + ".", std::string(kUrl), nvs_data, query);
  EXPECT_EQ(cache().size(), 1u);
}

TEST_P(NoVarySearchCacheTest, ReplayEraseMismatchedBaseUrl) {
  static constexpr std::string_view kUrl = "https://example.example/";
  const std::string partition_key = GenerateCachePartitionKey(kUrl);
  const auto nvs_data = HttpNoVarySearchData::CreateFromNoVaryParams({}, false);
  const std::optional<std::string> query = "t=1";
  const base::Time update_time;
  cache().ReplayInsert(partition_key, std::string(kUrl), nvs_data, query,
                       update_time);

  cache().ReplayErase(partition_key, std::string(kUrl) + ".", nvs_data, query);
  EXPECT_EQ(cache().size(), 1u);
}

TEST_P(NoVarySearchCacheTest, ReplayEraseMismatchedNVSData) {
  static constexpr std::string_view kUrl = "https://example.example/";
  const std::string partition_key = GenerateCachePartitionKey(kUrl);
  const auto nvs_data = HttpNoVarySearchData::CreateFromNoVaryParams({}, false);
  const std::optional<std::string> query = "t=1";
  const base::Time update_time;
  cache().ReplayInsert(partition_key, std::string(kUrl), nvs_data, query,
                       update_time);

  const auto mismatched_nvs_data =
      HttpNoVarySearchData::CreateFromNoVaryParams({"z"}, true);
  cache().ReplayErase(partition_key, std::string(kUrl), mismatched_nvs_data,
                      query);
  EXPECT_EQ(cache().size(), 1u);
}

TEST_P(NoVarySearchCacheTest, ReplayEraseMismatchedQuery) {
  static constexpr std::string_view kUrl = "https://example.example/";
  const std::string partition_key = GenerateCachePartitionKey(kUrl);
  const auto nvs_data = HttpNoVarySearchData::CreateFromNoVaryParams({}, false);
  const std::optional<std::string> query = "t=1";
  const base::Time update_time;
  cache().ReplayInsert(partition_key, std::string(kUrl), nvs_data, query,
                       update_time);

  const std::optional<std::string> mismatched_query = "t=2";
  cache().ReplayErase(partition_key, std::string(kUrl), nvs_data,
                      mismatched_query);
  EXPECT_EQ(cache().size(), 1u);
}

// This test doesn't actually cover the Replay methods, but uses the same data
// set for convenience.
TEST_P(NoVarySearchCacheReplayTest, MergeFrom) {
  const auto test_cases = ReplayTestCases();

  const base::Time before_inserts = base::Time::Now();

  for (const auto& [description, to_insert, no_vary_search_value, to_lookup] :
       test_cases) {
    cache().MaybeInsert(to_insert, TestHeaders(no_vary_search_value));
  }

  const base::Time after_inserts = base::Time::Now();

  NoVarySearchCache target(kMaxSize);
  ScopedMockJournal journal(target);

  EXPECT_CALL(journal, OnErase).Times(0);

  {
    InSequence s;
    for (const auto& [description, to_insert, no_vary_search_value, to_lookup] :
         test_cases) {
      auto expected_nvs_data =
          HttpNoVarySearchData::ParseFromHeaderValue(no_vary_search_value);
      const GURL& url = to_insert.url;
      std::optional<std::string_view> query;
      if (url.has_query()) {
        query = url.query();
      }
      std::string base_url = url.spec();
      if (size_t pos = base_url.find('?'); pos != std::string::npos) {
        base_url = base_url.substr(0, pos);
      }
      EXPECT_CALL(journal,
                  OnInsert(_, Eq(base_url), Eq(expected_nvs_data), Eq(query),
                           AllOf(Ge(before_inserts), Le(after_inserts))));
    }
  }

  target.MergeFrom(cache());

  EXPECT_EQ(cache().size(), target.size());

  for (const auto& [description, to_insert, no_vary_search_value, to_lookup] :
       test_cases) {
    SCOPED_TRACE(description);
    auto source_lookup_result = cache().Lookup(to_lookup);
    auto target_lookup_result = target.Lookup(to_lookup);

    ASSERT_TRUE(source_lookup_result);
    ASSERT_TRUE(target_lookup_result);
    EXPECT_EQ(source_lookup_result->original_url,
              target_lookup_result->original_url);
  }
}

TEST_P(NoVarySearchCacheReplayTest, MergeFromTargetQueriesConsideredOlder) {
  const auto query = QueryWithIParameter;
  NoVarySearchCache target(kMaxSize);

  // Fill the target cache.
  for (size_t i = 0; i < kMaxSize; ++i) {
    target.MaybeInsert(TestRequest(query(i)), TestHeaders(kVaryOnIParameter));
  }

  // Put one entry in the source cache.
  cache().MaybeInsert(TestRequest(query(kMaxSize)),
                      TestHeaders(kVaryOnIParameter));

  target.MergeFrom(cache());

  EXPECT_EQ(target.size(), kMaxSize);

  // i=0 has been evicted.
  EXPECT_FALSE(target.Lookup(TestRequest(query(0u))));
}

TEST_P(NoVarySearchCacheReplayTest, LRUOrderPreserved) {
  const auto query = QueryWithIParameter;
  NoVarySearchCache target(kMaxSize);

  // Fill the source cache.
  for (size_t i = 0; i < kMaxSize; ++i) {
    Insert(query(i), kVaryOnIParameter);
  }

  // Make i=1 be most recently used.
  EXPECT_TRUE(Exists(query(1u)));

  // Merge to target cache.
  target.MergeFrom(cache());

  int next_i = kMaxSize;
  const auto expect_to_evict = [&](size_t i) {
    target.MaybeInsert(TestRequest(query(next_i)),
                       TestHeaders(kVaryOnIParameter));
    EXPECT_FALSE(target.Lookup(TestRequest(query(i))));
    ++next_i;
  };

  // Evict i=0.
  expect_to_evict(0u);

  // Evict i=2 to i=kMaxSize-1.
  for (size_t i = 2; i < kMaxSize; ++i) {
    expect_to_evict(i);
  }

  // Evict i=1.
  expect_to_evict(1u);
}

TEST_P(NoVarySearchCacheTest, SetMaxSizeSame) {
  Insert("a=1", "key-order");
  Insert("a=2", "key-order");
  ASSERT_EQ(cache().size(), 2u);
  ASSERT_EQ(cache().max_size(), kMaxSize);

  cache().SetMaxSize(kMaxSize);

  EXPECT_EQ(cache().size(), 2u);
  EXPECT_EQ(cache().max_size(), kMaxSize);
  EXPECT_TRUE(Exists("a=1"));
  EXPECT_TRUE(Exists("a=2"));
}

TEST_P(NoVarySearchCacheTest, SetMaxSizeSmaller) {
  for (size_t i = 0; i < kMaxSize; ++i) {
    Insert(QueryWithIParameter(i), "key-order");
  }
  ASSERT_EQ(cache().size(), kMaxSize);

  cache().SetMaxSize(kMaxSize - 2);

  EXPECT_EQ(cache().size(), kMaxSize - 2);
  EXPECT_EQ(cache().max_size(), kMaxSize - 2);

  // The two least recently used items should be evicted.
  EXPECT_FALSE(Exists("i=0"));
  EXPECT_FALSE(Exists("i=1"));
  EXPECT_TRUE(Exists("i=2"));
  EXPECT_TRUE(Exists("i=3"));
  EXPECT_TRUE(Exists("i=4"));
}

TEST_P(NoVarySearchCacheTest, SetMaxSizeLarger) {
  for (size_t i = 0; i < kMaxSize; ++i) {
    Insert(QueryWithIParameter(i), "key-order");
  }
  ASSERT_EQ(cache().size(), kMaxSize);

  cache().SetMaxSize(kMaxSize + 2);

  EXPECT_EQ(cache().size(), kMaxSize);
  EXPECT_EQ(cache().max_size(), kMaxSize + 2);

  // All original items should still be there.
  for (size_t i = 0; i < kMaxSize; ++i) {
    EXPECT_TRUE(Exists(QueryWithIParameter(i)));
  }

  // Add two more items.
  Insert(QueryWithIParameter(kMaxSize), "key-order");
  Insert(QueryWithIParameter(kMaxSize + 1), "key-order");

  EXPECT_EQ(cache().size(), kMaxSize + 2);
  EXPECT_TRUE(Exists(QueryWithIParameter(kMaxSize)));
  EXPECT_TRUE(Exists(QueryWithIParameter(kMaxSize + 1)));
}

TEST_P(NoVarySearchCacheTest, SetMaxSizeOnEmptyCache) {
  ASSERT_EQ(cache().size(), 0u);
  cache().SetMaxSize(kMaxSize + 5);
  EXPECT_EQ(cache().size(), 0u);
  EXPECT_EQ(cache().max_size(), kMaxSize + 5);
}

// TODO(https://crbug.com/390216627): Test the various experiments that affect
// the cache key and make sure they also affect NoVarySearchCache.

}  // namespace

}  // namespace net
