/* 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/. */
"use strict";

// Skipping due to window creation being unsupported in Fennec
module.metadata = {
  engines: {
    'Firefox': '*'
  }
};

const { Cc, Ci } = require("chrome");
const { on } = require("sdk/event/core");
const { setTimeout } = require("sdk/timers");
const { LoaderWithHookedConsole, Loader } = require("sdk/test/loader");
const { Worker } = require("sdk/content/worker");
const { close } = require("sdk/window/helpers");
const { set: setPref } = require("sdk/preferences/service");
const { isArray } = require("sdk/lang/type");
const { URL } = require('sdk/url');
const fixtures = require("./fixtures");
const system = require("sdk/system/events");

const DEPRECATE_PREF = "devtools.errorconsole.deprecation_warnings";

const DEFAULT_CONTENT_URL = "data:text/html;charset=utf-8,foo";

const WINDOW_SCRIPT_URL = "data:text/html;charset=utf-8," +
                          "<script>window.addEventListener('message', function (e) {" +
                          "  if (e.data === 'from -> content-script')" +
                          "    window.postMessage('from -> window', '*');" +
                          "});</script>";

function makeWindow() {
  let content =
    "<?xml version=\"1.0\"?>" +
    "<window " +
    "xmlns=\"http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul\">" +
    "<script>var documentValue=true;</script>" +
    "</window>";
  var url = "data:application/vnd.mozilla.xul+xml;charset=utf-8," +
            encodeURIComponent(content);
  var features = ["chrome", "width=10", "height=10"];

  return Cc["@mozilla.org/embedcomp/window-watcher;1"].
         getService(Ci.nsIWindowWatcher).
         openWindow(null, url, null, features.join(","), null);
}

// Listen for only first one occurence of DOM event
function listenOnce(node, eventName, callback) {
  node.addEventListener(eventName, function onevent(event) {
    node.removeEventListener(eventName, onevent, true);
    callback(node);
  }, true);
}

// Load a given url in a given browser and fires the callback when it is loaded
function loadAndWait(browser, url, callback) {
  listenOnce(browser, "load", callback);
  // We have to wait before calling `loadURI` otherwise, if we call
  // `loadAndWait` during browser load event, the history will be broken
  setTimeout(function () {
    browser.loadURI(url);
  }, 0);
}

// Returns a test function that will automatically open a new chrome window
// with a <browser> element loaded on a given content URL
// The callback receive 3 arguments:
// - test: reference to the jetpack test object
// - browser: a reference to the <browser> xul node
// - done: a callback to call when test is over
function WorkerTest(url, callback) {
  return function testFunction(assert, done) {
    let chromeWindow = makeWindow();
    chromeWindow.addEventListener("load", function onload() {
      chromeWindow.removeEventListener("load", onload, true);
      let browser = chromeWindow.document.createElement("browser");
      browser.setAttribute("type", "content");
      chromeWindow.document.documentElement.appendChild(browser);
      // Wait for about:blank load event ...
      listenOnce(browser, "load", function onAboutBlankLoad() {
        // ... before loading the expected doc and waiting for its load event
        loadAndWait(browser, url, function onDocumentLoaded() {
          callback(assert, browser, function onTestDone() {

            close(chromeWindow).then(done);
          });
        });
      });
    }, true);
  };
}

exports["test:sample"] = WorkerTest(
  DEFAULT_CONTENT_URL,
  function(assert, browser, done) {

    assert.notEqual(browser.contentWindow.location.href, "about:blank",
                        "window is now on the right document");

    let window = browser.contentWindow
    let worker =  Worker({
      window: window,
      contentScript: "new " + function WorkerScope() {
        // window is accessible
        let myLocation = window.location.toString();
        self.on("message", function(data) {
          if (data == "hi!")
            self.postMessage("bye!");
        });
      },
      contentScriptWhen: "ready",
      onMessage: function(msg) {
        assert.equal("bye!", msg);
        assert.equal(worker.url, window.location.href,
                         "worker.url still works");
        done();
      }
    });

    assert.equal(worker.url, window.location.href,
                     "worker.url works");
    assert.equal(worker.contentURL, window.location.href,
                     "worker.contentURL works");
    worker.postMessage("hi!");
  }
);

