/* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
/* vim: set ts=2 et sw=2 tw=80: */
/* Any copyright is dedicated to the Public Domain.
   http://creativecommons.org/publicdomain/zero/1.0/ */

/*=============================================================================
  Globals
=============================================================================*/
XPCOMUtils.defineLazyModuleGetter(this, "Promise", "resource://gre/modules/Promise.jsm");
XPCOMUtils.defineLazyModuleGetter(this, "Task", "resource://gre/modules/Task.jsm");

/*=============================================================================
  Useful constants
=============================================================================*/
const serverRoot = "http://example.com/browser/metro/";
const baseURI = "http://mochi.test:8888/browser/metro/";
const chromeRoot = getRootDirectory(gTestPath);
const kDefaultWait = 2000;
const kDefaultInterval = 50;

/*=============================================================================
  Load Helpers
=============================================================================*/

let splitPath = chromeRoot.split('/');
if (!splitPath[splitPath.length-1]) {
  splitPath.pop();
}
// ../mochitest to make sure we're looking for the libs on the right path
// even for mochiperf tests.
splitPath.pop();
splitPath.push('mochitest');

const mochitestPath = splitPath.join('/') + '/';

[
  "helpers/BookmarksHelper.js",
  "helpers/HistoryHelper.js",
  "helpers/ViewStateHelper.js"
].forEach(function(lib) {
  Services.scriptloader.loadSubScript(mochitestPath + lib, this);
}, this);

/*=============================================================================
  Metro ui helpers
=============================================================================*/

function isLandscapeMode()
{
  return Elements.windowState.getAttribute("viewstate") == "landscape";
}

function setDevPixelEqualToPx()
{
  todo(false, "test depends on devPixelsPerPx set to 1.0 - see bugs 886624 and 859742");
  SpecialPowers.setCharPref("layout.css.devPixelsPerPx", "1.0");
  registerCleanupFunction(function () {
    SpecialPowers.clearUserPref("layout.css.devPixelsPerPx");
  });
}

function checkContextUIMenuItemCount(aCount)
{
  let visibleCount = 0;
  for (let idx = 0; idx < ContextMenuUI.commands.childNodes.length; idx++) {
    if (!ContextMenuUI.commands.childNodes[idx].hidden)
      visibleCount++;
  }
  is(visibleCount, aCount, "command list count");
}

function checkContextUIMenuItemVisibility(aVisibleList)
{
  let errors = 0;
  for (let idx = 0; idx < ContextMenuUI.commands.childNodes.length; idx++) {
    let item = ContextMenuUI.commands.childNodes[idx];
    if (aVisibleList.indexOf(item.id) != -1 && item.hidden) {
      // item should be visible
      errors++;
      info("should be visible:" + item.id);
    } else if (aVisibleList.indexOf(item.id) == -1 && !item.hidden) {
      // item should be hidden
      errors++;
      info("should be hidden:" + item.id);
    }
  }
  is(errors, 0, "context menu item list visibility");
}

function checkMonoclePositionRange(aMonocle, aMinX, aMaxX, aMinY, aMaxY)
{
  let monocle = null;
  if (aMonocle == "start")
    monocle = SelectionHelperUI._startMark;
  else if (aMonocle == "end")
    monocle = SelectionHelperUI._endMark;
  else if (aMonocle == "caret")
    monocle = SelectionHelperUI._caretMark;
  else
    ok(false, "bad monocle id");

  ok(monocle.xPos > aMinX && monocle.xPos < aMaxX,
    "X position is " + monocle.xPos + ", expected between " + aMinX + " and " + aMaxX);
  ok(monocle.yPos > aMinY && monocle.yPos < aMaxY,
    "Y position is " + monocle.yPos + ", expected between " + aMinY + " and " + aMaxY);
}

/*
 * showNotification - displays a test notification with the current
 * browser and waits for the noticiation to be fully displayed.
 *
 * Usage: yield showNotification();
 */
function showNotification()
{
  return Task.spawn(function() {
    let strings = Strings.browser;
    var buttons = [
      {
        isDefault: false,
        label: strings.GetStringFromName("popupButtonAllowOnce2"),
        accessKey: "",
        callback: function() { }
      },
      {
        label: strings.GetStringFromName("popupButtonAlwaysAllow3"),
        accessKey: "",
        callback: function() { }
      },
      {
        label: strings.GetStringFromName("popupButtonNeverWarn3"),
        accessKey: "",
        callback: function() { }
      }
    ];
    let notificationBox = Browser.getNotificationBox();
    const priority = notificationBox.PRIORITY_WARNING_MEDIUM;
    let note = notificationBox.appendNotification("test notification", "popup-blocked",
                                                  "chrome://browser/skin/images/infobar-popup.png",
                                                  priority, buttons);
    yield waitForEvent(notificationBox, "transitionend");
    throw new Task.Result(note);
  });
}

