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

"use strict";

// Import the inspector's head.js first (which itself imports shared-head.js).
Services.scriptloader.loadSubScript(
  "chrome://mochitests/content/browser/devtools/client/inspector/test/head.js",
  this
);

/**
 * Dispatch the copy event on the given element
 */
function fireCopyEvent(element) {
  const evt = element.ownerDocument.createEvent("Event");
  evt.initEvent("copy", true, true);
  element.dispatchEvent(evt);
}

/**
 * Return all the computed items in the computed view
 *
 * @param {CssComputedView} view
 *        The instance of the computed view panel
 * @returns {Array<Element>}
 */
function getComputedViewProperties(view) {
  return Array.from(
    view.styleDocument.querySelectorAll(
      "#computed-container .computed-property-view"
    )
  );
}

/**
 * Get references to the name and value span nodes corresponding to a given
 * property name in the computed-view
 *
 * @param {CssComputedView} view
 *        The instance of the computed view panel
 * @param {string} name
 *        The name of the property to retrieve
 * @return an object {nameSpan, valueSpan}
 */
function getComputedViewProperty(view, name) {
  let prop;
  for (const property of getComputedViewProperties(view)) {
    const nameSpan = property.querySelector(".computed-property-name");
    const valueSpan = property.querySelector(".computed-property-value");

    if (nameSpan.firstChild.textContent === name) {
      prop = { nameSpan, valueSpan };
      break;
    }
  }
  return prop;
}

/**
 * Get an instance of PropertyView from the computed-view.
 *
 * @param {CssComputedView} view
 *        The instance of the computed view panel
 * @param {string} name
 *        The name of the property to retrieve
 * @return {PropertyView}
 */
function getComputedViewPropertyView(view, name) {
  return view.propertyViews.find(propertyView => propertyView.name === name);
}

/**
 * Get a reference to the matched rules element for a given property name in
 * the computed-view.
 * A matched rule element is inside the property element (<li>) itself
 * and is only shown when the twisty icon is expanded on the property.
 * It contains matched rules, with selectors, properties, values and stylesheet links.
 *
 * @param {CssComputedView} view
 *        The instance of the computed view panel
 * @param {string} name
 *        The name of the property to retrieve
 * @return {Promise} A promise that resolves to the property matched rules
 * container
 */
var getComputedViewMatchedRules = async function (view, name) {
  let expander;
  let matchedRulesEl;
  for (const property of view.styleDocument.querySelectorAll(
    "#computed-container .computed-property-view"
  )) {
    const nameSpan = property.querySelector(".computed-property-name");
    if (nameSpan.firstChild.textContent === name) {
      expander = property.querySelector(".computed-expandable");
      matchedRulesEl = property.querySelector(".matchedselectors");

      break;
    }
  }

  if (!expander.hasAttribute("open")) {
    // Need to expand the property
    const onExpand = view.inspector.once("computed-view-property-expanded");
    expander.click();
    await onExpand;

    await waitFor(() => expander.hasAttribute("open"));
  }

  return matchedRulesEl;
};

/**
 * Get the text value of the property corresponding to a given name in the
 * computed-view
 *
 * @param {CssComputedView} view
 *        The instance of the computed view panel
 * @param {string} name
 *        The name of the property to retrieve
 * @return {string} The property value
 */
function getComputedViewPropertyValue(view, name) {
  return getComputedViewProperty(view, name).valueSpan.textContent;
}

/**
 * Expand a given property, given its index in the current property list of
 * the computed view
 *
 * @param {CssComputedView} view
 *        The instance of the computed view panel
 * @param {number} index
 *        The index of the property to be expanded
 * @return a promise that resolves when the property has been expanded, or
 * rejects if the property was not found
 */
function expandComputedViewPropertyByIndex(view, index) {
  info("Expanding property " + index + " in the computed view");
  const expandos = view.styleDocument.querySelectorAll(".computed-expandable");
  if (!expandos.length || !expandos[index]) {
    return Promise.reject();
  }

  const onExpand = view.inspector.once("computed-view-property-expanded");
  expandos[index].click();
  return onExpand;
}

/**
 * Get a rule-link from the computed-view given its index
 *
 * @param {CssComputedView} view
 *        The instance of the computed view panel
 * @param {number} index
 *        The index of the matched selector element
 * @return {DOMNode} The link at the given index, if one exists, null otherwise
 */
function getComputedViewLinkByIndex(view, index) {
  const matchedSelectors = view.styleDocument.querySelectorAll(
    ".matchedselectors > p"
  );
  const matchedSelector = matchedSelectors[index];
  if (!matchedSelector) {
    return null;
  }

  return matchedSelector.querySelector(`.rule-link .computed-link`);
}

/**
 * Trigger the select all action in the computed view.
 *
 * @param {CssComputedView} view
 *        The instance of the computed view panel
 */
function selectAllText(view) {
  info("Selecting all the text");
  view.contextMenu._onSelectAll();
}

/**
 * Select all the text, copy it, and check the content in the clipboard.
 *
 * @param {CssComputedView} view
 *        The instance of the computed view panel
 * @param {string} expectedPattern
 *        A regular expression used to check the content of the clipboard
 */
