/**
 * @license
 * Copyright The Closure Library Authors.
 * SPDX-License-Identifier: Apache-2.0
 */

goog.module('goog.cryptTest');
goog.setTestOnly();

const PropertyReplacer = goog.require('goog.testing.PropertyReplacer');
const crypt = goog.require('goog.crypt');
const googString = goog.require('goog.string');
const recordFunction = goog.require('goog.testing.recordFunction');
const testSuite = goog.require('goog.testing.testSuite');

const UTF8_RANGES_BYTE_ARRAY =
    [0x00, 0x7F, 0xC2, 0x80, 0xDF, 0xBF, 0xE0, 0xA0, 0x80, 0xEF, 0xBF, 0xBF];

const UTF8_SURROGATE_PAIR_RANGES_BYTE_ARRAY = [
  0xF0, 0x90, 0x80, 0x80,  // \uD800\uDC00
  0xF0, 0x90, 0x8F, 0xBF,  // \uD800\uDFFF
  0xF4, 0x8F, 0xB0, 0x80,  // \uDBFF\uDC00
  0xF4, 0x8F, 0xBF, 0xBF   // \uDBFF\uDFFF
];

const UTF8_RANGES_STRING = '\u0000\u007F\u0080\u07FF\u0800\uFFFF';

const UTF8_SURROGATE_PAIR_RANGES_STRING =
    '\uD800\uDC00\uD800\uDFFF\uDBFF\uDC00\uDBFF\uDFFF';

// Tests a one-megabyte byte array conversion to string.
// This would break on many JS implementations unless byteArrayToString
// split the input up.
// See discussion and bug report: http://goo.gl/LrWmZ9