exports["test:emit"] = WorkerTest(
  DEFAULT_CONTENT_URL,
  function(assert, browser, done) {

    let worker =  Worker({
        window: browser.contentWindow,
        contentScript: "new " + function WorkerScope() {
          // Validate self.on and self.emit
          self.port.on("addon-to-content", function (data) {
            self.port.emit("content-to-addon", data);
          });

          // Check for global pollution
          //if (typeof on != "undefined")
          //  self.postMessage("`on` is in globals");
          if (typeof once != "undefined")
            self.postMessage("`once` is in globals");
          if (typeof emit != "undefined")
            self.postMessage("`emit` is in globals");

        },
        onMessage: function(msg) {
          assert.fail("Got an unexpected message : "+msg);
        }
      });

    // Validate worker.port
    worker.port.on("content-to-addon", function (data) {
      assert.equal(data, "event data");
      done();
    });
    worker.port.emit("addon-to-content", "event data");
  }
);

exports["test:emit hack message"] = WorkerTest(
  DEFAULT_CONTENT_URL,
  function(assert, browser, done) {
    let worker =  Worker({
        window: browser.contentWindow,
        contentScript: "new " + function WorkerScope() {
          // Validate self.port
          self.port.on("message", function (data) {
            self.port.emit("message", data);
          });
          // We should not receive message on self, but only on self.port
          self.on("message", function (data) {
            self.postMessage("message", data);
          });
        },
        onError: function(e) {
          assert.fail("Got exception: "+e);
        }
      });

    worker.port.on("message", function (data) {
      assert.equal(data, "event data");
      done();
    });
    worker.on("message", function (data) {
      assert.fail("Got an unexpected message : "+msg);
    });
    worker.port.emit("message", "event data");
  }
);

exports["test:n-arguments emit"] = WorkerTest(
  DEFAULT_CONTENT_URL,
  function(assert, browser, done) {
    let repeat = 0;
    let worker =  Worker({
        window: browser.contentWindow,
        contentScript: "new " + function WorkerScope() {
          // Validate self.on and self.emit
          self.port.on("addon-to-content", function (a1, a2, a3) {
            self.port.emit("content-to-addon", a1, a2, a3);
          });
        }
      });

    // Validate worker.port
    worker.port.on("content-to-addon", function (arg1, arg2, arg3) {
      if (!repeat++) {
        this.emit("addon-to-content", "first argument", "second", "third");
      } else {
        assert.equal(arg1, "first argument");
        assert.equal(arg2, "second");
        assert.equal(arg3, "third");
        done();
      }
    });
    worker.port.emit("addon-to-content", "first argument", "second", "third");
  }
);

exports["test:post-json-values-only"] = WorkerTest(
  DEFAULT_CONTENT_URL,
  function(assert, browser, done) {

    let worker =  Worker({
        window: browser.contentWindow,
        contentScript: "new " + function WorkerScope() {
          self.on("message", function (message) {
            self.postMessage([ message.fun === undefined,
                               typeof message.w,
                               message.w && "port" in message.w,
                               message.w._url,
                               Array.isArray(message.array),
                               JSON.stringify(message.array)]);
          });
        }
      });

    // Validate worker.onMessage
    let array = [1, 2, 3];
    worker.on("message", function (message) {
      assert.ok(message[0], "function becomes undefined");
      assert.equal(message[1], "object", "object stays object");
      assert.ok(message[2], "object's attributes are enumerable");
      assert.equal(message[3], DEFAULT_CONTENT_URL,
                       "jsonable attributes are accessible");
      // See bug 714891, Arrays may be broken over compartements:
      assert.ok(message[4], "Array keeps being an array");
      assert.equal(message[5], JSON.stringify(array),
                       "Array is correctly serialized");
      done();
    });
    // Add a new url property sa the Class function used by
    // Worker doesn't set enumerables to true for non-functions
    worker._url = DEFAULT_CONTENT_URL;

    worker.postMessage({ fun: function () {}, w: worker, array: array });
  }
);

