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

const { Loader } = require('sdk/test/loader');
const { Page } = require("sdk/page-worker");
const { URL } = require("sdk/url");
const fixtures = require("./fixtures");
const testURI = fixtures.url("test.html");
const { getActiveView } = require("sdk/view/core");
const { getDocShell } = require('sdk/frame/utils');

const ERR_DESTROYED =
  "Couldn't find the worker to receive this message. " +
  "The script may not be initialized yet, or may already have been unloaded.";

const Isolate = fn => "(" + fn + ")()";

exports.testSimplePageCreation = function(assert, done) {
  let page = new Page({
    contentScript: "self.postMessage(window.location.href)",
    contentScriptWhen: "end",
    onMessage: function (message) {
      assert.equal(message, "about:blank",
                       "Page Worker should start with a blank page by default");
      assert.equal(this, page, "The 'this' object is the page itself.");
      done();
    }
  });
}

/*
 * Tests that we can't be tricked by document overloads as we have access
 * to wrapped nodes
 */
exports.testWrappedDOM = function(assert, done) {
  let page = Page({
    allow: { script: true },
    contentURL: "data:text/html;charset=utf-8,<script>document.getElementById=3;window.scrollTo=3;</script>",
    contentScript: 'new ' + function() {
      function send() {
        self.postMessage([typeof(document.getElementById), typeof(window.scrollTo)]);
      }
      if (document.readyState !== 'complete')
        window.addEventListener('load', send, true)
      else
        send();
    },
    onMessage: function (message) {
      assert.equal(message[0],
                       "function",
                       "getElementById from content script is the native one");

      assert.equal(message[1],
                       "function",
                       "scrollTo from content script is the native one");

      done();
    }
  });
}

/*
// We do not offer unwrapped access to DOM since bug 601295 landed
// See 660780 to track progress of unwrap feature
exports.testUnwrappedDOM = function(assert, done) {
  let page = Page({
    allow: { script: true },
    contentURL: "data:text/html;charset=utf-8,<script>document.getElementById=3;window.scrollTo=3;</script>",
    contentScript: "window.addEventListener('load', function () {" +
                   "return self.postMessage([typeof(unsafeWindow.document.getElementById), " +
                   "typeof(unsafeWindow.scrollTo)]); }, true)",
    onMessage: function (message) {
      assert.equal(message[0],
                       "number",
                       "document inside page is free to be changed");

      assert.equal(message[1],
                       "number",
                       "window inside page is free to be changed");

      done();
    }
  });
}
*/

exports.testPageProperties = function(assert) {
  let page = new Page();

  for (let prop of ['contentURL', 'allow', 'contentScriptFile',
                         'contentScript', 'contentScriptWhen', 'on',
                         'postMessage', 'removeListener']) {
    assert.ok(prop in page, prop + " property is defined on page.");
  }

  assert.ok(() => page.postMessage("foo") || true,
              "postMessage doesn't throw exception on page.");
}

exports.testConstructorAndDestructor = function(assert, done) {
  let loader = Loader(module);
  let { Page } = loader.require("sdk/page-worker");
  let global = loader.sandbox("sdk/page-worker");

  let pagesReady = 0;

  let page1 = Page({
    contentScript:      "self.postMessage('')",
    contentScriptWhen:  "end",
    onMessage:          pageReady
  });
  let page2 = Page({
    contentScript:      "self.postMessage('')",
    contentScriptWhen:  "end",
    onMessage:          pageReady
  });

  assert.notEqual(page1, page2,
                      "Page 1 and page 2 should be different objects.");

  function pageReady() {
    if (++pagesReady == 2) {
      page1.destroy();
      page2.destroy();

      assert.ok(isDestroyed(page1), "page1 correctly unloaded.");
      assert.ok(isDestroyed(page2), "page2 correctly unloaded.");

      loader.unload();
      done();
    }
  }
}

exports.testAutoDestructor = function(assert, done) {
  let loader = Loader(module);
  let { Page } = loader.require("sdk/page-worker");

  let page = Page({
    contentScript: "self.postMessage('')",
    contentScriptWhen: "end",
    onMessage: function() {
      loader.unload();
      assert.ok(isDestroyed(page), "Page correctly unloaded.");
      done();
    }
  });
}

