// Tests for wasm exception proposal JS API features.

load(libdir + "eqArrayHelper.js");

// WebAssembly.Tag tests.
assertErrorMessage(
  () => WebAssembly.Tag(),
  TypeError,
  /calling a builtin Tag constructor without new is forbidden/
);

assertErrorMessage(
  () => new WebAssembly.Tag(),
  TypeError,
  /At least 1 argument required/
);

assertErrorMessage(
  () => new WebAssembly.Tag(3),
  TypeError,
  /first argument must be a tag descriptor/
);

assertErrorMessage(
  () => new WebAssembly.Tag({ parameters: ["foobar"] }),
  TypeError,
  /bad value type/
);

new WebAssembly.Tag({ parameters: [] });
new WebAssembly.Tag({ parameters: ["i32"] });
new WebAssembly.Tag({ parameters: ["i32", "externref"] });

wasmEvalText(`(module (import "m" "e" (tag)))`, {
  m: { e: new WebAssembly.Tag({ parameters: [] }) },
});

wasmEvalText(`(module (import "m" "e" (tag (param i32))))`, {
  m: { e: new WebAssembly.Tag({ parameters: ["i32"] }) },
});

wasmEvalText(`(module (import "m" "e" (tag (param i32 i64))))`, {
  m: { e: new WebAssembly.Tag({ parameters: ["i32", "i64"] }) },
});

assertErrorMessage(
  () =>
    wasmEvalText(`(module (import "m" "e" (tag (param i32))))`, {
      m: { e: new WebAssembly.Tag({ parameters: [] }) },
    }),
  WebAssembly.LinkError,
  /imported tag 'm.e' signature mismatch/
);

assertErrorMessage(
  () =>
    wasmEvalText(`(module (import "m" "e" (tag (param))))`, {
      m: { e: new WebAssembly.Tag({ parameters: ["i32"] }) },
    }),
  WebAssembly.LinkError,
  /imported tag 'm.e' signature mismatch/
);

// Test WebAssembly.Tag methods.
// TODO: add runtime detection for js-types
// {
//   let params = [
//     [],
//     ["i32"],
//     ["i32", "i64"],
//     ["f32", "externref"],
//     ["i32", "i64", "f32", "f64"],
//   ];

//   for (const arg of params) {
//     const tag = new WebAssembly.Tag({ parameters: arg });
//     assertEqArray(tag.type().parameters, arg);
//   }
// }

// WebAssembly.Exception tests.
assertErrorMessage(
  () => WebAssembly.Exception(),
  TypeError,
  /calling a builtin Exception constructor without new is forbidden/
);

assertErrorMessage(
  () => new WebAssembly.Exception(),
  TypeError,
  /At least 2 arguments required/
);

assertErrorMessage(
  () => new WebAssembly.Exception(3, []),
  TypeError,
  /first argument must be a WebAssembly.Tag/
);

const { tag1, tag2, tag3, tag4, tag5, tag6, tag7, tag8, tag9 } = wasmEvalText(
  `(module
     (tag (export "tag1") (param))
     (tag (export "tag2") (param i32))
     (tag (export "tag3") (param i32 f32))
     (tag (export "tag4") (param i32 externref i32))
     (tag (export "tag5") (param i32 externref i32 externref))
     (tag (export "tag6") (param funcref))
     (tag (export "tag7") (param i64))
     (tag (export "tag8") (param i32 f64))
     (tag (export "tag9") (param externref funcref)))`
).exports;

new WebAssembly.Exception(tag1, []);
new WebAssembly.Exception(tag2, [3]);
new WebAssembly.Exception(tag3, [3, 5.5]);
new WebAssembly.Exception(tag4, [3, "foo", 4]);
new WebAssembly.Exception(tag5, [3, "foo", 4, "bar"]);

assertErrorMessage(
  () => new WebAssembly.Exception(tag2, []),
  TypeError,
  /expected 1 values but got 0/
);

assertErrorMessage(
  () => new WebAssembly.Exception(tag2, [3n]),
  TypeError,
  /can't convert BigInt to number/
);

assertErrorMessage(
  () => new WebAssembly.Exception(tag6, [undefined]),
  TypeError,
  /can only pass WebAssembly exported functions to funcref/
);

assertErrorMessage(
  () => new WebAssembly.Exception(tag7, [undefined]),
  TypeError,
  /can't convert undefined to BigInt/
);

