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

"use strict";

const URL = ROOT + "browser_formdata_xpath_sample.html";

/**
 * Bug 346337 - Generic form data restoration tests.
 */
add_setup(function () {
  // make sure we don't save form data at all (except for tab duplication)
  Services.prefs.setIntPref("browser.sessionstore.privacy_level", 2);

  registerCleanupFunction(() => {
    Services.prefs.clearUserPref("browser.sessionstore.privacy_level");
  });
});

const FILE1 = createFilePath("346337_test1.file");
const FILE2 = createFilePath("346337_test2.file");

const FIELDS = {
  "//input[@name='input']": Date.now().toString(16),
  "//input[@name='spaced 1']": Math.random().toString(),
  "//input[3]": "three",
  "//input[@type='checkbox']": true,
  "//input[@name='uncheck']": false,
  "//input[@type='radio'][1]": false,
  "//input[@type='radio'][2]": true,
  "//input[@type='radio'][3]": false,
  "//select": 2,
  "//select[@multiple]": [1, 3],
  "//textarea[1]": "",
  "//textarea[2]": "Some text... " + Math.random(),
  "//textarea[3]": "Some more text\n" + new Date(),
  "//input[@type='file'][1]": [FILE1],
  "//input[@type='file'][2]": [FILE1, FILE2],
};

add_task(async function test_form_data_restoration() {
  // Load page with some input fields.
  let tab = BrowserTestUtils.addTab(gBrowser, URL);
  let browser = tab.linkedBrowser;
  await promiseBrowserLoaded(browser);

  // Fill in some values.
  for (let xpath of Object.keys(FIELDS)) {
    await setFormValue(browser, xpath);
  }

  // Duplicate the tab.
  let tab2 = gBrowser.duplicateTab(tab);
  let browser2 = tab2.linkedBrowser;
  await promiseTabRestored(tab2);

  // Check that all form values have been duplicated.
  for (let xpath of Object.keys(FIELDS)) {
    let expected = JSON.stringify(FIELDS[xpath]);
    let actual = JSON.stringify(await getFormValue(browser2, xpath));
    is(
      actual,
      expected,
      'The value for "' + xpath + '" was correctly restored'
    );
  }

  // Remove all tabs.
  await promiseRemoveTabAndSessionState(tab2);
  await promiseRemoveTabAndSessionState(tab);

  // Restore one of the tabs again.
  tab = ss.undoCloseTab(window, 0);
  browser = tab.linkedBrowser;
  await promiseTabRestored(tab);

  // Check that none of the form values have been restored due to the privacy
  // level settings.
  for (let xpath of Object.keys(FIELDS)) {
    let expected = FIELDS[xpath];
    if (expected) {
      let actual = await getFormValue(browser, xpath, expected);
      isnot(
        actual,
        expected,
        'The value for "' + xpath + '" was correctly discarded'
      );
    }
  }

  // Cleanup.
  BrowserTestUtils.removeTab(tab);
});

function getPropertyOfXPath(browserContext, path, propName) {
  return SpecialPowers.spawn(
    browserContext,
    [path, propName],
    (pathChild, propNameChild) => {
      let doc = content.document;
      let xptype = doc.defaultView.XPathResult.FIRST_ORDERED_NODE_TYPE;
      let node = doc.evaluate(
        pathChild,
        doc,
        null,
        xptype,
        null
      ).singleNodeValue;
      return node[propNameChild];
    }
  );
}

function setPropertyOfXPath(browserContext, path, propName, newValue) {
  return SpecialPowers.spawn(
    browserContext,
    [path, propName, newValue],
    (pathChild, propNameChild, newValueChild) => {
      let doc = content.document;
      let xptype = doc.defaultView.XPathResult.FIRST_ORDERED_NODE_TYPE;
      let node = doc.evaluate(
        pathChild,
        doc,
        null,
        xptype,
        null
      ).singleNodeValue;
      node[propNameChild] = newValueChild;

      let event = node.ownerDocument.createEvent("UIEvents");
      event.initUIEvent("input", true, true, node.ownerGlobal, 0);
      node.dispatchEvent(event);
    }
  );
}

function execUsingXPath(browserContext, path, fnName, arg) {
  return SpecialPowers.spawn(
    browserContext,
    [path, fnName, arg],
    (pathChild, fnNameChild, argChild) => {
      let doc = content.document;
      let xptype = doc.defaultView.XPathResult.FIRST_ORDERED_NODE_TYPE;
      let node = doc.evaluate(
        pathChild,
        doc,
        null,
        xptype,
        null
      ).singleNodeValue;

      switch (fnNameChild) {
        case "getMultipleSelected":
          return Array.from(node.options, (opt, idx) => idx).filter(
            idx => node.options[idx].selected
          );
        case "setMultipleSelected":
          Array.prototype.forEach.call(
            node.options,
            (opt, idx) => (opt.selected = argChild.indexOf(idx) > -1)
          );
          break;
        case "getFileNameArray":
          return node.mozGetFileNameArray();
        case "setFileNameArray":
          node.mozSetFileNameArray(argChild, argChild.length);
          break;
      }

      let event = node.ownerDocument.createEvent("UIEvents");
      event.initUIEvent("input", true, true, node.ownerGlobal, 0);
      node.dispatchEvent(event);
      return undefined;
    }
  );
}

function createFilePath(leaf) {
  let file = Services.dirsvc.get("TmpD", Ci.nsIFile);
  file.append(leaf);
  return file.path;
}

function isArrayOfNumbers(value) {
  return Array.isArray(value) && value.every(n => typeof n === "number");
}

function isArrayOfStrings(value) {
  return Array.isArray(value) && value.every(n => typeof n === "string");
}

function getFormValue(browser, xpath) {
  let value = FIELDS[xpath];

  if (typeof value == "string") {
    return getPropertyOfXPath(browser, xpath, "value");
  }

  if (typeof value == "boolean") {
    return getPropertyOfXPath(browser, xpath, "checked");
  }

  if (typeof value == "number") {
    return getPropertyOfXPath(browser, xpath, "selectedIndex");
  }

  if (isArrayOfNumbers(value)) {
    return execUsingXPath(browser, xpath, "getMultipleSelected");
  }

  if (isArrayOfStrings(value)) {
    return execUsingXPath(browser, xpath, "getFileNameArray");
  }

  throw new Error("unknown input type");
}

function setFormValue(browser, xpath) {
  let value = FIELDS[xpath];

  if (typeof value == "string") {
    return setPropertyOfXPath(browser, xpath, "value", value);
  }

  if (typeof value == "boolean") {
    return setPropertyOfXPath(browser, xpath, "checked", value);
  }

  if (typeof value == "number") {
    return setPropertyOfXPath(browser, xpath, "selectedIndex", value);
  }

  if (isArrayOfNumbers(value)) {
    return execUsingXPath(browser, xpath, "setMultipleSelected", value);
  }

  if (isArrayOfStrings(value)) {
    return execUsingXPath(browser, xpath, "setFileNameArray", value);
  }

  throw new Error("unknown input type");
}
