// This file ensures that canceling a channel early does not
// send the request to the server (bug 350790)
//
// I've also shoehorned in a test that ENSURE_CALLED_BEFORE_CONNECT works as
// expected: see comments that start with ENSURE_CALLED_BEFORE_CONNECT:
//
// This test also checks that cancelling a channel before asyncOpen, after
// onStopRequest, or during onDataAvailable works as expected.

"use strict";

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

function inChildProcess() {
  return Services.appinfo.processType != Ci.nsIXULRuntime.PROCESS_TYPE_DEFAULT;
}

var ios = Services.io;
var ReferrerInfo = Components.Constructor(
  "@mozilla.org/referrer-info;1",
  "nsIReferrerInfo",
  "init"
);
var observer = {
  QueryInterface: ChromeUtils.generateQI(["nsIObserver"]),

  observe(subject) {
    subject = subject.QueryInterface(Ci.nsIRequest);
    subject.cancelWithReason(Cr.NS_BINDING_ABORTED, reason);

    // ENSURE_CALLED_BEFORE_CONNECT: setting values should still work
    try {
      subject.QueryInterface(Ci.nsIHttpChannel);
      let currentReferrer = subject.getRequestHeader("Referer");
      Assert.equal(currentReferrer, "http://site1.com/");
      var uri = ios.newURI("http://site2.com");
      subject.referrerInfo = new ReferrerInfo(
        Ci.nsIReferrerInfo.EMPTY,
        true,
        uri
      );
    } catch (ex) {
      do_throw("Exception: " + ex);
    }
  },
};

let cancelDuringOnStartListener = {
  onStartRequest: function test_onStartR(request) {
    Assert.equal(request.status, Cr.NS_BINDING_ABORTED);
    // We didn't sync the reason to child process.
    if (!inChildProcess()) {
      Assert.equal(request.canceledReason, reason);
    }

    // ENSURE_CALLED_BEFORE_CONNECT: setting referrer should now fail
    try {
      request.QueryInterface(Ci.nsIHttpChannel);
      let currentReferrer = request.getRequestHeader("Referer");
      Assert.equal(currentReferrer, "http://site2.com/");
      var uri = ios.newURI("http://site3.com/");

      // Need to set NECKO_ERRORS_ARE_FATAL=0 else we'll abort process
      Services.env.set("NECKO_ERRORS_ARE_FATAL", "0");
      // we expect setting referrer to fail
      try {
        request.referrerInfo = new ReferrerInfo(
          Ci.nsIReferrerInfo.EMPTY,
          true,
          uri
        );
        do_throw("Error should have been thrown before getting here");
      } catch (ex) {}
    } catch (ex) {
      do_throw("Exception: " + ex);
    }
  },

  onDataAvailable: function test_ODA() {
    do_throw("Should not get any data!");
  },

  onStopRequest: function test_onStopR() {
    this.resolved();
  },
};

var cancelDuringOnDataListener = {
  data: "",
  channel: null,
  receivedSomeData: null,
  onStartRequest: function test_onStartR(request) {
    Assert.equal(request.status, Cr.NS_OK);
  },

  onDataAvailable: function test_ODA(request, stream, offset, count) {
    let string = NetUtil.readInputStreamToString(stream, count);
    Assert.ok(!string.includes("b"));
    this.data += string;
    this.channel.cancel(Cr.NS_BINDING_ABORTED);
    if (this.receivedSomeData) {
      this.receivedSomeData();
    }
  },

  onStopRequest: function test_onStopR(request) {
    Assert.ok(this.data.includes("a"), `data: ${this.data}`);
    Assert.equal(request.status, Cr.NS_BINDING_ABORTED);
    this.resolved();
  },
};

function makeChan(url) {
  var chan = NetUtil.newChannel({
    uri: url,
    loadUsingSystemPrincipal: true,
  }).QueryInterface(Ci.nsIHttpChannel);

  // ENSURE_CALLED_BEFORE_CONNECT: set original value
  var uri = ios.newURI("http://site1.com");
  chan.referrerInfo = new ReferrerInfo(Ci.nsIReferrerInfo.EMPTY, true, uri);
  return chan;
}

