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

#include "chromecast/browser/lru_renderer_cache.h"

#include <memory>
#include <utility>

#include "base/memory/ptr_util.h"
#include "base/memory_coordinator/memory_coordinator_features.h"
#include "base/memory_coordinator/test_memory_consumer_registry.h"
#include "base/test/scoped_feature_list.h"
#include "chromecast/browser/renderer_prelauncher.h"
#include "content/public/browser/site_instance.h"
#include "content/public/test/browser_task_environment.h"
#include "content/public/test/test_browser_context.h"
#include "testing/gmock/include/gmock/gmock.h"
#include "testing/gtest/include/gtest/gtest.h"
#include "url/gurl.h"

#define EXPECT_CREATE_AND_PRELAUNCH(ptr, url)           \
  ptr = new MockPrelauncher(&browser_context_, url);    \
  EXPECT_CALL(*ptr, Prelaunch());                       \
  EXPECT_CALL(factory_, Create(&browser_context_, url)) \
      .WillOnce(Return(ByMove(std::unique_ptr<MockPrelauncher>(ptr))));

#define EXPECT_EVICTION(ptr) EXPECT_CALL(*ptr, Destroy());

using ::testing::_;
using ::testing::ByMove;
using ::testing::Expectation;
using ::testing::Mock;
using ::testing::Return;
using ::testing::StrictMock;

namespace chromecast {

class MockPrelauncher : public RendererPrelauncher {
 public:
  MockPrelauncher(content::BrowserContext* browser_context,
                  const GURL& page_url)
      : RendererPrelauncher(browser_context,
                            page_url) {}
  ~MockPrelauncher() override { Destroy(); }

  MOCK_METHOD0(Prelaunch, void());
  MOCK_METHOD0(Destroy, void());
};

class MockFactory : public RendererPrelauncherFactory {
 public:
  MOCK_METHOD2(Create,
               std::unique_ptr<RendererPrelauncher>(
                   content::BrowserContext* browser_context,
                   const GURL& page_url));
};

class LRURendererCacheTest : public testing::Test {
 protected:
  void SetUp() override {}

  void SetFactory() {
    DCHECK(lru_cache_);
    lru_cache_->SetFactoryForTesting(&factory_);
  }

  void SetMemoryLimit(int percentage) {
    memory_consumer_registry_.NotifyUpdateMemoryLimitAsync(
        percentage, task_environment_.QuitClosure());
    task_environment_.RunUntilQuit();
  }

  void ReleaseMemory() {
    memory_consumer_registry_.NotifyReleaseMemoryAsync(
        task_environment_.QuitClosure());
    task_environment_.RunUntilQuit();
  }

  void SimulateMemoryLimitAndRelease(int percentage) {
    SetMemoryLimit(percentage);
    ReleaseMemory();
  }

  void ReleaseRendererPrelauncherAndWait(const GURL& page_url) {
    lru_cache_->ReleaseRendererPrelauncher(page_url);
    base::SequencedTaskRunner::GetCurrentDefault()->PostTask(
        FROM_HERE, task_environment_.QuitClosure());
    task_environment_.RunUntilQuit();
  }