exports["test:emit-json-values-only"] = WorkerTest(
  DEFAULT_CONTENT_URL,
  function(assert, browser, done) {

    let worker =  Worker({
        window: browser.contentWindow,
        contentScript: "new " + function WorkerScope() {
          // Validate self.on and self.emit
          self.port.on("addon-to-content", function (fun, w, obj, array) {
            self.port.emit("content-to-addon", [
                            fun === null,
                            typeof w,
                            "port" in w,
                            w._url,
                            "fun" in obj,
                            Object.keys(obj.dom).length,
                            Array.isArray(array),
                            JSON.stringify(array)
                          ]);
          });
        }
      });

    // Validate worker.port
    let array = [1, 2, 3];
    worker.port.on("content-to-addon", function (result) {
      assert.ok(result[0], "functions become null");
      assert.equal(result[1], "object", "objects stay objects");
      assert.ok(result[2], "object's attributes are enumerable");
      assert.equal(result[3], DEFAULT_CONTENT_URL,
                       "json attribute is accessible");
      assert.ok(!result[4], "function as object attribute is removed");
      assert.equal(result[5], 0, "DOM nodes are converted into empty object");
      // See bug 714891, Arrays may be broken over compartments:
      assert.ok(result[6], "Array keeps being an array");
      assert.equal(result[7], JSON.stringify(array),
                       "Array is correctly serialized");
      done();
    });

    let obj = {
      fun: function () {},
      dom: browser.contentWindow.document.createElement("div")
    };
    // Add a new url property sa the Class function used by
    // Worker doesn't set enumerables to true for non-functions
    worker._url = DEFAULT_CONTENT_URL;
    worker.port.emit("addon-to-content", function () {}, worker, obj, array);
  }
);

exports["test:content is wrapped"] = WorkerTest(
  "data:text/html;charset=utf-8,<script>var documentValue=true;</script>",
  function(assert, browser, done) {

    let worker =  Worker({
      window: browser.contentWindow,
      contentScript: "new " + function WorkerScope() {
        self.postMessage(!window.documentValue);
      },
      contentScriptWhen: "ready",
      onMessage: function(msg) {
        assert.ok(msg,
          "content script has a wrapped access to content document");
        done();
      }
    });
  }
);

// ContentWorker is not for chrome
/*
exports["test:chrome is unwrapped"] = function(assert, done) {
  let window = makeWindow();

  listenOnce(window, "load", function onload() {

    let worker =  Worker({
      window: window,
      contentScript: "new " + function WorkerScope() {
        self.postMessage(window.documentValue);
      },
      contentScriptWhen: "ready",
      onMessage: function(msg) {
        assert.ok(msg,
          "content script has an unwrapped access to chrome document");
        close(window).then(done);
      }
    });

  });
}
*/

exports["test:nothing is leaked to content script"] = WorkerTest(
  DEFAULT_CONTENT_URL,
  function(assert, browser, done) {

    let worker =  Worker({
      window: browser.contentWindow,
      contentScript: "new " + function WorkerScope() {
        self.postMessage([
          "ContentWorker" in window,
          "UNWRAP_ACCESS_KEY" in window,
          "getProxyForObject" in window
        ]);
      },
      contentScriptWhen: "ready",
      onMessage: function(list) {
        assert.ok(!list[0], "worker API contrustor isn't leaked");
        assert.ok(!list[1], "Proxy API stuff isn't leaked 1/2");
        assert.ok(!list[2], "Proxy API stuff isn't leaked 2/2");
        done();
      }
    });
  }
);

exports["test:ensure console.xxx works in cs"] = WorkerTest(
  DEFAULT_CONTENT_URL,
  function(assert, browser, done) {
    const EXPECTED = ["time", "log", "info", "warn", "error", "error", "timeEnd"];

    let calls = [];
    let levels = [];

    system.on('console-api-log-event', onMessage);

    function onMessage({ subject }) {
      calls.push(subject.wrappedJSObject.arguments[0]);
      levels.push(subject.wrappedJSObject.level);
    }

    let worker =  Worker({
      window: browser.contentWindow,
      contentScript: "new " + function WorkerScope() {
        console.time("time");
        console.log("log");
        console.info("info");
        console.warn("warn");
        console.error("error");
        console.debug("debug");
        console.exception("error");
        console.timeEnd("timeEnd");
        self.postMessage();
      },
      onMessage: function() {
        system.off('console-api-log-event', onMessage);

        assert.equal(JSON.stringify(calls),
          JSON.stringify(EXPECTED),
          "console methods have been called successfully, in expected order");

        assert.equal(JSON.stringify(levels),
          JSON.stringify(EXPECTED),
          "console messages have correct log levels, in expected order");

        done();
      }
    });
  }
);

