// # Bug 418986, part 2.

/* eslint-disable mozilla/no-comparison-or-assignment-inside-ok */

const is_chrome_window = window.location.protocol === "chrome:";

const HTML_NS = "http://www.w3.org/1999/xhtml";

// Expected values. Format: [name, pref_off_value, pref_on_value]
// If pref_*_value is an array with two values, then we will match
// any value in between those two values. If a value is null, then
// we skip the media query.
var expected_values = [
  ["color", null, 8],
  ["color-index", null, 0],
  ["aspect-ratio", null, window.innerWidth + "/" + window.innerHeight],
  [
    "device-aspect-ratio",
    screen.width + "/" + screen.height,
    window.innerWidth + "/" + window.innerHeight,
  ],
  ["device-height", screen.height + "px", window.innerHeight + "px"],
  ["device-width", screen.width + "px", window.innerWidth + "px"],
  ["grid", null, 0],
  ["height", window.innerHeight + "px", window.innerHeight + "px"],
  ["monochrome", null, 0],
  // Square is defined as portrait:
  [
    "orientation",
    null,
    window.innerWidth > window.innerHeight ? "landscape" : "portrait",
  ],
  ["resolution", null, "96dpi"],
  [
    "resolution",
    [
      0.999 * window.devicePixelRatio + "dppx",
      1.001 * window.devicePixelRatio + "dppx",
    ],
    "1dppx",
  ],
  ["width", window.innerWidth + "px", window.innerWidth + "px"],
  ["-moz-device-pixel-ratio", window.devicePixelRatio, 1],
  [
    "-moz-device-orientation",
    screen.width > screen.height ? "landscape" : "portrait",
    window.innerWidth > window.innerHeight ? "landscape" : "portrait",
  ],
];

// These media queries return value 0 or 1 when the pref is off.
// When the pref is on, they should not match.
var suppressed_toggles = [
  // Not available on most OSs.
  "-moz-scrollbar-end-backward",
  "-moz-scrollbar-end-forward",
  "-moz-scrollbar-start-backward",
  "-moz-scrollbar-start-forward",
  "-moz-gtk-csd-available",
  "-moz-gtk-csd-minimize-button",
  "-moz-gtk-csd-maximize-button",
  "-moz-gtk-csd-close-button",
  "-moz-gtk-csd-reversed-placement",
];

var toggles_enabled_in_content = [];

// Read the current OS.
var OS = SpecialPowers.Services.appinfo.OS;

// __keyValMatches(key, val)__.
// Runs a media query and returns true if key matches to val.
var keyValMatches = (key, val) =>
  matchMedia("(" + key + ":" + val + ")").matches;

// __testMatch(key, val)__.
// Attempts to run a media query match for the given key and value.
// If value is an array of two elements [min max], then matches any
// value in-between.
var testMatch = function (key, val) {
  if (val === null) {
    return;
  } else if (Array.isArray(val)) {
    ok(
      keyValMatches("min-" + key, val[0]) &&
        keyValMatches("max-" + key, val[1]),
      "Expected " + key + " between " + val[0] + " and " + val[1]
    );
  } else {
    ok(keyValMatches(key, val), "Expected " + key + ":" + val);
  }
};

// __testToggles(resisting)__.
// Test whether we are able to match the "toggle" media queries.
var testToggles = function (resisting) {
  suppressed_toggles.forEach(function (key) {
    var exists = keyValMatches(key, 0) || keyValMatches(key, 1);
    if (!toggles_enabled_in_content.includes(key) && !is_chrome_window) {
      ok(!exists, key + " should not exist.");
    } else {
      ok(exists, key + " should exist.");
      if (resisting) {
        ok(
          keyValMatches(key, 0) && !keyValMatches(key, 1),
          "Should always match as false"
        );
      }
    }
  });
};

// __generateHtmlLines(resisting)__.
// Create a series of div elements that look like:
// `<div class='spoof' id='resolution'>resolution</div>`,
// where each line corresponds to a different media query.
var generateHtmlLines = function (resisting) {
  let fragment = document.createDocumentFragment();
  expected_values.forEach(function ([key, offVal, onVal]) {
    let val = resisting ? onVal : offVal;
    if (val) {
      let div = document.createElementNS(HTML_NS, "div");
      div.setAttribute("class", "spoof");
      div.setAttribute("id", key);
      div.textContent = key;
      fragment.appendChild(div);
    }
  });
  suppressed_toggles.forEach(function (key) {
    let div = document.createElementNS(HTML_NS, "div");
    div.setAttribute("class", "suppress");
    div.setAttribute("id", key);
    div.textContent = key;
    fragment.appendChild(div);
  });
  return fragment;
};

// __cssLine__.
// Creates a line of css that looks something like
// `@media (resolution: 1ppx) { .spoof#resolution { background-color: green; } }`.
var cssLine = function (query, clazz, id, color) {
  return (
    "@media " +
    query +
    " { ." +
    clazz +
    "#" +
    id +
    " { background-color: " +
    color +
    "; } }\n"
  );
};

// __constructQuery(key, val)__.
// Creates a CSS media query from key and val. If key is an array of
// two elements, constructs a range query (using min- and max-).
var constructQuery = function (key, val) {
  return Array.isArray(val)
    ? "(min-" + key + ": " + val[0] + ") and (max-" + key + ": " + val[1] + ")"
    : "(" + key + ": " + val + ")";
};

