/* Any copyright is dedicated to the Public Domain.
   http://creativecommons.org/publicdomain/zero/1.0/ */

/* globals browser */

"use strict";

const { ExtensionTestUtils } = ChromeUtils.importESModule(
  "resource://testing-common/ExtensionXPCShellUtils.sys.mjs"
);

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

const {
  createMissingIndexedDBDirs,
  extensionScriptWithMessageListener,
  ext_no_bg,
  getExtensionConfig,
  openAddonStoragePanel,
  shutdown,
  startupExtension,
} = require("resource://test/webextension-helpers.js");

const l10n = new Localization(["devtools/client/storage.ftl"], true);
const sessionString = l10n.formatValueSync("storage-expires-session");

// Ignore rejection related to the storage.onChanged listener being removed while the extension context is being closed.
const { PromiseTestUtils } = ChromeUtils.importESModule(
  "resource://testing-common/PromiseTestUtils.sys.mjs"
);
PromiseTestUtils.allowMatchingRejectionsGlobally(
  /Message manager disconnected/
);
PromiseTestUtils.allowMatchingRejectionsGlobally(
  /sendRemoveListener on closed conduit/
);

const { createAppInfo, promiseStartupManager } = AddonTestUtils;

const LEAVE_UUID_PREF = "extensions.webextensions.keepUuidOnUninstall";
const LEAVE_STORAGE_PREF = "extensions.webextensions.keepStorageOnUninstall";

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

ExtensionTestUtils.init(this);

add_setup(async function setup() {
  await promiseStartupManager();
  const dir = createMissingIndexedDBDirs();

  Assert.ok(
    dir.exists(),
    "Should have a 'storage/permanent' dir in the profile dir"
  );
});

add_task(async function test_extension_store_exists() {
  const extension = await startupExtension(getExtensionConfig());

  const { commands, extensionStorage } = await openAddonStoragePanel(
    extension.id
  );

  ok(extensionStorage, "Should have an extensionStorage store");

  await shutdown(extension, commands);
});

add_task(
  {
    // This test currently fails if the extension runs in the main process
    // like in Thunderbird (see bug 1575183 comment #15 for details).
    skip_if: () => !WebExtensionPolicy.useRemoteWebExtensions,
  },
  async function test_extension_origin_matches_debugger_target() {
    async function background() {
      // window is available in background scripts
      // eslint-disable-next-line no-undef
      browser.test.sendMessage("extension-origin", window.location.origin);
    }

    const extension = await startupExtension(
      getExtensionConfig({ background })
    );

    const { commands, extensionStorage } = await openAddonStoragePanel(
      extension.id
    );

    const { hosts } = extensionStorage;
    const expectedHost = await extension.awaitMessage("extension-origin");
    ok(
      expectedHost in hosts,
      "Should have the expected extension host in the extensionStorage store"
    );

    await shutdown(extension, commands);
  }
);

/**
 * Test case: Background page modifies items while storage panel is open.
 * - Load extension with background page.
 * - Open the add-on debugger storage panel.
 * - With the panel still open, from the extension background page:
 *   - Bulk add storage items
 *   - Edit the values of some of the storage items
 *   - Remove some storage items
 *   - Clear all storage items
 * - For each modification, the storage data in the panel should match the
 *   changes made by the extension.
 */
