"use strict";

this.EXPORTED_SYMBOLS = [
  "PlacesTestUtils",
];

const {classes: Cc, interfaces: Ci, utils: Cu, results: Cr} = Components;

Cu.importGlobalProperties(["URL"]);

Cu.import("resource://gre/modules/XPCOMUtils.jsm");
Cu.import("resource://gre/modules/Services.jsm");
XPCOMUtils.defineLazyModuleGetter(this, "Task",
                                  "resource://gre/modules/Task.jsm");
XPCOMUtils.defineLazyModuleGetter(this, "PlacesUtils",
                                  "resource://gre/modules/PlacesUtils.jsm");
XPCOMUtils.defineLazyModuleGetter(this, "NetUtil",
                                  "resource://gre/modules/NetUtil.jsm");

this.PlacesTestUtils = Object.freeze({
  /**
   * Asynchronously adds visits to a page.
   *
   * @param aPlaceInfo
   *        Can be an nsIURI, in such a case a single LINK visit will be added.
   *        Otherwise can be an object describing the visit to add, or an array
   *        of these objects:
   *          { uri: nsIURI of the page,
   *            [optional] transition: one of the TRANSITION_* from nsINavHistoryService,
   *            [optional] title: title of the page,
   *            [optional] visitDate: visit date, either in microseconds from the epoch or as a date object
   *            [optional] referrer: nsIURI of the referrer for this visit
   *          }
   *
   * @return {Promise}
   * @resolves When all visits have been added successfully.
   * @rejects JavaScript exception.
   */
  addVisits: Task.async(function* (placeInfo) {
    let places = [];
    let infos = [];

    if (placeInfo instanceof Ci.nsIURI ||
        placeInfo instanceof URL ||
        typeof placeInfo == "string") {
      places.push({ uri: placeInfo });
    }
    else if (Array.isArray(placeInfo)) {
      places = places.concat(placeInfo);
    } else if (typeof placeInfo == "object" && placeInfo.uri) {
      places.push(placeInfo)
    } else {
      throw new Error("Unsupported type passed to addVisits");
    }

    // Create a PageInfo for each entry.
    for (let place of places) {
      let info = {url: place.uri};
      info.title = (typeof place.title === "string") ? place.title : "test visit for " + info.url.spec ;
      if (typeof place.referrer == "string") {
        place.referrer = NetUtil.newURI(place.referrer);
      } else if (place.referrer && place.referrer instanceof URL) {
        place.referrer = NetUtil.newURI(place.referrer.href);
      }
      let visitDate = place.visitDate;
      if (visitDate) {
        if (!(visitDate instanceof Date)) {
          visitDate = PlacesUtils.toDate(visitDate);
        }
      } else {
        visitDate = new Date();
      }
      info.visits = [{
        transition: place.transition,
        date: visitDate,
        referrer: place.referrer
      }];
      infos.push(info);
    }
    return PlacesUtils.history.insertMany(infos);
  }),

  /**
   * Clear all history.
   *
   * @return {Promise}
   * @resolves When history was cleared successfully.
   * @rejects JavaScript exception.
   */
  clearHistory() {
    let expirationFinished = new Promise(resolve => {
      Services.obs.addObserver(function observe(subj, topic, data) {
        Services.obs.removeObserver(observe, topic);
        resolve();
      }, PlacesUtils.TOPIC_EXPIRATION_FINISHED, false);
    });

    return Promise.all([expirationFinished, PlacesUtils.history.clear()]);
  },

  /**
   * Waits for all pending async statements on the default connection.
   *
   * @return {Promise}
   * @resolves When all pending async statements finished.
   * @rejects Never.
   *
   * @note The result is achieved by asynchronously executing a query requiring
   *       a write lock.  Since all statements on the same connection are
   *       serialized, the end of this write operation means that all writes are
   *       complete.  Note that WAL makes so that writers don't block readers, but
   *       this is a problem only across different connections.
   */
  promiseAsyncUpdates() {
    return PlacesUtils.withConnectionWrapper("promiseAsyncUpdates", Task.async(function* (db) {
      try {
        yield db.executeCached("BEGIN EXCLUSIVE");
        yield db.executeCached("COMMIT");
      } catch (ex) {
        // If we fail to start a transaction, it's because there is already one.
        // In such a case we should not try to commit the existing transaction.
      }
    }));
  },

  /**
   * Asynchronously checks if an address is found in the database.
   * @param aURI
   *        nsIURI or address to look for.
   *
   * @return {Promise}
   * @resolves Returns true if the page is found.
   * @rejects JavaScript exception.
   */
  isPageInDB: Task.async(function* (aURI) {
    let url = aURI instanceof Ci.nsIURI ? aURI.spec : aURI;
    let db = yield PlacesUtils.promiseDBConnection();
    let rows = yield db.executeCached(
      "SELECT id FROM moz_places WHERE url_hash = hash(:url) AND url = :url",
      { url });
    return rows.length > 0;
  }),

  /**
   * Asynchronously checks how many visits exist for a specified page.
   * @param aURI
   *        nsIURI or address to look for.
   *
   * @return {Promise}
   * @resolves Returns the number of visits found.
   * @rejects JavaScript exception.
   */
  visitsInDB: Task.async(function* (aURI) {
    let url = aURI instanceof Ci.nsIURI ? aURI.spec : aURI;
    let db = yield PlacesUtils.promiseDBConnection();
    let rows = yield db.executeCached(
      `SELECT count(*) FROM moz_historyvisits v
       JOIN moz_places h ON h.id = v.place_id
       WHERE url_hash = hash(:url) AND url = :url`,
      { url });
    return rows[0].getResultByIndex(0);
  })
});