/* Any copyright is dedicated to the Public Domain.
   http://creativecommons.org/publicdomain/zero/1.0/ */

"use strict";

/* eslint no-unused-vars: [2, {"vars": "local"}] */
/* import-globals-from ../../../framework/test/shared-head.js */
/* import-globals-from ../../../framework/test/shared-redux-head.js */
/* import-globals-from ../../../commandline/test/helpers.js */
/* import-globals-from ../../../inspector/test/shared-head.js */

Services.scriptloader.loadSubScript(
  "chrome://mochitests/content/browser/devtools/client/framework/test/shared-head.js",
  this);
Services.scriptloader.loadSubScript(
  "chrome://mochitests/content/browser/devtools/client/framework/test/shared-redux-head.js",
  this);

// Import the GCLI test helper
Services.scriptloader.loadSubScript(
  "chrome://mochitests/content/browser/devtools/client/commandline/test/helpers.js",
  this);

// Import helpers registering the test-actor in remote targets
Services.scriptloader.loadSubScript(
  "chrome://mochitests/content/browser/devtools/client/shared/test/test-actor-registry.js",
  this);

// Import helpers for the inspector that are also shared with others
Services.scriptloader.loadSubScript(
  "chrome://mochitests/content/browser/devtools/client/inspector/test/shared-head.js",
  this);

const E10S_MULTI_ENABLED = Services.prefs.getIntPref("dom.ipc.processCount") > 1;
const TEST_URI_ROOT = "http://example.com/browser/devtools/client/responsive.html/test/browser/";
const OPEN_DEVICE_MODAL_VALUE = "OPEN_DEVICE_MODAL";

const { _loadPreferredDevices } = require("devtools/client/responsive.html/actions/devices");
const { getOwnerWindow } = require("sdk/tabs/utils");
const asyncStorage = require("devtools/shared/async-storage");
const { addDevice, removeDevice } = require("devtools/client/shared/devices");

SimpleTest.requestCompleteLog();
SimpleTest.waitForExplicitFinish();

// Toggling the RDM UI involves several docShell swap operations, which are somewhat slow
// on debug builds. Usually we are just barely over the limit, so a blanket factor of 2
// should be enough.
requestLongerTimeout(2);

flags.testing = true;
Services.prefs.clearUserPref("devtools.responsive.html.displayedDeviceList");
Services.prefs.setCharPref("devtools.devices.url",
  TEST_URI_ROOT + "devices.json");
Services.prefs.setBoolPref("devtools.responsive.html.enabled", true);

registerCleanupFunction(() => {
  flags.testing = false;
  Services.prefs.clearUserPref("devtools.devices.url");
  Services.prefs.clearUserPref("devtools.responsive.html.enabled");
  Services.prefs.clearUserPref("devtools.responsive.html.displayedDeviceList");
  asyncStorage.removeItem("devtools.devices.url_cache");
  asyncStorage.removeItem("devtools.devices.local");
});

// This depends on the "devtools.responsive.html.enabled" pref
const { ResponsiveUIManager } = require("resource://devtools/client/responsivedesign/responsivedesign.jsm");

/**
 * Open responsive design mode for the given tab.
 */
var openRDM = Task.async(function* (tab) {
  info("Opening responsive design mode");
  let manager = ResponsiveUIManager;
  let ui = yield manager.openIfNeeded(getOwnerWindow(tab), tab);
  info("Responsive design mode opened");
  return { ui, manager };
});

/**
 * Close responsive design mode for the given tab.
 */
var closeRDM = Task.async(function* (tab, options) {
  info("Closing responsive design mode");
  let manager = ResponsiveUIManager;
  yield manager.closeIfNeeded(getOwnerWindow(tab), tab, options);
  info("Responsive design mode closed");
});

/**
 * Adds a new test task that adds a tab with the given URL, opens responsive
 * design mode, runs the given generator, closes responsive design mode, and
 * removes the tab.
 *
 * Example usage:
 *
 *   addRDMTask(TEST_URL, function*({ ui, manager }) {
 *     // Your tests go here...
 *   });
 */
function addRDMTask(url, generator) {
  add_task(function* () {
    const tab = yield addTab(url);
    const results = yield openRDM(tab);

    try {
      yield* generator(results);
    } catch (err) {
      ok(false, "Got an error: " + DevToolsUtils.safeErrorString(err));
    }

    yield closeRDM(tab);
    yield removeTab(tab);
  });
}

function spawnViewportTask(ui, args, task) {
  return ContentTask.spawn(ui.getViewportBrowser(), args, task);
}