add_task(async function test_panel_live_updates() {
  const extension = await startupExtension(
    getExtensionConfig({ background: extensionScriptWithMessageListener })
  );

  const { commands, extensionStorage } = await openAddonStoragePanel(
    extension.id
  );

  const host = await extension.awaitMessage("extension-origin");

  let { data } = await extensionStorage.getStoreObjects(host);
  Assert.deepEqual(data, [], "Got the expected results on empty storage.local");

  info("Waiting for extension to bulk add 50 items to storage local");
  const bulkStorageItems = {};
  // limited by MAX_STORE_OBJECT_COUNT in devtools/server/actors/resources/storage/index.js
  const numItems = 2;
  for (let i = 1; i <= numItems; i++) {
    bulkStorageItems[i] = i;
  }

  // fireOnChanged avoids the race condition where the extension
  // modifies storage then immediately tries to access storage before
  // the storage actor has finished updating.
  extension.sendMessage("storage-local-fireOnChanged");
  extension.sendMessage("storage-local-set", {
    ...bulkStorageItems,
    a: 123,
    b: [4, 5],
    c: { d: 678 },
    d: true,
    e: "hi",
    f: null,
  });
  await extension.awaitMessage("storage-local-set:done");
  await extension.awaitMessage("storage-local-onChanged");

  info(
    "Confirming items added by extension match items in extensionStorage store"
  );
  const bulkStorageObjects = [];
  for (const [name, value] of Object.entries(bulkStorageItems)) {
    bulkStorageObjects.push({
      area: "local",
      name,
      value: { str: String(value) },
      isValueEditable: true,
    });
  }
  data = (await extensionStorage.getStoreObjects(host, null, { sessionString }))
    .data;
  Assert.deepEqual(
    data,
    [
      ...bulkStorageObjects,
      {
        area: "local",
        name: "a",
        value: { str: "123" },
        isValueEditable: true,
      },
      {
        area: "local",
        name: "b",
        value: { str: "[4,5]" },
        isValueEditable: true,
      },
      {
        area: "local",
        name: "c",
        value: { str: '{"d":678}' },
        isValueEditable: true,
      },
      {
        area: "local",
        name: "d",
        value: { str: "true" },
        isValueEditable: true,
      },
      {
        area: "local",
        name: "e",
        value: { str: "hi" },
        isValueEditable: true,
      },
      {
        area: "local",
        name: "f",
        value: { str: "null" },
        isValueEditable: true,
      },
    ],
    "Got the expected results on populated storage.local"
  );

  info("Waiting for extension to edit a few storage item values");
  extension.sendMessage("storage-local-fireOnChanged");
  extension.sendMessage("storage-local-set", {
    a: ["c", "d"],
    b: 456,
    c: false,
  });
  await extension.awaitMessage("storage-local-set:done");
  await extension.awaitMessage("storage-local-onChanged");

  info(
    "Confirming items edited by extension match items in extensionStorage store"
  );
  data = (await extensionStorage.getStoreObjects(host, null, { sessionString }))
    .data;
  Assert.deepEqual(
    data,
    [
      ...bulkStorageObjects,
      {
        area: "local",
        name: "a",
        value: { str: '["c","d"]' },
        isValueEditable: true,
      },
      {
        area: "local",
        name: "b",
        value: { str: "456" },
        isValueEditable: true,
      },
      {
        area: "local",
        name: "c",
        value: { str: "false" },
        isValueEditable: true,
      },
      {
        area: "local",
        name: "d",
        value: { str: "true" },
        isValueEditable: true,
      },
      {
        area: "local",
        name: "e",
        value: { str: "hi" },
        isValueEditable: true,
      },
      {
        area: "local",
        name: "f",
        value: { str: "null" },
        isValueEditable: true,
      },
    ],
    "Got the expected results on populated storage.local"
  );

  info("Waiting for extension to remove a few storage item values");
  extension.sendMessage("storage-local-fireOnChanged");
  extension.sendMessage("storage-local-remove", ["d", "e", "f"]);
  await extension.awaitMessage("storage-local-remove:done");
  await extension.awaitMessage("storage-local-onChanged");

  info(
    "Confirming items removed by extension were removed in extensionStorage store"
  );
  data = (await extensionStorage.getStoreObjects(host, null, { sessionString }))
    .data;
  Assert.deepEqual(
    data,
    [
      ...bulkStorageObjects,
      {
        area: "local",
        name: "a",
        value: { str: '["c","d"]' },
        isValueEditable: true,
      },
      {
        area: "local",
        name: "b",
        value: { str: "456" },
        isValueEditable: true,
      },
      {
        area: "local",
        name: "c",
        value: { str: "false" },
        isValueEditable: true,
      },
    ],
    "Got the expected results on populated storage.local"
  );

  info("Waiting for extension to remove all remaining storage items");
  extension.sendMessage("storage-local-fireOnChanged");
  extension.sendMessage("storage-local-clear");
  await extension.awaitMessage("storage-local-clear:done");
  await extension.awaitMessage("storage-local-onChanged");

  info("Confirming extensionStorage store was cleared");
  data = (await extensionStorage.getStoreObjects(host)).data;
  Assert.deepEqual(
    data,
    [],
    "Got the expected results on populated storage.local"
  );

  await shutdown(extension, commands);
});

/**
 * Test case: No bg page. Transient page adds item before storage panel opened.
 * - Load extension with no background page.
 * - Open an extension page in a tab that adds a local storage item.
 * - With the extension page still open, open the add-on storage panel.
 * - The data in the storage panel should match the items added by the extension.
 */
