"use strict";

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

const { ExtensionScriptingStore } = ChromeUtils.importESModule(
  "resource://gre/modules/ExtensionScriptingStore.sys.mjs"
);
const { TestUtils } = ChromeUtils.importESModule(
  "resource://testing-common/TestUtils.sys.mjs"
);

add_task(async function test_scriptingstore_rkv_recovery_rename() {
  ExtensionScriptingStore._getStoreForTesting()._uninitForTesting();
  const databaseDir = await makeRkvDatabaseDir("extension-store", {
    mockCorrupted: true,
  });
  await ExtensionScriptingStore._getStoreForTesting().lazyInit();
  Assert.ok(
    await IOUtils.exists(PathUtils.join(databaseDir, "data.safe.bin.corrupt")),
    "Expect corrupt file to be found"
  );
});

const makeExtension = ({ manifest: manifestProps, ...otherProps }) => {
  return ExtensionTestUtils.loadExtension({
    manifest: {
      manifest_version: 2,
      permissions: ["scripting"],
      ...manifestProps,
    },
    useAddonManager: "permanent",
    ...otherProps,
  });
};

const assertNumScriptsInStore = async (extension, expectedNum) => {
  Assert.notEqual(extension.id, null, "Expect a non-null extension id");

  // `registerContentScripts`/`updateContentScripts()`/`unregisterContentScripts`
  // call `ExtensionScriptingStore.persistAll()` without awaiting it, which
  // isn't a problem in practice but this becomes a problem in this test given
  // that we should make sure the startup cache is updated before checking it.
  await TestUtils.waitForCondition(async () => {
    let scripts =
      await ExtensionScriptingStore._getStoreForTesting().getByExtensionId(
        extension.id
      );
    return scripts.length === expectedNum;
  }, "wait until the store is updated with the expected number of scripts");

  let scripts =
    await ExtensionScriptingStore._getStoreForTesting().getByExtensionId(
      extension.id
    );
  Assert.equal(
    scripts.length,
    expectedNum,
    `expected ${expectedNum} script in store`
  );
};

const verifyRegisterContentScripts = async manifestVersion => {
  await AddonTestUtils.promiseStartupManager();

  let extension = makeExtension({
    manifest: {
      manifest_version: manifestVersion,
    },
    async background() {
      let scripts = await browser.scripting.getRegisteredContentScripts();

      // Only register the content script if it wasn't registered before. Since
      // there is only one script, we don't check its ID.
      if (!scripts.length) {
        const script = {
          id: "script",
          js: ["script.js"],
          matches: ["http://*/*/file_sample.html"],
          persistAcrossSessions: true,
        };

        await browser.scripting.registerContentScripts([script]);
        browser.test.sendMessage("script-registered");
        return;
      }

      browser.test.assertEq(1, scripts.length, "expected 1 registered script");
      browser.test.sendMessage("script-already-registered");
    },
    files: {
      "script.js": "",
    },
  });

  await extension.startup();
  await extension.awaitMessage("script-registered");
  await assertNumScriptsInStore(extension, 1);

  await AddonTestUtils.promiseRestartManager();
  await assertNumScriptsInStore(extension, 1);

  await extension.awaitStartup();
  await extension.awaitMessage("script-already-registered");

  const extId = extension.id;
  await extension.unload();
  await AddonTestUtils.promiseShutdownManager();
  await assertNumScriptsInStore({ id: extId }, 0);
};

add_task(async function test_registerContentScripts_mv2() {
  await verifyRegisterContentScripts(2);
});

add_task(async function test_registerContentScripts_mv3() {
  await verifyRegisterContentScripts(3);
});

