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

"use strict";

const BASE_DOMAIN_A = "example.com";
const ORIGIN_A = `https://${BASE_DOMAIN_A}`;
const ORIGIN_A_HTTP = `http://${BASE_DOMAIN_A}`;
const ORIGIN_A_SUB = `https://test1.${BASE_DOMAIN_A}`;

const BASE_DOMAIN_B = "example.org";
const ORIGIN_B = `https://${BASE_DOMAIN_B}`;
const ORIGIN_B_HTTP = `http://${BASE_DOMAIN_B}`;
const ORIGIN_B_SUB = `https://test1.${BASE_DOMAIN_B}`;

const TEST_ROOT_DIR = getRootDirectory(gTestPath);

// Session storage is only valid for the lifetime of a tab, so we need to keep
// tabs open for the duration of a test.
let originToTabs = {};

function getTestURLForOrigin(origin) {
  return TEST_ROOT_DIR.replace("chrome://mochitests/content", origin);
}

function getTestEntryName(origin, partitioned) {
  return `${origin}${partitioned ? "_partitioned" : ""}`;
}

async function testHasEntry(
  originFirstParty,
  isSet,
  originThirdParty,
  originAttributes = { userContextId: 0 }
) {
  // Get all matching tabs for this origin
  let tabs = originToTabs[originFirstParty];
  if (!tabs) {
    return;
  }

  // Filter tabs by userContextId if specified
  let relevantTabs = tabs;
  if (originAttributes.userContextId !== undefined) {
    relevantTabs = tabs.filter(
      tab => tab.userContextId === originAttributes.userContextId
    );
  }

  for (let tab of relevantTabs) {
    // For the partition test we inspect the sessionStorage of the iframe.
    let browsingContext = originThirdParty
      ? tab.linkedBrowser.browsingContext.children[0]
      : tab.linkedBrowser.browsingContext;
    let actualSet = await SpecialPowers.spawn(
      browsingContext,
      [
        getTestEntryName(
          originThirdParty || originFirstParty,
          !!originThirdParty
        ),
      ],
      key => {
        return !!content.sessionStorage.getItem(key);
      }
    );

    let msg = `${originFirstParty}${isSet ? " " : " not "}set`;
    if (originThirdParty) {
      msg = "Partitioned under " + msg;
    }
    if (tab.userContextId) {
      msg += ` (userContextId: ${tab.userContextId})`;
    }

    is(actualSet, isSet, msg);
  }
}

/**
 * Creates tabs and sets sessionStorage entries in first party and third party
 * context.
 *
 * @returns {Promise} - Promise which resolves once all tabs are initialized,
 * {@link originToTabs} is populated and (sub-)resources have loaded.
 */
function addTestTabs() {
  let promises = [
    [ORIGIN_A, ORIGIN_B],
    [ORIGIN_A_SUB, ORIGIN_B_SUB],
    [ORIGIN_A_HTTP, ORIGIN_B_HTTP],
    [ORIGIN_B, ORIGIN_A],
    [ORIGIN_B_SUB, ORIGIN_A_SUB],
    [ORIGIN_B_HTTP, ORIGIN_A_HTTP],
    [ORIGIN_A, ORIGIN_B, 1],
  ].map(async ([firstParty, thirdParty, userContextId]) => {
    info(
      `Creating new tab for ${firstParty}, userContextId: ${userContextId ?? 0}`
    );
    let tab = BrowserTestUtils.addTab(gBrowser, firstParty, { userContextId });
    await BrowserTestUtils.browserLoaded(tab.linkedBrowser);

    info(`Creating iframe for ${thirdParty}`);
    await SpecialPowers.spawn(
      tab.linkedBrowser,
      [getTestEntryName(firstParty, false), thirdParty],
      async (storageKey, url) => {
        // Set unpartitioned sessionStorage for firstParty.
        content.sessionStorage.setItem(storageKey, "foo");

        let iframe = content.document.createElement("iframe");
        iframe.src = url;

        let loadPromise = ContentTaskUtils.waitForEvent(iframe, "load", false);
        content.document.body.appendChild(iframe);
        await loadPromise;
      }
    );

    await SpecialPowers.spawn(
      tab.linkedBrowser.browsingContext.children[0],
      [getTestEntryName(thirdParty, true)],
      async storageKey => {
        // Set sessionStorage in partitioned third-party iframe.
        content.sessionStorage.setItem(storageKey, "foo");
      }
    );

    let tabs = originToTabs[firstParty];
    if (!tabs) {
      tabs = [];
      originToTabs[firstParty] = tabs;
    }
    tabs.push(tab);
  });

  return Promise.all(promises);
}