add_task(
  async function test_panel_data_matches_extension_with_transient_page_open() {
    const extension = await startupExtension(
      getExtensionConfig({ files: ext_no_bg.files })
    );

    const url = extension.extension.baseURI.resolve(
      "extension_page_in_tab.html"
    );
    const contentPage = await ExtensionTestUtils.loadContentPage(url, {
      extension,
    });

    const host = await extension.awaitMessage("extension-origin");

    extension.sendMessage("storage-local-set", { a: 123 });
    await extension.awaitMessage("storage-local-set:done");

    const { commands, extensionStorage } = await openAddonStoragePanel(
      extension.id
    );

    const { data } = await extensionStorage.getStoreObjects(host);
    Assert.deepEqual(
      data,
      [
        {
          area: "local",
          name: "a",
          value: { str: "123" },
          isValueEditable: true,
        },
      ],
      "Got the expected results on populated storage.local"
    );

    await contentPage.close();
    await shutdown(extension, commands);
  }
);

/**
 * Test case: No bg page. Transient page adds item then closes before storage panel opened.
 * - Load extension with no background page.
 * - Open an extension page in a tab that adds a local storage item.
 * - Close all extension pages.
 * - Open the add-on storage panel.
 * - The data in the storage panel should match the item added by the extension.
 */
add_task(async function test_panel_data_matches_extension_with_no_pages_open() {
  const extension = await startupExtension(
    getExtensionConfig({ files: ext_no_bg.files })
  );

  const url = extension.extension.baseURI.resolve("extension_page_in_tab.html");
  const contentPage = await ExtensionTestUtils.loadContentPage(url, {
    extension,
  });

  const host = await extension.awaitMessage("extension-origin");

  extension.sendMessage("storage-local-fireOnChanged");
  extension.sendMessage("storage-local-set", { a: 123 });
  await extension.awaitMessage("storage-local-set:done");
  await extension.awaitMessage("storage-local-onChanged");
  await contentPage.close();

  const { commands, extensionStorage } = await openAddonStoragePanel(
    extension.id
  );

  const { data } = await extensionStorage.getStoreObjects(host);
  Assert.deepEqual(
    data,
    [
      {
        area: "local",
        name: "a",
        value: { str: "123" },
        isValueEditable: true,
      },
    ],
    "Got the expected results on populated storage.local"
  );

  await shutdown(extension, commands);
});

/**
 * Test case: No bg page. Storage panel live updates when a transient page adds an item.
 * - Load extension with no background page.
 * - Open the add-on storage panel.
 * - With the storage panel still open, open an extension page in a new tab that adds an
 *   item.
 * - The data in the storage panel should live update to match the item added by the
 *   extension.
 * - If an extension page adds the same data again, the data in the storage panel should
 *   not change.
 */
add_task(
  async function test_panel_data_live_updates_for_extension_without_bg_page() {
    const extension = await startupExtension(
      getExtensionConfig({ files: ext_no_bg.files })
    );

    const { commands, extensionStorage } = await openAddonStoragePanel(
      extension.id
    );

    const url = extension.extension.baseURI.resolve(
      "extension_page_in_tab.html"
    );
    const contentPage = await ExtensionTestUtils.loadContentPage(url, {
      extension,
    });

    const host = await extension.awaitMessage("extension-origin");

    let { data } = await extensionStorage.getStoreObjects(host);
    Assert.deepEqual(
      data,
      [],
      "Got the expected results on empty storage.local"
    );

    extension.sendMessage("storage-local-fireOnChanged");
    extension.sendMessage("storage-local-set", { a: 123 });
    await extension.awaitMessage("storage-local-set:done");
    await extension.awaitMessage("storage-local-onChanged");

    data = (await extensionStorage.getStoreObjects(host)).data;
    Assert.deepEqual(
      data,
      [
        {
          area: "local",
          name: "a",
          value: { str: "123" },
          isValueEditable: true,
        },
      ],
      "Got the expected results on populated storage.local"
    );

    extension.sendMessage("storage-local-fireOnChanged");
    extension.sendMessage("storage-local-set", { a: 123 });
    await extension.awaitMessage("storage-local-set:done");
    await extension.awaitMessage("storage-local-onChanged");

    data = (await extensionStorage.getStoreObjects(host)).data;
    Assert.deepEqual(
      data,
      [
        {
          area: "local",
          name: "a",
          value: { str: "123" },
          isValueEditable: true,
        },
      ],
      "The results are unchanged when an extension page adds duplicate items"
    );

    await contentPage.close();
    await shutdown(extension, commands);
  }
);