function removeNotifications() {
  Browser.getNotificationBox().removeAllNotifications(true);
}

function getSelection(aElement) {
  if (!aElement)
    return null;

  // chrome text edit
  if (aElement instanceof Ci.nsIDOMXULTextBoxElement) {
    return aElement.QueryInterface(Components.interfaces.nsIDOMXULTextBoxElement)
                   .editor.selection;
  }

  // editable content element
  if (aElement instanceof Ci.nsIDOMNSEditableElement) {
    return aElement.QueryInterface(Ci.nsIDOMNSEditableElement)
                   .editor.selection;
  }

  // document or window
  if (aElement instanceof HTMLDocument || aElement instanceof Window) {
    return aElement.getSelection();
  }

  // browser
  return aElement.contentWindow.getSelection();
}

function getTrimmedSelection(aElement) {
  let sel = getSelection(aElement);
  if (!sel)
    return "";
  return sel.toString().trim();
}

/*
 * clearSelection(aTarget) - clears the current selection in
 * aTarget, shuts down the selection manager and purges all
 * message manager events to insure a reset state for the ui.
 */
function clearSelection(aTarget) {
  SelectionHelperUI.closeEditSession(true);
  getSelection(aTarget).removeAllRanges();
  purgeEventQueue();
}

// Hides the tab and context app bar if they are visible
function hideContextUI()
{
  purgeEventQueue();

  return Task.spawn(function() {
    if (ContextUI.tabbarVisible) {
      let promise = waitForEvent(Elements.tray, "transitionend", null, Elements.tray);
      if (ContextUI.dismiss()) {
        yield promise;
      }
    }

    if (ContextUI.contextAppbarVisible) {
      let promise = waitForEvent(Elements.contextappbar, "transitionend", null, Elements.contextappbar);
      ContextUI.dismissContextAppbar();
      yield promise;
    }
  });
}

function showNavBar()
{
  if (!ContextUI.navbarVisible) {
    let promise = waitForEvent(Elements.navbar, "transitionend");
    ContextUI.displayNavbar();
    return promise;
  }
  return Promise.resolve(null);
}

function hideNavBar()
{
  if (ContextUI.navbarVisible) {
    let promise = waitForEvent(Elements.navbar, "transitionend");
    ContextUI.dismissNavbar();
    return promise;
  }
  return Promise.resolve(null);
}

function fireAppBarDisplayEvent()
{
  let promise = waitForEvent(Elements.tray, "transitionend");
  let event = document.createEvent("Events");
  event.initEvent("MozEdgeUICompleted", true, false);
  gWindow.dispatchEvent(event);
  purgeEventQueue();
  return promise;
}

/*=============================================================================
  General test helpers
=============================================================================*/
let gOpenedTabs = [];

function loadUriInActiveTab(aUri)
{
  return Task.spawn(function() {
    let promise = waitForEvent(getBrowser(), "pageshow");
    BrowserUI.goToURI(aUri);
    yield waitForCondition(function () {
      return getBrowser().currentURI.spec == aUri
    }, "getBrowser().currentURI.spec == " + aUri);
    yield promise;
  });
}

function navForward() {
  return Task.spawn(function() {
    let promise = waitForEvent(getBrowser(), "pageshow");
    EventUtils.synthesizeKey("VK_RIGHT", { altKey: true }, window);
    yield promise;
  });
}

function navBackViaNavButton() {
  return Task.spawn(function() {
    let promise = waitForEvent(getBrowser(), "pageshow");
    let backButton = document.getElementById("overlay-back");
    sendElementTap(window, backButton);
    yield promise;
  });
}

/**
 *  Loads a URL in a new tab asynchronously.
 *
 * Usage:
 *    Task.spawn(function() {
 *      let tab = yield addTab("http://example.com/");
 *      ok(Browser.selectedTab == tab, "the new tab is selected");
 *    });
 *
 * @param aUrl the URL to load
 * @returns a task that resolves to the new tab object after the URL is loaded.
 */
function addTab(aUrl) {
  return Task.spawn(function() {
    info("Opening "+aUrl+" in a new tab");
    let tab = Browser.addTab(aUrl, true);
    yield tab.pageShowPromise;

    is(tab.browser.currentURI.spec, aUrl, aUrl + " is loaded");

    yield hideContextUI();

    gOpenedTabs.push(tab);

    throw new Task.Result(tab);
  });
}

