// Copyright (c) 2011 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 <stddef.h>

#include <string>

#include "base/message_loop/message_loop.h"
#include "components/web_cache/browser/web_cache_manager.h"
#include "content/public/test/test_browser_thread.h"
#include "testing/gtest/include/gtest/gtest.h"

using base::Time;
using base::TimeDelta;
using content::BrowserThread;

namespace web_cache {

class WebCacheManagerTest : public testing::Test {
 protected:
  typedef WebCacheManager::StatsMap StatsMap;
  typedef WebCacheManager::Allocation Allocation;
  typedef WebCacheManager::AllocationStrategy AllocationStrategy;

  static const int kRendererID;
  static const int kRendererID2;
  static const WebCacheManager::RendererInfo kStats;
  static const WebCacheManager::RendererInfo kStats2;

  WebCacheManagerTest()
      : ui_thread_(BrowserThread::UI, &message_loop_) {
  }

  // Thunks to access protected members of WebCacheManager
  static std::map<int, WebCacheManager::RendererInfo>& stats(
        WebCacheManager* h) {
    return h->stats_;
  }

  static void SimulateInactivity(WebCacheManager* h, int renderer_id) {
    stats(h)[renderer_id].access = Time::Now() - TimeDelta::FromMinutes(
        WebCacheManager::kRendererInactiveThresholdMinutes);
    h->FindInactiveRenderers();
  }

  static std::set<int>& active_renderers(WebCacheManager* h) {
    return h->active_renderers_;
  }
  static std::set<int>& inactive_renderers(WebCacheManager* h) {
    return h->inactive_renderers_;
  }
  static void GatherStats(WebCacheManager* h,
                          std::set<int> renderers,
                          WebCacheManager::RendererInfo* stats) {
    memset(stats, 0, sizeof(WebCacheManager::RendererInfo));
    h->GatherStats(renderers, &stats->capacity, &stats->live_size,
                   &stats->dead_size);
  }
  static uint64_t GetSize(int tactic,
                          const WebCacheManager::RendererInfo& stats) {
    return WebCacheManager::GetSize(
        static_cast<WebCacheManager::AllocationTactic>(tactic), stats.live_size,
        stats.dead_size);
  }
  static bool AttemptTactic(WebCacheManager* h,
                            int active_tactic,
                            const WebCacheManager::RendererInfo& active_stats,
                            int inactive_tactic,
                            const WebCacheManager::RendererInfo& inactive_stats,
                            std::list<std::pair<int, uint64_t>>* strategy) {
    return h->AttemptTactic(
        static_cast<WebCacheManager::AllocationTactic>(active_tactic),
        active_stats.live_size, active_stats.dead_size,
        static_cast<WebCacheManager::AllocationTactic>(inactive_tactic),
        inactive_stats.live_size, inactive_stats.dead_size, strategy);
  }
  static void AddToStrategy(WebCacheManager* h,
                            std::set<int> renderers,
                            int tactic,
                            uint64_t extra_bytes_to_allocate,
                            std::list<std::pair<int, uint64_t>>* strategy) {
    h->AddToStrategy(renderers,
                     static_cast<WebCacheManager::AllocationTactic>(tactic),
                     extra_bytes_to_allocate,
                     strategy);
  }

  static bool RendererInfoEqual(const WebCacheManager::RendererInfo& lhs,
                                const WebCacheManager::RendererInfo& rhs) {
    return lhs.capacity == rhs.capacity && lhs.live_size == rhs.live_size &&
           lhs.dead_size == rhs.dead_size;
  }

  enum {
    DIVIDE_EVENLY = WebCacheManager::DIVIDE_EVENLY,
    KEEP_CURRENT_WITH_HEADROOM = WebCacheManager::KEEP_CURRENT_WITH_HEADROOM,
    KEEP_CURRENT = WebCacheManager::KEEP_CURRENT,
    KEEP_LIVE_WITH_HEADROOM = WebCacheManager::KEEP_LIVE_WITH_HEADROOM,
    KEEP_LIVE = WebCacheManager::KEEP_LIVE,
  };

  WebCacheManager* manager() { return &manager_; }