/**
 * Test case: Bg page adds item while storage panel is open. Panel edits item's value.
 * - Load extension with background page.
 * - Open the add-on storage panel.
 * - With the storage panel still open, add item from the background page.
 * - Edit the value of the item in the storage panel
 * - The data in the storage panel should match the item added by the extension.
 * - The storage actor is correctly parsing and setting the string representation of
 *     the value in the storage local database when the item's value is edited in the
 *     storage panel
 */
add_task(
  async function test_editing_items_in_panel_parses_supported_values_correctly() {
    const extension = await startupExtension(
      getExtensionConfig({ background: extensionScriptWithMessageListener })
    );

    const host = await extension.awaitMessage("extension-origin");

    const { commands, extensionStorage } = await openAddonStoragePanel(
      extension.id
    );

    const oldItem = { a: 123 };
    const key = Object.keys(oldItem)[0];
    const oldValue = oldItem[key];
    // A tuple representing information for a new value entered into the panel for oldItem:
    // [
    //   value,
    //   editItem string representation of value,
    //   toStoreObject string representation of value,
    // ]
    const valueInfo = [
      [true, "true", "true"],
      ["hi", "hi", "hi"],
      [456, "456", "456"],
      [{ b: 789 }, "{b: 789}", '{"b":789}'],
      [[1, 2, 3], "[1, 2, 3]", "[1,2,3]"],
      [null, "null", "null"],
    ];
    for (const [value, editItemValueStr, toStoreObjectValueStr] of valueInfo) {
      info("Setting a storage item through the extension");
      extension.sendMessage("storage-local-fireOnChanged");
      extension.sendMessage("storage-local-set", oldItem);
      await extension.awaitMessage("storage-local-set:done");
      await extension.awaitMessage("storage-local-onChanged");

      info(
        "Editing the storage item in the panel with a new value of a different type"
      );
      // When the user edits an item in the panel, they are entering a string into a
      // textbox. This string is parsed by the storage actor's editItem method.
      await extensionStorage.editItem({
        host,
        field: "value",
        items: { name: key, value: editItemValueStr },
        oldValue,
      });

      info(
        "Verifying item in the storage actor matches the item edited in the panel"
      );
      const { data } = await extensionStorage.getStoreObjects(host);
      Assert.deepEqual(
        data,
        [
          {
            area: "local",
            name: key,
            value: { str: toStoreObjectValueStr },
            isValueEditable: true,
          },
        ],
        "Got the expected results on populated storage.local"
      );

      // The view layer is separate from the database layer; therefore while values are
      // stringified (via toStoreObject) for display in the client, the value (and its type)
      // in the database is unchanged.
      info(
        "Verifying the expected new value matches the value fetched in the extension"
      );
      extension.sendMessage("storage-local-get", key);
      const extItem = await extension.awaitMessage("storage-local-get:done");
      Assert.deepEqual(
        value,
        extItem[key],
        `The string value ${editItemValueStr} was correctly parsed to ${value}`
      );
    }

    await shutdown(extension, commands);
  }
);

/**
 * Test case: Modifying storage items from the panel update extension storage local data.
 * - Load extension with background page.
 * - Open the add-on storage panel. From the panel:
 *   - Edit the value of a storage item,
 *   - Remove a storage item,
 *   - Remove all of the storage items,
 * - For each modification, the storage data retrieved by the extension should match the
 *   data in the panel.
 */
