"use strict";

const { AddonTestUtils } = ChromeUtils.importESModule(
  "resource://testing-common/AddonTestUtils.sys.mjs"
);
const { ExtensionPermissions } = ChromeUtils.importESModule(
  "resource://gre/modules/ExtensionPermissions.sys.mjs"
);
const { Management } = ChromeUtils.importESModule(
  "resource://gre/modules/Extension.sys.mjs"
);

var gManagerWindow;

AddonTestUtils.initMochitest(this);

function get_test_items() {
  var items = {};

  for (let item of gManagerWindow.document.querySelectorAll("addon-card")) {
    items[item.getAttribute("addon-id")] = item;
  }

  return items;
}

function getHtmlElem(selector) {
  return gManagerWindow.document.querySelector(selector);
}

function getPrivateBrowsingBadge(card) {
  return card.querySelector(".addon-badge-private-browsing-allowed");
}

function getPreferencesButtonAtListView(card) {
  return card.querySelector("panel-item[action='preferences']");
}

function getPreferencesButtonAtDetailsView() {
  return getHtmlElem("panel-item[action='preferences']");
}

function isInlineOptionsVisible() {
  // The following button is used to open the inline options browser.
  return !getHtmlElem(".tab-button[name='preferences']").hidden;
}

function getPrivateBrowsingValue() {
  return getHtmlElem("input[type='radio'][name='private-browsing']:checked")
    .value;
}

async function setPrivateBrowsingValue(value, id) {
  let changePromise = new Promise(resolve => {
    const listener = (type, { extensionId, added, removed }) => {
      if (extensionId == id) {
        // Let's make sure we received the right message
        let { permissions } = value == "0" ? removed : added;
        ok(permissions.includes("internal:privateBrowsingAllowed"));
        Management.off("change-permissions", listener);
        resolve();
      }
    };
    Management.on("change-permissions", listener);
  });
  let radio = getHtmlElem(
    `input[type="radio"][name="private-browsing"][value="${value}"]`
  );
  // NOTE: not using EventUtils.synthesizeMouseAtCenter here because it
  // does make this test to fail intermittently in some jobs (e.g. TV jobs)
  radio.click();
  // Let's make sure we wait until the change has peristed in the database
  return changePromise;
}

// Check whether the private browsing inputs are visible in the details view.
function checkIsModifiable(expected) {
  if (expected) {
    is_element_visible(
      getHtmlElem(".addon-detail-row-private-browsing"),
      "Private browsing should be visible"
    );
  } else {
    is_element_hidden(
      getHtmlElem(".addon-detail-row-private-browsing"),
      "Private browsing should be hidden"
    );
  }
  checkHelpRow(".addon-detail-row-private-browsing", expected);
}

// Check whether the details view shows that private browsing is forcibly disallowed.
function checkIsDisallowed(expected) {
  if (expected) {
    is_element_visible(
      getHtmlElem(".addon-detail-row-private-browsing-disallowed"),
      "Private browsing should be disallowed"
    );
  } else {
    is_element_hidden(
      getHtmlElem(".addon-detail-row-private-browsing-disallowed"),
      "Private browsing should not be disallowed"
    );
  }
  checkHelpRow(".addon-detail-row-private-browsing-disallowed", expected);
}

// Check whether the details view shows that private browsing is forcibly allowed.
function checkIsRequired(expected) {
  if (expected) {
    is_element_visible(
      getHtmlElem(".addon-detail-row-private-browsing-required"),
      "Private browsing should be required"
    );
  } else {
    is_element_hidden(
      getHtmlElem(".addon-detail-row-private-browsing-required"),
      "Private browsing should not be required"
    );
  }
  checkHelpRow(".addon-detail-row-private-browsing-required", expected);
}

function checkHelpRow(selector, expected) {
  let helpRow = getHtmlElem(`${selector} + .addon-detail-help-row`);
  if (expected) {
    is_element_visible(helpRow, `Help row should be shown: ${selector}`);
    is_element_visible(helpRow.querySelector("a"), "Expected learn more link");
  } else {
    is_element_hidden(helpRow, `Help row should be hidden: ${selector}`);
  }
}

async function hasPrivateAllowed(id) {
  let perms = await ExtensionPermissions.get(id);
  return perms.permissions.includes("internal:privateBrowsingAllowed");
}

