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

// Non-extension add-ons are not supported on Android, but some tests install
// non-extension add-ons, such as static themes, dictionaries, langpacks.
const skipOnAndroid = () => ({
  skip_if: () => AppConstants.platform === "android",
});

const ID = "webextension1@tests.mozilla.org";

const profileDir = gProfD.clone();
profileDir.append("extensions");

const ADDONS = {
  webextension_1: {
    "manifest.json": {
      name: "Web Extension Name",
      version: "1.0",
      manifest_version: 2,
      browser_specific_settings: {
        gecko: {
          id: "webextension1@tests.mozilla.org",
        },
      },
      icons: {
        48: "icon48.png",
        64: "icon64.png",
      },
    },
    "chrome.manifest": "content webex ./\n",
  },
  webextension_3: {
    "manifest.json": {
      name: "Web Extensiøn __MSG_name__",
      description: "Descriptïon __MSG_desc__ of add-on",
      version: "1.0",
      manifest_version: 2,
      default_locale: "en",
      browser_specific_settings: {
        gecko: {
          id: "webextension3@tests.mozilla.org",
        },
      },
    },
    "_locales/en/messages.json": {
      name: {
        message: "foo ☹",
        description: "foo",
      },
      desc: {
        message: "bar ☹",
        description: "bar",
      },
    },
    "_locales/fr/messages.json": {
      name: {
        message: "le foo ☺",
        description: "foo",
      },
      desc: {
        message: "le bar ☺",
        description: "bar",
      },
    },
  },
};

let chromeReg = Cc["@mozilla.org/chrome/chrome-registry;1"].getService(
  Ci.nsIChromeRegistry
);

createAppInfo("xpcshell@tests.mozilla.org", "XPCShell", "1", "42");

const {
  ExtensionParent: { GlobalManager },
} = ChromeUtils.importESModule(
  "resource://gre/modules/ExtensionParent.sys.mjs"
);

add_task(async function test_1() {
  await promiseStartupManager();

  equal(GlobalManager.extensionMap.size, 0);

  let { addon } = await AddonTestUtils.promiseInstallXPI(ADDONS.webextension_1);

  equal(GlobalManager.extensionMap.size, 1);
  ok(GlobalManager.extensionMap.has(ID));

  Assert.throws(
    () =>
      chromeReg.convertChromeURL(
        Services.io.newURI("chrome://webex/content/webex.xul")
      ),
    error => error.result == Cr.NS_ERROR_FILE_NOT_FOUND,
    "Chrome manifest should not have been registered"
  );

  let uri = do_get_addon_root_uri(profileDir, ID);

  checkAddon(ID, addon, {
    version: "1.0",
    name: "Web Extension Name",
    isCompatible: true,
    appDisabled: false,
    isActive: true,
    isSystem: false,
    type: "extension",
    isWebExtension: true,
    signedState: AddonManager.SIGNEDSTATE_PRIVILEGED,
    iconURL: `${uri}icon48.png`,
  });

  // Should persist through a restart
  await promiseShutdownManager();

  equal(GlobalManager.extensionMap.size, 0);

  await promiseStartupManager();

  equal(GlobalManager.extensionMap.size, 1);
  ok(GlobalManager.extensionMap.has(ID));

  addon = await promiseAddonByID(ID);

  uri = do_get_addon_root_uri(profileDir, ID);

  checkAddon(ID, addon, {
    version: "1.0",
    name: "Web Extension Name",
    isCompatible: true,
    appDisabled: false,
    isActive: true,
    isSystem: false,
    type: "extension",
    signedState: AddonManager.SIGNEDSTATE_PRIVILEGED,
    iconURL: `${uri}icon48.png`,
  });

  await addon.disable();

  equal(GlobalManager.extensionMap.size, 0);

  await addon.enable();

  equal(GlobalManager.extensionMap.size, 1);
  ok(GlobalManager.extensionMap.has(ID));

  await addon.uninstall();

  equal(GlobalManager.extensionMap.size, 0);
  Assert.ok(!GlobalManager.extensionMap.has(ID));

  await promiseShutdownManager();
});

// Writing the manifest direct to the profile should work
add_task(async function test_2() {
  await promiseWriteWebManifestForExtension(
    {
      name: "Web Extension Name",
      version: "1.0",
      manifest_version: 2,
      browser_specific_settings: {
        gecko: {
          id: ID,
        },
      },
    },
    profileDir
  );

  await promiseStartupManager();

  let addon = await promiseAddonByID(ID);
  checkAddon(ID, addon, {
    version: "1.0",
    name: "Web Extension Name",
    isCompatible: true,
    appDisabled: false,
    isActive: true,
    isSystem: false,
    type: "extension",
    signedState: AddonManager.SIGNEDSTATE_PRIVILEGED,
  });

  await addon.uninstall();

  await promiseRestartManager();
});