exports["test:setTimeout works with string argument"] = WorkerTest(
  "data:text/html;charset=utf-8,<script>var docVal=5;</script>",
  function(assert, browser, done) {
    let worker = Worker({
      window: browser.contentWindow,
      contentScript: "new " + function ContentScriptScope() {
        // must use "window.scVal" instead of "var csVal"
        // since we are inside ContentScriptScope function.
        // i'm NOT putting code-in-string inside code-in-string </YO DAWG>
        window.csVal = 13;
        setTimeout("self.postMessage([" +
                      "csVal, " +
                      "window.docVal, " +
                      "'ContentWorker' in window, " +
                      "'UNWRAP_ACCESS_KEY' in window, " +
                      "'getProxyForObject' in window, " +
                    "])", 1);
      },
      contentScriptWhen: "ready",
      onMessage: function([csVal, docVal, chrome1, chrome2, chrome3]) {
        // test timer code is executed in the correct context
        assert.equal(csVal, 13, "accessing content-script values");
        assert.notEqual(docVal, 5, "can't access document values (directly)");
        assert.ok(!chrome1 && !chrome2 && !chrome3, "nothing is leaked from chrome");
        done();
      }
    });
  }
);

exports["test:setInterval works with string argument"] = WorkerTest(
  DEFAULT_CONTENT_URL,
  function(assert, browser, done) {
    let count = 0;
    let worker = Worker({
      window: browser.contentWindow,
      contentScript: "setInterval('self.postMessage(1)', 50)",
      contentScriptWhen: "ready",
      onMessage: function(one) {
        count++;
        assert.equal(one, 1, "got " + count + " message(s) from setInterval");
        if (count >= 3) done();
      }
    });
  }
);

exports["test:setInterval async Errors passed to .onError"] = WorkerTest(
  DEFAULT_CONTENT_URL,
  function(assert, browser, done) {
    let count = 0;
    let worker = Worker({
      window: browser.contentWindow,
      contentScript: "setInterval(() => { throw Error('ubik') }, 50)",
      contentScriptWhen: "ready",
      onError: function(err) {
        count++;
        assert.equal(err.message, "ubik",
            "error (corectly) propagated  " + count + " time(s)");
        if (count >= 3) done();
      }
    });
  }
);

exports["test:setTimeout throws array, passed to .onError"] = WorkerTest(
  DEFAULT_CONTENT_URL,
  function(assert, browser, done) {
    let worker = Worker({
      window: browser.contentWindow,
      contentScript: "setTimeout(function() { throw ['array', 42] }, 1)",
      contentScriptWhen: "ready",
      onError: function(arr) {
        assert.ok(isArray(arr),
            "the type of thrown/propagated object is array");
        assert.ok(arr.length==2,
            "the propagated thrown array is the right length");
        assert.equal(arr[1], 42,
            "element inside the thrown array correctly propagated");
        done();
      }
    });
  }
);

exports["test:setTimeout string arg with SyntaxError to .onError"] = WorkerTest(
  DEFAULT_CONTENT_URL,
  function(assert, browser, done) {
    let worker = Worker({
      window: browser.contentWindow,
      contentScript: "setTimeout('syntax 123 error', 1)",
      contentScriptWhen: "ready",
      onError: function(err) {
        assert.equal(err.name, "SyntaxError",
            "received SyntaxError thrown from bad code in string argument to setTimeout");
        assert.ok('fileName' in err,
            "propagated SyntaxError contains a fileName property");
        assert.ok('stack' in err,
            "propagated SyntaxError contains a stack property");
        assert.equal(err.message, "missing ; before statement",
            "propagated SyntaxError has the correct (helpful) message");
        assert.equal(err.lineNumber, 1,
            "propagated SyntaxError was thrown on the right lineNumber");
        done();
      }
    });
  }
);

