const HELPER_PAGE_URL =
  "https://example.com/browser/dom/tests/browser/page_localstorage.html";
const HELPER_PAGE_COOP_COEP_URL =
  "https://example.com/browser/dom/tests/browser/page_localstorage_coop+coep.html";
const HELPER_PAGE_ORIGIN = "https://example.com/";

let testDir = gTestPath.substr(0, gTestPath.lastIndexOf("/"));
Services.scriptloader.loadSubScript(testDir + "/helper_localStorage.js", this);

/* import-globals-from helper_localStorage.js */

// We spin up a ton of child processes.
requestLongerTimeout(4);

/**
 * Verify the basics of our multi-e10s localStorage support with fission.
 * We are focused on whitebox testing two things.
 * When this is being written, broadcast filtering is not in place, but the test
 * is intended to attempt to verify that its implementation does not break things.
 *
 * 1) That pages see the same localStorage state in a timely fashion when
 *    engaging in non-conflicting operations.  We are not testing races or
 *    conflict resolution; the spec does not cover that.
 *
 * 2) That there are no edge-cases related to when the Storage instance is
 *    created for the page or the StorageCache for the origin.  (StorageCache is
 *    what actually backs the Storage binding exposed to the page.)  This
 *    matters because the following reasons can exist for them to be created:
 *    - Preload, on the basis of knowing the origin uses localStorage.  The
 *      interesting edge case is when we have the same origin open in different
 *      processes and the origin starts using localStorage when it did not
 *      before.  Preload will not have instantiated bindings, which could impact
 *      correctness.
 *    - The page accessing localStorage for read or write purposes.  This is the
 *      obvious, boring one.
 *    - The page adding a "storage" listener.  This is less obvious and
 *      interacts with the preload edge-case mentioned above.  The page needs to
 *      hear "storage" events even if the page has not touched localStorage
 *      itself and its origin had nothing stored in localStorage when the page
 *      was created.
 *
 * According to current fission implementation, same origin pages will be loaded
 * by the same process, which process type is webIsolated=. And thanks to
 * Cross-Origin-Opener-Policy and Cross-Origin-Embedder-Policy headers support,
 * it is possible to load the same origin page by a special process, which type
 * is webCOOP+COEP=. These are the only two processes can be used to test
 * localStroage consistency between tabs in different tabs.
 *
 * We use the two child pages for testing, page_localstorage.html and
 * page_localstorage_coop+coep.html. Their content are the same, but
 * page_localstorage_coop+coep.html will be loaded with its ^headers^ file.
 * These pages provide followings
 * - can be instructed to listen for and record "storage" events
 * - can be instructed to issue a series of localStorage writes
 * - can be instructed to return the current entire localStorage contents
 *
 * To test localStorage consistency, four subtests are used.
 * Test case 1: one writer tab and one reader tab
 *   The writer tab issues a series of write operations, then verify the
 *   localStorage contents from the reader tab.
 *
 * Test case 2: one writer tab and one listener tab
 *   The writer tab issues a series of write operations, then verify the recorded
 *   storage events from the listener tab.
 *
 * Test case 3: one writeThenRead tab and one readThenWrite tab
 *   The writeThenRead first issues a series write of operations, and then verify
 *   the recorded storage events and localStorage contents from readThenWrite
 *   tab. After that readThenWrite tab issues a series of write operations, then
 *   verify the results from writeThenRead tab.
 *
 * Test case 4: one writer tab and one lateOpenSeesPreload tab
 *   The writer tab issues a series write of operations. Then open the
 *   lateOpenSeesPreload tab to make sure preloads exists.
 */

/**
 * Shared constants for test cases
 */
const noSentinelCheck = null;
const initialSentinel = "initial";
const initialWriteMutations = [
  // [key (null=clear), newValue (null=delete), oldValue (verification)]
  ["getsCleared", "1", null],
  ["alsoGetsCleared", "2", null],
  [null, null, null],
  ["stays", "3", null],
  ["clobbered", "pre", null],
  ["getsDeletedLater", "4", null],
  ["getsDeletedImmediately", "5", null],
  ["getsDeletedImmediately", null, "5"],
  ["alsoStays", "6", null],
  ["getsDeletedLater", null, "4"],
  ["clobbered", "post", "pre"],
];
const initialWriteState = {
  stays: "3",
  clobbered: "post",
  alsoStays: "6",
};

