let testModule = `(module
  (type $arrayMutI16 (array (mut i16)))

  (func $testImp
    (import "wasm:js-string" "test")
    (param externref)
    (result i32)
  )
  (func $castImp
    (import "wasm:js-string" "cast")
    (param externref)
    (result (ref extern))
  )
  (func $fromCharCodeArrayImp
    (import "wasm:js-string" "fromCharCodeArray")
    (param (ref null $arrayMutI16) i32 i32)
    (result (ref extern))
  )
  (func $intoCharCodeArrayImp
    (import "wasm:js-string" "intoCharCodeArray")
    (param externref (ref null $arrayMutI16) i32)
    (result i32)
  )
  (func $fromCharCodeImp
    (import "wasm:js-string" "fromCharCode")
    (param i32)
    (result (ref extern))
  )
  (func $fromCodePointImp
    (import "wasm:js-string" "fromCodePoint")
    (param i32)
    (result (ref extern))
  )
  (func $charCodeAtImp
    (import "wasm:js-string" "charCodeAt")
    (param externref i32)
    (result i32)
  )
  (func $codePointAtImp
    (import "wasm:js-string" "codePointAt")
    (param externref i32)
    (result i32)
  )
  (func $lengthImp
    (import "wasm:js-string" "length")
    (param externref)
    (result i32)
  )
  (func $concatImp
    (import "wasm:js-string" "concat")
    (param externref externref)
    (result (ref extern))
  )
  (func $substringImp
    (import "wasm:js-string" "substring")
    (param externref i32 i32)
    (result (ref extern))
  )
  (func $equalsImp
    (import "wasm:js-string" "equals")
    (param externref externref)
    (result i32)
  )
  (func $compareImp
    (import "wasm:js-string" "compare")
    (param externref externref)
    (result i32)
  )

  (func $test (export "test")
    (param externref)
    (result i32)
    local.get 0
    call $testImp
  )
  (func $cast (export "cast")
    (param externref)
    (result (ref extern))
    local.get 0
    call $castImp
  )
  (func $fromCharCodeArray (export "fromCharCodeArray")
    (param (ref null $arrayMutI16) i32 i32)
    (result (ref extern))
    local.get 0
    local.get 1
    local.get 2
    call $fromCharCodeArrayImp
  )
  (func $intoCharCodeArray (export "intoCharCodeArray")
    (param externref (ref null $arrayMutI16) i32)
    (result i32)
    local.get 0
    local.get 1
    local.get 2
    call $intoCharCodeArrayImp
  )
  (func $fromCharCode (export "fromCharCode")
    (param i32)
    (result externref)
    local.get 0
    call $fromCharCodeImp
  )
  (func $fromCodePoint (export "fromCodePoint")
    (param i32)
    (result externref)
    local.get 0
    call $fromCodePointImp
  )
  (func $charCodeAt (export "charCodeAt")
    (param externref i32)
    (result i32)
    local.get 0
    local.get 1
    call $charCodeAtImp
  )
  (func $codePointAt (export "codePointAt")
    (param externref i32)
    (result i32)
    local.get 0
    local.get 1
    call $codePointAtImp
  )
  (func $length (export "length")
    (param externref)
    (result i32)
    local.get 0
    call $lengthImp
  )
  (func $concat (export "concat")
    (param externref externref)
    (result externref)
    local.get 0
    local.get 1
    call $concatImp
  )
  (func $substring (export "substring")
    (param externref i32 i32)
    (result externref)
    local.get 0
    local.get 1
    local.get 2
    call $substringImp
  )
  (func $equals (export "equals")
    (param externref externref)
    (result i32)
    local.get 0
    local.get 1
    call $equalsImp
  )
  (func $compare (export "compare")
    (param externref externref)
    (result i32)
    local.get 0
    local.get 1
    call $compareImp
  )
)`;

let {
  createArrayMutI16,
  arrayLength,
  arraySet,
  arrayGet
} = wasmEvalText(`(module
  (type $arrayMutI16 (array (mut i16)))
  (func (export "createArrayMutI16") (param i32) (result anyref)
    i32.const 0
    local.get 0
    array.new $arrayMutI16
  )
  (func (export "arrayLength") (param arrayref) (result i32)
    local.get 0
    array.len
  )
  (func (export "arraySet") (param (ref $arrayMutI16) i32 i32)
    local.get 0
    local.get 1
    local.get 2
    array.set $arrayMutI16
  )
  (func (export "arrayGet") (param (ref $arrayMutI16) i32) (result i32)
    local.get 0
    local.get 1
    array.get_u $arrayMutI16
  )
)`).exports;

