/*
 * Tests for bug 1241377: A channel with nsIFormPOSTActionChannel interface
 * should be able to accept form POST.
 */

"use strict";

const SCHEME = "x-bug1241377";

const FORM_BASE = SCHEME + "://dummy/form/";
const NORMAL_FORM_URI = FORM_BASE + "normal.html";
const UPLOAD_FORM_URI = FORM_BASE + "upload.html";
const POST_FORM_URI = FORM_BASE + "post.html";

const ACTION_BASE = SCHEME + "://dummy/action/";
const NORMAL_ACTION_URI = ACTION_BASE + "normal.html";
const UPLOAD_ACTION_URI = ACTION_BASE + "upload.html";
const POST_ACTION_URI = ACTION_BASE + "post.html";

function CustomProtocolHandler() {}
CustomProtocolHandler.prototype = {
  /** nsIProtocolHandler */
  get scheme() {
    return SCHEME;
  },
  newChannel(aURI, aLoadInfo) {
    return new CustomChannel(aURI, aLoadInfo);
  },
  allowPort(port) {
    return port != -1;
  },

  /** nsISupports */
  QueryInterface: ChromeUtils.generateQI(["nsIProtocolHandler"]),
};

function CustomChannel(aURI, aLoadInfo) {
  this.uri = aURI;
  this.loadInfo = aLoadInfo;

  this._uploadStream = null;

  var interfaces = [Ci.nsIRequest, Ci.nsIChannel];
  if (this.uri.spec == POST_ACTION_URI) {
    interfaces.push(Ci.nsIFormPOSTActionChannel);
  } else if (this.uri.spec == UPLOAD_ACTION_URI) {
    interfaces.push(Ci.nsIUploadChannel);
  }
  this.QueryInterface = ChromeUtils.generateQI(interfaces);
}
CustomChannel.prototype = {
  /** nsIUploadChannel */
  get uploadStream() {
    return this._uploadStream;
  },
  set uploadStream(val) {
    throw Components.Exception("", Cr.NS_ERROR_NOT_IMPLEMENTED);
  },
  setUploadStream(aStream) {
    this._uploadStream = aStream;
  },

  /** nsIChannel */
  get originalURI() {
    return this.uri;
  },
  get URI() {
    return this.uri;
  },
  owner: null,
  notificationCallbacks: null,
  get securityInfo() {
    return null;
  },
  get contentType() {
    return "text/html";
  },
  set contentType(val) {},
  contentCharset: "UTF-8",
  get contentLength() {
    return -1;
  },
  set contentLength(val) {
    throw Components.Exception("", Cr.NS_ERROR_NOT_IMPLEMENTED);
  },
  open() {
    throw Components.Exception("", Cr.NS_ERROR_NOT_IMPLEMENTED);
  },
  asyncOpen(aListener) {
    var data = `
<!DOCTYPE html>
<html>
<head>
<meta charset="utf-8">
<title>test bug 1241377</title>
</head>
<body>
`;

    if (this.uri.spec.startsWith(FORM_BASE)) {
      data += `
<form id="form" action="${this.uri.spec.replace(FORM_BASE, ACTION_BASE)}"
      method="post" enctype="text/plain" target="frame">
<input type="hidden" name="foo" value="bar">
<input type="submit">
</form>

<iframe id="frame" name="frame" width="200" height="200"></iframe>

<script type="text/javascript">
<!--
document.getElementById('form').submit();
//-->
</script>
`;
    } else if (this.uri.spec.startsWith(ACTION_BASE)) {
      var postData = "";
      var headers = {};
      if (this._uploadStream) {
        var bstream = Cc["@mozilla.org/binaryinputstream;1"].createInstance(
          Ci.nsIBinaryInputStream
        );
        bstream.setInputStream(this._uploadStream);
        postData = bstream.readBytes(bstream.available());

        if (this._uploadStream instanceof Ci.nsIMIMEInputStream) {
          this._uploadStream.visitHeaders((name, value) => {
            headers[name] = value;
          });
        }
      }
      data += `
<input id="upload_stream" value="${this._uploadStream ? "yes" : "no"}">
<input id="post_data" value="${btoa(postData)}">
<input id="upload_headers" value='${JSON.stringify(headers)}'>
`;
    }

    data += `
</body>
</html>
`;

    var stream = Cc["@mozilla.org/io/string-input-stream;1"].createInstance(
      Ci.nsIStringInputStream
    );
    stream.setData(data, data.length);

    var runnable = {
      run: () => {
        try {
          aListener.onStartRequest(this, null);
        } catch (e) {}
        try {
          aListener.onDataAvailable(this, stream, 0, stream.available());
        } catch (e) {}
        try {
          aListener.onStopRequest(this, null, Cr.NS_OK);
        } catch (e) {}
      },
    };
    Services.tm.dispatchToMainThread(runnable);
  },

  /** nsIRequest */
  get name() {
    return this.uri.spec;
  },
  isPending() {
    return false;
  },
  get status() {
    return Cr.NS_OK;
  },
  cancel() {},
  loadGroup: null,
  loadFlags:
    Ci.nsIRequest.LOAD_NORMAL |
    Ci.nsIRequest.INHIBIT_CACHING |
    Ci.nsIRequest.LOAD_BYPASS_CACHE,
};