var httpserv = null;

add_task(async function setup() {
  httpserv = new HttpServer();
  httpserv.registerPathHandler("/failtest", failtest);
  httpserv.registerPathHandler("/cancel_middle", cancel_middle);
  httpserv.registerPathHandler("/normal_response", normal_response);
  httpserv.start(-1);

  registerCleanupFunction(async () => {
    await new Promise(resolve => httpserv.stop(resolve));
  });
});

add_task(async function test_cancel_during_onModifyRequest() {
  var chan = makeChan(
    "http://localhost:" + httpserv.identity.primaryPort + "/failtest"
  );

  if (!inChildProcess()) {
    Services.obs.addObserver(observer, "http-on-modify-request");
  } else {
    do_send_remote_message("register-observer");
    await do_await_remote_message("register-observer-done");
  }

  await new Promise(resolve => {
    cancelDuringOnStartListener.resolved = resolve;
    chan.asyncOpen(cancelDuringOnStartListener);
  });

  if (!inChildProcess()) {
    Services.obs.removeObserver(observer, "http-on-modify-request");
  } else {
    do_send_remote_message("unregister-observer");
    await do_await_remote_message("unregister-observer-done");
  }
});

add_task(async function test_cancel_before_asyncOpen() {
  var chan = makeChan(
    "http://localhost:" + httpserv.identity.primaryPort + "/failtest"
  );

  chan.cancel(Cr.NS_BINDING_ABORTED);

  Assert.throws(
    () => {
      chan.asyncOpen(cancelDuringOnStartListener);
    },
    /NS_BINDING_ABORTED/,
    "cannot open if already cancelled"
  );
});

add_task(async function test_cancel_during_onData() {
  var chan = makeChan(
    "http://localhost:" + httpserv.identity.primaryPort + "/cancel_middle"
  );

  await new Promise(resolve => {
    cancelDuringOnDataListener.resolved = resolve;
    cancelDuringOnDataListener.channel = chan;
    chan.asyncOpen(cancelDuringOnDataListener);
  });
});

var cancelAfterOnStopListener = {
  data: "",
  channel: null,
  onStartRequest: function test_onStartR(request) {
    Assert.equal(request.status, Cr.NS_OK);
  },

  onDataAvailable: function test_ODA(request, stream, offset, count) {
    let string = NetUtil.readInputStreamToString(stream, count);
    this.data += string;
  },

  onStopRequest: function test_onStopR(request) {
    info("onStopRequest");
    Assert.equal(request.status, Cr.NS_OK);
    this.resolved();
  },
};

add_task(async function test_cancel_after_onStop() {
  var chan = makeChan(
    "http://localhost:" + httpserv.identity.primaryPort + "/normal_response"
  );

  await new Promise(resolve => {
    cancelAfterOnStopListener.resolved = resolve;
    cancelAfterOnStopListener.channel = chan;
    chan.asyncOpen(cancelAfterOnStopListener);
  });
  Assert.equal(chan.status, Cr.NS_OK);

  // For now it's unclear if cancelling after onStop should throw,
  // silently fail, or overwrite the channel's status as we currently do.
  // See discussion in bug 1553083
  chan.cancel(Cr.NS_BINDING_ABORTED);
  Assert.equal(chan.status, Cr.NS_BINDING_ABORTED);
});

// PATHS

// /failtest
function failtest() {
  do_throw("This should not be reached");
}

function cancel_middle(metadata, response) {
  response.processAsync();
  response.setStatusLine(metadata.httpVersion, 200, "OK");
  let str1 = "a".repeat(128 * 1024);
  response.write(str1, str1.length);
  response.bodyOutputStream.flush();

  let p = new Promise(resolve => {
    cancelDuringOnDataListener.receivedSomeData = resolve;
  });
  p.then(() => {
    let str2 = "b".repeat(128 * 1024);
    response.write(str2, str2.length);
    response.finish();
  });
}

function normal_response(metadata, response) {
  response.setStatusLine(metadata.httpVersion, 200, "OK");
  let str1 = "Is this normal?";
  response.write(str1, str1.length);
}