function throwIfNotString(a) {
  if (typeof a !== "string") {
    throw new WebAssembly.RuntimeError();
  }
}
function throwIfNotStringOrNull(a) {
  if (a !== null && typeof a !== "string") {
    throw new WebAssembly.RuntimeError();
  }
}
let polyFillImports = {
  test: (string) => {
    if (string === null ||
        typeof string !== "string") {
      return 0;
    }
    return 1;
  },
  cast: (string) => {
    if (string === null ||
        typeof string !== "string") {
      throw new WebAssembly.RuntimeError();
    }
    return string;
  },
  fromCharCodeArray: (array, arrayStart, arrayEnd) => {
    arrayStart >>>= 0;
    arrayEnd >>>= 0;
    if (array == null ||
        arrayStart > arrayEnd ||
        arrayEnd > arrayLength(array)) {
      throw new WebAssembly.RuntimeError();
    }
    let result = '';
    for (let i = arrayStart; i < arrayEnd; i++) {
      result += String.fromCharCode(arrayGet(array, i));
    }
    return result;
  },
  intoCharCodeArray: (string, arr, arrayStart) => {
    arrayStart >>>= 0;
    throwIfNotString(string);
    if (arr == null) {
      throw new WebAssembly.RuntimeError();
    }
    let arrLength = arrayLength(arr);
    let stringLength = string.length;
    if (BigInt(arrayStart) + BigInt(stringLength) > BigInt(arrLength)) {
      throw new WebAssembly.RuntimeError();
    }
    for (let i = 0; i < stringLength; i++) {
      arraySet(arr, arrayStart + i, string[i].charCodeAt(0));
    }
    return stringLength;
  },
  fromCharCode: (charCode) => {
    charCode >>>= 0;
    return String.fromCharCode(charCode);
  },
  fromCodePoint: (codePoint) => {
    codePoint >>>= 0;
    return String.fromCodePoint(codePoint);
  },
  charCodeAt: (string, stringIndex) => {
    stringIndex >>>= 0;
    throwIfNotString(string);
    if (stringIndex >= string.length)
      throw new WebAssembly.RuntimeError();
    return string.charCodeAt(stringIndex);
  },
  codePointAt: (string, stringIndex) => {
    stringIndex >>>= 0;
    throwIfNotString(string);
    if (stringIndex >= string.length)
      throw new WebAssembly.RuntimeError();
    return string.codePointAt(stringIndex);
  },
  length: (string) => {
    throwIfNotString(string);
    return string.length;
  },
  concat: (stringA, stringB) => {
    throwIfNotString(stringA);
    throwIfNotString(stringB);
    return stringA + stringB;
  },
  substring: (string, startIndex, endIndex) => {
    startIndex >>>= 0;
    endIndex >>>= 0;
    throwIfNotString(string);
    if (startIndex > string.length ||
        endIndex < startIndex) {
      return "";
    }
    if (endIndex > string.length) {
      endIndex = string.length;
    }
    return string.substring(startIndex, endIndex);
  },
  equals: (stringA, stringB) => {
    throwIfNotStringOrNull(stringA);
    throwIfNotStringOrNull(stringB);
    return stringA === stringB;
  },
  compare: (stringA, stringB) => {
    throwIfNotString(stringA);
    throwIfNotString(stringB);
    if (stringA < stringB) {
      return -1;
    }
    return stringA === stringB ? 0 : 1;
  },
};

function assertSameBehavior(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) {
    assertEq(errA === null, errB === null, errA ? errA.message : errB.message);
    assertEq(Object.getPrototypeOf(errA), Object.getPrototypeOf(errB));
  }
  assertEq(resultA, resultB);

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

let builtinExports = wasmEvalText(testModule, {}, {builtins: ["js-string"]}).exports;
let polyfillExports = wasmEvalText(testModule, { 'wasm:js-string': polyFillImports }).exports;