exports["test:setTimeout can't be cancelled by content"] = WorkerTest(
  "data:text/html;charset=utf-8,<script>var documentValue=true;</script>",
  function(assert, browser, done) {

    let worker =  Worker({
      window: browser.contentWindow,
      contentScript: "new " + function WorkerScope() {
        let id = setTimeout(function () {
          self.postMessage("timeout");
        }, 100);
        unsafeWindow.eval("clearTimeout("+id+");");
      },
      contentScriptWhen: "ready",
      onMessage: function(msg) {
        assert.ok(msg,
          "content didn't managed to cancel our setTimeout");
        done();
      }
    });
  }
);

exports["test:clearTimeout"] = WorkerTest(
  "data:text/html;charset=utf-8,clear timeout",
  function(assert, browser, done) {
    let worker = Worker({
      window: browser.contentWindow,
      contentScript: "new " + function WorkerScope() {
        let id1 = setTimeout(function() {
          self.postMessage("failed");
        }, 10);
        let id2 = setTimeout(function() {
          self.postMessage("done");
        }, 100);
        clearTimeout(id1);
      },
      contentScriptWhen: "ready",
      onMessage: function(msg) {
        if (msg === "failed") {
          assert.fail("failed to cancel timer");
        } else {
          assert.pass("timer cancelled");
          done();
        }
      }
    });
  }
);

exports["test:clearInterval"] = WorkerTest(
  "data:text/html;charset=utf-8,clear timeout",
  function(assert, browser, done) {
    let called = 0;
    let worker = Worker({
      window: browser.contentWindow,
      contentScript: "new " + function WorkerScope() {
        let id = setInterval(function() {
          self.postMessage("intreval")
          clearInterval(id)
          setTimeout(function() {
            self.postMessage("done")
          }, 100)
        }, 10);
      },
      contentScriptWhen: "ready",
      onMessage: function(msg) {
        if (msg === "intreval") {
          called = called + 1;
          if (called > 1) assert.fail("failed to cancel timer");
        } else {
          assert.pass("interval cancelled");
          done();
        }
      }
    });
  }
)

exports["test:setTimeout are unregistered on content unload"] = WorkerTest(
  DEFAULT_CONTENT_URL,
  function(assert, browser, done) {

    let originalWindow = browser.contentWindow;
    let worker =  Worker({
      window: browser.contentWindow,
      contentScript: "new " + function WorkerScope() {
        document.title = "ok";
        let i = 0;
        setInterval(function () {
          document.title = i++;
        }, 10);
      },
      contentScriptWhen: "ready"
    });

    // Change location so that content script is destroyed,
    // and all setTimeout/setInterval should be unregistered.
    // Wait some cycles in order to execute some intervals.
    setTimeout(function () {
      // Bug 689621: Wait for the new document load so that we are sure that
      // previous document cancelled its intervals
      let url2 = "data:text/html;charset=utf-8,<title>final</title>";
      loadAndWait(browser, url2, function onload() {
        let titleAfterLoad = originalWindow.document.title;
        // Wait additional cycles to verify that intervals are really cancelled
        setTimeout(function () {
          assert.equal(browser.contentDocument.title, "final",
                           "New document has not been modified");
          assert.equal(originalWindow.document.title, titleAfterLoad,
                           "Nor previous one");

          done();
        }, 100);
      });
    }, 100);
  }
);

exports['test:check window attribute in iframes'] = WorkerTest(
  DEFAULT_CONTENT_URL,
  function(assert, browser, done) {

    // Create a first iframe and wait for its loading
    let contentWin = browser.contentWindow;
    let contentDoc = contentWin.document;
    let iframe = contentDoc.createElement("iframe");
    contentDoc.body.appendChild(iframe);

    listenOnce(iframe, "load", function onload() {

      // Create a second iframe inside the first one and wait for its loading
      let iframeDoc = iframe.contentWindow.document;
      let subIframe = iframeDoc.createElement("iframe");
      iframeDoc.body.appendChild(subIframe);

      listenOnce(subIframe, "load", function onload() {
        subIframe.removeEventListener("load", onload, true);

        // And finally create a worker against this second iframe
        let worker =  Worker({
          window: subIframe.contentWindow,
          contentScript: 'new ' + function WorkerScope() {
            self.postMessage([
              window.top !== window,
              frameElement,
              window.parent !== window,
              top.location.href,
              parent.location.href,
            ]);
          },
          onMessage: function(msg) {
            assert.ok(msg[0], "window.top != window");
            assert.ok(msg[1], "window.frameElement is defined");
            assert.ok(msg[2], "window.parent != window");
            assert.equal(msg[3], contentWin.location.href,
                             "top.location refers to the toplevel content doc");
            assert.equal(msg[4], iframe.contentWindow.location.href,
                             "parent.location refers to the first iframe doc");
            done();
          }
        });

      });
      subIframe.setAttribute("src", "data:text/html;charset=utf-8,bar");

    });
    iframe.setAttribute("src", "data:text/html;charset=utf-8,foo");
  }
);

