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

#include "services/network/prefetch_cache.h"

#include "base/strings/string_number_conversions.h"
#include "base/strings/stringprintf.h"
#include "base/test/scoped_feature_list.h"
#include "base/test/task_environment.h"
#include "net/base/isolation_info.h"
#include "net/base/network_isolation_key.h"
#include "net/base/schemeful_site.h"
#include "net/cookies/site_for_cookies.h"
#include "services/network/prefetch_url_loader_client.h"
#include "services/network/public/cpp/features.h"
#include "services/network/public/cpp/resource_request.h"
#include "testing/gtest/include/gtest/gtest.h"
#include "url/gurl.h"
#include "url/origin.h"

namespace network {

namespace {

constexpr size_t kMaxSize = 10;

// These Test* functions return a value of the appropriate type useful for
// testing. By passing in an index, you can make them produce different
// hostnames that are not same-origin with each other.
GURL TestURL(int index = 0) {
  return GURL(base::StringPrintf("https://origin%d.example/i.js", index));
}

url::Origin TestOrigin(int index = 0) {
  return url::Origin::Create(TestURL(index));
}

net::IsolationInfo TestIsolationInfo(int index = 0) {
  return net::IsolationInfo::Create(
      net::IsolationInfo::RequestType::kMainFrame, TestOrigin(index),
      TestOrigin(index), net::SiteForCookies::FromOrigin(TestOrigin(index)));
}

net::NetworkIsolationKey TestNIK(int index = 0) {
  const net::NetworkIsolationKey nik =
      TestIsolationInfo(index).network_isolation_key();
  return nik;
}

ResourceRequest MakeResourceRequest(GURL url,
                                    net::IsolationInfo isolation_info) {
  ResourceRequest request;
  request.url = std::move(url);
  request.trusted_params.emplace();
  request.trusted_params->isolation_info = std::move(isolation_info);
  return request;
}

class PrefetchCacheTest : public ::testing::Test {
 protected:
  PrefetchCacheTest() {
    feature_list_.InitAndEnableFeatureWithParameters(
        features::kNetworkContextPrefetch,
        {{"max_loaders", base::NumberToString(kMaxSize)}});
    erase_grace_time_ = base::GetFieldTrialParamByFeatureAsTimeDelta(
        features::kNetworkContextPrefetch,
        /*name=*/"erase_grace_time",
        /*default_value=*/base::Seconds(1));
  }

  PrefetchCache& cache() { return cache_; }

  void FastForwardBy(base::TimeDelta delta) {
    task_environment_.FastForwardBy(delta);
  }

  base::TimeDelta erase_grace_time() const { return erase_grace_time_; }

  base::TimeDelta half_grace_time() const { return erase_grace_time_ / 2; }

