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

#include "components/precache/core/precache_database.h"

#include <stdint.h>

#include <map>

#include "base/containers/hash_tables.h"
#include "base/files/file_path.h"
#include "base/files/scoped_temp_dir.h"
#include "base/memory/scoped_ptr.h"
#include "base/message_loop/message_loop.h"
#include "base/metrics/histogram_base.h"
#include "base/test/histogram_tester.h"
#include "base/time/time.h"
#include "components/history/core/browser/history_constants.h"
#include "testing/gmock/include/gmock/gmock.h"
#include "testing/gtest/include/gtest/gtest.h"
#include "url/gurl.h"

namespace {

using ::testing::ContainerEq;
using ::testing::ElementsAre;
using base::Bucket;

const GURL kURL("http://url.com");
const base::TimeDelta kLatency = base::TimeDelta::FromMilliseconds(5);
const base::Time kFetchTime = base::Time() + base::TimeDelta::FromHours(1000);
const base::Time kOldFetchTime = kFetchTime - base::TimeDelta::FromDays(1);
const int64_t kSize = 5000;

std::map<GURL, base::Time> BuildURLTableMap(const GURL& url,
                                            const base::Time& precache_time) {
  std::map<GURL, base::Time> url_table_map;
  url_table_map[url] = precache_time;
  return url_table_map;
}

}  // namespace

namespace precache {

class PrecacheDatabaseTest : public testing::Test {
 public:
  PrecacheDatabaseTest() {}
  ~PrecacheDatabaseTest() override {}

 protected:
  void SetUp() override {
    precache_database_.reset(new PrecacheDatabase());

    ASSERT_TRUE(scoped_temp_dir_.CreateUniqueTempDir());
    base::FilePath db_path = scoped_temp_dir_.path().Append(
        base::FilePath(FILE_PATH_LITERAL("precache_database")));
    precache_database_->Init(db_path);
  }

  std::map<GURL, base::Time> GetActualURLTableMap() {
    // Flush any buffered writes so that the URL table will be up to date.
    precache_database_->Flush();

    std::map<GURL, base::Time> url_table_map;
    precache_url_table()->GetAllDataForTesting(&url_table_map);
    return url_table_map;
  }

  PrecacheURLTable* precache_url_table() {
    return &precache_database_->precache_url_table_;
  }

  // Convenience methods for recording different types of URL fetches. These
  // exist to improve the readability of the tests.
  void RecordPrecacheFromNetwork(const GURL& url,
                                 base::TimeDelta latency,
                                 const base::Time& fetch_time,
                                 int64_t size);
  void RecordPrecacheFromCache(const GURL& url,
                               const base::Time& fetch_time,
                               int64_t size);
  void RecordFetchFromNetwork(const GURL& url,
                              base::TimeDelta latency,
                              const base::Time& fetch_time,
                              int64_t size);
  void RecordFetchFromNetwork(const GURL& url,
                              base::TimeDelta latency,
                              const base::Time& fetch_time,
                              int64_t size,
                              int host_rank);
  void RecordFetchFromNetworkCellular(const GURL& url,
                                      base::TimeDelta latency,
                                      const base::Time& fetch_time,
                                      int64_t size);
  void RecordFetchFromCache(const GURL& url,
                            const base::Time& fetch_time,
                            int64_t size);
  void RecordFetchFromCacheCellular(const GURL& url,
                                    const base::Time& fetch_time,
                                    int64_t size);

  // Must be declared first so that it is destroyed last.
  base::ScopedTempDir scoped_temp_dir_;

  // Having this MessageLoop member variable causes base::MessageLoop::current()
  // to be set properly.
  base::MessageLoopForUI loop_;

  scoped_ptr<PrecacheDatabase> precache_database_;
  base::HistogramTester histograms_;
  base::HistogramTester::CountsMap expected_histogram_counts_;

  void ExpectNewSample(const std::string& histogram_name,
                       base::HistogramBase::Sample sample) {
    histograms_.ExpectUniqueSample(histogram_name, sample, 1);
    expected_histogram_counts_[histogram_name]++;
  }

