// A collection of helper functions that make use of the `remoteContextHelper`
// to test BFCache support and behavior.

// Call `prepareForBFCache()` before navigating away from the page. This simply
// sets a variable in window.
async function prepareForBFCache(remoteContextHelper) {
  await remoteContextHelper.executeScript(() => {
    window.beforeBFCache = true;
  });
}

// Call `getBeforeCache()` after navigating back to the page. This returns the
// value in window.
async function getBeforeBFCache(remoteContextHelper) {
  return await remoteContextHelper.executeScript(() => {
    return window.beforeBFCache;
  });
}

// If the value in window is set to true, this means that the page was reloaded,
// i.e., the page was restored from BFCache.
// Call `prepareForBFCache()` before navigating away to call this function.
async function assertImplementsBFCacheOptional(remoteContextHelper) {
  var beforeBFCache = await getBeforeBFCache(remoteContextHelper);
  assert_implements_optional(beforeBFCache == true, 'BFCache not supported.');
}

// Subtracts set `b` from set `a` and returns the result.
function setMinus(a, b) {
  const minus = new Set();
  a.forEach(e => {
    if (!b.has(e)) {
      minus.add(e);
    }
  });
  return minus;
}

// Return a sorted Array from the iterable `s`.
function sorted(s) {
  return Array.from(s).sort();
}

// Assert expected reasons are all present. Note that the extra reasons are allowed
// as UAs might block bfcache for their specific reasons.
function matchReasons(expectedNotRestoredReasonsSet, notRestoredReasonsSet) {
  const missing = setMinus(
    expectedNotRestoredReasonsSet, notRestoredReasonsSet, 'Missing reasons');
  const extra = setMinus(
      notRestoredReasonsSet, expectedNotRestoredReasonsSet, 'Extra reasons');
  assert_true(missing.size == 0, `Expected: ${sorted(expectedNotRestoredReasonsSet)}\n` +
    `Got: ${sorted(notRestoredReasonsSet)}\n` +
    `Missing: ${sorted(missing)}\n` +
    `Extra: ${sorted(extra)}\n`);
}

// This function takes a set of reasons and extracts reasons out of it and returns a set of strings.
// For example, if the input is [{"reason": "error-document"}, {"reason": "masked"}],
// the output is ["error-document", "masked"].
function extractReason(reasonSet) {
  let reasonsExtracted = new Set();
  for (let reason of reasonSet) {
    reasonsExtracted.add(reason.reason);
  }
  return reasonsExtracted;
}

// A helper function to assert that the page is not restored from BFCache by
// checking whether the `beforeBFCache` value from `window` is undefined
// due to page reload.
// This function also takes an optional `notRestoredReasons` list which
// indicates the set of expected reasons that make the page not restored.
// If the reasons list is undefined, the check will be skipped. Otherwise
// this check will use the `notRestoredReasons` API, to obtain the reasons
// in a tree structure, and flatten the reasons before making the order-
// insensitive comparison.
// If the API is not available, the function will terminate instead of marking
// the assertion failed.
// Call `prepareForBFCache()` before navigating away to call this function.
async function assertNotRestoredFromBFCache(
    remoteContextHelper, notRestoredReasons) {
  var beforeBFCache = await getBeforeBFCache(remoteContextHelper);
  assert_equals(beforeBFCache, undefined, 'document unexpectedly BFCached');

  // The reason is optional, so skip the remaining test if the
  // `notRestoredReasons` is not set.
  if (notRestoredReasons === undefined) {
    return;
  }

  let isFeatureEnabled = await remoteContextHelper.executeScript(() => {
    return 'notRestoredReasons' in
        performance.getEntriesByType('navigation')[0];
  });

  // Return if the `notRestoredReasons` API is not available.
  if (!isFeatureEnabled) {
    return;
  }

  let result = await remoteContextHelper.executeScript(() => {
    return performance.getEntriesByType('navigation')[0].notRestoredReasons;
  });

  let expectedNotRestoredReasonsSet = new Set(notRestoredReasons);
  let notRestoredReasonsSet = new Set();

  // Flatten the reasons from the main frame and all the child frames.
  const collectReason = (node) => {
    for (let reason of node.reasons) {
      notRestoredReasonsSet.add(reason.reason);
    }
    for (let child of node.children) {
      collectReason(child);
    }
  };
  collectReason(result);
  matchReasons(expectedNotRestoredReasonsSet, notRestoredReasonsSet);
}

// A helper function that combines the steps of setting window property,
// navigating away and back, and making assertion on whether BFCache is
// supported.
// This function can be used to check if the current page is eligible for
// BFCache.
async function assertBFCacheEligibility(
    remoteContextHelper, shouldRestoreFromBFCache) {
  await prepareForBFCache(remoteContextHelper);
  // Navigate away and back.
  const newRemoteContextHelper = await remoteContextHelper.navigateToNew();
  await newRemoteContextHelper.historyBack();

  if (shouldRestoreFromBFCache) {
    await assertImplementsBFCacheOptional(remoteContextHelper);
  } else {
    await assertNotRestoredFromBFCache(remoteContextHelper);
  }
}