add_task(async function test_badge_and_toggle_incognito() {
  let addons = new Map([
    [
      "@test-default",
      {
        useAddonManager: "temporary",
        manifest: {
          browser_specific_settings: {
            gecko: { id: "@test-default" },
          },
        },
      },
    ],
    [
      "@test-override",
      {
        useAddonManager: "temporary",
        manifest: {
          browser_specific_settings: {
            gecko: { id: "@test-override" },
          },
        },
        incognitoOverride: "spanning",
      },
    ],
    [
      "@test-override-permanent",
      {
        useAddonManager: "permanent",
        manifest: {
          browser_specific_settings: {
            gecko: { id: "@test-override-permanent" },
          },
        },
        incognitoOverride: "spanning",
      },
    ],
    [
      "@test-not-allowed",
      {
        useAddonManager: "temporary",
        manifest: {
          browser_specific_settings: {
            gecko: { id: "@test-not-allowed" },
          },
          incognito: "not_allowed",
        },
      },
    ],
  ]);
  let extensions = [];
  for (let definition of addons.values()) {
    let extension = ExtensionTestUtils.loadExtension(definition);
    extensions.push(extension);
    await extension.startup();
  }

  gManagerWindow = await open_manager("addons://list/extension");
  let items = get_test_items();
  for (let [id, definition] of addons.entries()) {
    ok(items[id], `${id} listed`);
    let badge = getPrivateBrowsingBadge(items[id]);
    if (definition.incognitoOverride == "spanning") {
      is_element_visible(badge, `private browsing badge is visible`);
    } else {
      is_element_hidden(badge, `private browsing badge is hidden`);
    }
  }
  await close_manager(gManagerWindow);

  for (let [id, definition] of addons.entries()) {
    gManagerWindow = await open_manager(
      "addons://detail/" + encodeURIComponent(id)
    );
    ok(true, `==== ${id} detail opened`);
    if (definition.manifest.incognito == "not_allowed") {
      checkIsModifiable(false);
      ok(!(await hasPrivateAllowed(id)), "Private browsing permission not set");
      checkIsDisallowed(true);
    } else {
      // This assumes PERM_CAN_CHANGE_PRIVATEBROWSING_ACCESS, we test other options in a later test in this file.
      checkIsModifiable(true);
      if (definition.incognitoOverride == "spanning") {
        is(getPrivateBrowsingValue(), "1", "Private browsing should be on");
        ok(await hasPrivateAllowed(id), "Private browsing permission set");
        await setPrivateBrowsingValue("0", id);
        is(getPrivateBrowsingValue(), "0", "Private browsing should be off");
        ok(
          !(await hasPrivateAllowed(id)),
          "Private browsing permission removed"
        );
      } else {
        is(getPrivateBrowsingValue(), "0", "Private browsing should be off");
        ok(
          !(await hasPrivateAllowed(id)),
          "Private browsing permission not set"
        );
        await setPrivateBrowsingValue("1", id);
        is(getPrivateBrowsingValue(), "1", "Private browsing should be on");
        ok(await hasPrivateAllowed(id), "Private browsing permission set");
      }
    }
    await close_manager(gManagerWindow);
  }

  for (let extension of extensions) {
    await extension.unload();
  }
});

