var TestUtils = (function() {
  function randomString() {
    var result = "";
    for (var i = 0; i < 5; i++)
        result += String.fromCharCode(97 + Math.floor(Math.random() * 26));
    return result;
  };

  /**
   * Representation of one datatype.
   * @typedef Datatype
   * @type{object}
   * @property{string} name Name of the datatype.
   * @property{function():boolean} supported
   *     Whether this datatype is supported by this user agent.
   * @method{function():Void} add A function to add an instance of the datatype.
   * @method{function():boolean} isEmpty A function that tests whether
   *     the datatype's storage backend is empty.
   */
  var Datatype;

  var TestUtils = {};

  /**
   * Various storage backends that are part of the 'storage' datatype.
   * @param{Array.<Datatype>}
   */
  TestUtils.STORAGE = [
    {
      "name": "local storage",
      "supported": function() { !!window.localStorage; },
      "add": function() {
        return new Promise(function(resolve, reject) {
          localStorage.setItem(randomString(), randomString());
          resolve();
        });
      },
      "isEmpty": function() {
        return new Promise(function(resolve, reject) {
          resolve(!localStorage.length);
        });
      }
    },
    {
      "name": "Indexed DB",
      "supported": function() { return !!window.indexedDB; },
      "add": function() {
        return new Promise(function(resolve, reject) {
          var request = window.indexedDB.open("database");
          request.onupgradeneeded = function() {
            request.result.createObjectStore("store");
          };
          request.onsuccess = function() {
            request.result.close();
            resolve();
          }
        });
      },
      "isEmpty": function() {
        return new Promise(function(resolve, reject) {
          var request = window.indexedDB.open("database");
          request.onsuccess = function() {
            var database = request.result;
            try {
              var transaction = database.transaction(["store"]);
              resolve(false);
            } catch(error) {
              // The database is empty. However, by testing that, we have also
              // created it, which means that |onupgradeneeded| in the "add"
              // method will not run the next time. Delete the database before
              // reporting that it was empty.
              var deletion = window.indexedDB.deleteDatabase("database");
              deletion.onsuccess = resolve.bind(this, true);
            } finally {
              database.close();
            }
          };
        });
      }
    },
    {
      // TODO(@msramek): We should also test the PERSISTENT filesystem, however,
      // that might require storage permissions.
      "name": "filesystems",
      "supported": function() {
        return window.requestFileSystem || window.webkitRequestFileSystem;
      },
      "add": function() {
        return new Promise(function(resolve, reject) {
          var onSuccess = function(fileSystem) {
            fileSystem.root.getFile('file', {"create": true}, resolve, resolve);
          }
          var onFailure = resolve;

          var requestFileSystem =
              window.requestFileSystem || window.webkitRequestFileSystem;
          requestFileSystem(window.TEMPORARY, 1 /* 1B */,
                            onSuccess, onFailure);
        });
      },
      "isEmpty": function() {
        return new Promise(function(resolve, reject) {
          var onSuccess = function(fileSystem) {
            fileSystem.root.getFile(
                'file', {},
                resolve.bind(this, false) /* opened successfully */,
                resolve.bind(this, true) /* failed to open */);
          }
          var onFailure = resolve.bind(this, true);

          var requestFileSystem =
              window.requestFileSystem || window.webkitRequestFileSystem;
          requestFileSystem(window.TEMPORARY, 1 /* 1B */,
                            onSuccess, onFailure);
        });
      }
    },
    {
      "name": "service workers",
      "supported": function() { return !!navigator.serviceWorker; },
      "add": function() {
        return navigator.serviceWorker.register(
            "support/service_worker.js",
            { scope: "support/"});
      },
      "isEmpty": function() {
        return new Promise(function(resolve, reject) {
          navigator.serviceWorker.getRegistrations()
              .then(function(registrations) {
                resolve(!registrations.length);
              });
        });
      }
    },
    {
      "name": "WebSQL",
      "supported": function() { return !!window.openDatabase; },
      "add": function() {
        return new Promise(function(resolve, reject) {
          var database = window.openDatabase(
              "database", "1.0", "database", 1024 /* 1 kB */);
          database.transaction(function(context) {
            context.executeSql("CREATE TABLE IF NOT EXISTS data (column)");
            context.executeSql(
                "INSERT INTO data (column) VALUES (1)", [], resolve);
          });
        });
      },
      "isEmpty": function() {
        return new Promise(function(resolve, reject) {
          var database = window.openDatabase(
              "database", "1.0", "database", 1024 /* 1 kB */);
          database.transaction(function(context) {
            context.executeSql("CREATE TABLE IF NOT EXISTS data (column)");
            context.executeSql(
                "SELECT * FROM data", [],
                function(transaction, result) {
                  resolve(!result.rows.length);
                });
          });
        });
      }
    }
  ].filter(function(backend) { return backend.supported(); });

  /**
   * All datatypes supported by Clear-Site-Data.
   * @param{Array.<Datatype>}
   */
  TestUtils.DATATYPES = [
    {
      "name": "cookies",
      "supported": function() { return typeof document.cookie == "string"; },
      "add": function() {
        return new Promise(function(resolve, reject) {
          document.cookie = randomString() + "=" + randomString();
          resolve();
        });
      },
      "isEmpty": function() {
        return new Promise(function(resolve, reject) {
          resolve(!document.cookie);
        });
      }
    },
    {
      "name": "storage",
      "supported": TestUtils.STORAGE[0].supported,
      "add": TestUtils.STORAGE[0].add,
      "isEmpty": TestUtils.STORAGE[0].isEmpty,
    }
  ].filter(function(datatype) { return datatype.supported(); });

  /**
   * All possible combinations of datatypes.
   * @property {Array.<Array.<Datatype>>}
   */
  TestUtils.COMBINATIONS = (function() {
    var combinations = [];
    for (var mask = 0; mask < (1 << TestUtils.DATATYPES.length); mask++) {
      var combination = [];

      for (var datatype = 0;
           datatype < TestUtils.DATATYPES.length; datatype++) {
        if (mask & (1 << datatype))
          combination.push(TestUtils.DATATYPES[datatype]);
      }

      combinations.push(combination);
    }
    return combinations;
  })();

  /**
   * Populates |datatypes| by calling the "add" method on each of them,
   * and verifies that they are nonempty.
   * @param {Array.<Datatype>} datatypes to be populated.
   * @private
   */
  function populate(datatypes) {
    return Promise.all(datatypes.map(function(datatype) {
      return new Promise(function(resolve, reject) {
        datatype.add().then(function() {
          datatype.isEmpty().then(function(isEmpty) {
            assert_false(
                isEmpty,
                datatype.name +
                    " has to be nonempty before the test starts.");
            resolve();
          });
        });
      });
    }));
  };

  /**
   * Ensures that all datatypes are nonempty. Should be called in the test
   * setup phase.
   */
  TestUtils.populateDatatypes = populate.bind(this, TestUtils.DATATYPES);

  /**
   * Ensures that all backends of the "storage" datatype are nonempty. Should
   * be called in the test setup phase.
   */
  TestUtils.populateStorage = populate.bind(this, TestUtils.STORAGE);

  /**
   * Get the support server URL that returns a Clear-Site-Data header
   * to clear |datatypes|.
   * @param{Array.<Datatype>} datatypes The list of datatypes to be deleted.
   * @return string The URL to be queried.
   */
  TestUtils.getClearSiteDataUrl = function(datatypes) {
    names = datatypes.map(function(e) { return e.name });
    return "support/echo-clear-site-data.py?" + names.join("&");
  }

  /**
   * @param{string} page_scheme Scheme of the page. "http" or "https".
   * @param{string} resource_scheme Scheme of the resource. "http" or "https".
   * @return The URL of a page that contains a resource requesting the deletion
   *     of storage.
   */
  TestUtils.getPageWithResourceUrl = function(page_scheme, resource_scheme) {
      if (page_scheme != "https" && page_scheme != "http")
        throw "Unsupported scheme: " + page_scheme;
      if (resource_scheme != "https" && resource_scheme != "http")
        throw "Unsupported scheme: " + resource_scheme;
      return page_scheme + "://{{domains[]}}:" +
          (page_scheme == "https" ? {{ports[https][0]}} : {{ports[http][0]}}) +
          "/clear-site-data/support/page_with_resource.sub.html?scheme=" +
          resource_scheme;
  }

  return TestUtils;
})();
