"use strict";

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

AddonTestUtils.init(this);
AddonTestUtils.overrideCertDB();
AddonTestUtils.createAppInfo(
  "xpcshell@tests.mozilla.org",
  "XPCShell",
  "1",
  "42"
);

// We intercept the "webextension-optional-permission-prompt" notification in
// optionalPermissionsPromptHandler.
//
// On desktop, this works because this is a xpcshell test environment where the
// usual browser UI is not loaded.
// On Android, ExtensionPromptObserver in GeckoViewWebExtension.sys.mjs handles
// this but due to this being a xpcshell test without native part, the handler
// rejects async with "No listener for GeckoView:WebExtension:OptionalPrompt".
// The optionalPermissionsPromptHandler is synchronous and will therefore
// always beat the ExtensionPromptObserver if any. Ignore the error:
PromiseTestUtils.allowMatchingRejectionsGlobally(
  /No listener for GeckoView:WebExtension:OptionalPrompt/
);

add_setup(async () => {
  // Bug 1646182: Force ExtensionPermissions to run in rkv mode, the legacy
  // storage mode will run in xpcshell-legacy-ep.toml
  await ExtensionPermissions._uninit();

  optionalPermissionsPromptHandler.init();

  await AddonTestUtils.promiseStartupManager();
  AddonTestUtils.usePrivilegedSignatures = false;
});

// Test that an optional-only permission in the "permissions" array is not
// accepted. Test in MV2 and MV3 to maximize coverage across all manifest
// versions, since the implementation has subtle differences.
async function test_optional_only_permission_in_permissions(manifest_version) {
  let extension = ExtensionTestUtils.loadExtension({
    manifest: {
      manifest_version,
      permissions: ["trialML", "webNavigation"],
    },
    background() {
      browser.test.onMessage.addListener(async msg => {
        if (msg === "check_permissions") {
          browser.test.assertEq(
            await browser.permissions.contains({ permissions: ["trialML"] }),
            false,
            "Optional-only permission not granted when in permissions[]."
          );
          browser.test.assertTrue(
            browser.webNavigation,
            "Other permission unaffected by refusal of optional-only permission"
          );
          browser.test.sendMessage("check_permissions:done");
          return;
        }
        if (msg === "request") {
          await browser.test.assertRejects(
            browser.permissions.request({ permissions: ["trialML"] }),
            "Cannot request permission trialML since it was not declared in optional_permissions",
            "Cannot request permission not listed in optional_permissions"
          );
          browser.test.sendMessage("request:done");
          return;
        }
        browser.test.fail(`Unexpected message: ${msg}`);
      });
    },
  });

  let { messages } = await promiseConsoleOutput(async () => {
    ExtensionTestUtils.failOnSchemaWarnings(false);
    await extension.startup();
    ExtensionTestUtils.failOnSchemaWarnings(true);
  });

  extension.sendMessage("check_permissions");
  await extension.awaitMessage("check_permissions:done");

  await withHandlingUserInput(extension, async () => {
    extension.sendMessage("request");
    await extension.awaitMessage("request:done");
    ok(!optionalPermissionsPromptHandler.sawPrompt, "Should not be prompted");
  });

  Assert.ok(
    !extension.extension.hasPermission("trialML"),
    "extension.hasPermission() of optional-only permission is false"
  );

  await extension.unload();

  // Note: the permission being rejected at parse time should be sufficient to
  // prevent the permission from appearing in extension UI. For additional test
  // coverage, the test_userScripts_cannot_be_install_time_permission test in
  // browser_permission_prompt_userScripts.js confirms that the "userScripts"
  // OptionalOnlyPermission is really hidden from install prompts.
  AddonTestUtils.checkMessages(messages, {
    expected: [
      {
        message:
          /Reading manifest: Warning processing permissions: Error processing permissions.0: Value "trialML" must either:/,
      },
    ],
  });
}

add_task(async function test_optional_only_permission_in_permissions_mv2() {
  await test_optional_only_permission_in_permissions(2);
});

