/* This Source Code Form is subject to the terms of the Mozilla Public
 * License, v. 2.0. If a copy of the MPL was not distributed with this
 * file, You can obtain one at http://mozilla.org/MPL/2.0/. */

// keep eslint happy until it knows about WebTransport
/* global WebTransport:false */
/* global TextDecoderStream:false */

// Using multiple files to reduce racing
// This file tests reading/writing to incoming/outgoing streams (uni & bidi)
//
"use strict";

var h3Port;
var host;

registerCleanupFunction(async () => {
  Services.prefs.clearUserPref("network.dns.localDomains");
  Services.prefs.clearUserPref("network.webtransport.enabled");
  Services.prefs.clearUserPref("network.webtransport.datagrams.enabled");
  Services.prefs.clearUserPref("network.webtransport.redirect.enabled");
});

var { NetUtil } = ChromeUtils.importESModule(
  "resource://gre/modules/NetUtil.sys.mjs"
);

function readFile(file) {
  let fstream = Cc["@mozilla.org/network/file-input-stream;1"].createInstance(
    Ci.nsIFileInputStream
  );
  fstream.init(file, -1, 0, 0);
  let data = NetUtil.readInputStreamToString(fstream, fstream.available());
  fstream.close();
  return data;
}

function addCertFromFile(certdb, filename, trustString) {
  let certFile = do_get_file(filename, false);
  let pem = readFile(certFile)
    .replace(/-----BEGIN CERTIFICATE-----/, "")
    .replace(/-----END CERTIFICATE-----/, "")
    .replace(/[\r\n]/g, "");
  certdb.addCertFromBase64(pem, trustString);
}

add_setup(async function setup() {
  Services.prefs.setCharPref("network.dns.localDomains", "foo.example.com");
  Services.prefs.setBoolPref("network.webtransport.enabled", true);
  Services.prefs.setBoolPref("network.webtransport.datagrams.enabled", true);
  Services.prefs.setBoolPref("network.webtransport.redirect.enabled", true);

  h3Port = Services.env.get("MOZHTTP3_PORT");
  Assert.notEqual(h3Port, null);
  Assert.notEqual(h3Port, "");
  host = "foo.example.com:" + h3Port;
  do_get_profile();

  let certdb = Cc["@mozilla.org/security/x509certdb;1"].getService(
    Ci.nsIX509CertDB
  );
  // `../unit/` so that unit_ipc tests can use as well
  addCertFromFile(
    certdb,
    "../../../../netwerk/test/unit/http2-ca.pem",
    "CTu,u,u"
  );
});

// Read all chunks from |readable_stream|, decode chunks to a utf-8 string, then
// return the string. (borrowed from wpt tests)
async function read_stream_as_string(readable_stream) {
  const decoder = new TextDecoderStream();
  const decode_stream = readable_stream.pipeThrough(decoder);
  const reader = decode_stream.getReader();

  let chunks = "";
  while (true) {
    const { value: chunk, done } = await reader.read();
    if (done) {
      break;
    }
    chunks += chunk;
  }
  reader.releaseLock();
  return chunks;
}

add_task(async function test_wt_incoming_unidi_stream() {
  // trigger stream creation server side and default echo
  let wt = new WebTransport(
    "https://" + host + "/create_unidi_stream_and_hello"
  );
  await wt.ready;

  const streams = await wt.incomingUnidirectionalStreams;
  const stream_reader = streams.getReader();
  const { value: recv_stream } = await stream_reader.read();
  let str = await read_stream_as_string(recv_stream);
  stream_reader.releaseLock();
  Assert.equal(str, "qwerty");

  wt.close();
});

add_task(async function test_wt_incoming_and_outgoing_unidi_stream() {
  // create the client's incoming stream from the server side
  // we need it to listen to the echo back
  let wt = new WebTransport("https://" + host + "/create_unidi_stream");
  await wt.ready;

  // send hello to server
  let expected = "uni_hello";
  let writableStream = await wt.createUnidirectionalStream(); // only triggers NewStream OnWrite
  let wsDefaultWriter = writableStream.getWriter();
  await wsDefaultWriter.ready;
  let data = new TextEncoder().encode(expected);
  await wsDefaultWriter.write(data); // triggers Http3ServerEvent::Data
  await wsDefaultWriter.close();
  wsDefaultWriter.releaseLock();

  // read the echo
  const streams = await wt.incomingUnidirectionalStreams;
  const stream_reader = streams.getReader();
  const { value: recv_stream } = await stream_reader.read();
  let str = await read_stream_as_string(recv_stream);
  Assert.equal(str, expected);
  stream_reader.releaseLock();
  await recv_stream.closed;

  wt.close();
});

add_task(async function test_wt_outgoing_bidi_stream() {
  let wt = new WebTransport("https://" + host + "/success");
  await wt.ready;

  // write to server
  let wtbds = await wt.createBidirectionalStream();
  let writableStream = wtbds.writable;
  let wsDefaultWriter = writableStream.getWriter();
  await wsDefaultWriter.ready;
  let expected = "xyzhello";
  let data = new TextEncoder().encode(expected);
  await wsDefaultWriter.write(data);
  await wsDefaultWriter.close();
  wsDefaultWriter.releaseLock();

  // string goes through server and is echoed back here
  const str = await read_stream_as_string(wtbds.readable);
  Assert.equal(str, expected);

  wt.close();
});

add_task(async function test_wt_incoming_bidi_stream() {
  let wt = new WebTransport(
    "https://" + host + "/create_bidi_stream_and_hello"
  );
  // await wt.ready; // causes occasional hang on release --verify

  const stream_reader = wt.incomingBidirectionalStreams.getReader();
  const { value: bidi_stream } = await stream_reader.read();
  stream_reader.releaseLock();

  const str = await read_stream_as_string(bidi_stream.readable);
  Assert.equal(str, "asdfg");

  wt.close();
});

add_task(async function test_wt_incoming_bidi_stream_huge_data() {
  let wt = new WebTransport(
    "https://" + host + "/create_bidi_stream_and_large_data"
  );
  // await wt.ready; // causes occasional hang on release --verify

  const stream_reader = wt.incomingBidirectionalStreams.getReader();
  const { value: bidi_stream } = await stream_reader.read();
  stream_reader.releaseLock();

  const str = await read_stream_as_string(bidi_stream.readable);
  Assert.equal(str.length, 32 * 1024 * 1024);
  wt.close();
});