function waitForFrameLoad(ui, targetURL) {
  return spawnViewportTask(ui, { targetURL }, function* (args) {
    if ((content.document.readyState == "complete" ||
         content.document.readyState == "interactive") &&
        content.location.href == args.targetURL) {
      return;
    }
    yield ContentTaskUtils.waitForEvent(this, "DOMContentLoaded");
  });
}

function waitForViewportResizeTo(ui, width, height) {
  return new Promise(Task.async(function* (resolve) {
    let isSizeMatching = (data) => data.width == width && data.height == height;

    // If the viewport has already the expected size, we resolve the promise immediately.
    let size = yield getContentSize(ui);
    if (isSizeMatching(size)) {
      resolve();
      return;
    }

    // Otherwise, we'll listen to both content's resize event and browser's load end;
    // since a racing condition can happen, where the content's listener is added after
    // the resize, because the content's document was reloaded; therefore the test would
    // hang forever. See bug 1302879.
    let browser = ui.getViewportBrowser();

    let onResize = (_, data) => {
      if (!isSizeMatching(data)) {
        return;
      }
      ui.off("content-resize", onResize);
      browser.removeEventListener("mozbrowserloadend", onBrowserLoadEnd);
      info(`Got content-resize to ${width} x ${height}`);
      resolve();
    };

    let onBrowserLoadEnd = Task.async(function* () {
      let data = yield getContentSize(ui);
      onResize(undefined, data);
    });

    info(`Waiting for content-resize to ${width} x ${height}`);
    ui.on("content-resize", onResize);
    browser.addEventListener("mozbrowserloadend",
      onBrowserLoadEnd, { once: true });
  }));
}

var setViewportSize = Task.async(function* (ui, manager, width, height) {
  let size = ui.getViewportSize();
  info(`Current size: ${size.width} x ${size.height}, ` +
       `set to: ${width} x ${height}`);
  if (size.width != width || size.height != height) {
    let resized = waitForViewportResizeTo(ui, width, height);
    ui.setViewportSize({ width, height });
    yield resized;
  }
});

function getElRect(selector, win) {
  let el = win.document.querySelector(selector);
  return el.getBoundingClientRect();
}

/**
 * Drag an element identified by 'selector' by [x,y] amount. Returns
 * the rect of the dragged element as it was before drag.
 */
function dragElementBy(selector, x, y, win) {
  let React = win.require("devtools/client/shared/vendor/react");
  let { Simulate } = React.addons.TestUtils;
  let rect = getElRect(selector, win);
  let startPoint = {
    clientX: Math.floor(rect.left + rect.width / 2),
    clientY: Math.floor(rect.top + rect.height / 2),
  };
  let endPoint = [ startPoint.clientX + x, startPoint.clientY + y ];

  let elem = win.document.querySelector(selector);

  // mousedown is a React listener, need to use its testing tools to avoid races
  Simulate.mouseDown(elem, startPoint);

  // mousemove and mouseup are regular DOM listeners
  EventUtils.synthesizeMouseAtPoint(...endPoint, { type: "mousemove" }, win);
  EventUtils.synthesizeMouseAtPoint(...endPoint, { type: "mouseup" }, win);

  return rect;
}

function* testViewportResize(ui, selector, moveBy,
                             expectedViewportSize, expectedHandleMove) {
  let win = ui.toolWindow;
  let resized = waitForViewportResizeTo(ui, ...expectedViewportSize);
  let startRect = dragElementBy(selector, ...moveBy, win);
  yield resized;

  let endRect = getElRect(selector, win);
  is(endRect.left - startRect.left, expectedHandleMove[0],
    `The x move of ${selector} is as expected`);
  is(endRect.top - startRect.top, expectedHandleMove[1],
    `The y move of ${selector} is as expected`);
}

function openDeviceModal({ toolWindow }) {
  let { document } = toolWindow;
  let React = toolWindow.require("devtools/client/shared/vendor/react");
  let { Simulate } = React.addons.TestUtils;
  let select = document.querySelector(".viewport-device-selector");
  let modal = document.querySelector("#device-modal-wrapper");

  info("Checking initial device modal state");
  ok(modal.classList.contains("closed") && !modal.classList.contains("opened"),
    "The device modal is closed by default.");

  info("Opening device modal through device selector.");
  select.value = OPEN_DEVICE_MODAL_VALUE;
  Simulate.change(select);
  ok(modal.classList.contains("opened") && !modal.classList.contains("closed"),
    "The device modal is displayed.");
}

