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

"use strict";

/* import-globals-from ../../../shared/test/shared-head.js */

const MOCKS_ROOT = CHROME_URL_ROOT + "mocks/";
/* import-globals-from mocks/helper-adb-mock.js */
Services.scriptloader.loadSubScript(MOCKS_ROOT + "helper-adb-mock.js", this);
/* import-globals-from mocks/helper-client-wrapper-mock.js */
Services.scriptloader.loadSubScript(
  MOCKS_ROOT + "helper-client-wrapper-mock.js",
  this
);
/* import-globals-from mocks/helper-runtime-client-factory-mock.js */
Services.scriptloader.loadSubScript(
  MOCKS_ROOT + "helper-runtime-client-factory-mock.js",
  this
);

const {
  RUNTIMES,
} = require("resource://devtools/client/aboutdebugging/src/constants.js");

/**
 * This wrapper around the mocks used in about:debugging tests provides helpers to
 * quickly setup mocks for runtime tests involving USB, network or wifi runtimes that can
 * are difficult to setup in a test environment.
 */
class Mocks {
  constructor() {
    // Setup the adb mock to rely on internal arrays.
    this.adbMock = createAdbMock();
    this.adbProcessMock = createAdbProcessMock();
    this._usbRuntimes = [];
    this._usbDevices = [];
    this.adbMock.adb.getRuntimes = () => {
      return this._usbRuntimes;
    };
    this.adbMock.adb.getDevices = () => {
      const runtimeDevices = this._usbRuntimes.map(r => {
        return { id: r.deviceId, name: r.deviceName };
      });
      return runtimeDevices.concat(this._usbDevices);
    };

    // adb.updateRuntimes should ultimately fire the "runtime-list-updated" event.
    this.adbMock.adb.updateRuntimes = () => {
      this.emitUSBUpdate();
    };

    this.adbMock.adb.isProcessStarted = () => true;

    // Prepare a fake observer to be able to emit events from this mock.
    this._observerMock = addObserverMock(this.adbMock.adb);

    // Setup the runtime-client-factory mock to rely on the internal _clients map.
    this.runtimeClientFactoryMock = createRuntimeClientFactoryMock();
    this._clients = {
      [RUNTIMES.NETWORK]: {},
      [RUNTIMES.THIS_FIREFOX]: {},
      [RUNTIMES.USB]: {},
    };
    this.runtimeClientFactoryMock.createClientForRuntime = runtime => {
      return this._clients[runtime.type][runtime.id];
    };

    // Add a client for THIS_FIREFOX, since about:debugging will start on the This Firefox
    // page.
    this._thisFirefoxClient = createThisFirefoxClientMock();
    this._clients[RUNTIMES.THIS_FIREFOX][RUNTIMES.THIS_FIREFOX] =
      this._thisFirefoxClient;

    // Enable mocks and remove them after the test.
    this.enableMocks();
    registerCleanupFunction(() => this.disableMocks());
  }

  get thisFirefoxClient() {
    return this._thisFirefoxClient;
  }

  enableMocks() {
    enableAdbMock(this.adbMock);
    enableAdbProcessMock(this.adbProcessMock);
    enableRuntimeClientFactoryMock(this.runtimeClientFactoryMock);
  }

  disableMocks() {
    disableAdbMock();
    disableAdbProcessMock();
    disableRuntimeClientFactoryMock();

    for (const host of Object.keys(this._clients[RUNTIMES.NETWORK])) {
      this.removeNetworkRuntime(host);
    }
  }

  createNetworkRuntime(host, runtimeInfo) {
    const {
      addNetworkLocation,
    } = require("resource://devtools/client/aboutdebugging/src/modules/network-locations.js");
    addNetworkLocation(host);

    // Add a valid client that can be returned for this particular runtime id.
    const mockNetworkClient = createClientMock();
    mockNetworkClient.getDeviceDescription = () => {
      return {
        name: runtimeInfo.name || "TestBrand",
        channel: runtimeInfo.channel || "release",
        version: runtimeInfo.version || "1.0",
      };
    };
    this._clients[RUNTIMES.NETWORK][host] = mockNetworkClient;

    return mockNetworkClient;
  }

  removeNetworkRuntime(host) {
    const {
      removeNetworkLocation,
    } = require("resource://devtools/client/aboutdebugging/src/modules/network-locations.js");
    removeNetworkLocation(host);

    delete this._clients[RUNTIMES.NETWORK][host];
  }

  emitUSBUpdate() {
    this._observerMock.emit("runtime-list-updated");
  }