/**
 * Cleans up tabs left open by addTab().
 * This is being called at runTests() after the test loop.
 */
function cleanUpOpenedTabs() {
  let tab;
  while(tab = gOpenedTabs.shift()) {
    cleanupNotificationsForBrowser(tab.browser);
    Browser.closeTab(Browser.getTabFromChrome(tab.chromeTab), { forceClose: true })
  }
}

function cleanupNotificationsForBrowser(aBrowser) {
  let notificationBox = Browser.getNotificationBox(aBrowser);
  notificationBox && notificationBox.removeAllNotifications(true);
}

/**
 * Waits a specified number of miliseconds for a specified event to be
 * fired on a specified element.
 *
 * Usage:
 *    let receivedEvent = waitForEvent(element, "eventName");
 *    // Do some processing here that will cause the event to be fired
 *    // ...
 *    // Now yield until the Promise is fulfilled
 *    yield receivedEvent;
 *    if (receivedEvent && !(receivedEvent instanceof Error)) {
 *      receivedEvent.msg == "eventName";
 *      // ...
 *    }
 *
 * @param aSubject the element that should receive the event
 * @param aEventName the event to wait for
 * @param aTimeoutMs the number of miliseconds to wait before giving up
 * @returns a Promise that resolves to the received event, or to an Error
 */
function waitForEvent(aSubject, aEventName, aTimeoutMs, aTarget) {
  let eventDeferred = Promise.defer();
  let timeoutMs = aTimeoutMs || kDefaultWait;
  let stack = new Error().stack;
  let timerID = setTimeout(function wfe_canceller() {
    aSubject.removeEventListener(aEventName, listener);
    eventDeferred.reject( new Error(aEventName+" event timeout at " + stack) );
  }, timeoutMs);

  var listener = function (aEvent) {
    if (aTarget && aTarget !== aEvent.target)
        return;

    // stop the timeout clock and resume
    clearTimeout(timerID);
    eventDeferred.resolve(aEvent);
  }

  function cleanup(aEventOrError) {
    // unhook listener in case of success or failure
    aSubject.removeEventListener(aEventName, listener);
    return aEventOrError;
  }
  aSubject.addEventListener(aEventName, listener, false);
  return eventDeferred.promise.then(cleanup, cleanup);
}

/**
 * Wait for an nsIMessageManager IPC message.
 */
function waitForMessage(aName, aMessageManager) {
  let deferred = Promise.defer();
  let manager = aMessageManager || messageManager;
  function listener(aMessage) {
    deferred.resolve(aMessage);
  }
  manager.addMessageListener(aName, listener);
  function cleanup(aEventOrError) {
    manager.removeMessageListener(aName, listener);
  }
  return deferred.promise.then(cleanup, cleanup);
}

/**
 * Waits a specified number of miliseconds.
 *
 * Usage:
 *    let wait = yield waitForMs(2000);
 *    ok(wait, "2 seconds should now have elapsed");
 *
 * @param aMs the number of miliseconds to wait for
 * @returns a Promise that resolves to true after the time has elapsed
 */
function waitForMs(aMs) {
  info("Wating for " + aMs + "ms");
  let deferred = Promise.defer();
  let startTime = Date.now();
  setTimeout(done, aMs);

  function done() {
    deferred.resolve(true);
    info("waitForMs finished waiting, waited for "
       + (Date.now() - startTime)
       + "ms");
  }

  return deferred.promise;
}

/**
 * Waits a specified number of miliseconds for a supplied callback to
 * return a truthy value.
 *
 * Usage:
 *     let success = yield waitForCondition(myTestFunction);
 *     if (success && !(success instanceof Error)) {
 *       // ...
 *     }
 *
 * @param aCondition the callback that must return a truthy value
 * @param aTimeoutMs the number of miliseconds to wait before giving up
 * @param aIntervalMs the number of miliseconds between calls to aCondition
 * @returns a Promise that resolves to true, or to an Error
 */
function waitForCondition(aCondition, aTimeoutMs, aIntervalMs) {
  let deferred = Promise.defer();
  let timeoutMs = aTimeoutMs || kDefaultWait;
  let intervalMs = aIntervalMs || kDefaultInterval;
  let startTime = Date.now();
  let stack = new Error().stack;

  function testCondition() {
    let now = Date.now();
    if((now - startTime) > timeoutMs) {
      deferred.reject( new Error("Timed out waiting for condition to be true at " + stack) );
      return;
    }

    let condition;
    try {
      condition = aCondition();
    } catch (e) {
      deferred.reject( new Error("Got exception while attempting to test condition: " + e) );
      return;
    }

    if (condition) {
      deferred.resolve(true);
    } else {
      setTimeout(testCondition, intervalMs);
    }
  }

  setTimeout(testCondition, 0);
  return deferred.promise;
}

