/* eslint-disable mozilla/no-comparison-or-assignment-inside-ok */

function getType(a) {
  if (a === null || a === undefined) {
    return "null";
  }

  if (Array.isArray(a)) {
    return "array";
  }

  if (typeof a == "object") {
    return "object";
  }

  if (
    SpecialPowers.Cu.getJSTestingFunctions().wasmIsSupported() &&
    a instanceof WebAssembly.Module
  ) {
    return "wasm";
  }

  return "primitive";
}

function compare(a, b) {
  is(getType(a), getType(b), "Type matches");

  var type = getType(a);
  if (type == "array") {
    is(a.length, b.length, "Array.length matches");
    for (var i = 0; i < a.length; ++i) {
      compare(a[i], b[i]);
    }

    return;
  }

  if (type == "object") {
    ok(a !== b, "They should not match");

    var aProps = [];
    for (var p in a) {
      aProps.push(p);
    }

    var bProps = [];
    for (var p in b) {
      bProps.push(p);
    }

    is(aProps.length, bProps.length, "Props match");
    is(aProps.sort().toString(), bProps.sort().toString(), "Props names match");

    for (var p in a) {
      compare(a[p], b[p]);
    }

    return;
  }

  if (type == "wasm") {
    var wasmA = new WebAssembly.Instance(a);
    ok(wasmA instanceof WebAssembly.Instance, "got an instance");

    var wasmB = new WebAssembly.Instance(b);
    ok(wasmB instanceof WebAssembly.Instance, "got an instance");

    ok(wasmA.exports.foo() === wasmB.exports.foo(), "Same result!");
    ok(wasmB.exports.foo() === 42, "We want 42");
  }

  if (type != "null") {
    is(a, b, "Same value");
  }
}

var clonableObjects = [
  { target: "all", data: "hello world" },
  { target: "all", data: 123 },
  { target: "all", data: null },
  { target: "all", data: true },
  { target: "all", data: new Date() },
  { target: "all", data: [1, "test", true, new Date()] },
  {
    target: "all",
    data: { a: true, b: null, c: new Date(), d: [true, false, {}] },
  },
  { target: "all", data: new Blob([123], { type: "plain/text" }) },
  { target: "all", data: new ImageData(2, 2) },
];

function create_fileList() {
  var url = SimpleTest.getTestFileURL("script_postmessages_fileList.js");
  var script = SpecialPowers.loadChromeScript(url);

  function onOpened(message) {
    var fileList = document.getElementById("fileList");
    SpecialPowers.wrap(fileList).mozSetFileArray([message.file]);

    // Just a simple test
    var domFile = fileList.files[0];
    is(domFile.name, "prefs.js", "fileName should be prefs.js");

    clonableObjects.push({ target: "all", data: fileList.files });
    script.destroy();
    next();
  }

  script.addMessageListener("file.opened", onOpened);
  script.sendAsyncMessage("file.open");
}

function create_directory() {
  if (navigator.userAgent.toLowerCase().includes("Android")) {
    next();
    return;
  }

  var url = SimpleTest.getTestFileURL("script_postmessages_fileList.js");
  var script = SpecialPowers.loadChromeScript(url);

  function onOpened(message) {
    var fileList = document.getElementById("fileList");
    SpecialPowers.wrap(fileList).mozSetDirectory(message.dir);

    SpecialPowers.wrap(fileList)
      .getFilesAndDirectories()
      .then(function (list) {
        list = SpecialPowers.unwrap(list);
        // Just a simple test
        is(list.length, 1, "This list has 1 element");
        ok(list[0] instanceof Directory, "We have a directory.");

        clonableObjects.push({ target: "all", data: list[0] });
        script.destroy();
        next();
      });
  }

  script.addMessageListener("dir.opened", onOpened);
  script.sendAsyncMessage("dir.open");
}

function create_wasmModule() {
  info("Checking if we can play with WebAssembly...");

  if (!SpecialPowers.Cu.getJSTestingFunctions().wasmIsSupported()) {
    next();
    return;
  }

  ok(WebAssembly, "WebAssembly object should exist");
  ok(WebAssembly.compile, "WebAssembly.compile function should exist");

  /*
    js -e '
      t = wasmTextToBinary(`
        (module
          (func $foo (result i32) (i32.const 42))
          (export "foo" (func $foo))
        )
      `);
      print(t)
    '
  */
  // prettier-ignore
  const fooModuleCode = new Uint8Array([0,97,115,109,1,0,0,0,1,5,1,96,0,1,127,3,2,1,0,7,7,1,3,102,111,111,0,0,10,6,1,4,0,65,42,11,0,13,4,110,97,109,101,1,6,1,0,3,102,111,111]);

  WebAssembly.compile(fooModuleCode).then(
    m => {
      ok(m instanceof WebAssembly.Module, "The WasmModule has been compiled.");
      clonableObjects.push({ target: "sameProcess", data: m });
      next();
    },
    () => {
      ok(false, "The compilation of the wasmModule failed.");
    }
  );
}