add_task(async function test_manifest_localization() {
  const extensionId = "webextension3@tests.mozilla.org";

  let { addon } = await AddonTestUtils.promiseInstallXPI(ADDONS.webextension_3);

  await addon.disable();

  checkAddon(ID, addon, {
    name: "Web Extensiøn foo ☹",
    description: "Descriptïon bar ☹ of add-on",
  });

  await restartWithLocales(["fr-FR"]);

  addon = await promiseAddonByID(extensionId);
  checkAddon(ID, addon, {
    name: "Web Extensiøn le foo ☺",
    description: "Descriptïon le bar ☺ of add-on",
  });

  await restartWithLocales(["de"]);

  addon = await promiseAddonByID(extensionId);
  checkAddon(ID, addon, {
    name: "Web Extensiøn foo ☹",
    description: "Descriptïon bar ☹ of add-on",
  });

  await addon.uninstall();
});

// Missing version should cause a failure
add_task(async function test_3() {
  await promiseWriteWebManifestForExtension(
    {
      name: "Web Extension Name",
      manifest_version: 2,
      browser_specific_settings: {
        gecko: {
          id: ID,
        },
      },
    },
    profileDir
  );

  await promiseRestartManager();

  let addon = await promiseAddonByID(ID);
  Assert.equal(addon, null);

  let file = getFileForAddon(profileDir, ID);
  Assert.ok(!file.exists());

  await promiseRestartManager();
});

// Incorrect manifest version should cause a failure
add_task(async function test_4() {
  await promiseWriteWebManifestForExtension(
    {
      name: "Web Extension Name",
      version: "1.0",
      manifest_version: 1,
      browser_specific_settings: {
        gecko: {
          id: ID,
        },
      },
    },
    profileDir
  );

  await promiseRestartManager();

  let addon = await promiseAddonByID(ID);
  Assert.equal(addon, null);

  let file = getFileForAddon(profileDir, ID);
  Assert.ok(!file.exists());

  await promiseRestartManager();
});

// Test that the "options_ui" manifest section is processed correctly.
add_task(async function test_options_ui() {
  let OPTIONS_RE =
    /^moz-extension:\/\/[\da-f]{8}-[\da-f]{4}-[\da-f]{4}-[\da-f]{4}-[\da-f]{12}\/options\.html$/;

  const extensionId = "webextension@tests.mozilla.org";
  let addon = await promiseInstallWebExtension({
    manifest: {
      browser_specific_settings: { gecko: { id: extensionId } },
      options_ui: {
        page: "options.html",
      },
    },
  });

  checkAddon(extensionId, addon, {
    optionsType: AddonManager.OPTIONS_TYPE_INLINE_BROWSER,
  });

  ok(
    OPTIONS_RE.test(addon.optionsURL),
    "Addon should have a moz-extension: options URL for /options.html"
  );

  await addon.uninstall();

  const ID2 = "webextension2@tests.mozilla.org";
  addon = await promiseInstallWebExtension({
    manifest: {
      browser_specific_settings: { gecko: { id: ID2 } },
      options_ui: {
        page: "options.html",
        open_in_tab: true,
      },
    },
  });

  checkAddon(ID2, addon, {
    optionsType: AddonManager.OPTIONS_TYPE_TAB,
  });

  ok(
    OPTIONS_RE.test(addon.optionsURL),
    "Addon should have a moz-extension: options URL for /options.html"
  );

  await addon.uninstall();

  info("Test again with options_page manifest property");
  const ID3 = "options_page_alias@tests.mozilla.org";
  addon = await promiseInstallWebExtension({
    manifest: {
      browser_specific_settings: { gecko: { id: ID3 } },
      options_page: "options.html",
    },
  });

  checkAddon(ID3, addon, {
    optionsType: AddonManager.OPTIONS_TYPE_TAB,
  });

  ok(
    OPTIONS_RE.test(addon.optionsURL),
    "Addon should have a moz-extension: options URL for /options.html"
  );

  await addon.uninstall();

  info("Test options_page and options_page set to a different page");

  const ID4 = "options_page_warning@tests.mozilla.org";
  addon = await promiseInstallWebExtension({
    manifest: {
      browser_specific_settings: { gecko: { id: ID4 } },
      options_page: "options_page.html",
      options_ui: {
        page: "options.html",
        open_in_tab: false,
      },
    },
  });

  checkAddon(ID4, addon, {
    optionsType: AddonManager.OPTIONS_TYPE_INLINE_BROWSER,
  });

  ok(
    OPTIONS_RE.test(addon.optionsURL),
    "Addon should have a moz-extension: options URL for /options.html"
  );

  await addon.uninstall();

  info("Test options_page and options_page are both set to the same page");

  const ID5 = "options_page_and_ui_same_page@tests.mozilla.org";
  addon = await promiseInstallWebExtension({
    manifest: {
      browser_specific_settings: { gecko: { id: ID5 } },
      options_page: "options.html",
      options_ui: { page: "options.html" },
    },
  });

  checkAddon(ID5, addon, {
    optionsType: AddonManager.OPTIONS_TYPE_INLINE_BROWSER,
  });

  ok(
    OPTIONS_RE.test(addon.optionsURL),
    "Addon should have a moz-extension: options URL for /options.html"
  );

  await addon.uninstall();
});