add_task(async function test_addon_preferences_button() {
  let addons = new Map([
    [
      "test-inline-options@mozilla.com",
      {
        useAddonManager: "temporary",
        manifest: {
          name: "Extension with inline options",
          browser_specific_settings: {
            gecko: { id: "test-inline-options@mozilla.com" },
          },
          options_ui: { page: "options.html", open_in_tab: false },
        },
      },
    ],
    [
      "test-newtab-options@mozilla.com",
      {
        useAddonManager: "temporary",
        manifest: {
          name: "Extension with options page in a new tab",
          browser_specific_settings: {
            gecko: { id: "test-newtab-options@mozilla.com" },
          },
          options_ui: { page: "options.html", open_in_tab: true },
        },
      },
    ],
    [
      "test-not-allowed@mozilla.com",
      {
        useAddonManager: "temporary",
        manifest: {
          name: "Extension not allowed in PB windows",
          incognito: "not_allowed",
          browser_specific_settings: {
            gecko: { id: "test-not-allowed@mozilla.com" },
          },
          options_ui: { page: "options.html", open_in_tab: true },
        },
      },
    ],
  ]);

  async function runTest(openInPrivateWin) {
    const win = await BrowserTestUtils.openNewBrowserWindow({
      private: openInPrivateWin,
    });

    gManagerWindow = await open_manager(
      "addons://list/extension",
      undefined,
      undefined,
      undefined,
      win
    );

    const checkPrefsVisibility = (id, hasInlinePrefs, expectVisible) => {
      if (!hasInlinePrefs) {
        const detailsPrefBtn = getPreferencesButtonAtDetailsView();
        is(
          !detailsPrefBtn.hidden,
          expectVisible,
          `The ${id} prefs button in the addon details has the expected visibility`
        );
      } else {
        is(
          isInlineOptionsVisible(),
          expectVisible,
          `The ${id} inline prefs in the addon details has the expected visibility`
        );
      }
    };

    const setAddonPrivateBrowsingAccess = async (id, allowPrivateBrowsing) => {
      const cardUpdatedPromise = BrowserTestUtils.waitForEvent(
        getHtmlElem("addon-card"),
        "update"
      );
      is(
        getPrivateBrowsingValue(),
        allowPrivateBrowsing ? "0" : "1",
        `Private browsing should be initially ${
          allowPrivateBrowsing ? "off" : "on"
        }`
      );

      // Get the DOM element we want to click on (to allow or disallow the
      // addon on private browsing windows).
      await setPrivateBrowsingValue(allowPrivateBrowsing ? "1" : "0", id);

      info(`Waiting for details view of ${id} to be reloaded`);
      await cardUpdatedPromise;

      is(
        getPrivateBrowsingValue(),
        allowPrivateBrowsing ? "1" : "0",
        `Private browsing should be initially ${
          allowPrivateBrowsing ? "on" : "off"
        }`
      );

      is(
        await hasPrivateAllowed(id),
        allowPrivateBrowsing,
        `Private browsing permission ${
          allowPrivateBrowsing ? "added" : "removed"
        }`
      );
      let badge = getPrivateBrowsingBadge(getHtmlElem("addon-card"));
      is(
        !badge.hidden,
        allowPrivateBrowsing,
        `Expected private browsing badge at ${id}`
      );
    };

    const extensions = [];
    for (const definition of addons.values()) {
      const extension = ExtensionTestUtils.loadExtension(definition);
      extensions.push(extension);
      await extension.startup();
    }

    const items = get_test_items();

    for (const id of addons.keys()) {
      // Check the preferences button in the addon list page.
      is(
        getPreferencesButtonAtListView(items[id]).hidden,
        openInPrivateWin,
        `The ${id} prefs button in the addon list has the expected visibility`
      );
    }

    for (const [id, definition] of addons.entries()) {
      // Check the preferences button or inline frame in the addon
      // details page.
      info(`Opening addon details for ${id}`);
      const hasInlinePrefs = !definition.manifest.options_ui.open_in_tab;
      const onceViewChanged = wait_for_view_load(gManagerWindow, null, true);
      gManagerWindow.loadView(`addons://detail/${encodeURIComponent(id)}`);
      await onceViewChanged;

      checkPrefsVisibility(id, hasInlinePrefs, !openInPrivateWin);

      // While testing in a private window, also check that the preferences
      // are going to be visible when we toggle the PB access for the addon.
      if (openInPrivateWin && definition.manifest.incognito !== "not_allowed") {
        await setAddonPrivateBrowsingAccess(id, true);
        checkPrefsVisibility(id, hasInlinePrefs, true);

        await setAddonPrivateBrowsingAccess(id, false);
        checkPrefsVisibility(id, hasInlinePrefs, false);
      }
    }

    for (const extension of extensions) {
      await extension.unload();
    }

    await close_manager(gManagerWindow);
    await BrowserTestUtils.closeWindow(win);
  }

  // run tests in private and non-private windows.
  await runTest(true);
  await runTest(false);
});

