// META: global=window,dedicatedworker,jsshell
// META: script=/wasm/jsapi/assertions.js
// META: script=/wasm/jsapi/wasm-module-builder.js
// META: script=/wasm/jsapi/js-string/polyfill.js

// Generate two sets of exports, one from a polyfill implementation and another
// from the builtins provided by the host.
let polyfillExports;
let builtinExports;
setup(() => {
  // Compile a module that exports a function for each builtin that will call
  // it. We could just generate a module that re-exports the builtins, but that
  // would not catch any special codegen that could happen when direct calling
  // a known builtin function from wasm.
  const builder = new WasmModuleBuilder();
  const arrayIndex = builder.addArray(kWasmI16, true, kNoSuperType, true);
  const builtins = [
    {
      name: "test",
      params: [kWasmExternRef],
      results: [kWasmI32],
    },
    {
      name: "cast",
      params: [kWasmExternRef],
      results: [wasmRefType(kWasmExternRef)],
    },
    {
      name: "fromCharCodeArray",
      params: [wasmRefNullType(arrayIndex), kWasmI32, kWasmI32],
      results: [wasmRefType(kWasmExternRef)],
    },
    {
      name: "intoCharCodeArray",
      params: [kWasmExternRef, wasmRefNullType(arrayIndex), kWasmI32],
      results: [kWasmI32],
    },
    {
      name: "fromCharCode",
      params: [kWasmI32],
      results: [wasmRefType(kWasmExternRef)],
    },
    {
      name: "fromCodePoint",
      params: [kWasmI32],
      results: [wasmRefType(kWasmExternRef)],
    },
    {
      name: "charCodeAt",
      params: [kWasmExternRef, kWasmI32],
      results: [kWasmI32],
    },
    {
      name: "codePointAt",
      params: [kWasmExternRef, kWasmI32],
      results: [kWasmI32],
    },
    {
      name: "length",
      params: [kWasmExternRef],
      results: [kWasmI32],
    },
    {
      name: "concat",
      params: [kWasmExternRef, kWasmExternRef],
      results: [wasmRefType(kWasmExternRef)],
    },
    {
      name: "substring",
      params: [kWasmExternRef, kWasmI32, kWasmI32],
      results: [wasmRefType(kWasmExternRef)],
    },
    {
      name: "equals",
      params: [kWasmExternRef, kWasmExternRef],
      results: [kWasmI32],
    },
    {
      name: "compare",
      params: [kWasmExternRef, kWasmExternRef],
      results: [kWasmI32],
    },
  ];

  // Add a function type for each builtin
  for (let builtin of builtins) {
    builtin.type = builder.addType({
      params: builtin.params,
      results: builtin.results
    });
  }

  // Add an import for each builtin
  for (let builtin of builtins) {
    builtin.importFuncIndex = builder.addImport(
      "wasm:js-string",
      builtin.name,
      builtin.type);
  }

  // Generate an exported function to call the builtin
  for (let builtin of builtins) {
    let func = builder.addFunction(builtin.name + "Imp", builtin.type);
    func.addLocals(builtin.params.length);
    let body = [];
    for (let i = 0; i < builtin.params.length; i++) {
      body.push(kExprLocalGet);
      body.push(...wasmSignedLeb(i));
    }
    body.push(kExprCallFunction);
    body.push(...wasmSignedLeb(builtin.importFuncIndex));
    func.addBody(body);
    func.exportAs(builtin.name);
  }

  const buffer = builder.toBuffer();

  // Instantiate this module using the builtins from the host
  const builtinModule = new WebAssembly.Module(buffer, {
    builtins: ["js-string"]
  });
  const builtinInstance = new WebAssembly.Instance(builtinModule, {});
  builtinExports = builtinInstance.exports;

  // Instantiate this module using the polyfill module
  const polyfillModule = new WebAssembly.Module(buffer);
  const polyfillInstance = new WebAssembly.Instance(polyfillModule, {
    "wasm:js-string": polyfillImports
  });
  polyfillExports = polyfillInstance.exports;
});

// A helper function to assert that the behavior of two functions are the
// same.
function assert_same_behavior(funcA, funcB, ...params) {
  let resultA;
  let errA = null;
  try {
    resultA = funcA(...params);
  } catch (err) {
    errA = err;
  }

  let resultB;
  let errB = null;
  try {
    resultB = funcB(...params);
  } catch (err) {
    errB = err;
  }

  if (errA || errB) {
    assert_equals(errA === null, errB === null, errA ? errA.message : errB.message);
    assert_equals(Object.getPrototypeOf(errA), Object.getPrototypeOf(errB));
  }
  assert_equals(resultA, resultB);

  if (errA) {
    throw errA;
  }
  return resultA;
}

function assert_throws_if(func, shouldThrow, constructor) {
  let error = null;
  try {
    func();
  } catch (e) {
    error = e;
  }
  assert_equals(error !== null, shouldThrow, "shouldThrow mismatch");
  if (shouldThrow && error !== null) {
    assert_true(error instanceof constructor);
  }
}