// Test that experiments permissions add the appropriate dependencies.
add_task(async function test_experiments_dependencies() {
  let addon = await promiseInstallWebExtension({
    manifest: {
      browser_specific_settings: { gecko: { id: "meh@experiment" } },
      permissions: ["experiments.meh"],
    },
  });

  checkAddon(addon.id, addon, {
    dependencies: ["meh@experiments.addons.mozilla.org"],
    // Add-on should be app disabled due to missing dependencies
    appDisabled: true,
  });

  await addon.uninstall();
});

add_task(async function developerShouldOverride() {
  let addon = await promiseInstallWebExtension({
    manifest: {
      default_locale: "en",
      developer: {
        name: "__MSG_name__",
        url: "__MSG_url__",
      },
      author: "Will be overridden by developer",
      homepage_url: "https://will.be.overridden",
    },
    files: {
      "_locales/en/messages.json": `{
        "name": {
          "message": "en name"
        },
        "url": {
          "message": "https://example.net/en"
        }
      }`,
    },
  });

  checkAddon(ID, addon, {
    creator: "en name",
    homepageURL: "https://example.net/en",
  });

  await addon.uninstall();
});

add_task(skipOnAndroid(), async function test_invalid_developer_is_ignored() {
  for (const { type, manifestProps, files } of [
    {
      type: "dictionary",
      manifestProps: {
        dictionaries: {
          "en-US": "en-US.dic",
        },
      },
      files: {
        "en-US.dic": "",
        "en-US.aff": "",
      },
    },
    {
      type: "theme",
      manifestProps: {
        theme: {
          colors: {
            frame: "#FFF",
            tab_background_text: "#000",
          },
        },
      },
    },
    {
      type: "locale",
      manifestProps: {
        langpack_id: "und",
        languages: {
          und: {
            chrome_resources: {
              global: "chrome/und/locale/und/global",
            },
            version: "20190326174300",
          },
        },
      },
    },
  ]) {
    const id = `${type}@mozilla.com`;
    const creator = "Some author";
    const homepageURL = "https://example.net";

    info(`== loading add-on with id=${id} ==`);

    for (let developer of [{}, null, { name: null, url: null }]) {
      let addon = await promiseInstallWebExtension({
        manifest: {
          author: creator,
          homepage_url: homepageURL,
          developer,
          browser_specific_settings: { gecko: { id } },
          ...manifestProps,
        },
        files,
      });

      checkAddon(id, addon, { type, creator, homepageURL });

      await addon.uninstall();
    }
  }
});

add_task(async function authorNotString() {
  ExtensionTestUtils.failOnSchemaWarnings(false);
  for (let author of [{}, [], 42]) {
    let addon = await promiseInstallWebExtension({
      manifest: {
        author,
        manifest_version: 2,
        name: "Web Extension Name",
        version: "1.0",
      },
    });

    checkAddon(ID, addon, {
      creator: null,
    });

    await addon.uninstall();
  }
  ExtensionTestUtils.failOnSchemaWarnings(true);
});

add_task(skipOnAndroid(), async function testThemeExtension() {
  let addon = await promiseInstallWebExtension({
    manifest: {
      author: "Some author",
      manifest_version: 2,
      name: "Web Extension Name",
      version: "1.0",
      theme: { images: { theme_frame: "example.png" } },
    },
  });

  checkAddon(ID, addon, {
    creator: "Some author",
    version: "1.0",
    name: "Web Extension Name",
    isCompatible: true,
    appDisabled: false,
    isActive: false,
    userDisabled: true,
    isSystem: false,
    type: "theme",
    isWebExtension: true,
    signedState: AddonManager.SIGNEDSTATE_PRIVILEGED,
  });

  await addon.uninstall();

  // Also test one without a proper 'theme' section.
  ExtensionTestUtils.failOnSchemaWarnings(false);
  addon = await promiseInstallWebExtension({
    manifest: {
      author: "Some author",
      manifest_version: 2,
      name: "Web Extension Name",
      version: "1.0",
      theme: null,
    },
  });
  ExtensionTestUtils.failOnSchemaWarnings(true);

  checkAddon(ID, addon, {
    type: "extension",
    isWebExtension: true,
  });

  await addon.uninstall();
});