testSuite({
  testStringToUtf8ByteArray() {
    // Known encodings taken from Java's String.getBytes("UTF8")

    assertArrayEquals(
        'ASCII', [72, 101, 108, 108, 111, 44, 32, 119, 111, 114, 108, 100],
        crypt.stringToUtf8ByteArray('Hello, world'));

    assertArrayEquals(
        'Latin', [83, 99, 104, 195, 182, 110],
        crypt.stringToUtf8ByteArray('Sch\u00f6n'));

    assertArrayEquals(
        'limits of the first 3 UTF-8 character ranges', UTF8_RANGES_BYTE_ARRAY,
        crypt.stringToUtf8ByteArray(UTF8_RANGES_STRING));

    assertArrayEquals(
        'Surrogate Pair', UTF8_SURROGATE_PAIR_RANGES_BYTE_ARRAY,
        crypt.stringToUtf8ByteArray(UTF8_SURROGATE_PAIR_RANGES_STRING));
  },

  testTextToByteArray() {
    // Known encodings taken from Java's String.getBytes("UTF8")

    assertArrayEquals(
        'ASCII', [72, 101, 108, 108, 111, 44, 32, 119, 111, 114, 108, 100],
        crypt.textToByteArray('Hello, world'));

    assertArrayEquals(
        'Latin', [83, 99, 104, 195, 182, 110],
        crypt.textToByteArray('Sch\u00f6n'));

    assertArrayEquals(
        'limits of the first 3 UTF-8 character ranges', UTF8_RANGES_BYTE_ARRAY,
        crypt.textToByteArray(UTF8_RANGES_STRING));

    assertArrayEquals(
        'Surrogate Pair', UTF8_SURROGATE_PAIR_RANGES_BYTE_ARRAY,
        crypt.textToByteArray(UTF8_SURROGATE_PAIR_RANGES_STRING));
  },

  testUtf8ByteArrayToString() {
    // Known encodings taken from Java's String.getBytes("UTF8")

    assertEquals('ASCII', 'Hello, world', crypt.utf8ByteArrayToString([
      72,
      101,
      108,
      108,
      111,
      44,
      32,
      119,
      111,
      114,
      108,
      100,
    ]));

    assertEquals(
        'Latin', 'Sch\u00f6n',
        crypt.utf8ByteArrayToString([83, 99, 104, 195, 182, 110]));

    assertEquals(
        'limits of the first 3 UTF-8 character ranges', UTF8_RANGES_STRING,
        crypt.utf8ByteArrayToString(UTF8_RANGES_BYTE_ARRAY));

    assertEquals(
        'Surrogate Pair', UTF8_SURROGATE_PAIR_RANGES_STRING,
        crypt.utf8ByteArrayToString(UTF8_SURROGATE_PAIR_RANGES_BYTE_ARRAY));
  },

  testByteArrayToText() {
    // Known encodings taken from Java's String.getBytes("UTF8")

    assertEquals('ASCII', 'Hello, world', crypt.byteArrayToText([
      72,
      101,
      108,
      108,
      111,
      44,
      32,
      119,
      111,
      114,
      108,
      100,
    ]));

    assertEquals(
        'Latin', 'Sch\u00f6n',
        crypt.byteArrayToText([83, 99, 104, 195, 182, 110]));

    assertEquals(
        'limits of the first 3 UTF-8 character ranges', UTF8_RANGES_STRING,
        crypt.byteArrayToText(UTF8_RANGES_BYTE_ARRAY));

    assertEquals(
        'Surrogate Pair', UTF8_SURROGATE_PAIR_RANGES_STRING,
        crypt.byteArrayToText(UTF8_SURROGATE_PAIR_RANGES_BYTE_ARRAY));
  },

  /**
   * Same as testUtf8ByteArrayToString but with Uint8Array instead of
   * Array<number>.
   */
  testUint8ArrayToString() {
    if (!globalThis.Uint8Array) {
      // Uint8Array not supported.
      return;
    }

    let arr = new Uint8Array(
        [72, 101, 108, 108, 111, 44, 32, 119, 111, 114, 108, 100]);
    assertEquals('ASCII', 'Hello, world', crypt.utf8ByteArrayToString(arr));

    arr = new Uint8Array([83, 99, 104, 195, 182, 110]);
    assertEquals('Latin', 'Sch\u00f6n', crypt.utf8ByteArrayToString(arr));

    arr = new Uint8Array(UTF8_RANGES_BYTE_ARRAY);
    assertEquals(
        'limits of the first 3 UTF-8 character ranges', UTF8_RANGES_STRING,
        crypt.utf8ByteArrayToString(arr));
  },

  /**
   * Same as testByteArrayToText but with Uint8Array instead of
   * Array<number>.
   */
  testByteArrayToText_Uint8Array() {
    if (!globalThis.Uint8Array) {
      // Uint8Array not supported.
      return;
    }

    let arr = new Uint8Array(
        [72, 101, 108, 108, 111, 44, 32, 119, 111, 114, 108, 100]);
    assertEquals('ASCII', 'Hello, world', crypt.byteArrayToText(arr));

    arr = new Uint8Array([83, 99, 104, 195, 182, 110]);
    assertEquals('Latin', 'Sch\u00f6n', crypt.byteArrayToText(arr));

    arr = new Uint8Array(UTF8_RANGES_BYTE_ARRAY);
    assertEquals(
        'limits of the first 3 UTF-8 character ranges', UTF8_RANGES_STRING,
        crypt.byteArrayToText(arr));
  },

  testByteArrayToString() {
    assertEquals('', crypt.byteArrayToString([]));
    assertEquals('abc', crypt.byteArrayToString([97, 98, 99]));
    assertEquals('\xa0\x12\xa1', crypt.byteArrayToString([0xa0, 0x12, 0xa1]));
  },

  testByteArrayToBinaryString() {
    assertEquals('', crypt.byteArrayToBinaryString([]));
    assertEquals('abc', crypt.byteArrayToBinaryString([97, 98, 99]));
    assertEquals(
        '\xa0\x12\xa1', crypt.byteArrayToBinaryString([0xa0, 0x12, 0xa1]));
  },

  testStringToByteArray() {
    assertArrayEquals([], crypt.stringToByteArray(''));
    assertArrayEquals([97, 98, 99], crypt.stringToByteArray('abc'));
    assertArrayEquals(
        [0xa0, 0x12, 0xa1], crypt.stringToByteArray('\xa0\x12\xa1'));
    assertThrows(() => crypt.stringToByteArray('\u0102'));
  },

  testStringToByteArray_asyncThrow() {
    const stubs = new PropertyReplacer();
    const stubThrowException = recordFunction();
    stubs.replace(crypt.TEST_ONLY, 'throwException', stubThrowException);
    stubs.replace(crypt.TEST_ONLY, 'alwaysThrowSynchronously', false);
    try {
      assertArrayEquals([], crypt.stringToByteArray(''));
      assertArrayEquals([97, 98, 99], crypt.stringToByteArray('abc'));
      assertArrayEquals(
          [0xa0, 0x12, 0xa1], crypt.stringToByteArray('\xa0\x12\xa1'));
      if (stubThrowException.getCallCount() > 0) {
        throw stubThrowException.getLastCall().getArgument(0);
      }

      // Test async throwing behavior.
      assertArrayEquals([2, 1], crypt.stringToByteArray('\u0102'));
      assertEquals(1, stubThrowException.getCallCount());
    } finally {
      stubs.reset();
    }
  },

  testBinaryStringToByteArray() {
    assertArrayEquals([], crypt.binaryStringToByteArray(''));
    assertArrayEquals([97, 98, 99], crypt.binaryStringToByteArray('abc'));
    assertArrayEquals(
        [0xa0, 0x12, 0xa1], crypt.binaryStringToByteArray('\xa0\x12\xa1'));
    assertThrows(() => crypt.binaryStringToByteArray('\u0100'));
  },

  testHexToByteArray() {
    assertElementsEquals(
        [202, 254, 222, 173],
        // Java magic number
        crypt.hexToByteArray('cafedead'));

    assertElementsEquals(
        [222, 173, 190, 239],
        // IBM magic number
        crypt.hexToByteArray('DEADBEEF'));
  },

  testByteArrayToHex() {
    assertEquals(
        // Java magic number
        'cafedead', crypt.byteArrayToHex([202, 254, 222, 173]));

    assertEquals(
        // IBM magic number
        'deadbeef', crypt.byteArrayToHex([222, 173, 190, 239]));

    assertEquals('c0:ff:ee', crypt.byteArrayToHex([192, 255, 238], ':'));
  },

  /**
     Same as testByteArrayToHex but with Uint8Array instead of Array<number>.
   */
  testUint8ArrayToHex() {
    if (globalThis.Uint8Array === undefined) {
      // Uint8Array not supported.
      return;
    }

    assertEquals(
        // Java magic number
        'cafedead', crypt.byteArrayToHex(new Uint8Array([202, 254, 222, 173])));

    assertEquals(
        // IBM magic number
        'deadbeef', crypt.byteArrayToHex(new Uint8Array([222, 173, 190, 239])));

    assertEquals(
        'c0:ff:ee', crypt.byteArrayToHex(new Uint8Array([192, 255, 238]), ':'));
  },

  testXorByteArray() {
    assertElementsEquals(
        [20, 83, 96, 66],
        crypt.xorByteArray([202, 254, 222, 173], [222, 173, 190, 239]));
  },

  /** Same as testXorByteArray but with Uint8Array instead of Array<number>. */
  testXorUint8Array() {
    if (globalThis.Uint8Array === undefined) {
      // Uint8Array not supported.
      return;
    }

    assertElementsEquals(
        [20, 83, 96, 66],
        crypt.xorByteArray(
            new Uint8Array([202, 254, 222, 173]),
            new Uint8Array([222, 173, 190, 239])));
  },

  testByteArrayToStringCallStack() {
    // One megabyte is 2 to the 20th.
    const count = Math.pow(2, 20);
    const bytes = [];
    for (let i = 0; i < count; i++) {
      bytes.push('A'.charCodeAt(0));
    }
    const str = crypt.byteArrayToString(bytes);
    assertEquals(googString.repeat('A', count), str);
  },
});
