/* vim: set ft=javascript ts=2 et sw=2 tw=80: */
/* Any copyright is dedicated to the Public Domain.
   http://creativecommons.org/publicdomain/zero/1.0/ */

"use strict";

/* import-globals-from storage-helpers.js */
Services.scriptloader.loadSubScript(
  "chrome://mochitests/content/browser/devtools/server/tests/browser/storage-helpers.js",
  this);

// beforeReload references an object representing the initialized state of the
// storage actor.
const beforeReload = {
  cookies: {
    "http://test1.example.org": ["c1", "cs2", "c3", "uc1"],
    "http://sectest1.example.org": ["uc1", "cs2"],
  },
  indexedDB: {
    "http://test1.example.org": [
      JSON.stringify(["idb1", "obj1"]),
      JSON.stringify(["idb1", "obj2"]),
      JSON.stringify(["idb2", "obj3"]),
    ],
    "http://sectest1.example.org": [],
  },
  localStorage: {
    "http://test1.example.org": ["ls1", "ls2"],
    "http://sectest1.example.org": ["iframe-u-ls1"],
  },
  sessionStorage: {
    "http://test1.example.org": ["ss1"],
    "http://sectest1.example.org": ["iframe-u-ss1", "iframe-u-ss2"],
  },
};

// afterIframeAdded references the items added when an iframe containing storage
// items is added to the page.
const afterIframeAdded = {
  cookies: {
    "https://sectest1.example.org": [
      getCookieId("cs2", ".example.org", "/"),
      getCookieId("sc1", "sectest1.example.org",
                  "/browser/devtools/server/tests/browser/"),
    ],
    "http://sectest1.example.org": [
      getCookieId("sc1", "sectest1.example.org",
                  "/browser/devtools/server/tests/browser/"),
    ],
  },
  indexedDB: {
    // empty because indexed db creation happens after the page load, so at
    // the time of window-ready, there was no indexed db present.
    "https://sectest1.example.org": [],
  },
  localStorage: {
    "https://sectest1.example.org": ["iframe-s-ls1"],
  },
  sessionStorage: {
    "https://sectest1.example.org": ["iframe-s-ss1"],
  },
};

// afterIframeRemoved references the items deleted when an iframe containing
// storage items is removed from the page.
const afterIframeRemoved = {
  cookies: {
    "http://sectest1.example.org": [],
  },
  indexedDB: {
    "http://sectest1.example.org": [],
  },
  localStorage: {
    "http://sectest1.example.org": [],
  },
  sessionStorage: {
    "http://sectest1.example.org": [],
  },
};

add_task(async function() {
  const { target, front } =
    await openTabAndSetupStorage(MAIN_DOMAIN + "storage-dynamic-windows.html");

  const data = await front.listStores();

  await testStores(data, front);

  await clearStorage();

  // Forcing GC/CC to get rid of docshells and windows created by this test.
  forceCollections();
  await target.destroy();
  forceCollections();
  DebuggerServer.destroy();
  forceCollections();
});

async function testStores(data, front) {
  testWindowsBeforeReload(data);

  await testAddIframe(front);
  await testRemoveIframe(front);
}

function testWindowsBeforeReload(data) {
  for (const storageType in beforeReload) {
    ok(data[storageType], `${storageType} storage actor is present`);

    // If this test is run with chrome debugging enabled we get an extra
    // key for "chrome". We don't want the test to fail in this case, so
    // ignore it.
    if (storageType == "indexedDB") {
      delete data[storageType].hosts.chrome;
    }

    is(Object.keys(data[storageType].hosts).length,
       Object.keys(beforeReload[storageType]).length,
        `Number of hosts for ${storageType} match`);
    for (const host in beforeReload[storageType]) {
      ok(data[storageType].hosts[host], `Host ${host} is present`);
    }
  }
}

async function testAddIframe(front) {
  info("Testing if new iframe addition works properly");

  const update = front.once("stores-update");

  await ContentTask.spawn(gBrowser.selectedBrowser, ALT_DOMAIN_SECURED,
    secured => {
      const doc = content.document;

      const iframe = doc.createElement("iframe");
      iframe.src = secured + "storage-secured-iframe.html";

      doc.querySelector("body").appendChild(iframe);
    }
  );

  const data = await update;

  validateStorage(data, afterIframeAdded, "added");
}

async function testRemoveIframe(front) {
  info("Testing if iframe removal works properly");

  const update = front.once("stores-update");

  await ContentTask.spawn(gBrowser.selectedBrowser, {}, () => {
    for (const iframe of content.document.querySelectorAll("iframe")) {
      if (iframe.src.startsWith("http:")) {
        iframe.remove();
        break;
      }
    }
  });

  const data = await update;

  validateStorage(data, afterIframeRemoved, "deleted");
}

function validateStorage(actual, expected, category = "") {
  if (category) {
    for (const cat of ["added", "changed", "deleted"]) {
      if (cat === category) {
        ok(actual[cat], `Data from the iframe has been ${cat}.`);
      } else {
        ok(!actual[cat], `No data was ${cat}.`);
      }
    }
  }

  for (const [type, expectedData] of Object.entries(expected)) {
    const actualData = category ? actual[category][type] : actual[type];

    ok(actualData, `${type} contains data.`);

    const actualKeys = Object.keys(actualData);
    const expectedKeys = Object.keys(expectedData);
    is(actualKeys.length, expectedKeys.length, `${type} data is the correct length.`);

    for (const [key, dataValues] of Object.entries(expectedData)) {
      ok(actualData[key], `${type} data contains the key ${key}.`);

      for (const dataValue of dataValues) {
        ok(actualData[key].includes(dataValue),
          `${type}[${key}] contains "${dataValue}".`);
      }
    }
  }
}
