"use strict";

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

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

var httpServer = null;

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

if (isParentProcess()) {
  // ensure the cache service is prepped when running the test
  // We only do this in the main process, as the cache storage service leaks
  // when instantiated in the content process.
  Services.cache2;
}

var gotOnProgress;
var gotOnStatus;

function make_channel(url, body, cb) {
  gotOnProgress = false;
  gotOnStatus = false;
  var chan = NetUtil.newChannel({
    uri: url,
    loadUsingSystemPrincipal: true,
  }).QueryInterface(Ci.nsIHttpChannel);
  chan.notificationCallbacks = {
    numChecks: 0,
    QueryInterface: ChromeUtils.generateQI([
      "nsINetworkInterceptController",
      "nsIInterfaceRequestor",
      "nsIProgressEventSink",
    ]),
    getInterface(iid) {
      return this.QueryInterface(iid);
    },
    onProgress() {
      gotOnProgress = true;
    },
    onStatus() {
      gotOnStatus = true;
    },
    shouldPrepareForIntercept() {
      Assert.equal(this.numChecks, 0);
      this.numChecks++;
      return true;
    },
    channelIntercepted(channel) {
      channel.QueryInterface(Ci.nsIInterceptedChannel);
      if (body) {
        var synthesized = Cc[
          "@mozilla.org/io/string-input-stream;1"
        ].createInstance(Ci.nsIStringInputStream);
        synthesized.setByteStringData(body);

        channel.startSynthesizedResponse(synthesized, null, null, "", false);
        channel.finishSynthesizedResponse();
      }
      if (cb) {
        cb(channel);
      }
      return {
        dispatch() {},
      };
    },
  };
  return chan;
}

const REMOTE_BODY = "http handler body";
const NON_REMOTE_BODY = "synthesized body";
const NON_REMOTE_BODY_2 = "synthesized body #2";

function bodyHandler(metadata, response) {
  response.setHeader("Content-Type", "text/plain");
  response.write(REMOTE_BODY);
}

function run_test() {
  httpServer = new HttpServer();
  httpServer.registerPathHandler("/body", bodyHandler);
  httpServer.start(-1);

  run_next_test();
}

function handle_synthesized_response(request, buffer) {
  Assert.equal(buffer, NON_REMOTE_BODY);
  Assert.ok(gotOnStatus);
  Assert.ok(gotOnProgress);
  run_next_test();
}

function handle_synthesized_response_2(request, buffer) {
  Assert.equal(buffer, NON_REMOTE_BODY_2);
  Assert.ok(gotOnStatus);
  Assert.ok(gotOnProgress);
  run_next_test();
}

function handle_remote_response(request, buffer) {
  Assert.equal(buffer, REMOTE_BODY);
  Assert.ok(gotOnStatus);
  Assert.ok(gotOnProgress);
  run_next_test();
}

// hit the network instead of synthesizing
add_test(function () {
  var chan = make_channel(URL + "/body", null, function (channel) {
    channel.resetInterception(false);
  });
  chan.asyncOpen(new ChannelListener(handle_remote_response, null));
});

// synthesize a response
add_test(function () {
  var chan = make_channel(URL + "/body", NON_REMOTE_BODY);
  chan.asyncOpen(
    new ChannelListener(handle_synthesized_response, null, CL_ALLOW_UNKNOWN_CL)
  );
});

// hit the network instead of synthesizing, to test that no previous synthesized
// cache entry is used.
add_test(function () {
  var chan = make_channel(URL + "/body", null, function (channel) {
    channel.resetInterception(false);
  });
  chan.asyncOpen(new ChannelListener(handle_remote_response, null));
});

// synthesize a different response to ensure no previous response is cached
add_test(function () {
  var chan = make_channel(URL + "/body", NON_REMOTE_BODY_2);
  chan.asyncOpen(
    new ChannelListener(
      handle_synthesized_response_2,
      null,
      CL_ALLOW_UNKNOWN_CL
    )
  );
});

// ensure that the channel waits for a decision and synthesizes headers correctly
add_test(function () {
  var chan = make_channel(URL + "/body", null, function (channel) {
    do_timeout(100, function () {
      var synthesized = Cc[
        "@mozilla.org/io/string-input-stream;1"
      ].createInstance(Ci.nsIStringInputStream);
      synthesized.setByteStringData(NON_REMOTE_BODY);
      channel.synthesizeHeader("Content-Length", NON_REMOTE_BODY.length);
      channel.startSynthesizedResponse(synthesized, null, null, "", false);
      channel.finishSynthesizedResponse();
    });
  });
  chan.asyncOpen(new ChannelListener(handle_synthesized_response, null));
});

