/* 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/. */

// This file tests message ports and semantics of the frameworker which aren't
// directly related to the sandbox.  See also browser_frameworker_sandbox.js.

function makeWorkerUrl(runner) {
  let prefix =  "http://example.com/browser/toolkit/components/social/test/browser/echo.sjs?";
  if (typeof runner == "function") {
    runner = "var run=" + runner.toSource() + ";run();";
  }
  return prefix + encodeURI(runner);
}

var getFrameWorkerHandle;
function test() {
  waitForExplicitFinish();

  let scope = {};
  Cu.import("resource://gre/modules/FrameWorker.jsm", scope);
  getFrameWorkerHandle = scope.getFrameWorkerHandle;

  runTests(tests);
}

var tests = {
  testSimple: function(cbnext) {
    let run = function() {
      onconnect = function(e) {
        let port = e.ports[0];
        port.onmessage = function(e) {
          if (e.data.topic == "ping") {
            port.postMessage({topic: "pong"});
          }
        }
      }
    }

    let worker = getFrameWorkerHandle(makeWorkerUrl(run), undefined, "testSimple");

    worker.port.onmessage = function(e) {
      if (e.data.topic == "pong") {
        worker.terminate();
        cbnext();
      }
    }
    worker.port.postMessage({topic: "ping"})
  },

  // when the client closes early but the worker tries to send anyway...
  // XXX - disabled due to bug 919878 - we close the frameworker before the
  // remote browser has completed initializing, leading to failures.  Given
  // this can realistically only happen in this synthesized test environment,
  // disabling just this test seems OK for now.
/***
  testEarlyClose: function(cbnext) {
    let run = function() {
      onconnect = function(e) {
        let port = e.ports[0];
        port.postMessage({topic: "oh hai"});
      }
    }

    let worker = getFrameWorkerHandle(makeWorkerUrl(run), undefined, "testEarlyClose");
    worker.port.close();
    worker.terminate();
    cbnext();
  },
***/

  // Check we do get a social.port-closing message as the port is closed.
  testPortClosingMessage: function(cbnext) {
    // We use 2 ports - we close the first and report success via the second.
    let run = function() {
      let firstPort, secondPort;
      onconnect = function(e) {
        let port = e.ports[0];
        if (firstPort === undefined) {
          firstPort = port;
          port.onmessage = function(e) {
            if (e.data.topic == "social.port-closing") {
              secondPort.postMessage({topic: "got-closing"});
            }
          }
        } else {
          secondPort = port;
          // now both ports are connected we can trigger the client side
          // closing the first.
          secondPort.postMessage({topic: "connected"});
        }
      }
    }
    let workerurl = makeWorkerUrl(run);
    let worker1 = getFrameWorkerHandle(workerurl, undefined, "testPortClosingMessage worker1");
    let worker2 = getFrameWorkerHandle(workerurl, undefined, "testPortClosingMessage worker2");
    worker2.port.onmessage = function(e) {
      if (e.data.topic == "connected") {
        // both ports connected, so close the first.
        worker1.port.close();
      } else if (e.data.topic == "got-closing") {
        worker2.terminate();
        cbnext();
      }
    }
  },

  // Tests that prototypes added to core objects work with data sent over
  // the message ports.
  testPrototypes: function(cbnext) {
    let run = function() {
      // Modify the Array prototype...
      Array.prototype.customfunction = function() {};
      onconnect = function(e) {
        let port = e.ports[0];
        port.onmessage = function(e) {
          // Check the data we get via the port has the prototype modification
          if (e.data.topic == "hello" && e.data.data.customfunction) {
            port.postMessage({topic: "hello", data: [1,2,3]});
          }
        }
      }
    }
    // hrmph - this kinda sucks as it is really just testing the actual
    // implementation rather than the end result, but is OK for now.
    // Really we are just testing that JSON.parse in the client window
    // is called.
    let fakeWindow = {
      JSON: {
        parse: function(s) {
          let data = JSON.parse(s);
          data.data.somextrafunction = function() {};
          return data;
        }
      }
    }
    let worker = getFrameWorkerHandle(makeWorkerUrl(run), fakeWindow, "testPrototypes");
    worker.port.onmessage = function(e) {
      if (e.data.topic == "hello") {
        ok(e.data.data.somextrafunction, "have someextrafunction")
        worker.terminate();
        cbnext();
      }
    }
    worker.port.postMessage({topic: "hello", data: [1,2,3]});
  },

  testSameOriginImport: function(cbnext) {
    let run = function() {
      onconnect = function(e) {
        let port = e.ports[0];
        port.onmessage = function(e) {
          if (e.data.topic == "ping") {
            try {
              importScripts("http://mochi.test:8888/error");
            } catch(ex) {
              port.postMessage({topic: "pong", data: ex});
              return;
            }
            port.postMessage({topic: "pong", data: null});
          }
        }
      }
    }

    let worker = getFrameWorkerHandle(makeWorkerUrl(run), undefined, "testSameOriginImport");
    worker.port.onmessage = function(e) {
      if (e.data.topic == "pong") {
        isnot(e.data.data, null, "check same-origin applied to importScripts");
        worker.terminate();
        cbnext();
      }
    }
    worker.port.postMessage({topic: "ping"})
  },

  testRelativeImport: function(cbnext) {
    let url = "https://example.com/browser/toolkit/components/social/test/browser/worker_relative.js";
    let worker = getFrameWorkerHandle(url, undefined, "testSameOriginImport");
    worker.port.onmessage = function(e) {
      if (e.data.topic == "done") {
        is(e.data.result, "ok", "check relative url in importScripts");
        worker.terminate();
        cbnext();
      }
    }
  },

  testNavigator: function(cbnext) {
    let run = function() {
      let port;
      ononline = function() {
        port.postMessage({topic: "ononline", data: navigator.onLine});
      }
      onoffline = function() {
        port.postMessage({topic: "onoffline", data: navigator.onLine});
      }
      onconnect = function(e) {
        port = e.ports[0];
        port.postMessage({topic: "ready",
                          data: {
                            appName: navigator.appName,
                            appVersion: navigator.appVersion,
                            platform: navigator.platform,
                            userAgent: navigator.userAgent,
                          }
                         });
      }
    }
    let ioService = Cc["@mozilla.org/network/io-service;1"].getService(Ci.nsIIOService2);
    let oldManage = ioService.manageOfflineStatus;
    let oldOffline = ioService.offline;

    ioService.manageOfflineStatus = false;
    let worker = getFrameWorkerHandle(makeWorkerUrl(run), undefined, "testNavigator");
    let expected_topic = "onoffline";
    let expected_data = false;
    worker.port.onmessage = function(e) {
      is(e.data.topic, "ready");
      for (let attr of ['appName', 'appVersion', 'platform', 'userAgent']) {
        // each attribute must be a string with length > 0.
        is(typeof e.data.data[attr], "string");
        ok(e.data.data[attr].length > 0);
      }

      worker.port.onmessage = function(e) {
        // a handler specifically for the offline notification.
        is(e.data.topic, "onoffline");
        is(e.data.data, false);

        // add another handler specifically for the 'online' case.
        worker.port.onmessage = function(e) {
          is(e.data.topic, "ononline");
          is(e.data.data, true);
          // all good!
          ioService.manageOfflineStatus = oldManage;
          ioService.offline = oldOffline;
          worker.terminate();
          cbnext();
        }
        ioService.offline = false;
      }
      ioService.offline = true;
    }
  },

  testMissingWorker: function(cbnext) {
    // don't ever create this file!  We want a 404.
    let url = "https://example.com/browser/toolkit/components/social/test/browser/worker_is_missing.js";
    let worker = getFrameWorkerHandle(url, undefined, "testMissingWorker");
    Services.obs.addObserver(function handleError(subj, topic, data) {
      Services.obs.removeObserver(handleError, "social:frameworker-error");
      is(data, worker._worker.origin, "social:frameworker-error was handled");
      worker.terminate();
      cbnext();
    }, 'social:frameworker-error', false);
    worker.port.onmessage = function(e) {
      ok(false, "social:frameworker-error was handled");
      cbnext();
    }
  },

  testNoConnectWorker: function(cbnext) {
    let worker = getFrameWorkerHandle(makeWorkerUrl(function () {}),
                                      undefined, "testNoConnectWorker");
    Services.obs.addObserver(function handleError(subj, topic, data) {
      Services.obs.removeObserver(handleError, "social:frameworker-error");
      is(data, worker._worker.origin, "social:frameworker-error was handled");
      worker.terminate();
      cbnext();
    }, 'social:frameworker-error', false);
    worker.port.onmessage = function(e) {
      ok(false, "social:frameworker-error was handled");
      cbnext();
    }
  },

  testEmptyWorker: function(cbnext) {
    let worker = getFrameWorkerHandle(makeWorkerUrl(''),
                                      undefined, "testEmptyWorker");
    Services.obs.addObserver(function handleError(subj, topic, data) {
      Services.obs.removeObserver(handleError, "social:frameworker-error");
      is(data, worker._worker.origin, "social:frameworker-error was handled");
      worker.terminate();
      cbnext();
    }, 'social:frameworker-error', false);
    worker.port.onmessage = function(e) {
      ok(false, "social:frameworker-error was handled");
      cbnext();
    }
  },

  testWorkerConnectError: function(cbnext) {
    let run = function () {
      onconnect = function(e) {
        throw new Error("worker failure");
      }
    }
    let worker = getFrameWorkerHandle(makeWorkerUrl(run),
                                      undefined, "testWorkerConnectError");
    Services.obs.addObserver(function handleError(subj, topic, data) {
      Services.obs.removeObserver(handleError, "social:frameworker-error");
      is(data, worker._worker.origin, "social:frameworker-error was handled");
      worker.terminate();
      cbnext();
    }, 'social:frameworker-error', false);
    worker.port.onmessage = function(e) {
      ok(false, "social:frameworker-error was handled");
      cbnext();
    }
  },

  // This will create the worker, then send a message to the port, then close
  // the port - all before the worker has actually initialized.
  testCloseFirstSend: function(cbnext) {
    let run = function() {
      let numPings = 0, numCloses = 0;
      onconnect = function(e) {
        let port = e.ports[0];
        port.onmessage = function(e) {
          if (e.data.topic == "ping") {
            numPings += 1;
          } else if (e.data.topic == "social.port-closing") {
            numCloses += 1;
          } else if (e.data.topic == "get-counts") {
            port.postMessage({topic: "result",
                             result: {ping: numPings, close: numCloses}});
          }
        }
      }
    }

    let worker = getFrameWorkerHandle(makeWorkerUrl(run), undefined, "testSendAndClose");
    worker.port.postMessage({topic: "ping"});
    worker.port.close();
    let newPort = getFrameWorkerHandle(makeWorkerUrl(run), undefined, "testSendAndClose").port;
    newPort.onmessage = function(e) {
      if (e.data.topic == "result") {
        is(e.data.result.ping, 1, "the worker got the ping");
        is(e.data.result.close, 1, "the worker got 1 close message");
        worker.terminate();
        cbnext();
      }
    }
    newPort.postMessage({topic: "get-counts"});
  },

  // Like testCloseFirstSend, although in this test the worker has already
  // initialized (so the "connect pending ports" part of the worker isn't
  // what needs to handle this case.)
  testCloseAfterInit: function(cbnext) {
    let run = function() {
      let numPings = 0, numCloses = 0;
      onconnect = function(e) {
        let port = e.ports[0];
        port.onmessage = function(e) {
          if (e.data.topic == "ping") {
            numPings += 1;
          } else if (e.data.topic == "social.port-closing") {
            numCloses += 1;
          } else if (e.data.topic == "get-counts") {
            port.postMessage({topic: "result",
                             result: {ping: numPings, close: numCloses}});
          } else if (e.data.topic == "get-ready") {
            port.postMessage({topic: "ready"});
          }
        }
      }
    }

    let worker = getFrameWorkerHandle(makeWorkerUrl(run), undefined, "testSendAndClose");
    worker.port.onmessage = function(e) {
      if (e.data.topic == "ready") {
        let newPort = getFrameWorkerHandle(makeWorkerUrl(run), undefined, "testSendAndClose").port;
        newPort.postMessage({topic: "ping"});
        newPort.close();
        worker.port.postMessage({topic: "get-counts"});
      } else if (e.data.topic == "result") {
        is(e.data.result.ping, 1, "the worker got the ping");
        is(e.data.result.close, 1, "the worker got 1 close message");
        worker.terminate();
        cbnext();
      }
    }
    worker.port.postMessage({topic: "get-ready"});
  },
}