assertErrorMessage(
  () => new WebAssembly.Exception(tag7, {}),
  TypeError,
  /\({}\) is not iterable/
);

assertErrorMessage(
  () => new WebAssembly.Exception(tag7, 1),
  TypeError,
  /second argument must be an object/
);

// Test Exception methods.
{
  const exn1 = new WebAssembly.Exception(tag1, []);
  assertEq(exn1.is(tag1), true);
  assertEq(exn1.is(tag2), false);
  assertErrorMessage(
    () => exn1.is(),
    TypeError,
    /At least 1 argument required/
  );
  assertErrorMessage(
    () => exn1.is(5),
    TypeError,
    /first argument must be a WebAssembly.Tag/
  );

  const exn2 = new WebAssembly.Exception(tag2, [3]);
  assertEq(exn2.getArg(tag2, 0), 3);

  assertEq(new WebAssembly.Exception(tag2, [undefined]).getArg(tag2, 0), 0);

  const exn4 = new WebAssembly.Exception(tag4, [3, "foo", 4]);
  assertEq(exn4.getArg(tag4, 0), 3);
  assertEq(exn4.getArg(tag4, 1), "foo");
  assertEq(exn4.getArg(tag4, 2), 4);

  const exn5 = new WebAssembly.Exception(tag5, [3, "foo", 4, "bar"]);
  assertEq(exn5.getArg(tag5, 3), "bar");

  const { funcref } = wasmEvalText(
    `(module (func (export "funcref")))`
  ).exports;
  const exn9 = new WebAssembly.Exception(tag9, ["foo", funcref]);
  assertEq(exn9.getArg(tag9, 0), "foo");
  assertEq(exn9.getArg(tag9, 1), funcref);

  assertErrorMessage(
    () => exn2.getArg(),
    TypeError,
    /At least 2 arguments required/
  );
  assertErrorMessage(
    () => exn2.getArg(5, 0),
    TypeError,
    /first argument must be a WebAssembly.Tag/
  );
  assertErrorMessage(
    () => exn2.getArg(tag2, "foo"),
    TypeError,
    /bad Exception getArg index/
  );
  assertErrorMessage(
    () => exn2.getArg(tag2, 10),
    RangeError,
    /bad Exception getArg index/
  );
}

// Test throwing a JS constructed exception to Wasm.
assertEq(
  wasmEvalText(
    `(module
       (import "m" "exn" (tag $exn (param i32)))
       (import "m" "f" (func $f))
       (func (export "f") (result i32)
         try (result i32)
           call $f
           (i32.const 0)
         catch $exn
         end))`,
    {
      m: {
        exn: tag2,
        f: () => {
          throw new WebAssembly.Exception(tag2, [42]);
        },
      },
    }
  ).exports.f(),
  42
);

assertEqArray(
  wasmEvalText(
    `(module
       (import "m" "exn" (tag $exn (param i32 f32)))
       (import "m" "f" (func $f))
       (func (export "f") (result i32 f32)
         try (result i32 f32)
           call $f
           (i32.const 0)
           (f32.const 0)
         catch $exn
         end))`,
    {
      m: {
        exn: tag3,
        f: () => {
          throw new WebAssembly.Exception(tag3, [42, 5.5]);
        },
      },
    }
  ).exports.f(),
  [42, 5.5]
);

assertEqArray(
  wasmEvalText(
    `(module
       (import "m" "exn" (tag $exn (param i32 f64)))
       (import "m" "f" (func $f))
       (func (export "f") (result i32 f64)
         try (result i32 f64)
           call $f
           (i32.const 0)
           (f64.const 0)
         catch $exn
         end))`,
    {
      m: {
        exn: tag8,
        f: () => {
          throw new WebAssembly.Exception(tag8, [9999, 9999]);
        },
      },
    }
  ).exports.f(),
  [9999, 9999]
);

assertEqArray(
  wasmEvalText(
    `(module
       (import "m" "exn" (tag $exn (param i32 externref i32)))
       (import "m" "f" (func $f))
       (func (export "f") (result i32 externref i32)
         try (result i32 externref i32)
           call $f
           (i32.const 0)
           (ref.null extern)
           (i32.const 0)
         catch $exn
         end))`,
    {
      m: {
        exn: tag4,
        f: () => {
          throw new WebAssembly.Exception(tag4, [42, "foo", 42]);
        },
      },
    }
  ).exports.f(),
  [42, "foo", 42]
);