exports.testValidateOptions = function(assert) {
  assert.throws(
    () => Page({ contentURL: 'home' }),
    /The `contentURL` option must be a valid URL\./,
    "Validation correctly denied a non-URL contentURL"
  );

  assert.throws(
    () => Page({ onMessage: "This is not a function."}),
    /The option "onMessage" must be one of the following types: function/,
    "Validation correctly denied a non-function onMessage."
  );

  assert.pass("Options validation is working.");
}

exports.testContentAndAllowGettersAndSetters = function(assert, done) {
  let content = "data:text/html;charset=utf-8,<script>window.localStorage.allowScript=3;</script>";

  // Load up the page with testURI initially for the resource:// principal,
  // then load the actual data:* content, as data:* URIs no longer
  // have localStorage
  let page = Page({
    contentURL: testURI,
    contentScript: "if (window.location.href==='"+testURI+"')" +
      "  self.postMessage('reload');" +
      "else " +
      "  self.postMessage(window.localStorage.allowScript)",
    contentScriptWhen: "end",
    onMessage: step0
  });

  function step0(message) {
    if (message === 'reload')
      return page.contentURL = content;
    assert.equal(message, "3",
                     "Correct value expected for allowScript - 3");
    assert.equal(page.contentURL, content,
                     "Correct content expected");
    page.removeListener('message', step0);
    page.on('message', step1);
    page.allow = { script: false };
    page.contentURL = content =
      "data:text/html;charset=utf-8,<script>window.localStorage.allowScript='f'</script>";
  }

  function step1(message) {
    assert.equal(message, "3",
                     "Correct value expected for allowScript - 3");
    assert.equal(page.contentURL, content, "Correct content expected");
    page.removeListener('message', step1);
    page.on('message', step2);
    page.allow = { script: true };
    page.contentURL = content =
      "data:text/html;charset=utf-8,<script>window.localStorage.allowScript='g'</script>";
  }

  function step2(message) {
    assert.equal(message, "g",
                     "Correct value expected for allowScript - g");
    assert.equal(page.contentURL, content, "Correct content expected");
    page.removeListener('message', step2);
    page.on('message', step3);
    page.allow.script = false;
    page.contentURL = content =
      "data:text/html;charset=utf-8,<script>window.localStorage.allowScript=3</script>";
  }

  function step3(message) {
    assert.equal(message, "g",
                     "Correct value expected for allowScript - g");
    assert.equal(page.contentURL, content, "Correct content expected");
    page.removeListener('message', step3);
    page.on('message', step4);
    page.allow.script = true;
    page.contentURL = content =
      "data:text/html;charset=utf-8,<script>window.localStorage.allowScript=4</script>";
  }

  function step4(message) {
    assert.equal(message, "4",
                     "Correct value expected for allowScript - 4");
    assert.equal(page.contentURL, content, "Correct content expected");
    done();
  }

}

exports.testOnMessageCallback = function(assert, done) {
  Page({
    contentScript: "self.postMessage('')",
    contentScriptWhen: "end",
    onMessage: function() {
      assert.pass("onMessage callback called");
      done();
    }
  });
}

exports.testMultipleOnMessageCallbacks = function(assert, done) {
  let count = 0;
  let page = Page({
    contentScript: "self.postMessage('')",
    contentScriptWhen: "end",
    onMessage: () => count += 1
  });
  page.on('message', () => count += 2);
  page.on('message', () => count *= 3);
  page.on('message', () =>
    assert.equal(count, 9, "All callbacks were called, in order."));
  page.on('message', done);
};

exports.testLoadContentPage = function(assert, done) {
  let page = Page({
    onMessage: function(message) {
      // The message is an array whose first item is the test method to call
      // and the rest of whose items are arguments to pass it.
      let msg = message.shift();
      if (msg == "done")
        return done();
      assert[msg].apply(assert, message);
    },
    contentURL: fixtures.url("addon-sdk/data/test-page-worker.html"),
    contentScriptFile: fixtures.url("addon-sdk/data/test-page-worker.js"),
    contentScriptWhen: "ready"
  });
}

exports.testLoadContentPageRelativePath = function(assert, done) {
  const self = require("sdk/self");
  const { merge } = require("sdk/util/object");

  const options = merge({}, require('@loader/options'),
      { id: "testloader", prefixURI: require('./fixtures').url() });

  let loader = Loader(module, null, options);

  let page = loader.require("sdk/page-worker").Page({
    onMessage: function(message) {
      // The message is an array whose first item is the test method to call
      // and the rest of whose items are arguments to pass it.
      let msg = message.shift();
      if (msg == "done")
        return done();
      assert[msg].apply(assert, message);
    },
    contentURL: "./test-page-worker.html",
    contentScriptFile: "./test-page-worker.js",
    contentScriptWhen: "ready"
  });
}