add_task(async function test_optional_only_permission_in_permissions_mv3() {
  await test_optional_only_permission_in_permissions(3);
});

// Test that optional-only permissions can be in the "optional_permissions"
// array in the manifest, and that it can be granted and revoked as usual.
async function test_optional_only_in_optional_permissions(manifest_version) {
  let extension = ExtensionTestUtils.loadExtension({
    manifest: {
      manifest_version,
      optional_permissions: ["trialML"],
    },
    background() {
      browser.runtime.onInstalled.addListener(async () => {
        browser.test.assertEq(
          await browser.permissions.contains({ permissions: ["trialML"] }),
          false,
          "Optional-only permission not granted at first."
        );
        browser.test.sendMessage("installed");
      });
      browser.test.onMessage.addListener(async msg => {
        browser.test.assertEq("request", msg);
        let granted = await browser.permissions.request({
          permissions: ["trialML"],
        });
        browser.test.sendMessage("request:result", granted);
      });
    },
  });

  await extension.startup();

  await extension.awaitMessage("installed");

  info("Testing permissions.request() and cancel");
  optionalPermissionsPromptHandler.acceptPrompt = false;
  await withHandlingUserInput(extension, async () => {
    extension.sendMessage("request");
    let granted = await extension.awaitMessage("request:result");
    ok(optionalPermissionsPromptHandler.sawPrompt, "Got prompt");
    equal(granted, false, "Not granted when canceled");
  });

  info("Testing permissions.request() and accept");
  optionalPermissionsPromptHandler.acceptPrompt = true;
  await withHandlingUserInput(extension, async () => {
    extension.sendMessage("request");
    let granted = await extension.awaitMessage("request:result");
    ok(optionalPermissionsPromptHandler.sawPrompt, "Got prompt");
    equal(granted, true, "Granted when approved");
  });

  await extension.unload();
}

add_task(async function test_optional_only_in_optional_permissions_mv2() {
  await test_optional_only_in_optional_permissions(2);
});

add_task(async function test_optional_only_in_optional_permissions_mv3() {
  await test_optional_only_in_optional_permissions(3);
});