const lastWriteSentinel = "lastWrite";
const lastWriteMutations = [
  ["lastStays", "20", null],
  ["lastDeleted", "21", null],
  ["lastClobbered", "lastPre", null],
  ["lastClobbered", "lastPost", "lastPre"],
  ["lastDeleted", null, "21"],
];
const lastWriteState = Object.assign({}, initialWriteState, {
  lastStays: "20",
  lastClobbered: "lastPost",
});

/**
 * Test case 1: one writer tab and one reader tab
 * Test steps
 *   1. Clear origin storage to make sure no data and preloads.
 *   2. Open the writer and reader tabs and verify preloads do not exist.
 *      Open writer tab in webIsolated= process
 *      Open reader tab in webCOOP+COEP= process
 *   3. Issue a series write operations in the writer tab, and then verify the
 *      storage state on the tab.
 *   4. Verify the storage state on the reader tab.
 *   5. Issue another series write operations in the writer tab, and then verify
 *      the storage state on the tab.
 *   6. Verify the storage state on the reader tab.
 *   7. Close tabs and clear origin storage.
 */
add_task(async function () {
  if (!Services.domStorageManager.nextGenLocalStorageEnabled) {
    ok(true, "Test ignored when the next gen local storage is not enabled.");
    return;
  }

  await SpecialPowers.pushPrefEnv({
    set: [
      // Stop the preallocated process manager from speculatively creating
      // processes.  Our test explicitly asserts on whether preload happened or
      // not for each tab's process.  This information is loaded and latched by
      // the StorageDBParent constructor which the child process's
      // LocalStorageManager() constructor causes to be created via a call to
      // LocalStorageCache::StartDatabase().  Although the service is lazily
      // created and should not have been created prior to our opening the tab,
      // it's safest to ensure the process simply didn't exist before we ask for
      // it.
      //
      // This is done in conjunction with our use of forceNewProcess when
      // opening tabs.  There would be no point if we weren't also requesting a
      // new process.
      ["dom.ipc.processPrelaunch.enabled", false],
      // Enable LocalStorage's testing API so we can explicitly trigger a flush
      // when needed.
      ["dom.storage.testing", true],
    ],
  });

  // Ensure that there is no localstorage data or potential false positives for
  // localstorage preloads by forcing the origin to be cleared prior to the
  // start of our test.
  await clearOriginStorageEnsuringNoPreload(HELPER_PAGE_ORIGIN);

  // Make sure mOriginsHavingData gets updated.
  await triggerAndWaitForLocalStorageFlush();

  // - Open tabs.  Don't configure any of them yet.
  const knownTabs = new KnownTabs();
  const writerTab = await openTestTab(
    HELPER_PAGE_URL,
    "writer",
    knownTabs,
    true
  );
  const readerTab = await openTestTab(
    HELPER_PAGE_COOP_COEP_URL,
    "reader",
    knownTabs,
    true
  );
  // Sanity check that preloading did not occur in the tabs.
  await verifyTabPreload(writerTab, false, HELPER_PAGE_ORIGIN);
  await verifyTabPreload(readerTab, false, HELPER_PAGE_ORIGIN);

  // - Issue the initial batch of writes and verify.
  info("initial writes");
  await mutateTabStorage(writerTab, initialWriteMutations, initialSentinel);

  // We expect the writer tab to have the correct state because it just did the
  // writes.  We do not perform a sentinel-check because the writes should be
  // locally available and consistent.
  await verifyTabStorageState(writerTab, initialWriteState, noSentinelCheck);
  // We expect the reader tab to retrieve the current localStorage state from
  // the database.
  await verifyTabStorageState(readerTab, initialWriteState, initialSentinel);

  // - Issue last set of writes from writerTab.
  info("last set of writes");
  await mutateTabStorage(writerTab, lastWriteMutations, lastWriteSentinel);

  // The writer performed the writes, no need to wait for the sentinel.
  await verifyTabStorageState(writerTab, lastWriteState, noSentinelCheck);
  // We need to wait for the sentinel to show up for the reader.
  await verifyTabStorageState(readerTab, lastWriteState, lastWriteSentinel);

  // - Clean up.
  await cleanupTabs(knownTabs);

  clearOriginStorageEnsuringNoPreload(HELPER_PAGE_ORIGIN);
});