 private:
  base::TimeDelta erase_grace_time_;
  base::test::ScopedFeatureList feature_list_;
  base::test::TaskEnvironment task_environment_{
      base::test::TaskEnvironment::TimeSource::MOCK_TIME};
  PrefetchCache cache_;
};

TEST_F(PrefetchCacheTest, Emplace) {
  PrefetchURLLoaderClient* client =
      cache().Emplace(MakeResourceRequest(TestURL(), TestIsolationInfo()));
  ASSERT_TRUE(client);
  EXPECT_EQ(client->url(), TestURL());
  EXPECT_EQ(client->network_isolation_key(), TestNIK());
}

TEST_F(PrefetchCacheTest, EmplaceNoNIK) {
  ResourceRequest request;
  request.url = TestURL();
  // This will log a warning when debug logging is enabled but it is harmless.
  EXPECT_FALSE(cache().Emplace(request));
}

TEST_F(PrefetchCacheTest, EmplaceTransientNIK) {
  // This will log a warning when debug logging is enabled but it is harmless.
  EXPECT_FALSE(cache().Emplace(MakeResourceRequest(
      TestURL(), net::IsolationInfo::CreateTransient(/*nonce=*/std::nullopt))));
}

TEST_F(PrefetchCacheTest, EmplaceFileURL) {
  // This will log a warning when debug logging is enabled but it is harmless.
  EXPECT_FALSE(cache().Emplace(
      MakeResourceRequest(GURL("file:///tmp/bogus.js"), TestIsolationInfo())));
}

TEST_F(PrefetchCacheTest, EmplaceSameURLNIK) {
  EXPECT_TRUE(
      cache().Emplace(MakeResourceRequest(TestURL(), TestIsolationInfo())));
  EXPECT_FALSE(
      cache().Emplace(MakeResourceRequest(TestURL(), TestIsolationInfo())));
}

TEST_F(PrefetchCacheTest, EmplaceDifferentURLSameNIK) {
  EXPECT_TRUE(
      cache().Emplace(MakeResourceRequest(TestURL(0), TestIsolationInfo())));
  EXPECT_TRUE(
      cache().Emplace(MakeResourceRequest(TestURL(1), TestIsolationInfo())));
}

TEST_F(PrefetchCacheTest, EmplaceSameURLDifferentNIK) {
  EXPECT_TRUE(
      cache().Emplace(MakeResourceRequest(TestURL(), TestIsolationInfo(0))));
  EXPECT_TRUE(
      cache().Emplace(MakeResourceRequest(TestURL(), TestIsolationInfo(1))));
}

TEST_F(PrefetchCacheTest, SuccessfulLookup) {
  PrefetchURLLoaderClient* emplaced_client =
      cache().Emplace(MakeResourceRequest(TestURL(), TestIsolationInfo()));
  EXPECT_TRUE(emplaced_client);
  PrefetchURLLoaderClient* retrieved_client =
      cache().Lookup(TestNIK(), TestURL());

  ASSERT_TRUE(retrieved_client);
  EXPECT_EQ(emplaced_client, retrieved_client);
  EXPECT_EQ(retrieved_client->url(), TestURL());
  EXPECT_EQ(retrieved_client->network_isolation_key(), TestNIK());
}

TEST_F(PrefetchCacheTest, FailedLookup) {
  EXPECT_TRUE(
      cache().Emplace(MakeResourceRequest(TestURL(1), TestIsolationInfo(1))));
  EXPECT_FALSE(cache().Lookup(TestNIK(2), TestURL(2)));
}

TEST_F(PrefetchCacheTest, EmplaceRespectsMaxSize) {
  // Insert kMaxSize distinct items into the cache.
  for (size_t i = 0; i < kMaxSize; ++i) {
    EXPECT_TRUE(
        cache().Emplace(MakeResourceRequest(TestURL(i), TestIsolationInfo())));
  }

  // Verify they are all still there.
  for (size_t i = 0; i < kMaxSize; ++i) {
    EXPECT_TRUE(cache().Lookup(TestNIK(), TestURL(i)));
  }

  // Add another item.
  EXPECT_TRUE(cache().Emplace(
      MakeResourceRequest(TestURL(kMaxSize), TestIsolationInfo())));

  // The oldest one should now be gone.
  EXPECT_FALSE(cache().Lookup(TestNIK(), TestURL(0)));

  // The rest should still be there.
  for (size_t i = 1; i < kMaxSize + 1; ++i) {
    EXPECT_TRUE(cache().Lookup(TestNIK(), TestURL(i)));
  }
}

TEST_F(PrefetchCacheTest, ConsumedNodesNoLongerInCache) {
  auto* client =
      cache().Emplace(MakeResourceRequest(TestURL(), TestIsolationInfo()));
  cache().Consume(client);
  EXPECT_FALSE(cache().Lookup(TestNIK(), TestURL()));
  cache().Erase(client);
}

TEST_F(PrefetchCacheTest, ConsumedNodesDontCountTowardsCacheSize) {
  // Insert kMaxSize distinct items into the cache.
  for (size_t i = 0; i < kMaxSize; ++i) {
    EXPECT_TRUE(
        cache().Emplace(MakeResourceRequest(TestURL(i), TestIsolationInfo())));
  }

  // Consume one of them.
  auto* client = cache().Lookup(TestNIK(), TestURL(kMaxSize - 1));
  cache().Consume(client);

  // Insert a new item into the cache.
  EXPECT_TRUE(cache().Emplace(
      MakeResourceRequest(TestURL(kMaxSize), TestIsolationInfo())));

  // Check the remaining items are all still there.
  for (size_t i = 0; i < kMaxSize - 1; ++i) {
    EXPECT_TRUE(cache().Lookup(TestNIK(), TestURL(i)));
  }

  cache().Erase(client);
}

TEST_F(PrefetchCacheTest, ExpiryHappens) {
  EXPECT_TRUE(
      cache().Emplace(MakeResourceRequest(TestURL(), TestIsolationInfo())));
  FastForwardBy(PrefetchCache::kMaxAge);
  EXPECT_FALSE(cache().Lookup(TestNIK(), TestURL()));
}

// Prefetches that are submitted close together are expired from the cache
// together, to reduce the number of unnecessary wake-ups.
TEST_F(PrefetchCacheTest, TimerSlackIsApplied) {
  const auto kTimeBetweenPrefetches = base::Milliseconds(1);
  EXPECT_TRUE(
      cache().Emplace(MakeResourceRequest(TestURL(0), TestIsolationInfo())));
  FastForwardBy(kTimeBetweenPrefetches);
  EXPECT_TRUE(
      cache().Emplace(MakeResourceRequest(TestURL(1), TestIsolationInfo())));

  FastForwardBy(PrefetchCache::kMaxAge - kTimeBetweenPrefetches);

  EXPECT_FALSE(cache().Lookup(TestNIK(), TestURL(0)));
  EXPECT_FALSE(cache().Lookup(TestNIK(), TestURL(1)));
}

TEST_F(PrefetchCacheTest, SeparatedPrefetchesExpiredSeparatedly) {
  const auto kPrefetchSeparation = PrefetchCache::kMaxAge / 2;
  EXPECT_TRUE(
      cache().Emplace(MakeResourceRequest(TestURL(0), TestIsolationInfo())));
  FastForwardBy(kPrefetchSeparation);
  EXPECT_TRUE(
      cache().Emplace(MakeResourceRequest(TestURL(1), TestIsolationInfo())));

  FastForwardBy(PrefetchCache::kMaxAge - kPrefetchSeparation);

  EXPECT_FALSE(cache().Lookup(TestNIK(), TestURL(0)));
  EXPECT_TRUE(cache().Lookup(TestNIK(), TestURL(1)));

  FastForwardBy(kPrefetchSeparation);
  EXPECT_FALSE(cache().Lookup(TestNIK(), TestURL(1)));
}

TEST_F(PrefetchCacheTest, ConsumedPrefetchesAreNotExpired) {
  auto* client =
      cache().Emplace(MakeResourceRequest(TestURL(0), TestIsolationInfo()));
  ASSERT_TRUE(client);
  FastForwardBy(PrefetchCache::kMaxAge / 2);
  EXPECT_TRUE(
      cache().Emplace(MakeResourceRequest(TestURL(1), TestIsolationInfo())));
  cache().Consume(client);
  FastForwardBy(PrefetchCache::kMaxAge);
  // An ASAN build would catch the error here if the client had been deleted.
  EXPECT_EQ(client->url(), TestURL(0));
  // The other entry still gets expired correctly.
  EXPECT_FALSE(cache().Lookup(TestNIK(), TestURL(1)));

  cache().Erase(client);
}

TEST_F(PrefetchCacheTest, DelayedEraseErasesAfterADelay) {
  auto* client =
      cache().Emplace(MakeResourceRequest(TestURL(0), TestIsolationInfo()));
  ASSERT_TRUE(client);
  cache().DelayedErase(client);
  // Should be still there.
  FastForwardBy(half_grace_time());
  // Still there.
  EXPECT_TRUE(cache().Lookup(TestNIK(), TestURL(0)));
  FastForwardBy(erase_grace_time());
  // Gone.
  EXPECT_FALSE(cache().Lookup(TestNIK(), TestURL(0)));
}

TEST_F(PrefetchCacheTest, MultipleDelayedErases) {
  // This test requires that erase_grace_time() be evenly divisible by two.
  ASSERT_EQ(half_grace_time() + half_grace_time(), erase_grace_time())
      << "This test requires that kEraseGraceTime be evenly divisible by two";

  const auto still_there = [&](int client_index) {
    return cache().Lookup(TestNIK(), TestURL(client_index));
  };

  for (int i = 0; i < 3; ++i) {
    auto* client =
        cache().Emplace(MakeResourceRequest(TestURL(i), TestIsolationInfo()));
    ASSERT_TRUE(client);
    cache().DelayedErase(client);
    FastForwardBy(half_grace_time());
    EXPECT_TRUE(still_there(i));
    if (i >= 1) {
      EXPECT_FALSE(still_there(i - 1));
    }
  }

  EXPECT_TRUE(still_there(2));
  FastForwardBy(half_grace_time());
  EXPECT_FALSE(still_there(2));
}

TEST_F(PrefetchCacheTest, DelayedEraseRacesWithExpiry) {
  auto* client =
      cache().Emplace(MakeResourceRequest(TestURL(0), TestIsolationInfo()));
  ASSERT_TRUE(client);
  FastForwardBy(PrefetchCache::kMaxAge - half_grace_time());
  cache().DelayedErase(client);
  FastForwardBy(erase_grace_time());
  // Should be gone now.
  EXPECT_FALSE(cache().Lookup(TestNIK(), TestURL(0)));
}

}  // namespace

}  // namespace network