// ensure that the channel waits for a decision
add_test(function () {
  var chan = make_channel(URL + "/body", null, function (channel) {
    do_timeout(100, function () {
      channel.resetInterception(false);
    });
  });
  chan.asyncOpen(new ChannelListener(handle_remote_response, null));
});

// ensure that the intercepted channel supports suspend/resume
add_test(function () {
  var chan = make_channel(URL + "/body", null, function (intercepted) {
    var synthesized = Cc[
      "@mozilla.org/io/string-input-stream;1"
    ].createInstance(Ci.nsIStringInputStream);
    synthesized.setByteStringData(NON_REMOTE_BODY);

    // set the content-type to ensure that the stream converter doesn't hold up notifications
    // and cause the test to fail
    intercepted.synthesizeHeader("Content-Type", "text/plain");
    intercepted.startSynthesizedResponse(synthesized, null, null, "", false);
    intercepted.finishSynthesizedResponse();
  });
  chan.asyncOpen(
    new ChannelListener(
      handle_synthesized_response,
      null,
      CL_ALLOW_UNKNOWN_CL | CL_SUSPEND | CL_EXPECT_3S_DELAY
    )
  );
});

// ensure that the intercepted channel can be cancelled
add_test(function () {
  var chan = make_channel(URL + "/body", null, function (intercepted) {
    intercepted.cancelInterception(Cr.NS_BINDING_ABORTED);
  });
  chan.asyncOpen(new ChannelListener(run_next_test, null, CL_EXPECT_FAILURE));
});

// ensure that the channel can't be cancelled via nsIInterceptedChannel after making a decision
add_test(function () {
  var chan = make_channel(URL + "/body", null, function (channel) {
    channel.resetInterception(false);
    do_timeout(0, function () {
      var gotexception = false;
      try {
        channel.cancelInterception();
      } catch (x) {
        gotexception = true;
      }
      Assert.ok(gotexception);
    });
  });
  chan.asyncOpen(new ChannelListener(handle_remote_response, null));
});

// ensure that the intercepted channel can be canceled during the response
add_test(function () {
  var chan = make_channel(URL + "/body", null, function (intercepted) {
    var synthesized = Cc[
      "@mozilla.org/io/string-input-stream;1"
    ].createInstance(Ci.nsIStringInputStream);
    synthesized.setByteStringData(NON_REMOTE_BODY);

    let channel = intercepted.channel;
    intercepted.startSynthesizedResponse(synthesized, null, null, "", false);
    intercepted.finishSynthesizedResponse();
    channel.cancel(Cr.NS_BINDING_ABORTED);
  });
  chan.asyncOpen(
    new ChannelListener(
      run_next_test,
      null,
      CL_EXPECT_FAILURE | CL_ALLOW_UNKNOWN_CL
    )
  );
});

// ensure that the intercepted channel can be canceled before the response
add_test(function () {
  var chan = make_channel(URL + "/body", null, function (intercepted) {
    var synthesized = Cc[
      "@mozilla.org/io/string-input-stream;1"
    ].createInstance(Ci.nsIStringInputStream);
    synthesized.setByteStringData(NON_REMOTE_BODY);

    intercepted.channel.cancel(Cr.NS_BINDING_ABORTED);

    // This should not throw, but result in the channel firing callbacks
    // with an error status.
    intercepted.startSynthesizedResponse(synthesized, null, null, "", false);
    intercepted.finishSynthesizedResponse();
  });
  chan.asyncOpen(
    new ChannelListener(
      run_next_test,
      null,
      CL_EXPECT_FAILURE | CL_ALLOW_UNKNOWN_CL
    )
  );
});

// Ensure that nsIInterceptedChannel.channelIntercepted() can return an error.
// In this case we should automatically ResetInterception() and complete the
// network request.
add_test(function () {
  var chan = make_channel(URL + "/body", null, function () {
    throw new Error("boom");
  });
  chan.asyncOpen(new ChannelListener(handle_remote_response, null));
});

add_test(function () {
  httpServer.stop(run_next_test);
});