/**
 * Test case 2: one writer tab and one linsener tab
 * Test steps
 *   1. Clear origin storage to make sure no data and preloads.
 *   2. Open the writer and listener tabs and verify preloads do not exist.
 *      Open writer tab in webIsolated= process
 *      Open listener tab in webCOOP+COEP= process
 *   3. Ask the listener tab to listen and record storage events.
 *   4. Issue a series write operations in the writer tab, and then verify the
 *      storage state on the tab.
 *   5. Verify the storage events record from the listener tab is as expected.
 *   6. Verify the storage state on the listener tab.
 *   7. Ask the listener tab to listen and record storage events.
 *   8. Issue another series write operations in the writer tab, and then verify
 *      the storage state on the tab.
 *   9. Verify the storage events record from the listener tab is as expected.
 *   10. Verify the storage state on the listener tab.
 *   11. Close tabs and clear origin storage.
 */
add_task(async function () {
  if (!Services.domStorageManager.nextGenLocalStorageEnabled) {
    ok(true, "Test ignored when the next gen local storage is not enabled.");
    return;
  }

  await SpecialPowers.pushPrefEnv({
    set: [
      ["dom.ipc.processPrelaunch.enabled", false],
      ["dom.storage.testing", true],
    ],
  });

  // Ensure that there is no localstorage data or potential false positives for
  // localstorage preloads by forcing the origin to be cleared prior to the
  // start of our test.
  await clearOriginStorageEnsuringNoPreload(HELPER_PAGE_ORIGIN);

  // Make sure mOriginsHavingData gets updated.
  await triggerAndWaitForLocalStorageFlush();

  // - Open tabs.  Don't configure any of them yet.
  const knownTabs = new KnownTabs();
  const writerTab = await openTestTab(
    HELPER_PAGE_URL,
    "writer",
    knownTabs,
    true
  );
  const listenerTab = await openTestTab(
    HELPER_PAGE_COOP_COEP_URL,
    "listener",
    knownTabs,
    true
  );
  // Sanity check that preloading did not occur in the tabs.
  await verifyTabPreload(writerTab, false, HELPER_PAGE_ORIGIN);
  await verifyTabPreload(listenerTab, false, HELPER_PAGE_ORIGIN);

  // - Ask the listener tab to listen and record the storage events..
  await recordTabStorageEvents(listenerTab, initialSentinel);

  // - Issue the initial batch of writes and verify.
  info("initial writes");
  await mutateTabStorage(writerTab, initialWriteMutations, initialSentinel);

  // We expect the writer tab to have the correct state because it just did the
  // writes.  We do not perform a sentinel-check because the writes should be
  // locally available and consistent.
  await verifyTabStorageState(writerTab, initialWriteState, noSentinelCheck);
  // We expect the listener tab to have heard all events despite preload not
  // having occurred and despite not issuing any reads or writes itself.  We
  // intentionally check the events before the state because we're most
  // interested in adding the listener having had a side-effect of subscribing
  // to changes for the process.
  //
  // We ensure it had a chance to hear all of the events because we told
  // recordTabStorageEvents to listen for the given sentinel.  The state check
  // then does not need to do a sentinel check.
  await verifyTabStorageEvents(
    listenerTab,
    initialWriteMutations,
    initialSentinel
  );
  await verifyTabStorageState(listenerTab, initialWriteState, noSentinelCheck);

  // - Ask the listener tab to listen and record the storage events.
  await recordTabStorageEvents(listenerTab, lastWriteSentinel);

  // - Issue last set of writes from writerTab.
  info("last set of writes");
  await mutateTabStorage(writerTab, lastWriteMutations, lastWriteSentinel);

  // The writer performed the writes, no need to wait for the sentinel.
  await verifyTabStorageState(writerTab, lastWriteState, noSentinelCheck);
  // Wait for the sentinel event to be received, then check.
  await verifyTabStorageEvents(
    listenerTab,
    lastWriteMutations,
    lastWriteSentinel
  );
  await verifyTabStorageState(listenerTab, lastWriteState, noSentinelCheck);

  // - Clean up.
  await cleanupTabs(knownTabs);

  clearOriginStorageEnsuringNoPreload(HELPER_PAGE_ORIGIN);
});

