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

"use strict";

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

const {
  COMPATIBILITY_UPDATE_SELECTED_NODE_COMPLETE,
  COMPATIBILITY_UPDATE_TOP_LEVEL_TARGET_COMPLETE,
} = require("resource://devtools/client/inspector/compatibility/actions/index.js");

const {
  toCamelCase,
} = require("resource://devtools/client/inspector/compatibility/utils/cases.js");

async function openCompatibilityView() {
  info("Open the compatibility view");
  await pushPref("devtools.inspector.compatibility.enabled", true);

  const { inspector } = await openInspectorSidebarTab("compatibilityview");
  await Promise.all([
    waitForUpdateSelectedNodeAction(inspector.store),
    waitForUpdateTopLevelTargetAction(inspector.store),
  ]);
  const panel = inspector.panelDoc.querySelector(
    "#compatibilityview-panel .inspector-tabpanel"
  );

  const selectedElementPane = panel.querySelector(
    "#compatibility-app--selected-element-pane"
  );

  const allElementsPane = panel.querySelector(
    "#compatibility-app--all-elements-pane"
  );

  return { allElementsPane, inspector, panel, selectedElementPane };
}

/**
 * Check whether the content of issue item element is matched with the expected values.
 *
 * @param {Element} panel
 * @param {Array} expectedIssues
 *        Array of the issue expected.
 *        For the structure of issue items, see types.js.
 */
async function assertIssueList(panel, expectedIssues) {
  info("Check the number of issues");
  await waitUntil(
    () =>
      panel.querySelectorAll("[data-qa-property]").length ===
      expectedIssues.length
  );
  ok(true, "The number of issues is correct");

  if (expectedIssues.length === 0) {
    // No issue.
    return;
  }

  const getFluentString = await getFluentStringHelper([
    "devtools/client/compatibility.ftl",
  ]);

  for (const expectedIssue of expectedIssues) {
    const property = expectedIssue.property;
    info(`Check an element for ${property}`);
    const issueEl = getIssueItem(property, panel);
    ok(issueEl, `Issue element for the ${property} is in the panel`);

    if (expectedIssue.unsupportedBrowsers) {
      // We only display a single icon per unsupported browser, so we need to
      // group the expected unsupported browsers (versions) by their browser id.
      const expectedUnsupportedBrowsersById = new Map();
      for (const unsupportedBrowser of expectedIssue.unsupportedBrowsers) {
        if (!expectedUnsupportedBrowsersById.has(unsupportedBrowser.id)) {
          expectedUnsupportedBrowsersById.set(unsupportedBrowser.id, []);
        }
        expectedUnsupportedBrowsersById
          .get(unsupportedBrowser.id)
          .push(unsupportedBrowser);
      }

      const unsupportedBrowserListEl = issueEl.querySelector(
        ".compatibility-unsupported-browser-list"
      );
      const unsupportedBrowsersEl =
        unsupportedBrowserListEl.querySelectorAll("li");

      is(
        unsupportedBrowsersEl.length,
        expectedUnsupportedBrowsersById.size,
        "The expected number of browser icons are displayed"
      );

      for (const unsupportedBrowserEl of unsupportedBrowsersEl) {
        const expectedUnsupportedBrowsers = expectedUnsupportedBrowsersById.get(
          unsupportedBrowserEl.getAttribute("data-browser-id")
        );

        ok(expectedUnsupportedBrowsers, "The expected browser is displayed");
        // debugger;
        is(
          unsupportedBrowserEl.querySelector(".compatibility-browser-version")
            .innerText,
          // If esr is not supported, but a newest version isn't as well, we don't display
          // the esr version number
          (
            expectedUnsupportedBrowsers.find(
              ({ status }) => status !== "esr"
            ) || expectedUnsupportedBrowsers[0]
          ).version,
          "The expected browser version is displayed"
        );

        is(
          unsupportedBrowserEl.getAttribute("title"),
          getFluentString("compatibility-issue-browsers-list", "title", {
            browsers: expectedUnsupportedBrowsers
              .map(
                ({ name, status, version }) =>
                  `${name} ${version}${status ? ` (${status})` : ""}`
              )
              .join("\n"),
          }),
          "The brower item has the expected title attribute"
        );
      }
    }

    for (const [key, value] of Object.entries(expectedIssue)) {
      const datasetKey = toCamelCase(`qa-${key}`);
      is(
        issueEl.dataset[datasetKey],
        JSON.stringify(value),
        `The value of ${datasetKey} is correct`
      );
    }

    const propertyEl = issueEl.querySelector(
      ".compatibility-issue-item__property"
    );
    const MDN_CLASSNAME = "compatibility-issue-item__mdn-link";
    const SPEC_CLASSNAME = "compatibility-issue-item__spec-link";

    is(
      propertyEl.textContent,
      property,
      "property name is displayed as expected"
    );

    is(
      propertyEl.classList.contains(MDN_CLASSNAME),
      !!expectedIssue.url,
      `${property} element ${
        expectedIssue.url ? "has" : "does not have"
      } mdn link class`
    );
    is(
      propertyEl.classList.contains(SPEC_CLASSNAME),
      !!expectedIssue.specUrl,
      `${property} element ${
        expectedIssue.specUrl ? "has" : "does not have"
      } spec link class`
    );

    if (expectedIssue.url || expectedIssue.specUrl) {
      is(
        propertyEl.nodeName.toLowerCase(),
        "a",
        `Link rendered for ${property}`
      );

      const expectedUrl = expectedIssue.url
        ? expectedIssue.url +
          "?utm_source=devtools&utm_medium=inspector-compatibility&utm_campaign=default"
        : expectedIssue.specUrl;
      const { link } = await simulateLinkClick(propertyEl);
      is(
        link,
        expectedUrl,
        `Click on ${property} link navigates user to expected url`
      );
    } else {
      is(
        propertyEl.nodeName.toLowerCase(),
        "span",
        `No link rendered for ${property}`
      );

      const { link } = await simulateLinkClick(propertyEl);
      is(link, null, `Click on ${property} does not navigate`);
    }
  }
}