// __mediaQueryCSSLine(key, val, color)__.
// Creates a line containing a CSS media query and a CSS expression.
var mediaQueryCSSLine = function (key, val, color) {
  if (val === null) {
    return "";
  }
  return cssLine(constructQuery(key, val), "spoof", key, color);
};

// __suppressedMediaQueryCSSLine(key, color)__.
// Creates a CSS line that matches the existence of a
// media query that is supposed to be suppressed.
var suppressedMediaQueryCSSLine = function (key, color, suppressed) {
  let query = "(" + key + ": 0), (" + key + ": 1)";
  return cssLine(query, "suppress", key, color);
};

// __generateCSSLines(resisting)__.
// Creates a series of lines of CSS, each of which corresponds to
// a different media query. If the query produces a match to the
// expected value, then the element will be colored green.
var generateCSSLines = function (resisting) {
  let lines = ".spoof { background-color: red;}\n";
  expected_values.forEach(function ([key, offVal, onVal]) {
    lines += mediaQueryCSSLine(key, resisting ? onVal : offVal, "green");
  });
  lines +=
    ".suppress { background-color: " + (resisting ? "green" : "red") + ";}\n";
  suppressed_toggles.forEach(function (key) {
    if (
      !toggles_enabled_in_content.includes(key) &&
      !resisting &&
      !is_chrome_window
    ) {
      lines += "#" + key + " { background-color: green; }\n";
    } else {
      lines += suppressedMediaQueryCSSLine(key, "green");
    }
  });
  return lines;
};

// __green__.
// Returns the computed color style corresponding to green.
var green = "rgb(0, 128, 0)";

// __testCSS(resisting)__.
// Creates a series of divs and CSS using media queries to set their
// background color. If all media queries match as expected, then
// all divs should have a green background color.
var testCSS = function (resisting) {
  document.getElementById("display").appendChild(generateHtmlLines(resisting));
  document.getElementById("test-css").textContent = generateCSSLines(resisting);
  let cssTestDivs = document.querySelectorAll(".spoof,.suppress");
  for (let div of cssTestDivs) {
    let color = window.getComputedStyle(div).backgroundColor;
    ok(color === green, "CSS for '" + div.id + "'");
  }
};

// __testOSXFontSmoothing(resisting)__.
// When fingerprinting resistance is enabled, the `getComputedStyle`
// should always return `undefined` for `MozOSXFontSmoothing`.
var testOSXFontSmoothing = function (resisting) {
  let div = document.createElementNS(HTML_NS, "div");
  div.style.MozOsxFontSmoothing = "unset";
  document.documentElement.appendChild(div);
  let readBack = window.getComputedStyle(div).MozOsxFontSmoothing;
  div.remove();
  let smoothingPref = SpecialPowers.getBoolPref(
    "layout.css.osx-font-smoothing.enabled",
    false
  );
  is(
    readBack,
    resisting ? "" : smoothingPref ? "auto" : "",
    "-moz-osx-font-smoothing"
  );
};

// __sleep(timeoutMs)__.
// Returns a promise that resolves after the given timeout.
var sleep = function (timeoutMs) {
  return new Promise(function (resolve, reject) {
    window.setTimeout(resolve);
  });
};

// __testMediaQueriesInPictureElements(resisting)__.
// Test to see if media queries are properly spoofed in picture elements
// when we are resisting fingerprinting.
var testMediaQueriesInPictureElements = async function (resisting) {
  const MATCH = "/tests/layout/style/test/chrome/match.png";
  let container = document.getElementById("pictures");
  let testImages = [];
  for (let [key, offVal, onVal] of expected_values) {
    let expected = resisting ? onVal : offVal;
    if (expected) {
      let picture = document.createElementNS(HTML_NS, "picture");
      let query = constructQuery(key, expected);
      ok(matchMedia(query).matches, `${query} should match`);

      let source = document.createElementNS(HTML_NS, "source");
      source.setAttribute("srcset", MATCH);
      source.setAttribute("media", query);

      let image = document.createElementNS(HTML_NS, "img");
      image.setAttribute("title", key + ":" + expected);
      image.setAttribute("class", "testImage");
      image.setAttribute("src", "/tests/layout/style/test/chrome/mismatch.png");
      image.setAttribute("alt", key);

      testImages.push(image);

      picture.appendChild(source);
      picture.appendChild(image);
      container.appendChild(picture);
    }
  }
  const matchURI = new URL(MATCH, document.baseURI).href;
  await sleep(0);
  for (let testImage of testImages) {
    is(
      testImage.currentSrc,
      matchURI,
      "Media query '" + testImage.title + "' in picture should match."
    );
  }
};

// __pushPref(key, value)__.
// Set a pref value asynchronously, returning a promise that resolves
// when it succeeds.
var pushPref = function (key, value) {
  return new Promise(function (resolve, reject) {
    SpecialPowers.pushPrefEnv({ set: [[key, value]] }, resolve);
  });
};

// __test(isContent)__.
// Run all tests.
var test = async function (isContent) {
  for (prefValue of [false, true]) {
    await pushPref("privacy.resistFingerprinting", prefValue);
    let resisting = prefValue && isContent;
    expected_values.forEach(function ([key, offVal, onVal]) {
      testMatch(key, resisting ? onVal : offVal);
    });
    testToggles(resisting);
    testCSS(resisting);
    if (OS === "Darwin") {
      testOSXFontSmoothing(resisting);
    }
    await testMediaQueriesInPictureElements(resisting);
  }
};