let testStrings = ["", "a", "1", "ab", "hello, world", "\n", "☺", "☺smiley", String.fromCodePoint(0x10000, 0x10001)];
let testStringsAndNull = [...testStrings, null];
let testCharCodes = [1, 2, 3, 10, 0x7f, 0xff, 0xfffe, 0xffff];
let testCodePoints = [1, 2, 3, 10, 0x7f, 0xff, 0xfffe, 0xffff, 0x10000, 0x10001];

for (let a of WasmExternrefValues) {
  assertSameBehavior(
    builtinExports['test'],
    polyfillExports['test'],
    a
  );
  try {
    assertSameBehavior(
      builtinExports['cast'],
      polyfillExports['cast'],
      a
    );
  } catch (err) {
    assertEq(err instanceof WebAssembly.RuntimeError, true);
  }
}

for (let a of testCharCodes) {
  assertSameBehavior(
    builtinExports['fromCharCode'],
    polyfillExports['fromCharCode'],
    a
  );
}

for (let a of testCodePoints) {
  assertSameBehavior(
    builtinExports['fromCodePoint'],
    polyfillExports['fromCodePoint'],
    a
  );
}

for (let a of testStrings) {
  let length = assertSameBehavior(
    builtinExports['length'],
    polyfillExports['length'],
    a
  );

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

  let arrayMutI16 = createArrayMutI16(length);
  assertSameBehavior(
    builtinExports['intoCharCodeArray'],
    polyfillExports['intoCharCodeArray'],
    a, arrayMutI16, 0
  );
  assertSameBehavior(
    builtinExports['fromCharCodeArray'],
    polyfillExports['fromCharCodeArray'],
    arrayMutI16, 0, length
  );

  for (let i = 0; i < length; i++) {
    // The end parameter is interpreted as unsigned and is always clamped to
    // the string length. This means that -1, and string.length + 1 are valid
    // end indices.
    for (let j = -1; j <= length + 1; j++) {
      assertSameBehavior(
        builtinExports['substring'],
        polyfillExports['substring'],
        a, i, j
      );
    }
  }
}

for (let a of testStrings) {
  for (let b of testStrings) {
    assertSameBehavior(
      builtinExports['concat'],
      polyfillExports['concat'],
      a, b
    );
    assertSameBehavior(
      builtinExports['compare'],
      polyfillExports['compare'],
      a, b
    );
  }
}

for (let a of testStringsAndNull) {
  for (let b of testStringsAndNull) {
    assertSameBehavior(
      builtinExports['equals'],
      polyfillExports['equals'],
      a, b
    );
  }
}

// equals must trap on non-string non-null inputs
for (let a of WasmExternrefValues) {
  if (a === null || typeof a === "string") {
    continue;
  }
  for (let b of testStringsAndNull) {
    assertErrorMessage(() => assertSameBehavior(
      builtinExports['equals'],
      polyfillExports['equals'],
      a, b
    ), WebAssembly.RuntimeError, /./);
    assertErrorMessage(() => assertSameBehavior(
      builtinExports['equals'],
      polyfillExports['equals'],
      b, a
    ), WebAssembly.RuntimeError, /./);
  }
}

// fromCharCodeArray endIndex is an unsigned integer
{
  let arrayMutI16 = createArrayMutI16(1);
  assertErrorMessage(() => assertSameBehavior(
    builtinExports['fromCharCodeArray'],
    polyfillExports['fromCharCodeArray'],
    arrayMutI16, 1, -1
  ), WebAssembly.RuntimeError, /./);
}

// fromCharCodeArray is startIndex and endIndex, not a count
{
  let arrayMutI16 = createArrayMutI16(1);
  // Ask for [1, 1) to get an empty string. If misinterpreted as a count, this
  // will result in a trap.
  assertEq(assertSameBehavior(
    builtinExports['fromCharCodeArray'],
    polyfillExports['fromCharCodeArray'],
    arrayMutI16, 1, 1
  ), "");
}

// fromCharCodeArray array is null
{
  assertErrorMessage(() => assertSameBehavior(
    builtinExports['fromCharCodeArray'],
    polyfillExports['fromCharCodeArray'],
    null, 0, 0
  ), WebAssembly.RuntimeError, /./);
}

// intoCharCodeArray array is null
{
  assertErrorMessage(() => assertSameBehavior(
    builtinExports['intoCharCodeArray'],
    polyfillExports['intoCharCodeArray'],
    "test", null, 0,
  ), WebAssembly.RuntimeError, /./);
}