function cleanup() {
  Object.values(originToTabs).flat().forEach(BrowserTestUtils.removeTab);
  originToTabs = {};
  Services.obs.notifyObservers(null, "browser:purge-sessionStorage");
}

add_setup(async function () {
  await SpecialPowers.pushPrefEnv({
    set: [["network.cookie.cookieBehavior", 5]],
  });
  cleanup();
});

add_task(async function test_deleteDataFromSite() {
  await addTestTabs();

  info("Clearing sessionStorage for base domain A " + BASE_DOMAIN_A);
  await new Promise(resolve => {
    Services.clearData.deleteDataFromSite(
      BASE_DOMAIN_A,
      {},
      false,
      Ci.nsIClearDataService.CLEAR_DOM_QUOTA,
      resolve
    );
  });

  info("All entries for A should have been cleared.");
  await testHasEntry(ORIGIN_A, false);
  await testHasEntry(ORIGIN_A, false, null, { userContextId: 1 });
  await testHasEntry(ORIGIN_A_SUB, false);
  await testHasEntry(ORIGIN_A_HTTP, false);

  info("Entries for B should still exist.");
  await testHasEntry(ORIGIN_B, true);
  await testHasEntry(ORIGIN_B_SUB, true);
  await testHasEntry(ORIGIN_B_HTTP, true);

  info("All partitioned entries for B under A should have been cleared.");
  await testHasEntry(ORIGIN_A, false, ORIGIN_B);
  await testHasEntry(ORIGIN_A, false, ORIGIN_B, { userContextId: 1 });
  await testHasEntry(ORIGIN_A_SUB, false, ORIGIN_B_SUB);
  await testHasEntry(ORIGIN_A_HTTP, false, ORIGIN_B_HTTP);

  info("All partitioned entries of A under B should have been cleared.");
  await testHasEntry(ORIGIN_B, false, ORIGIN_A);
  await testHasEntry(ORIGIN_B, false, ORIGIN_A, { userContextId: 1 });
  await testHasEntry(ORIGIN_B_SUB, false, ORIGIN_A_SUB);
  await testHasEntry(ORIGIN_B_HTTP, false, ORIGIN_A_HTTP);

  cleanup();
});

add_task(async function test_deleteDataFromSiteAndPattern() {
  await addTestTabs();

  let pattern = {
    userContextId: 1,
  };

  info(
    `Clearing sessionStorage for base domain A ${BASE_DOMAIN_A} with pattern ${JSON.stringify(pattern)}.`
  );
  await new Promise(resolve => {
    Services.clearData.deleteDataFromSite(
      BASE_DOMAIN_A,
      pattern,
      false,
      Ci.nsIClearDataService.CLEAR_DOM_QUOTA,
      resolve
    );
  });

  info("Only entries for A that match the pattern should have been cleared.");
  await testHasEntry(ORIGIN_A, true);
  await testHasEntry(ORIGIN_A, false, null, { userContextId: 1 });
  await testHasEntry(ORIGIN_A_SUB, true);
  await testHasEntry(ORIGIN_A_HTTP, true);

  info("Entries for B should still exist.");
  await testHasEntry(ORIGIN_B, true);
  await testHasEntry(ORIGIN_B_SUB, true);
  await testHasEntry(ORIGIN_B_HTTP, true);

  info(
    "All partitioned entries for B under A  which match the pattern should have been cleared."
  );
  await testHasEntry(ORIGIN_A, true, ORIGIN_B);
  await testHasEntry(ORIGIN_A, false, ORIGIN_B, { userContextId: 1 });
  await testHasEntry(ORIGIN_A_SUB, true, ORIGIN_B_SUB);
  await testHasEntry(ORIGIN_A_HTTP, true, ORIGIN_B_HTTP);

  info(
    "All partitioned entries of A under B which match the pattern should have been cleared."
  );
  await testHasEntry(ORIGIN_B, true, ORIGIN_A);
  await testHasEntry(ORIGIN_B, false, ORIGIN_A, { userContextId: 1 });
  await testHasEntry(ORIGIN_B_SUB, true, ORIGIN_A_SUB);
  await testHasEntry(ORIGIN_B_HTTP, true, ORIGIN_A_HTTP);

  cleanup();
});