const verifyUpdateContentScripts = async manifestVersion => {
  await AddonTestUtils.promiseStartupManager();

  let extension = makeExtension({
    manifest: {
      manifest_version: manifestVersion,
    },
    async background() {
      let scripts = await browser.scripting.getRegisteredContentScripts();

      // Only register the content script if it wasn't registered before. Since
      // there is only one script, we don't check its ID.
      if (!scripts.length) {
        const script = {
          id: "script",
          js: ["script.js"],
          matches: ["http://*/*/file_sample.html"],
          persistAcrossSessions: true,
        };

        await browser.scripting.registerContentScripts([script]);
        browser.test.sendMessage("script-registered");
        return;
      }

      browser.test.assertEq(1, scripts.length, "expected 1 registered script");
      await browser.scripting.updateContentScripts([
        { id: scripts[0].id, persistAcrossSessions: false },
      ]);
      browser.test.sendMessage("script-updated");
    },
    files: {
      "script.js": "",
    },
  });

  await extension.startup();
  await extension.awaitMessage("script-registered");
  await assertNumScriptsInStore(extension, 1);

  // Simulate a new session.
  await AddonTestUtils.promiseRestartManager();
  await assertNumScriptsInStore(extension, 1);

  await extension.awaitStartup();
  await extension.awaitMessage("script-updated");
  await assertNumScriptsInStore(extension, 0);

  // Simulate another new session.
  await AddonTestUtils.promiseRestartManager();

  await extension.awaitStartup();
  await extension.awaitMessage("script-registered");
  await assertNumScriptsInStore(extension, 1);

  const extId = extension.id;
  await extension.unload();
  await AddonTestUtils.promiseShutdownManager();
  await assertNumScriptsInStore({ id: extId }, 0);
};

add_task(async function test_updateContentScripts() {
  await verifyUpdateContentScripts(2);
});

add_task(async function test_updateContentScripts_mv3() {
  await verifyUpdateContentScripts(3);
});

const verifyUnregisterContentScripts = async manifestVersion => {
  await AddonTestUtils.promiseStartupManager();

  let extension = makeExtension({
    manifest: {
      manifest_version: manifestVersion,
    },
    async background() {
      let scripts = await browser.scripting.getRegisteredContentScripts();

      // Only register the content script if it wasn't registered before. Since
      // there is only one script, we don't check its ID.
      if (!scripts.length) {
        const script = {
          id: "script",
          js: ["script.js"],
          matches: ["http://*/*/file_sample.html"],
          persistAcrossSessions: true,
        };

        await browser.scripting.registerContentScripts([script]);
        browser.test.sendMessage("script-registered");
        return;
      }

      browser.test.assertEq(1, scripts.length, "expected 1 registered script");
      await browser.scripting.unregisterContentScripts();
      browser.test.sendMessage("script-unregistered");
    },
    files: {
      "script.js": "",
    },
  });

  await extension.startup();
  await extension.awaitMessage("script-registered");
  await assertNumScriptsInStore(extension, 1);

  // Simulate a new session.
  await AddonTestUtils.promiseRestartManager();

  // Script should be still persisted...
  await assertNumScriptsInStore(extension, 1);
  await extension.awaitStartup();
  // ...and we should now enter the second branch of the background script.
  await extension.awaitMessage("script-unregistered");
  await assertNumScriptsInStore(extension, 0);

  // Simulate another new session.
  await AddonTestUtils.promiseRestartManager();

  await extension.awaitStartup();
  await extension.awaitMessage("script-registered");
  await assertNumScriptsInStore(extension, 1);

  const extId = extension.id;
  await extension.unload();
  await AddonTestUtils.promiseShutdownManager();
  await assertNumScriptsInStore({ id: extId }, 0);
};

add_task(async function test_unregisterContentScripts() {
  await verifyUnregisterContentScripts(2);
});

add_task(async function test_unregisterContentScripts_mv3() {
  await verifyUnregisterContentScripts(3);
});

add_task(async function test_reload_extension() {
  await AddonTestUtils.promiseStartupManager();

  let extension = makeExtension({
    async background() {
      browser.test.onMessage.addListener(msg => {
        browser.test.assertEq("reload-extension", msg, `expected msg: ${msg}`);
        browser.runtime.reload();
      });

      let scripts = await browser.scripting.getRegisteredContentScripts();

      // Only register the content script if it wasn't registered before. Since
      // there is only one script, we don't check its ID.
      if (!scripts.length) {
        const script = {
          id: "script",
          js: ["script.js"],
          matches: ["http://*/*/file_sample.html"],
          persistAcrossSessions: true,
        };

        await browser.scripting.registerContentScripts([script]);
        browser.test.sendMessage("script-registered");
        return;
      }

      browser.test.assertEq(1, scripts.length, "expected 1 registered script");
      browser.test.sendMessage("script-already-registered");
    },
    files: {
      "script.js": "",
    },
  });

  await extension.startup();
  await extension.awaitMessage("script-registered");
  await assertNumScriptsInStore(extension, 1);

  extension.sendMessage("reload-extension");
  // Wait for extension to restart, to make sure reloads works.
  await AddonTestUtils.promiseWebExtensionStartup(extension.id);
  await extension.awaitMessage("script-already-registered");
  await assertNumScriptsInStore(extension, 1);

  const extId = extension.id;
  await extension.unload();
  await AddonTestUtils.promiseShutdownManager();
  await assertNumScriptsInStore({ id: extId }, 0);
});