function changeSelectValue({ toolWindow }, selector, value) {
  let { document } = toolWindow;
  let React = toolWindow.require("devtools/client/shared/vendor/react");
  let { Simulate } = React.addons.TestUtils;

  info(`Selecting ${value} in ${selector}.`);

  let select = document.querySelector(selector);
  isnot(select, null, `selector "${selector}" should match an existing element.`);

  let option = [...select.options].find(o => o.value === String(value));
  isnot(option, undefined, `value "${value}" should match an existing option.`);

  select.value = value;
  Simulate.change(select);
}

const selectDevice = (ui, value) => Promise.all([
  once(ui, "device-changed"),
  changeSelectValue(ui, ".viewport-device-selector", value)
]);

const selectDPR = (ui, value) =>
  changeSelectValue(ui, "#global-dpr-selector > select", value);

const selectNetworkThrottling = (ui, value) => Promise.all([
  once(ui, "network-throttling-changed"),
  changeSelectValue(ui, "#global-network-throttling-selector", value)
]);

function getSessionHistory(browser) {
  return ContentTask.spawn(browser, {}, function* () {
    /* eslint-disable no-undef */
    let { interfaces: Ci } = Components;
    let webNav = docShell.QueryInterface(Ci.nsIWebNavigation);
    let sessionHistory = webNav.sessionHistory;
    let result = {
      index: sessionHistory.index,
      entries: []
    };

    for (let i = 0; i < sessionHistory.count; i++) {
      let entry = sessionHistory.getEntryAtIndex(i, false);
      result.entries.push({
        uri: entry.URI.spec,
        title: entry.title
      });
    }

    return result;
    /* eslint-enable no-undef */
  });
}

function getContentSize(ui) {
  return spawnViewportTask(ui, {}, () => ({
    width: content.screen.width,
    height: content.screen.height
  }));
}

function waitForPageShow(browser) {
  let mm = browser.messageManager;
  return new Promise(resolve => {
    let onShow = message => {
      if (message.target != browser) {
        return;
      }
      mm.removeMessageListener("PageVisibility:Show", onShow);
      resolve();
    };
    mm.addMessageListener("PageVisibility:Show", onShow);
  });
}

function waitForViewportLoad(ui) {
  return new Promise(resolve => {
    let browser = ui.getViewportBrowser();
    browser.addEventListener("mozbrowserloadend", () => {
      resolve();
    }, { once: true });
  });
}

function load(browser, url) {
  let loaded = BrowserTestUtils.browserLoaded(browser, false, url);
  browser.loadURI(url, null, null);
  return loaded;
}

function back(browser) {
  let shown = waitForPageShow(browser);
  browser.goBack();
  return shown;
}

function forward(browser) {
  let shown = waitForPageShow(browser);
  browser.goForward();
  return shown;
}

function addDeviceForTest(device) {
  info(`Adding Test Device "${device.name}" to the list.`);
  addDevice(device);

  registerCleanupFunction(() => {
    // Note that assertions in cleanup functions are not displayed unless they failed.
    ok(removeDevice(device), `Removed Test Device "${device.name}" from the list.`);
  });
}

function waitForClientClose(ui) {
  return new Promise(resolve => {
    info("Waiting for RDM debugger client to close");
    ui.client.addOneTimeListener("closed", () => {
      info("RDM's debugger client is now closed");
      resolve();
    });
  });
}

function* testTouchEventsOverride(ui, expected) {
  let { document } = ui.toolWindow;
  let touchButton = document.querySelector("#global-touch-simulation-button");

  let flag = yield ui.emulationFront.getTouchEventsOverride();
  is(flag === Ci.nsIDocShell.TOUCHEVENTS_OVERRIDE_ENABLED, expected,
    `Touch events override should be ${expected ? "enabled" : "disabled"}`);
  is(touchButton.classList.contains("checked"), expected,
    `Touch simulation button should be ${expected ? "" : "in"}active.`);
}

function testViewportDeviceSelectLabel(ui, expected) {
  info("Test viewport's device select label");

  let select = ui.toolWindow.document.querySelector(".viewport-device-selector");
  is(select.selectedOptions[0].textContent, expected,
     `Device Select value should be: ${expected}`);
}

function* enableTouchSimulation(ui) {
  let { document } = ui.toolWindow;
  let touchButton = document.querySelector("#global-touch-simulation-button");
  let loaded = waitForViewportLoad(ui);
  touchButton.click();
  yield loaded;
}

function* testUserAgent(ui, expected) {
  let ua = yield ContentTask.spawn(ui.getViewportBrowser(), {}, function* () {
    return content.navigator.userAgent;
  });
  is(ua, expected, `UA should be set to ${expected}`);
}