add_task(async function test_deleteAll() {
  await addTestTabs();

  info("Clearing sessionStorage for base domain A " + BASE_DOMAIN_A);
  await new Promise(resolve => {
    Services.clearData.deleteData(
      Ci.nsIClearDataService.CLEAR_DOM_QUOTA,
      resolve
    );
  });

  info("All entries should have been cleared.");
  await testHasEntry(ORIGIN_A, false);
  await testHasEntry(ORIGIN_A_SUB, false);
  await testHasEntry(ORIGIN_A_HTTP, false);
  await testHasEntry(ORIGIN_B, false);
  await testHasEntry(ORIGIN_B_SUB, false);
  await testHasEntry(ORIGIN_B_HTTP, false);

  info("All partitioned entries should have been cleared.");
  await testHasEntry(ORIGIN_A, false, ORIGIN_B);
  await testHasEntry(ORIGIN_A_SUB, false, ORIGIN_B_SUB);
  await testHasEntry(ORIGIN_A_HTTP, false, ORIGIN_B_HTTP);
  await testHasEntry(ORIGIN_B, false, ORIGIN_A);
  await testHasEntry(ORIGIN_B_SUB, false, ORIGIN_A_SUB);
  await testHasEntry(ORIGIN_B_HTTP, false, ORIGIN_A_HTTP);

  cleanup();
});

add_task(async function test_deleteFromPrincipal() {
  await addTestTabs();

  info("Clearing sessionStorage for partitioned principal A " + BASE_DOMAIN_A);

  let principalA = Services.scriptSecurityManager.createContentPrincipal(
    Services.io.newURI(ORIGIN_A),
    { partitionKey: `(https,${BASE_DOMAIN_B})` }
  );

  info("principal: " + principalA.origin);
  info("principal partitionKey " + principalA.originAttributes.partitionKey);
  await new Promise(resolve => {
    Services.clearData.deleteDataFromPrincipal(
      principalA,
      false,
      Ci.nsIClearDataService.CLEAR_DOM_QUOTA,
      resolve
    );
  });

  info("Unpartitioned entries should still exist.");
  await testHasEntry(ORIGIN_A, true);
  await testHasEntry(ORIGIN_A_SUB, true);
  await testHasEntry(ORIGIN_A_HTTP, true);
  await testHasEntry(ORIGIN_B, true);
  await testHasEntry(ORIGIN_B_SUB, true);
  await testHasEntry(ORIGIN_B_HTTP, true);

  info("Only entries of principal should have been cleared.");
  await testHasEntry(ORIGIN_A, true, ORIGIN_B);
  await testHasEntry(ORIGIN_A_SUB, true, ORIGIN_B_SUB);
  await testHasEntry(ORIGIN_A_HTTP, true, ORIGIN_B_HTTP);

  await testHasEntry(ORIGIN_B, false, ORIGIN_A);

  await testHasEntry(ORIGIN_B_SUB, true, ORIGIN_A_SUB);
  await testHasEntry(ORIGIN_B_HTTP, true, ORIGIN_A_HTTP);

  cleanup();
});