  /**
   * Creates a USB runtime for which a client conenction can be established.
   *
   * @param {string} id
   *        The id of the runtime.
   * @param {object} optional object used to create the fake runtime & device
   *        - channel: {String} Release channel, for instance "release", "nightly"
   *        - clientWrapper: {ClientWrapper} optional ClientWrapper for this runtime
   *        - deviceId: {String} Device id
   *        - deviceName: {String} Device name
   *        - isFenix: {Boolean} set by ADB if the package name matches a Fenix package
   *        - name: {String} Application name, for instance "Firefox"
   *        - shortName: {String} Short name for the device
   *        - socketPath: {String} (should only be used for connecting, so not here)
   *        - version: {String} Version, for instance "63.0a"
   *        - versionName: {String} Version return by ADB "63.0a"
   * @return {object} Returns the mock client created for this runtime so that methods
   * can be overridden on it.
   */
  createUSBRuntime(id, runtimeInfo = {}) {
    // Add a new runtime to the list of scanned runtimes.
    this._usbRuntimes.push({
      deviceId: runtimeInfo.deviceId || "test device id",
      deviceName: runtimeInfo.deviceName || "test device name",
      id,
      isFenix: runtimeInfo.isFenix,
      shortName: runtimeInfo.shortName || "testshort",
      socketPath: runtimeInfo.socketPath || "test/path",
      versionName: runtimeInfo.versionName || "1.0",
    });

    // Add a valid client that can be returned for this particular runtime id.
    let mockUsbClient = runtimeInfo.clientWrapper;
    if (mockUsbClient) {
      const originalGetDeviceDescription =
        mockUsbClient.getDeviceDescription.bind(mockUsbClient);
      mockUsbClient.getDeviceDescription = async () => {
        const deviceDescription = await originalGetDeviceDescription();
        return {
          channel: runtimeInfo.channel || deviceDescription.channel,
          name: runtimeInfo.name || deviceDescription.name,
          version: runtimeInfo.version || deviceDescription.version,
        };
      };
    } else {
      // If no clientWrapper was provided, create a mock client here.
      mockUsbClient = createClientMock();
      mockUsbClient.getDeviceDescription = () => {
        return {
          channel: runtimeInfo.channel || "release",
          name: runtimeInfo.name || "TestBrand",
          version: runtimeInfo.version || "1.0",
        };
      };
    }

    this._clients[RUNTIMES.USB][id] = mockUsbClient;

    return mockUsbClient;
  }

  removeUSBRuntime(id) {
    this._usbRuntimes = this._usbRuntimes.filter(runtime => runtime.id !== id);
    delete this._clients[RUNTIMES.USB][id];
  }

  addDevice(deviceId, deviceName) {
    this._usbDevices.push({
      id: deviceId,
      name: deviceName,
    });
  }

  removeDevice(deviceId) {
    this._usbDevices = this._usbDevices.filter(d => {
      return d.id !== deviceId;
    });
  }

  removeRuntime(id) {
    if (this._clients[RUNTIMES.USB][id]) {
      this.removeUSBRuntime(id);
    } else if (this._clients[RUNTIMES.NETWORK][id]) {
      this.removeNetworkRuntime(id);
    }
  }
}
/* exported Mocks */

const silenceWorkerUpdates = function () {
  const {
    removeMockedModule,
    setMockedModule,
  } = require("resource://devtools/shared/loader/browser-loader-mocks.js");

  const mock = {
    WorkersListener: () => {
      return {
        addListener: () => {},
        removeListener: () => {},
      };
    },
  };
  setMockedModule(mock, "devtools/client/shared/workers-listener");

  registerCleanupFunction(() => {
    removeMockedModule("devtools/client/shared/workers-listener");
  });
};
/* exported silenceWorkerUpdates */

async function createLocalClientWrapper() {
  info("Create a local DevToolsClient");

  // First, instantiate a DevToolsServer, the same way it is being done when running
  // firefox --start-debugger-server
  const {
    useDistinctSystemPrincipalLoader,
    releaseDistinctSystemPrincipalLoader,
  } = ChromeUtils.importESModule(
    "resource://devtools/shared/loader/DistinctSystemPrincipalLoader.sys.mjs"
  );
  const requester = {};
  const serverLoader = useDistinctSystemPrincipalLoader(requester);
  registerCleanupFunction(() => {
    releaseDistinctSystemPrincipalLoader(requester);
  });
  const { DevToolsServer } = serverLoader.require(
    "resource://devtools/server/devtools-server.js"
  );
  DevToolsServer.init();
  DevToolsServer.registerAllActors();
  DevToolsServer.allowChromeProcess = true;

  // Then spawn a DevToolsClient connected to this new DevToolsServer
  const {
    DevToolsClient,
  } = require("resource://devtools/client/devtools-client.js");
  const {
    ClientWrapper,
  } = require("resource://devtools/client/aboutdebugging/src/modules/client-wrapper.js");

  const client = new DevToolsClient(DevToolsServer.connectPipe());

  await client.connect();
  return new ClientWrapper(client);
}
/* exported createLocalClientWrapper */

// Create a basic mock for this-firefox client, and setup a runtime-client-factory mock
// to return our mock client when needed.
function setupThisFirefoxMock() {
  const runtimeClientFactoryMock = createRuntimeClientFactoryMock();
  const thisFirefoxClient = createThisFirefoxClientMock();
  runtimeClientFactoryMock.createClientForRuntime = runtime => {
    if (runtime.id === RUNTIMES.THIS_FIREFOX) {
      return thisFirefoxClient;
    }
    throw new Error("Unexpected runtime id " + runtime.id);
  };

  info("Enable mocks");
  enableRuntimeClientFactoryMock(runtimeClientFactoryMock);
  registerCleanupFunction(() => {
    disableRuntimeClientFactoryMock();
  });

  return thisFirefoxClient;
}
/* exported setupThisFirefoxMock */