/**
 * Test case 3: one writeThenRead tab and one readThenWrite tab
 * Test steps
 *   1. Clear origin storage to make sure no data and preloads.
 *   2. Open the writeThenRead and readThenWrite tabs and verify preloads do not
 *      exist.
 *      Open writeThenRead tab in webIsolated= process
 *      Open readThenWrite tab in webCOOP+COEP= process
 *   3. Ask the readThenWrite tab to listen and record storage events.
 *   4. Issue a series write operations in the writeThenRead tab, and then verify
 *      the storage state on the tab.
 *   5. Verify the storage events record from the readThenWrite tab is as
 *      expected.
 *   6. Verify the storage state on the readThenWrite tab.
 *   7. Ask the writeThenRead tab to listen and record storage events.
 *   8. Issue another series write operations in the readThenWrite tab, and then
 *      verify the storage state on the tab.
 *   9. Verify the storage events record from the writeThenRead tab is as
 *      expected.
 *   10. Verify the storage state on the writeThenRead tab.
 *   11. Close tabs and clear origin storage.
 **/
add_task(async function () {
  if (!Services.domStorageManager.nextGenLocalStorageEnabled) {
    ok(true, "Test ignored when the next gen local storage is not enabled.");
    return;
  }

  await SpecialPowers.pushPrefEnv({
    set: [
      ["dom.ipc.processPrelaunch.enabled", false],
      ["dom.storage.testing", true],
    ],
  });

  // Ensure that there is no localstorage data or potential false positives for
  // localstorage preloads by forcing the origin to be cleared prior to the
  // start of our test.
  await clearOriginStorageEnsuringNoPreload(HELPER_PAGE_ORIGIN);

  // Make sure mOriginsHavingData gets updated.
  await triggerAndWaitForLocalStorageFlush();

  // - Open tabs.  Don't configure any of them yet.
  const knownTabs = new KnownTabs();
  const writeThenReadTab = await openTestTab(
    HELPER_PAGE_URL,
    "writerthenread",
    knownTabs,
    true
  );
  const readThenWriteTab = await openTestTab(
    HELPER_PAGE_COOP_COEP_URL,
    "readthenwrite",
    knownTabs,
    true
  );
  // Sanity check that preloading did not occur in the tabs.
  await verifyTabPreload(writeThenReadTab, false, HELPER_PAGE_ORIGIN);
  await verifyTabPreload(readThenWriteTab, false, HELPER_PAGE_ORIGIN);

  // - Ask readThenWrite tab to listen and record storageEvents.
  await recordTabStorageEvents(readThenWriteTab, initialSentinel);

  // - Issue the initial batch of writes and verify.
  info("initial writes");
  await mutateTabStorage(
    writeThenReadTab,
    initialWriteMutations,
    initialSentinel
  );

  // We expect the writer tab to have the correct state because it just did the
  // writes.  We do not perform a sentinel-check because the writes should be
  // locally available and consistent.
  await verifyTabStorageState(
    writeThenReadTab,
    initialWriteState,
    noSentinelCheck
  );

  // We expect the listener tab to have heard all events despite preload not
  // having occurred and despite not issuing any reads or writes itself.  We
  // intentionally check the events before the state because we're most
  // interested in adding the listener having had a side-effect of subscribing
  // to changes for the process.
  //
  // We ensure it had a chance to hear all of the events because we told
  // recordTabStorageEvents to listen for the given sentinel.  The state check
  // then does not need to do a sentinel check.
  await verifyTabStorageEvents(
    readThenWriteTab,
    initialWriteMutations,
    initialSentinel
  );
  await verifyTabStorageState(
    readThenWriteTab,
    initialWriteState,
    noSentinelCheck
  );

  // - Issue last set of writes from writerTab.
  info("last set of writes");
  await recordTabStorageEvents(writeThenReadTab, lastWriteSentinel);

  await mutateTabStorage(
    readThenWriteTab,
    lastWriteMutations,
    lastWriteSentinel
  );

  // The writer performed the writes, no need to wait for the sentinel.
  await verifyTabStorageState(
    readThenWriteTab,
    lastWriteState,
    noSentinelCheck
  );
  // Wait for the sentinel event to be received, then check.
  await verifyTabStorageEvents(
    writeThenReadTab,
    lastWriteMutations,
    lastWriteSentinel
  );
  await verifyTabStorageState(
    writeThenReadTab,
    lastWriteState,
    noSentinelCheck
  );

  // - Clean up.
  await cleanupTabs(knownTabs);

  clearOriginStorageEnsuringNoPreload(HELPER_PAGE_ORIGIN);
});