function runTests(obj) {
  ok(
    "clonableObjectsEveryWhere" in obj &&
      "clonableObjectsSameProcess" in obj &&
      "transferableObjects" in obj &&
      (obj.clonableObjectsEveryWhere ||
        obj.clonableObjectsSameProcess ||
        obj.transferableObjects),
    "We must run some test!"
  );

  // cloning tests - everyWhere
  new Promise(function (resolve, reject) {
    var clonableObjectsId = 0;
    function runClonableTest() {
      if (clonableObjectsId >= clonableObjects.length) {
        resolve();
        return;
      }

      var object = clonableObjects[clonableObjectsId++];

      if (object.target != "all") {
        runClonableTest();
        return;
      }

      obj
        .send(object.data, [])
        .catch(() => {
          return { error: true };
        })
        .then(received => {
          if (!obj.clonableObjectsEveryWhere) {
            ok(received.error, "Error expected");
          } else {
            ok(!received.error, "Error not expected");
            compare(received.data, object.data);
          }
          runClonableTest();
        });
    }

    runClonableTest();
  })

    // clonable same process
    .then(function () {
      return new Promise(function (resolve, reject) {
        var clonableObjectsId = 0;
        function runClonableTest() {
          if (clonableObjectsId >= clonableObjects.length) {
            resolve();
            return;
          }

          var object = clonableObjects[clonableObjectsId++];

          if (object.target != "sameProcess") {
            runClonableTest();
            return;
          }

          obj
            .send(object.data, [])
            .catch(() => {
              return { error: true };
            })
            .then(received => {
              if (!obj.clonableObjectsSameProcess) {
                ok(received.error, "Error expected");
              } else {
                ok(!received.error, "Error not expected");
                compare(received.data, object.data);
              }
              runClonableTest();
            });
        }

        runClonableTest();
      });
    })

    // transfering tests
    .then(function () {
      if (!obj.transferableObjects) {
        return Promise.resolve();
      }

      // MessagePort
      return new Promise(function (r, rr) {
        var mc = new MessageChannel();
        obj.send(42, [mc.port1]).then(function (received) {
          is(received.ports.length, 1, "MessagePort has been transferred");
          mc.port2.postMessage("hello world");
          received.ports[0].onmessage = function (e) {
            is(e.data, "hello world", "Ports are connected!");
            r();
          };
        });
      });
    })

    // no dup transfering
    .then(function () {
      if (!obj.transferableObjects) {
        return Promise.resolve();
      }

      // MessagePort
      return new Promise(function (r, rr) {
        var mc = new MessageChannel();
        obj
          .send(42, [mc.port1, mc.port1])
          .then(
            function (received) {
              ok(false, "Duplicate ports should throw!");
            },
            function () {
              ok(true, "Duplicate ports should throw!");
            }
          )
          .then(r);
      });
    })

    // maintaining order of transferred ports
    .then(function () {
      if (!obj.transferableObjects) {
        return Promise.resolve();
      }

      // MessagePort
      return new Promise(function (r, rr) {
        var mcs = [];
        const NPORTS = 50;
        for (let i = 0; i < NPORTS; i++) {
          mcs.push(new MessageChannel());
        }
        obj
          .send(
            42,
            mcs.map(channel => channel.port1)
          )
          .then(function (received) {
            is(
              received.ports.length,
              NPORTS,
              `all ${NPORTS} ports transferred`
            );
            const promises = Array(NPORTS)
              .fill()
              .map(
                (_, i) =>
                  new Promise(function (subr, subrr) {
                    mcs[i].port2.postMessage(i);
                    received.ports[i].onmessage = e => subr(e.data == i);
                  })
              );
            return Promise.all(promises);
          })
          .then(function (result) {
            let in_order = 0;
            for (const correct of result) {
              if (correct) {
                in_order++;
              }
            }
            is(in_order, NPORTS, "All transferred ports are in order");
          })
          .then(r);
      });
    })

    // non transfering tests
    .then(function () {
      if (obj.transferableObjects) {
        return Promise.resolve();
      }

      // MessagePort
      return new Promise(function (r, rr) {
        var mc = new MessageChannel();
        obj
          .send(42, [mc.port1])
          .then(
            function (received) {
              ok(false, "This object should not support port transferring");
            },
            function () {
              ok(true, "This object should not support port transferring");
            }
          )
          .then(r);
      });
    })

    // done.
    .then(function () {
      obj.finished();
    });
}

function next() {
  if (!tests.length) {
    SimpleTest.finish();
    return;
  }

  var test = tests.shift();
  test();
}

var tests = [create_fileList, create_directory, create_wasmModule];