add_task(async function test_disable_and_reenable_extension() {
  await AddonTestUtils.promiseStartupManager();

  let extension = makeExtension({
    async background() {
      let scripts = await browser.scripting.getRegisteredContentScripts();

      // Only register the content script if it wasn't registered before. Since
      // there is only one script, we don't check its ID.
      if (!scripts.length) {
        const script = {
          id: "script",
          js: ["script.js"],
          matches: ["http://*/*/file_sample.html"],
          persistAcrossSessions: true,
        };

        await browser.scripting.registerContentScripts([script]);
        browser.test.sendMessage("script-registered");
        return;
      }

      browser.test.assertEq(1, scripts.length, "expected 1 registered script");
      browser.test.sendMessage("script-already-registered");
    },
    files: {
      "script.js": "",
    },
  });

  await extension.startup();
  await extension.awaitMessage("script-registered");
  await assertNumScriptsInStore(extension, 1);

  // Disable...
  await extension.addon.disable();
  // then re-enable the extension.
  await extension.addon.enable();

  await extension.awaitMessage("script-already-registered");
  await assertNumScriptsInStore(extension, 1);

  const extId = extension.id;
  await extension.unload();
  await AddonTestUtils.promiseShutdownManager();
  await assertNumScriptsInStore({ id: extId }, 0);
});

add_task(async function test_updateContentScripts_persistAcrossSessions_true() {
  await AddonTestUtils.promiseStartupManager();

  let extension = makeExtension({
    async background() {
      const script = {
        id: "script",
        js: ["script-1.js"],
        matches: ["http://*/*/file_sample.html"],
        persistAcrossSessions: false,
      };

      const scripts = await browser.scripting.getRegisteredContentScripts();

      browser.test.onMessage.addListener(async msg => {
        switch (msg) {
          case "persist-script":
            await browser.scripting.updateContentScripts([
              { id: script.id, persistAcrossSessions: true },
            ]);
            browser.test.sendMessage(`${msg}-done`);
            break;

          case "add-new-js":
            await browser.scripting.updateContentScripts([
              { id: script.id, js: ["script-1.js", "script-2.js"] },
            ]);
            browser.test.sendMessage(`${msg}-done`);
            break;

          case "verify-script":
            // We expect a single registered script, which is the one declared
            // above but at this point we should have 2 JS files and the
            // `persistAcrossSessions` option set to `true`.
            browser.test.assertEq(
              JSON.stringify([
                {
                  id: script.id,
                  allFrames: false,
                  matches: script.matches,
                  matchOriginAsFallback: false,
                  runAt: "document_idle",
                  world: "ISOLATED",
                  persistAcrossSessions: true,
                  cssOrigin: "author",
                  js: ["script-1.js", "script-2.js"],
                },
              ]),
              JSON.stringify(scripts),
              "expected scripts"
            );
            browser.test.sendMessage(`${msg}-done`);
            break;

          default:
            browser.test.fail(`unexpected message: ${msg}`);
        }
      });

      // Only register the content script if it wasn't registered before. Since
      // there is only one script, we don't check its ID.
      if (!scripts.length) {
        await browser.scripting.registerContentScripts([script]);
        browser.test.sendMessage("script-registered");
      } else {
        browser.test.sendMessage("script-already-registered");
      }
    },
    files: {
      "script-1.js": "",
      "script-2.js": "",
    },
  });

  await extension.startup();
  await extension.awaitMessage("script-registered");
  await assertNumScriptsInStore(extension, 0);

  // Simulate a new session.
  await AddonTestUtils.promiseRestartManager();
  await assertNumScriptsInStore(extension, 0);

  // We expect the script to be registered again because it isn't persisted.
  await extension.awaitStartup();
  await extension.awaitMessage("script-registered");
  await assertNumScriptsInStore(extension, 0);

  // We now tell the background script to update the script to persist it
  // across sessions.
  extension.sendMessage("persist-script");
  await extension.awaitMessage("persist-script-done");

  // Simulate another new session. We expect the content script to be already
  // registered since it was persisted in the previous (simulated) session.
  await AddonTestUtils.promiseRestartManager();
  await assertNumScriptsInStore(extension, 1);

  await extension.awaitStartup();
  await extension.awaitMessage("script-already-registered");
  await assertNumScriptsInStore(extension, 1);

  // We tell the background script to update the content script with a new JS
  // file and we don't change the `persistAcrossSessions` option.
  extension.sendMessage("add-new-js");
  await extension.awaitMessage("add-new-js-done");

  // Simulate another new session. We expect the content script to have 2 JS
  // files and to be registered since it was persisted in the previous
  // (simulated) session and we didn't update the option.
  await AddonTestUtils.promiseRestartManager();
  await assertNumScriptsInStore(extension, 1);

  await extension.awaitStartup();
  await extension.awaitMessage("script-already-registered");
  await assertNumScriptsInStore(extension, 1);

  // Let's verify that the script fetched by the background script is the one
  // we expect at this point: it should have two JS files.
  extension.sendMessage("verify-script");
  await extension.awaitMessage("verify-script-done");

  const extId = extension.id;
  await extension.unload();
  await AddonTestUtils.promiseShutdownManager();
  await assertNumScriptsInStore({ id: extId }, 0);
});

