/**
 * Test for the "CacheEntryId" under several cases.
 */

"use strict";

const { HttpServer } = ChromeUtils.importESModule(
  "resource://testing-common/httpd.sys.mjs"
);

ChromeUtils.defineLazyGetter(this, "URL", function () {
  return "http://localhost:" + httpServer.identity.primaryPort + "/content";
});

var httpServer = null;

const responseContent = "response body";
const responseContent2 = "response body 2";
const altContent = "!@#$%^&*()";
const altContentType = "text/binary";

function isParentProcess() {
  let appInfo = Cc["@mozilla.org/xre/app-info;1"];
  return (
    !appInfo ||
    Services.appinfo.processType == Ci.nsIXULRuntime.PROCESS_TYPE_DEFAULT
  );
}

var handlers = [
  (m, r) => {
    r.bodyOutputStream.write(responseContent, responseContent.length);
  },
  (m, r) => {
    r.setStatusLine(m.httpVersion, 304, "Not Modified");
  },
  (m, r) => {
    r.setStatusLine(m.httpVersion, 304, "Not Modified");
  },
  (m, r) => {
    r.setStatusLine(m.httpVersion, 304, "Not Modified");
  },
  (m, r) => {
    r.setStatusLine(m.httpVersion, 304, "Not Modified");
  },
  (m, r) => {
    r.bodyOutputStream.write(responseContent2, responseContent2.length);
  },
  (m, r) => {
    r.setStatusLine(m.httpVersion, 304, "Not Modified");
  },
];

function contentHandler(metadata, response) {
  response.setHeader("Content-Type", "text/plain");
  response.setHeader("Cache-Control", "no-cache");

  var handler = handlers.shift();
  if (handler) {
    handler(metadata, response);
    return;
  }

  Assert.ok(false, "Should not reach here.");
}

function fetch(preferredDataType = null) {
  return new Promise(resolve => {
    var chan = NetUtil.newChannel({ uri: URL, loadUsingSystemPrincipal: true });

    if (preferredDataType) {
      var cc = chan.QueryInterface(Ci.nsICacheInfoChannel);
      cc.preferAlternativeDataType(
        altContentType,
        "",
        Ci.nsICacheInfoChannel.ASYNC
      );
    }

    chan.asyncOpen(
      new ChannelListener((request, buffer, ctx, isFromCache, cacheEntryId) => {
        resolve({ request, buffer, isFromCache, cacheEntryId });
      }, null)
    );
  });
}

function check(
  response,
  content,
  preferredDataType,
  isFromCache,
  cacheEntryIdChecker
) {
  var cc = response.request.QueryInterface(Ci.nsICacheInfoChannel);

  Assert.equal(response.buffer, content);
  Assert.equal(cc.alternativeDataType, preferredDataType);
  Assert.equal(response.isFromCache, isFromCache);
  Assert.ok(!cacheEntryIdChecker || cacheEntryIdChecker(response.cacheEntryId));

  return response;
}

function writeAltData(request) {
  var cc = request.QueryInterface(Ci.nsICacheInfoChannel);
  var os = cc.openAlternativeOutputStream(altContentType, altContent.length);
  os.write(altContent, altContent.length);
  os.close();
  gc(); // We need to do a GC pass to ensure the cache entry has been freed.

  return new Promise(resolve => {
    if (isParentProcess()) {
      Services.cache2.QueryInterface(Ci.nsICacheTesting).flush(resolve);
    } else {
      do_send_remote_message("flush");
      do_await_remote_message("flushed").then(resolve);
    }
  });
}

function run_test() {
  do_get_profile();
  httpServer = new HttpServer();
  httpServer.registerPathHandler("/content", contentHandler);
  httpServer.start(-1);
  do_test_pending();

  var targetCacheEntryId = null;
  var secondCacheEntryId = null;

  return (
    Promise.resolve()
      // Setup testing environment: Placing alternative data into HTTP cache.
      .then(_ => fetch(altContentType))
      .then(r =>
        check(
          r,
          responseContent,
          "",
          false,
          cacheEntryId => cacheEntryId !== undefined
        )
      )
      .then(r => {
        targetCacheEntryId = r.cacheEntryId;
        writeAltData(r.request);
      })

      // Start testing.
      .then(_ => fetch(altContentType))
      .then(r =>
        check(
          r,
          altContent,
          altContentType,
          true,
          cacheEntryId => cacheEntryId !== undefined
        )
      )

      .then(_ => fetch())
      .then(r =>
        check(
          r,
          responseContent,
          "",
          true,
          cacheEntryId => cacheEntryId === targetCacheEntryId
        )
      )

      .then(_ => fetch(altContentType))
      .then(r =>
        check(
          r,
          altContent,
          altContentType,
          true,
          cacheEntryId => cacheEntryId === targetCacheEntryId
        )
      )

      .then(_ => fetch())
      .then(r =>
        check(
          r,
          responseContent,
          "",
          true,
          cacheEntryId => cacheEntryId === targetCacheEntryId
        )
      )

      .then(_ => fetch()) // The response is changed here.
      .then(r =>
        check(
          r,
          responseContent2,
          "",
          false,
          cacheEntryId =>
            cacheEntryId !== undefined && cacheEntryId !== targetCacheEntryId
        )
      )
      .then(r => {
        secondCacheEntryId = r.cacheEntryId;
      })

      .then(_ => fetch())
      .then(r =>
        check(
          r,
          responseContent2,
          "",
          true,
          cacheEntryId => cacheEntryId === secondCacheEntryId
        )
      )

      // Tear down.
      .catch(e => Assert.ok(false, "Unexpected exception: " + e))
      .then(_ => Assert.equal(handlers.length, 0))
      .then(_ => httpServer.stop(do_test_finished))
  );
}
