// META: global=window,worker
// META: script=resources/readable-stream-from-array.js
// META: script=resources/readable-stream-to-array.js

'use strict';
const inputString = 'I \u{1F499} streams';
const expectedOutputBytes = [0x49, 0x20, 0xf0, 0x9f, 0x92, 0x99, 0x20, 0x73,
                             0x74, 0x72, 0x65, 0x61, 0x6d, 0x73];
// This is a character that must be represented in two code units in a string,
// ie. it is not in the Basic Multilingual Plane.
const astralCharacter = '\u{1F499}';  // BLUE HEART
const astralCharacterEncoded = [0xf0, 0x9f, 0x92, 0x99];
const leading = astralCharacter[0];
const trailing = astralCharacter[1];
const replacementEncoded = [0xef, 0xbf, 0xbd];

// These tests assume that the implementation correctly classifies leading and
// trailing surrogates and treats all the code units in each set equivalently.

const testCases = [
  {
    input: [inputString],
    output: [expectedOutputBytes],
    description: 'encoding one string of UTF-8 should give one complete chunk'
  },
  {
    input: [leading, trailing],
    output: [astralCharacterEncoded],
    description: 'a character split between chunks should be correctly encoded'
  },
  {
    input: [leading, trailing + astralCharacter],
    output: [astralCharacterEncoded.concat(astralCharacterEncoded)],
    description: 'a character following one split between chunks should be ' +
        'correctly encoded'
  },
  {
    input: [leading, trailing + leading, trailing],
    output: [astralCharacterEncoded, astralCharacterEncoded],
    description: 'two consecutive astral characters each split down the ' +
        'middle should be correctly reassembled'
  },
  {
    input: [leading, trailing + leading + leading, trailing],
    output: [astralCharacterEncoded.concat(replacementEncoded), astralCharacterEncoded],
    description: 'two consecutive astral characters each split down the ' +
        'middle with an invalid surrogate in the middle should be correctly ' +
        'encoded'
  },
  {
    input: [leading],
    output: [replacementEncoded],
    description: 'a stream ending in a leading surrogate should emit a ' +
        'replacement character as a final chunk'
  },
  {
    input: [leading, astralCharacter],
    output: [replacementEncoded.concat(astralCharacterEncoded)],
    description: 'an unmatched surrogate at the end of a chunk followed by ' +
        'an astral character in the next chunk should be replaced with ' +
        'the replacement character at the start of the next output chunk'
  },
  {
    input: [leading, 'A'],
    output: [replacementEncoded.concat([65])],
    description: 'an unmatched surrogate at the end of a chunk followed by ' +
        'an ascii character in the next chunk should be replaced with ' +
        'the replacement character at the start of the next output chunk'
  },
  {
    input: [leading, leading, trailing],
    output: [replacementEncoded, astralCharacterEncoded],
    description: 'an unmatched surrogate at the end of a chunk followed by ' +
        'a plane 1 character split into two chunks should result in ' +
        'the encoded plane 1 character appearing in the last output chunk'
  },
  {
    input: [leading, leading],
    output: [replacementEncoded, replacementEncoded],
    description: 'two leading chunks should result in two replacement ' +
        'characters'
  },
  {
    input: [leading + leading, trailing],
    output: [replacementEncoded, astralCharacterEncoded],
    description: 'a non-terminal unpaired leading surrogate should ' +
        'immediately be replaced'
  },
  {
    input: [trailing, astralCharacter],
    output: [replacementEncoded, astralCharacterEncoded],
    description: 'a terminal unpaired trailing surrogate should ' +
        'immediately be replaced'
  },
  {
    input: [leading, '', trailing],
    output: [astralCharacterEncoded],
    description: 'a leading surrogate chunk should be carried past empty chunks'
  },
  {
    input: [leading, ''],
    output: [replacementEncoded],
    description: 'a leading surrogate chunk should error when it is clear ' +
        'it didn\'t form a pair'
  },
  {
    input: [''],
    output: [],
    description: 'an empty string should result in no output chunk'
  },
  {
    input: ['', inputString],
    output: [expectedOutputBytes],
    description: 'a leading empty chunk should be ignored'
  },
  {
    input: [inputString, ''],
    output: [expectedOutputBytes],
    description: 'a trailing empty chunk should be ignored'
  },
  {
    input: ['A'],
    output: [[65]],
    description: 'a plain ASCII chunk should be converted'
  },
  {
    input: ['\xff'],
    output: [[195, 191]],
    description: 'characters in the ISO-8859-1 range should be encoded correctly'
  },
];

for (const {input, output, description} of testCases) {
  promise_test(async () => {
    const inputStream = readableStreamFromArray(input);
    const outputStream = inputStream.pipeThrough(new TextEncoderStream());
    const chunkArray = await readableStreamToArray(outputStream);
    assert_equals(chunkArray.length, output.length,
                  'number of chunks should match');
    for (let i = 0; i < output.length; ++i) {
      assert_array_equals(chunkArray[i], output[i], `chunk ${i} should match`);
    }
  }, description);
}