/**
 * same as waitForCondition but with better test output.
 *
 * @param aCondition the callback that must return a truthy value
 * @param aTestMsg test condition message printed when the test succeeds or
 * fails. Defaults to the stringified version of aCondition.
 * @param aTimeoutMs the number of miliseconds to wait before giving up
 * @param aIntervalMs the number of miliseconds between calls to aCondition
 * @returns a Promise that resolves to true, or to an Error
 */
function waitForCondition2(aCondition, aTestMsg, aTimeoutMs, aIntervalMs) {
  let deferred = Promise.defer();
  let msg = aTestMsg || aCondition;
  let timeoutMs = aTimeoutMs || kDefaultWait;
  let intervalMs = aIntervalMs || kDefaultInterval;
  let startTime = Date.now();

  function testCondition() {
    let now = Date.now();
    if((now - startTime) > timeoutMs) {
      deferred.reject( new Error("Timed out waiting for " + msg) );
      return;
    }

    let condition;
    try {
      condition = aCondition();
    } catch (e) {
      deferred.reject( new Error("Got exception while attempting to test '" + msg + "': " + e) );
      return;
    }

    if (condition) {
      ok(true, msg);
      deferred.resolve(true);
    } else {
      setTimeout(testCondition, intervalMs);
    }
  }

  setTimeout(testCondition, 0);
  return deferred.promise;
}

/*
 * Waits for an image in a page to load. Wrapper around waitForCondition.
 *
 * @param aWindow the tab or window that contains the image.
 * @param aImageId the id of the image in the page.
 * @returns a Promise that resolves to true, or to an Error
 */
function waitForImageLoad(aWindow, aImageId) {
  let elem = aWindow.document.getElementById(aImageId);
  return waitForCondition(function () {
    let request = elem.getRequest(Ci.nsIImageLoadingContent.CURRENT_REQUEST);
    if (request && (request.imageStatus & request.STATUS_SIZE_AVAILABLE))
      return true;
    return false;
  }, 5000, 100);
}

/**
 * Waits a specified number of miliseconds for an observer event.
 *
 * @param aObsEvent the observer event to wait for
 * @param aTimeoutMs the number of miliseconds to wait before giving up
 * @returns a Promise that resolves to true, or to an Error
 */
function waitForObserver(aObsEvent, aTimeoutMs, aObsData) {
  try {

  let deferred = Promise.defer();
  let timeoutMs = aTimeoutMs || kDefaultWait;
  let timerID = 0;

  var observeWatcher = {
    onEvent: function () {
      clearTimeout(timerID);
      Services.obs.removeObserver(this, aObsEvent);
      deferred.resolve();
    },

    onError: function () {
      clearTimeout(timerID);
      Services.obs.removeObserver(this, aObsEvent);
      deferred.reject(new Error(aObsEvent + " event timeout"));
    },

    observe: function (aSubject, aTopic, aData) {
      if (aTopic == aObsEvent &&
        (!aObsData || (aObsData == aData))) {
        this.onEvent();
      }
    },

    QueryInterface: function (aIID) {
      if (!aIID.equals(Ci.nsIObserver) &&
          !aIID.equals(Ci.nsISupportsWeakReference) &&
          !aIID.equals(Ci.nsISupports)) {
        throw Components.results.NS_ERROR_NO_INTERFACE;
      }
      return this;
    },
  }

  timerID = setTimeout(function wfo_canceller() {
    observeWatcher.onError();
  }, timeoutMs);

  Services.obs.addObserver(observeWatcher, aObsEvent, true);
  return deferred.promise;

  } catch (ex) {
    info(ex.message);
  }
}


/*=============================================================================
 * Input mode helpers - these helpers notify observers to metro_precise_input
 * and metro_imprecise_input respectively, triggering the same behaviour as user touch or mouse input
 *
 * Usage: let promise = waitForObservers("metro_imprecise_input");
 *        notifyImprecise();
 *        yield promise; // you are now in imprecise mode
 *===========================================================================*/
function notifyPrecise()
{
  Services.obs.notifyObservers(null, "metro_precise_input", null);
}

