/*
 * Bug 1264577 - A test case for testing caches of various submodules.
 *   This test case will load two pages that each page loads various resources
 *   within the same third party domain for the same originAttributes or different
 *   originAttributes. And then, it verifies the number of cache entries and
 *   the originAttributes of loading channels. If these two pages belong to
 *   the same originAttributes, the number of cache entries for a certain
 *   resource would be one. Otherwise, it would be two.
 */

const CC = Components.Constructor;

let protocolProxyService = Cc[
  "@mozilla.org/network/protocol-proxy-service;1"
].getService(Ci.nsIProtocolProxyService);

const TEST_DOMAIN = "http://example.net";
const TEST_PATH = "/browser/browser/components/originattributes/test/browser/";
const TEST_PAGE = TEST_DOMAIN + TEST_PATH + "file_cache.html";

let suffixes = [
  "iframe.html",
  "link.css",
  "script.js",
  "img.png",
  "object.png",
  "embed.png",
  "xhr.html",
  "worker.xhr.html",
  "audio.ogg",
  "video.webm",
  "track.vtt",
  "fetch.html",
  "worker.fetch.html",
  "request.html",
  "worker.request.html",
  "import.js",
  "worker.js",
  "sharedworker.js",
  "font.woff",
];

// A random value for isolating video/audio elements across different tests.
let randomSuffix;

function clearAllImageCaches() {
  let tools = SpecialPowers.Cc["@mozilla.org/image/tools;1"].getService(
    SpecialPowers.Ci.imgITools
  );
  let imageCache = tools.getImgCacheForDocument(window.document);
  imageCache.clearCache(true); // true=chrome
  imageCache.clearCache(false); // false=content
}

function cacheDataForContext(loadContextInfo) {
  return new Promise(resolve => {
    let cacheEntries = [];
    let cacheVisitor = {
      onCacheStorageInfo() {},
      onCacheEntryInfo(uri, idEnhance) {
        cacheEntries.push({ uri, idEnhance });
      },
      onCacheEntryVisitCompleted() {
        resolve(cacheEntries);
      },
      QueryInterface: ChromeUtils.generateQI(["nsICacheStorageVisitor"]),
    };
    // Visiting the disk cache also visits memory storage so we do not
    // need to use Services.cache2.memoryCacheStorage() here.
    let storage = Services.cache2.diskCacheStorage(loadContextInfo);
    storage.asyncVisitStorage(cacheVisitor, true);
  });
}

let countMatchingCacheEntries = function (cacheEntries, domain, fileSuffix) {
  return cacheEntries
    .map(entry => entry.uri.asciiSpec)
    .filter(spec => spec.includes(domain))
    .filter(spec => spec.includes("file_thirdPartyChild." + fileSuffix)).length;
};

function observeChannels(onChannel) {
  // We use a dummy proxy filter to catch all channels, even those that do not
  // generate an "http-on-modify-request" notification, such as link preconnects.
  let proxyFilter = {
    applyFilter(aChannel, aProxy, aCallback) {
      // We have the channel; provide it to the callback.
      onChannel(aChannel);
      // Pass on aProxy unmodified.
      aCallback.onProxyFilterResult(aProxy);
    },
  };
  protocolProxyService.registerChannelFilter(proxyFilter, 0);
  // Return the stop() function:
  return () => protocolProxyService.unregisterChannelFilter(proxyFilter);
}

function startObservingChannels(aMode) {
  let stopObservingChannels = observeChannels(function (channel) {
    let originalURISpec = channel.originalURI.spec;
    if (!originalURISpec.includes("example.net")) {
      return;
    }
    let loadInfo = channel.loadInfo;
    info(
      `OA ${originalURISpec} mode=${aMode}: ${JSON.stringify(ChromeUtils.fillNonDefaultOriginAttributes(loadInfo.originAttributes))}`
    );

    switch (aMode) {
      case TEST_MODE_FIRSTPARTY:
        ok(
          loadInfo.originAttributes.firstPartyDomain === "example.com" ||
            loadInfo.originAttributes.firstPartyDomain === "example.org",
          "first party for " +
            originalURISpec +
            " is " +
            loadInfo.originAttributes.firstPartyDomain
        );
        break;

      case TEST_MODE_NO_ISOLATION: {
        let dotComOA = ChromeUtils.fillNonDefaultOriginAttributes();
        dotComOA.partitionKey = `(http,example.com)`;
        let dotOrgOA = ChromeUtils.fillNonDefaultOriginAttributes();
        dotOrgOA.partitionKey = `(http,example.org)`;
        ok(
          ChromeUtils.isOriginAttributesEqual(
            loadInfo.originAttributes,
            dotComOA
          ) ||
            ChromeUtils.isOriginAttributesEqual(
              loadInfo.originAttributes,
              dotOrgOA
            ) ||
            ChromeUtils.isOriginAttributesEqual(
              loadInfo.originAttributes,
              ChromeUtils.fillNonDefaultOriginAttributes()
            ),
          `OriginAttributes for ${originalURISpec} is expected.`
        );
        break;
      }

      case TEST_MODE_CONTAINERS:
        ok(
          loadInfo.originAttributes.userContextId === 1 ||
            loadInfo.originAttributes.userContextId === 2,
          "userContextId for " +
            originalURISpec +
            " is " +
            loadInfo.originAttributes.userContextId
        );
        break;
      default:
        ok(false, "Unknown test mode.");
    }
  });
  return stopObservingChannels;
}