/**
 * Check whether the content of node item element is matched with the expected values.
 *
 * @param {Element} panel
 * @param {Array} expectedNodes
 *        e.g.
 *        [{ property: "margin-inline-end", nodes: ["body", "div.classname"] },...]
 */
async function assertNodeList(panel, expectedNodes) {
  for (const { property, nodes } of expectedNodes) {
    info(`Check nodes for ${property}`);
    const issueEl = getIssueItem(property, panel);

    await waitUntil(
      () =>
        issueEl.querySelectorAll(".compatibility-node-item").length ===
        nodes.length
    );
    ok(true, "The number of nodes is correct");

    const nodeEls = [...issueEl.querySelectorAll(".compatibility-node-item")];
    for (const node of nodes) {
      const nodeEl = nodeEls.find(el => el.textContent === node);
      ok(nodeEl, "The text content of the node element is correct");
    }
  }
}

/**
 * Get IssueItem of given property from given element.
 *
 * @param {String} property
 * @param {Element} element
 * @return {Element}
 */
function getIssueItem(property, element) {
  return element.querySelector(`[data-qa-property=\"\\"${property}\\"\"]`);
}

/**
 * Toggle enable/disable checkbox of a specific property on rule view.
 *
 * @param {Inspector} inspector
 * @param {Number} ruleIndex
 * @param {Number} propIndex
 */
async function togglePropStatusOnRuleView(inspector, ruleIndex, propIndex) {
  const ruleView = inspector.getPanel("ruleview").view;
  const rule = getRuleViewRuleEditor(ruleView, ruleIndex).rule;
  // In case of inline style changes, we track the mutations via the
  // inspector's markupmutation event to react to dynamic style changes
  // which Resource Watcher doesn't cover yet.
  // If an inline style is applied to the element, we need to wait on the
  // markupmutation event
  const onMutation =
    ruleIndex === 0 ? inspector.once("markupmutation") : Promise.resolve();
  const textProp = rule.textProps[propIndex];
  const onRuleviewChanged = ruleView.once("ruleview-changed");
  textProp.editor.enable.click();
  await Promise.all([onRuleviewChanged, onMutation]);
}

/**
 * Return a promise which waits for COMPATIBILITY_UPDATE_SELECTED_NODE_COMPLETE action.
 *
 * @param {Object} store
 * @return {Promise}
 */
function waitForUpdateSelectedNodeAction(store) {
  return waitForDispatch(store, COMPATIBILITY_UPDATE_SELECTED_NODE_COMPLETE);
}

/**
 * Return a promise which waits for COMPATIBILITY_UPDATE_TOP_LEVEL_TARGET_COMPLETE action.
 *
 * @param {Object} store
 * @return {Promise}
 */
function waitForUpdateTopLevelTargetAction(store) {
  return waitForDispatch(store, COMPATIBILITY_UPDATE_TOP_LEVEL_TARGET_COMPLETE);
}
