"use strict";

/* globals browser */

Cu.import("resource://gre/modules/LegacyExtensionsUtils.jsm");

// Import EmbeddedExtensionManager to be able to check that the
// tacked instances are cleared after the embedded extension shutdown.
const {
  EmbeddedExtensionManager,
} = Cu.import("resource://gre/modules/LegacyExtensionsUtils.jsm", {});

/**
 * This test case ensures that the LegacyExtensionsUtils.EmbeddedExtension:
 *  - load the embedded webextension resources from a "/webextension/" dir
 *    inside the XPI.
 *  - EmbeddedExtension.prototype.api returns an API object which exposes
 *    a working `runtime.onConnect` event object (e.g. the API can receive a port
 *    when the embedded webextension is started  and it can exchange messages
 *    with the background page).
 *  - EmbeddedExtension.prototype.startup/shutdown methods manage the embedded
 *    webextension lifecycle as expected.
 */
add_task(async function test_embedded_webextension_utils() {
  function backgroundScript() {
    let port = browser.runtime.connect();

    port.onMessage.addListener((msg) => {
      if (msg == "legacy_extension -> webextension") {
        port.postMessage("webextension -> legacy_extension");
        port.disconnect();
      }
    });
  }

  const id = "@test.embedded.web.extension";

  // Extensions.generateXPI is used here (and in the other hybrid addons tests in this same
  // test dir) to be able to generate an xpi with the directory layout that we expect from
  // an hybrid legacy+webextension addon (where all the embedded webextension resources are
  // loaded from a 'webextension/' directory).
  let fakeHybridAddonFile = Extension.generateZipFile({
    "webextension/manifest.json": {
      applications: {gecko: {id}},
      name: "embedded webextension name",
      manifest_version: 2,
      version: "1.0",
      background: {
        scripts: ["bg.js"],
      },
    },
    "webextension/bg.js": `new ${backgroundScript}`,
  });

  // Remove the generated xpi file and flush the its jar cache
  // on cleanup.
  registerCleanupFunction(() => {
    Services.obs.notifyObservers(fakeHybridAddonFile, "flush-cache-entry");
    fakeHybridAddonFile.remove(false);
  });

  let fileURI = Services.io.newFileURI(fakeHybridAddonFile);
  let resourceURI = Services.io.newURI(`jar:${fileURI.spec}!/`);

  let embeddedExtension = LegacyExtensionsUtils.getEmbeddedExtensionFor({
    id, resourceURI,
  });

  ok(embeddedExtension, "Got the embeddedExtension object");

  equal(EmbeddedExtensionManager.embeddedExtensionsByAddonId.size, 1,
        "Got the expected number of tracked embedded extension instances");

  info("waiting embeddedExtension.startup");
  let embeddedExtensionAPI = await embeddedExtension.startup();
  ok(embeddedExtensionAPI, "Got the embeddedExtensionAPI object");

  let waitConnectPort = new Promise(resolve => {
    let {browser} = embeddedExtensionAPI;
    browser.runtime.onConnect.addListener(port => {
      resolve(port);
    });
  });

  let port = await waitConnectPort;

  ok(port, "Got the Port API object");

  let waitPortMessage = new Promise(resolve => {
    port.onMessage.addListener((msg) => {
      resolve(msg);
    });
  });

  port.postMessage("legacy_extension -> webextension");

  let msg = await waitPortMessage;

  equal(msg, "webextension -> legacy_extension",
        "LegacyExtensionContext received the expected message from the webextension");

  let waitForDisconnect = new Promise(resolve => {
    port.onDisconnect.addListener(resolve);
  });

  info("Wait for the disconnect port event");
  await waitForDisconnect;
  info("Got the disconnect port event");

  await embeddedExtension.shutdown();

  equal(EmbeddedExtensionManager.embeddedExtensionsByAddonId.size, 0,
        "EmbeddedExtension instances has been untracked from the EmbeddedExtensionManager");
});

async function createManifestErrorTestCase(id, xpi, expectedError) {
  // Remove the generated xpi file and flush the its jar cache
  // on cleanup.
  registerCleanupFunction(() => {
    Services.obs.notifyObservers(xpi, "flush-cache-entry");
    xpi.remove(false);
  });

  let fileURI = Services.io.newFileURI(xpi);
  let resourceURI = Services.io.newURI(`jar:${fileURI.spec}!/`);

  let embeddedExtension = LegacyExtensionsUtils.getEmbeddedExtensionFor({
    id, resourceURI,
  });

  await Assert.rejects(embeddedExtension.startup(), expectedError,
                       "embedded extension startup rejected");

  // Shutdown a "never-started" addon with an embedded webextension should not
  // raise any exception, and if it does this test will fail.
  await embeddedExtension.shutdown();
}

add_task(async function test_startup_error_empty_manifest() {
  const id = "empty-manifest@test.embedded.web.extension";
  const files = {
    "webextension/manifest.json": ``,
  };
  const expectedError = "(NS_BASE_STREAM_CLOSED)";

  let fakeHybridAddonFile = Extension.generateZipFile(files);

  await createManifestErrorTestCase(id, fakeHybridAddonFile, expectedError);
});

add_task(async function test_startup_error_invalid_json_manifest() {
  const id = "invalid-json-manifest@test.embedded.web.extension";
  const files = {
    "webextension/manifest.json": `{ "name": }`,
  };
  const expectedError = "JSON.parse:";

  let fakeHybridAddonFile = Extension.generateZipFile(files);

  await createManifestErrorTestCase(id, fakeHybridAddonFile, expectedError);
});

add_task(async function test_startup_error_blocking_validation_errors() {
  const id = "blocking-manifest-validation-error@test.embedded.web.extension";
  const files = {
    "webextension/manifest.json": {
      name: "embedded webextension name",
      manifest_version: 2,
      version: "1.0",
      background: {
        scripts: {},
      },
    },
  };

  function expectedError(actual) {
    if (actual.errors && actual.errors.length == 1 &&
        actual.errors[0].startsWith("Reading manifest:")) {
      return true;
    }

    return false;
  }

  let fakeHybridAddonFile = Extension.generateZipFile(files);

  await createManifestErrorTestCase(id, fakeHybridAddonFile, expectedError);
});
