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

"use strict";

// https rather than chrome to improve coverage
const TESTCASE_URI = TEST_BASE_HTTPS + "media-rules.html";
const SIDEBAR_PREF = "devtools.styleeditor.showAtRulesSidebar";

const RESIZE_W = 300;
const RESIZE_H = 450;
const LABELS = [
  "not all",
  "all",
  "(max-width: 550px)",
  "(min-height: 300px) and (max-height: 320px)",
  "(max-width: 750px)",
  "",
  "print",
];
const LINE_NOS = [1, 7, 19, 25, 31, 34, 39];
const NEW_RULE = `
  @media (max-width: 750px) {
    div {
      color: blue;
      @layer {
        border-color: tomato;
      }
    }

    @media print {
      body {
        filter: grayscale(100%);
      }
    }
  }`;

waitForExplicitFinish();

add_task(async function () {
  // Enable @property rules
  await pushPref("layout.css.properties-and-values.enabled", true);
  // Enable @custom-media
  await pushPref("layout.css.custom-media.enabled", true);

  const { ui } = await openStyleEditorForURL(TESTCASE_URI);

  is(ui.editors.length, 4, "correct number of editors");

  info("Test first plain css editor");
  const plainEditor = ui.editors[0];
  await openEditor(plainEditor);
  testPlainEditor(plainEditor);

  info("Test editor for inline sheet with at-rules");
  const inlineAtRulesEditor = ui.editors[3];
  await openEditor(inlineAtRulesEditor);
  await testInlineAtRulesEditor(ui, inlineAtRulesEditor);

  info("Test editor with @media rules");
  const mediaEditor = ui.editors[1];
  await openEditor(mediaEditor);
  await testMediaEditor(ui, mediaEditor);

  info("Test that sidebar hides when flipping pref");
  await testShowHide(ui, mediaEditor);

  info("Test adding a rule updates the list");
  await testMediaRuleAdded(ui, mediaEditor);

  info("Test resizing and seeing @media matching state change");
  const originalWidth = window.outerWidth;
  const originalHeight = window.outerHeight;

  const onMatchesChange = ui.once("at-rules-list-changed");
  window.resizeTo(RESIZE_W, RESIZE_H);
  await onMatchesChange;

  testMediaMatchChanged(mediaEditor);

  window.resizeTo(originalWidth, originalHeight);
});

function testPlainEditor(editor) {
  const sidebar = editor.details.querySelector(".stylesheet-sidebar");
  is(sidebar.hidden, true, "sidebar is hidden on editor without @media");
}

async function testInlineAtRulesEditor(ui, editor) {
  const sidebar = editor.details.querySelector(".stylesheet-sidebar");
  is(sidebar.hidden, false, "sidebar is showing on editor with @media");

  const entries = sidebar.querySelectorAll(".at-rule-label");
  is(entries.length, 16, "16 at-rules displayed in sidebar");

  await testRule({
    ui,
    editor,
    rule: entries[0],
    conditionText: "screen",
    matches: true,
    line: 2,
    type: "media",
  });

  await testRule({
    ui,
    editor,
    rule: entries[1],
    conditionText: "(display: flex)",
    line: 7,
    type: "support",
  });

  await testRule({
    ui,
    editor,
    rule: entries[2],
    conditionText: "(1px < height < 10000px)",
    matches: true,
    line: 8,
    type: "media",
  });

  await testRule({
    ui,
    editor,
    rule: entries[3],
    line: 16,
    type: "layer",
    layerName: "myLayer",
  });

  await testRule({
    ui,
    editor,
    rule: entries[4],
    conditionText: "(min-width: 1px)",
    line: 17,
    type: "container",
  });

  await testRule({
    ui,
    editor,
    rule: entries[5],
    conditionText: "selector(&)",
    line: 21,
    type: "support",
  });

  await testRule({
    ui,
    editor,
    rule: entries[6],
    conditionText: "--my-container (height > 42px)",
    line: 29,
    type: "container",
  });

  await testRule({
    ui,
    editor,
    rule: entries[7],
    conditionText: "--my-container",
    line: 31,
    type: "container",
  });

  await testRule({
    ui,
    editor,
    rule: entries[8],
    line: 33,
    type: "property",
    propertyName: "--my-property",
  });

  await testRule({
    ui,
    editor,
    rule: entries[9],
    line: 39,
    type: "position-try",
    positionTryName: "--pt-custom-bottom",
  });

  await testRule({
    ui,
    editor,
    rule: entries[10],
    line: 45,
    type: "custom-media",
    customMediaName: "--mobile-breakpoint",
    customMediaQuery: [
      { text: "(width < 320px) and (height < 1420px)" },
      { text: ", " },
      { text: "not print" },
    ],
  });

  await testRule({
    ui,
    editor,
    rule: entries[11],
    line: 46,
    type: "custom-media",
    customMediaName: "--enabled",
    customMediaQuery: [{ text: "true" }],
  });

  await testRule({
    ui,
    editor,
    rule: entries[12],
    line: 47,
    type: "custom-media",
    customMediaName: "--disabled",
    customMediaQuery: [{ text: "false", matches: false }],
  });

  await testRule({
    ui,
    editor,
    rule: entries[13],
    line: 52,
    type: "media",
    conditionText: "(--mobile-breakpoint)",
    matches: false,
  });

  await testRule({
    ui,
    editor,
    rule: entries[14],
    line: 56,
    type: "media",
    conditionText: "(--enabled)",
    matches: false,
  });

  await testRule({
    ui,
    editor,
    rule: entries[15],
    line: 60,
    type: "media",
    conditionText: "(--disabled)",
    matches: false,
  });
}

