// Copyright (c) 2017 The Chromium Embedded Framework 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 "tests/ceftests/extensions/extension_test_handler.h"
#include "tests/ceftests/test_util.h"
#include "tests/shared/browser/extension_util.h"

#define STORAGE_TEST_GROUP_ALL(name, test_class) \
  EXTENSION_TEST_GROUP_ALL(ChromeStorage##name, test_class)
#define STORAGE_TEST_GROUP_MINIMAL(name, test_class) \
  EXTENSION_TEST_GROUP_MINIMAL(ChromeStorage##name, test_class)

namespace {

const char kExtensionPath[] = "storage-extension";
const char kSuccessMessage[] = "success";

// Base class for testing chrome.storage methods.
// See https://developer.chrome.com/extensions/storage
class StorageTestHandler : public ExtensionTestHandler {
 public:
  explicit StorageTestHandler(RequestContextType request_context_type)
      : ExtensionTestHandler(request_context_type) {
    // Only creating the extension browser.
    set_create_main_browser(false);
  }

  // CefExtensionHandler methods:
  void OnExtensionLoaded(CefRefPtr<CefExtension> extension) override {
    EXPECT_TRUE(CefCurrentlyOn(TID_UI));
    EXPECT_FALSE(got_loaded_);
    got_loaded_.yes();

    // Verify |extension| contents.
    EXPECT_FALSE(extension->GetIdentifier().empty());
    EXPECT_STREQ(("extensions/" + std::string(kExtensionPath)).c_str(),
                 client::extension_util::GetInternalExtensionResourcePath(
                     extension->GetPath())
                     .c_str());
    TestDictionaryEqual(CreateManifest(), extension->GetManifest());

    EXPECT_FALSE(extension_);
    extension_ = extension;

    CreateBrowserForExtension();
  }

  void OnExtensionUnloaded(CefRefPtr<CefExtension> extension) override {
    EXPECT_TRUE(CefCurrentlyOn(TID_UI));
    EXPECT_TRUE(extension_);
    EXPECT_TRUE(extension_->IsSame(extension));
    EXPECT_FALSE(got_unloaded_);
    got_unloaded_.yes();
    extension_ = NULL;

    // Execute asynchronously so call stacks have a chance to unwind.
    // Will close the browser windows.
    CefPostTask(TID_UI, base::Bind(&StorageTestHandler::DestroyTest, this));
  }

  // CefLoadHandler methods:
  void OnLoadingStateChange(CefRefPtr<CefBrowser> browser,
                            bool isLoading,
                            bool canGoBack,
                            bool canGoForward) override {
    CefRefPtr<CefExtension> extension = browser->GetHost()->GetExtension();
    EXPECT_TRUE(extension);
    EXPECT_TRUE(extension_->IsSame(extension));

    if (isLoading) {
      EXPECT_FALSE(extension_browser_);
      extension_browser_ = browser;
    } else {
      EXPECT_TRUE(browser->IsSame(extension_browser_));

      const std::string& url = browser->GetMainFrame()->GetURL();
      EXPECT_STREQ(extension_url_.c_str(), url.c_str());
    }
  }

  // CefRequestHandler methods:
  CefRefPtr<CefResourceHandler> GetResourceHandler(
      CefRefPtr<CefBrowser> browser,
      CefRefPtr<CefFrame> frame,
      CefRefPtr<CefRequest> request) override {
    EXPECT_TRUE(browser->IsSame(extension_browser_));

    CefRefPtr<CefExtension> extension = browser->GetHost()->GetExtension();
    EXPECT_TRUE(extension);
    EXPECT_TRUE(extension_->IsSame(extension));

    const std::string& url = request->GetURL();
    EXPECT_STREQ(extension_url_.c_str(), url.c_str());

    EXPECT_FALSE(got_url_request_);
    got_url_request_.yes();

    // Handle the resource request.
    return RoutingTestHandler::GetResourceHandler(browser, frame, request);
  }

 protected:
  void OnLoadExtensions() override {
    LoadExtension(kExtensionPath, CreateManifest());
  }

  bool OnMessage(CefRefPtr<CefBrowser> browser,
                 const std::string& message) override {
    if (message == "extension_onload") {
      // From body onLoad in the extension browser.
      EXPECT_TRUE(browser->IsSame(extension_browser_));
      EXPECT_FALSE(got_body_onload_);
      got_body_onload_.yes();
      TriggerStorageApiJSFunction();
      return true;
    }
    EXPECT_TRUE(browser->IsSame(extension_browser_));
    EXPECT_FALSE(got_success_message_);
    got_success_message_.yes();
    EXPECT_STREQ(kSuccessMessage, message.c_str());
    TriggerDestroyTest();
    return true;
  }

  void OnDestroyTest() override {
    extension_browser_ = NULL;

    EXPECT_TRUE(got_loaded_);
    EXPECT_TRUE(got_url_request_);
    EXPECT_TRUE(got_body_onload_);
    EXPECT_TRUE(got_trigger_api_function_);
    EXPECT_TRUE(got_success_message_);
    EXPECT_TRUE(got_unloaded_);
  }

  // Create a manifest that grants access to the storage API.
  virtual CefRefPtr<CefDictionaryValue> CreateManifest() const {
    ApiPermissionsList api_permissions;
    api_permissions.push_back("storage");
    return CreateDefaultManifest(api_permissions);
  }

  // Add resources in the extension browser.
  virtual void OnAddExtensionResources(const std::string& origin) {
    extension_url_ = origin + "extension.html";
    AddResource(extension_url_, GetExtensionHTML(), "text/html");
  }

  // Returns the chrome.storage.* JS that is executed in the extension browser
  // when the triggerStorageApi() JS function is called.
  virtual std::string GetStorageApiJS() const = 0;

  // Returns the JS that will be loaded in the extension browser. This
  // implements the triggerStorageApi() JS function called from
  // TriggerStorageApiJSFunction().
  virtual std::string GetExtensionJS() const {
    return "function triggerStorageApi() {" + GetStorageApiJS() + "}";
  }

  // Returns the HTML that will be loaded in the extension browser.
  virtual std::string GetExtensionHTML() const {
    return "<html><head><script>" + GetExtensionJS() +
           "</script></head><body onLoad=" + GetMessageJS("extension_onload") +
           ">Extension</body></html>";
  }

  virtual void TriggerDestroyTest() {
    // Execute asynchronously so call stacks have a chance to unwind.
    CefPostTask(TID_UI, base::Bind(&StorageTestHandler::UnloadExtension, this,
                                   extension_));
  }

  CefRefPtr<CefExtension> extension() const { return extension_; }
  std::string extension_url() const { return extension_url_; }
  CefRefPtr<CefBrowser> extension_browser() const { return extension_browser_; }

  bool got_success_message() const { return got_success_message_; }

 private:
  void CreateBrowserForExtension() {
    const std::string& identifier = extension_->GetIdentifier();
    EXPECT_FALSE(identifier.empty());
    const std::string& origin =
        client::extension_util::GetExtensionOrigin(identifier);
    EXPECT_FALSE(origin.empty());

    // Add extension resources.
    OnAddExtensionResources(origin);

    // Create a browser to host the extension.
    CreateBrowser(extension_url_, request_context());
  }

  void TriggerStorageApiJSFunction() {
    EXPECT_FALSE(got_trigger_api_function_);
    got_trigger_api_function_.yes();

    extension_browser_->GetMainFrame()->ExecuteJavaScript(
        "triggerStorageApi();", extension_url_, 0);
  }

  CefRefPtr<CefExtension> extension_;
  std::string extension_url_;
  CefRefPtr<CefBrowser> extension_browser_;

  TrackCallback got_loaded_;
  TrackCallback got_url_request_;
  TrackCallback got_body_onload_;
  TrackCallback got_trigger_api_function_;
  TrackCallback got_success_message_;
  TrackCallback got_unloaded_;
};
}  // namespace

namespace {

// Test for chrome.storage.local.set(object items, function callback)
// and  for chrome.storage.local.get(string or array of string or object keys,
//                                   function callback)
class LocalStorageTestHandler : public StorageTestHandler {
 public:
  explicit LocalStorageTestHandler(RequestContextType request_context_type)
      : StorageTestHandler(request_context_type) {}

 protected:
  std::string GetStorageApiJS() const override {
    return "chrome.storage.local.set({\"local_key_1\": \"local_value_1\"}, function() {"
           "chrome.storage.local.get(\"local_key_1\", function (items) {"
           "if(items[\"local_key_1\"] == \"local_value_1\") {" +
           GetMessageJS(kSuccessMessage) +
           "}});"
           "});";
  }

 private:
  IMPLEMENT_REFCOUNTING(LocalStorageTestHandler);
  DISALLOW_COPY_AND_ASSIGN(LocalStorageTestHandler);
};
}  // namespace

STORAGE_TEST_GROUP_ALL(LocalStorage, LocalStorageTestHandler);

namespace {

// Test for chrome.storage.local.getBytesInUse(string or array of string keys,
//                                             function callback)
class LocalStorageGetBytesInUseTestHandler : public StorageTestHandler {
 public:
  explicit LocalStorageGetBytesInUseTestHandler(
      RequestContextType request_context_type)
      : StorageTestHandler(request_context_type) {}

 protected:
  std::string GetStorageApiJS() const override {
    return "chrome.storage.local.set({\"local_key_2\": \"local_value_2\"}, function() {"
           "chrome.storage.local.getBytesInUse(\"local_key_2\", function (bytesInUse) {"
           "if (bytesInUse == 26) {" +
           GetMessageJS(kSuccessMessage) +
           "}});"
           "});";
  }

 private:
  IMPLEMENT_REFCOUNTING(LocalStorageGetBytesInUseTestHandler);
  DISALLOW_COPY_AND_ASSIGN(LocalStorageGetBytesInUseTestHandler);
};
}  // namespace

STORAGE_TEST_GROUP_MINIMAL(LocalStorageGetBytesInUse,
                           LocalStorageGetBytesInUseTestHandler);

namespace {

// Test for chrome.storage.local.remove(string or array of string keys, function
// callback)
class LocalStorageRemoveTestHandler : public StorageTestHandler {
 public:
  explicit LocalStorageRemoveTestHandler(
      RequestContextType request_context_type)
      : StorageTestHandler(request_context_type) {}

 protected:
  std::string GetStorageApiJS() const override {
    return "chrome.storage.local.set({\"local_key_3\": \"local_value_3\"}, function() {"
           "chrome.storage.local.remove(\"local_key_3\", function () {"
           "chrome.storage.local.get(\"local_key_3\", function(items) {"
           "if (items[\"local_key_3\"] == undefined) {" +
           GetMessageJS(kSuccessMessage) +
           "}})})"
           "});";
  }

 private:
  IMPLEMENT_REFCOUNTING(LocalStorageRemoveTestHandler);
  DISALLOW_COPY_AND_ASSIGN(LocalStorageRemoveTestHandler);
};
}  // namespace

STORAGE_TEST_GROUP_MINIMAL(LocalStorageRemove, LocalStorageRemoveTestHandler);

namespace {

// Test for chrome.storage.local.clear(function callback)
class LocalStorageClearTestHandler : public StorageTestHandler {
 public:
  explicit LocalStorageClearTestHandler(RequestContextType request_context_type)
      : StorageTestHandler(request_context_type) {}

 protected:
  std::string GetStorageApiJS() const override {
    return "var value1Cleared = false;"
           "var value2Cleared = false;"
           "function checkCleared() {"
           "if (value1Cleared && value2Cleared) {"
           + GetMessageJS(kSuccessMessage) +
           "}}"
           "chrome.storage.local.set({\"local_key_4\": \"local_value_4\","
           "\"local_key_5\": \"local_value_5\"}, function() {"
           "chrome.storage.local.clear(function () {"

           "chrome.storage.local.get(\"local_key_4\", function(items) {"
           "if (items[\"local_key_4\"] == undefined) {"
           "value1Cleared = true;"
           "checkCleared();"
           "}});"

           "chrome.storage.local.get(\"local_key_5\", function(items) {"
           "if (items[\"local_key_5\"] == undefined) {"
           "value2Cleared = true;"
           "checkCleared();"
           "}});"
           "})});";
  }

 private:
  IMPLEMENT_REFCOUNTING(LocalStorageClearTestHandler);
  DISALLOW_COPY_AND_ASSIGN(LocalStorageClearTestHandler);
};
}  // namespace

STORAGE_TEST_GROUP_MINIMAL(LocalStorageClear, LocalStorageClearTestHandler);

namespace {

// Test for chrome.storage.sync.set(object items, function callback)
// and  for chrome.storage.sync.get(string or array of string or object keys,
//                                   function callback)
class SyncStorageTestHandler : public StorageTestHandler {
 public:
  explicit SyncStorageTestHandler(RequestContextType request_context_type)
      : StorageTestHandler(request_context_type) {}

 protected:
  std::string GetStorageApiJS() const override {
    return "chrome.storage.sync.set({\"sync_key_1\": \"sync_value_1\"}, function() {"
           "chrome.storage.sync.get(\"sync_key_1\", function (items) {"
           "if (items[\"sync_key_1\"] == \"sync_value_1\") {" +
           GetMessageJS(kSuccessMessage) +
           "}});"
           "});";
  }

 private:
  IMPLEMENT_REFCOUNTING(SyncStorageTestHandler);
  DISALLOW_COPY_AND_ASSIGN(SyncStorageTestHandler);
};
}  // namespace

STORAGE_TEST_GROUP_ALL(SyncStorage, SyncStorageTestHandler);

namespace {

// Test for chrome.storage.sync.getBytesInUse(string or array of string keys,
//                                             function callback)
class SyncStorageGetBytesInUseTestHandler : public StorageTestHandler {
 public:
  explicit SyncStorageGetBytesInUseTestHandler(
      RequestContextType request_context_type)
      : StorageTestHandler(request_context_type) {}

 protected:
  std::string GetStorageApiJS() const override {
    return "chrome.storage.sync.set({\"sync_key_2\": \"sync_value_2\"}, function() {"
           "chrome.storage.sync.getBytesInUse(\"sync_key_2\", function (bytesInUse) {"
           "if (bytesInUse == 24) {" +
           GetMessageJS(kSuccessMessage) +
           "}});"
           "});";
  }

 private:
  IMPLEMENT_REFCOUNTING(SyncStorageGetBytesInUseTestHandler);
  DISALLOW_COPY_AND_ASSIGN(SyncStorageGetBytesInUseTestHandler);
};
}  // namespace

STORAGE_TEST_GROUP_MINIMAL(SyncStorageGetBytesInUse,
                           SyncStorageGetBytesInUseTestHandler);

namespace {

// Test for chrome.storage.sync.remove(string or array of string keys, function
// callback)
class SyncStorageRemoveTestHandler : public StorageTestHandler {
 public:
  explicit SyncStorageRemoveTestHandler(RequestContextType request_context_type)
      : StorageTestHandler(request_context_type) {}

 protected:
  std::string GetStorageApiJS() const override {
    return "chrome.storage.sync.set({\"sync_key_3\": \"sync_value_3\"}, function() {"
           "chrome.storage.sync.remove(\"sync_key_3\", function () {"
           "chrome.storage.sync.get(\"sync_key_3\", function(items) {"
           "if (items[\"sync_key_3\"] == undefined) {" +
           GetMessageJS(kSuccessMessage) +
           "}})})"
           "});";
  }

 private:
  IMPLEMENT_REFCOUNTING(SyncStorageRemoveTestHandler);
  DISALLOW_COPY_AND_ASSIGN(SyncStorageRemoveTestHandler);
};
}  // namespace

STORAGE_TEST_GROUP_MINIMAL(SyncStorageRemove, SyncStorageRemoveTestHandler);

namespace {

// Test for chrome.storage.sync.clear(function callback)
class SyncStorageClearTestHandler : public StorageTestHandler {
 public:
  explicit SyncStorageClearTestHandler(RequestContextType request_context_type)
      : StorageTestHandler(request_context_type) {}

 protected:
  std::string GetStorageApiJS() const override {
    return "var value1Cleared = false;"
           "var value2Cleared = false;"

           "function checkCleared() {"
           "if (value1Cleared && value2Cleared) {"
           + GetMessageJS(kSuccessMessage) +
           "}}"

           "chrome.storage.sync.set({\"sync_key_4\": \"sync_value_4\","
           "\"sync_key_5\": \"sync_value_5\"}, function() {"
           "chrome.storage.sync.clear(function () {"

           "chrome.storage.sync.get(\"sync_key_4\", function(items) {"
           "if (items[\"sync_key_4\"] == undefined) {"
           "value1Cleared = true;"
           "checkCleared();"
           "}});"

           "chrome.storage.sync.get(\"sync_key_5\", function(items) {"
           "if (items[\"sync_key_5\"] == undefined) {"
           "value2Cleared = true;"
           "checkCleared();"
           "}});"

           "})});";
  }

 private:
  IMPLEMENT_REFCOUNTING(SyncStorageClearTestHandler);
  DISALLOW_COPY_AND_ASSIGN(SyncStorageClearTestHandler);
};
}  // namespace

STORAGE_TEST_GROUP_MINIMAL(SyncStorageClear, SyncStorageClearTestHandler);