add_task(
  async function test_modifying_items_in_panel_updates_extension_storage_data() {
    const extension = await startupExtension(
      getExtensionConfig({ background: extensionScriptWithMessageListener })
    );

    const host = await extension.awaitMessage("extension-origin");

    const { commands, extensionStorage } = await openAddonStoragePanel(
      extension.id
    );

    const DEFAULT_VALUE = "value"; // global in devtools/server/actors/resources/storage/index.js
    let items = {
      guid_1: DEFAULT_VALUE,
      guid_2: DEFAULT_VALUE,
      guid_3: DEFAULT_VALUE,
    };

    info("Adding storage items from the extension");
    let storesUpdate = extensionStorage.once("single-store-update");
    extension.sendMessage("storage-local-set", items);
    await extension.awaitMessage("storage-local-set:done");

    info("Waiting for the storage actor to emit a 'stores-update' event");
    let data = await storesUpdate;
    Assert.deepEqual(
      {
        added: {
          extensionStorage: {
            [host]: ["guid_1", "guid_2", "guid_3"],
          },
        },
        changed: undefined,
        deleted: undefined,
      },
      data,
      "The change data from the storage actor's 'stores-update' event matches the changes made in the client."
    );

    info("Waiting for panel to edit some items");
    storesUpdate = extensionStorage.once("single-store-update");
    await extensionStorage.editItem({
      host,
      field: "value",
      items: { name: "guid_1", value: "anotherValue" },
      DEFAULT_VALUE,
    });

    info("Waiting for the storage actor to emit a 'stores-update' event");
    data = await storesUpdate;
    Assert.deepEqual(
      {
        added: undefined,
        changed: {
          extensionStorage: {
            [host]: ["guid_1"],
          },
        },
        deleted: undefined,
      },
      data,
      "The change data from the storage actor's 'stores-update' event matches the changes made in the client."
    );

    items = {
      guid_1: "anotherValue",
      guid_2: DEFAULT_VALUE,
      guid_3: DEFAULT_VALUE,
    };
    extension.sendMessage("storage-local-get", Object.keys(items));
    let extItems = await extension.awaitMessage("storage-local-get:done");
    Assert.deepEqual(
      items,
      extItems,
      `The storage items in the extension match the items in the panel`
    );

    info("Waiting for panel to remove an item");
    storesUpdate = extensionStorage.once("single-store-update");
    await extensionStorage.removeItem(host, "guid_3");

    info("Waiting for the storage actor to emit a 'stores-update' event");
    data = await storesUpdate;
    Assert.deepEqual(
      {
        added: undefined,
        changed: undefined,
        deleted: {
          extensionStorage: {
            [host]: ["guid_3"],
          },
        },
      },
      data,
      "The change data from the storage actor's 'stores-update' event matches the changes made in the client."
    );

    items = {
      guid_1: "anotherValue",
      guid_2: DEFAULT_VALUE,
    };
    extension.sendMessage("storage-local-get", Object.keys(items));
    extItems = await extension.awaitMessage("storage-local-get:done");
    Assert.deepEqual(
      items,
      extItems,
      `The storage items in the extension match the items in the panel`
    );

    info("Waiting for panel to remove all items");
    const storesCleared = extensionStorage.once("single-store-cleared");
    await extensionStorage.removeAll(host);

    info("Waiting for the storage actor to emit a 'stores-cleared' event");
    data = await storesCleared;
    Assert.deepEqual(
      {
        clearedHostsOrPaths: {
          [host]: [],
        },
      },
      data,
      "The change data from the storage actor's 'stores-cleared' event matches the changes made in the client."
    );

    items = {};
    extension.sendMessage("storage-local-get", Object.keys(items));
    extItems = await extension.awaitMessage("storage-local-get:done");
    Assert.deepEqual(
      items,
      extItems,
      `The storage items in the extension match the items in the panel`
    );

    await shutdown(extension, commands);
  }
);

/**
 * Test case: Storage panel shows extension storage data added prior to extension startup
 * - Load extension that adds a storage item
 * - Uninstall the extension
 * - Reinstall the extension
 * - Open the add-on storage panel.
 * - The data in the storage panel should match the data added the first time the extension
 *   was installed
 * Related test case: Storage panel shows extension storage data when an extension that has
 * already migrated to the IndexedDB storage backend prior to extension startup adds
 * another storage item.
 * - (Building from previous steps)
 * - The reinstalled extension adds a storage item
 * - The data in the storage panel should live update with both items: the item added from
 *   the first and the item added from the reinstall.
 */
