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

const interpretChar = (chars, index) => {
  return chars.charCodeAt(index).toString(16).padStart(4, "0");
};

const hexEncode = str => {
  let result = "";
  const len = str.length;
  for (let i = 0; i < len; ++i) {
    result += interpretChar(str, i);
  }
  return result;
};

const collectCorrupted = (expected, actual) => {
  const len = Math.min(expected.length, actual.length);
  let notEquals = [];
  for (let i = 0; i < len; ++i) {
    if (expected[i] !== actual[i]) {
      notEquals.push([hexEncode(expected[i]), hexEncode(actual[i])]);
    }
  }
  return notEquals;
};

const sanitizeOutputWithSurrogates = (testValue, prefix = "") => {
  let utf8What = prefix;
  for (let i = 0; i < testValue.length; ++i) {
    const valueChar = testValue.charCodeAt(i);
    const isPlanar = 0xd800 <= valueChar && valueChar <= 0xdfff;
    utf8What += isPlanar ? "\\u" + interpretChar(testValue, i) : testValue[i];
  }
  return utf8What;
};

const getEncodingSample = () => {
  const expectedSample =
    "3681207208613504e0a5028800b945551988c60050008027ebc2808c00d38e806e03d8210ac906722b85499be9d00000";

  let result = "";
  const len = expectedSample.length;
  for (let i = 0; i < len; i += 4) {
    result += String.fromCharCode(parseInt(expectedSample.slice(i, i + 4), 16));
  }
  return result;
};

const getSeparatedBasePlane = () => {
  let result = "";
  for (let i = 0xffff; i >= 0; --i) {
    result += String.fromCharCode(i) + "\n";
  }
  return result;
};

const getJoinedBasePlane = () => {
  let result = "";
  for (let i = 0; i <= 0xffff; ++i) {
    result += String.fromCharCode(i);
  }
  return result;
};

const getSurrogateCombinations = () => {
  const upperLead = String.fromCharCode(0xdbff);
  const lowerTrail = String.fromCharCode(0xdc00);

  const regularSlot = ["w", "abcdefghijklmnopqrst", "aaaaaaaaaaaaaaaaaaaa", ""];
  const surrogateSlot = [lowerTrail, upperLead];

  let samples = [];
  for (const leadSnippet of regularSlot) {
    for (const firstSlot of surrogateSlot) {
      for (const trailSnippet of regularSlot) {
        for (const secondSlot of surrogateSlot) {
          samples.push(leadSnippet + firstSlot + secondSlot + trailSnippet);
        }
        samples.push(leadSnippet + firstSlot + trailSnippet);
      }
    }
  }

  return samples;
};

const fetchFrom = async (itemKey, sample, meanwhile) => {
  const principal = getPrincipal("http://example.com/", {});

  let request = clearOrigin(principal);
  await requestFinished(request);

  const storage = getLocalStorage(principal);

  await storage.setItem(itemKey, sample);

  await meanwhile(principal);

  return storage.getItem(itemKey);
};

/**
 * Value fetched from existing snapshot based on
 * existing in-memory datastore in the parent process
 * without any communication between content/parent
 */
const fetchFromExistingSnapshotExistingDatastore = async (itemKey, sample) => {
  return fetchFrom(itemKey, sample, async () => {});
};

/**
 * Value fetched from newly created snapshot based on
 * existing in-memory datastore in the parent process
 */
const fetchFromNewSnapshotExistingDatastore = async (itemKey, sample) => {
  return fetchFrom(itemKey, sample, async () => {
    await returnToEventLoop();
  });
};

/**
 * Value fetched from newly created snapshot based on newly created
 * in-memory datastore based on database in the parent process
 */
const fetchFromNewSnapshotNewDatastore = async (itemKey, sample) => {
  return fetchFrom(itemKey, sample, async principal => {
    let request = resetClient(principal);
    await requestFinished(request);
  });
};

add_task(async function testSteps() {
  /* This test is based on bug 1681300 */
  Services.prefs.setBoolPref(
    "dom.storage.enable_unsupported_legacy_implementation",
    false
  );
  Services.prefs.setBoolPref("dom.storage.snapshot_reusing", false);

  const reportWhat = (testKey, testValue) => {
    if (testKey.length + testValue.length > 82) {
      return testKey;
    }
    return sanitizeOutputWithSurrogates(testValue, /* prefix */ testKey + ":");
  };

  const testFetchMode = async (testType, storeAndLookup) => {
    const testPairs = [
      { testEmptyValue: [""] },
      { testSampleKey: [getEncodingSample()] },
      { testSeparatedKey: [getSeparatedBasePlane()] },
      { testJoinedKey: [getJoinedBasePlane()] },
      { testCombinations: getSurrogateCombinations() },
    ];

    for (const testPair of testPairs) {
      for (const [testKey, expectedValues] of Object.entries(testPair)) {
        for (const expected of expectedValues) {
          const actual = await storeAndLookup(testKey, expected);
          const testInfo = reportWhat(testKey, expected);
          is(
            null != actual,
            true,
            testType + ": Value not null for " + testInfo
          );
          is(
            expected.length,
            actual.length,
            testType + ": Returned size for " + testInfo
          );

          const notEquals = collectCorrupted(expected, actual);
          for (let i = 0; i < notEquals.length; ++i) {
            is(
              notEquals[i][0],
              notEquals[i][1],
              testType + ": Unequal character at " + i + " for " + testInfo
            );
          }
        }
      }
    }
  };

  await testFetchMode(
    "ExistingSnapshotExistingDatastore",
    fetchFromExistingSnapshotExistingDatastore
  );

  await testFetchMode(
    "NewSnapshotExistingDatastore",
    fetchFromNewSnapshotExistingDatastore
  );

  await testFetchMode(
    "NewSnapshotNewDatastore",
    fetchFromNewSnapshotNewDatastore
  );
});