exports.testAllowScriptDefault = function(assert, done) {
  let page = Page({
    onMessage: function(message) {
      assert.ok(message, "Script is allowed to run by default.");
      done();
    },
    contentURL: "data:text/html;charset=utf-8,<script>document.documentElement.setAttribute('foo', 3);</script>",
    contentScript: "self.postMessage(document.documentElement.getAttribute('foo'))",
    contentScriptWhen: "ready"
  });
}

exports.testAllowScript = function(assert, done) {
  let page = Page({
    onMessage: function(message) {
      assert.ok(message, "Script runs when allowed to do so.");
      page.destroy();
      done();
    },
    allow: { script: true },
    contentURL: "data:text/html;charset=utf-8,<script>document.documentElement.setAttribute('foo', 3);</script>",
    contentScript: "self.postMessage(document.documentElement.hasAttribute('foo') && " +
                   "                 document.documentElement.getAttribute('foo') == 3)",
    contentScriptWhen: "ready"
  });

  let frame = getActiveView(page);
  assert.equal(getDocShell(frame).allowJavascript, true, "allowJavascript is true");
}

exports.testGetActiveViewAndDestroy = function(assert) {
  let page = Page({
    contentURL: "data:text/html;charset=utf-8,<title>test</title>"
  });
  let frame = getActiveView(page);
  assert.ok(frame.parentNode, "there is a parent node");
  page.destroy();
  assert.ok(!frame.parentNode, "there is not a parent node");
}

exports.testPingPong = function(assert, done) {
  let page = Page({
    contentURL: 'data:text/html;charset=utf-8,ping-pong',
    contentScript: 'self.on("message", message => self.postMessage("pong"));'
      + 'self.postMessage("ready");',
    onMessage: function(message) {
      if ('ready' == message) {
        page.postMessage('ping');
      }
      else {
        assert.ok(message, 'pong', 'Callback from contentScript');
        done();
      }
    }
  });
};

exports.testRedirect = function (assert, done) {
  let page = Page({
    contentURL: 'data:text/html;charset=utf-8,first-page',
    contentScriptWhen: "end",
    contentScript: '' +
      'if (/first-page/.test(document.location.href)) ' +
      '  document.location.href = "data:text/html;charset=utf-8,redirect";' +
      'else ' +
      '  self.port.emit("redirect", document.location.href);'
  });

  page.port.on('redirect', function (url) {
    assert.equal(url, 'data:text/html;charset=utf-8,redirect', 'Reinjects contentScript on reload');
    done();
  });
};

exports.testRedirectIncludeArrays = function (assert, done) {
  let firstURL = 'data:text/html;charset=utf-8,first-page';
  let page = Page({
    contentURL: firstURL,
    contentScript: '(function () {' +
      'self.port.emit("load", document.location.href);' +
      '  self.port.on("redirect", function (url) {' +
      '   document.location.href = url;' +
      '  })' +
      '})();',
    include: ['about:blank', 'data:*']
  });

  page.port.on('load', function (url) {
    if (url === firstURL) {
      page.port.emit('redirect', 'about:blank');
    } else if (url === 'about:blank') {
      page.port.emit('redirect', 'about:mozilla');
      assert.ok('`include` property handles arrays');
      assert.equal(url, 'about:blank', 'Redirects work with accepted domains');
      done();
    } else if (url === 'about:mozilla') {
      assert.fail('Should not redirect to restricted domain');
    }
  });
};

exports.testRedirectFromWorker = function (assert, done) {
  let firstURL = 'data:text/html;charset=utf-8,first-page';
  let secondURL = 'data:text/html;charset=utf-8,second-page';
  let thirdURL = 'data:text/html;charset=utf-8,third-page';
  let page = Page({
    contentURL: firstURL,
    contentScript: '(function () {' +
      'self.port.emit("load", document.location.href);' +
      '  self.port.on("redirect", function (url) {' +
      '   document.location.href = url;' +
      '  })' +
      '})();',
    include: 'data:*'
  });

  page.port.on('load', function (url) {
    if (url === firstURL) {
      page.port.emit('redirect', secondURL);
    } else if (url === secondURL) {
      page.port.emit('redirect', thirdURL);
    } else if (url === thirdURL) {
      page.port.emit('redirect', 'about:mozilla');
      assert.equal(url, thirdURL, 'Redirects work with accepted domains on include strings');
      done();
    } else {
      assert.fail('Should not redirect to unauthorized domains');
    }
  });
};