add_task(async function test_addon_postinstall_incognito_hidden_checkbox() {
  await SpecialPowers.pushPrefEnv({
    set: [["extensions.langpacks.signatures.required", false]],
  });

  const TEST_ADDONS = [
    {
      manifest: {
        name: "Extension incognito default opt-in",
        browser_specific_settings: {
          gecko: { id: "ext-incognito-default-opt-in@mozilla.com" },
        },
      },
    },
    {
      manifest: {
        name: "Extension incognito not_allowed",
        browser_specific_settings: {
          gecko: { id: "ext-incognito-not-allowed@mozilla.com" },
        },
        incognito: "not_allowed",
      },
    },
    {
      manifest: {
        name: "Static Theme",
        browser_specific_settings: {
          gecko: { id: "static-theme@mozilla.com" },
        },
        theme: {
          colors: {
            frame: "#FFFFFF",
            tab_background_text: "#000",
          },
        },
      },
    },
    {
      manifest: {
        name: "Dictionary",
        browser_specific_settings: { gecko: { id: "dictionary@mozilla.com" } },
        dictionaries: {
          und: "dictionaries/und.dic",
        },
      },
      files: {
        "dictionaries/und.dic": "",
        "dictionaries/und.aff": "",
      },
    },
    {
      manifest: {
        name: "Langpack",
        browser_specific_settings: { gecko: { id: "langpack@mozilla.com" } },
        langpack_id: "und",
        languages: {
          und: {
            chrome_resources: {
              global: "chrome/und/locale/und/global",
            },
            version: "20190326174300",
          },
        },
      },
    },
  ];

  for (let definition of TEST_ADDONS) {
    let { id } = definition.manifest.browser_specific_settings.gecko;
    info(
      `Testing incognito checkbox visibility on ${id} post install notification`
    );

    const xpi = AddonTestUtils.createTempWebExtensionFile(definition);
    let install = await AddonManager.getInstallForFile(xpi);

    const notificationId = definition.manifest.theme
      ? "theme-installed"
      : "addon-installed";

    await Promise.all([
      waitAppMenuNotificationShown(notificationId, id, true),
      install.install().then(() => {
        Services.obs.notifyObservers(
          {
            addon: install.addon,
            target: gBrowser.selectedBrowser,
          },
          "webextension-install-notify"
        );
      }),
    ]);

    const { addon } = install;
    const { permissions } = addon;
    const canChangePBAccess = Boolean(
      permissions & AddonManager.PERM_CAN_CHANGE_PRIVATEBROWSING_ACCESS
    );

    if (id === "ext-incognito-default-opt-in@mozilla.com") {
      ok(
        canChangePBAccess,
        `${id} should have the PERM_CAN_CHANGE_PRIVATEBROWSING_ACCESS permission`
      );
    } else {
      ok(
        !canChangePBAccess,
        `${id} should not have the PERM_CAN_CHANGE_PRIVATEBROWSING_ACCESS permission`
      );
    }

    // This tests the visibility of various private detail rows.
    gManagerWindow = await open_manager(
      "addons://detail/" + encodeURIComponent(id)
    );
    info(`addon ${id} detail opened`);
    if (addon.type === "extension") {
      checkIsModifiable(canChangePBAccess);
      let required = addon.incognito === "spanning";
      checkIsRequired(!canChangePBAccess && required);
      checkIsDisallowed(!canChangePBAccess && !required);
    } else {
      checkIsModifiable(false);
      checkIsRequired(false);
      checkIsDisallowed(false);
    }
    await close_manager(gManagerWindow);

    await addon.uninstall();
  }

  // It is not possible to create a privileged add-on and install it, so just
  // simulate an installed privileged add-on and check the UI.
  await test_incognito_of_privileged_addons();
});

// Checks that the private browsing flag of privileged add-ons cannot be modified.
async function test_incognito_of_privileged_addons() {
  // In mochitests it is not possible to create and install a privileged add-on
  // or a system add-on, so create a mock provider that simulates privileged
  // add-ons (which lack the PERM_CAN_CHANGE_PRIVATEBROWSING_ACCESS permission).
  let provider = new MockProvider();
  provider.createAddons([
    {
      name: "default incognito",
      id: "default-incognito@mock",
      incognito: "spanning", // This is the default.
      // Anything without the PERM_CAN_CHANGE_PRIVATEBROWSING_ACCESS permission.
      permissions: 0,
    },
    {
      name: "not_allowed incognito",
      id: "not-allowed-incognito@mock",
      incognito: "not_allowed",
      // Anything without the PERM_CAN_CHANGE_PRIVATEBROWSING_ACCESS permission.
      permissions: 0,
    },
  ]);

  gManagerWindow = await open_manager(
    "addons://detail/default-incognito%40mock"
  );
  checkIsModifiable(false);
  checkIsRequired(true);
  checkIsDisallowed(false);
  await close_manager(gManagerWindow);

  gManagerWindow = await open_manager(
    "addons://detail/not-allowed-incognito%40mock"
  );
  checkIsModifiable(false);
  checkIsRequired(false);
  checkIsDisallowed(true);
  await close_manager(gManagerWindow);

  provider.unregister();
}
