/* This Source Code Form is subject to the terms of the Mozilla Public
 * License, v. 2.0. If a copy of the MPL was not distributed with this
 * file, You can obtain one at http://mozilla.org/MPL/2.0/. */

var W3CTest = {
  /**
   * Dictionary mapping a test URL to either the string "all", which means that
   * all tests in this file are expected to fail, or a dictionary mapping test
   * names to either the boolean |true|, or the string "debug". The former
   * means that this test is expected to fail in all builds, and the latter
   * that it is only expected to fail in debug builds.
   */
  "expectedFailures": {},

  /**
   * If set to true, we will dump the test failures to the console.
   */
  "dumpFailures": false,

  /**
   * If dumpFailures is true, this holds a structure like necessary for
   * expectedFailures, for ease of updating the expectations.
   */
  "failures": {},

  /**
   * List of test results, needed by TestRunner to update the UI.
   */
  "tests": [],

  /**
   * Number of unlogged passes, to stop buildbot from truncating the log.
   * We will print a message every MAX_COLLAPSED_MESSAGES passes.
   */
  "collapsedMessages": 0,
  "MAX_COLLAPSED_MESSAGES": 100,

  /**
   * Reference to the TestRunner object in the parent frame if the
   * test and the parent frame are same-origin. Otherwise, return
   * a stub object that relays method calls to the parent frame's
   * TestRunner via postMessage calls.
   */
  "runner": function() {

    /**
     * If true, these tests are running in cross-origin iframes
     */
    var xOrigin = function(){
        try {
            void parent.TestRunner;
            return false;
        } catch {
            return true;
        }
    }();
    if (!xOrigin) {
      return parent === this ? null : parent.TestRunner || parent.wrappedJSObject.TestRunner;
    }
    let documentURL = new URL(document.URL);
    return {
      currentTestURL: function() {
        return documentURL.searchParams.get("currentTestURL");
      }(),
      testFinished(tests) {
        parent.postMessage(
          {
            harnessType: "SimpleTest",
            command: "testFinished",
            applyOn: "runner",
            params: [tests],
          },
          "*"
        );
      },
      getParameterInfo() {
        return { closeWhenDone: documentURL.searchParams.get("closeWhenDone") };
      },
      structuredLogger: {
        testStatus(url, subtest, status, expected, diagnostic, stack) {
          parent.postMessage(
            {
              harnessType: "SimpleTest",
              command: "structuredLogger.testStatus",
              applyOn: "logger",
              params: [url, subtest, status, expected, diagnostic, stack],
            },
            "*"
          );
        },
      },
      expectAssertions(min, max) {
        parent.postMessage(
          {
            harnessType: "SimpleTest",
            command: "expectAssertions",
            applyOn: "runner",
            params: [min, max],
          },
          "*"
        );
      },
    };
   }(),


  /**
   * Prefixes for the error logging. Indexed first by int(todo) and second by
   * int(result). Also contains the test's status, and expected status.
   */
  "prefixes": [
    [
      {status: 'FAIL', expected: 'PASS', message: "TEST-UNEXPECTED-FAIL"},
      {status: 'PASS', expected: 'PASS', message: "TEST-PASS"}
    ],
    [
      {status: 'FAIL', expected: 'FAIL', message: "TEST-KNOWN-FAIL"},
      {status: 'PASS', expected: 'FAIL', message: "TEST-UNEXPECTED-PASS"}
    ]
  ],

  /**
   * Prefix of the path to parent of the the failures directory.
   */
  "pathprefix": "/tests/dom/imptests/",

  /**
   * Returns the URL of the current test, relative to the root W3C tests
   * directory. Used as a key into the expectedFailures dictionary.
   */
  "getPath": function() {
    var url = this.getURL();
    if (!url.startsWith(this.pathprefix)) {
      return "";
    }
    return url.substring(this.pathprefix.length);
  },

  /**
   * Returns the root-relative URL of the current test.
   */
  "getURL": function() {
    return this.runner ? this.runner.currentTestURL : location.pathname;
  },

  /**
   * Report the results in the tests array.
   */
  "reportResults": function() {
    var element = function element(aLocalName) {
      var xhtmlNS = "http://www.w3.org/1999/xhtml";
      return document.createElementNS(xhtmlNS, aLocalName);
    };

    var stylesheet = element("link");
    stylesheet.setAttribute("rel", "stylesheet");
    stylesheet.setAttribute("href", "/resources/testharness.css");
    var heads = document.getElementsByTagName("head");
    if (heads.length) {
      heads[0].appendChild(stylesheet);
    }

    var log = document.getElementById("log");
    if (!log) {
      return;
    }
    var section = log.appendChild(element("section"));
    section.id = "summary";
    section.appendChild(element("h2")).textContent = "Details";

    var table = section.appendChild(element("table"));
    table.id = "results";

    var tr = table.appendChild(element("thead")).appendChild(element("tr"));
    for (var header of ["Result", "Test Name", "Message"]) {
      tr.appendChild(element("th")).textContent = header;
    }
    var statuses = [
      ["Unexpected Fail", "Pass"],
      ["Known Fail", "Unexpected Pass"]
    ];
    var tbody = table.appendChild(element("tbody"));
    for (var test of this.tests) {
      tr = tbody.appendChild(element("tr"));
      tr.className = (test.result === !test.todo ? "pass" : "fail");
      tr.appendChild(element("td")).textContent =
        statuses[+test.todo][+test.result];
      tr.appendChild(element("td")).textContent = test.name;
      tr.appendChild(element("td")).textContent = test.message;
    }
  },

  /**
   * Returns a printable message based on aTest's 'name' and 'message'
   * properties.
   */
  "formatTestMessage": function(aTest) {
    return aTest.name + (aTest.message ? ": " + aTest.message : "");
  },

  /**
   * Lets the test runner know about a test result.
   */
  "_log": function(test) {
    var url = this.getURL();
    var message = this.formatTestMessage(test);
    var result = this.prefixes[+test.todo][+test.result];

    if (this.runner) {
      this.runner.structuredLogger.testStatus(url,
                                              test.name,
                                              result.status,
                                              result.expected,
                                              message);
    } else {
      var msg = result.message + " | ";
      if (url) {
        msg += url;
      }
      msg += " | " + this.formatTestMessage(test);
      dump(msg + "\n");
    }
  },

  /**
   * Logs a message about collapsed messages (if any), and resets the counter.
   */
  "_logCollapsedMessages": function() {
    if (this.collapsedMessages) {
      this._log({
        "name": document.title,
        "result": true,
        "todo": false,
        "message": "Elided " + this.collapsedMessages + " passes or known failures."
      });
    }
    this.collapsedMessages = 0;
  },

  /**
   * Maybe logs a result, eliding up to MAX_COLLAPSED_MESSAGES consecutive
   * passes.
   */
  "_maybeLog": function(test) {
    var success = (test.result === !test.todo);
    if (success && ++this.collapsedMessages < this.MAX_COLLAPSED_MESSAGES) {
      return;
    }
    this._logCollapsedMessages();
    this._log(test);
  },

  /**
   * Reports a test result. The argument is an object with the following
   * properties:
   *
   * o message (string): message to be reported
   * o result (boolean): whether this test failed
   * o todo (boolean): whether this test is expected to fail
   */
  "report": function(test) {
    this.tests.push(test);
    this._maybeLog(test);
  },

  /**
   * Returns true if this test is expected to fail, and false otherwise.
   */
  "_todo": function(test) {
    if (this.expectedFailures === "all") {
      return true;
    }
    var value = this.expectedFailures[test.name];
    return value === true || (value === "debug" && !!SpecialPowers.isDebugBuild);
  },

  /**
   * Callback function for testharness.js. Called when one test in a file
   * finishes.
   */
  "result": function(test) {
    var url = this.getPath();
    this.report({
      "name": test.name,
      "message": test.message || "",
      "result": test.status === test.PASS,
      "todo": this._todo(test)
    });
    if (this.dumpFailures && test.status !== test.PASS) {
      this.failures[test.name] = true;
    }
  },

  /**
   * Callback function for testharness.js. Called when the entire test file
   * finishes.
   */
  "finish": function(tests, status) {
    var url = this.getPath();
    this.report({
      "name": "Finished test",
      "message": "Status: " + status.status,
      "result": status.status === status.OK,
      "todo":
        url in this.expectedFailures &&
        this.expectedFailures[url] === "error"
    });

    this._logCollapsedMessages();

    if (this.dumpFailures) {
      dump("@@@ @@@ Failures\n");
      dump(url + "@@@" + JSON.stringify(this.failures) + "\n");
    }
    if (this.runner) {
      this.runner.testFinished(this.tests.map(function(aTest) {
        return {
          "message": this.formatTestMessage(aTest),
          "result": aTest.result,
          "todo": aTest.todo
        };
      }, this));
    } else {
      this.reportResults();
    }
  },

  /**
   * Log an unexpected failure. Intended to be used from harness code, not
   * from tests.
   */
  "logFailure": function(aTestName, aMessage) {
    this.report({
      "name": aTestName,
      "message": aMessage,
      "result": false,
      "todo": false
    });
  },

  /**
   * Timeout the current test. Intended to be used from harness code, not
   * from tests.
   */
  "timeout": async function() {
    this.logFailure("Timeout", "Test runner timed us out.");
    timeout();
  }
};
(function() {
  try {
    var path = W3CTest.getPath();
    if (path) {
      // Get expected fails.  If there aren't any, there will be a 404, which is
      // fine.  Anything else is unexpected.
      var url = W3CTest.pathprefix + "failures/" + path + ".json";
      var request = new XMLHttpRequest();
      request.open("GET", url, false);
      request.send();
      if (request.status === 200) {
        W3CTest.expectedFailures = JSON.parse(request.responseText);
      } else if (request.status !== 404) {
        W3CTest.logFailure("Fetching failures file", "Request status was " + request.status);
      }
    }

    add_result_callback(W3CTest.result.bind(W3CTest));
    add_completion_callback(W3CTest.finish.bind(W3CTest));
    setup({
      "output": W3CTest.runner && !W3CTest.runner.getParameterInfo().closeWhenDone,
      "explicit_timeout": true
    });
  } catch (e) {
    W3CTest.logFailure("Harness setup", "Unexpected exception: " + e);
  }
})();
