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

module.metadata = {
  "engines": {
    "Firefox": "*"
  }
};

const { Tool } = require("dev/toolbox");
const { Panel } = require("dev/panel");
const { Class } = require("sdk/core/heritage");
const { openToolbox, closeToolbox, getCurrentPanel } = require("dev/utils");
const { MessageChannel } = require("sdk/messaging");
const { when } = require("sdk/dom/events");
const { viewFor } = require("sdk/view/core");
const { createView } = require("dev/panel/view");

const iconURI = "data:image/png;base64, ";
const makeHTML = fn =>
  "data:text/html;charset=utf-8,<script>(" + fn + ")();</script>";


const test = function(unit) {
  return function*(assert) {
    assert.isRendered = (panel, toolbox) => {
      const doc = toolbox.doc;
      assert.ok(doc.querySelector("[value='" + panel.label + "']"),
                "panel.label is found in the developer toolbox DOM");
      assert.ok(doc.querySelector("[tooltiptext='" + panel.tooltip + "']"),
                "panel.tooltip is found in the developer toolbox DOM");

      assert.ok(doc.querySelector("#toolbox-panel-" + panel.id),
                "toolbar panel with a matching id is present");
    };


    yield* unit(assert);
  };
};

exports["test Panel API"] = test(function*(assert) {
  const MyPanel = Class({
    extends: Panel,
    label: "test panel",
    tooltip: "my test panel",
    icon: iconURI,
    url: makeHTML(() => {
      document.documentElement.innerHTML = "hello world";
    }),
    setup: function({debuggee}) {
      this.debuggee = debuggee;
      assert.equal(this.readyState, "uninitialized",
                   "at construction time panel document is not inited");
    },
    dispose: function() {
      delete this.debuggee;
    }
  });
  assert.ok(MyPanel, "panel is defined");

  const myTool = new Tool({
    panels: {
      myPanel: MyPanel
    }
  });
  assert.ok(myTool, "tool is defined");


  var toolbox = yield openToolbox(MyPanel);
  var panel = yield getCurrentPanel(toolbox);
  assert.ok(panel instanceof MyPanel, "is instance of MyPanel");

  assert.isRendered(panel, toolbox);

  if (panel.readyState === "uninitialized") {
    yield panel.ready();
    assert.equal(panel.readyState, "interactive", "panel is ready");
  }

  yield panel.loaded();
  assert.equal(panel.readyState, "complete", "panel is loaded");

  yield closeToolbox();

  assert.equal(panel.readyState, "destroyed", "panel is destroyed");

  myTool.destroy();
});

exports["test forbid remote https docs"] = test(function*(assert) {
  const MyPanel = Class({
    extends: Panel,
    label: "test https panel",
    tooltip: "my test panel",
    icon: iconURI,
    url: "https://mozilla.org",
  });

  assert.throws(() => {
    new Tool({ panels: { myPanel: MyPanel } });
  },
  /The `options.url` must be a valid local URI/,
  "can't use panel with remote URI");
});

exports["test forbid remote http docs"] = test(function*(assert) {
  const MyPanel = Class({
    extends: Panel,
    label: "test http panel",
    tooltip: "my test panel",
    icon: iconURI,
    url: "http://arewefastyet.com/",
  });

  assert.throws(() => {
    new Tool({ panels: { myPanel: MyPanel } });
  },
  /The `options.url` must be a valid local URI/,
  "can't use panel with remote URI");
});

exports["test forbid remote ftp docs"] = test(function*(assert) {
  const MyPanel = Class({
    extends: Panel,
    label: "test ftp panel",
    tooltip: "my test panel",
    icon: iconURI,
    url: "ftp://ftp.mozilla.org/",
  });

  assert.throws(() => {
    new Tool({ panels: { myPanel: MyPanel } });
  },
  /The `options.url` must be a valid local URI/,
  "can't use panel with remote URI");
});


exports["test Panel communication"] = test(function*(assert) {
  const MyPanel = Class({
    extends: Panel,
    label: "communication",
    tooltip: "test palen communication",
    icon: iconURI,
    url: makeHTML(() => {
      window.addEventListener("message", event => {
        if (event.source === window) {
          var port = event.ports[0];
          port.start();
          port.postMessage("ping");
          port.onmessage = (event) => {
            if (event.data === "pong") {
              port.postMessage("bye");
              port.close();
            }
          };
        }
      });
    }),
    dispose: function() {
      delete this.port;
    }
  });


  const myTool = new Tool({
    panels: {
      myPanel: MyPanel
    }
  });


  const toolbox = yield openToolbox(MyPanel);
  const panel = yield getCurrentPanel(toolbox);
  assert.ok(panel instanceof MyPanel, "is instance of MyPanel");

  assert.isRendered(panel, toolbox);

  yield panel.ready();
  const { port1, port2 } = new MessageChannel();
  panel.port = port1;
  panel.postMessage("connect", [port2]);
  panel.port.start();

  const ping = yield when(panel.port, "message");

  assert.equal(ping.data, "ping", "received ping from panel doc");

  panel.port.postMessage("pong");

  const bye = yield when(panel.port, "message");

  assert.equal(bye.data, "bye", "received bye from panel doc");

  panel.port.close();

  yield closeToolbox();

  assert.equal(panel.readyState, "destroyed", "panel is destroyed");
  myTool.destroy();
});

