/**
 * Copyright (c) Meta Platforms, Inc. and affiliates.
 *
 * This source code is licensed under the MIT license found in the
 * LICENSE file in the root directory of this source tree.
 *
 * @emails react-core
 */

'use strict';

// Polyfills for test environment
global.ReadableStream =
  require('web-streams-polyfill/ponyfill/es6').ReadableStream;
global.TextEncoder = require('util').TextEncoder;
global.TextDecoder = require('util').TextDecoder;

// let serverExports;
let webpackServerMap;
let ReactServerDOMServer;
let ReactServerDOMClient;

describe('ReactFlightDOMReply', () => {
  beforeEach(() => {
    jest.resetModules();
    // Simulate the condition resolution
    jest.mock('react', () => require('react/react.shared-subset'));
    jest.mock('react-server-dom-webpack/server', () =>
      require('react-server-dom-webpack/server.browser'),
    );
    const WebpackMock = require('./utils/WebpackMock');
    // serverExports = WebpackMock.serverExports;
    webpackServerMap = WebpackMock.webpackServerMap;
    ReactServerDOMServer = require('react-server-dom-webpack/server.browser');
    jest.resetModules();
    ReactServerDOMClient = require('react-server-dom-webpack/client');
  });

  // This method should exist on File but is not implemented in JSDOM
  async function arrayBuffer(file) {
    return new Promise((resolve, reject) => {
      const reader = new FileReader();
      reader.onload = function () {
        return resolve(reader.result);
      };
      reader.onerror = function () {
        return reject(reader.error);
      };
      reader.readAsArrayBuffer(file);
    });
  }

  it('can pass undefined as a reply', async () => {
    const body = await ReactServerDOMClient.encodeReply(undefined);
    const missing = await ReactServerDOMServer.decodeReply(
      body,
      webpackServerMap,
    );
    expect(missing).toBe(undefined);

    const body2 = await ReactServerDOMClient.encodeReply({
      array: [undefined, null, undefined],
      prop: undefined,
    });
    const object = await ReactServerDOMServer.decodeReply(
      body2,
      webpackServerMap,
    );
    expect(object.array.length).toBe(3);
    expect(object.array[0]).toBe(undefined);
    expect(object.array[1]).toBe(null);
    expect(object.array[3]).toBe(undefined);
    expect(object.prop).toBe(undefined);
    // These should really be true but our deserialization doesn't currently deal with it.
    expect('3' in object.array).toBe(false);
    expect('prop' in object).toBe(false);
  });

  it('can pass an iterable as a reply', async () => {
    const body = await ReactServerDOMClient.encodeReply({
      [Symbol.iterator]: function* () {
        yield 'A';
        yield 'B';
        yield 'C';
      },
    });
    const iterable = await ReactServerDOMServer.decodeReply(
      body,
      webpackServerMap,
    );
    const items = [];
    // eslint-disable-next-line no-for-of-loops/no-for-of-loops
    for (const item of iterable) {
      items.push(item);
    }
    expect(items).toEqual(['A', 'B', 'C']);
  });

  it('can pass weird numbers as a reply', async () => {
    const nums = [0, -0, Infinity, -Infinity, NaN];
    const body = await ReactServerDOMClient.encodeReply(nums);
    const nums2 = await ReactServerDOMServer.decodeReply(
      body,
      webpackServerMap,
    );

    expect(nums).toEqual(nums2);
    expect(nums.every((n, i) => Object.is(n, nums2[i]))).toBe(true);
  });

  it('can pass a BigInt as a reply', async () => {
    const body = await ReactServerDOMClient.encodeReply(90071992547409910000n);
    const n = await ReactServerDOMServer.decodeReply(body, webpackServerMap);

    expect(n).toEqual(90071992547409910000n);
  });

  it('can pass FormData as a reply', async () => {
    const formData = new FormData();
    formData.set('hello', 'world');
    formData.append('list', '1');
    formData.append('list', '2');
    formData.append('list', '3');
    const typedArray = new Uint8Array([0, 1, 2, 3]);
    const blob = new Blob([typedArray]);
    formData.append('blob', blob, 'filename.blob');

    const body = await ReactServerDOMClient.encodeReply(formData);
    const formData2 = await ReactServerDOMServer.decodeReply(
      body,
      webpackServerMap,
    );

    expect(formData2).not.toBe(formData);
    expect(Array.from(formData2).length).toBe(5);
    expect(formData2.get('hello')).toBe('world');
    expect(formData2.getAll('list')).toEqual(['1', '2', '3']);
    const blob2 = formData.get('blob');
    expect(blob2.size).toBe(4);
    expect(blob2.name).toBe('filename.blob');
    expect(blob2.type).toBe('');
    const typedArray2 = new Uint8Array(await arrayBuffer(blob2));
    expect(typedArray2).toEqual(typedArray);
  });

  it('can pass multiple Files in FormData', async () => {
    const typedArrayA = new Uint8Array([0, 1, 2, 3]);
    const typedArrayB = new Uint8Array([4, 5]);
    const blobA = new Blob([typedArrayA]);
    const blobB = new Blob([typedArrayB]);
    const formData = new FormData();
    formData.append('filelist', 'string');
    formData.append('filelist', blobA);
    formData.append('filelist', blobB);

    const body = await ReactServerDOMClient.encodeReply(formData);
    const formData2 = await ReactServerDOMServer.decodeReply(
      body,
      webpackServerMap,
    );

    const filelist2 = formData2.getAll('filelist');
    expect(filelist2.length).toBe(3);
    expect(filelist2[0]).toBe('string');
    const blobA2 = filelist2[1];
    expect(blobA2.size).toBe(4);
    expect(blobA2.name).toBe('blob');
    expect(blobA2.type).toBe('');
    const typedArrayA2 = new Uint8Array(await arrayBuffer(blobA2));
    expect(typedArrayA2).toEqual(typedArrayA);
    const blobB2 = filelist2[2];
    expect(blobB2.size).toBe(2);
    expect(blobB2.name).toBe('blob');
    expect(blobB2.type).toBe('');
    const typedArrayB2 = new Uint8Array(await arrayBuffer(blobB2));
    expect(typedArrayB2).toEqual(typedArrayB);
  });

  it('can pass two independent FormData with same keys', async () => {
    const formDataA = new FormData();
    formDataA.set('greeting', 'hello');
    const formDataB = new FormData();
    formDataB.set('greeting', 'hi');

    const body = await ReactServerDOMClient.encodeReply({
      a: formDataA,
      b: formDataB,
    });
    const {a: formDataA2, b: formDataB2} =
      await ReactServerDOMServer.decodeReply(body, webpackServerMap);

    expect(Array.from(formDataA2).length).toBe(1);
    expect(Array.from(formDataB2).length).toBe(1);
    expect(formDataA2.get('greeting')).toBe('hello');
    expect(formDataB2.get('greeting')).toBe('hi');
  });

  it('can pass a Date as a reply', async () => {
    const d = new Date(1234567890123);
    const body = await ReactServerDOMClient.encodeReply(d);
    const d2 = await ReactServerDOMServer.decodeReply(body, webpackServerMap);

    expect(d).toEqual(d2);
    expect(d % 1000).toEqual(123); // double-check the milliseconds made it through
  });

  it('can pass a Map as a reply', async () => {
    const objKey = {obj: 'key'};
    const m = new Map([
      ['hi', {greet: 'world'}],
      [objKey, 123],
    ]);
    const body = await ReactServerDOMClient.encodeReply(m);
    const m2 = await ReactServerDOMServer.decodeReply(body, webpackServerMap);

    expect(m2 instanceof Map).toBe(true);
    expect(m2.size).toBe(2);
    expect(m2.get('hi').greet).toBe('world');
    expect(m2).toEqual(m);
  });

  it('can pass a Set as a reply', async () => {
    const objKey = {obj: 'key'};
    const s = new Set(['hi', objKey]);

    const body = await ReactServerDOMClient.encodeReply(s);
    const s2 = await ReactServerDOMServer.decodeReply(body, webpackServerMap);

    expect(s2 instanceof Set).toBe(true);
    expect(s2.size).toBe(2);
    expect(s2.has('hi')).toBe(true);
    expect(s2).toEqual(s);
  });

  it('does not hang indefinitely when calling decodeReply with FormData', async () => {
    let error;
    try {
      await ReactServerDOMServer.decodeReply(new FormData(), webpackServerMap);
    } catch (e) {
      error = e;
    }
    expect(error.message).toBe('Connection closed.');
  });
});
