/**
 * 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';

const heldValues = [];
let finalizationCallback;
function FinalizationRegistryMock(callback) {
  finalizationCallback = callback;
}
FinalizationRegistryMock.prototype.register = function (target, heldValue) {
  heldValues.push(heldValue);
};
global.FinalizationRegistry = FinalizationRegistryMock;

function gc() {
  for (let i = 0; i < heldValues.length; i++) {
    finalizationCallback(heldValues[i]);
  }
  heldValues.length = 0;
}

let act;
let use;
let startTransition;
let React;
let ReactServer;
let ReactNoop;
let ReactNoopFlightServer;
let ReactNoopFlightClient;
let ErrorBoundary;
let NoErrorExpected;
let Scheduler;
let assertLog;

describe('ReactFlight', () => {
  beforeEach(() => {
    jest.resetModules();
    jest.mock('react', () => require('react/react.shared-subset'));
    ReactServer = require('react');
    ReactNoopFlightServer = require('react-noop-renderer/flight-server');
    // This stores the state so we need to preserve it
    const flightModules = require('react-noop-renderer/flight-modules');
    __unmockReact();
    jest.resetModules();
    jest.mock('react-noop-renderer/flight-modules', () => flightModules);
    React = require('react');
    startTransition = React.startTransition;
    use = React.use;
    ReactNoop = require('react-noop-renderer');
    ReactNoopFlightClient = require('react-noop-renderer/flight-client');
    act = require('internal-test-utils').act;
    Scheduler = require('scheduler');
    const InternalTestUtils = require('internal-test-utils');
    assertLog = InternalTestUtils.assertLog;

    ErrorBoundary = class extends React.Component {
      state = {hasError: false, error: null};
      static getDerivedStateFromError(error) {
        return {
          hasError: true,
          error,
        };
      }
      componentDidMount() {
        expect(this.state.hasError).toBe(true);
        expect(this.state.error).toBeTruthy();
        if (__DEV__) {
          expect(this.state.error.message).toContain(
            this.props.expectedMessage,
          );
          expect(this.state.error.digest).toBe('a dev digest');
        } else {
          expect(this.state.error.message).toBe(
            'An error occurred in the Server Components render. The specific message is omitted in production' +
              ' builds to avoid leaking sensitive details. A digest property is included on this error instance which' +
              ' may provide additional details about the nature of the error.',
          );
          expect(this.state.error.digest).toContain(this.props.expectedMessage);
          expect(this.state.error.stack).toBe(
            'Error: ' + this.state.error.message,
          );
        }
      }
      render() {
        if (this.state.hasError) {
          return this.state.error.message;
        }
        return this.props.children;
      }
    };

    NoErrorExpected = class extends React.Component {
      state = {hasError: false, error: null};
      static getDerivedStateFromError(error) {
        return {
          hasError: true,
          error,
        };
      }
      componentDidMount() {
        expect(this.state.error).toBe(null);
        expect(this.state.hasError).toBe(false);
      }
      render() {
        if (this.state.hasError) {
          return this.state.error.message;
        }
        return this.props.children;
      }
    };
  });

  afterEach(() => {
    jest.restoreAllMocks();
  });

  function createServerContext(globalName, defaultValue, withStack) {
    let ctx;
    expect(() => {
      ctx = React.createServerContext(globalName, defaultValue);
    }).toErrorDev(
      'Server Context is deprecated and will soon be removed. ' +
        'It was never documented and we have found it not to be useful ' +
        'enough to warrant the downside it imposes on all apps.',
      {withoutStack: !withStack},
    );
    return ctx;
  }

  function createServerServerContext(globalName, defaultValue, withStack) {
    let ctx;
    expect(() => {
      ctx = ReactServer.createServerContext(globalName, defaultValue);
    }).toErrorDev(
      'Server Context is deprecated and will soon be removed. ' +
        'It was never documented and we have found it not to be useful ' +
        'enough to warrant the downside it imposes on all apps.',
      {withoutStack: !withStack},
    );
    return ctx;
  }

  function clientReference(value) {
    return Object.defineProperties(
      function () {
        throw new Error('Cannot call a client function from the server.');
      },
      {
        $$typeof: {value: Symbol.for('react.client.reference')},
        value: {value: value},
      },
    );
  }

  it('can render a Server Component', async () => {
    function Bar({text}) {
      return text.toUpperCase();
    }
    function Foo() {
      return {
        bar: (
          <div>
            <Bar text="a" />, <Bar text="b" />
          </div>
        ),
      };
    }
    const transport = ReactNoopFlightServer.render({
      foo: <Foo />,
    });
    const model = await ReactNoopFlightClient.read(transport);
    expect(model).toEqual({
      foo: {
        bar: (
          <div>
            {'A'}
            {', '}
            {'B'}
          </div>
        ),
      },
    });
  });

  it('can render a Client Component using a module reference and render there', async () => {
    function UserClient(props) {
      return (
        <span>
          {props.greeting}, {props.name}
        </span>
      );
    }
    const User = clientReference(UserClient);

    function Greeting({firstName, lastName}) {
      return <User greeting="Hello" name={firstName + ' ' + lastName} />;
    }

    const model = {
      greeting: <Greeting firstName="Seb" lastName="Smith" />,
    };

    const transport = ReactNoopFlightServer.render(model);

    await act(async () => {
      const rootModel = await ReactNoopFlightClient.read(transport);
      const greeting = rootModel.greeting;
      ReactNoop.render(greeting);
    });

    expect(ReactNoop).toMatchRenderedOutput(<span>Hello, Seb Smith</span>);
  });

  it('can render an iterable as an array', async () => {
    function ItemListClient(props) {
      return <span>{props.items}</span>;
    }
    const ItemList = clientReference(ItemListClient);

    function Items() {
      const iterable = {
        [Symbol.iterator]: function* () {
          yield 'A';
          yield 'B';
          yield 'C';
        },
      };
      return <ItemList items={iterable} />;
    }

    const model = <Items />;

    const transport = ReactNoopFlightServer.render(model);

    await act(async () => {
      ReactNoop.render(await ReactNoopFlightClient.read(transport));
    });

    expect(ReactNoop).toMatchRenderedOutput(<span>ABC</span>);
  });

  it('can render undefined', async () => {
    function Undefined() {
      return undefined;
    }

    const model = <Undefined />;

    const transport = ReactNoopFlightServer.render(model);

    await act(async () => {
      ReactNoop.render(await ReactNoopFlightClient.read(transport));
    });

    expect(ReactNoop).toMatchRenderedOutput(null);
  });

  // @gate FIXME
  it('should transport undefined object values', async () => {
    function ServerComponent(props) {
      return 'prop' in props
        ? `\`prop\` in props as '${props.prop}'`
        : '`prop` not in props';
    }
    const ClientComponent = clientReference(ServerComponent);

    const model = (
      <>
        <div>
          Server: <ServerComponent prop={undefined} />
        </div>
        <div>
          Client: <ClientComponent prop={undefined} />
        </div>
      </>
    );

    const transport = ReactNoopFlightServer.render(model);

    await act(async () => {
      ReactNoop.render(await ReactNoopFlightClient.read(transport));
    });

    expect(ReactNoop).toMatchRenderedOutput(
      <>
        <div>Server: `prop` in props as 'undefined'</div>
        <div>Client: `prop` in props as 'undefined'</div>
      </>,
    );
  });

  it('can render an empty fragment', async () => {
    function Empty() {
      return <React.Fragment />;
    }

    const model = <Empty />;

    const transport = ReactNoopFlightServer.render(model);

    await act(async () => {
      ReactNoop.render(await ReactNoopFlightClient.read(transport));
    });

    expect(ReactNoop).toMatchRenderedOutput(null);
  });

  it('can transport weird numbers', async () => {
    const nums = [0, -0, Infinity, -Infinity, NaN];
    function ComponentClient({prop}) {
      expect(prop).not.toBe(nums);
      expect(prop).toEqual(nums);
      expect(prop.every((p, i) => Object.is(p, nums[i]))).toBe(true);
      return `prop: ${prop}`;
    }
    const Component = clientReference(ComponentClient);

    const model = <Component prop={nums} />;

    const transport = ReactNoopFlightServer.render(model);

    await act(async () => {
      ReactNoop.render(await ReactNoopFlightClient.read(transport));
    });

    expect(ReactNoop).toMatchRenderedOutput(
      // already checked -0 with expects above
      'prop: 0,0,Infinity,-Infinity,NaN',
    );
  });

  it('can transport BigInt', async () => {
    function ComponentClient({prop}) {
      return `prop: ${prop} (${typeof prop})`;
    }
    const Component = clientReference(ComponentClient);

    const model = <Component prop={90071992547409910000n} />;

    const transport = ReactNoopFlightServer.render(model);

    await act(async () => {
      ReactNoop.render(await ReactNoopFlightClient.read(transport));
    });

    expect(ReactNoop).toMatchRenderedOutput(
      'prop: 90071992547409910000 (bigint)',
    );
  });

  it('can transport Date', async () => {
    function ComponentClient({prop}) {
      return `prop: ${prop.toISOString()}`;
    }
    const Component = clientReference(ComponentClient);

    const model = <Component prop={new Date(1234567890123)} />;

    const transport = ReactNoopFlightServer.render(model);

    await act(async () => {
      ReactNoop.render(await ReactNoopFlightClient.read(transport));
    });

    expect(ReactNoop).toMatchRenderedOutput('prop: 2009-02-13T23:31:30.123Z');
  });

  it('can transport Map', async () => {
    function ComponentClient({prop, selected}) {
      return `
        map: ${prop instanceof Map}
        size: ${prop.size}
        greet: ${prop.get('hi').greet}
        content: ${JSON.stringify(Array.from(prop))}
        selected: ${prop.get(selected)}
      `;
    }
    const Component = clientReference(ComponentClient);

    const objKey = {obj: 'key'};
    const map = new Map([
      ['hi', {greet: 'world'}],
      [objKey, 123],
    ]);
    const model = <Component prop={map} selected={objKey} />;

    const transport = ReactNoopFlightServer.render(model);

    await act(async () => {
      ReactNoop.render(await ReactNoopFlightClient.read(transport));
    });

    expect(ReactNoop).toMatchRenderedOutput(`
        map: true
        size: 2
        greet: world
        content: [["hi",{"greet":"world"}],[{"obj":"key"},123]]
        selected: 123
      `);
  });

  it('can transport Set', async () => {
    function ComponentClient({prop, selected}) {
      return `
        set: ${prop instanceof Set}
        size: ${prop.size}
        hi: ${prop.has('hi')}
        content: ${JSON.stringify(Array.from(prop))}
        selected: ${prop.has(selected)}
      `;
    }
    const Component = clientReference(ComponentClient);

    const objKey = {obj: 'key'};
    const set = new Set(['hi', objKey]);
    const model = <Component prop={set} selected={objKey} />;

    const transport = ReactNoopFlightServer.render(model);

    await act(async () => {
      ReactNoop.render(await ReactNoopFlightClient.read(transport));
    });

    expect(ReactNoop).toMatchRenderedOutput(`
        set: true
        size: 2
        hi: true
        content: ["hi",{"obj":"key"}]
        selected: true
      `);
  });

  it('can transport cyclic objects', async () => {
    function ComponentClient({prop}) {
      expect(prop.obj.obj.obj).toBe(prop.obj.obj);
    }
    const Component = clientReference(ComponentClient);

    const cyclic = {obj: null};
    cyclic.obj = cyclic;
    const model = <Component prop={cyclic} />;

    const transport = ReactNoopFlightServer.render(model);

    await act(async () => {
      ReactNoop.render(await ReactNoopFlightClient.read(transport));
    });
  });

  it('can render a lazy component as a shared component on the server', async () => {
    function SharedComponent({text}) {
      return (
        <div>
          shared<span>{text}</span>
        </div>
      );
    }

    let load = null;
    const loadSharedComponent = () => {
      return new Promise(res => {
        load = () => res({default: SharedComponent});
      });
    };

    const LazySharedComponent = React.lazy(loadSharedComponent);

    function ServerComponent() {
      return (
        <React.Suspense fallback={'Loading...'}>
          <LazySharedComponent text={'a'} />
        </React.Suspense>
      );
    }

    const transport = ReactNoopFlightServer.render(<ServerComponent />);

    await act(async () => {
      const rootModel = await ReactNoopFlightClient.read(transport);
      ReactNoop.render(rootModel);
    });
    expect(ReactNoop).toMatchRenderedOutput('Loading...');
    await load();

    await act(async () => {
      const rootModel = await ReactNoopFlightClient.read(transport);
      ReactNoop.render(rootModel);
    });
    expect(ReactNoop).toMatchRenderedOutput(
      <div>
        shared<span>a</span>
      </div>,
    );
  });

  it('errors on a Lazy element being used in Component position', async () => {
    function SharedComponent({text}) {
      return (
        <div>
          shared<span>{text}</span>
        </div>
      );
    }

    let load = null;

    const LazyElementDisguisedAsComponent = React.lazy(() => {
      return new Promise(res => {
        load = () => res({default: <SharedComponent text={'a'} />});
      });
    });

    function ServerComponent() {
      return (
        <React.Suspense fallback={'Loading...'}>
          <LazyElementDisguisedAsComponent text={'b'} />
        </React.Suspense>
      );
    }

    const transport = ReactNoopFlightServer.render(<ServerComponent />);

    await act(async () => {
      const rootModel = await ReactNoopFlightClient.read(transport);
      ReactNoop.render(rootModel);
    });
    expect(ReactNoop).toMatchRenderedOutput('Loading...');
    spyOnDevAndProd(console, 'error').mockImplementation(() => {});
    await load();
    expect(console.error).toHaveBeenCalledTimes(1);
  });

  it('can render a lazy element', async () => {
    function SharedComponent({text}) {
      return (
        <div>
          shared<span>{text}</span>
        </div>
      );
    }

    let load = null;

    const lazySharedElement = React.lazy(() => {
      return new Promise(res => {
        load = () => res({default: <SharedComponent text={'a'} />});
      });
    });

    function ServerComponent() {
      return (
        <React.Suspense fallback={'Loading...'}>
          {lazySharedElement}
        </React.Suspense>
      );
    }

    const transport = ReactNoopFlightServer.render(<ServerComponent />);

    await act(async () => {
      const rootModel = await ReactNoopFlightClient.read(transport);
      ReactNoop.render(rootModel);
    });
    expect(ReactNoop).toMatchRenderedOutput('Loading...');
    await load();

    await act(async () => {
      const rootModel = await ReactNoopFlightClient.read(transport);
      ReactNoop.render(rootModel);
    });
    expect(ReactNoop).toMatchRenderedOutput(
      <div>
        shared<span>a</span>
      </div>,
    );
  });

  it('errors with lazy value in element position that resolves to Component', async () => {
    function SharedComponent({text}) {
      return (
        <div>
          shared<span>{text}</span>
        </div>
      );
    }

    let load = null;

    const componentDisguisedAsElement = React.lazy(() => {
      return new Promise(res => {
        load = () => res({default: SharedComponent});
      });
    });

    function ServerComponent() {
      return (
        <React.Suspense fallback={'Loading...'}>
          {componentDisguisedAsElement}
        </React.Suspense>
      );
    }

    const transport = ReactNoopFlightServer.render(<ServerComponent />);

    await act(async () => {
      const rootModel = await ReactNoopFlightClient.read(transport);
      ReactNoop.render(rootModel);
    });
    expect(ReactNoop).toMatchRenderedOutput('Loading...');
    spyOnDevAndProd(console, 'error').mockImplementation(() => {});
    await load();
    expect(console.error).toHaveBeenCalledTimes(1);
  });

  it('can render a lazy module reference', async () => {
    function ClientComponent() {
      return <div>I am client</div>;
    }

    const ClientComponentReference = clientReference(ClientComponent);

    let load = null;
    const loadClientComponentReference = () => {
      return new Promise(res => {
        load = () => res({default: ClientComponentReference});
      });
    };

    const LazyClientComponentReference = React.lazy(
      loadClientComponentReference,
    );

    function ServerComponent() {
      return (
        <React.Suspense fallback={'Loading...'}>
          <LazyClientComponentReference />
        </React.Suspense>
      );
    }

    const transport = ReactNoopFlightServer.render(<ServerComponent />);

    await act(async () => {
      const rootModel = await ReactNoopFlightClient.read(transport);
      ReactNoop.render(rootModel);
    });
    expect(ReactNoop).toMatchRenderedOutput('Loading...');
    await load();

    await act(async () => {
      const rootModel = await ReactNoopFlightClient.read(transport);
      ReactNoop.render(rootModel);
    });
    expect(ReactNoop).toMatchRenderedOutput(<div>I am client</div>);
  });

  it('should error if a non-serializable value is passed to a host component', async () => {
    function ClientImpl({children}) {
      return children;
    }
    const Client = clientReference(ClientImpl);

    function EventHandlerProp() {
      return (
        <div className="foo" onClick={function () {}}>
          Test
        </div>
      );
    }
    function FunctionProp() {
      return <div>{() => {}}</div>;
    }
    function SymbolProp() {
      return <div foo={Symbol('foo')} />;
    }

    const ref = React.createRef();
    function RefProp() {
      return <div ref={ref} />;
    }

    function EventHandlerPropClient() {
      return (
        <Client className="foo" onClick={function () {}}>
          Test
        </Client>
      );
    }
    function FunctionPropClient() {
      return <Client>{() => {}}</Client>;
    }
    function SymbolPropClient() {
      return <Client foo={Symbol('foo')} />;
    }

    function RefPropClient() {
      return <Client ref={ref} />;
    }

    const options = {
      onError(x) {
        return __DEV__ ? 'a dev digest' : `digest("${x.message}")`;
      },
    };
    const event = ReactNoopFlightServer.render(<EventHandlerProp />, options);
    const fn = ReactNoopFlightServer.render(<FunctionProp />, options);
    const symbol = ReactNoopFlightServer.render(<SymbolProp />, options);
    const refs = ReactNoopFlightServer.render(<RefProp />, options);
    const eventClient = ReactNoopFlightServer.render(
      <EventHandlerPropClient />,
      options,
    );
    const fnClient = ReactNoopFlightServer.render(
      <FunctionPropClient />,
      options,
    );
    const symbolClient = ReactNoopFlightServer.render(
      <SymbolPropClient />,
      options,
    );
    const refsClient = ReactNoopFlightServer.render(<RefPropClient />, options);

    function Render({promise}) {
      return use(promise);
    }

    await act(() => {
      startTransition(() => {
        ReactNoop.render(
          <>
            <ErrorBoundary expectedMessage="Event handlers cannot be passed to Client Component props.">
              <Render promise={ReactNoopFlightClient.read(event)} />
            </ErrorBoundary>
            <ErrorBoundary
              expectedMessage={
                'Functions cannot be passed directly to Client Components unless you explicitly expose it by marking it with "use server".'
              }>
              <Render promise={ReactNoopFlightClient.read(fn)} />
            </ErrorBoundary>
            <ErrorBoundary expectedMessage="Only global symbols received from Symbol.for(...) can be passed to Client Components.">
              <Render promise={ReactNoopFlightClient.read(symbol)} />
            </ErrorBoundary>
            <ErrorBoundary expectedMessage="Refs cannot be used in Server Components, nor passed to Client Components.">
              <Render promise={ReactNoopFlightClient.read(refs)} />
            </ErrorBoundary>
            <ErrorBoundary expectedMessage="Event handlers cannot be passed to Client Component props.">
              <Render promise={ReactNoopFlightClient.read(eventClient)} />
            </ErrorBoundary>
            <ErrorBoundary
              expectedMessage={
                'Functions cannot be passed directly to Client Components unless you explicitly expose it by marking it with "use server".'
              }>
              <Render promise={ReactNoopFlightClient.read(fnClient)} />
            </ErrorBoundary>
            <ErrorBoundary expectedMessage="Only global symbols received from Symbol.for(...) can be passed to Client Components.">
              <Render promise={ReactNoopFlightClient.read(symbolClient)} />
            </ErrorBoundary>
            <ErrorBoundary expectedMessage="Refs cannot be used in Server Components, nor passed to Client Components.">
              <Render promise={ReactNoopFlightClient.read(refsClient)} />
            </ErrorBoundary>
          </>,
        );
      });
    });
  });

  it('should trigger the inner most error boundary inside a Client Component', async () => {
    function ServerComponent() {
      throw new Error('This was thrown in the Server Component.');
    }

    function ClientComponent({children}) {
      // This should catch the error thrown by the Server Component, even though it has already happened.
      // We currently need to wrap it in a div because as it's set up right now, a lazy reference will
      // throw during reconciliation which will trigger the parent of the error boundary.
      // This is similar to how these will suspend the parent if it's a direct child of a Suspense boundary.
      // That's a bug.
      return (
        <ErrorBoundary expectedMessage="This was thrown in the Server Component.">
          <div>{children}</div>
        </ErrorBoundary>
      );
    }

    const ClientComponentReference = clientReference(ClientComponent);

    function Server() {
      return (
        <ClientComponentReference>
          <ServerComponent />
        </ClientComponentReference>
      );
    }

    const data = ReactNoopFlightServer.render(<Server />, {
      onError(x) {
        // ignore
      },
    });

    function Client({promise}) {
      return use(promise);
    }

    await act(() => {
      startTransition(() => {
        ReactNoop.render(
          <NoErrorExpected>
            <Client promise={ReactNoopFlightClient.read(data)} />
          </NoErrorExpected>,
        );
      });
    });
  });

  it('should warn in DEV if a toJSON instance is passed to a host component', () => {
    const obj = {
      toJSON() {
        return 123;
      },
    };
    expect(() => {
      const transport = ReactNoopFlightServer.render(<input value={obj} />);
      ReactNoopFlightClient.read(transport);
    }).toErrorDev(
      'Only plain objects can be passed to Client Components from Server Components. ' +
        'Objects with toJSON methods are not supported. ' +
        'Convert it manually to a simple value before passing it to props.\n' +
        '  <input value={{toJSON: function}}>\n' +
        '               ^^^^^^^^^^^^^^^^^^^^',
      {withoutStack: true},
    );
  });

  it('should warn in DEV if a toJSON instance is passed to a host component child', () => {
    class MyError extends Error {
      toJSON() {
        return 123;
      }
    }
    expect(() => {
      const transport = ReactNoopFlightServer.render(
        <div>Womp womp: {new MyError('spaghetti')}</div>,
      );
      ReactNoopFlightClient.read(transport);
    }).toErrorDev(
      'Error objects cannot be rendered as text children. Try formatting it using toString().\n' +
        '  <div>Womp womp: {Error}</div>\n' +
        '                  ^^^^^^^',
      {withoutStack: true},
    );
  });

  it('should warn in DEV if a special object is passed to a host component', () => {
    expect(() => {
      const transport = ReactNoopFlightServer.render(<input value={Math} />);
      ReactNoopFlightClient.read(transport);
    }).toErrorDev(
      'Only plain objects can be passed to Client Components from Server Components. ' +
        'Math objects are not supported.\n' +
        '  <input value={Math}>\n' +
        '               ^^^^^^',
      {withoutStack: true},
    );
  });

  it('should warn in DEV if an object with symbols is passed to a host component', () => {
    expect(() => {
      const transport = ReactNoopFlightServer.render(
        <input value={{[Symbol.iterator]: {}}} />,
      );
      ReactNoopFlightClient.read(transport);
    }).toErrorDev(
      'Only plain objects can be passed to Client Components from Server Components. ' +
        'Objects with symbol properties like Symbol.iterator are not supported.',
      {withoutStack: true},
    );
  });

  it('should warn in DEV if a toJSON instance is passed to a Client Component', () => {
    const obj = {
      toJSON() {
        return 123;
      },
    };
    function ClientImpl({value}) {
      return <div>{value}</div>;
    }
    const Client = clientReference(ClientImpl);
    expect(() => {
      const transport = ReactNoopFlightServer.render(<Client value={obj} />);
      ReactNoopFlightClient.read(transport);
    }).toErrorDev(
      'Only plain objects can be passed to Client Components from Server Components. ' +
        'Objects with toJSON methods are not supported.',
      {withoutStack: true},
    );
  });

  it('should warn in DEV if a toJSON instance is passed to a Client Component child', () => {
    const obj = {
      toJSON() {
        return 123;
      },
    };
    function ClientImpl({children}) {
      return <div>{children}</div>;
    }
    const Client = clientReference(ClientImpl);
    expect(() => {
      const transport = ReactNoopFlightServer.render(
        <Client>Current date: {obj}</Client>,
      );
      ReactNoopFlightClient.read(transport);
    }).toErrorDev(
      'Only plain objects can be passed to Client Components from Server Components. ' +
        'Objects with toJSON methods are not supported. ' +
        'Convert it manually to a simple value before passing it to props.\n' +
        '  <>Current date: {{toJSON: function}}</>\n' +
        '                  ^^^^^^^^^^^^^^^^^^^^',
      {withoutStack: true},
    );
  });

  it('should warn in DEV if a special object is passed to a Client Component', () => {
    function ClientImpl({value}) {
      return <div>{value}</div>;
    }
    const Client = clientReference(ClientImpl);
    expect(() => {
      const transport = ReactNoopFlightServer.render(<Client value={Math} />);
      ReactNoopFlightClient.read(transport);
    }).toErrorDev(
      'Only plain objects can be passed to Client Components from Server Components. ' +
        'Math objects are not supported.\n' +
        '  <... value={Math}>\n' +
        '             ^^^^^^',
      {withoutStack: true},
    );
  });

  it('should warn in DEV if an object with symbols is passed to a Client Component', () => {
    function ClientImpl({value}) {
      return <div>{value}</div>;
    }
    const Client = clientReference(ClientImpl);
    expect(() => {
      const transport = ReactNoopFlightServer.render(
        <Client value={{[Symbol.iterator]: {}}} />,
      );
      ReactNoopFlightClient.read(transport);
    }).toErrorDev(
      'Only plain objects can be passed to Client Components from Server Components. ' +
        'Objects with symbol properties like Symbol.iterator are not supported.',
      {withoutStack: true},
    );
  });

  it('should warn in DEV if a special object is passed to a nested object in Client Component', () => {
    function ClientImpl({value}) {
      return <div>{value}</div>;
    }
    const Client = clientReference(ClientImpl);
    expect(() => {
      const transport = ReactNoopFlightServer.render(
        <Client value={{hello: Math, title: <h1>hi</h1>}} />,
      );
      ReactNoopFlightClient.read(transport);
    }).toErrorDev(
      'Only plain objects can be passed to Client Components from Server Components. ' +
        'Math objects are not supported.\n' +
        '  {hello: Math, title: <h1/>}\n' +
        '          ^^^^',
      {withoutStack: true},
    );
  });

  it('should warn in DEV if a special object is passed to a nested array in Client Component', () => {
    function ClientImpl({value}) {
      return <div>{value}</div>;
    }
    const Client = clientReference(ClientImpl);
    expect(() => {
      const transport = ReactNoopFlightServer.render(
        <Client
          value={['looooong string takes up noise', Math, <h1>hi</h1>]}
        />,
      );
      ReactNoopFlightClient.read(transport);
    }).toErrorDev(
      'Only plain objects can be passed to Client Components from Server Components. ' +
        'Math objects are not supported.\n' +
        '  [..., Math, <h1/>]\n' +
        '        ^^^^',
      {withoutStack: true},
    );
  });

  it('should NOT warn in DEV for key getters', () => {
    const transport = ReactNoopFlightServer.render(<div key="a" />);
    ReactNoopFlightClient.read(transport);
  });

  it('should warn in DEV a child is missing keys', () => {
    function ParentClient({children}) {
      return children;
    }
    const Parent = clientReference(ParentClient);
    expect(() => {
      const transport = ReactNoopFlightServer.render(
        <Parent>{Array(6).fill(<div>no key</div>)}</Parent>,
      );
      ReactNoopFlightClient.read(transport);
    }).toErrorDev(
      'Each child in a list should have a unique "key" prop. ' +
        'See https://reactjs.org/link/warning-keys for more information.',
    );
  });

  it('should error if a class instance is passed to a host component', () => {
    class Foo {
      method() {}
    }
    const errors = [];
    ReactNoopFlightServer.render(<input value={new Foo()} />, {
      onError(x) {
        errors.push(x.message);
      },
    });

    expect(errors).toEqual([
      'Only plain objects, and a few built-ins, can be passed to Client Components ' +
        'from Server Components. Classes or null prototypes are not supported.',
    ]);
  });

  it('should warn in DEV if a a client reference is passed to useContext()', () => {
    const Context = React.createContext();
    const ClientContext = clientReference(Context);
    function ServerComponent() {
      return ReactServer.useContext(ClientContext);
    }
    expect(() => {
      const transport = ReactNoopFlightServer.render(<ServerComponent />);
      ReactNoopFlightClient.read(transport);
    }).toErrorDev('Cannot read a Client Context from a Server Component.', {
      withoutStack: true,
    });
  });

  describe('Hooks', () => {
    function DivWithId({children}) {
      const id = ReactServer.useId();
      return <div prop={id}>{children}</div>;
    }

    it('should support useId', async () => {
      function App() {
        return (
          <>
            <DivWithId />
            <DivWithId />
          </>
        );
      }

      const transport = ReactNoopFlightServer.render(<App />);
      await act(async () => {
        ReactNoop.render(await ReactNoopFlightClient.read(transport));
      });
      expect(ReactNoop).toMatchRenderedOutput(
        <>
          <div prop=":S1:" />
          <div prop=":S2:" />
        </>,
      );
    });

    it('accepts an identifier prefix that prefixes generated ids', async () => {
      function App() {
        return (
          <>
            <DivWithId />
            <DivWithId />
          </>
        );
      }

      const transport = ReactNoopFlightServer.render(<App />, {
        identifierPrefix: 'foo',
      });
      await act(async () => {
        ReactNoop.render(await ReactNoopFlightClient.read(transport));
      });
      expect(ReactNoop).toMatchRenderedOutput(
        <>
          <div prop=":fooS1:" />
          <div prop=":fooS2:" />
        </>,
      );
    });

    it('[TODO] it does not warn if you render a server element passed to a client module reference twice on the client when using useId', async () => {
      // @TODO Today if you render a Server Component with useId and pass it to a Client Component and that Client Component renders the element in two or more
      // places the id used on the server will be duplicated in the client. This is a deviation from the guarantees useId makes for Fizz/Client and is a consequence
      // of the fact that the Server Component is actually rendered on the server and is reduced to a set of host elements before being passed to the Client component
      // so the output passed to the Client has no knowledge of the useId use. In the future we would like to add a DEV warning when this happens. For now
      // we just accept that it is a nuance of useId in Flight
      function App() {
        const id = ReactServer.useId();
        const div = <div prop={id}>{id}</div>;
        return <ClientDoublerModuleRef el={div} />;
      }

      function ClientDoubler({el}) {
        Scheduler.log('ClientDoubler');
        return (
          <>
            {el}
            {el}
          </>
        );
      }

      const ClientDoublerModuleRef = clientReference(ClientDoubler);

      const transport = ReactNoopFlightServer.render(<App />);
      assertLog([]);

      await act(async () => {
        ReactNoop.render(await ReactNoopFlightClient.read(transport));
      });

      assertLog(['ClientDoubler']);
      expect(ReactNoop).toMatchRenderedOutput(
        <>
          <div prop=":S1:">:S1:</div>
          <div prop=":S1:">:S1:</div>
        </>,
      );
    });
  });

  describe('ServerContext', () => {
    // @gate enableServerContext
    it('supports basic createServerContext usage', async () => {
      const ServerContext = createServerServerContext(
        'ServerContext',
        'hello from server',
      );
      function Foo() {
        const context = ReactServer.useContext(ServerContext);
        return <div>{context}</div>;
      }

      const transport = ReactNoopFlightServer.render(<Foo />);
      await act(async () => {
        ReactNoop.render(await ReactNoopFlightClient.read(transport));
      });

      expect(ReactNoop).toMatchRenderedOutput(<div>hello from server</div>);
    });

    // @gate enableServerContext
    it('propagates ServerContext providers in flight', async () => {
      const ServerContext = createServerServerContext(
        'ServerContext',
        'default',
      );

      function Foo() {
        return (
          <div>
            <ServerContext.Provider value="hi this is server">
              <Bar />
            </ServerContext.Provider>
          </div>
        );
      }
      function Bar() {
        const context = ReactServer.useContext(ServerContext);
        return context;
      }

      const transport = ReactNoopFlightServer.render(<Foo />);
      await act(async () => {
        ReactNoop.render(await ReactNoopFlightClient.read(transport));
      });

      expect(ReactNoop).toMatchRenderedOutput(<div>hi this is server</div>);
    });

    // @gate enableServerContext
    it('errors if you try passing JSX through ServerContext value', () => {
      const ServerContext = createServerServerContext('ServerContext', {
        foo: {
          bar: <span>hi this is default</span>,
        },
      });

      function Foo() {
        return (
          <div>
            <ServerContext.Provider
              value={{
                foo: {
                  bar: <span>hi this is server</span>,
                },
              }}>
              <Bar />
            </ServerContext.Provider>
          </div>
        );
      }
      function Bar() {
        const context = ReactServer.useContext(ServerContext);
        return context.foo.bar;
      }

      expect(() => {
        ReactNoopFlightServer.render(<Foo />);
      }).toErrorDev('React elements are not allowed in ServerContext', {
        withoutStack: true,
      });
    });

    // @gate enableServerContext
    it('propagates ServerContext and cleans up the providers in flight', async () => {
      const ServerContext = createServerServerContext(
        'ServerContext',
        'default',
      );

      function Foo() {
        return (
          <>
            <ServerContext.Provider value="hi this is server outer">
              <ServerContext.Provider value="hi this is server">
                <Bar />
              </ServerContext.Provider>
              <ServerContext.Provider value="hi this is server2">
                <Bar />
              </ServerContext.Provider>
              <Bar />
            </ServerContext.Provider>
            <ServerContext.Provider value="hi this is server outer2">
              <Bar />
            </ServerContext.Provider>
            <Bar />
          </>
        );
      }
      function Bar() {
        const context = ReactServer.useContext(ServerContext);
        return <span>{context}</span>;
      }

      const transport = ReactNoopFlightServer.render(<Foo />);
      await act(async () => {
        ReactNoop.render(await ReactNoopFlightClient.read(transport));
      });

      expect(ReactNoop).toMatchRenderedOutput(
        <>
          <span>hi this is server</span>
          <span>hi this is server2</span>
          <span>hi this is server outer</span>
          <span>hi this is server outer2</span>
          <span>default</span>
        </>,
      );
    });

    // @gate enableServerContext
    it('propagates ServerContext providers in flight after suspending', async () => {
      const ServerContext = createServerServerContext(
        'ServerContext',
        'default',
      );

      function Foo() {
        return (
          <div>
            <ServerContext.Provider value="hi this is server">
              <React.Suspense fallback={'Loading'}>
                <Bar />
              </React.Suspense>
            </ServerContext.Provider>
          </div>
        );
      }

      let resolve;
      const promise = new Promise(res => {
        resolve = () => {
          promise.unsuspend = true;
          res();
        };
      });

      function Bar() {
        if (!promise.unsuspend) {
          Scheduler.log('suspended');
          throw promise;
        }
        Scheduler.log('rendered');
        const context = ReactServer.useContext(ServerContext);
        return context;
      }

      const transport = ReactNoopFlightServer.render(<Foo />);

      assertLog(['suspended']);

      await act(async () => {
        resolve();
        await promise;
        jest.runAllImmediates();
      });

      assertLog(['rendered']);

      await act(async () => {
        ReactNoop.render(await ReactNoopFlightClient.read(transport));
      });

      expect(ReactNoop).toMatchRenderedOutput(<div>hi this is server</div>);
    });

    // @gate enableServerContext
    it('serializes ServerContext to client', async () => {
      const ServerContext = createServerServerContext(
        'ServerContext',
        'default',
      );
      const ClientContext = createServerContext('ServerContext', 'default');

      function ClientBar() {
        Scheduler.log('ClientBar');
        const context = React.useContext(ClientContext);
        return <span>{context}</span>;
      }

      const Bar = clientReference(ClientBar);

      function Foo() {
        return (
          <ServerContext.Provider value="hi this is server">
            <Bar />
          </ServerContext.Provider>
        );
      }

      const model = {
        foo: <Foo />,
      };

      const transport = ReactNoopFlightServer.render(model);

      assertLog([]);

      await act(async () => {
        const flightModel = await ReactNoopFlightClient.read(transport);
        ReactNoop.render(flightModel.foo);
      });

      assertLog(['ClientBar']);
      expect(ReactNoop).toMatchRenderedOutput(<span>hi this is server</span>);

      expect(() => {
        createServerContext('ServerContext', 'default');
      }).toThrow('ServerContext: ServerContext already defined');
    });

    // @gate enableServerContext
    it('takes ServerContext from the client for refetching use cases', async () => {
      const ServerContext = createServerServerContext(
        'ServerContext',
        'default',
      );
      function Bar() {
        return <span>{ReactServer.useContext(ServerContext)}</span>;
      }
      const transport = ReactNoopFlightServer.render(<Bar />, {
        context: [['ServerContext', 'Override']],
      });

      await act(async () => {
        const flightModel = await ReactNoopFlightClient.read(transport);
        ReactNoop.render(flightModel);
      });
      expect(ReactNoop).toMatchRenderedOutput(<span>Override</span>);
    });

    // @gate enableServerContext
    it('sets default initial value when defined lazily on server or client', async () => {
      let ServerContext;
      function inlineLazyServerContextInitialization() {
        if (!ServerContext) {
          ServerContext = createServerServerContext('ServerContext', 'default');
        }
        return ServerContext;
      }

      let ClientContext;
      function inlineContextInitialization() {
        if (!ClientContext) {
          ClientContext = createServerContext('ServerContext', 'default', true);
        }
        return ClientContext;
      }

      function ClientBaz() {
        const context = inlineContextInitialization();
        const value = React.useContext(context);
        return <div>{value}</div>;
      }

      const Baz = clientReference(ClientBaz);

      function Bar() {
        return (
          <article>
            <div>
              {ReactServer.useContext(inlineLazyServerContextInitialization())}
            </div>
            <Baz />
          </article>
        );
      }

      function ServerApp() {
        const Context = inlineLazyServerContextInitialization();
        return (
          <>
            <Context.Provider value="test">
              <Bar />
            </Context.Provider>
            <Bar />
          </>
        );
      }

      function ClientApp({serverModel}) {
        return (
          <>
            {serverModel}
            <ClientBaz />
          </>
        );
      }

      const transport = ReactNoopFlightServer.render(<ServerApp />);

      expect(ClientContext).toBe(undefined);

      // Reset all modules, except flight-modules which keeps the registry of Client Components
      const flightModules = require('react-noop-renderer/flight-modules');
      jest.resetModules();
      jest.mock('react', () => require('react/react.shared-subset'));
      jest.mock('react-noop-renderer/flight-modules', () => flightModules);

      ReactServer = require('react');
      ReactNoopFlightServer = require('react-noop-renderer/flight-server');

      __unmockReact();
      jest.resetModules();
      jest.mock('react-noop-renderer/flight-modules', () => flightModules);
      React = require('react');
      ReactNoop = require('react-noop-renderer');
      ReactNoopFlightClient = require('react-noop-renderer/flight-client');
      act = require('internal-test-utils').act;
      Scheduler = require('scheduler');

      await act(async () => {
        const serverModel = await ReactNoopFlightClient.read(transport);
        ReactNoop.render(<ClientApp serverModel={serverModel} />);
      });

      expect(ClientContext).not.toBe(ServerContext);

      expect(ReactNoop).toMatchRenderedOutput(
        <>
          <article>
            <div>test</div>
            <div>test</div>
          </article>
          <article>
            <div>default</div>
            <div>default</div>
          </article>
          <div>default</div>
        </>,
      );
    });
  });

  // @gate enableTaint
  it('errors when a tainted object is serialized', async () => {
    function UserClient({user}) {
      return <span>{user.name}</span>;
    }
    const User = clientReference(UserClient);

    const user = {
      name: 'Seb',
      age: 'rather not say',
    };
    ReactServer.experimental_taintObjectReference(
      "Don't pass the raw user object to the client",
      user,
    );
    const errors = [];
    ReactNoopFlightServer.render(<User user={user} />, {
      onError(x) {
        errors.push(x.message);
      },
    });

    expect(errors).toEqual(["Don't pass the raw user object to the client"]);
  });

  // @gate enableTaint
  it('errors with a specific message when a tainted function is serialized', async () => {
    function UserClient({user}) {
      return <span>{user.name}</span>;
    }
    const User = clientReference(UserClient);

    function change() {}
    ReactServer.experimental_taintObjectReference(
      'A change handler cannot be passed to a client component',
      change,
    );
    const errors = [];
    ReactNoopFlightServer.render(<User onChange={change} />, {
      onError(x) {
        errors.push(x.message);
      },
    });

    expect(errors).toEqual([
      'A change handler cannot be passed to a client component',
    ]);
  });

  // @gate enableTaint
  it('errors when a tainted string is serialized', async () => {
    function UserClient({user}) {
      return <span>{user.name}</span>;
    }
    const User = clientReference(UserClient);

    const process = {
      env: {
        SECRET: '3e971ecc1485fe78625598bf9b6f85db',
      },
    };
    ReactServer.experimental_taintUniqueValue(
      'Cannot pass a secret token to the client',
      process,
      process.env.SECRET,
    );

    const errors = [];
    ReactNoopFlightServer.render(<User token={process.env.SECRET} />, {
      onError(x) {
        errors.push(x.message);
      },
    });

    expect(errors).toEqual(['Cannot pass a secret token to the client']);

    // This just ensures the process object is kept alive for the life time of
    // the test since we're simulating a global as an example.
    expect(process.env.SECRET).toBe('3e971ecc1485fe78625598bf9b6f85db');
  });

  // @gate enableTaint
  it('errors when a tainted bigint is serialized', async () => {
    function UserClient({user}) {
      return <span>{user.name}</span>;
    }
    const User = clientReference(UserClient);

    const currentUser = {
      name: 'Seb',
      token: BigInt('0x3e971ecc1485fe78625598bf9b6f85dc'),
    };
    ReactServer.experimental_taintUniqueValue(
      'Cannot pass a secret token to the client',
      currentUser,
      currentUser.token,
    );

    function App({user}) {
      return <User token={user.token} />;
    }

    const errors = [];
    ReactNoopFlightServer.render(<App user={currentUser} />, {
      onError(x) {
        errors.push(x.message);
      },
    });

    expect(errors).toEqual(['Cannot pass a secret token to the client']);
  });

  // @gate enableTaint && enableBinaryFlight
  it('errors when a tainted binary value is serialized', async () => {
    function UserClient({user}) {
      return <span>{user.name}</span>;
    }
    const User = clientReference(UserClient);

    const currentUser = {
      name: 'Seb',
      token: new Uint32Array([0x3e971ecc, 0x1485fe78, 0x625598bf, 0x9b6f85dd]),
    };
    ReactServer.experimental_taintUniqueValue(
      'Cannot pass a secret token to the client',
      currentUser,
      currentUser.token,
    );

    function App({user}) {
      const clone = user.token.slice();
      return <User token={clone} />;
    }

    const errors = [];
    ReactNoopFlightServer.render(<App user={currentUser} />, {
      onError(x) {
        errors.push(x.message);
      },
    });

    expect(errors).toEqual(['Cannot pass a secret token to the client']);
  });

  // @gate enableTaint
  it('keep a tainted value tainted until the end of any pending requests', async () => {
    function UserClient({user}) {
      return <span>{user.name}</span>;
    }
    const User = clientReference(UserClient);

    function getUser() {
      const user = {
        name: 'Seb',
        token: '3e971ecc1485fe78625598bf9b6f85db',
      };
      ReactServer.experimental_taintUniqueValue(
        'Cannot pass a secret token to the client',
        user,
        user.token,
      );
      return user;
    }

    function App() {
      const user = getUser();
      const derivedValue = {...user};
      // A garbage collection can happen at any time. Even before the end of
      // this request. This would clean up the user object.
      gc();
      // We should still block the tainted value.
      return <User user={derivedValue} />;
    }

    let errors = [];
    ReactNoopFlightServer.render(<App />, {
      onError(x) {
        errors.push(x.message);
      },
    });

    expect(errors).toEqual(['Cannot pass a secret token to the client']);

    // After the previous requests finishes, the token can be rendered again.

    errors = [];
    ReactNoopFlightServer.render(
      <User user={{token: '3e971ecc1485fe78625598bf9b6f85db'}} />,
      {
        onError(x) {
          errors.push(x.message);
        },
      },
    );

    expect(errors).toEqual([]);
  });
});