async function copyAllAndCheckClipboard(view, expectedPattern) {
  selectAllText(view);
  const contentDoc = view.styleDocument;
  const prop = contentDoc.querySelector(
    "#computed-container .computed-property-view"
  );

  try {
    info("Trigger a copy event and wait for the clipboard content");
    await waitForClipboardPromise(
      () => fireCopyEvent(prop),
      () => checkClipboard(expectedPattern)
    );
  } catch (e) {
    failClipboardCheck(expectedPattern);
  }
}

/**
 * Select some text, copy it, and check the content in the clipboard.
 *
 * @param {CssComputedView} view
 *        The instance of the computed view panel
 * @param {object} positions
 *        The start and end positions of the text to be selected. This must be an object
 *        like this:
 *        { start: {prop: 1, offset: 0}, end: {prop: 3, offset: 5} }
 * @param {string} expectedPattern
 *        A regular expression used to check the content of the clipboard
 */
async function copySomeTextAndCheckClipboard(view, positions, expectedPattern) {
  info("Testing selection copy");

  const contentDocument = view.styleDocument;
  const props = contentDocument.querySelectorAll(
    "#computed-container .computed-property-view"
  );

  info("Create the text selection range");
  const range = contentDocument.createRange();
  range.setStart(props[positions.start.prop], positions.start.offset);
  range.setEnd(props[positions.end.prop], positions.end.offset);
  contentDocument.defaultView.getSelection().addRange(range);

  try {
    info("Trigger a copy event and wait for the clipboard content");
    await waitForClipboardPromise(
      () => fireCopyEvent(props[0]),
      () => checkClipboard(expectedPattern)
    );
  } catch (e) {
    failClipboardCheck(expectedPattern);
  }
}

function checkClipboard(expectedPattern) {
  const actual = SpecialPowers.getClipboardData("text/plain");
  const expectedRegExp = new RegExp(expectedPattern, "g");
  return expectedRegExp.test(actual);
}

function failClipboardCheck(expectedPattern) {
  // Format expected text for comparison
  const terminator = Services.appinfo.OS == "WINNT" ? "\r\n" : "\n";
  expectedPattern = expectedPattern.replace(/\[\\r\\n\][+*]/g, terminator);
  expectedPattern = expectedPattern.replace(/\\\(/g, "(");
  expectedPattern = expectedPattern.replace(/\\\)/g, ")");

  let actual = SpecialPowers.getClipboardData("text/plain");

  // Trim the right hand side of our strings. This is because expectedPattern
  // accounts for windows sometimes adding a newline to our copied data.
  expectedPattern = expectedPattern.trimRight();
  actual = actual.trimRight();

  dump(
    "TEST-UNEXPECTED-FAIL | Clipboard text does not match expected ... " +
      "results (escaped for accurate comparison):\n"
  );
  info("Actual: " + escape(actual));
  info("Expected: " + escape(expectedPattern));
}

/**
 * Check that the given property has the expected value and the expected matched selectors
 *
 * @param {CssComputedView} view
 *        The instance of the computed view panel
 * @param {object} options
 * @param {string} options.property
 *        The property name to check
 * @param {string} options.expectedComputedValue
 *        The expected value displayed for the property
 * @param {object[]} options.expectedMatchedSelectors
 *        An array of objects describing the expected matched selectors
 * @param {string} options.expectedMatchedSelectors[].selector
 *        The selector that should be displayed at this index
 * @param {string} options.expectedMatchedSelectors[].value
 *        The value that should be displayed at this index
 * @param {boolean} options.expectedMatchedSelectors[].match
 *        Whether the selector should match the currently selected element. Defaults to true.
 */
async function checkMatchedSelectorForProperty(
  view,
  { property, expectedComputedValue, expectedMatchedSelectors }
) {
  const propertyView = getComputedViewPropertyView(view, property);
  ok(propertyView, `found PropertyView for "${property}"`);
  const { valueNode } = propertyView;
  is(
    valueNode.textContent,
    expectedComputedValue,
    `Expected displayed computed value for "${property}"`
  );

  is(propertyView.hasMatchedSelectors, true, "hasMatchedSelectors is true");

  info("Expanding the matched selectors");
  propertyView.matchedExpanded = true;
  await propertyView.refreshMatchedSelectors();

  const selectorsEl =
    propertyView.matchedSelectorsContainer.querySelectorAll(".rule-text");
  is(
    selectorsEl.length,
    expectedMatchedSelectors.length,
    "Expected number of selectors are displayed"
  );

  selectorsEl.forEach((selectorEl, index) => {
    is(
      selectorEl.querySelector(".fix-get-selection").innerText,
      expectedMatchedSelectors[index].selector,
      `Selector #${index} is the expected one`
    );
    is(
      selectorEl.querySelector(".computed-other-property-value").innerText,
      expectedMatchedSelectors[index].value,
      `Selector #${index} ("${expectedMatchedSelectors[index].selector}") has the expected "${property}"`
    );
    const classToMatch = index === 0 ? "bestmatch" : "matched";
    const expectedMatch = expectedMatchedSelectors[index].match ?? true;
    is(
      selectorEl.classList.contains(classToMatch),
      expectedMatch,
      `Selector #${index} ("${expectedMatchedSelectors[index].selector}") element does ${expectedMatch ? "" : "not "}have a matching class`
    );
  });
}
