/* -*- Mode: indent-tabs-mode: nil; js-indent-level: 2 -*- */
/* vim: set sts=2 sw=2 et tw=80: */

"use strict";

Services.prefs.setBoolPref("extensions.blocklist.enabled", false);

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

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

const LEAVE_UUID_PREF = "extensions.webextensions.keepUuidOnUninstall";

const BASE64_R_PIXEL =
  "iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAYAAAAfFcSJAAAADUlEQVQIW2P4z8DwHwAFAAH/F1FwBgAAAABJRU5ErkJggg==";
const BASE64_G_PIXEL =
  "iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAYAAAAfFcSJAAAADUlEQVQIW2Ng+M/wHwAEAQH/7yMK/gAAAABJRU5ErkJggg==";
const BASE64_B_PIXEL =
  "iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAYAAAAfFcSJAAAADUlEQVQIW2NgYPj/HwADAgH/eL9GtQAAAABJRU5ErkJggg==";

const toArrayBuffer = b64data =>
  Uint8Array.from(atob(b64data), c => c.charCodeAt(0));
const IMAGE_RED = toArrayBuffer(BASE64_R_PIXEL).buffer;
const IMAGE_GREEN = toArrayBuffer(BASE64_G_PIXEL).buffer;
const IMAGE_BLUE = toArrayBuffer(BASE64_B_PIXEL).buffer;

const RGB_RED = "rgb(255, 0, 0)";
const RGB_GREEN = "rgb(0, 255, 0)";
const RGB_BLUE = "rgb(0, 0, 255)";

const CSS_RED_BG = `body { background-color: ${RGB_RED}; }`;
const CSS_GREEN_BG = `body { background-color: ${RGB_GREEN}; }`;
const CSS_BLUE_BG = `body { background-color: ${RGB_BLUE}; }`;

const ADDON_ID = "test-cached-resources@test";

const manifest = {
  version: "1",
  browser_specific_settings: { gecko: { id: ADDON_ID } },
};

const files = {
  "extpage.html": `<!DOCTYPE html>
     <html>
       <head>
         <link rel="stylesheet" href="extpage.css">
       </head>
       <body>
         <img id="test-image" src="image.png">
       </body>
     </html>
  `,
  "other_extpage.html": `<!DOCTYPE html>
     <html>
       <body>
       </body>
     </html>
  `,
  "extpage.css": CSS_RED_BG,
  "image.png": IMAGE_RED,
};

const getBackgroundColor = () => {
  return this.content.getComputedStyle(this.content.document.body)
    .backgroundColor;
};

const hasCachedImage = imgUrl => {
  const { document } = this.content;

  const imageCache = Cc["@mozilla.org/image/tools;1"]
    .getService(Ci.imgITools)
    .getImgCacheForDocument(document);

  const imgCacheProps = imageCache.findEntryProperties(
    Services.io.newURI(imgUrl),
    document
  );

  // return true if the image was in the cache.
  return !!imgCacheProps;
};

const getImageColor = () => {
  const { document } = this.content;
  const img = document.querySelector("img#test-image");
  const canvas = document.createElement("canvas");
  canvas.width = 1;
  canvas.height = 1;
  const ctx = canvas.getContext("2d");
  ctx.drawImage(img, 0, 0); // Draw without scaling.
  const [r, g, b, a] = ctx.getImageData(0, 0, 1, 1).data;
  if (a < 1) {
    return `rgba(${r}, ${g}, ${b}, ${a})`;
  }
  return `rgb(${r}, ${g}, ${b})`;
};

async function assertBackgroundColor(page, color, message) {
  equal(
    await page.spawn([], getBackgroundColor),
    color,
    `Got the expected ${message}`
  );
}

async function assertImageColor(page, color, message) {
  equal(await page.spawn([], getImageColor), color, message);
}

async function assertImageCached(page, imageUrl, message) {
  ok(await page.spawn([imageUrl], hasCachedImage), message);
}