/**
 * Test case 4: one writerRead tab and one lateOpenSeesPreload tab
 * Test steps
 *   1. Clear origin storage to make sure no data and preloads.
 *   2. Open the writer tab and verify preloads do not exist.
 *      Open writer tab in webIsolated= process
 *   3. Issue a series write operations in the writer tab, and then verify the
 *      storage state on the tab.
 *   4. Issue another series write operations in the writer tab, and then verify
 *      the storage state on the tab.
 *   5. Open lateOpenSeesPreload tab in webCOOP+COEP process
 *   6. Verify the preloads on the lateOpenSeesPreload tab
 *   7. Close tabs and clear origin storage.
 */
add_task(async function () {
  await SpecialPowers.pushPrefEnv({
    set: [
      ["dom.ipc.processPrelaunch.enabled", false],
      ["dom.storage.testing", true],
    ],
  });

  // Ensure that there is no localstorage data or potential false positives for
  // localstorage preloads by forcing the origin to be cleared prior to the
  // start of our test.
  await clearOriginStorageEnsuringNoPreload(HELPER_PAGE_ORIGIN);

  // Make sure mOriginsHavingData gets updated.
  await triggerAndWaitForLocalStorageFlush();

  // - Open tabs.  Don't configure any of them yet.
  const knownTabs = new KnownTabs();
  const writerTab = await openTestTab(
    HELPER_PAGE_URL,
    "writer",
    knownTabs,
    true
  );
  // Sanity check that preloading did not occur in the tabs.
  await verifyTabPreload(writerTab, false, HELPER_PAGE_ORIGIN);

  // - Configure the tabs.

  // - Issue the initial batch of writes and verify.
  info("initial writes");
  await mutateTabStorage(writerTab, initialWriteMutations, initialSentinel);

  // We expect the writer tab to have the correct state because it just did the
  // writes.  We do not perform a sentinel-check because the writes should be
  // locally available and consistent.
  await verifyTabStorageState(writerTab, initialWriteState, noSentinelCheck);

  // - Force a LocalStorage DB flush so mOriginsHavingData is updated.
  // mOriginsHavingData is only updated when the storage thread runs its
  // accumulated operations during the flush.  If we don't initiate and ensure
  // that a flush has occurred before moving on to the next step,
  // mOriginsHavingData may not include our origin when it's sent down to the
  // child process.
  info("flush to make preload check work");
  await triggerAndWaitForLocalStorageFlush();

  // - Open a fresh tab and make sure it sees the precache/preload
  info("late open preload check");
  const lateOpenSeesPreload = await openTestTab(
    HELPER_PAGE_COOP_COEP_URL,
    "lateOpenSeesPreload",
    knownTabs,
    true
  );
  await verifyTabPreload(lateOpenSeesPreload, true, HELPER_PAGE_ORIGIN);

  // - Clean up.
  await cleanupTabs(knownTabs);

  clearOriginStorageEnsuringNoPreload(HELPER_PAGE_ORIGIN);
});
