const SAME_COMPARTMENT = "same-compartment";
const IFRAME_COMPARTMENT = "iframe-compartment";
const BIG_BUFFER_SIZE = 1000000;
const ITER_MAX = 10;

function makeBuffer(size) {
  let buffer = new Uint8Array(size);
  buffer.fill(42);

  let value = 0;
  for (let i = 0; i < 1000000; i += 1000) {
    buffer.set([++value % 255], i);
  }

  return buffer;
}

function apply_compartment(compartment, data) {
  if (compartment == SAME_COMPARTMENT) {
    return self[data.func](data.args, self);
  }

  if (compartment == IFRAME_COMPARTMENT) {
    const iframe = document.querySelector("#iframe").contentWindow;
    return iframe[data.func](data.args, self);
  }

  ok(false, "Invalid compartment value");
  return undefined;
}

async function test_nativeStream(compartment) {
  info("test_nativeStream");

  let r = await fetch("/");

  return apply_compartment(compartment, {
    func: "test_nativeStream_continue",
    args: r,
  });
}

async function test_nativeStream_continue(r, that) {
  that.ok(r.body instanceof that.ReadableStream, "We have a ReadableStream");

  let a = r.clone();
  that.ok(a instanceof that.Response, "We have a cloned Response");
  that.ok(a.body instanceof that.ReadableStream, "We have a ReadableStream");

  let b = a.clone();
  that.ok(b instanceof that.Response, "We have a cloned Response");
  that.ok(b.body instanceof that.ReadableStream, "We have a ReadableStream");

  let blob = await r.blob();

  that.ok(blob instanceof that.Blob, "We have a blob");
  let d = await a.body.getReader().read();

  that.ok(!d.done, "We have read something!");
  blob = await b.blob();

  that.ok(blob instanceof that.Blob, "We have a blob");
}

async function test_timeout(compartment) {
  info("test_timeout");

  let blob = new Blob([""]);
  let r = await fetch(URL.createObjectURL(blob));

  return apply_compartment(compartment, {
    func: "test_timeout_continue",
    args: r,
  });
}

async function test_timeout_continue(r, that) {
  await r.body.getReader().read();

  await new Promise(resolve => setTimeout(resolve, 0));

  try {
    await r.blob();
    that.ok(false, "We cannot have a blob here!");
  } catch (exc) {
    that.ok(true, "We cannot have a blob here!");
  }
}

async function test_nonNativeStream(compartment) {
  info("test_nonNativeStream");

  let buffer = makeBuffer(BIG_BUFFER_SIZE);
  info("Buffer size: " + buffer.byteLength);

  let r = new Response(
    new ReadableStream({
      start: controller => {
        controller.enqueue(buffer);
        controller.close();
      },
    })
  );

  return apply_compartment(compartment, {
    func: "test_nonNativeStream_continue",
    args: { r, buffer },
  });
}

async function test_nonNativeStream_continue(data, that) {
  that.ok(
    data.r.body instanceof that.ReadableStream,
    "We have a ReadableStream"
  );

  let a = data.r.clone();
  that.ok(a instanceof that.Response, "We have a cloned Response");
  that.ok(a.body instanceof that.ReadableStream, "We have a ReadableStream");

  let b = a.clone();
  that.ok(b instanceof that.Response, "We have a cloned Response");
  that.ok(b.body instanceof that.ReadableStream, "We have a ReadableStream");

  let blob = await data.r.blob();

  that.ok(blob instanceof that.Blob, "We have a blob");
  let d = await a.body.getReader().read();

  that.ok(!d.done, "We have read something!");
  blob = await b.blob();

  that.ok(blob instanceof that.Blob, "We have a blob");
  that.is(blob.size, data.buffer.byteLength, "Blob size matches");
}

async function test_noUint8Array(compartment) {
  info("test_noUint8Array");

  let r = new Response(
    new ReadableStream({
      start: controller => {
        controller.enqueue("hello world!");
        controller.close();
      },
    })
  );

  return apply_compartment(compartment, {
    func: "test_noUint8Array_continue",
    args: r,
  });
}

async function test_noUint8Array_continue(r, that) {
  that.ok(r.body instanceof that.ReadableStream, "We have a ReadableStream");

  try {
    await r.blob();
    that.ok(false, "We cannot have a blob here!");
  } catch (ex) {
    that.ok(true, "We cannot have a blob here!");
  }
}

async function test_pendingStream(compartment) {
  let r = new Response(
    new ReadableStream({
      start: controller => {
        controller.enqueue(makeBuffer(BIG_BUFFER_SIZE));
        // Let's keep this controler open.
        self.ccc = controller;
      },
    })
  );

  return apply_compartment(compartment, {
    func: "test_pendingStream_continue",
    args: r,
  });
}

async function test_pendingStream_continue(r, that) {
  let d = await r.body.getReader().read();

  that.ok(!d.done, "We have read something!");

  if ("close" in that) {
    that.close();
  }
}

async function test_nativeStream_cache(compartment) {
  info("test_nativeStream_cache");

  let origBody = "123456789abcdef";
  let url = "/nativeStream";

  let cache = await caches.open("nativeStream");

  info("Storing a body as a string");
  await cache.put(url, new Response(origBody));

  return apply_compartment(compartment, {
    func: "test_nativeStream_cache_continue",
    args: { caches, cache, url, origBody },
  });
}