// Constant values used in the tests below
const testStrings = [
  "",
  "a",
  "1",
  "ab",
  "hello, world",
  "\n",
  "☺",
  "☺☺",
  String.fromCodePoint(0x10000, 0x10001)
];
const testCharCodes = [1, 2, 3, 10, 0x7f, 0xff, 0xfffe, 0xffff];
const testCodePoints = [1, 2, 3, 10, 0x7f, 0xff, 0xfffe, 0xffff, 0x10000, 0x10001];
const testExternRefValues = [
  null,
  undefined,
  true,
  false,
  {x:1337},
  ["abracadabra"],
  13.37,
  -0,
  0x7fffffff + 0.1,
  -0x7fffffff - 0.1,
  0x80000000 + 0.1,
  -0x80000000 - 0.1,
  0xffffffff + 0.1,
  -0xffffffff - 0.1,
  Number.EPSILON,
  Number.MAX_SAFE_INTEGER,
  Number.MIN_SAFE_INTEGER,
  Number.MIN_VALUE,
  Number.MAX_VALUE,
  Number.NaN,
  "hi",
  37n,
  new Number(42),
  new Boolean(true),
  Symbol("status"),
  () => 1337,
];

// Test that `test` and `cast` work on various JS values. Run all the
// other builtins and assert that they also perform equivalent type
// checks.
test(() => {
  for (let a of testExternRefValues) {
    let isString = assert_same_behavior(
      builtinExports['test'],
      polyfillExports['test'],
      a
    );

    assert_throws_if(() => assert_same_behavior(
        builtinExports['cast'],
        polyfillExports['cast'],
        a
      ), !isString, WebAssembly.RuntimeError);

    let arrayMutI16 = helperExports.createArrayMutI16(10);
    assert_throws_if(() => assert_same_behavior(
        builtinExports['intoCharCodeArray'],
        polyfillExports['intoCharCodeArray'],
        a, arrayMutI16, 0
      ), !isString, WebAssembly.RuntimeError);

    assert_throws_if(() => assert_same_behavior(
        builtinExports['charCodeAt'],
        polyfillExports['charCodeAt'],
        a, 0
      ), !isString, WebAssembly.RuntimeError);

    assert_throws_if(() => assert_same_behavior(
        builtinExports['codePointAt'],
        polyfillExports['codePointAt'],
        a, 0
      ), !isString, WebAssembly.RuntimeError);

    assert_throws_if(() => assert_same_behavior(
        builtinExports['length'],
        polyfillExports['length'],
        a
      ), !isString, WebAssembly.RuntimeError);

    assert_throws_if(() => assert_same_behavior(
        builtinExports['concat'],
        polyfillExports['concat'],
        a, a
      ), !isString, WebAssembly.RuntimeError);

    assert_throws_if(() => assert_same_behavior(
        builtinExports['substring'],
        polyfillExports['substring'],
        a, 0, 0
      ), !isString, WebAssembly.RuntimeError);

    assert_throws_if(() => assert_same_behavior(
        builtinExports['equals'],
        polyfillExports['equals'],
        a, a
      ), a !== null && !isString, WebAssembly.RuntimeError);

    assert_throws_if(() => assert_same_behavior(
        builtinExports['compare'],
        polyfillExports['compare'],
        a, a
      ), !isString, WebAssembly.RuntimeError);
  }
});

// Test that `fromCharCode` works on various char codes
test(() => {
  for (let a of testCharCodes) {
    assert_same_behavior(
      builtinExports['fromCharCode'],
      polyfillExports['fromCharCode'],
      a
    );
  }
});

// Test that `fromCodePoint` works on various code points
test(() => {
  for (let a of testCodePoints) {
    assert_same_behavior(
      builtinExports['fromCodePoint'],
      polyfillExports['fromCodePoint'],
      a
    );
  }
});

// Perform tests on various strings
test(() => {
  for (let a of testStrings) {
    let length = assert_same_behavior(
      builtinExports['length'],
      polyfillExports['length'],
      a
    );

    for (let i = 0; i < length; i++) {
      let charCode = assert_same_behavior(
        builtinExports['charCodeAt'],
        polyfillExports['charCodeAt'],
        a, i
      );
    }

    for (let i = 0; i < length; i++) {
      let charCode = assert_same_behavior(
        builtinExports['codePointAt'],
        polyfillExports['codePointAt'],
        a, i
      );
    }

    let arrayMutI16 = helperExports.createArrayMutI16(length);
    assert_same_behavior(
      builtinExports['intoCharCodeArray'],
      polyfillExports['intoCharCodeArray'],
      a, arrayMutI16, 0
    );

    assert_same_behavior(
      builtinExports['fromCharCodeArray'],
      polyfillExports['fromCharCodeArray'],
      arrayMutI16, 0, length
    );

    for (let i = 0; i < length; i++) {
      for (let j = 0; j < length; j++) {
        assert_same_behavior(
          builtinExports['substring'],
          polyfillExports['substring'],
          a, i, j
        );
      }
    }
  }
});

// Test various binary operations
test(() => {
  for (let a of testStrings) {
    for (let b of testStrings) {
      assert_same_behavior(
        builtinExports['concat'],
        polyfillExports['concat'],
        a, b
      );

      assert_same_behavior(
        builtinExports['equals'],
        polyfillExports['equals'],
        a, b
      );

      assert_same_behavior(
        builtinExports['compare'],
        polyfillExports['compare'],
        a, b
      );
    }
  }
});