async function testMediaEditor(ui, editor) {
  const sidebar = editor.details.querySelector(".stylesheet-sidebar");
  is(sidebar.hidden, false, "sidebar is showing on editor with @media");

  const entries = [...sidebar.querySelectorAll(".at-rule-label")];
  is(entries.length, 4, "four @media rules displayed in sidebar");

  await testRule({
    ui,
    editor,
    rule: entries[0],
    conditionText: LABELS[0],
    matches: false,
    line: LINE_NOS[0],
  });
  await testRule({
    ui,
    editor,
    rule: entries[1],
    conditionText: LABELS[1],
    matches: true,
    line: LINE_NOS[1],
  });
  await testRule({
    ui,
    editor,
    rule: entries[2],
    conditionText: LABELS[2],
    matches: false,
    line: LINE_NOS[2],
  });
  await testRule({
    ui,
    editor,
    rule: entries[3],
    conditionText: LABELS[3],
    matches: false,
    line: LINE_NOS[3],
  });
}

function testMediaMatchChanged(editor) {
  const sidebar = editor.details.querySelector(".stylesheet-sidebar");

  const cond = sidebar.querySelectorAll(".at-rule-condition")[2];
  is(
    cond.textContent,
    "(max-width: 550px)",
    "third rule condition text is correct"
  );
  ok(
    !cond.classList.contains("media-condition-unmatched"),
    "media rule is now matched after resizing"
  );
}

async function testShowHide(ui, editor) {
  let sidebarChange = ui.once("at-rules-list-changed");
  Services.prefs.setBoolPref(SIDEBAR_PREF, false);
  await sidebarChange;

  const sidebar = editor.details.querySelector(".stylesheet-sidebar");
  is(sidebar.hidden, true, "sidebar is hidden after flipping pref");

  sidebarChange = ui.once("at-rules-list-changed");
  Services.prefs.clearUserPref(SIDEBAR_PREF);
  await sidebarChange;

  is(sidebar.hidden, false, "sidebar is showing after flipping pref back");
}

async function testMediaRuleAdded(ui, editor) {
  await editor.getSourceEditor();
  const sidebar = editor.details.querySelector(".stylesheet-sidebar");
  is(
    sidebar.querySelectorAll(".at-rule-label").length,
    4,
    "4 @media rules after changing text"
  );

  let text = editor.sourceEditor.getText();
  text += NEW_RULE;

  const listChange = ui.once("at-rules-list-changed");
  editor.sourceEditor.setText(text);
  await listChange;

  const entries = [...sidebar.querySelectorAll(".at-rule-label")];
  is(entries.length, 7, "7 @media rules after changing text");

  await testRule({
    ui,
    editor,
    rule: entries[4],
    conditionText: LABELS[4],
    matches: false,
    line: LINE_NOS[4],
  });

  await testRule({
    ui,
    editor,
    rule: entries[5],
    type: "layer",
    conditionText: LABELS[5],
    line: LINE_NOS[5],
  });

  await testRule({
    ui,
    editor,
    rule: entries[6],
    conditionText: LABELS[6],
    matches: false,
    line: LINE_NOS[6],
  });
}