exports['test:check window attribute in toplevel documents'] = WorkerTest(
  DEFAULT_CONTENT_URL,
  function(assert, browser, done) {

    let worker =  Worker({
      window: browser.contentWindow,
      contentScript: 'new ' + function WorkerScope() {
        self.postMessage([
          window.top === window,
          frameElement,
          window.parent === window
        ]);
      },
      onMessage: function(msg) {
        assert.ok(msg[0], "window.top == window");
        assert.ok(!msg[1], "window.frameElement is null");
        assert.ok(msg[2], "window.parent == window");
        done();
      }
    });
  }
);

exports["test:check worker API with page history"] = WorkerTest(
  DEFAULT_CONTENT_URL,
  function(assert, browser, done) {
    let url2 = "data:text/html;charset=utf-8,bar";

    loadAndWait(browser, url2, function () {
      let worker =  Worker({
        window: browser.contentWindow,
        contentScript: "new " + function WorkerScope() {
          // Just before the content script is disable, we register a timeout
          // that will be disable until the page gets visible again
          self.on("pagehide", function () {
            setTimeout(function () {
              self.port.emit("timeout");
            }, 0);
          });

          self.on("message", function() {
            self.postMessage("saw message");
          });

          self.on("event", function() {
            self.port.emit("event", "saw event");
          });
        },
        contentScriptWhen: "start"
      });

      // postMessage works correctly when the page is visible
      worker.postMessage("ok");

      // We have to wait before going back into history,
      // otherwise `goBack` won't do anything.
      setTimeout(function () {
        browser.goBack();
      }, 0);

      // Wait for the document to be hidden
      browser.addEventListener("pagehide", function onpagehide() {
        browser.removeEventListener("pagehide", onpagehide, false);
        // Now any event sent to this worker should be cached

        worker.postMessage("message");
        worker.port.emit("event");

        // Display the page with attached content script back in order to resume
        // its timeout and receive the expected message.
        // We have to delay this in order to not break the history.
        // We delay for a non-zero amount of time in order to ensure that we
        // do not receive the message immediatly, so that the timeout is
        // actually disabled
        setTimeout(function () {
          worker.on("pageshow", function() {
            let promise = Promise.all([
              new Promise(resolve => {
                worker.port.on("event", () => {
                  assert.pass("Saw event");
                  resolve();
                });
              }),
              new Promise(resolve => {
                worker.on("message", () => {
                  assert.pass("Saw message");
                  resolve();
                });
              }),
              new Promise(resolve => {
                worker.port.on("timeout", () => {
                  assert.pass("Timer fired");
                  resolve();
                });
              })
            ]);
            promise.then(done);
          });

          browser.goForward();
        }, 500);

      }, false);
    });

  }
);

exports['test:conentScriptFile as URL instance'] = WorkerTest(
  DEFAULT_CONTENT_URL,
  function(assert, browser, done) {

    let url = new URL(fixtures.url("test-contentScriptFile.js"));
    let worker =  Worker({
      window: browser.contentWindow,
      contentScriptFile: url,
      onMessage: function(msg) {
        assert.equal(msg, "msg from contentScriptFile",
            "received a wrong message from contentScriptFile");
        done();
      }
    });
  }
);

