/* Any copyright is dedicated to the Public Domain.
   http://creativecommons.org/publicdomain/zero/1.0/ */

"use strict";

/* Exercise prefix-based forwarding of packets to other transports. */

const { RootActor } = require("resource://devtools/server/actors/root.js");

var gMainConnection, gMainTransport;
var gSubconnection1, gSubconnection2;
var gClient;

function run_test() {
  DevToolsServer.init();

  add_test(createMainConnection);
  add_test(TestNoForwardingYet);
  add_test(createSubconnection1);
  add_test(TestForwardPrefix1OnlyRoot);
  add_test(createSubconnection2);
  add_test(TestForwardPrefix12OnlyRoot);
  add_test(TestForwardPrefix12WithActor1);
  add_test(TestForwardPrefix12WithActor12);
  run_next_test();
}

/*
 * Create a pipe connection, and return an object |{ conn, transport }|,
 * where |conn| is the new DevToolsServerConnection instance, and
 * |transport| is the client side of the transport on which it communicates
 * (that is, packets sent on |transport| go to the new connection, and
 * |transport|'s hooks receive replies).
 *
 * |prefix| is optional; if present, it's the prefix (minus the '/') for
 * actors in the new connection.
 */
function newConnection(prefix) {
  let conn;
  DevToolsServer.createRootActor = function (connection) {
    conn = connection;
    return new RootActor(connection, {});
  };

  const transport = DevToolsServer.connectPipe(prefix);

  return { conn, transport };
}

/* Create the main connection for these tests. */
function createMainConnection() {
  ({ conn: gMainConnection, transport: gMainTransport } = newConnection());
  gClient = new DevToolsClient(gMainTransport);
  gClient.connect().then(() => run_next_test());
}

/*
 * Exchange 'echo' messages with five actors:
 * - root
 * - prefix1/root
 * - prefix1/actor
 * - prefix2/root
 * - prefix2/actor
 *
 * Expect proper echos from those named in |reachables|, and 'noSuchActor'
 * errors from the others. When we've gotten all our replies (errors or
 * otherwise), call |completed|.
 *
 * To avoid deep stacks, we call completed from the next tick.
 */
async function tryActors(reachables, completed) {
  for (const actor of [
    "root",
    "prefix1/root",
    "prefix1/actor",
    "prefix2/root",
    "prefix2/actor",
  ]) {
    let response;
    try {
      if (actor.endsWith("root")) {
        // Root actor doesn't expose any echo method,
        // so fallback on getRoot which returns `{ from: "root" }`.
        // For the top level root actor, we have to use its front.
        if (actor == "root") {
          response = await gClient.mainRoot.getRoot();
        } else {
          response = await gClient.request({ to: actor, type: "getRoot" });
        }
      } else {
        response = await gClient.request({
          to: actor,
          type: "echo",
          value: "tango",
        });
      }
    } catch (e) {
      response = e;
    }
    if (reachables.has(actor)) {
      if (actor.endsWith("root")) {
        // RootActor's getRoot response is almost empty on xpcshell
        Assert.deepEqual({ from: actor }, response);
      } else {
        Assert.deepEqual(
          { from: actor, to: actor, type: "echo", value: "tango" },
          response
        );
      }
    } else {
      Assert.deepEqual(
        {
          from: actor,
          error: "noSuchActor",
          message: "No such actor for ID: " + actor,
        },
        response
      );
    }
  }
  executeSoon(completed, "tryActors callback " + completed.name);
}

/*
 * With no forwarding established, sending messages to root should work,
 * but sending messages to prefixed actor names, or anyone else, should get
 * an error.
 */
function TestNoForwardingYet() {
  tryActors(new Set(["root"]), run_next_test);
}

/*
 * Create a new pipe connection which forwards its reply packets to
 * gMainConnection's client, and to which gMainConnection forwards packets
 * directed to actors whose names begin with |prefix + '/'|, and.
 *
 * Return an object { conn, transport }, as for newConnection.
 */
function newSubconnection(prefix) {
  const { conn, transport } = newConnection(prefix);
  transport.hooks = {
    onPacket: packet => gMainConnection.send(packet),
  };
  gMainConnection.setForwarding(prefix, transport);

  return { conn, transport };
}

/* Create a second root actor, to which we can forward things. */
function createSubconnection1() {
  const { conn, transport } = newSubconnection("prefix1");
  gSubconnection1 = conn;
  transport.ready();
  gClient.expectReply("prefix1/root", () => run_next_test());
}

// Establish forwarding, but don't put any actors in that server.
function TestForwardPrefix1OnlyRoot() {
  tryActors(new Set(["root", "prefix1/root"]), run_next_test);
}

/* Create a third root actor, to which we can forward things. */
function createSubconnection2() {
  const { conn, transport } = newSubconnection("prefix2");
  gSubconnection2 = conn;
  transport.ready();
  gClient.expectReply("prefix2/root", () => run_next_test());
}

function TestForwardPrefix12OnlyRoot() {
  tryActors(new Set(["root", "prefix1/root", "prefix2/root"]), run_next_test);
}

// A dumb actor that implements 'echo'.
//
// It's okay that both subconnections' actors behave identically, because
// the reply-sending code attaches the replying actor's name to the packet,
// so simply matching the 'from' field in the reply ensures that we heard
// from the right actor.
const { Actor } = require("resource://devtools/shared/protocol/Actor.js");
class EchoActor extends Actor {
  constructor(conn) {
    super(conn, { typeName: "EchoActor", methods: [] });

    this.requestTypes = {
      echo: EchoActor.prototype.onEcho,
    };
  }

  onEcho(request) {
    /*
     * Request packets are frozen. Copy request, so that
     * DevToolsServerConnection.onPacket can attach a 'from' property.
     */
    return JSON.parse(JSON.stringify(request));
  }
}

function TestForwardPrefix12WithActor1() {
  const actor = new EchoActor(gSubconnection1);
  actor.actorID = "prefix1/actor";
  gSubconnection1.addActor(actor);

  tryActors(
    new Set(["root", "prefix1/root", "prefix1/actor", "prefix2/root"]),
    run_next_test
  );
}

function TestForwardPrefix12WithActor12() {
  const actor = new EchoActor(gSubconnection2);
  actor.actorID = "prefix2/actor";
  gSubconnection2.addActor(actor);

  tryActors(
    new Set([
      "root",
      "prefix1/root",
      "prefix1/actor",
      "prefix2/root",
      "prefix2/actor",
    ]),
    run_next_test
  );
}
