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

describe('ReactDOMOption', () => {
  let React;
  let ReactDOM;
  let ReactDOMServer;
  let ReactTestUtils;

  beforeEach(() => {
    jest.resetModules();
    React = require('react');
    ReactDOM = require('react-dom');
    ReactDOMServer = require('react-dom/server');
    ReactTestUtils = require('react-dom/test-utils');
  });

  it('should flatten children to a string', () => {
    const stub = (
      <option>
        {1} {'foo'}
      </option>
    );
    const node = ReactTestUtils.renderIntoDocument(stub);

    expect(node.innerHTML).toBe('1 foo');
  });

  it('should warn for invalid child tags', () => {
    const el = (
      <option value="12">
        {1} <div /> {2}
      </option>
    );
    let node;
    expect(() => {
      node = ReactTestUtils.renderIntoDocument(el);
    }).toErrorDev(
      'validateDOMNesting(...): <div> cannot appear as a child of <option>.\n' +
        '    in div (at **)\n' +
        '    in option (at **)',
    );
    expect(node.innerHTML).toBe('1 <div></div> 2');
    ReactTestUtils.renderIntoDocument(el);
  });

  it('should warn for component child if no value prop is provided', () => {
    function Foo() {
      return '2';
    }
    const el = (
      <option>
        {1} <Foo /> {3}
      </option>
    );
    let node;
    expect(() => {
      node = ReactTestUtils.renderIntoDocument(el);
    }).toErrorDev(
      'Cannot infer the option value of complex children. ' +
        'Pass a `value` prop or use a plain string as children to <option>.',
    );
    expect(node.innerHTML).toBe('1 2 3');
    ReactTestUtils.renderIntoDocument(el);
  });

  it('should not warn for component child if value prop is provided', () => {
    function Foo() {
      return '2';
    }
    const el = (
      <option value="123">
        {1} <Foo /> {3}
      </option>
    );
    const node = ReactTestUtils.renderIntoDocument(el);
    expect(node.innerHTML).toBe('1 2 3');
    ReactTestUtils.renderIntoDocument(el);
  });

  it('should ignore null/undefined/false children without warning', () => {
    const stub = (
      <option>
        {1} {false}
        {true}
        {null}
        {undefined} {2}
      </option>
    );
    const node = ReactTestUtils.renderIntoDocument(stub);

    expect(node.innerHTML).toBe('1  2');
  });

  it('should throw on object children', () => {
    expect(() => {
      ReactTestUtils.renderIntoDocument(<option>{{}}</option>);
    }).toThrow('Objects are not valid as a React child');
    expect(() => {
      ReactTestUtils.renderIntoDocument(<option>{[{}]}</option>);
    }).toThrow('Objects are not valid as a React child');
    expect(() => {
      ReactTestUtils.renderIntoDocument(
        <option>
          {{}}
          <span />
        </option>,
      );
    }).toThrow('Objects are not valid as a React child');
    expect(() => {
      ReactTestUtils.renderIntoDocument(
        <option>
          {'1'}
          {{}}
          {2}
        </option>,
      );
    }).toThrow('Objects are not valid as a React child');
  });

  it('should support element-ish child', () => {
    // This is similar to <fbt>.
    // We don't toString it because you must instead provide a value prop.
    const obj = {
      $$typeof: Symbol.for('react.element'),
      type: props => props.content,
      ref: null,
      key: null,
      props: {
        content: 'hello',
      },
      toString() {
        return this.props.content;
      },
    };

    let node = ReactTestUtils.renderIntoDocument(
      <option value="a">{obj}</option>,
    );
    expect(node.innerHTML).toBe('hello');

    node = ReactTestUtils.renderIntoDocument(
      <option value="b">{[obj]}</option>,
    );
    expect(node.innerHTML).toBe('hello');

    node = ReactTestUtils.renderIntoDocument(
      <option value={obj}>{obj}</option>,
    );
    expect(node.innerHTML).toBe('hello');
    expect(node.value).toBe('hello');

    node = ReactTestUtils.renderIntoDocument(
      <option value={obj}>
        {'1'}
        {obj}
        {2}
      </option>,
    );
    expect(node.innerHTML).toBe('1hello2');
    expect(node.value).toBe('hello');
  });

  it('should be able to use dangerouslySetInnerHTML on option', () => {
    const stub = <option dangerouslySetInnerHTML={{__html: 'foobar'}} />;
    let node;
    expect(() => {
      node = ReactTestUtils.renderIntoDocument(stub);
    }).toErrorDev(
      'Pass a `value` prop if you set dangerouslyInnerHTML so React knows which value should be selected.\n' +
        '    in option (at **)',
    );

    expect(node.innerHTML).toBe('foobar');
  });

  it('should set attribute for empty value', () => {
    const container = document.createElement('div');
    const option = ReactDOM.render(<option value="" />, container);
    expect(option.hasAttribute('value')).toBe(true);
    expect(option.getAttribute('value')).toBe('');

    ReactDOM.render(<option value="lava" />, container);
    expect(option.hasAttribute('value')).toBe(true);
    expect(option.getAttribute('value')).toBe('lava');
  });

  it('should allow ignoring `value` on option', () => {
    const a = 'a';
    const stub = (
      <select value="giraffe" onChange={() => {}}>
        <option>monkey</option>
        <option>gir{a}ffe</option>
        <option>gorill{a}</option>
      </select>
    );
    const options = stub.props.children;
    const container = document.createElement('div');
    const node = ReactDOM.render(stub, container);

    expect(node.selectedIndex).toBe(1);

    ReactDOM.render(<select value="gorilla">{options}</select>, container);
    expect(node.selectedIndex).toEqual(2);
  });

  it('generates a warning and hydration error when an invalid nested tag is used as a child', () => {
    const ref = React.createRef();
    const children = (
      <select readOnly={true} value="bar">
        <option value="bar">
          {['Bar', false, 'Foo', <div key="1" ref={ref} />, 'Baz']}
        </option>
      </select>
    );

    const container = document.createElement('div');

    container.innerHTML = ReactDOMServer.renderToString(children);

    expect(container.firstChild.getAttribute('value')).toBe(null);
    expect(container.firstChild.getAttribute('defaultValue')).toBe(null);

    const option = container.firstChild.firstChild;
    expect(option.nodeName).toBe('OPTION');

    expect(option.textContent).toBe('BarFooBaz');
    expect(option.selected).toBe(true);

    expect(() => ReactDOM.hydrate(children, container)).toErrorDev([
      'Text content did not match. Server: "FooBaz" Client: "Foo"',
      'validateDOMNesting(...): <div> cannot appear as a child of <option>.',
    ]);

    expect(option.textContent).toBe('BarFooBaz');
    expect(option.selected).toBe(true);

    expect(ref.current.nodeName).toBe('DIV');
    expect(ref.current.parentNode).toBe(option);
  });
});