exports["test communication with debuggee"] = test(function*(assert) {
  const MyPanel = Class({
    extends: Panel,
    label: "debuggee",
    tooltip: "test debuggee",
    icon: iconURI,
    url: makeHTML(() => {
      window.addEventListener("message", event => {
        if (event.source === window) {
          var debuggee = event.ports[0];
          var port = event.ports[1];
          debuggee.start();
          port.start();


          debuggee.onmessage = (event) => {
            port.postMessage(event.data);
          };
          port.onmessage = (event) => {
            debuggee.postMessage(event.data);
          };
        }
      });
    }),
    setup: function({debuggee}) {
      this.debuggee = debuggee;
    },
    onReady: function() {
      const { port1, port2 } = new MessageChannel();
      this.port = port1;
      this.port.start();
      this.debuggee.start();

      this.postMessage("connect", [this.debuggee, port2]);
    },
    dispose: function() {
      this.port.close();
      this.debuggee.close();

      delete this.port;
      delete this.debuggee;
    }
  });


  const myTool = new Tool({
    panels: {
      myPanel: MyPanel
    }
  });


  const toolbox = yield openToolbox(MyPanel);
  const panel = yield getCurrentPanel(toolbox);
  assert.ok(panel instanceof MyPanel, "is instance of MyPanel");

  assert.isRendered(panel, toolbox);

  yield panel.ready();
  const intro = yield when(panel.port, "message");

  assert.equal(intro.data.from, "root", "intro message from root");

  panel.port.postMessage({
    to: "root",
    type: "echo",
    text: "ping"
  });

  const pong = yield when(panel.port, "message");

  assert.deepEqual(pong.data, {
    to: "root",
    from: "root",
    type: "echo",
    text: "ping"
  }, "received message back from root");

  yield closeToolbox();

  assert.equal(panel.readyState, "destroyed", "panel is destroyed");

  myTool.destroy();
});


exports["test viewFor panel"] = test(function*(assert) {
  const url = "data:text/html;charset=utf-8,viewFor";
  const MyPanel = Class({
    extends: Panel,
    label: "view for panel",
    tooltip: "my panel view",
    icon: iconURI,
    url: url
  });

  const myTool = new Tool({
    panels: {
      myPanel: MyPanel
    }
  });


  const toolbox = yield openToolbox(MyPanel);
  const panel = yield getCurrentPanel(toolbox);
  assert.ok(panel instanceof MyPanel, "is instance of MyPanel");

  const frame = viewFor(panel);

  assert.equal(frame.nodeName.toLowerCase(), "iframe",
               "viewFor(panel) returns associated iframe");

  yield panel.loaded();

  assert.equal(frame.contentDocument.URL, url, "is expected iframe");

  yield closeToolbox();

  myTool.destroy();
});


exports["test createView panel"] = test(function*(assert) {
  var frame = null;
  var panel = null;

  const url = "data:text/html;charset=utf-8,createView";
  const id = Math.random().toString(16).substr(2);
  const MyPanel = Class({
    extends: Panel,
    label: "create view",
    tooltip: "panel creator",
    icon: iconURI,
    url: url
  });

  createView.define(MyPanel, (instance, document) => {
    var view = document.createElement("iframe");
    view.setAttribute("type", "content");

    // save instances for later asserts
    frame = view;
    panel = instance;

    return view;
  });

  const myTool = new Tool({
    panels: {
      myPanel: MyPanel
    }
  });

  const toolbox = yield openToolbox(MyPanel);
  const myPanel = yield getCurrentPanel(toolbox);

  assert.equal(myPanel, panel,
               "panel passed to createView is one instantiated");
  assert.equal(viewFor(panel), frame,
               "createView has created an iframe");

  yield panel.loaded();

  assert.equal(frame.contentDocument.URL, url, "is expected iframe");

  yield closeToolbox();

  myTool.destroy();
});


exports["test ports is an optional"] = test(function*(assert) {
  const MyPanel = Class({
    extends: Panel,
    label: "no-port",
    icon: iconURI,
    url: makeHTML(() => {
      window.addEventListener("message", event => {
        if (event.ports.length) {
          event.ports[0].postMessage(window.firstPacket);
        } else {
          window.firstPacket = event.data;
        }
      });
    })
  });


  const myTool = new Tool({
    panels: {
      myPanel: MyPanel
    }
  });


  const toolbox = yield openToolbox(MyPanel);
  const panel = yield getCurrentPanel(toolbox);
  assert.ok(panel instanceof MyPanel, "is instance of MyPanel");

  assert.isRendered(panel, toolbox);

  yield panel.ready();

  const { port1, port2 } = new MessageChannel();
  port1.start();

  panel.postMessage("hi");
  panel.postMessage("bye", [port2]);

  const packet = yield when(port1, "message");

  assert.equal(packet.data, "hi", "got first packet back");

  yield closeToolbox();

  assert.equal(panel.readyState, "destroyed", "panel is destroyed");

  myTool.destroy();
});

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