  content::BrowserTaskEnvironment task_environment_;
  content::TestBrowserContext browser_context_;
  base::TestMemoryConsumerRegistry memory_consumer_registry_;
  MockFactory factory_;
  std::unique_ptr<LRURendererCache> lru_cache_;
};

TEST_F(LRURendererCacheTest, SimpleTakeAndRelease) {
  const GURL kUrl("https://www.one.com");

  lru_cache_ = std::make_unique<LRURendererCache>(&browser_context_, 1);
  SetFactory();
  MockPrelauncher* p1;
  std::unique_ptr<RendererPrelauncher> taken;

  // Don't return a prelauncher the first time, since the cache is empty.
  EXPECT_CALL(factory_, Create(_, _)).Times(0);
  taken = lru_cache_->TakeRendererPrelauncher(kUrl);
  ASSERT_FALSE(taken);
  // Cache: []
  // In-use: [ 1 ]

  // Releasing the prelauncher will cache it and prelaunch for later use.
  EXPECT_CREATE_AND_PRELAUNCH(p1, kUrl);
  ReleaseRendererPrelauncherAndWait(kUrl);
  // Cache: [ 1 ]
  // In-use: []

  // Get the cached prelauncher.
  taken = lru_cache_->TakeRendererPrelauncher(kUrl);
  ASSERT_TRUE(taken);
  ASSERT_TRUE(taken->IsForURL(kUrl));
  // Cache: [ ]
  // In-use: [ 1 ]

  // Return the prelauncher again, it should be cached the same as before.
  EXPECT_CREATE_AND_PRELAUNCH(p1, kUrl);
  ReleaseRendererPrelauncherAndWait(kUrl);
  // Cache: [ 1 ]
  // In-use: []
}

TEST_F(LRURendererCacheTest, SimpleCacheEviction) {
  const GURL kUrl("https://www.one.com");

  lru_cache_ = std::make_unique<LRURendererCache>(&browser_context_, 1);
  SetFactory();
  MockPrelauncher* p1;
  std::unique_ptr<RendererPrelauncher> taken;

  // Fill the cache.
  EXPECT_CALL(factory_, Create(_, _)).Times(0);
  taken = lru_cache_->TakeRendererPrelauncher(kUrl);
  ASSERT_FALSE(taken);
  EXPECT_CREATE_AND_PRELAUNCH(p1, kUrl);
  ReleaseRendererPrelauncherAndWait(kUrl);
  // Cache: [ 1 ]
  // In-use: []

  // Taking a different prelauncher destroys the cached one.
  EXPECT_CALL(factory_, Create(_, _)).Times(0);
  EXPECT_EVICTION(p1);
  taken = lru_cache_->TakeRendererPrelauncher(GURL("https://www.two.com"));
  ASSERT_FALSE(taken);
  // Cache: [ ]
  // In-use: [ 2 ]
}

TEST_F(LRURendererCacheTest, CapacityOne) {
  const GURL kUrl1("https://www.one.com");
  const GURL kUrl2("https://www.two.com");

  lru_cache_ = std::make_unique<LRURendererCache>(&browser_context_, 1);
  SetFactory();
  MockPrelauncher* p1;
  MockPrelauncher* p2;
  std::unique_ptr<RendererPrelauncher> taken;

  // Don't return a prelauncher the first time, since the cache is empty.
  EXPECT_CALL(factory_, Create(_, _)).Times(0);
  taken = lru_cache_->TakeRendererPrelauncher(kUrl1);
  ASSERT_FALSE(taken);
  // Cache: []
  // In-use: [ 1 ]

  // Releasing the prelauncher will cache it and prelaunch for later use.
  EXPECT_CREATE_AND_PRELAUNCH(p1, kUrl1);
  ReleaseRendererPrelauncherAndWait(kUrl1);
  // Cache: [ 1 ]
  // In-use: []

  // Get the cached prelauncher.
  taken = lru_cache_->TakeRendererPrelauncher(kUrl1);
  ASSERT_TRUE(taken);
  ASSERT_TRUE(taken->IsForURL(kUrl1));
  // Cache: [ ]
  // In-use: [ 1 ]

  // Return the prelauncher again, it should be cached the same as before.
  EXPECT_CREATE_AND_PRELAUNCH(p1, kUrl1);
  ReleaseRendererPrelauncherAndWait(kUrl1);
  // Cache: [ 1 ]
  // In-use: []

  // Getting the prelauncher for a non-cached URL will return nullptr. The cache
  // will evict 1 to stay below the renderer limit.
  EXPECT_CALL(factory_, Create(_, _)).Times(0);
  EXPECT_EVICTION(p1);
  taken = lru_cache_->TakeRendererPrelauncher(kUrl2);
  ASSERT_FALSE(taken);
  // Cache: [ ]
  // In-use: [ 2 ]

  // Return prelauncher 2, it should be cached.
  EXPECT_CREATE_AND_PRELAUNCH(p2, kUrl2);
  ReleaseRendererPrelauncherAndWait(kUrl2);
  // Cache: [ 2 ]
  // In-use: [ ]
  taken = lru_cache_->TakeRendererPrelauncher(kUrl2);
  ASSERT_TRUE(taken);
  ASSERT_TRUE(taken->IsForURL(kUrl2));
  // Cache: [ ]
  // In-use: [ 2 ]

  // Return prelauncher 2 once more, it will be cached.
  EXPECT_CREATE_AND_PRELAUNCH(p2, kUrl2);
  ReleaseRendererPrelauncherAndWait(kUrl2);
  // Cache: [ 2 ]
  // In-use: [ ]

  // Prelauncher 1 was evicted when 2 was cached. Taking 1 will evict 2.
  EXPECT_CALL(factory_, Create(_, _)).Times(0);
  EXPECT_EVICTION(p2);
  taken = lru_cache_->TakeRendererPrelauncher(kUrl1);
  ASSERT_FALSE(taken);
  // Cache: [ ]
  // In-use: [ 1 ]

  // Prelauncher 2 was evicted when 1 was taken.
  EXPECT_CALL(factory_, Create(_, _)).Times(0);
  taken = lru_cache_->TakeRendererPrelauncher(kUrl2);
  ASSERT_FALSE(taken);
  // Cache: [ ]
  // In-use: [ 1, 2 ]

  // Returning one of the two in-use pages to the cache won't actually cache it,
  // since there's still exactly 1 renderer in-use.
  EXPECT_CALL(factory_, Create(_, _)).Times(0);
  ReleaseRendererPrelauncherAndWait(kUrl2);
  // Cache: [ ]
  // In-use: [ 1 ]
}

TEST_F(LRURendererCacheTest, CapacityTwo) {
  const GURL kUrl1("https://www.one.com");
  const GURL kUrl2("https://www.two.com");
  const GURL kUrl3("https://www.three.com");

  lru_cache_ = std::make_unique<LRURendererCache>(&browser_context_, 2);
  SetFactory();
  MockPrelauncher* p1;
  MockPrelauncher* p2;
  std::unique_ptr<RendererPrelauncher> taken;

  // Take three renderers.
  EXPECT_CALL(factory_, Create(_, _)).Times(0);
  taken = lru_cache_->TakeRendererPrelauncher(kUrl1);
  ASSERT_FALSE(taken);
  EXPECT_CALL(factory_, Create(_, _)).Times(0);
  taken = lru_cache_->TakeRendererPrelauncher(kUrl2);
  ASSERT_FALSE(taken);
  EXPECT_CALL(factory_, Create(_, _)).Times(0);
  taken = lru_cache_->TakeRendererPrelauncher(kUrl3);
  ASSERT_FALSE(taken);
  // Cache: []
  // In-use: [ 1, 2, 3 ]

  // Don't cache renderer 3 since there are still 2 in use.
  EXPECT_CALL(factory_, Create(_, _)).Times(0);
  ReleaseRendererPrelauncherAndWait(kUrl3);
  // In-use: [ 1, 2 ]

  // Fill the cache with remaining 2 renderers.
  EXPECT_CREATE_AND_PRELAUNCH(p2, kUrl2);
  ReleaseRendererPrelauncherAndWait(kUrl2);
  EXPECT_CREATE_AND_PRELAUNCH(p1, kUrl1);
  ReleaseRendererPrelauncherAndWait(kUrl1);
  // Cache: [ 1, 2 ]
  // In-use: [ ]

  // Cache hit for renderer 1.
  taken = lru_cache_->TakeRendererPrelauncher(kUrl1);
  ASSERT_TRUE(taken);
  ASSERT_TRUE(taken->IsForURL(kUrl1));
  // Cache: [ 2 ]
  // In-use: [ 1 ]

  // Return renderer 1.
  EXPECT_CREATE_AND_PRELAUNCH(p1, kUrl1);
  ReleaseRendererPrelauncherAndWait(kUrl1);
  // Cache: [ 1, 2 ]
  // In-use: [ ]

  // Evict the least-recently cached renderer (2).
  EXPECT_CALL(factory_, Create(_, _)).Times(0);
  EXPECT_EVICTION(p2);
  taken = lru_cache_->TakeRendererPrelauncher(kUrl3);
  ASSERT_FALSE(taken);
  // Cache: [ 1 ]
  // In-use: [ 3 ]

  // Getting renderer 2 will fail since it's no long cached. This will evict
  // renderer 1.
  EXPECT_CALL(factory_, Create(_, _)).Times(0);
  EXPECT_EVICTION(p1);
  taken = lru_cache_->TakeRendererPrelauncher(kUrl2);
  ASSERT_FALSE(taken);
  // Cache: [ ]
  // In-use: [ 2, 3 ]
}

TEST_F(LRURendererCacheTest, MemoryConsumer_Stateful) {
  base::test::ScopedFeatureList scoped_feature_list(
      base::kStatefulMemoryPressure);
  const GURL kUrl1("https://www.one.com");
  const GURL kUrl2("https://www.two.com");

  lru_cache_ = std::make_unique<LRURendererCache>(&browser_context_, 2);
  SetFactory();
  MockPrelauncher* p1;
  MockPrelauncher* p2;

  // Fill the cache with 2 renderers.
  lru_cache_->TakeRendererPrelauncher(kUrl1);
  lru_cache_->TakeRendererPrelauncher(kUrl2);

  EXPECT_CREATE_AND_PRELAUNCH(p2, kUrl2);
  ReleaseRendererPrelauncherAndWait(kUrl2);

  EXPECT_CREATE_AND_PRELAUNCH(p1, kUrl1);
  ReleaseRendererPrelauncherAndWait(kUrl1);
  // Cache: [ 1, 2 ]
  // In-use: []

  // Reduce memory limit to 50% (should allow only 1 renderer).
  // OnUpdateMemoryLimit should NOT evict anything.
  EXPECT_CALL(*p1, Destroy()).Times(0);
  EXPECT_CALL(*p2, Destroy()).Times(0);
  SetMemoryLimit(50);
  // Cache still has 2 renderers.

  // OnReleaseMemory should now perform the eviction.
  EXPECT_EVICTION(p2);
  ReleaseMemory();
  // Cache: [ 1 ]

  // Further reduce memory limit to 0%.
  SetMemoryLimit(0);
  EXPECT_EVICTION(p1);
  ReleaseMemory();
  // Cache: []
}

TEST_F(LRURendererCacheTest, MemoryConsumer_NonStateful) {
  base::test::ScopedFeatureList scoped_feature_list;
  scoped_feature_list.InitAndDisableFeature(base::kStatefulMemoryPressure);
  const GURL kUrl1("https://www.one.com");

  lru_cache_ = std::make_unique<LRURendererCache>(&browser_context_, 1);
  SetFactory();
  MockPrelauncher* p1;

  // Fill the cache.
  lru_cache_->TakeRendererPrelauncher(kUrl1);
  EXPECT_CREATE_AND_PRELAUNCH(p1, kUrl1);
  ReleaseRendererPrelauncherAndWait(kUrl1);
  // Cache: [ 1 ]

  // Set memory limit to 50% (approx. MODERATE allocation). Non-stateful doesn't
  // care.
  SetMemoryLimit(50);
  ReleaseMemory();
  // Cache still has 1 renderer.

  // Set memory limit to 0% (approx. CRITICAL allocation).
  SetMemoryLimit(0);
  EXPECT_EVICTION(p1);
  ReleaseMemory();
  // Cache is cleared.
}

}  // namespace chromecast