add_task(
  async function test_panel_data_matches_data_added_prior_to_ext_startup() {
    // The pref to leave the addonid->uuid mapping around after uninstall so that we can
    // re-attach to the same storage
    Services.prefs.setBoolPref(LEAVE_UUID_PREF, true);

    // The pref to prevent cleaning up storage on uninstall
    Services.prefs.setBoolPref(LEAVE_STORAGE_PREF, true);

    let extension = await startupExtension(
      getExtensionConfig({ background: extensionScriptWithMessageListener })
    );

    const host = await extension.awaitMessage("extension-origin");

    extension.sendMessage("storage-local-set", { a: 123 });
    await extension.awaitMessage("storage-local-set:done");

    await shutdown(extension);

    // Reinstall the same extension
    extension = await startupExtension(
      getExtensionConfig({ background: extensionScriptWithMessageListener })
    );

    await extension.awaitMessage("extension-origin");

    const { commands, extensionStorage } = await openAddonStoragePanel(
      extension.id
    );

    let { data } = await extensionStorage.getStoreObjects(host);
    Assert.deepEqual(
      data,
      [
        {
          area: "local",
          name: "a",
          value: { str: "123" },
          isValueEditable: true,
        },
      ],
      "Got the expected results on populated storage.local"
    );

    // Related test case
    extension.sendMessage("storage-local-fireOnChanged");
    extension.sendMessage("storage-local-set", { b: 456 });
    await extension.awaitMessage("storage-local-set:done");
    await extension.awaitMessage("storage-local-onChanged");

    data = (
      await extensionStorage.getStoreObjects(host, null, { sessionString })
    ).data;
    Assert.deepEqual(
      data,
      [
        {
          area: "local",
          name: "a",
          value: { str: "123" },
          isValueEditable: true,
        },
        {
          area: "local",
          name: "b",
          value: { str: "456" },
          isValueEditable: true,
        },
      ],
      "Got the expected results on populated storage.local"
    );

    Services.prefs.setBoolPref(LEAVE_STORAGE_PREF, false);
    Services.prefs.setBoolPref(LEAVE_UUID_PREF, false);

    await shutdown(extension, commands);
  }
);

add_task(
  function cleanup_for_test_panel_data_matches_data_added_prior_to_ext_startup() {
    Services.prefs.clearUserPref(LEAVE_UUID_PREF);
    Services.prefs.clearUserPref(LEAVE_STORAGE_PREF);
  }
);

/**
 * Test case: Transient page adds an item to storage. With storage panel open,
 * reload extension.
 * - Load extension with no background page.
 * - Open transient page that adds a storage item on message.
 * - Open the add-on storage panel.
 * - With the storage panel still open, reload the extension.
 * - The data in the storage panel should match the item added prior to reloading.
 */
add_task(async function test_panel_live_reload_for_extension_without_bg_page() {
  const EXTENSION_ID = "test_local_storage_live_reload@xpcshell.mozilla.org";
  let manifest = {
    version: "1.0",
    browser_specific_settings: {
      gecko: {
        id: EXTENSION_ID,
      },
    },
  };

  info("Loading and starting extension version 1.0");
  const extension = await startupExtension(
    getExtensionConfig({
      manifest,
      files: ext_no_bg.files,
    })
  );

  info("Opening extension page in a tab");
  const url = extension.extension.baseURI.resolve("extension_page_in_tab.html");
  const contentPage = await ExtensionTestUtils.loadContentPage(url, {
    extension,
  });

  const host = await extension.awaitMessage("extension-origin");

  info("Waiting for extension page in a tab to add storage item");
  extension.sendMessage("storage-local-fireOnChanged");
  extension.sendMessage("storage-local-set", { a: 123 });
  await extension.awaitMessage("storage-local-set:done");
  await extension.awaitMessage("storage-local-onChanged");
  await contentPage.close();

  info("Opening storage panel");
  const { commands, extensionStorage } = await openAddonStoragePanel(
    extension.id
  );

  manifest = {
    ...manifest,
    version: "2.0",
  };
  // "Reload" is most similar to an upgrade, as e.g. storage data is preserved
  info("Updating extension to version 2.0");
  await extension.upgrade(
    getExtensionConfig({
      manifest,
      files: ext_no_bg.files,
    })
  );

  const { data } = await extensionStorage.getStoreObjects(host);
  Assert.deepEqual(
    data,
    [
      {
        area: "local",
        name: "a",
        value: { str: "123" },
        isValueEditable: true,
      },
    ],
    "Got the expected results on populated storage.local"
  );

  await shutdown(extension, commands);
});

/**
 * Test case: Bg page auto adds item(s). With storage panel open, reload extension.
 * - Load extension with background page that automatically adds a storage item on startup.
 * - Open the add-on storage panel.
 * - With the storage panel still open, reload the extension.
 * - The data in the storage panel should match the item(s) added by the reloaded
 *   extension.
 */
