// Copyright (c) 2012 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 "content/renderer/dom_storage/dom_storage_cached_area.h"

#include <stdint.h>

#include <list>

#include "base/bind.h"
#include "base/strings/utf_string_conversions.h"
#include "content/renderer/dom_storage/dom_storage_proxy.h"
#include "testing/gtest/include/gtest/gtest.h"

namespace content {

namespace {
// A mock implementation of the DOMStorageProxy interface.
class MockProxy : public DOMStorageProxy {
 public:
  MockProxy() {
    ResetObservations();
  }

  // DOMStorageProxy interface for use by DOMStorageCachedArea.

  void LoadArea(int connection_id,
                DOMStorageValuesMap* values,
                const CompletionCallback& callback) override {
    pending_callbacks_.push_back(callback);
    observed_load_area_ = true;
    observed_connection_id_ = connection_id;
    *values = load_area_return_values_;
  }

  void SetItem(int connection_id,
               const base::string16& key,
               const base::string16& value,
               const GURL& page_url,
               const CompletionCallback& callback) override {
    pending_callbacks_.push_back(callback);
    observed_set_item_ = true;
    observed_connection_id_ = connection_id;
    observed_key_ = key;
    observed_value_ = value;
    observed_page_url_ = page_url;
  }

  void RemoveItem(int connection_id,
                  const base::string16& key,
                  const GURL& page_url,
                  const CompletionCallback& callback) override {
    pending_callbacks_.push_back(callback);
    observed_remove_item_ = true;
    observed_connection_id_ = connection_id;
    observed_key_ = key;
    observed_page_url_ = page_url;
  }

  void ClearArea(int connection_id,
                 const GURL& page_url,
                 const CompletionCallback& callback) override {
    pending_callbacks_.push_back(callback);
    observed_clear_area_ = true;
    observed_connection_id_ = connection_id;
    observed_page_url_ = page_url;
  }

  // Methods and members for use by test fixtures.

  void ResetObservations() {
    observed_load_area_ = false;
    observed_set_item_ = false;
    observed_remove_item_ = false;
    observed_clear_area_ = false;
    observed_connection_id_ = 0;
    observed_key_.clear();
    observed_value_.clear();
    observed_page_url_ = GURL();
  }

  void CompleteAllPendingCallbacks() {
    while (!pending_callbacks_.empty())
      CompleteOnePendingCallback(true);
  }

  void CompleteOnePendingCallback(bool success) {
    ASSERT_TRUE(!pending_callbacks_.empty());
    pending_callbacks_.front().Run(success);
    pending_callbacks_.pop_front();
  }

  typedef std::list<CompletionCallback> CallbackList;

  DOMStorageValuesMap load_area_return_values_;
  CallbackList pending_callbacks_;
  bool observed_load_area_;
  bool observed_set_item_;
  bool observed_remove_item_;
  bool observed_clear_area_;
  int observed_connection_id_;
  base::string16 observed_key_;
  base::string16 observed_value_;
  GURL observed_page_url_;

 private:
  ~MockProxy() override {}
};

}  // namespace

class DOMStorageCachedAreaTest : public testing::Test {
 public:
  DOMStorageCachedAreaTest()
    : kNamespaceId(10),
      kOrigin("http://dom_storage/"),
      kKey(base::ASCIIToUTF16("key")),
      kValue(base::ASCIIToUTF16("value")),
      kPageUrl("http://dom_storage/page") {
  }

  const int64_t kNamespaceId;
  const GURL kOrigin;
  const base::string16 kKey;
  const base::string16 kValue;
  const GURL kPageUrl;

  void SetUp() override { mock_proxy_ = new MockProxy(); }

  bool IsPrimed(DOMStorageCachedArea* cached_area) {
    return cached_area->map_.get();
  }

  bool IsIgnoringAllMutations(DOMStorageCachedArea* cached_area) {
    return cached_area->ignore_all_mutations_;
  }

  bool IsIgnoringKeyMutations(DOMStorageCachedArea* cached_area,
                              const base::string16& key) {
    return cached_area->should_ignore_key_mutation(key);
  }

  void ResetAll(DOMStorageCachedArea* cached_area) {
    cached_area->Reset();
    mock_proxy_->ResetObservations();
    mock_proxy_->pending_callbacks_.clear();
  }

