/**
 * 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
 * @jest-environment node
 */

'use strict';

let webpackServerMap;
let busboy;
let ReactServerDOMServer;
let ReactServerDOMClient;

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

    busboy = require('busboy');
  });

  // Writes the body to busboy as a multipart stream. Blob entries become
  // `filename`-bearing parts so busboy emits them as 'file' events (with
  // streamed data) rather than 'field' events.
  async function pipeBodyToBusboy(bb, body, boundary) {
    // eslint-disable-next-line no-for-of-loops/no-for-of-loops
    for (const [name, value] of body) {
      if (typeof value === 'string') {
        bb.write(
          `--${boundary}\r\n` +
            `Content-Disposition: form-data; name="${name}"\r\n` +
            `\r\n` +
            `${value}\r\n`,
        );
      } else {
        const filename =
          typeof value.name === 'string' && value.name !== ''
            ? value.name
            : 'blob';
        const mimeType =
          typeof value.type === 'string' && value.type !== ''
            ? value.type
            : 'application/octet-stream';
        const buffer = Buffer.from(await value.arrayBuffer());
        bb.write(
          `--${boundary}\r\n` +
            `Content-Disposition: form-data; name="${name}"; filename="${filename}"\r\n` +
            `Content-Type: ${mimeType}\r\n` +
            `\r\n`,
        );
        bb.write(buffer);
        bb.write('\r\n');
      }
    }
    bb.end(`--${boundary}--\r\n`);
  }

  // FormData iterates entries in insertion order per spec, so a referenced
  // FormData must round-trip with its entry order intact even when files
  // and text fields are interleaved in the payload.
  it('preserves entry order when referenced FormDatas interleave files and text', async () => {
    const a = new FormData();
    a.append('text_a', 'value_a');
    a.append('file_a', new Blob(['content_a'], {type: 'text/plain'}), 'a.txt');
    const b = new FormData();
    b.append('text_b', 'value_b');
    b.append('file_b', new Blob(['content_b'], {type: 'text/plain'}), 'b.txt');

    const body = await ReactServerDOMClient.encodeReply([a, b]);
    const boundary = 'boundary';
    const bb = busboy({
      headers: {
        'content-type': `multipart/form-data; boundary=${boundary}`,
      },
    });
    const reply = ReactServerDOMServer.decodeReplyFromBusboy(
      bb,
      webpackServerMap,
    );
    await pipeBodyToBusboy(bb, body, boundary);

    const result = await reply;
    expect(result).toHaveLength(2);
    const [decodedA, decodedB] = result;

    const aEntries = Array.from(decodedA.entries());
    expect(aEntries.map(([k]) => k)).toEqual(['text_a', 'file_a']);
    expect(aEntries[0][1]).toBe('value_a');
    expect(aEntries[1][1]).toBeInstanceOf(File);
    expect(aEntries[1][1].name).toBe('a.txt');

    const bEntries = Array.from(decodedB.entries());
    expect(bEntries.map(([k]) => k)).toEqual(['text_b', 'file_b']);
    expect(bEntries[0][1]).toBe('value_b');
    expect(bEntries[1][1]).toBeInstanceOf(File);
    expect(bEntries[1][1].name).toBe('b.txt');
  });

  // Every entry of a referenced FormData must be present in the decoded
  // FormData regardless of where files appear in its iteration order.
  it('does not drop entries when referenced FormDatas iterate files before text', async () => {
    const a = new FormData();
    a.append('file_a', new Blob(['content_a'], {type: 'text/plain'}), 'a.txt');
    a.append('text_a', 'value_a');
    const b = new FormData();
    b.append('file_b', new Blob(['content_b'], {type: 'text/plain'}), 'b.txt');
    b.append('text_b', 'value_b');

    const body = await ReactServerDOMClient.encodeReply([a, b]);
    const boundary = 'boundary';
    const bb = busboy({
      headers: {
        'content-type': `multipart/form-data; boundary=${boundary}`,
      },
    });
    const reply = ReactServerDOMServer.decodeReplyFromBusboy(
      bb,
      webpackServerMap,
    );
    await pipeBodyToBusboy(bb, body, boundary);

    const result = await reply;
    expect(result).toHaveLength(2);
    const [decodedA, decodedB] = result;

    const aKeys = Array.from(decodedA.keys()).sort();
    expect(aKeys).toEqual(['file_a', 'text_a']);
    expect(decodedA.get('text_a')).toBe('value_a');
    expect(decodedA.get('file_a')).toBeInstanceOf(File);

    const bKeys = Array.from(decodedB.keys()).sort();
    expect(bKeys).toEqual(['file_b', 'text_b']);
    expect(decodedB.get('text_b')).toBe('value_b');
    expect(decodedB.get('file_b')).toBeInstanceOf(File);
  });
});