function notifyImprecise()
{
  Services.obs.notifyObservers(null, "metro_imprecise_input", null);
}

/*=============================================================================
 * Native input helpers - these helpers send input directly to the os
 * generating os level input events that get processed by widget and
 * apzc logic.
 *===========================================================================*/
function synthesizeNativeMouse(aElement, aOffsetX, aOffsetY, aMsg) {
  let x = aOffsetX;
  let y = aOffsetY;
  if (aElement) {
    if (aElement.getBoundingClientRect) {
      let rect = aElement.getBoundingClientRect();
      x += rect.left;
      y += rect.top;
    } else if(aElement.left && aElement.top) {
      x += aElement.left;
      y += aElement.top;
    }
  }
  Browser.windowUtils.sendNativeMouseEvent(x, y, aMsg, 0, null);
}

function synthesizeNativeMouseMove(aElement, aOffsetX, aOffsetY) {
  synthesizeNativeMouse(aElement,
                        aOffsetX,
                        aOffsetY,
                        0x0001);  // MOUSEEVENTF_MOVE
}

function synthesizeNativeMouseLDown(aElement, aOffsetX, aOffsetY) {
  synthesizeNativeMouse(aElement,
                        aOffsetX,
                        aOffsetY,
                        0x0002);  // MOUSEEVENTF_LEFTDOWN
}

function synthesizeNativeMouseLUp(aElement, aOffsetX, aOffsetY) {
  synthesizeNativeMouse(aElement,
                        aOffsetX,
                        aOffsetY,
                        0x0004);  // MOUSEEVENTF_LEFTUP
}

function synthesizeNativeMouseRDown(aElement, aOffsetX, aOffsetY) {
  synthesizeNativeMouse(aElement,
                        aOffsetX,
                        aOffsetY,
                        0x0008);  // MOUSEEVENTF_RIGHTDOWN
}

function synthesizeNativeMouseRUp(aElement, aOffsetX, aOffsetY) {
  synthesizeNativeMouse(aElement,
                        aOffsetX,
                        aOffsetY,
                        0x0010);  // MOUSEEVENTF_RIGHTUP
}

function synthesizeNativeMouseMDown(aElement, aOffsetX, aOffsetY) {
  synthesizeNativeMouse(aElement,
                        aOffsetX,
                        aOffsetY,
                        0x0020);  // MOUSEEVENTF_MIDDLEDOWN
}

function synthesizeNativeMouseMUp(aElement, aOffsetX, aOffsetY) {
  synthesizeNativeMouse(aElement,
                        aOffsetX,
                        aOffsetY,
                        0x0040);  // MOUSEEVENTF_MIDDLEUP
}

// WARNING: these calls can trigger the soft keyboard on tablets, but not
// on test slaves (bug 947428).
// WARNING: When testing the apzc, be careful of bug 933990. Events sent
// shortly after loading a page may get ignored.

function sendNativeLongTap(aElement, aX, aY) {
  let coords = logicalCoordsForElement(aElement, aX, aY);
  Browser.windowUtils.sendNativeTouchTap(coords.x, coords.y, true);
}

function sendNativeTap(aElement, aX, aY) {
  let coords = logicalCoordsForElement(aElement, aX, aY);
  Browser.windowUtils.sendNativeTouchTap(coords.x, coords.y, false);
}

function sendNativeDoubleTap(aElement, aX, aY) {
  let coords = logicalCoordsForElement(aElement, aX, aY);
  Browser.windowUtils.sendNativeTouchTap(coords.x, coords.y, false);
  Browser.windowUtils.sendNativeTouchTap(coords.x, coords.y, false);
}

function clearNativeTouchSequence() {
  Browser.windowUtils.clearNativeTouchSequence();
}

/*=============================================================================
 * Synthesized event helpers - these helpers synthesize input events that get
 * dispatched directly to the dom. As such widget and apzc logic is bypassed.
 *===========================================================================*/

/*
 * logicalCoordsForElement - given coordinates relative to top-left of
 * given element, returns logical coordinates for window. If a non-numeric
 * X or Y value is given, a value for the center of the element in that
 * dimension is used.
 *
 * @param aElement element coordinates are relative to.
 * @param aX, aY relative coordinates.
 */
function logicalCoordsForElement (aElement, aX, aY) {
  let coords = { x: null, y: null };
  let rect = aElement.getBoundingClientRect();

  coords.x = isNaN(aX) ? rect.left + (rect.width / 2) : rect.left + aX;
  coords.y = isNaN(aY) ? rect.top + (rect.height / 2) : rect.top + aY;

  return coords;
}