add_task(async function test_multiple_extensions_and_scripts() {
  await AddonTestUtils.promiseStartupManager();

  let extension1 = makeExtension({
    async background() {
      let scripts = await browser.scripting.getRegisteredContentScripts();

      if (!scripts.length) {
        await browser.scripting.registerContentScripts([
          {
            id: "0",
            js: ["script-1.js"],
            matches: ["http://*/*/file_sample.html"],
            // We should persist this script by default.
          },
          {
            id: "/",
            js: ["script-2.js"],
            matches: ["http://*/*/file_sample.html"],
            persistAcrossSessions: true,
          },
          {
            id: "3",
            js: ["script-3.js"],
            matches: ["http://*/*/file_sample.html"],
            persistAcrossSessions: false,
          },
        ]);
        browser.test.sendMessage("scripts-registered");
        return;
      }

      browser.test.assertEq(2, scripts.length, "expected 2 registered scripts");
      browser.test.sendMessage("scripts-already-registered");
    },
    files: {
      "script-1.js": "",
      "script-2.js": "",
      "script-3.js": "",
    },
  });

  let extension2 = makeExtension({
    async background() {
      let scripts = await browser.scripting.getRegisteredContentScripts();

      if (!scripts.length) {
        await browser.scripting.registerContentScripts([
          {
            id: "1",
            js: ["script-1.js"],
            matches: ["http://*/*/file_sample.html"],
            // We should persist this script by default.
          },
          {
            id: "2",
            js: ["script-2.js"],
            matches: ["http://*/*/file_sample.html"],
            persistAcrossSessions: false,
          },
          {
            id: "\uFFFD 🍕 Boö",
            js: ["script-3.js"],
            matches: ["http://*/*/file_sample.html"],
            persistAcrossSessions: true,
          },
        ]);
        browser.test.sendMessage("scripts-registered");
        return;
      }

      browser.test.assertEq(2, scripts.length, "expected 2 registered scripts");
      browser.test.assertEq(
        JSON.stringify(["script-1.js"]),
        JSON.stringify(scripts[0].js),
        "expected a single 'script-1.js' js file"
      );
      browser.test.assertEq(
        "\uFFFD 🍕 Boö",
        scripts[1].id,
        "expected correct ID"
      );
      browser.test.sendMessage("scripts-already-registered");
    },
    files: {
      "script-1.js": "",
      "script-2.js": "",
      "script-3.js": "",
    },
  });

  await Promise.all([extension1.startup(), extension2.startup()]);

  await Promise.all([
    extension1.awaitMessage("scripts-registered"),
    extension2.awaitMessage("scripts-registered"),
  ]);
  await assertNumScriptsInStore(extension1, 2);
  await assertNumScriptsInStore(extension2, 2);

  await AddonTestUtils.promiseRestartManager();
  await assertNumScriptsInStore(extension1, 2);
  await assertNumScriptsInStore(extension2, 2);

  await Promise.all([extension1.awaitStartup(), extension2.awaitStartup()]);
  await Promise.all([
    extension1.awaitMessage("scripts-already-registered"),
    extension2.awaitMessage("scripts-already-registered"),
  ]);

  const ext1Id = extension1.id;
  const ext2Id = extension2.id;
  await Promise.all([extension1.unload(), extension2.unload()]);
  await AddonTestUtils.promiseShutdownManager();
  await assertNumScriptsInStore({ id: ext1Id }, 0);
  await assertNumScriptsInStore({ id: ext2Id }, 0);
});