  void ExpectNoOtherSamples() {
    EXPECT_THAT(histograms_.GetTotalCountsForPrefix("Precache."),
                ContainerEq(expected_histogram_counts_));
  }
};

void PrecacheDatabaseTest::RecordPrecacheFromNetwork(
    const GURL& url,
    base::TimeDelta latency,
    const base::Time& fetch_time,
    int64_t size) {
  precache_database_->RecordURLPrefetch(url, latency, fetch_time, size,
                                        false /* was_cached */);
}

void PrecacheDatabaseTest::RecordPrecacheFromCache(const GURL& url,
                                                   const base::Time& fetch_time,
                                                   int64_t size) {
  precache_database_->RecordURLPrefetch(url, base::TimeDelta() /* latency */,
                                        fetch_time, size,
                                        true /* was_cached */);
}

void PrecacheDatabaseTest::RecordFetchFromNetwork(const GURL& url,
                                                  base::TimeDelta latency,
                                                  const base::Time& fetch_time,
                                                  int64_t size) {
  precache_database_->RecordURLNonPrefetch(
      url, latency, fetch_time, size, false /* was_cached */,
      history::kMaxTopHosts, false /* is_connection_cellular */);
}

void PrecacheDatabaseTest::RecordFetchFromNetwork(const GURL& url,
                                                  base::TimeDelta latency,
                                                  const base::Time& fetch_time,
                                                  int64_t size,
                                                  int host_rank) {
  precache_database_->RecordURLNonPrefetch(url, latency, fetch_time, size,
                                           false /* was_cached */, host_rank,
                                           false /* is_connection_cellular */);
}

void PrecacheDatabaseTest::RecordFetchFromNetworkCellular(
    const GURL& url,
    base::TimeDelta latency,
    const base::Time& fetch_time,
    int64_t size) {
  precache_database_->RecordURLNonPrefetch(
      url, latency, fetch_time, size, false /* was_cached */,
      history::kMaxTopHosts, true /* is_connection_cellular */);
}

void PrecacheDatabaseTest::RecordFetchFromCache(const GURL& url,
                                                const base::Time& fetch_time,
                                                int64_t size) {
  precache_database_->RecordURLNonPrefetch(
      url, base::TimeDelta() /* latency */, fetch_time, size,
      true /* was_cached */, history::kMaxTopHosts,
      false /* is_connection_cellular */);
}

void PrecacheDatabaseTest::RecordFetchFromCacheCellular(
    const GURL& url,
    const base::Time& fetch_time,
    int64_t size) {
  precache_database_->RecordURLNonPrefetch(
      url, base::TimeDelta() /* latency */, fetch_time, size,
      true /* was_cached */, history::kMaxTopHosts,
      true /* is_connection_cellular */);
}

namespace {

TEST_F(PrecacheDatabaseTest, PrecacheOverNetwork) {
  RecordPrecacheFromNetwork(kURL, kLatency, kFetchTime, kSize);

  EXPECT_EQ(BuildURLTableMap(kURL, kFetchTime), GetActualURLTableMap());

  ExpectNewSample("Precache.DownloadedPrecacheMotivated", kSize);
  ExpectNewSample("Precache.Latency.Prefetch", kLatency.InMilliseconds());
  ExpectNoOtherSamples();
}

TEST_F(PrecacheDatabaseTest, PrecacheFromCacheWithURLTableEntry) {
  precache_url_table()->AddURL(kURL, kOldFetchTime);
  RecordPrecacheFromCache(kURL, kFetchTime, kSize);

  // The URL table entry should have been updated to have |kFetchTime| as the
  // timestamp.
  EXPECT_EQ(BuildURLTableMap(kURL, kFetchTime), GetActualURLTableMap());

  ExpectNewSample("Precache.Latency.Prefetch", 0);
  ExpectNoOtherSamples();
}

TEST_F(PrecacheDatabaseTest, PrecacheFromCacheWithoutURLTableEntry) {
  RecordPrecacheFromCache(kURL, kFetchTime, kSize);

  EXPECT_TRUE(GetActualURLTableMap().empty());

  ExpectNewSample("Precache.Latency.Prefetch", 0);
  ExpectNoOtherSamples();
}

TEST_F(PrecacheDatabaseTest, FetchOverNetwork_NonCellular) {
  RecordFetchFromNetwork(kURL, kLatency, kFetchTime, kSize);

  EXPECT_TRUE(GetActualURLTableMap().empty());

  ExpectNewSample("Precache.DownloadedNonPrecache", kSize);
  ExpectNewSample("Precache.Latency.NonPrefetch", kLatency.InMilliseconds());
  ExpectNewSample("Precache.Latency.NonPrefetch.NonTopHosts",
                  kLatency.InMilliseconds());
  ExpectNoOtherSamples();
}

TEST_F(PrecacheDatabaseTest, FetchOverNetwork_NonCellular_TopHosts) {
  RecordFetchFromNetwork(kURL, kLatency, kFetchTime, kSize, 0 /* host_rank */);

  EXPECT_TRUE(GetActualURLTableMap().empty());

  ExpectNewSample("Precache.DownloadedNonPrecache", kSize);
  ExpectNewSample("Precache.Latency.NonPrefetch", kLatency.InMilliseconds());
  ExpectNewSample("Precache.Latency.NonPrefetch.TopHosts",
                  kLatency.InMilliseconds());
  ExpectNoOtherSamples();
}

TEST_F(PrecacheDatabaseTest, FetchOverNetwork_Cellular) {
  RecordFetchFromNetworkCellular(kURL, kLatency, kFetchTime, kSize);

  EXPECT_TRUE(GetActualURLTableMap().empty());

  ExpectNewSample("Precache.DownloadedNonPrecache", kSize);
  ExpectNewSample("Precache.DownloadedNonPrecache.Cellular", kSize);
  ExpectNewSample("Precache.Latency.NonPrefetch", kLatency.InMilliseconds());
  ExpectNewSample("Precache.Latency.NonPrefetch.NonTopHosts",
                  kLatency.InMilliseconds());
  ExpectNoOtherSamples();
}

TEST_F(PrecacheDatabaseTest, FetchOverNetworkWithURLTableEntry) {
  precache_url_table()->AddURL(kURL, kOldFetchTime);
  RecordFetchFromNetwork(kURL, kLatency, kFetchTime, kSize);

  // The URL table entry should have been deleted.
  EXPECT_TRUE(GetActualURLTableMap().empty());

  ExpectNewSample("Precache.DownloadedNonPrecache", kSize);
  ExpectNewSample("Precache.Latency.NonPrefetch", kLatency.InMilliseconds());
  ExpectNewSample("Precache.Latency.NonPrefetch.NonTopHosts",
                  kLatency.InMilliseconds());
  ExpectNoOtherSamples();
}

TEST_F(PrecacheDatabaseTest, FetchFromCacheWithURLTableEntry_NonCellular) {
  precache_url_table()->AddURL(kURL, kOldFetchTime);
  RecordFetchFromCache(kURL, kFetchTime, kSize);

  // The URL table entry should have been deleted.
  EXPECT_TRUE(GetActualURLTableMap().empty());

  ExpectNewSample("Precache.Latency.NonPrefetch", 0);
  ExpectNewSample("Precache.Latency.NonPrefetch.NonTopHosts", 0);
  ExpectNewSample("Precache.Saved", kSize);
  ExpectNoOtherSamples();
}

TEST_F(PrecacheDatabaseTest, FetchFromCacheWithURLTableEntry_Cellular) {
  precache_url_table()->AddURL(kURL, kOldFetchTime);
  RecordFetchFromCacheCellular(kURL, kFetchTime, kSize);

  // The URL table entry should have been deleted.
  EXPECT_TRUE(GetActualURLTableMap().empty());

  ExpectNewSample("Precache.Latency.NonPrefetch", 0);
  ExpectNewSample("Precache.Latency.NonPrefetch.NonTopHosts", 0);
  ExpectNewSample("Precache.Saved", kSize);
  ExpectNewSample("Precache.Saved.Cellular", kSize);
  ExpectNoOtherSamples();
}

TEST_F(PrecacheDatabaseTest, FetchFromCacheWithoutURLTableEntry) {
  RecordFetchFromCache(kURL, kFetchTime, kSize);

  EXPECT_TRUE(GetActualURLTableMap().empty());

  ExpectNewSample("Precache.Latency.NonPrefetch", 0);
  ExpectNewSample("Precache.Latency.NonPrefetch.NonTopHosts", 0);
  ExpectNoOtherSamples();
}

TEST_F(PrecacheDatabaseTest, DeleteExpiredPrecacheHistory) {
  const base::Time kToday = base::Time() + base::TimeDelta::FromDays(1000);
  const base::Time k59DaysAgo = kToday - base::TimeDelta::FromDays(59);
  const base::Time k61DaysAgo = kToday - base::TimeDelta::FromDays(61);

  precache_url_table()->AddURL(GURL("http://expired-precache.com"), k61DaysAgo);
  precache_url_table()->AddURL(GURL("http://old-precache.com"), k59DaysAgo);

  precache_database_->DeleteExpiredPrecacheHistory(kToday);

  EXPECT_EQ(BuildURLTableMap(GURL("http://old-precache.com"), k59DaysAgo),
            GetActualURLTableMap());
}

TEST_F(PrecacheDatabaseTest, SampleInteraction) {
  const GURL kURL1("http://url1.com");
  const int64_t kSize1 = 1;
  const GURL kURL2("http://url2.com");
  const int64_t kSize2 = 2;
  const GURL kURL3("http://url3.com");
  const int64_t kSize3 = 3;
  const GURL kURL4("http://url4.com");
  const int64_t kSize4 = 4;
  const GURL kURL5("http://url5.com");
  const int64_t kSize5 = 5;

  RecordPrecacheFromNetwork(kURL1, kLatency, kFetchTime, kSize1);
  RecordPrecacheFromNetwork(kURL2, kLatency, kFetchTime, kSize2);
  RecordPrecacheFromNetwork(kURL3, kLatency, kFetchTime, kSize3);
  RecordPrecacheFromNetwork(kURL4, kLatency, kFetchTime, kSize4);

  RecordFetchFromCacheCellular(kURL1, kFetchTime, kSize1);
  RecordFetchFromCacheCellular(kURL1, kFetchTime, kSize1);
  RecordFetchFromNetworkCellular(kURL2, kLatency, kFetchTime, kSize2);
  RecordFetchFromNetworkCellular(kURL5, kLatency, kFetchTime, kSize5);
  RecordFetchFromCacheCellular(kURL5, kFetchTime, kSize5);

  RecordPrecacheFromCache(kURL1, kFetchTime, kSize1);
  RecordPrecacheFromNetwork(kURL2, kLatency, kFetchTime, kSize2);
  RecordPrecacheFromCache(kURL3, kFetchTime, kSize3);
  RecordPrecacheFromCache(kURL4, kFetchTime, kSize4);

  RecordFetchFromCache(kURL1, kFetchTime, kSize1);
  RecordFetchFromNetwork(kURL2, kLatency, kFetchTime, kSize2);
  RecordFetchFromCache(kURL3, kFetchTime, kSize3);
  RecordFetchFromCache(kURL5, kFetchTime, kSize5);

  EXPECT_THAT(histograms_.GetAllSamples("Precache.DownloadedPrecacheMotivated"),
              ElementsAre(Bucket(kSize1, 1), Bucket(kSize2, 2),
                          Bucket(kSize3, 1), Bucket(kSize4, 1)));

  EXPECT_THAT(histograms_.GetAllSamples("Precache.DownloadedNonPrecache"),
              ElementsAre(Bucket(kSize2, 2), Bucket(kSize5, 1)));

  EXPECT_THAT(
      histograms_.GetAllSamples("Precache.DownloadedNonPrecache.Cellular"),
      ElementsAre(Bucket(kSize2, 1), Bucket(kSize5, 1)));

  EXPECT_THAT(histograms_.GetAllSamples("Precache.Latency.Prefetch"),
              ElementsAre(Bucket(0, 3), Bucket(kLatency.InMilliseconds(), 5)));

  EXPECT_THAT(histograms_.GetAllSamples("Precache.Latency.NonPrefetch"),
              ElementsAre(Bucket(0, 6), Bucket(kLatency.InMilliseconds(), 3)));

  EXPECT_THAT(histograms_.GetAllSamples("Precache.Saved"),
              ElementsAre(Bucket(kSize1, 1), Bucket(kSize3, 1)));

  EXPECT_THAT(histograms_.GetAllSamples("Precache.Saved.Cellular"),
              ElementsAre(Bucket(kSize1, 1)));
}

}  // namespace

}  // namespace precache