"use strict";

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

const server = createHttpServer({
  hosts: ["example.com", "anotherdomain.com"],
});
server.registerPathHandler("/dummy", (request, response) => {
  response.setStatusLine(request.httpVersion, 200, "OK");
  response.setHeader("Content-Type", "text/html", false);
  response.write("test_ext_cache_api.js");
});

add_setup(() => {
  // NOTE: Services.io.offline shouldn't be set to offline,
  // otherwise we would hit an unexpected behavior when
  // the extension worker tries to fetch from an
  // http url or cache an http url response, see Bug 1845317.
  Assert.ok(
    !Services.io.offline,
    "Services.io.offline should not be set to true while running this test"
  );
});

add_task(async function test_cache_api_http_resource_allowed() {
  async function background() {
    try {
      const BASE_URL = `http://example.com/dummy`;

      const cache = await window.caches.open("test-cache-api");
      browser.test.assertTrue(
        await window.caches.has("test-cache-api"),
        "CacheStorage.has should resolve to true"
      );

      // Test that adding and requesting cached http urls
      // works as well.
      await cache.add(BASE_URL);
      browser.test.assertEq(
        "test_ext_cache_api.js",
        await cache.match(BASE_URL).then(res => res?.text()),
        "Got the expected content from the cached http url"
      );

      // Test that the http urls that the cache API is allowed
      // to fetch and cache are limited by the host permissions
      // associated to the extensions (same as when the extension
      // for fetch from those urls using fetch or XHR).
      await browser.test.assertRejects(
        cache.add(`http://anotherdomain.com/dummy`),
        "NetworkError when attempting to fetch resource.",
        "Got the expected rejection of requesting an http not allowed by host permissions"
      );

      // Test that deleting the cache storage works as expected.
      browser.test.assertTrue(
        await window.caches.delete("test-cache-api"),
        "Cache deleted successfully"
      );
      browser.test.assertTrue(
        !(await window.caches.has("test-cache-api")),
        "CacheStorage.has should resolve to false"
      );
    } catch (err) {
      browser.test.fail(`Unexpected error on using Cache API: ${err}`);
      throw err;
    } finally {
      browser.test.sendMessage("test-cache-api-allowed");
    }
  }

  // Verify that Cache API support for http urls is available
  // regardless of extensions.backgroundServiceWorker.enabled pref.
  const extension = ExtensionTestUtils.loadExtension({
    manifest: { permissions: ["http://example.com/*"] },
    background,
  });

  await extension.startup();

  await extension.awaitMessage("test-cache-api-allowed");
  await extension.unload();
});

// This test is similar to `test_cache_api_http_resource_allowed` but it does
// exercise the Cache API from a moz-extension shared worker.
// We expect the cache API calls to be successfull when it is being used to
// cache an HTTP url that is allowed for the extensions based on its host
// permission, but to fail if the extension doesn't have the required host
// permission to fetch data from that url.
add_task(async function test_cache_api_from_ext_shared_worker() {
  if (!WebExtensionPolicy.useRemoteWebExtensions) {
    // Ensure RemoteWorkerService has been initialized in the main
    // process.
    Services.obs.notifyObservers(null, "profile-after-change");
  }

  const background = async function () {
    const BASE_URL_OK = `http://example.com/dummy`;
    const BASE_URL_KO = `http://anotherdomain.com/dummy`;
    const worker = new SharedWorker("worker.js");
    const { data: resultOK } = await new Promise(resolve => {
      worker.port.onmessage = resolve;
      worker.port.postMessage(["worker-cacheapi-test-allowed", BASE_URL_OK]);
    });
    browser.test.assertDeepEq(
      ["worker-cacheapi-test-allowed:result", { success: true }],
      resultOK,
      "Got success result from extension worker for allowed host url"
    );
    const { data: resultKO } = await new Promise(resolve => {
      worker.port.onmessage = resolve;
      worker.port.postMessage(["worker-cacheapi-test-disallowed", BASE_URL_KO]);
    });
    browser.test.assertDeepEq(
      [
        "worker-cacheapi-test-disallowed:result",
        { error: "NetworkError when attempting to fetch resource." },
      ],
      resultKO,
      "Got result from extension worker for disallowed host url"
    );

    browser.test.assertTrue(
      await window.caches.has("test-cache-api"),
      "CacheStorage.has should resolve to true"
    );
    const cache = await window.caches.open("test-cache-api");
    browser.test.assertEq(
      "test_ext_cache_api.js",
      await cache.match(BASE_URL_OK).then(res => res?.text()),
      "Got the expected content from the cached http url"
    );
    browser.test.assertEq(
      true,
      await cache.match(BASE_URL_KO).then(res => res == undefined),
      "Got no match for the http url that isn't allowed by host permissions"
    );

    browser.test.sendMessage("test-cacheapi-sharedworker:done");
  };

  const extension = ExtensionTestUtils.loadExtension({
    background,
    manifest: { permissions: ["http://example.com/*"] },
    files: {
      "worker.js": function () {
        self.onconnect = evt => {
          const port = evt.ports[0];
          port.onmessage = async evt => {
            let result = {};
            let message;
            try {
              const [msg, BASE_URL] = evt.data;
              message = msg;
              const cache = await self.caches.open("test-cache-api");
              await cache.add(BASE_URL);
              result.success = true;
            } catch (err) {
              result.error = err.message;
              throw err;
            } finally {
              port.postMessage([`${message}:result`, result]);
            }
          };
        };
      },
    },
  });

  await extension.startup();
  await extension.awaitMessage("test-cacheapi-sharedworker:done");
  await extension.unload();
});