function sendContextMenuMouseClickToElement(aWindow, aElement, aX, aY) {
  let utils = aWindow.QueryInterface(Ci.nsIInterfaceRequestor)
                     .getInterface(Ci.nsIDOMWindowUtils);
  let coords = logicalCoordsForElement(aElement, aX, aY);

  utils.sendMouseEventToWindow("mousedown", coords.x, coords.y, 2, 1, 0);
  utils.sendMouseEventToWindow("mouseup", coords.x, coords.y, 2, 1, 0);
  utils.sendMouseEventToWindow("contextmenu", coords.x, coords.y, 2, 1, 0);
}

function sendMouseClick(aWindow, aX, aY) {
  EventUtils.synthesizeMouseAtPoint(aX, aY, {}, aWindow);
}

/*
 * sendContextMenuClick - simulates a press-hold touch input event. Event
 * is delivered to the main window of the application through the top-level
 * widget.
 *
 * @param aX, aY logical coordinates of the event.
 */
function sendContextMenuClick(aX, aY) {
  let mediator = Components.classes["@mozilla.org/appshell/window-mediator;1"]
                            .getService(Components.interfaces.nsIWindowMediator);
  let mainwin = mediator.getMostRecentWindow("navigator:browser");
  let utils = mainwin.QueryInterface(Components.interfaces.nsIInterfaceRequestor)
                      .getInterface(Components.interfaces.nsIDOMWindowUtils);
  utils.sendMouseEvent("contextmenu", aX, aY, 2, 1, 0, true,
                        1, Ci.nsIDOMMouseEvent.MOZ_SOURCE_TOUCH);
}

/*
 * sendContextMenuClickToSelection - simulates a press-hold touch input event
 * selected text in a window.
 */
function sendContextMenuClickToSelection(aWindow) {
  let selection = aWindow.getSelection();
  if (!selection || !selection.rangeCount) {
    ok(false, "no selection to tap!");
    return;
  }
  let range = selection.getRangeAt(0);
  let rect = range.getBoundingClientRect();
  let x = rect.left + (rect.width / 2);
  let y = rect.top + (rect.height / 2);
  let utils = aWindow.QueryInterface(Components.interfaces.nsIInterfaceRequestor)
                      .getInterface(Components.interfaces.nsIDOMWindowUtils);
  utils.sendMouseEventToWindow("contextmenu", x, y, 2, 1, 0, true,
                                1, Ci.nsIDOMMouseEvent.MOZ_SOURCE_TOUCH);
}

/*
 * sendContextMenuClickToWindow - simulates a press-hold touch input event.
 *
 * @param aWindow window used to retrieve dom window utils, and the
 * target window for the event.
 * @param aX, aY logical coordinates of the event relative to aWindow.
 */
function sendContextMenuClickToWindow(aWindow, aX, aY) {
  let utils = aWindow.QueryInterface(Components.interfaces.nsIInterfaceRequestor)
                      .getInterface(Components.interfaces.nsIDOMWindowUtils);

  utils.sendMouseEventToWindow("contextmenu", aX, aY, 2, 1, 0, true,
                                1, Ci.nsIDOMMouseEvent.MOZ_SOURCE_TOUCH);
}

function sendContextMenuClickToElement(aWindow, aElement, aX, aY) {
  let utils = aWindow.QueryInterface(Components.interfaces.nsIInterfaceRequestor)
                      .getInterface(Components.interfaces.nsIDOMWindowUtils);
  let coords = logicalCoordsForElement(aElement, aX, aY);
  utils.sendMouseEventToWindow("contextmenu", coords.x, coords.y, 2, 1, 0, true,
                                1, Ci.nsIDOMMouseEvent.MOZ_SOURCE_TOUCH);
}

/*
 * sendDoubleTap - simulates a double click or double tap.
 */
function sendDoubleTap(aWindow, aX, aY) {
  EventUtils.synthesizeMouseAtPoint(aX, aY, {
      clickCount: 1,
      inputSource: Ci.nsIDOMMouseEvent.MOZ_SOURCE_TOUCH
    }, aWindow);

  EventUtils.synthesizeMouseAtPoint(aX, aY, {
      clickCount: 2,
      inputSource: Ci.nsIDOMMouseEvent.MOZ_SOURCE_TOUCH
    }, aWindow);
}

function sendTap(aWindow, aX, aY) {
  EventUtils.synthesizeMouseAtPoint(aX, aY, {
      clickCount: 1,
      inputSource: Ci.nsIDOMMouseEvent.MOZ_SOURCE_TOUCH
    }, aWindow);
}

