"use strict";

ChromeUtils.defineESModuleGetters(this, {
  setTimeout: "resource://gre/modules/Timer.sys.mjs",
});

const kFixtureBaseURL =
  "https://example.com/browser/toolkit/modules/tests/browser/";

function removeDupes(list) {
  let j = 0;
  for (let i = 1; i < list.length; i++) {
    if (list[i] != list[j]) {
      j++;
      if (i != j) {
        list[j] = list[i];
      }
    }
  }
  list.length = j + 1;
}

function compareLists(list1, list2, kind) {
  list1.sort();
  removeDupes(list1);
  list2.sort();
  removeDupes(list2);
  is(String(list1), String(list2), `${kind} URLs correct`);
}

async function promiseOpenFindbar(findbar) {
  await gBrowser.getFindBar();
  findbar.onFindCommand();
  return gFindBar._startFindDeferred && gFindBar._startFindDeferred.promise;
}

function promiseFindResult(findbar, str = null) {
  let highlightFinished = false;
  let findFinished = false;
  return new Promise(resolve => {
    let listener = {
      onFindResult({ searchString }) {
        if (str !== null && str != searchString) {
          return;
        }
        findFinished = true;
        if (highlightFinished) {
          findbar.browser.finder.removeResultListener(listener);
          resolve();
        }
      },
      onHighlightFinished() {
        highlightFinished = true;
        if (findFinished) {
          findbar.browser.finder.removeResultListener(listener);
          resolve();
        }
      },
      onMatchesCountResult: () => {},
    };
    findbar.browser.finder.addResultListener(listener);
  });
}

function promiseEnterStringIntoFindField(findbar, str) {
  let promise = promiseFindResult(findbar, str);
  for (let i = 0; i < str.length; i++) {
    let event = new KeyboardEvent("keypress", {
      bubbles: true,
      cancelable: true,
      view: null,
      keyCode: 0,
      charCode: str.charCodeAt(i),
    });
    findbar._findField.dispatchEvent(event);
  }
  return promise;
}

function promiseTestHighlighterOutput(
  browser,
  word,
  expectedResult,
  extraTest = () => {}
) {
  return SpecialPowers.spawn(
    browser,
    [{ word, expectedResult, extraTest: extraTest.toSource() }],
    async function ({ word, expectedResult, extraTest }) {
      return new Promise(resolve => {
        let stubbed = {};
        let callCounts = {
          insertCalls: [],
          removeCalls: [],
          animationCalls: [],
        };
        let lastMaskNode, lastOutlineNode;
        let rects = [];

        // Amount of milliseconds to wait after the last time one of our stubs
        // was called.
        const kTimeoutMs = 1000;
        // The initial timeout may wait for a while for results to come in.
        let timeout = content.setTimeout(
          () => finish(false, "Timeout"),
          kTimeoutMs * 5
        );

        function finish(ok = true, message = "finished with error") {
          // Restore the functions we stubbed out.
          try {
            content.document.insertAnonymousContent = stubbed.insert;
            content.document.removeAnonymousContent = stubbed.remove;
          } catch (ex) {}
          stubbed = {};
          content.clearTimeout(timeout);

          if (expectedResult.rectCount !== 0) {
            Assert.ok(ok, message);
          }

          Assert.greaterOrEqual(
            callCounts.insertCalls.length,
            expectedResult.insertCalls[0],
            `Min. insert calls should match for '${word}'.`
          );
          Assert.lessOrEqual(
            callCounts.insertCalls.length,
            expectedResult.insertCalls[1],
            `Max. insert calls should match for '${word}'.`
          );
          Assert.greaterOrEqual(
            callCounts.removeCalls.length,
            expectedResult.removeCalls[0],
            `Min. remove calls should match for '${word}'.`
          );
          Assert.lessOrEqual(
            callCounts.removeCalls.length,
            expectedResult.removeCalls[1],
            `Max. remove calls should match for '${word}'.`
          );

          // We reached the amount of calls we expected, so now we can check
          // the amount of rects.
          if (!lastMaskNode && expectedResult.rectCount !== 0) {
            Assert.ok(
              false,
              `No mask node found, but expected ${expectedResult.rectCount} rects.`
            );
          }

          Assert.equal(
            rects.length,
            expectedResult.rectCount,
            `Amount of inserted rects should match for '${word}'.`
          );

          if ("animationCalls" in expectedResult) {
            Assert.greaterOrEqual(
              callCounts.animationCalls.length,
              expectedResult.animationCalls[0],
              `Min. animation calls should match for '${word}'.`
            );
            Assert.lessOrEqual(
              callCounts.animationCalls.length,
              expectedResult.animationCalls[1],
              `Max. animation calls should match for '${word}'.`
            );
          }

          // Allow more specific assertions to be tested in `extraTest`.
          // eslint-disable-next-line no-eval
          extraTest = eval(extraTest);
          extraTest(lastMaskNode, lastOutlineNode, rects);

          resolve();
        }

        function stubAnonymousContentNode(domNode, anonNode) {
          let originals = [
            anonNode.setTextContentForElement,
            anonNode.setAttributeForElement,
            anonNode.removeAttributeForElement,
            anonNode.setCutoutRectsForElement,
            anonNode.setAnimationForElement,
          ];
          anonNode.setTextContentForElement = (id, text) => {
            try {
              (domNode.querySelector("#" + id) || domNode).textContent = text;
            } catch (ex) {}
            return originals[0].call(anonNode, id, text);
          };
          anonNode.setAttributeForElement = (id, attrName, attrValue) => {
            try {
              (domNode.querySelector("#" + id) || domNode).setAttribute(
                attrName,
                attrValue
              );
            } catch (ex) {}
            return originals[1].call(anonNode, id, attrName, attrValue);
          };
          anonNode.removeAttributeForElement = (id, attrName) => {
            try {
              let node = domNode.querySelector("#" + id) || domNode;
              if (node.hasAttribute(attrName)) {
                node.removeAttribute(attrName);
              }
            } catch (ex) {}
            return originals[2].call(anonNode, id, attrName);
          };
          anonNode.setCutoutRectsForElement = (id, cutoutRects) => {
            rects = cutoutRects;
            return originals[3].call(anonNode, id, cutoutRects);
          };
          anonNode.setAnimationForElement = (id, keyframes, options) => {
            callCounts.animationCalls.push([keyframes, options]);
            return originals[4].call(anonNode, id, keyframes, options);
          };
        }

        // Create a function that will stub the original version and collects
        // the arguments so we can check the results later.
        function stub(which) {
          stubbed[which] = content.document[which + "AnonymousContent"];
          let prop = which + "Calls";
          return function (node) {
            callCounts[prop].push(node);
            if (which == "insert") {
              if (node.outerHTML.indexOf("outlineMask") > -1) {
                lastMaskNode = node;
              } else {
                lastOutlineNode = node;
              }
            }
            content.clearTimeout(timeout);
            timeout = content.setTimeout(() => {
              finish();
            }, kTimeoutMs);
            let res = stubbed[which].call(content.document, node);
            if (which == "insert") {
              stubAnonymousContentNode(node, res);
            }
            return res;
          };
        }
        content.document.insertAnonymousContent = stub("insert");
        content.document.removeAnonymousContent = stub("remove");
      });
    }
  );
}