add_task(async function test_cache_storage_evicted_on_addon_uninstalled() {
  async function background() {
    try {
      const BASE_URL = `http://example.com/dummy`;

      const cache = await window.caches.open("test-cache-api");
      browser.test.assertTrue(
        await window.caches.has("test-cache-api"),
        "CacheStorage.has should resolve to true"
      );

      // Test that adding and requesting cached http urls
      // works as well.
      await cache.add(BASE_URL);
      browser.test.assertEq(
        "test_ext_cache_api.js",
        await cache.match(BASE_URL).then(res => res?.text()),
        "Got the expected content from the cached http url"
      );
    } catch (err) {
      browser.test.fail(`Unexpected error on using Cache API: ${err}`);
      throw err;
    } finally {
      browser.test.sendMessage("cache-storage-created");
    }
  }

  const extension = ExtensionTestUtils.loadExtension({
    manifest: { permissions: ["http://example.com/*"] },
    background,
    // Necessary to be sure the expected extension stored data cleanup callback
    // will be called when the extension is uninstalled from an AddonManager
    // perspective.
    useAddonManager: "temporary",
  });

  await AddonTestUtils.promiseStartupManager();
  await extension.startup();
  await extension.awaitMessage("cache-storage-created");

  const extURL = `moz-extension://${extension.extension.uuid}`;
  const extPrincipal = Services.scriptSecurityManager.createContentPrincipal(
    Services.io.newURI(extURL),
    {}
  );
  let extCacheStorage = new CacheStorage("content", extPrincipal);

  ok(
    await extCacheStorage.has("test-cache-api"),
    "Got the expected extension cache storage"
  );

  await extension.unload();

  ok(
    !(await extCacheStorage.has("test-cache-api")),
    "The extension cache storage data should have been evicted on addon uninstall"
  );
});

add_task(
  {
    // Pref used to allow to use the Cache WebAPI related to a page loaded from http
    // (otherwise Gecko will throw a SecurityError when trying to access the webpage
    // cache storage from the content script, unless the webpage is loaded from https).
    pref_set: [["dom.caches.testing.enabled", true]],
  },
  async function test_cache_put_from_contentscript() {
    const extension = ExtensionTestUtils.loadExtension({
      manifest: {
        content_scripts: [
          {
            matches: ["http://example.com/*"],
            js: ["content.js"],
          },
        ],
      },
      files: {
        "content.js": async function () {
          const cache = await caches.open("test-cachestorage");
          const request = "http://example.com";
          const response = await fetch(request);
          await cache.put(request, response).catch(err => {
            browser.test.sendMessage("cache-put-error", {
              name: err.name,
              message: err.message,
            });
          });
        },
      },
    });

    await extension.startup();

    const page = await ExtensionTestUtils.loadContentPage("http://example.com");
    const actualError = await extension.awaitMessage("cache-put-error");
    equal(
      actualError.name,
      "SecurityError",
      "Got a security error from cache.put call as expected"
    );
    ok(
      /Disallowed on WebExtension ContentScript Request/.test(
        actualError.message
      ),
      `Got the expected error message: ${actualError.message}`
    );

    await page.close();
    await extension.unload();
  }
);