async function test_nativeStream_cache_continue(data, that) {
  that.info("Retrieving the stored value");
  let cacheResponse = await data.cache.match(data.url);

  that.info("Converting the response to text");
  let cacheBody = await cacheResponse.text();

  that.is(data.origBody, cacheBody, "Bodies match");

  await data.caches.delete("nativeStream");
}

async function test_nonNativeStream_cache(compartment) {
  info("test_nonNativeStream_cache");

  let url = "/nonNativeStream";

  let cache = await caches.open("nonNativeStream");
  let buffer = makeBuffer(BIG_BUFFER_SIZE);
  info("Buffer size: " + buffer.byteLength);

  info("Storing a body as a string");
  let r = new Response(
    new ReadableStream({
      start: controller => {
        controller.enqueue(buffer);
        controller.close();
      },
    })
  );

  return apply_compartment(compartment, {
    func: "test_nonNativeStream_cache_continue",
    args: { caches, cache, buffer, r },
  });
}

async function test_nonNativeStream_cache_continue(data, that) {
  await data.cache.put(data.url, data.r);

  that.info("Retrieving the stored value");
  let cacheResponse = await data.cache.match(data.url);

  that.info("Converting the response to text");
  let cacheBody = await cacheResponse.arrayBuffer();

  that.ok(cacheBody instanceof that.ArrayBuffer, "Body is an array buffer");
  that.is(cacheBody.byteLength, BIG_BUFFER_SIZE, "Body length is correct");

  let value = 0;
  for (let i = 0; i < 1000000; i += 1000) {
    that.is(
      new Uint8Array(cacheBody)[i],
      ++value % 255,
      "byte in position " + i + " is correct"
    );
  }

  await data.caches.delete("nonNativeStream");
}

async function test_codeExecution(compartment) {
  info("test_codeExecution");

  let r = new Response(
    new ReadableStream({
      start(c) {
        controller = c;
      },
      pull() {
        console.log("pull called");
      },
    })
  );

  return apply_compartment(compartment, {
    func: "test_codeExecution_continue",
    args: r,
  });
}

// This is intended to just be a drop-in replacement for an old observer
// notification.
function addConsoleStorageListener(listener) {
  const ConsoleAPIStorage = SpecialPowers.Cc[
    "@mozilla.org/consoleAPI-storage;1"
  ].getService(SpecialPowers.Ci.nsIConsoleAPIStorage);
  listener.__handler = (message, id) => {
    listener.observe(message, id);
  };
  ConsoleAPIStorage.addLogEventListener(
    listener.__handler,
    SpecialPowers.wrap(document).nodePrincipal
  );
}

function removeConsoleStorageListener(listener) {
  const ConsoleAPIStorage = SpecialPowers.Cc[
    "@mozilla.org/consoleAPI-storage;1"
  ].getService(SpecialPowers.Ci.nsIConsoleAPIStorage);
  ConsoleAPIStorage.removeLogEventListener(listener.__handler);
}

async function test_codeExecution_continue(r, that) {
  function consoleListener() {
    addConsoleStorageListener(this);
  }

  var promise = new Promise(resolve => {
    consoleListener.prototype = {
      observe(aSubject) {
        that.ok(true, "Something has been received");

        var obj = aSubject.wrappedJSObject;
        if (obj.arguments[0] && obj.arguments[0] === "pull called") {
          that.ok(true, "Message received!");
          removeConsoleStorageListener(this);
          resolve();
        }
      },
    };
  });

  var cl = new consoleListener();

  r.body.getReader().read();
  await promise;
}

async function test_global(compartment) {
  info("test_global: " + compartment);

  self.foo = 42;
  self.iter = ITER_MAX;

  let r = new Response(
    new ReadableStream({
      start(c) {
        self.controller = c;
      },
      pull() {
        if (!("iter" in self) || self.iter < 0 || self.iter > ITER_MAX) {
          throw "Something bad is happening here!";
        }

        let buffer = new Uint8Array(1);
        buffer.fill(self.foo);
        self.controller.enqueue(buffer);

        if (--self.iter == 0) {
          controller.close();
        }
      },
    })
  );

  return apply_compartment(compartment, {
    func: "test_global_continue",
    args: r,
  });
}

async function test_global_continue(r, that) {
  let a = await r.arrayBuffer();

  that.is(
    Object.getPrototypeOf(a),
    that.ArrayBuffer.prototype,
    "Body is an array buffer"
  );
  that.is(a.byteLength, ITER_MAX, "Body length is correct");

  for (let i = 0; i < ITER_MAX; ++i) {
    that.is(new Uint8Array(a)[i], 42, "Byte " + i + " is correct");
  }
}

function workify(func) {
  info("Workifying " + func);

  return new Promise((resolve, reject) => {
    let worker = new Worker("worker_readableStreams.js");
    worker.postMessage(func);
    worker.onmessage = function (e) {
      if (e.data.type == "done") {
        resolve();
        return;
      }

      if (e.data.type == "error") {
        reject(e.data.message);
        return;
      }

      if (e.data.type == "test") {
        ok(e.data.test, e.data.message);
        return;
      }

      if (e.data.type == "info") {
        info(e.data.message);
      }
    };
  });
}