add_task(
  async function test_panel_live_reload_when_extension_auto_adds_items() {
    async function background() {
      await browser.storage.local.set({ a: { b: 123 }, c: { d: 456 } });
      // window is available in background scripts
      // eslint-disable-next-line no-undef
      browser.test.sendMessage("extension-origin", window.location.origin);
    }
    const EXTENSION_ID = "test_local_storage_live_reload@xpcshell.mozilla.org";
    let manifest = {
      version: "1.0",
      browser_specific_settings: {
        gecko: {
          id: EXTENSION_ID,
        },
      },
    };

    info("Loading and starting extension version 1.0");
    const extension = await startupExtension(
      getExtensionConfig({ manifest, background })
    );

    info("Waiting for message from test extension");
    const host = await extension.awaitMessage("extension-origin");

    info("Opening storage panel");
    const { commands, extensionStorage } = await openAddonStoragePanel(
      extension.id
    );

    manifest = {
      ...manifest,
      version: "2.0",
    };
    // "Reload" is most similar to an upgrade, as e.g. storage data is preserved
    info("Update to version 2.0");
    await extension.upgrade(
      getExtensionConfig({
        manifest,
        background,
      })
    );

    await extension.awaitMessage("extension-origin");

    const { data } = await extensionStorage.getStoreObjects(host, null, {
      sessionString,
    });
    Assert.deepEqual(
      data,
      [
        {
          area: "local",
          name: "a",
          value: { str: '{"b":123}' },
          isValueEditable: true,
        },
        {
          area: "local",
          name: "c",
          value: { str: '{"d":456}' },
          isValueEditable: true,
        },
      ],
      "Got the expected results on populated storage.local"
    );

    await shutdown(extension, commands);
  }
);

/**
 * Test case: Bg page adds one storage.local item and one storage.sync item.
 * - Load extension with background page that automatically adds two storage items on startup.
 * - Open the add-on storage panel.
 * - Assert that only the storage.local item is shown in the panel.
 */
add_task(
  async function test_panel_data_only_updates_for_storage_local_changes() {
    async function background() {
      await browser.storage.local.set({ a: { b: 123 } });
      await browser.storage.sync.set({ c: { d: 456 } });
      // window is available in background scripts
      // eslint-disable-next-line no-undef
      browser.test.sendMessage("extension-origin", window.location.origin);
    }

    // Using the storage.sync API requires a non-temporary extension ID, see Bug 1323228.
    const EXTENSION_ID =
      "test_panel_data_only_updates_for_storage_local_changes@xpcshell.mozilla.org";
    const manifest = {
      browser_specific_settings: {
        gecko: {
          id: EXTENSION_ID,
        },
      },
    };

    info("Loading and starting extension");
    const extension = await startupExtension(
      getExtensionConfig({ manifest, background })
    );

    info("Waiting for message from test extension");
    const host = await extension.awaitMessage("extension-origin");

    info("Opening storage panel");
    const { commands, extensionStorage } = await openAddonStoragePanel(
      extension.id
    );

    const { data } = await extensionStorage.getStoreObjects(host);
    Assert.deepEqual(
      data,
      [
        {
          area: "local",
          name: "a",
          value: { str: '{"b":123}' },
          isValueEditable: true,
        },
      ],
      "Got the expected results on populated storage.local"
    );

    await shutdown(extension, commands);
  }
);

// This test verifies that Bug 1802929 fix doesn't regress.
add_task(async function test_live_update_with_no_extension_listener() {
  const EXTENSION_ID = "test_with_no_listeners@xpcshell.mozilla.org";
  let manifest = {
    version: "1.0",
    browser_specific_settings: {
      gecko: {
        id: EXTENSION_ID,
      },
    },
  };

  function background() {
    browser.test.onMessage.addListener(async (msg, ...args) => {
      if (msg !== "storage-local-api-call") {
        browser.test.fail(`Got unexpected test message: ${msg}`);
        return;
      }

      const [{ method, methodArgs }] = args;
      const res = await browser.storage.local[method](...methodArgs);
      browser.test.sendMessage(`${msg}:done`, res);
    });
  }

  const extension = await startupExtension(
    getExtensionConfig({ manifest, background })
  );

  const { target, extensionStorage } = await openAddonStoragePanel(
    extension.id
  );

  const { baseURI } = extension.extension;
  const host = `${baseURI.scheme}://${baseURI.host}`;

  let { data } = await extensionStorage.getStoreObjects(host);
  Assert.deepEqual(data, [], "Got the expected results on empty storage.local");

  async function testStorageLocalUpdate(storageValue) {
    info("Store extension data");
    await extension.sendMessage("storage-local-api-call", {
      method: "set",
      methodArgs: [{ storageKeyName: storageValue }],
    });
    await extension.awaitMessage("storage-local-api-call:done");

    info("Verify stored extension data");
    await extension.sendMessage("storage-local-api-call", {
      method: "get",
      methodArgs: [],
    });

    Assert.deepEqual(
      await extension.awaitMessage("storage-local-api-call:done"),
      { storageKeyName: storageValue },
      "Got the expected value from browser.storage.local.get"
    );

    await TestUtils.waitForCondition(async () => {
      const res = await extensionStorage.getStoreObjects(host);
      return res.data?.length > 0;
    }, "Wait for the extension storage panel updates");

    data = (await extensionStorage.getStoreObjects(host)).data;
    Assert.deepEqual(
      data,
      [
        {
          area: "local",
          name: "storageKeyName",
          value: { str: `${storageValue}` },
          isValueEditable: true,
        },
      ],
      "Expected DevTools Storage panel data to have been updated"
    );
  }

  await testStorageLocalUpdate("aStorageValue 01");

  manifest = {
    ...manifest,
    version: "2.0",
  };
  // "Reload" is most similar to an upgrade, as e.g. storage data is preserved
  info("Update to version 2.0");
  await extension.upgrade(getExtensionConfig({ manifest, background }));

  await testStorageLocalUpdate("aStorageValue 02");

  await shutdown(extension, target);
});