let stopObservingChannels;
let gMode = -1;

// The init function, which clears image and network caches, and generates
// the random value for isolating video and audio elements across different
// test runs.
async function doInit(aMode) {
  await SpecialPowers.pushPrefEnv({
    set: [
      ["network.predictor.enabled", false],
      ["network.predictor.enable-prefetch", false],
      ["dom.security.https_first", false],
    ],
  });
  clearAllImageCaches();

  Services.cache2.clear();

  randomSuffix = Math.random();
  stopObservingChannels = startObservingChannels(aMode);
  gMode = aMode;
}

// In the test function, we dynamically generate the video and audio element,
// and assign a random suffix to their URL to isolate them across different
// test runs.
async function doTest(aBrowser) {
  let argObj = {
    randomSuffix,
    urlPrefix: TEST_DOMAIN + TEST_PATH,
  };

  await SpecialPowers.spawn(aBrowser, [argObj], async function (arg) {
    ChromeUtils.clearResourceCache({
      types: ["stylesheet", "script"],
    });

    let videoURL = arg.urlPrefix + "file_thirdPartyChild.video.webm";
    let audioURL = arg.urlPrefix + "file_thirdPartyChild.audio.ogg";
    let trackURL = arg.urlPrefix + "file_thirdPartyChild.track.vtt";
    let URLSuffix = "?r=" + arg.randomSuffix;

    // Create the audio and video elements.
    let audio = content.document.createElement("audio");
    let video = content.document.createElement("video");
    let audioSource = content.document.createElement("source");
    let audioTrack = content.document.createElement("track");

    // Append the audio and track element into the body, and wait until they're finished.
    await new content.Promise(resolve => {
      let audioLoaded = false;
      let trackLoaded = false;

      let audioListener = () => {
        Assert.ok(true, `Audio suspended: ${audioURL + URLSuffix}`);
        audio.removeEventListener("suspend", audioListener);

        audioLoaded = true;
        if (audioLoaded && trackLoaded) {
          resolve();
        }
      };

      let trackListener = () => {
        Assert.ok(true, `Audio track loaded: ${audioURL + URLSuffix}`);
        audioTrack.removeEventListener("load", trackListener);

        trackLoaded = true;
        if (audioLoaded && trackLoaded) {
          resolve();
        }
      };

      Assert.ok(true, `Loading audio: ${audioURL + URLSuffix}`);

      // Add the event listeners before everything in case we lose events.
      audioTrack.addEventListener("load", trackListener);
      audio.addEventListener("suspend", audioListener);

      // Assign attributes for the audio element.
      audioSource.setAttribute("src", audioURL + URLSuffix);
      audioSource.setAttribute("type", "audio/ogg");
      audioTrack.setAttribute("src", trackURL);
      audioTrack.setAttribute("kind", "subtitles");
      audioTrack.setAttribute("default", true);

      audio.appendChild(audioSource);
      audio.appendChild(audioTrack);
      audio.autoplay = true;

      content.document.body.appendChild(audio);
    });

    // Append the video element into the body, and wait until it's finished.
    await new content.Promise(resolve => {
      let listener = () => {
        Assert.ok(true, `Video suspended: ${videoURL + URLSuffix}`);
        video.removeEventListener("suspend", listener);
        resolve();
      };

      Assert.ok(true, `Loading video: ${videoURL + URLSuffix}`);

      // Add the event listener before everything in case we lose the event.
      video.addEventListener("suspend", listener);

      // Assign attributes for the video element.
      video.setAttribute("src", videoURL + URLSuffix);
      video.setAttribute("type", "video/ogg");

      content.document.body.appendChild(video);
    });
  });

  return 0;
}

// The check function, which checks the number of cache entries.
async function doCheck(aShouldIsolate) {
  let data = [];
  data = data.concat(
    await cacheDataForContext(Services.loadContextInfo.default)
  );
  data = data.concat(
    await cacheDataForContext(Services.loadContextInfo.private)
  );
  for (let private of [true, false]) {
    for (let anon of [true, false]) {
      for (let userContextId of [0, 1, 2]) {
        for (let firstPartyDomain of [
          undefined,
          "example.com",
          "example.org",
        ]) {
          for (let partitionKey of [
            undefined,
            `(http,example.com)`,
            `(http,example.org)`,
            `(http,example.net)`,
          ]) {
            data = data.concat(
              await cacheDataForContext(
                Services.loadContextInfo.custom(anon, {
                  partitionKey,
                  firstPartyDomain,
                  userContextId,
                  privateBrowsingId: private ? 1 : 0,
                })
              )
            );
          }
        }
      }
    }
  }

  for (let suffix of suffixes) {
    let foundEntryCount = countMatchingCacheEntries(
      data,
      "example.net",
      suffix
    );

    // When isolation is off, we get one or two entries depending on
    // whether the subresource does correct network partitioning.
    let expectedEntryCount = aShouldIsolate ? [2] : [1, 2];
    ok(
      expectedEntryCount.includes(foundEntryCount),
      `${foundEntryCount} entries for ${suffix} for mode=${gMode} isolate=${aShouldIsolate ? "on" : "off"}`
    );
  }

  stopObservingChannels();
  stopObservingChannels = undefined;
  return true;
}

let testArgs = {
  url: TEST_PAGE,
  firstFrameSetting: DEFAULT_FRAME_SETTING,
  secondFrameSetting: [TEST_TYPE_FRAME],
};

IsolationTestTools.runTests(testArgs, doTest, doCheck, doInit);