  void ResetCacheOnly(DOMStorageCachedArea* cached_area) {
    cached_area->Reset();
  }

 protected:
  scoped_refptr<MockProxy> mock_proxy_;
};

TEST_F(DOMStorageCachedAreaTest, Basics) {
  EXPECT_TRUE(mock_proxy_->HasOneRef());
  scoped_refptr<DOMStorageCachedArea> cached_area =
      new DOMStorageCachedArea(kNamespaceId, kOrigin, mock_proxy_.get());
  EXPECT_EQ(kNamespaceId, cached_area->namespace_id());
  EXPECT_EQ(kOrigin, cached_area->origin());
  EXPECT_FALSE(mock_proxy_->HasOneRef());
  cached_area->ApplyMutation(base::NullableString16(kKey, false),
                             base::NullableString16(kValue, false));
  EXPECT_FALSE(IsPrimed(cached_area.get()));

  ResetAll(cached_area.get());
  EXPECT_EQ(kNamespaceId, cached_area->namespace_id());
  EXPECT_EQ(kOrigin, cached_area->origin());

  const int kConnectionId = 1;
  EXPECT_EQ(0u, cached_area->GetLength(kConnectionId));
  EXPECT_TRUE(cached_area->SetItem(kConnectionId, kKey, kValue, kPageUrl));
  EXPECT_EQ(1u, cached_area->GetLength(kConnectionId));
  EXPECT_EQ(kKey, cached_area->GetKey(kConnectionId, 0).string());
  EXPECT_EQ(kValue, cached_area->GetItem(kConnectionId, kKey).string());
  cached_area->RemoveItem(kConnectionId, kKey, kPageUrl);
  EXPECT_EQ(0u, cached_area->GetLength(kConnectionId));
}

TEST_F(DOMStorageCachedAreaTest, Getters) {
  const int kConnectionId = 7;
  scoped_refptr<DOMStorageCachedArea> cached_area =
      new DOMStorageCachedArea(kNamespaceId, kOrigin, mock_proxy_.get());

  // GetLength, we expect to see one call to load in the proxy.
  EXPECT_FALSE(IsPrimed(cached_area.get()));
  EXPECT_EQ(0u, cached_area->GetLength(kConnectionId));
  EXPECT_TRUE(IsPrimed(cached_area.get()));
  EXPECT_TRUE(mock_proxy_->observed_load_area_);
  EXPECT_EQ(kConnectionId, mock_proxy_->observed_connection_id_);
  EXPECT_EQ(1u, mock_proxy_->pending_callbacks_.size());
  EXPECT_TRUE(IsIgnoringAllMutations(cached_area.get()));
  mock_proxy_->CompleteAllPendingCallbacks();
  EXPECT_FALSE(IsIgnoringAllMutations(cached_area.get()));

  // GetKey, expect the one call to load.
  ResetAll(cached_area.get());
  EXPECT_FALSE(IsPrimed(cached_area.get()));
  EXPECT_TRUE(cached_area->GetKey(kConnectionId, 2).is_null());
  EXPECT_TRUE(IsPrimed(cached_area.get()));
  EXPECT_TRUE(mock_proxy_->observed_load_area_);
  EXPECT_EQ(kConnectionId, mock_proxy_->observed_connection_id_);
  EXPECT_EQ(1u, mock_proxy_->pending_callbacks_.size());

  // GetItem, ditto.
  ResetAll(cached_area.get());
  EXPECT_FALSE(IsPrimed(cached_area.get()));
  EXPECT_TRUE(cached_area->GetItem(kConnectionId, kKey).is_null());
  EXPECT_TRUE(IsPrimed(cached_area.get()));
  EXPECT_TRUE(mock_proxy_->observed_load_area_);
  EXPECT_EQ(kConnectionId, mock_proxy_->observed_connection_id_);
  EXPECT_EQ(1u, mock_proxy_->pending_callbacks_.size());
}

TEST_F(DOMStorageCachedAreaTest, Setters) {
  const int kConnectionId = 7;
  scoped_refptr<DOMStorageCachedArea> cached_area =
      new DOMStorageCachedArea(kNamespaceId, kOrigin, mock_proxy_.get());

  // SetItem, we expect a call to load followed by a call to set item
  // in the proxy.
  EXPECT_FALSE(IsPrimed(cached_area.get()));
  EXPECT_TRUE(cached_area->SetItem(kConnectionId, kKey, kValue, kPageUrl));
  EXPECT_TRUE(IsPrimed(cached_area.get()));
  EXPECT_TRUE(mock_proxy_->observed_load_area_);
  EXPECT_TRUE(mock_proxy_->observed_set_item_);
  EXPECT_EQ(kConnectionId, mock_proxy_->observed_connection_id_);
  EXPECT_EQ(kPageUrl, mock_proxy_->observed_page_url_);
  EXPECT_EQ(kKey, mock_proxy_->observed_key_);
  EXPECT_EQ(kValue, mock_proxy_->observed_value_);
  EXPECT_EQ(2u, mock_proxy_->pending_callbacks_.size());

  // Clear, we expect a just the one call to clear in the proxy since
  // there's no need to load the data prior to deleting it.
  ResetAll(cached_area.get());
  EXPECT_FALSE(IsPrimed(cached_area.get()));
  cached_area->Clear(kConnectionId, kPageUrl);
  EXPECT_TRUE(IsPrimed(cached_area.get()));
  EXPECT_TRUE(mock_proxy_->observed_clear_area_);
  EXPECT_EQ(kConnectionId, mock_proxy_->observed_connection_id_);
  EXPECT_EQ(kPageUrl, mock_proxy_->observed_page_url_);
  EXPECT_EQ(1u, mock_proxy_->pending_callbacks_.size());

  // RemoveItem with nothing to remove, expect just one call to load.
  ResetAll(cached_area.get());
  EXPECT_FALSE(IsPrimed(cached_area.get()));
  cached_area->RemoveItem(kConnectionId, kKey, kPageUrl);
  EXPECT_TRUE(IsPrimed(cached_area.get()));
  EXPECT_TRUE(mock_proxy_->observed_load_area_);
  EXPECT_FALSE(mock_proxy_->observed_remove_item_);
  EXPECT_EQ(kConnectionId, mock_proxy_->observed_connection_id_);
  EXPECT_EQ(1u, mock_proxy_->pending_callbacks_.size());

  // RemoveItem with something to remove, expect a call to load followed
  // by a call to remove.
  ResetAll(cached_area.get());
  mock_proxy_->load_area_return_values_[kKey] =
      base::NullableString16(kValue, false);
  EXPECT_FALSE(IsPrimed(cached_area.get()));
  cached_area->RemoveItem(kConnectionId, kKey, kPageUrl);
  EXPECT_TRUE(IsPrimed(cached_area.get()));
  EXPECT_TRUE(mock_proxy_->observed_load_area_);
  EXPECT_TRUE(mock_proxy_->observed_remove_item_);
  EXPECT_EQ(kConnectionId, mock_proxy_->observed_connection_id_);
  EXPECT_EQ(kPageUrl, mock_proxy_->observed_page_url_);
  EXPECT_EQ(kKey, mock_proxy_->observed_key_);
  EXPECT_EQ(2u, mock_proxy_->pending_callbacks_.size());
}

TEST_F(DOMStorageCachedAreaTest, MutationsAreIgnoredUntilLoadCompletion) {
  const int kConnectionId = 7;
  scoped_refptr<DOMStorageCachedArea> cached_area =
      new DOMStorageCachedArea(kNamespaceId, kOrigin, mock_proxy_.get());
  EXPECT_TRUE(cached_area->GetItem(kConnectionId, kKey).is_null());
  EXPECT_TRUE(IsPrimed(cached_area.get()));
  EXPECT_TRUE(IsIgnoringAllMutations(cached_area.get()));

  // Before load completion, the mutation should be ignored.
  cached_area->ApplyMutation(base::NullableString16(kKey, false),
                             base::NullableString16(kValue, false));
  EXPECT_TRUE(cached_area->GetItem(kConnectionId, kKey).is_null());

  // Call the load completion callback.
  mock_proxy_->CompleteOnePendingCallback(true);
  EXPECT_FALSE(IsIgnoringAllMutations(cached_area.get()));

  // Verify that mutations are now applied.
  cached_area->ApplyMutation(base::NullableString16(kKey, false),
                             base::NullableString16(kValue, false));
  EXPECT_EQ(kValue, cached_area->GetItem(kConnectionId, kKey).string());
}

TEST_F(DOMStorageCachedAreaTest, MutationsAreIgnoredUntilClearCompletion) {
  const int kConnectionId = 4;
  scoped_refptr<DOMStorageCachedArea> cached_area =
      new DOMStorageCachedArea(kNamespaceId, kOrigin, mock_proxy_.get());
  cached_area->Clear(kConnectionId, kPageUrl);
  EXPECT_TRUE(IsIgnoringAllMutations(cached_area.get()));
  mock_proxy_->CompleteOnePendingCallback(true);
  EXPECT_FALSE(IsIgnoringAllMutations(cached_area.get()));

  // Verify that calling Clear twice works as expected, the first
  // completion callback should have been cancelled.
  ResetCacheOnly(cached_area.get());
  cached_area->Clear(kConnectionId, kPageUrl);
  EXPECT_TRUE(IsIgnoringAllMutations(cached_area.get()));
  cached_area->Clear(kConnectionId, kPageUrl);
  EXPECT_TRUE(IsIgnoringAllMutations(cached_area.get()));
  mock_proxy_->CompleteOnePendingCallback(true);
  EXPECT_TRUE(IsIgnoringAllMutations(cached_area.get()));
  mock_proxy_->CompleteOnePendingCallback(true);
  EXPECT_FALSE(IsIgnoringAllMutations(cached_area.get()));
}

TEST_F(DOMStorageCachedAreaTest, KeyMutationsAreIgnoredUntilCompletion) {
  const int kConnectionId = 8;
  scoped_refptr<DOMStorageCachedArea> cached_area =
      new DOMStorageCachedArea(kNamespaceId, kOrigin, mock_proxy_.get());

  // SetItem
  EXPECT_TRUE(cached_area->SetItem(kConnectionId, kKey, kValue, kPageUrl));
  mock_proxy_->CompleteOnePendingCallback(true);  // load completion
  EXPECT_FALSE(IsIgnoringAllMutations(cached_area.get()));
  EXPECT_TRUE(IsIgnoringKeyMutations(cached_area.get(), kKey));
  cached_area->ApplyMutation(base::NullableString16(kKey, false),
                             base::NullableString16());
  EXPECT_EQ(kValue, cached_area->GetItem(kConnectionId, kKey).string());
  mock_proxy_->CompleteOnePendingCallback(true);  // set completion
  EXPECT_FALSE(IsIgnoringKeyMutations(cached_area.get(), kKey));

  // RemoveItem
  cached_area->RemoveItem(kConnectionId, kKey, kPageUrl);
  EXPECT_TRUE(IsIgnoringKeyMutations(cached_area.get(), kKey));
  mock_proxy_->CompleteOnePendingCallback(true);  // remove completion
  EXPECT_FALSE(IsIgnoringKeyMutations(cached_area.get(), kKey));

  // Multiple mutations to the same key.
  EXPECT_TRUE(cached_area->SetItem(kConnectionId, kKey, kValue, kPageUrl));
  cached_area->RemoveItem(kConnectionId, kKey, kPageUrl);
  EXPECT_TRUE(IsIgnoringKeyMutations(cached_area.get(), kKey));
  mock_proxy_->CompleteOnePendingCallback(true);  // set completion
  EXPECT_TRUE(IsIgnoringKeyMutations(cached_area.get(), kKey));
  mock_proxy_->CompleteOnePendingCallback(true);  // remove completion
  EXPECT_FALSE(IsIgnoringKeyMutations(cached_area.get(), kKey));

  // A failed set item operation should Reset the cache.
  EXPECT_TRUE(cached_area->SetItem(kConnectionId, kKey, kValue, kPageUrl));
  EXPECT_TRUE(IsIgnoringKeyMutations(cached_area.get(), kKey));
  mock_proxy_->CompleteOnePendingCallback(false);
  EXPECT_FALSE(IsPrimed(cached_area.get()));
}

}  // namespace content