// This test verifies that cached css are cleared across addon upgrades and downgrades
// for permanently installed addon (See Bug 1746841).
add_task(async function test_cached_resources_cleared_across_addon_updates() {
  await AddonTestUtils.promiseStartupManager();

  const extension = ExtensionTestUtils.loadExtension({
    useAddonManager: "permanent",
    manifest,
    files,
  });

  await extension.startup();
  equal(
    extension.version,
    "1",
    "Got the expected version for the initial extension"
  );

  const url = extension.extension.baseURI.resolve("extpage.html");
  let page = await ExtensionTestUtils.loadContentPage(url);
  await assertBackgroundColor(
    page,
    RGB_RED,
    "background color (initial extension version)"
  );
  await assertImageColor(page, RGB_RED, "image (initial extension version)");

  info("Verify extension page css and image after addon upgrade");

  await extension.upgrade({
    useAddonManager: "permanent",
    manifest: {
      ...manifest,
      version: "2",
    },
    files: {
      ...files,
      "extpage.css": CSS_GREEN_BG,
      "image.png": IMAGE_GREEN,
    },
  });
  equal(
    extension.version,
    "2",
    "Got the expected version for the upgraded extension"
  );

  await page.loadURL(url);

  await assertBackgroundColor(
    page,
    RGB_GREEN,
    "background color (upgraded extension version)"
  );
  await assertImageColor(page, RGB_GREEN, "image (upgraded extension version)");

  info("Verify extension page css and image after addon downgrade");

  await extension.upgrade({
    useAddonManager: "permanent",
    manifest,
    files,
  });
  equal(
    extension.version,
    "1",
    "Got the expected version for the downgraded extension"
  );

  await page.loadURL(url);

  await assertBackgroundColor(
    page,
    RGB_RED,
    "background color (downgraded extension version)"
  );
  await assertImageColor(
    page,
    RGB_RED,
    "image color (downgraded extension version)"
  );

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

// This test verifies that cached css are cleared if we are installing a new
// extension and we did not clear the cache for a previous one with the same uuid
// when it was uninstalled (See Bug 1746841).
add_task(async function test_cached_resources_cleared_on_addon_install() {
  // Make sure the test addon installed without an AddonManager addon wrapper
  // and the ones installed right after that using the AddonManager will share
  // the same uuid (and so also the same moz-extension resource urls).
  Services.prefs.setBoolPref(LEAVE_UUID_PREF, true);
  registerCleanupFunction(() => Services.prefs.clearUserPref(LEAVE_UUID_PREF));

  await AddonTestUtils.promiseStartupManager();

  const nonAOMExtension = ExtensionTestUtils.loadExtension({
    manifest,
    files: {
      ...files,
      // Override css with a different color from the one expected
      // later in this test case.
      "extpage.css": CSS_BLUE_BG,
      "image.png": IMAGE_BLUE,
    },
  });

  await nonAOMExtension.startup();
  equal(
    await AddonManager.getAddonByID(ADDON_ID),
    null,
    "No AOM addon wrapper found as expected"
  );
  let url = nonAOMExtension.extension.baseURI.resolve("extpage.html");
  let page = await ExtensionTestUtils.loadContentPage(url);
  await assertBackgroundColor(
    page,
    RGB_BLUE,
    "background color (addon installed without uninstall observer)"
  );
  await assertImageColor(
    page,
    RGB_BLUE,
    "image (addon uninstalled without clearing cache)"
  );

  // NOTE: unloading a test extension that does not have an AddonManager addon wrapper
  // does not trigger the uninstall observer, and this is what this test needs to make
  // sure that if the cached resources were not cleared on uninstall, then we will still
  // clear it when a newly installed addon is installed even if the two extensions
  // are sharing the same addon uuid (and so also the same moz-extension resource urls).
  await nonAOMExtension.unload();

  const extension = ExtensionTestUtils.loadExtension({
    useAddonManager: "permanent",
    manifest,
    files,
  });

  await extension.startup();
  await page.loadURL(url);

  await assertBackgroundColor(
    page,
    RGB_RED,
    "background color (newly installed addon, same addon id)"
  );
  await assertImageColor(
    page,
    RGB_RED,
    "image (newly installed addon, same addon id)"
  );

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

// This test verifies that reloading a temporarily installed addon after
// changing a css file cached in a previous run clears the previously
// cached css and uses the new one changed on disk (See Bug 1746841).
add_task(
  async function test_cached_resources_cleared_on_temporary_addon_reload() {
    await AddonTestUtils.promiseStartupManager();

    const xpi = AddonTestUtils.createTempWebExtensionFile({
      manifest,
      files,
    });

    // This temporary directory is going to be removed from the
    // cleanup function, but also make it unique as we do for the
    // other temporary files (e.g. like getTemporaryFile as defined
    // in XPIInstall.sys.mjs).
    const random = Math.round(Math.random() * 36 ** 3).toString(36);
    const tmpDirName = `xpcshelltest_unpacked_addons_${random}`;
    let tmpExtPath = FileUtils.getDir("TmpD", [tmpDirName]);
    tmpExtPath.create(Ci.nsIFile.DIRECTORY_TYPE, FileUtils.PERMS_DIRECTORY);
    registerCleanupFunction(() => {
      tmpExtPath.remove(true);
    });

    // Unpacking the xpi file into the temporary directory.
    const extDir = await AddonTestUtils.manuallyInstall(
      xpi,
      tmpExtPath,
      null,
      /* unpacked */ true
    );

    let extension = ExtensionTestUtils.expectExtension(ADDON_ID);
    await AddonManager.installTemporaryAddon(extDir);
    await extension.awaitStartup();

    equal(
      extension.version,
      "1",
      "Got the expected version for the initial extension"
    );

    const url = extension.extension.baseURI.resolve("extpage.html");
    let page = await ExtensionTestUtils.loadContentPage(url);
    await assertBackgroundColor(
      page,
      RGB_RED,
      "background color (initial extension version)"
    );
    await assertImageColor(page, RGB_RED, "image (initial extension version)");

    info("Verify updated extension page css and image after addon reload");

    const targetCSSFile = extDir.clone();
    targetCSSFile.append("extpage.css");
    ok(
      targetCSSFile.exists(),
      `Found the ${targetCSSFile.path} target file on disk`
    );
    await IOUtils.writeUTF8(targetCSSFile.path, CSS_GREEN_BG);

    const targetPNGFile = extDir.clone();
    targetPNGFile.append("image.png");
    ok(
      targetPNGFile.exists(),
      `Found the ${targetPNGFile.path} target file on disk`
    );
    await IOUtils.write(targetPNGFile.path, toArrayBuffer(BASE64_G_PIXEL));

    const addon = await AddonManager.getAddonByID(ADDON_ID);
    ok(addon, "Got an AddonWrapper for the test extension");
    await addon.reload();

    await page.loadURL(url);

    await assertBackgroundColor(
      page,
      RGB_GREEN,
      "background (updated files on disk)"
    );
    await assertImageColor(page, RGB_GREEN, "image (updated files on disk)");

    await page.close();
    await addon.uninstall();
    await AddonTestUtils.promiseShutdownManager();
  }
);

// This test verifies that cached images are not cleared between
// permanently installed addon reloads.
add_task(async function test_cached_image_kept_on_permanent_addon_restarts() {
  await AddonTestUtils.promiseStartupManager();
  const extension = ExtensionTestUtils.loadExtension({
    useAddonManager: "permanent",
    manifest,
    files,
  });

  await extension.startup();

  equal(
    extension.version,
    "1",
    "Got the expected version for the initial extension"
  );

  const imageUrl = extension.extension.baseURI.resolve("image.png");
  const url = extension.extension.baseURI.resolve("extpage.html");

  let page = await ExtensionTestUtils.loadContentPage(url);
  await assertBackgroundColor(
    page,
    RGB_RED,
    "background color (first startup)"
  );
  await assertImageColor(page, RGB_RED, "image (first startup)");
  await assertImageCached(page, imageUrl, "image cached (first startup)");

  info("Reload the AddonManager to simulate browser restart");
  extension.setRestarting();
  await AddonTestUtils.promiseRestartManager();
  await extension.awaitStartup();

  await page.loadURL(extension.extension.baseURI.resolve("other_extpage.html"));
  await assertImageCached(
    page,
    imageUrl,
    "image still cached after AddonManager restart"
  );

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