exports["test:worker events"] = WorkerTest(
  DEFAULT_CONTENT_URL,
  function (assert, browser, done) {
    let window = browser.contentWindow;
    let events = [];
    let worker = Worker({
      window: window,
      contentScript: 'new ' + function WorkerScope() {
        self.postMessage('start');
      },
      onAttach: win => {
        events.push('attach');
        assert.pass('attach event called when attached');
        assert.equal(window, win, 'attach event passes in attached window');
      },
      onError: err => {
        assert.equal(err.message, 'Custom',
          'Error passed into error event');
        worker.detach();
      },
      onMessage: msg => {
        assert.pass('`onMessage` handles postMessage')
        throw new Error('Custom');
      },
      onDetach: _ => {
        assert.pass('`onDetach` called when worker detached');
        done();
      }
    });
    // `attach` event is called synchronously during instantiation,
    // so we can't listen to that, TODO FIX?
    //  worker.on('attach', obj => console.log('attach', obj));
  }
);

exports["test:onDetach in contentScript on destroy"] = WorkerTest(
  "data:text/html;charset=utf-8,foo#detach",
  function(assert, browser, done) {
    let worker = Worker({
      window: browser.contentWindow,
      contentScript: 'new ' + function WorkerScope() {
        self.port.on('detach', function(reason) {
          window.location.hash += '!' + reason;
        })
      },
    });
    browser.contentWindow.addEventListener('hashchange', _ => {
      assert.equal(browser.contentWindow.location.hash, '#detach!',
                   "location.href is as expected");
      done();
    })
    worker.destroy();
  }
);

exports["test:onDetach in contentScript on unload"] = WorkerTest(
  "data:text/html;charset=utf-8,foo#detach",
  function(assert, browser, done) {
    let { loader } = LoaderWithHookedConsole(module);
    let worker = loader.require("sdk/content/worker").Worker({
      window: browser.contentWindow,
      contentScript: 'new ' + function WorkerScope() {
        self.port.on('detach', function(reason) {
          window.location.hash += '!' + reason;
        })
      },
    });
    browser.contentWindow.addEventListener('hashchange', _ => {
      assert.equal(browser.contentWindow.location.hash, '#detach!shutdown',
                   "location.href is as expected");
      done();
    })
    loader.unload('shutdown');
  }
);

exports["test:console method log functions properly"] = WorkerTest(
  DEFAULT_CONTENT_URL,
  function(assert, browser, done) {
    let logs = [];

    system.on('console-api-log-event', onMessage);

    function onMessage({ subject }) {
      logs.push(clean(subject.wrappedJSObject.arguments[0]));
    }

    let clean = message =>
          message.trim().
          replace(/[\r\n]/g, " ").
          replace(/ +/g, " ");

    let worker =  Worker({
      window: browser.contentWindow,
      contentScript: "new " + function WorkerScope() {
        console.log(Function);
        console.log((foo) => foo * foo);
        console.log(function foo(bar) { return bar + bar });

        self.postMessage();
      },
      onMessage: () => {
        system.off('console-api-log-event', onMessage);

        assert.deepEqual(logs, [
          "function Function() { [native code] }",
          "(foo) => foo * foo",
          "function foo(bar) { \"use strict\"; return bar + bar }"
        ]);

        done();
      }
    });
  }
);

exports["test:global postMessage"] = WorkerTest(
  WINDOW_SCRIPT_URL,
  function(assert, browser, done) {
    let contentScript = "window.addEventListener('message', function (e) {" +
                        "  if (e.data === 'from -> window')" +
                        "    self.port.emit('response', e.data, e.origin);" +
                        "});" +
                        "postMessage('from -> content-script', '*');";
    let { loader } = LoaderWithHookedConsole(module);
    let worker =  loader.require("sdk/content/worker").Worker({
      window: browser.contentWindow,
      contentScriptWhen: "ready",
      contentScript: contentScript
    });

    worker.port.on("response", (data, origin) => {
      assert.equal(data, "from -> window", "Communication from content-script to window completed");
      done();
    });
});

exports["test:destroy unbinds listeners from port"] = WorkerTest(
  "data:text/html;charset=utf-8,portdestroyer",
  function(assert, browser, done) {
    let destroyed = false;
    let worker = Worker({
      window: browser.contentWindow,
      contentScript: "new " + function WorkerScope() {
        self.port.emit("destroy");
        setInterval(self.port.emit, 10, "ping");
      },
      onDestroy: done
    });
    worker.port.on("ping", () => {
      if (destroyed) {
        assert.fail("Should not call events on port after destroy.");
      }
    });
    worker.port.on("destroy", () => {
      destroyed = true;
      worker.destroy();
      assert.pass("Worker destroyed, waiting for no future listeners handling events.");
      setTimeout(done, 500);
    });
  }
);