assertEqArray(
  wasmEvalText(
    `(module
       (import "m" "exn" (tag $exn (param i32 externref i32 externref)))
       (import "m" "f" (func $f))
       (func (export "f") (result i32 externref i32 externref)
         try (result i32 externref i32 externref)
           call $f
           (i32.const 0)
           (ref.null extern)
           (i32.const 0)
           (ref.null extern)
         catch $exn
         end))`,
    {
      m: {
        exn: tag5,
        f: () => {
          throw new WebAssembly.Exception(tag5, [42, "foo", 42, "bar"]);
        },
      },
    }
  ).exports.f(),
  [42, "foo", 42, "bar"]
);

{
  const { funcref } = wasmEvalText(
    `(module (func (export "funcref")))`
  ).exports;
  assertEqArray(
    wasmEvalText(
      `(module
         (import "m" "exn" (tag $exn (param externref funcref)))
         (import "m" "f" (func $f))
         (func (export "f") (result externref funcref)
           try (result externref funcref)
             call $f
             (ref.null extern)
             (ref.null func)
           catch $exn
           end))`,
      {
        m: {
          exn: tag9,
          f: () => {
            throw new WebAssembly.Exception(tag9, ["foo", funcref]);
          },
        },
      }
    ).exports.f(),
    ["foo", funcref]
  );
}

assertEq(
  wasmEvalText(
    `(module
       (import "m" "exn" (tag $exn))
       (import "m" "f" (func $f))
       (func (export "f") (result i32)
         try (result i32)
           call $f
           (i32.const 0)
         catch $exn
           (i32.const 0)
         catch_all
           (i32.const 1)
         end))`,
    {
      m: {
        exn: tag1,
        f: () => {
          throw new WebAssembly.Exception(tag2, [42]);
        },
      },
    }
  ).exports.f(),
  1
);

{
  const exn = new WebAssembly.Tag({ parameters: ["i32"] });
  assertEq(
    wasmEvalText(
      `(module
         (import "m" "exn" (tag $exn (param i32)))
         (import "m" "f" (func $f))
         (func (export "f") (result i32)
           try (result i32)
             call $f
             (i32.const 0)
           catch $exn
           end))`,
      {
        m: {
          exn,
          f: () => {
            throw new WebAssembly.Exception(exn, [42]);
          },
        },
      }
    ).exports.f(),
    42
  );
}

{
  const exn1 = new WebAssembly.Tag({ parameters: ["i32"] });
  const exn2 = new WebAssembly.Tag({ parameters: ["i32"] });
  assertEq(
    wasmEvalText(
      `(module
         (import "m" "exn" (tag $exn (param i32)))
         (import "m" "f" (func $f))
         (func (export "f") (result i32)
           try (result i32)
             call $f
             (i32.const 0)
           catch $exn
           catch_all
             (i32.const 1)
           end))`,
      {
        m: {
          exn: exn1,
          f: () => {
            throw new WebAssembly.Exception(exn2, [42]);
          },
        },
      }
    ).exports.f(),
    1
  );
}

// Test `getArg` on a Wasm-thrown exception.
assertEq(
  (() => {
    try {
      wasmEvalText(
        `(module
           (import "m" "exn" (tag $exn (param i32 f64)))
           (func (export "f")
             (i32.const 9999)
             (f64.const 9999)
             throw $exn))`,
        { m: { exn: tag8 } }
      ).exports.f();
    } catch (exn) {
      return exn.getArg(tag8, 1);
    }
  })(),
  9999
);

assertEqArray(
  (() => {
    try {
      wasmEvalText(
        `(module
           (import "m" "exn" (tag $exn (param i32 externref i32 externref)))
           (func (export "f") (param externref externref)
             (i32.const 1)
             (local.get 0)
             (i32.const 2)
             (local.get 1)
             throw $exn))`,
        { m: { exn: tag5 } }
      ).exports.f("foo", "bar");
    } catch (exn) {
      return [
        exn.getArg(tag5, 0),
        exn.getArg(tag5, 1),
        exn.getArg(tag5, 2),
        exn.getArg(tag5, 3),
      ];
    }
  })(),
  [1, "foo", 2, "bar"]
);