/**
 * Run assertion on given rule
 *
 * @param {object} options
 * @param {StyleEditorUI} options.ui
 * @param {StyleSheetEditor} options.editor: The editor the rule is displayed in
 * @param {Element} options.rule: The rule element in the media sidebar
 * @param {string} options.conditionText: at-rule condition text (for @media, @container, @support)
 * @param {boolean} options.matches: Whether or not the document matches the rule
 * @param {string} options.layerName: Optional name of the @layer
 * @param {string} options.positionTryName: Name of the @position-try if type is "position-try"
 * @param {string} options.propertyName: Name of the @property if type is "property"
 * @param {string} options.customMediaName: Name of the @custom-media if type is "custom-media"
 * @param {Array<object>} options.customMediaQuery: query parts of the @custom-media if type is "custom-media"
 * @param {string} options.customMediaQuery[].text: the query string of the part of the @custom-media
 *        if type is "custom-media"
 * @param {boolean} options.customMediaQuery[].matches: whether or not this part is style as matching,
 *        if type is "custom-media". Defaults to true.
 * @param {number} options.line: Line of the rule
 * @param {string} options.type: The type of the rule (container, layer, media, support, property ).
 *                               Defaults to "media".
 */
async function testRule({
  ui,
  editor,
  rule,
  conditionText = "",
  matches,
  layerName,
  positionTryName,
  propertyName,
  customMediaName,
  customMediaQuery,
  line,
  type = "media",
}) {
  const atTypeEl = rule.querySelector(".at-rule-type");
  let name;
  if (type === "layer") {
    name = layerName;
  } else if (type === "property") {
    name = propertyName;
  } else if (type === "position-try") {
    name = positionTryName;
  }

  if (type === "custom-media") {
    const atTypeChilNodes = Array.from(atTypeEl.childNodes);
    is(
      atTypeChilNodes.shift().textContent,
      `@custom-media\u00A0`,
      "label for @custom-media is correct"
    );
    is(
      atTypeChilNodes.shift().textContent,
      `${customMediaName} `,
      "name for @custom-media is correct"
    );
    is(
      atTypeChilNodes.length,
      customMediaQuery.length,
      `Got expected number of children of @custom-media (got ${JSON.stringify(atTypeChilNodes.map(n => n.textContent))})`
    );
    for (let i = 0; i < atTypeChilNodes.length; i++) {
      const node = atTypeChilNodes[i];
      is(
        node.textContent,
        customMediaQuery[i].text,
        `Got expected text for part #${i} of @custom-media`
      );
      if (customMediaQuery[i].matches ?? true) {
        ok(
          // handle TextNode
          !node.classList ||
            !node.classList.contains("media-condition-unmatched"),
          `Text for part #${i} of @custom-media ("${node.textContent}") does not have unmatching class`
        );
      } else {
        ok(
          node.classList.contains("media-condition-unmatched"),
          `Text for part #${i} of @custom-media ("${node.textContent}") has expected unmatching class`
        );
      }
    }
  } else {
    is(
      atTypeEl.textContent,
      `@${type}\u00A0${name ? `${name}\u00A0` : ""}`,
      "label for at-rule type is correct"
    );
  }

  const cond = rule.querySelector(".at-rule-condition");
  is(
    cond.textContent,
    conditionText,
    "condition label is correct for " + conditionText
  );

  if (type == "media") {
    const matched = !cond.classList.contains("media-condition-unmatched");
    ok(
      matches ? matched : !matched,
      "media rule is " + (matches ? "matched" : "unmatched")
    );
  }

  const ruleLine = rule.querySelector(".at-rule-line");
  is(ruleLine.textContent, ":" + line, "correct line number shown");

  info(
    "Check that clicking on the rule jumps to the expected position in the stylesheet"
  );
  rule.click();
  await waitFor(
    () =>
      ui.selectedEditor == editor &&
      editor.sourceEditor.getCursor().line == line - 1
  );
  ok(true, "Jumped to the expected location");
}

/* Helpers */

function openEditor(editor) {
  getLinkFor(editor).click();

  return editor.getSourceEditor();
}

function getLinkFor(editor) {
  return editor.summary.querySelector(".stylesheet-name");
}