function sendElementTap(aWindow, aElement, aX, aY) {
  let coords = logicalCoordsForElement(aElement, aX, aY);
  EventUtils.synthesizeMouseAtPoint(coords.x, coords.y, {
      clickCount: 1,
      inputSource: Ci.nsIDOMMouseEvent.MOZ_SOURCE_TOUCH
    }, aWindow);
}

/*
 * sendTouchDrag - sends a touch series composed of a touchstart,
 * touchmove, and touchend w3c event.
 */
function sendTouchDrag(aWindow, aStartX, aStartY, aEndX, aEndY) {
  EventUtils.synthesizeTouchAtPoint(aStartX, aStartY, { type: "touchstart" }, aWindow);
  EventUtils.synthesizeTouchAtPoint(aEndX, aEndY, { type: "touchmove" }, aWindow);
  EventUtils.synthesizeTouchAtPoint(aEndX, aEndY, { type: "touchend" }, aWindow);
}

/*
 * TouchDragAndHold - simulates a drag and hold sequence of events.
 */
function TouchDragAndHold() {
}

TouchDragAndHold.prototype = {
  _timeoutStep: 2,
  _numSteps: 50,
  _debug: false,
  _win: null,
  _native: false,
  _pointerId: 1,
  _dui: Components.interfaces.nsIDOMWindowUtils,

  set useNativeEvents(aValue) {
    this._native = aValue;
  },

  set stepTimeout(aValue) {
    this._timeoutStep = aValue;
  },

  set numSteps(aValue) {
    this._numSteps = aValue;
  },

  set nativePointerId(aValue) {
    this._pointerId = aValue;
  },

  callback: function callback() {
    if (this._win == null)
      return;

    if (this._debug) {
      SelectionHelperUI.debugDisplayDebugPoint(this._currentPoint.xPos,
        this._currentPoint.yPos, 5, "#FF0000", true);
    }

    if (++this._step.steps >= this._numSteps) {
      if (this._native) {
        this._utils.sendNativeTouchPoint(this._pointerId, this._dui.TOUCH_CONTACT,
                                         this._endPoint.xPos, this._endPoint.yPos,
                                         1, 90);
      } else {
        EventUtils.synthesizeTouchAtPoint(this._endPoint.xPos, this._endPoint.yPos,
                                          { type: "touchmove" }, this._win);
      }
      this._defer.resolve();
      return;
    }
    this._currentPoint.xPos += this._step.x;
    this._currentPoint.yPos += this._step.y;
    if (this._debug) {
      info("[" + this._step.steps + "] touchmove " + this._currentPoint.xPos + " x " + this._currentPoint.yPos);
    }

    if (this._native) {
      this._utils.sendNativeTouchPoint(this._pointerId, this._dui.TOUCH_CONTACT,
                                       this._currentPoint.xPos, this._currentPoint.yPos,
                                       1, 90);
    } else {
      EventUtils.synthesizeTouchAtPoint(this._currentPoint.xPos, this._currentPoint.yPos,
                                        { type: "touchmove" }, this._win);
    }

    let self = this;
    setTimeout(function () { self.callback(); }, this._timeoutStep);
  },

  start: function start(aWindow, aStartX, aStartY, aEndX, aEndY) {
    this._defer = Promise.defer();
    this._win = aWindow;
    this._utils = aWindow.QueryInterface(Ci.nsIInterfaceRequestor)
                         .getInterface(Ci.nsIDOMWindowUtils);
    this._endPoint = { xPos: aEndX, yPos: aEndY };
    this._currentPoint = { xPos: aStartX, yPos: aStartY };
    this._step = { steps: 0, x: (aEndX - aStartX) / this._numSteps, y: (aEndY - aStartY) / this._numSteps };
    if (this._debug) {
      info("[0] touchstart " + aStartX + " x " + aStartY);
    }
    // flush layout, bug 914847
    this._utils.elementFromPoint(aStartX, aStartY, false, true);
    if (this._native) {
      this._utils.sendNativeTouchPoint(this._pointerId, this._dui.TOUCH_CONTACT,
                                       aStartX, aStartY, 1, 90);
    } else {
      EventUtils.synthesizeTouchAtPoint(aStartX, aStartY, { type: "touchstart" }, aWindow);
    }
    let self = this;
    setTimeout(function () { self.callback(); }, this._timeoutStep);
    return this._defer.promise;
  },

  move: function move(aEndX, aEndY) {
    if (this._win == null)
      return;
    if (this._debug) {
      info("[0] continuation to " + aEndX + " x " + aEndY);
    }
    this._defer = Promise.defer();
    this._step = { steps: 0,
                   x: (aEndX - this._endPoint.xPos) / this._numSteps,
                   y: (aEndY - this._endPoint.yPos) / this._numSteps };
    this._endPoint = { xPos: aEndX, yPos: aEndY };
    let self = this;
    setTimeout(function () { self.callback(); }, this._timeoutStep);
    return this._defer.promise;
  },

  end: function end() {
    if (this._debug) {
      info("[" + this._step.steps + "] touchend " + this._endPoint.xPos + " x " + this._endPoint.yPos);
      SelectionHelperUI.debugClearDebugPoints();
    }
    if (this._native) {
      this._utils.sendNativeTouchPoint(this._pointerId, this._dui.TOUCH_REMOVE,
                                       this._endPoint.xPos, this._endPoint.yPos,
                                       1, 90);
    } else {
      EventUtils.synthesizeTouchAtPoint(this._endPoint.xPos, this._endPoint.yPos,
                                        { type: "touchend" }, this._win);
    }
    this._win = null;
  },
};