// By design, when an optional-only permission is specified, it cannot be
// requested together with other permissions. This test verifies that behavior.
add_task(
  {
    pref_set: [
      // Here we do not care about the prompt, only about the validation.
      ["extensions.webextOptionalPermissionPrompts", false],
      [
        // This pref controls the Cu.isInAutomation flag that is needed to use
        // browser.test.withHandlingUserInput in xpcshell tests (bug 1598804):
        "security.turn_off_all_security_so_that_viruses_can_take_over_this_computer",
        true,
      ],
    ],
  },
  async function at_most_one_optional_only_permission_in_request() {
    let extension = ExtensionTestUtils.loadExtension({
      manifest: {
        manifest_version: 3,
        optional_permissions: [
          // Note: cookies permission does not have a permission warning.
          "cookies",
          // Optional-only permission:
          "trialML",
          // Optional-only permission:
          "userScripts",
          // Regular permission with warning:
          "webNavigation",
        ],
        host_permissions: [
          // Host permission with warning:
          "https://example.com/*",
        ],
      },
      async background() {
        const ERROR_ONLY_ONE_PERM_ALLOWED =
          "Cannot request permission trialML with another permission";

        async function testPermissionsRequest(permissions) {
          return new Promise(resolve => {
            browser.test.withHandlingUserInput(() =>
              resolve(browser.permissions.request(permissions))
            );
          });
        }

        await browser.test.assertRejects(
          testPermissionsRequest({ permissions: ["webNavigation", "trialML"] }),
          ERROR_ONLY_ONE_PERM_ALLOWED,
          "Should reject regular permission + optional-only permission"
        );

        await browser.test.assertRejects(
          testPermissionsRequest({ permissions: ["trialML", "cookies"] }),
          ERROR_ONLY_ONE_PERM_ALLOWED,
          "Should reject optional-only permission + permission without warning"
        );

        await browser.test.assertRejects(
          testPermissionsRequest({ permissions: ["trialML", "webNavigation"] }),
          ERROR_ONLY_ONE_PERM_ALLOWED,
          "Should reject optional-only permissions + regular permission"
        );

        await browser.test.assertRejects(
          testPermissionsRequest({ permissions: ["trialML", "userScripts"] }),
          ERROR_ONLY_ONE_PERM_ALLOWED,
          "Should reject optional-only permissions (trialML, userScripts)"
        );

        // The UI logic (isUserScriptsRequest in ExtensionsUI.sys.mjs) assumes
        // that "userScripts" is the only permission if present in a permission
        // request. That is the case because "userScripts" is an
        // OptionalOnlyPermission, which the following check confirms:
        await browser.test.assertRejects(
          testPermissionsRequest({ permissions: ["userScripts", "trialML"] }),
          // Note: this is comparable to the ["trialML", "userScripts"] test
          // above, but the error message differs because the message includes
          // the first optional-only permission, "userScripts" in this case.
          "Cannot request permission userScripts with another permission",
          "Should reject optional-only permissions (userScripts, trialML)"
        );

        await browser.test.assertRejects(
          testPermissionsRequest({
            permissions: ["trialML"],
            origins: ["https://example.com/*"],
          }),
          ERROR_ONLY_ONE_PERM_ALLOWED,
          "Should reject optional-only permission + host permission"
        );

        await browser.test.assertRejects(
          // In theory we could detect that the same permission is listed twice
          // and recognize that it will only generate one permission warning.
          // Our implementation does not deduplicate, and there is no reason
          // for supporting it, so we just reject it.
          testPermissionsRequest({
            permissions: ["trialML", "trialML"],
            origins: ["https://example.com/*"],
          }),
          ERROR_ONLY_ONE_PERM_ALLOWED,
          "Should reject optional-only permission if it is listed twice"
        );

        browser.test.sendMessage("done");
      },
    });

    await extension.startup();
    await extension.awaitMessage("done");
    await extension.unload();
  }
);

add_task(
  {
    pref_set: [
      // Here we do not care about the prompt, only about the validation.
      ["extensions.webextOptionalPermissionPrompts", false],
      [
        // This pref controls the Cu.isInAutomation flag that is needed to use
        // browser.test.withHandlingUserInput in xpcshell tests (bug 1598804):
        "security.turn_off_all_security_so_that_viruses_can_take_over_this_computer",
        true,
      ],
      ["extensions.dataCollectionPermissions.enabled", true],
    ],
  },
  async function at_most_one_optional_only_permission_in_request_with_data_collection() {
    let extension = ExtensionTestUtils.loadExtension({
      manifest: {
        manifest_version: 3,
        optional_permissions: [
          // Optional-only permission:
          "trialML",
          // Optional-only permission:
          "userScripts",
        ],
        browser_specific_settings: {
          gecko: {
            data_collection_permissions: {
              optional: ["technicalAndInteraction"],
            },
          },
        },
      },
      async background() {
        async function testPermissionsRequest(permissions) {
          return new Promise(resolve => {
            browser.test.withHandlingUserInput(() =>
              resolve(browser.permissions.request(permissions))
            );
          });
        }

        await browser.test.assertRejects(
          testPermissionsRequest({
            permissions: ["trialML"],
            data_collection: ["technicalAndInteraction"],
          }),
          "Cannot request permission trialML with another permission",
          "Should reject data collection permission + optional-only permission"
        );

        await browser.test.assertRejects(
          testPermissionsRequest({
            permissions: ["userScripts"],
            data_collection: ["technicalAndInteraction"],
          }),
          "Cannot request permission userScripts with another permission",
          "Should reject data collection permission + optional-only permission"
        );

        browser.test.sendMessage("done");
      },
    });

    await extension.startup();
    await extension.awaitMessage("done");
    await extension.unload();
  }
);
