import { ComponentProps, createRef } from 'react';
import { fireEvent, render, screen } from '@testing-library/react';
import userEvent from '@testing-library/user-event';
import TextField from './TextField';
import expectToThrow from './utils/testing/expectToThrow';

const LABEL = 'textfieldLabel';

const renderTextField = ({
  // Cmp Props
  id = 'test',

  onChange = jest.fn(),
  onFocus = jest.fn(),
  onBlur = jest.fn(),
  maxLength,
}: {
  id?: ComponentProps<typeof TextField>['id'];
  onChange?: ComponentProps<typeof TextField>['onChange'];
  onFocus?: ComponentProps<typeof TextField>['onFocus'];
  onBlur?: ComponentProps<typeof TextField>['onBlur'];
  maxLength?: ComponentProps<typeof TextField>['maxLength'];
}) =>
  render(
    <TextField
      id={id}
      label={LABEL}
      maxLength={maxLength}
      onBlur={onBlur}
      onChange={onChange}
      onFocus={onFocus}
    />,
  );

describe('TextField', () => {
  it('has an accessible maxLength', async () => {
    const errorAccessibilityLabel = 'Limit reached. You can only use 20 characters in this field.';

    renderTextField({
      maxLength: {
        characterCount: 20,
        errorAccessibilityLabel,
      },
    });

    const userInput = 'text';
    const userInput2 = 'very, very long text!';

    expect(screen.getByText('0/20')).toBeVisible();

    await userEvent.type(screen.getByLabelText(LABEL), userInput);

    expect(screen.getByText('4/20')).toBeVisible();

    await userEvent.type(screen.getByLabelText(LABEL), userInput2);

    expect(screen.getByText('20/20', { ignore: '.warning' })).not.toBeVisible();

    expect(screen.getByText('20/20', { ignore: '.subtle' })).toBeVisible();

    expect(screen.getByText(errorAccessibilityLabel)).toBeVisible();
  });

  /* eslint-disable-next-line jest/expect-expect */
  it('throws error on invalid maxLength', async () => {
    const errorAccessibilityLabel = 'Limit reached. You can only use 20 characters in this field.';
    expectToThrow(() => {
      renderTextField({
        maxLength: {
          characterCount: -20,
          errorAccessibilityLabel,
        },
      });
    }, '`maxLength` must be an integer value 0 or higher.');
  });

  it('renders error message on errorMessage prop change', () => {
    const { rerender } = render(
      <TextField id="test" onBlur={jest.fn()} onChange={jest.fn()} onFocus={jest.fn()} />,
    );
    expect(() => {
      screen.getByText('Error message');
    }).toThrow('Unable to find an element with the text: Error message');

    rerender(
      <TextField
        errorMessage="Error message"
        id="test"
        onBlur={jest.fn()}
        onChange={jest.fn()}
        onFocus={jest.fn()}
      />,
    );
    expect(screen.getByText('Error message')).toBeVisible();
  });

  it('reads the error message on focus', () => {
    render(
      <TextField
        errorMessage="Error message"
        id="test"
        onBlur={jest.fn()}
        onChange={jest.fn()}
        onFocus={jest.fn()}
        value="TextField Text"
      />,
    );

    fireEvent.focus(screen.getByDisplayValue('TextField Text'));
    // @ts-expect-error - TS2339 - Property 'toHaveAccessibleDescription' does not exist on type 'JestMatchers<HTMLElement>'.
    expect(screen.getByDisplayValue('TextField Text')).toHaveAccessibleDescription('Error message');
  });

  it('forwards a ref to <input />', () => {
    const ref = createRef<HTMLInputElement>();
    render(
      <TextField
        ref={ref}
        id="test"
        onBlur={jest.fn()}
        onChange={jest.fn()}
        onFocus={jest.fn()}
        value="TextField Text"
      />,
    );
    expect(ref.current instanceof HTMLInputElement).toEqual(true);
    expect(ref.current?.value).toEqual('TextField Text');
  });

  it('handles blur events', () => {
    const mockBlur = jest.fn<
      [
        {
          event: React.FocusEvent<HTMLInputElement>;
          value: string;
        },
      ],
      // @ts-expect-error - TS2344 - Type 'undefined' does not satisfy the constraint 'any[]'.
      undefined
    >();
    render(<TextField id="test" onBlur={mockBlur} onChange={jest.fn()} value="TextField Text" />);

    fireEvent.blur(screen.getByDisplayValue('TextField Text'));
    expect(mockBlur).toHaveBeenCalled();
  });

  it('handles change events', () => {
    const mockChange = jest.fn<
      [
        {
          event: React.ChangeEvent<HTMLInputElement>;
          value: string;
        },
      ],
      // @ts-expect-error - TS2344 - Type 'undefined' does not satisfy the constraint 'any[]'.
      undefined
    >();
    const { container } = render(
      <TextField id="test" onChange={mockChange} value="TextField Text" />,
    );

    // eslint-disable-next-line testing-library/no-container, testing-library/no-node-access -- Please fix the next time this file is touched!
    const input = container.querySelector('input');
    expect(input).not.toBe(null);

    if (input) {
      fireEvent.change(input, {
        target: { value: 'panda' },
      });
    }
    expect(mockChange).toHaveBeenCalled();
  });

  it('handles focus events', () => {
    const mockFocus = jest.fn<
      [
        {
          event: React.FocusEvent<HTMLInputElement>;
          value: string;
        },
      ],
      // @ts-expect-error - TS2344 - Type 'undefined' does not satisfy the constraint 'any[]'.
      undefined
    >();
    render(<TextField id="test" onChange={jest.fn()} onFocus={mockFocus} value="TextField Text" />);

    fireEvent.focus(screen.getByDisplayValue('TextField Text'));
    expect(mockFocus).toHaveBeenCalled();
  });

  it('handles key down events', () => {
    const mockKeyDown = jest.fn<
      [
        {
          event: React.KeyboardEvent<HTMLInputElement>;
          value: string;
        },
      ],
      // @ts-expect-error - TS2344 - Type 'undefined' does not satisfy the constraint 'any[]'.
      undefined
    >();
    const { container } = render(
      <TextField id="test" onChange={() => {}} onKeyDown={mockKeyDown} value="TextField Text" />,
    );

    // eslint-disable-next-line testing-library/no-container, testing-library/no-node-access -- Please fix the next time this file is touched!
    const input = container.querySelector('input');
    expect(input).not.toBe(null);

    if (input) {
      fireEvent.keyDown(input, {
        target: { value: 'panda' },
      });
    }
    expect(mockKeyDown).toHaveBeenCalled();
  });

  it('shows a label for the text field', () => {
    render(
      <TextField
        id="test"
        label="Label for the text field"
        onChange={() => {}}
        value="TextField Text"
      />,
    );
    expect(screen.getByText('Label for the text field')).toBeVisible();
  });

  it('shows helper text for the text field', () => {
    render(
      <TextField
        helperText="Helper text for the text field"
        id="test"
        label="Label for the text field"
        onChange={() => {}}
        value="TextField Text"
      />,
    );
    expect(screen.getByText('Helper text for the text field')).toBeVisible();
  });

  it('hides the helper text for the text field when an error message is shown', () => {
    render(
      <TextField
        errorMessage="Error message for the text field"
        helperText="Helper text for the text field"
        id="test"
        label="Label for the text field"
        onChange={() => {}}
        value="TextField Text"
      />,
    );
    expect(() => {
      screen.getByText('Helper text for the text field');
    }).toThrow('Unable to find an element with the text: Helper text for the text field');
  });

  it('adds a "medium" classname by default', () => {
    const { container } = render(
      <TextField id="test" onChange={() => {}} value="TextField Text" />,
    );
    // eslint-disable-next-line testing-library/no-container, testing-library/no-node-access -- Please fix the next time this file is touched!
    expect(container.querySelector('.medium')).toBeVisible();
  });

  it('adds a "large" classname when size is set to "lg"', () => {
    const { container } = render(
      <TextField id="test" onChange={() => {}} size="lg" value="TextField Text" />,
    );
    // eslint-disable-next-line testing-library/no-container, testing-library/no-node-access -- Please fix the next time this file is touched!
    expect(container.querySelector('.large')).toBeVisible();
  });
});