// This test verifies that Bug 1994355 fix doesn't regress.
add_task(
  async function test_extension_onChanged_emitted_with_active_storage_panel() {
    function background() {
      const onChangedCalls = [];

      browser.test.onMessage.addListener(async (msg, ...args) => {
        switch (msg) {
          case "storage-local-add-listener":
            browser.storage.local.onChanged.addListener(changes => {
              browser.test.log(
                `Extension onChanged listener called with changes: ${JSON.stringify(changes)}`
              );
              onChangedCalls.push(changes);
            });
            browser.test.sendMessage("storage-local-add-listener:done");
            break;
          case "storage-local-set":
            await browser.storage.local.set({ testKey: args[0] });
            browser.test.sendMessage("storage-local-set:done");
            break;
          case "get-onChanged-calls":
            browser.test.sendMessage(
              "get-onChanged-calls:done",
              onChangedCalls
            );
            break;
          default:
            browser.test.fail(`Unexpected test message received: ${msg}`);
        }
      });

      browser.test.sendMessage("background-ready");
    }

    const extension = await startupExtension(
      getExtensionConfig({ background })
    );

    await extension.awaitMessage("background-ready");

    // Set the storage.local key to an js object to force the
    // value to be serialize into a StructureCloneHolder.
    const TEST_VALUE_INITIAL = { valueObjKey: "initialTestValue" };
    const TEST_VALUE_UPDATED = { valueObjKey: "newTestValue" };

    extension.sendMessage("storage-local-set", TEST_VALUE_INITIAL);
    await extension.awaitMessage("storage-local-set:done");

    const { commands, extensionStorage } = await openAddonStoragePanel(
      extension.id
    );

    const { baseURI } = extension.extension;
    const host = `${baseURI.scheme}://${baseURI.host}`;

    let { data } = await extensionStorage.getStoreObjects(host);
    Assert.deepEqual(
      data,
      [
        {
          area: "local",
          name: "testKey",
          value: { str: JSON.stringify(TEST_VALUE_INITIAL) },
          isValueEditable: true,
        },
      ],
      "Storage actor got the expected initial data"
    );

    extension.sendMessage("storage-local-add-listener");
    await extension.awaitMessage("storage-local-add-listener:done");

    extension.sendMessage("storage-local-set", TEST_VALUE_UPDATED);
    await extension.awaitMessage("storage-local-set:done");

    await TestUtils.waitForCondition(async () => {
      const res = await extensionStorage.getStoreObjects(host);
      return res.data?.length > 0;
    }, "Wait for extension storage panel to update");

    data = (await extensionStorage.getStoreObjects(host)).data;
    Assert.deepEqual(
      data,
      [
        {
          area: "local",
          name: "testKey",
          value: { str: JSON.stringify(TEST_VALUE_UPDATED) },
          isValueEditable: true,
        },
      ],
      "Storage actor received the change"
    );

    extension.sendMessage("get-onChanged-calls");
    const onChangedCalls = await extension.awaitMessage(
      "get-onChanged-calls:done"
    );

    Assert.deepEqual(
      onChangedCalls,
      [
        {
          testKey: {
            oldValue: TEST_VALUE_INITIAL,
            newValue: TEST_VALUE_UPDATED,
          },
        },
      ],
      "Extension onChanged listener was called for the expected changes"
    );

    await shutdown(extension, commands);
  }
);