exports.testRedirectWithContentURL = function (assert, done) {
  let firstURL = 'data:text/html;charset=utf-8,first-page';
  let secondURL = 'data:text/html;charset=utf-8,second-page';
  let thirdURL = 'data:text/html;charset=utf-8,third-page';
  let page = Page({
    contentURL: firstURL,
    contentScript: '(function () {' +
      'self.port.emit("load", document.location.href);' +
      '})();',
    include: 'data:*'
  });

  page.port.on('load', function (url) {
    if (url === firstURL) {
      page.contentURL = secondURL;
    } else if (url === secondURL) {
      page.contentURL = thirdURL;
    } else if (url === thirdURL) {
      page.contentURL = 'about:mozilla';
      assert.equal(url, thirdURL, 'Redirects work with accepted domains on include strings');
      done();
    } else {
      assert.fail('Should not redirect to unauthorized domains');
    }
  });
};


exports.testMultipleDestroys = function(assert) {
  let page = Page();
  page.destroy();
  page.destroy();
  assert.pass("Multiple destroys should not cause an error");
};

exports.testContentScriptOptionsOption = function(assert, done) {
  let page = new Page({
    contentScript: "self.postMessage( [typeof self.options.d, self.options] );",
    contentScriptWhen: "end",
    contentScriptOptions: {a: true, b: [1,2,3], c: "string", d: function(){ return 'test'}},
    onMessage: function(msg) {
      assert.equal(msg[0], 'undefined', 'functions are stripped from contentScriptOptions');
      assert.equal(typeof msg[1], 'object', 'object as contentScriptOptions');
      assert.equal(msg[1].a, true, 'boolean in contentScriptOptions');
      assert.equal(msg[1].b.join(), '1,2,3', 'array and numbers in contentScriptOptions');
      assert.equal(msg[1].c, 'string', 'string in contentScriptOptions');
      done();
    }
  });
};

exports.testMessageQueue = function (assert, done) {
  let page = new Page({
    contentScript: 'self.on("message", function (m) {' +
      'self.postMessage(m);' +
      '});',
    contentURL: 'data:text/html;charset=utf-8,',
  });
  page.postMessage('ping');
  page.on('message', function (m) {
    assert.equal(m, 'ping', 'postMessage should queue messages');
    done();
  });
};

exports.testWindowStopDontBreak = function (assert, done) {
  const { Ci, Cc } = require('chrome');
  const consoleService = Cc['@mozilla.org/consoleservice;1'].
                            getService(Ci.nsIConsoleService);
  const listener = {
    observe: ({message}) => {
      if (message.includes('contentWorker is null'))
        assert.fail('contentWorker is null');
    }
  };
  consoleService.registerListener(listener)

  let page = new Page({
    contentURL: 'data:text/html;charset=utf-8,testWindowStopDontBreak',
    contentScriptWhen: 'ready',
    contentScript: Isolate(() => {
      window.stop();
      self.port.on('ping', () => self.port.emit('pong'));
    })
  });

  page.port.on('pong', () => {
    assert.pass('page-worker works after window.stop');
    page.destroy();
    consoleService.unregisterListener(listener);
    done();
  });

  page.port.emit("ping");
};

/**
 * bug 1138545 - the docs claim you can pass in a bare regexp.
 */
exports.testRegexArgument = function (assert, done) {
  let url = 'data:text/html;charset=utf-8,testWindowStopDontBreak';

  let page = new Page({
    contentURL: url,
    contentScriptWhen: 'ready',
    contentScript: Isolate(() => {
     self.port.emit("pong", document.location.href);
    }),
    include: /^data\:text\/html;.*/
  });

  assert.pass("We can pass in a RegExp into page-worker's include option.");

  page.port.on("pong", (href) => {
    assert.equal(href, url, "we get back the same url from the content script.");
    page.destroy();
    done();
  });
};

function isDestroyed(page) {
  try {
    page.postMessage("foo");
  }
  catch (err) {
    if (err.message == ERR_DESTROYED) {
      return true;
    }
    else {
      throw err;
    }
  }
  return false;
}

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