/* Any copyright is dedicated to the Public Domain.
   http://creativecommons.org/publicdomain/zero/1.0/ */
/* eslint-disable mozilla/no-arbitrary-setTimeout */

"use strict";

const { require } = ChromeUtils.importESModule(
  "resource://devtools/shared/loader/Loader.sys.mjs"
);
const EventEmitter = require("resource://devtools/shared/event-emitter.js");
const discovery = require("resource://devtools/shared/discovery/discovery.js");
const { setTimeout, clearTimeout } = ChromeUtils.importESModule(
  "resource://gre/modules/Timer.sys.mjs"
);

Services.prefs.setBoolPref("devtools.discovery.log", true);

registerCleanupFunction(() => {
  Services.prefs.clearUserPref("devtools.discovery.log");
});

function log(msg) {
  info("DISCOVERY: " + msg);
}

// Global map of actively listening ports to TestTransport instances
var gTestTransports = {};

/**
 * Implements the same API as Transport in discovery.js.  Here, no UDP sockets
 * are used.  Instead, messages are delivered immediately.
 */
function TestTransport(port) {
  EventEmitter.decorate(this);
  this.port = port;
  gTestTransports[this.port] = this;
}

TestTransport.prototype = {
  send(object, port) {
    log("Send to " + port + ":\n" + JSON.stringify(object, null, 2));
    if (!gTestTransports[port]) {
      log("No listener on port " + port);
      return;
    }
    const message = JSON.stringify(object);
    gTestTransports[port].onPacketReceived(null, message);
  },

  destroy() {
    delete gTestTransports[this.port];
  },

  // nsIUDPSocketListener

  onPacketReceived(socket, message) {
    const object = JSON.parse(message);
    object.from = "localhost";
    log("Recv on " + this.port + ":\n" + JSON.stringify(object, null, 2));
    this.emit("message", object);
  },

  onStopListening() {},
};

// Use TestTransport instead of the usual Transport
discovery._factories.Transport = TestTransport;

// Ignore name generation on b2g and force a fixed value
Object.defineProperty(discovery.device, "name", {
  get() {
    return "test-device";
  },
});

add_task(async function () {
  // At startup, no remote devices are known
  deepEqual(discovery.getRemoteDevicesWithService("devtools"), []);
  deepEqual(discovery.getRemoteDevicesWithService("penguins"), []);

  discovery.scan();

  // No services added yet, still empty
  deepEqual(discovery.getRemoteDevicesWithService("devtools"), []);
  deepEqual(discovery.getRemoteDevicesWithService("penguins"), []);

  discovery.addService("devtools", { port: 1234 });

  // Changes not visible until next scan
  deepEqual(discovery.getRemoteDevicesWithService("devtools"), []);
  deepEqual(discovery.getRemoteDevicesWithService("penguins"), []);

  await scanForChange("devtools", "added");

  // Now we see the new service
  deepEqual(discovery.getRemoteDevicesWithService("devtools"), ["test-device"]);
  deepEqual(discovery.getRemoteDevicesWithService("penguins"), []);

  discovery.addService("penguins", { tux: true });
  await scanForChange("penguins", "added");

  deepEqual(discovery.getRemoteDevicesWithService("devtools"), ["test-device"]);
  deepEqual(discovery.getRemoteDevicesWithService("penguins"), ["test-device"]);
  deepEqual(discovery.getRemoteDevices(), ["test-device"]);

  deepEqual(discovery.getRemoteService("devtools", "test-device"), {
    port: 1234,
    host: "localhost",
  });
  deepEqual(discovery.getRemoteService("penguins", "test-device"), {
    tux: true,
    host: "localhost",
  });

  discovery.removeService("devtools");
  await scanForChange("devtools", "removed");

  discovery.addService("penguins", { tux: false });
  await scanForChange("penguins", "updated");

  // Scan again, but nothing should be removed
  await scanForNoChange("penguins", "removed");

  // Split the scanning side from the service side to simulate the machine with
  // the service becoming unreachable
  gTestTransports = {};

  discovery.removeService("penguins");
  await scanForChange("penguins", "removed");
});

function scanForChange(service, changeType) {
  return new Promise((resolve, reject) => {
    const timer = setTimeout(() => {
      reject(new Error("Reply never arrived"));
    }, discovery.replyTimeout + 500);
    discovery.on(service + "-device-" + changeType, function onChange() {
      discovery.off(service + "-device-" + changeType, onChange);
      clearTimeout(timer);
      resolve();
    });
    discovery.scan();
  });
}

function scanForNoChange(service, changeType) {
  return new Promise((resolve, reject) => {
    const timer = setTimeout(() => {
      resolve();
    }, discovery.replyTimeout + 500);
    discovery.on(service + "-device-" + changeType, function onChange() {
      discovery.off(service + "-device-" + changeType, onChange);
      clearTimeout(timer);
      reject(new Error("Unexpected change occurred"));
    });
    discovery.scan();
  });
}
