"use strict";

ChromeUtils.import("resource://gre/modules/XPCOMUtils.jsm");

/**
 * Defers one or more callbacks until the next turn of the event loop. Multiple
 * callbacks are executed in order.
 *
 * @param {Function[]} callbacks The callbacks to execute. One callback will be
 *  executed per tick.
 */
function waterfall(...callbacks) {
  callbacks.reduce((promise, callback) => promise.then(() => {
    callback();
  }), Promise.resolve()).catch(Cu.reportError);
}

/**
 * Minimal implementation of a mock WebSocket connect to be used with
 * PushService. Forwards and receive messages from the implementation
 * that lives in the content process.
 */
function MockWebSocketParent(originalURI) {
  this._originalURI = originalURI;
}

MockWebSocketParent.prototype = {
  _originalURI: null,

  _listener: null,
  _context: null,

  QueryInterface: XPCOMUtils.generateQI([
    Ci.nsISupports,
    Ci.nsIWebSocketChannel
  ]),

  get originalURI() {
    return this._originalURI;
  },

  asyncOpen(uri, origin, windowId, listener, context) {
    this._listener = listener;
    this._context = context;
    waterfall(() => this._listener.onStart(this._context));
  },

  sendMsg(msg) {
    sendAsyncMessage("socket-client-msg", msg);
  },

  close() {
    waterfall(() => this._listener.onStop(this._context, Cr.NS_OK));
  },

  serverSendMsg(msg) {
    waterfall(() => this._listener.onMessageAvailable(this._context, msg),
              () => this._listener.onAcknowledge(this._context, 0));
  },
};

var pushService = Cc["@mozilla.org/push/Service;1"].
                  getService(Ci.nsIPushService).
                  wrappedJSObject;

var mockSocket;
var serverMsgs = [];

addMessageListener("socket-setup", function () {
  pushService.replaceServiceBackend({
    serverURI: "wss://push.example.org/",
    makeWebSocket(uri) {
      mockSocket = new MockWebSocketParent(uri);
      while (serverMsgs.length > 0) {
        let msg = serverMsgs.shift();
        mockSocket.serverSendMsg(msg);
      }
      return mockSocket;
    }
  });
});

addMessageListener("socket-teardown", function (msg) {
  pushService.restoreServiceBackend().then(_ => {
    serverMsgs.length = 0;
    if (mockSocket) {
      mockSocket.close();
      mockSocket = null;
    }
    sendAsyncMessage("socket-server-teardown");
  }).catch(error => {
    Cu.reportError(`Error restoring service backend: ${error}`);
  })
});

addMessageListener("socket-server-msg", function (msg) {
  if (mockSocket) {
    mockSocket.serverSendMsg(msg);
  } else {
    serverMsgs.push(msg);
  }
});

var MockService = {
  requestID: 1,
  resolvers: new Map(),

  sendRequest(name, params) {
    return new Promise((resolve, reject) => {
      let id = this.requestID++;
      this.resolvers.set(id, { resolve, reject });
      sendAsyncMessage("service-request", {
        name: name,
        id: id,
        params: params,
      });
    });
  },

  handleResponse(response) {
    if (!this.resolvers.has(response.id)) {
      Cu.reportError(`Unexpected response for request ${response.id}`);
      return;
    }
    let resolver = this.resolvers.get(response.id);
    this.resolvers.delete(response.id);
    if (response.error) {
      resolver.reject(response.error);
    } else {
      resolver.resolve(response.result);
    }
  },

  init() {},

  register(pageRecord) {
    return this.sendRequest("register", pageRecord);
  },

  registration(pageRecord) {
    return this.sendRequest("registration", pageRecord);
  },

  unregister(pageRecord) {
    return this.sendRequest("unregister", pageRecord);
  },

  reportDeliveryError(messageId, reason) {
    sendAsyncMessage("service-delivery-error", {
      messageId: messageId,
      reason: reason,
    });
  },
};

addMessageListener("service-replace", function () {
  pushService.service = MockService;
});

addMessageListener("service-restore", function () {
  pushService.service = null;
});

addMessageListener("service-response", function (response) {
  MockService.handleResponse(response);
});