// Test that we can update from a webextension to a webextension-theme
add_task(skipOnAndroid(), async function test_theme_upgrade() {
  // First install a regular webextension
  let addon = await promiseInstallWebExtension({
    manifest: {
      version: "1.0",
      name: "Test WebExtension 1 (temporary)",
      browser_specific_settings: {
        gecko: {
          id: ID,
        },
      },
    },
  });

  checkAddon(ID, addon, {
    version: "1.0",
    name: "Test WebExtension 1 (temporary)",
    isCompatible: true,
    appDisabled: false,
    isActive: true,
    type: "extension",
    signedState: AddonManager.SIGNEDSTATE_PRIVILEGED,
  });

  // Create a webextension theme with the same ID
  addon = await promiseInstallWebExtension({
    manifest: {
      version: "2.0",
      name: "Test WebExtension 1 (temporary)",
      browser_specific_settings: {
        gecko: {
          id: ID,
        },
      },
      theme: { images: { theme_frame: "example.png" } },
    },
  });

  checkAddon(ID, addon, {
    version: "2.0",
    name: "Test WebExtension 1 (temporary)",
    isCompatible: true,
    appDisabled: false,
    isActive: true,
    // This is what we're really interested in:
    type: "theme",
    isWebExtension: true,
  });

  await addon.uninstall();

  addon = await promiseAddonByID(ID);
  Assert.equal(addon, null);
});

add_task(skipOnAndroid(), async function test_developer_properties() {
  const name = "developer-name";
  const url = "https://example.org";

  for (const { type, manifestProps, files } of [
    {
      type: "dictionary",
      manifestProps: {
        dictionaries: {
          "en-US": "en-US.dic",
        },
      },
      files: {
        "en-US.dic": "",
        "en-US.aff": "",
      },
    },
    {
      type: "statictheme",
      manifestProps: {
        theme: {
          colors: {
            frame: "#FFF",
            tab_background_text: "#000",
          },
        },
      },
    },
    {
      type: "langpack",
      manifestProps: {
        langpack_id: "und",
        languages: {
          und: {
            chrome_resources: {
              global: "chrome/und/locale/und/global",
            },
            version: "20190326174300",
          },
        },
      },
    },
  ]) {
    const id = `${type}@mozilla.com`;

    info(`== loading add-on with id=${id} ==`);

    let addon = await promiseInstallWebExtension({
      manifest: {
        developer: {
          name,
          url,
        },
        author: "Will be overridden by developer",
        homepage_url: "https://will.be.overridden",
        browser_specific_settings: { gecko: { id } },
        ...manifestProps,
      },
      files,
    });

    checkAddon(id, addon, { creator: name, homepageURL: url });

    await addon.uninstall();
  }
});

add_task(async function test_invalid_homepage_and_developer_urls() {
  const INVALID_URLS = [
    "chrome://browser/content/",
    "data:text/json,...",
    "javascript:;",
    "/",
    "not-an-url",
  ];
  const EXPECTED_ERROR_RE =
    /Access denied for URL|may not load or link to|is not a valid URL/;

  for (let url of INVALID_URLS) {
    // First, we verify `homepage_url`, which has a `url` "format" defined
    // since it exists.
    let normalized = await ExtensionTestUtils.normalizeManifest({
      homepage_url: url,
    });
    ok(
      EXPECTED_ERROR_RE.test(normalized.error),
      `got expected error for ${url}`
    );

    // The `developer.url` now has a "format" but it was a late addition so we
    // are only raising a warning instead of an error.
    ExtensionTestUtils.failOnSchemaWarnings(false);
    normalized = await ExtensionTestUtils.normalizeManifest({
      developer: { url },
    });
    ok(!normalized.error, "expected no error");
    ok(
      // Despites this prop being named `errors`, we are checking the warnings
      // here.
      EXPECTED_ERROR_RE.test(normalized.errors[0]),
      `got expected warning for ${url}`
    );
    ExtensionTestUtils.failOnSchemaWarnings(true);
  }
});

add_task(async function test_valid_homepage_and_developer_urls() {
  let normalized = await ExtensionTestUtils.normalizeManifest({
    developer: { url: "https://example.com" },
  });
  ok(!normalized.error, "expected no error");

  normalized = await ExtensionTestUtils.normalizeManifest({
    homepage_url: "https://example.com",
  });
  ok(!normalized.error, "expected no error");
});