/*=============================================================================
  System utilities
=============================================================================*/

/*
 * emptyClipboard - clear the windows clipboard.
 */
function emptyClipboard() {
  Cc["@mozilla.org/widget/clipboard;1"].getService(Ci.nsIClipboard)
                                       .emptyClipboard(Ci.nsIClipboard.kGlobalClipboard);
}

/*
 * purgeEventQueue - purges the event queue on the calling thread.
 * Pumps latent in-process message manager events awaiting delivery.
 */
function purgeEventQueue() {
  let thread = Services.tm.currentThread;
  while (thread.hasPendingEvents()) {
    if (!thread.processNextEvent(true))
      break;
  }
}

/*=============================================================================
  Test-running helpers
=============================================================================*/
let gCurrentTest = null;
let gTests = [];

function runTests() {
  waitForExplicitFinish();

  Task.spawn(function() {
    while((gCurrentTest = gTests.shift())){
      try {
        if ('function' == typeof gCurrentTest.setUp) {
          info("SETUP " + gCurrentTest.desc);
          yield Task.spawn(gCurrentTest.setUp.bind(gCurrentTest));
        }
        try {
          info("RUN " + gCurrentTest.desc);
          yield Task.spawn(gCurrentTest.run.bind(gCurrentTest));
        } finally {
          if ('function' == typeof gCurrentTest.tearDown) {
            info("TEARDOWN " + gCurrentTest.desc);
            yield Task.spawn(gCurrentTest.tearDown.bind(gCurrentTest));
          }
        }
      } catch (ex) {
        ok(false, "runTests: Task failed - " + ex + ' at ' + ex.stack);
      } finally {
        info("END " + gCurrentTest.desc);
      }
    }

    try {
      cleanUpOpenedTabs();

      let badTabs = [];
      Browser.tabs.forEach(function(item, index, array) {
        let location = item.browser.currentURI.spec;
        if (index == 0 && location == "about:blank" || location == "about:start") {
          return;
        }
        ok(false, "Left over tab after test: '" + location + "'");
        badTabs.push(item);
      });

      badTabs.forEach(function(item, index, array) {
        Browser.closeTab(item, { forceClose: true });
      });
    } catch (ex) {
      ok(false, "Cleanup tabs failed - " + ex);
    }

    finish();
  });
}

// wrap a method with a spy that records how and how many times it gets called
// the spy is returned; use spy.restore() to put the original back
function spyOnMethod(aObj, aMethod) {
  let origFunc = aObj[aMethod];
  let spy = function() {
    let callArguments = Array.slice(arguments);
    spy.callCount++;
    spy.calledWith = callArguments;
    spy.argsForCall.push(callArguments);
    return (spy.returnValue = origFunc.apply(aObj, arguments));
  };
  spy.callCount = 0;
  spy.argsForCall = [];
  spy.restore = function() {
    return (aObj[aMethod] = origFunc);
  };
  return (aObj[aMethod] = spy);
}

// replace a method with a stub that records how and how many times it gets called
// the stub is returned; use stub.restore() to put the original back
function stubMethod(aObj, aMethod) {
  let origFunc = aObj[aMethod];
  let func = function() {
    func.calledWith = Array.slice(arguments);
    func.callCount++;
  };
  func.callCount = 0;
  func.restore = function() {
    return (aObj[aMethod] = origFunc);
  };
  aObj[aMethod] = func;
  return func;
}