 private:
  WebCacheManager manager_;
  base::MessageLoop message_loop_;
  content::TestBrowserThread ui_thread_;
};

// static
const int WebCacheManagerTest::kRendererID = 146;

// static
const int WebCacheManagerTest::kRendererID2 = 245;

// static
const WebCacheManager::RendererInfo WebCacheManagerTest::kStats = {
    base::Time(), 0, 1024 * 1024, 1024 * 1024, 256 * 1024, 512,
};

// static
const WebCacheManager::RendererInfo WebCacheManagerTest::kStats2 = {
    base::Time(), 0, 2 * 1024 * 1024, 2 * 1024 * 1024, 2 * 256 * 1024, 2 * 512,
};

TEST_F(WebCacheManagerTest, AddRemoveRendererTest) {
  EXPECT_EQ(0U, active_renderers(manager()).size());
  EXPECT_EQ(0U, inactive_renderers(manager()).size());

  manager()->Add(kRendererID);
  EXPECT_EQ(1U, active_renderers(manager()).count(kRendererID));
  EXPECT_EQ(0U, inactive_renderers(manager()).count(kRendererID));

  manager()->Remove(kRendererID);
  EXPECT_EQ(0U, active_renderers(manager()).size());
  EXPECT_EQ(0U, inactive_renderers(manager()).size());
}

TEST_F(WebCacheManagerTest, ActiveInactiveTest) {
  manager()->Add(kRendererID);

  manager()->ObserveActivity(kRendererID);
  EXPECT_EQ(1U, active_renderers(manager()).count(kRendererID));
  EXPECT_EQ(0U, inactive_renderers(manager()).count(kRendererID));

  SimulateInactivity(manager(), kRendererID);
  EXPECT_EQ(0U, active_renderers(manager()).count(kRendererID));
  EXPECT_EQ(1U, inactive_renderers(manager()).count(kRendererID));

  manager()->ObserveActivity(kRendererID);
  EXPECT_EQ(1U, active_renderers(manager()).count(kRendererID));
  EXPECT_EQ(0U, inactive_renderers(manager()).count(kRendererID));

  manager()->Remove(kRendererID);
}

TEST_F(WebCacheManagerTest, ObserveStatsTest) {
  manager()->Add(kRendererID);

  EXPECT_EQ(1U, stats(manager()).size());

  manager()->ObserveStats(kRendererID, kStats.min_dead_capacity,
                          kStats.max_dead_capacity, kStats.capacity,
                          kStats.live_size, kStats.dead_size);

  EXPECT_EQ(1U, stats(manager()).size());
  EXPECT_TRUE(RendererInfoEqual(kStats, stats(manager())[kRendererID]));

  manager()->Remove(kRendererID);
}

TEST_F(WebCacheManagerTest, SetGlobalSizeLimitTest) {
  uint64_t limit = manager()->GetDefaultGlobalSizeLimit();
  manager()->SetGlobalSizeLimit(limit);
  EXPECT_EQ(limit, manager()->global_size_limit());

  manager()->SetGlobalSizeLimit(0);
  EXPECT_EQ(0U, manager()->global_size_limit());
}

TEST_F(WebCacheManagerTest, GatherStatsTest) {
  manager()->Add(kRendererID);
  manager()->Add(kRendererID2);

  manager()->ObserveStats(kRendererID, kStats.min_dead_capacity,
                          kStats.max_dead_capacity, kStats.capacity,
                          kStats.live_size, kStats.dead_size);
  manager()->ObserveStats(kRendererID2, kStats2.min_dead_capacity,
                          kStats2.max_dead_capacity, kStats2.capacity,
                          kStats2.live_size, kStats2.dead_size);

  std::set<int> renderer_set;
  renderer_set.insert(kRendererID);

  WebCacheManager::RendererInfo stats;
  GatherStats(manager(), renderer_set, &stats);

  EXPECT_TRUE(RendererInfoEqual(kStats, stats));

  renderer_set.insert(kRendererID2);
  GatherStats(manager(), renderer_set, &stats);

  WebCacheManager::RendererInfo expected_stats = kStats;
  expected_stats.min_dead_capacity += kStats2.min_dead_capacity;
  expected_stats.max_dead_capacity += kStats2.max_dead_capacity;
  expected_stats.capacity += kStats2.capacity;
  expected_stats.live_size += kStats2.live_size;
  expected_stats.dead_size += kStats2.dead_size;

  EXPECT_TRUE(RendererInfoEqual(expected_stats, stats));

  manager()->Remove(kRendererID);
  manager()->Remove(kRendererID2);
}

TEST_F(WebCacheManagerTest, GetSizeTest) {
  EXPECT_EQ(0U, GetSize(DIVIDE_EVENLY, kStats));
  EXPECT_LT(256 * 1024u + 512, GetSize(KEEP_CURRENT_WITH_HEADROOM, kStats));
  EXPECT_EQ(256 * 1024u + 512, GetSize(KEEP_CURRENT, kStats));
  EXPECT_LT(256 * 1024u, GetSize(KEEP_LIVE_WITH_HEADROOM, kStats));
  EXPECT_EQ(256 * 1024u, GetSize(KEEP_LIVE, kStats));
}

TEST_F(WebCacheManagerTest, AttemptTacticTest) {
  manager()->Add(kRendererID);
  manager()->Add(kRendererID2);

  manager()->ObserveActivity(kRendererID);
  SimulateInactivity(manager(), kRendererID2);

  manager()->ObserveStats(kRendererID, kStats.min_dead_capacity,
                          kStats.max_dead_capacity, kStats.capacity,
                          kStats.live_size, kStats.dead_size);
  manager()->ObserveStats(kRendererID2, kStats2.min_dead_capacity,
                          kStats2.max_dead_capacity, kStats2.capacity,
                          kStats2.live_size, kStats2.dead_size);

  manager()->SetGlobalSizeLimit(kStats.live_size + kStats.dead_size +
                                kStats2.live_size + kStats2.dead_size / 2);

  AllocationStrategy strategy;

  EXPECT_FALSE(AttemptTactic(manager(),
                             KEEP_CURRENT,
                             kStats,
                             KEEP_CURRENT,
                             kStats2,
                             &strategy));
  EXPECT_TRUE(strategy.empty());

  EXPECT_TRUE(AttemptTactic(manager(),
                            KEEP_CURRENT,
                            kStats,
                            KEEP_LIVE,
                            kStats2,
                            &strategy));
  EXPECT_EQ(2U, strategy.size());

  AllocationStrategy::iterator iter = strategy.begin();
  while (iter != strategy.end()) {
    if (iter->first == kRendererID)
      EXPECT_LE(kStats.live_size + kStats.dead_size, iter->second);
    else if (iter->first == kRendererID2)
      EXPECT_LE(kStats2.live_size, iter->second);
    else
      ADD_FAILURE();   // Unexpected entry in strategy.
    ++iter;
  }

  manager()->Remove(kRendererID);
  manager()->Remove(kRendererID2);
}

TEST_F(WebCacheManagerTest, AddToStrategyTest) {
  manager()->Add(kRendererID);
  manager()->Add(kRendererID2);

  std::set<int> renderer_set;
  renderer_set.insert(kRendererID);
  renderer_set.insert(kRendererID2);

  manager()->ObserveStats(kRendererID, kStats.min_dead_capacity,
                          kStats.max_dead_capacity, kStats.capacity,
                          kStats.live_size, kStats.dead_size);
  manager()->ObserveStats(kRendererID2, kStats2.min_dead_capacity,
                          kStats2.max_dead_capacity, kStats2.capacity,
                          kStats2.live_size, kStats2.dead_size);

  const uint64_t kExtraBytesToAllocate = 10 * 1024;

  AllocationStrategy strategy;
  AddToStrategy(manager(),
                renderer_set,
                KEEP_CURRENT,
                kExtraBytesToAllocate,
                &strategy);

  EXPECT_EQ(2U, strategy.size());

  uint64_t total_bytes = 0;
  AllocationStrategy::iterator iter = strategy.begin();
  while (iter != strategy.end()) {
    total_bytes += iter->second;

    if (iter->first == kRendererID)
      EXPECT_LE(kStats.live_size + kStats.dead_size, iter->second);
    else if (iter->first == kRendererID2)
      EXPECT_LE(kStats2.live_size + kStats2.dead_size, iter->second);
    else
      ADD_FAILURE();  // Unexpected entry in strategy.
    ++iter;
  }

  uint64_t expected_total_bytes = kExtraBytesToAllocate + kStats.live_size +
                                  kStats.dead_size + kStats2.live_size +
                                  kStats2.dead_size;

  EXPECT_GE(expected_total_bytes, total_bytes);

  manager()->Remove(kRendererID);
  manager()->Remove(kRendererID2);
}

// Regression test for http://crbug.com/12362.
// There are three operations in the following order will cause the crash:
// Remove(kRendererID) -> ObserveActivity(kRendererID) -> Remove(kRendererID2)
// To prevent similar failures in the future, 6 tests are added in total to
// cover all the possible orderings of these three operations.
TEST_F(WebCacheManagerTest,
       CallRemoveRendererAndObserveActivityInAnyOrderShouldNotCrashTest_1) {
  EXPECT_EQ(0U, active_renderers(manager()).size());
  EXPECT_EQ(0U, inactive_renderers(manager()).size());

  // Add, Remove, and ObserveActivity trigger deferred
  // calls to ReviseAllocationStrategy and that we call it directly after each
  // operation to sidestep the need to wait for an unobservable background
  // operation.
  manager()->Add(kRendererID);
  manager()->ReviseAllocationStrategy();
  manager()->Add(kRendererID2);
  manager()->ReviseAllocationStrategy();

  // The following order will cause a crash in http://crbug.com/12362.
  manager()->Remove(kRendererID);
  manager()->ReviseAllocationStrategy();

  manager()->ObserveActivity(kRendererID);
  manager()->ReviseAllocationStrategy();

  manager()->Remove(kRendererID2);
  manager()->ReviseAllocationStrategy();
}

TEST_F(WebCacheManagerTest,
       CallRemoveRendererAndObserveActivityInAnyOrderShouldNotCrashTest_2) {
  EXPECT_EQ(0U, active_renderers(manager()).size());
  EXPECT_EQ(0U, inactive_renderers(manager()).size());

  // Add, Remove, and ObserveActivity trigger deferred
  // calls to ReviseAllocationStrategy and that we call it directly after each
  // operation to sidestep the need to wait for an unobservable background
  // operation.
  manager()->Add(kRendererID);
  manager()->ReviseAllocationStrategy();
  manager()->Add(kRendererID2);
  manager()->ReviseAllocationStrategy();

  manager()->Remove(kRendererID);
  manager()->ReviseAllocationStrategy();

  manager()->Remove(kRendererID2);
  manager()->ReviseAllocationStrategy();

  manager()->ObserveActivity(kRendererID);
  manager()->ReviseAllocationStrategy();

}

TEST_F(WebCacheManagerTest,
       CallRemoveRendererAndObserveActivityInAnyOrderShouldNotCrashTest_3) {
  EXPECT_EQ(0U, active_renderers(manager()).size());
  EXPECT_EQ(0U, inactive_renderers(manager()).size());

  // Add, Remove, and ObserveActivity trigger deferred
  // calls to ReviseAllocationStrategy and that we call it directly after each
  // operation to sidestep the need to wait for an unobservable background
  // operation.
  manager()->Add(kRendererID);
  manager()->ReviseAllocationStrategy();
  manager()->Add(kRendererID2);
  manager()->ReviseAllocationStrategy();

  manager()->ObserveActivity(kRendererID);
  EXPECT_EQ(0U, inactive_renderers(manager()).size());

  manager()->Remove(kRendererID);
  manager()->ReviseAllocationStrategy();

  manager()->Remove(kRendererID2);
  manager()->ReviseAllocationStrategy();
}

TEST_F(WebCacheManagerTest,
       CallRemoveRendererAndObserveActivityInAnyOrderShouldNotCrashTest_4) {
  EXPECT_EQ(0U, active_renderers(manager()).size());
  EXPECT_EQ(0U, inactive_renderers(manager()).size());

  // Add, Remove, and ObserveActivity trigger deferred
  // calls to ReviseAllocationStrategy and that we call it directly after each
  // operation to sidestep the need to wait for an unobservable background
  // operation.
  manager()->Add(kRendererID);
  manager()->ReviseAllocationStrategy();
  manager()->Add(kRendererID2);
  manager()->ReviseAllocationStrategy();

  manager()->ObserveActivity(kRendererID);
  EXPECT_EQ(0U, inactive_renderers(manager()).size());

  manager()->Remove(kRendererID2);
  manager()->ReviseAllocationStrategy();

  manager()->Remove(kRendererID);
  manager()->ReviseAllocationStrategy();
}

TEST_F(WebCacheManagerTest,
       CallRemoveRendererAndObserveActivityInAnyOrderShouldNotCrashTest_5) {
  EXPECT_EQ(0U, active_renderers(manager()).size());
  EXPECT_EQ(0U, inactive_renderers(manager()).size());

  // Add, Remove, and ObserveActivity trigger deferred
  // calls to ReviseAllocationStrategy and that we call it directly after each
  // operation to sidestep the need to wait for an unobservable background
  // operation.
  manager()->Add(kRendererID);
  manager()->ReviseAllocationStrategy();
  manager()->Add(kRendererID2);
  manager()->ReviseAllocationStrategy();

  manager()->Remove(kRendererID2);
  manager()->ReviseAllocationStrategy();

  manager()->ObserveActivity(kRendererID);
  manager()->ReviseAllocationStrategy();

  manager()->Remove(kRendererID);
  manager()->ReviseAllocationStrategy();
}

TEST_F(WebCacheManagerTest,
       CallRemoveRendererAndObserveActivityInAnyOrderShouldNotCrashTest_6) {
  EXPECT_EQ(0U, active_renderers(manager()).size());
  EXPECT_EQ(0U, inactive_renderers(manager()).size());

  // Add, Remove, and ObserveActivity trigger deferred
  // calls to ReviseAllocationStrategy and that we call it directly after each
  // operation to sidestep the need to wait for an unobservable background
  // operation.
  manager()->Add(kRendererID);
  manager()->ReviseAllocationStrategy();
  manager()->Add(kRendererID2);
  manager()->ReviseAllocationStrategy();

  manager()->Remove(kRendererID2);
  manager()->ReviseAllocationStrategy();

  manager()->Remove(kRendererID);
  manager()->ReviseAllocationStrategy();

  manager()->ObserveActivity(kRendererID);
  manager()->ReviseAllocationStrategy();
}

}  // namespace web_cache