function frameScript() {
  /* eslint-env mozilla/frame-script */
  /* eslint-disable mozilla/no-arbitrary-setTimeout */
  addMessageListener("Test:WaitForIFrame", function () {
    var check = function () {
      if (content) {
        var frame = content.document.getElementById("frame");
        if (frame) {
          var upload_stream =
            frame.contentDocument.getElementById("upload_stream");
          var post_data = frame.contentDocument.getElementById("post_data");
          var headers = frame.contentDocument.getElementById("upload_headers");
          if (upload_stream && post_data && headers) {
            sendAsyncMessage("Test:IFrameLoaded", [
              upload_stream.value,
              post_data.value,
              headers.value,
            ]);
            return;
          }
        }
      }

      setTimeout(check, 100);
    };

    check();
  });
  /* eslint-enable mozilla/no-arbitrary-setTimeout */
}

function loadTestTab(uri) {
  gBrowser.selectedTab = BrowserTestUtils.addTab(gBrowser, uri);
  var browser = gBrowser.selectedBrowser;

  let manager = browser.messageManager;
  browser.messageManager.loadFrameScript(
    "data:,(" + frameScript.toString() + ")();",
    true
  );

  return new Promise(resolve => {
    function listener({ data: [hasUploadStream, postData, headers] }) {
      manager.removeMessageListener("Test:IFrameLoaded", listener);
      resolve([hasUploadStream, atob(postData), JSON.parse(headers)]);
    }

    manager.addMessageListener("Test:IFrameLoaded", listener);
    manager.sendAsyncMessage("Test:WaitForIFrame");
  });
}

add_task(async function () {
  var handler = new CustomProtocolHandler();
  Services.io.registerProtocolHandler(
    SCHEME,
    handler,
    Ci.nsIProtocolHandler.URI_NORELATIVE |
      Ci.nsIProtocolHandler.URI_IS_LOCAL_RESOURCE,
    -1
  );
  registerCleanupFunction(function () {
    Services.io.unregisterProtocolHandler(SCHEME);
  });
});

add_task(async function () {
  var [hasUploadStream] = await loadTestTab(NORMAL_FORM_URI);
  is(hasUploadStream, "no", "normal action should not have uploadStream");

  gBrowser.removeCurrentTab();
});

add_task(async function () {
  var [hasUploadStream] = await loadTestTab(UPLOAD_FORM_URI);
  is(hasUploadStream, "no", "upload action should not have uploadStream");

  gBrowser.removeCurrentTab();
});

add_task(async function () {
  var [hasUploadStream, postData, headers] = await loadTestTab(POST_FORM_URI);

  is(hasUploadStream, "yes", "post action should have uploadStream");
  is(postData, "foo=bar\r\n", "POST data is received correctly");

  is(headers["Content-Type"], "text/plain", "Content-Type header is correct");
  is(headers["Content-Length"], undefined, "Content-Length header is correct");

  gBrowser.removeCurrentTab();
});