exports["test:destroy kills child worker"] = WorkerTest(
  "data:text/html;charset=utf-8,<html><body><p id='detail'></p></body></html>",
  function(assert, browser, done) {
    let worker1 = Worker({
      window: browser.contentWindow,
      contentScript: "new " + function WorkerScope() {
        self.port.on("ping", detail => {
          let event = document.createEvent("CustomEvent");
          event.initCustomEvent("Test:Ping", true, true, detail);
          document.dispatchEvent(event);
          self.port.emit("pingsent");
        });

        let listener = function(event) {
          self.port.emit("pong", event.detail);
        };

        self.port.on("detach", () => {
          window.removeEventListener("Test:Pong", listener);
        });
        window.addEventListener("Test:Pong", listener);
      },
      onAttach: function() {
        let worker2 = Worker({
          window: browser.contentWindow,
          contentScript: "new " + function WorkerScope() {
            let listener = function(event) {
              let newEvent = document.createEvent("CustomEvent");
              newEvent.initCustomEvent("Test:Pong", true, true, event.detail);
              document.dispatchEvent(newEvent);
            };
            self.port.on("detach", () => {
              window.removeEventListener("Test:Ping", listener);
            })
            window.addEventListener("Test:Ping", listener);
            self.postMessage();
          },
          onMessage: function() {
            worker1.port.emit("ping", "test1");
            worker1.port.once("pong", detail => {
              assert.equal(detail, "test1", "Saw the right message");
              worker1.port.once("pingsent", () => {
                assert.pass("The message was sent");

                worker2.destroy();

                worker1.port.emit("ping", "test2");
                worker1.port.once("pong", detail => {
                  assert.fail("worker2 shouldn't have responded");
                })
                worker1.port.once("pingsent", () => {
                  assert.pass("The message was sent");
                  worker1.destroy();
                  done();
                });
              });
            })
          }
        });
      }
    });
  }
);

exports["test:unload kills child worker"] = WorkerTest(
  "data:text/html;charset=utf-8,<html><body><p id='detail'></p></body></html>",
  function(assert, browser, done) {
    let loader = Loader(module);
    let worker1 = Worker({
      window: browser.contentWindow,
      contentScript: "new " + function WorkerScope() {
        self.port.on("ping", detail => {
          let event = document.createEvent("CustomEvent");
          event.initCustomEvent("Test:Ping", true, true, detail);
          document.dispatchEvent(event);
          self.port.emit("pingsent");
        });

        let listener = function(event) {
          self.port.emit("pong", event.detail);
        };

        self.port.on("detach", () => {
          window.removeEventListener("Test:Pong", listener);
        });
        window.addEventListener("Test:Pong", listener);
      },
      onAttach: function() {
        let worker2 = loader.require("sdk/content/worker").Worker({
          window: browser.contentWindow,
          contentScript: "new " + function WorkerScope() {
            let listener = function(event) {
              let newEvent = document.createEvent("CustomEvent");
              newEvent.initCustomEvent("Test:Pong", true, true, event.detail);
              document.dispatchEvent(newEvent);
            };
            self.port.on("detach", () => {
              window.removeEventListener("Test:Ping", listener);
            })
            window.addEventListener("Test:Ping", listener);
            self.postMessage();
          },
          onMessage: function() {
            worker1.port.emit("ping", "test1");
            worker1.port.once("pong", detail => {
              assert.equal(detail, "test1", "Saw the right message");
              worker1.port.once("pingsent", () => {
                assert.pass("The message was sent");

                loader.unload();

                worker1.port.emit("ping", "test2");
                worker1.port.once("pong", detail => {
                  assert.fail("worker2 shouldn't have responded");
                })
                worker1.port.once("pingsent", () => {
                  assert.pass("The message was sent");
                  worker1.destroy();
                  done();
                });
              });
            })
          }
        });
      }
    });
  }
);

require("sdk/test").run(exports);