add_task(async function test_persisted_scripts_cleared_on_addon_updates() {
  await AddonTestUtils.promiseStartupManager();

  function background() {
    browser.test.onMessage.addListener(async (msg, ...args) => {
      switch (msg) {
        case "registerContentScripts":
          await browser.scripting.registerContentScripts(...args);
          break;
        case "unregisterContentScripts":
          await browser.scripting.unregisterContentScripts(...args);
          break;
        default:
          browser.test.fail(`Unexpected test message: ${msg}`);
      }
      browser.test.sendMessage(`${msg}:done`);
    });
  }

  async function registerContentScript(ext, scriptFileName) {
    ext.sendMessage("registerContentScripts", [
      {
        id: scriptFileName,
        js: [scriptFileName],
        matches: ["http://*/*/file_sample.html"],
        persistAcrossSessions: true,
      },
    ]);
    await ext.awaitMessage("registerContentScripts:done");
  }

  let extension1Data = {
    manifest: {
      manifest_version: 2,
      permissions: ["scripting"],
      version: "1.0",
      browser_specific_settings: {
        // Set an explicit extension id so that extension.upgrade
        // will trigger the extension to be started with the expected
        // "ADDON_UPGRADE" / "ADDON_DOWNGRADE" extension.startupReason.
        gecko: { id: "extension1@mochi.test" },
      },
    },
    useAddonManager: "permanent",
    background,
    files: {
      "script-1.js": "",
    },
  };

  let extension1 = ExtensionTestUtils.loadExtension(extension1Data);

  let extension2 = ExtensionTestUtils.loadExtension({
    manifest: {
      manifest_version: 2,
      permissions: ["scripting"],
      browser_specific_settings: {
        gecko: { id: "extension2@mochi.test" },
      },
    },
    useAddonManager: "permanent",
    background,
    files: {
      "script-2.js": "",
    },
  });

  await extension1.startup();
  await assertNumScriptsInStore(extension1, 0);
  await assertIsPersistentScriptsCachedFlag(extension1, false);

  await extension2.startup();
  await assertNumScriptsInStore(extension2, 0);
  await assertIsPersistentScriptsCachedFlag(extension2, false);

  await registerContentScript(extension1, "script-1.js");
  await assertNumScriptsInStore(extension1, 1);
  await assertIsPersistentScriptsCachedFlag(extension1, true);

  await registerContentScript(extension2, "script-2.js");
  await assertNumScriptsInStore(extension2, 1);
  await assertIsPersistentScriptsCachedFlag(extension2, true);

  info("Verify that scripts are still registered on a browser startup");
  await AddonTestUtils.promiseRestartManager();
  await extension1.awaitStartup();
  await extension2.awaitStartup();
  equal(
    extension1.extension.startupReason,
    "APP_STARTUP",
    "Got the expected startupReason on AOM restart"
  );

  await assertNumScriptsInStore(extension1, 1);
  await assertIsPersistentScriptsCachedFlag(extension1, true);
  await assertNumScriptsInStore(extension2, 1);
  await assertIsPersistentScriptsCachedFlag(extension2, true);

  async function testOnAddonUpdates(
    extensionUpdateData,
    expectedStartupReason
  ) {
    await extension1.upgrade(extensionUpdateData);
    equal(
      extension1.extension.startupReason,
      expectedStartupReason,
      "Got the expected startupReason on upgrade"
    );

    await assertNumScriptsInStore(extension1, 0);
    await assertIsPersistentScriptsCachedFlag(extension1, false);
    await assertNumScriptsInStore(extension2, 1);
    await assertIsPersistentScriptsCachedFlag(extension2, true);
  }

  info("Verify that scripts are cleared on upgrade");
  await testOnAddonUpdates(
    {
      ...extension1Data,
      manifest: {
        ...extension1Data.manifest,
        version: "2.0",
      },
    },
    "ADDON_UPGRADE"
  );

  await registerContentScript(extension1, "script-1.js");
  await assertNumScriptsInStore(extension1, 1);

  info("Verify that scripts are cleared on downgrade");
  await testOnAddonUpdates(extension1Data, "ADDON_DOWNGRADE");

  await registerContentScript(extension1, "script-1.js");
  await assertNumScriptsInStore(extension1, 1);

  info("Verify that scripts are cleared on upgrade to same version");
  await testOnAddonUpdates(extension1Data, "ADDON_UPGRADE");

  const ext1Id = extension1.id;
  const ext1Version = extension1.version;
  const ext2Id = extension2.id;
  const ext2Version = extension2.version;
  await extension1.unload();
  await extension2.unload();

  await assertNumScriptsInStore({ id: ext1Id }, 0);
  await assertIsPersistentScriptsCachedFlag(
    { id: ext1Id, version: ext1Version },
    undefined
  );
  await assertNumScriptsInStore({ id: ext2Id }, 0);
  await assertIsPersistentScriptsCachedFlag(
    { id: ext2Id, version: ext2Version },
    undefined
  );

  info("Verify stale persisted scripts cleared on re-install");
  // Inject a stale persisted script into the store.

  await ExtensionScriptingStore._getStoreForTesting().writeMany(ext1Id, [
    {
      id: "script-1.js",
      allFrames: false,
      matches: ["http://*/*/file_sample.html"],
      matchOriginAsFallback: false,
      runAt: "document_idle",
      world: "ISOLATED",
      persistAcrossSessions: true,
      js: ["script-1.js"],
    },
  ]);
  await assertNumScriptsInStore({ id: ext1Id }, 1);
  const extension1Reinstalled =
    ExtensionTestUtils.loadExtension(extension1Data);
  await extension1Reinstalled.startup();
  equal(
    extension1Reinstalled.extension.startupReason,
    "ADDON_INSTALL",
    "Got the expected startupReason on re-install"
  );
  await assertNumScriptsInStore(extension1Reinstalled, 0);
  await assertIsPersistentScriptsCachedFlag(extension1Reinstalled, false);
  await extension1Reinstalled.unload();
  await assertNumScriptsInStore({ id: ext1Id }, 0);
  await assertIsPersistentScriptsCachedFlag(
    { id: ext1Id, version: ext1Version },
    undefined
  );

  await AddonTestUtils.promiseShutdownManager();
});

// Regression test for bug 2058719: A race condition was possible where
// policy.contentScripts in the parent missed entries.
add_task(async function content_scripts_persist_across_restart_race() {
  await AddonTestUtils.promiseStartupManager();

  function loadExt() {
    return makeExtension({
      manifest: { manifest_version: 3 },
      background() {
        browser.runtime.onInstalled.addListener(async () => {
          await browser.scripting.registerContentScripts([
            { id: "cs", matches: ["<all_urls>"], js: ["cs.js"] },
          ]);
          browser.test.notifyPass();
        });
      },
    });
  }

  let extA = loadExt();
  let extB = loadExt();
  await Promise.all([extA.startup(), extA.awaitFinish()]);
  await Promise.all([extB.startup(), extB.awaitFinish()]);

  await AddonTestUtils.promiseShutdownManager();
  await AddonTestUtils.promiseStartupManager();
  await Promise.all([extA.awaitStartup(), extB.awaitStartup()]);

  Assert.equal(extA.extension.policy.contentScripts.length, 1, "A has one cs");
  Assert.equal(extB.extension.policy.contentScripts.length, 1, "B has one cs");

  await Promise.all([extA.unload(), extB.unload()]);
  await AddonTestUtils.promiseShutdownManager();
});
