import { DateInput, DateInputProps, Field } from '..';
import { mockMatchMedia, mockResizeObserver, render, userEvent, screen } from '../test-utils';

mockMatchMedia();
mockResizeObserver();

describe('Date Input Component', () => {
  const props: DateInputProps = {
    onChange: jest.fn(),
    onFocus: jest.fn(),
    onBlur: jest.fn(),
  };

  afterEach(jest.clearAllMocks);

  describe('when initialised without a model', () => {
    it('sets day field to empty', () => {
      render(<DateInput {...props} />);
      const dayInput = screen.getByRole('textbox', { name: /day/i });
      expect(dayInput).toHaveValue('');
    });

    it('sets month field to empty', () => {
      render(<DateInput {...props} />);
      const monthSelect = screen.getByRole('combobox', { name: /month/i });
      expect(monthSelect).toHaveValue('');
    });

    it('sets year field to empty', () => {
      render(<DateInput {...props} />);
      const yearInput = screen.getByRole('textbox', { name: /year/i });
      expect(yearInput).toHaveValue('');
    });
  });

  describe('when initialised with a model', () => {
    it('sets values correctly with a valid Date instance', () => {
      render(<DateInput {...props} value="1971-08-01" />);
      expect(screen.getByRole('textbox', { name: /day/i })).toHaveValue('1');
      expect(screen.getByRole('combobox', { name: /month/i })).toHaveTextContent('August');
      expect(screen.getByRole('textbox', { name: /year/i })).toHaveValue('1971');
    });

    it('sets values correctly with a valid short ISO8601 string', () => {
      render(<DateInput {...props} value="1990-08-22" />);
      expect(screen.getByRole('textbox', { name: /day/i })).toHaveValue('22');
      expect(screen.getByRole('combobox', { name: /month/i })).toHaveTextContent('August');
      expect(screen.getByRole('textbox', { name: /year/i })).toHaveValue('1990');
    });

    it('sets values correctly with a valid short ISO8601 string with year and month only', () => {
      render(<DateInput {...props} value="1990-08" />);
      expect(screen.getByRole('textbox', { name: /day/i })).toHaveValue('');
      expect(screen.getByRole('combobox', { name: /month/i })).toHaveTextContent('August');
      expect(screen.getByRole('textbox', { name: /year/i })).toHaveValue('1990');
    });

    it('sets values correctly with a valid long ISO8601 string', () => {
      render(<DateInput {...props} value="1990-02-28T00:00:00.000Z" />);
      expect(screen.getByRole('textbox', { name: /day/i })).toHaveValue('28');
      expect(screen.getByRole('combobox', { name: /month/i })).toHaveTextContent('February');
      expect(screen.getByRole('textbox', { name: /year/i })).toHaveValue('1990');
    });

    it('sets values to disabled when disabled is set to true', () => {
      render(<DateInput {...props} disabled />);
      expect(screen.getByRole('textbox', { name: /day/i })).toBeDisabled();
      expect(screen.getByRole('combobox', { name: /month/i })).toBeDisabled();
      expect(screen.getByRole('textbox', { name: /year/i })).toBeDisabled();
    });

    it("doesn't set values to disabled when disabled is set to false", () => {
      render(<DateInput {...props} />);
      expect(screen.getByRole('textbox', { name: /day/i })).toBeEnabled();
      expect(screen.getByRole('combobox', { name: /month/i })).toBeEnabled();
      expect(screen.getByRole('textbox', { name: /year/i })).toBeEnabled();
    });
  });

  describe('when initialised', () => {
    it(`doesn't call the onChange callback without an initial value`, () => {
      render(<DateInput {...props} />);
      expect(props.onChange).not.toHaveBeenCalled();
    });

    it(`doesn't call the onChange callback with an initial value`, () => {
      render(<DateInput {...props} value="1990-08-08" />);
      expect(props.onChange).not.toHaveBeenCalled();
    });
  });

  describe('when interacting', () => {
    it('calls the onChange callback with null when changing the day but nothing else is filled out', async () => {
      render(<DateInput {...props} />);
      await userEvent.type(screen.getByRole('textbox', { name: /day/i }), '4');
      expect(props.onChange).toHaveBeenCalledWith(null);
    });

    it('calls the onChange callback with null when changing the month but nothing else is filled out', async () => {
      render(<DateInput {...props} />);
      const monthSelect = screen.getByLabelText(/month/i);
      await userEvent.click(monthSelect);
      await userEvent.click(screen.getByRole('option', { name: /January/ }));
      expect(props.onChange).toHaveBeenCalledWith(null);
    }, 10_000);

    it('calls the onChange callback with null when changing the year but nothing else is filled out', async () => {
      render(<DateInput {...props} />);
      await userEvent.type(screen.getByRole('textbox', { name: /day/i }), '4444');
      expect(props.onChange).toHaveBeenCalledWith(null);
    });

    it('calls the onChange callback with the correct value when changing the day', async () => {
      render(<DateInput {...props} value="2022-12-1" />);
      const dayInput = screen.getByRole('textbox', { name: /day/i });
      // add 8 to the 1 that's already there
      await userEvent.type(dayInput, '8');
      expect(props.onChange).toHaveBeenCalledWith('2022-12-18');
    });

    it('calls the onChange callback with the correct value when changing the month', async () => {
      render(<DateInput {...props} value="2022-12-1" />);
      const monthSelect = screen.getByRole('combobox', { name: /month/i });
      await userEvent.click(monthSelect);
      await userEvent.click(screen.getByRole('option', { name: /January/ }));
      expect(props.onChange).toHaveBeenCalledWith('2022-01-01');
    }, 10_000);

    it('calls the onChange callback with the correct value when changing the year', async () => {
      render(<DateInput {...props} value="0122-12-1" />);
      const yearInput = screen.getByRole('textbox', { name: /year/i });
      // 0122 ends up being 122 in the box when it's supplied as the starting value
      await userEvent.type(yearInput, '2');
      expect(props.onChange).toHaveBeenCalledWith('1222-12-01');
    });

    it('calls the onChange callback with the null when not having a 4 digit year', async () => {
      render(<DateInput {...props} />);
      const yearInput = screen.getByRole('textbox', { name: /year/i });
      await userEvent.type(yearInput, '123');
      expect(props.onChange).toHaveBeenCalledWith(null);
    });

    it('calls the onChange callback with the null when changing the day to a day that is higher than max value', async () => {
      render(<DateInput {...props} value="2022-11-3" />);
      const dayInput = screen.getByRole('textbox', { name: /day/i });
      // add 1 to the 3 that's already there
      await userEvent.type(dayInput, '1');
      expect(props.onChange).toHaveBeenCalledWith(null);
    });

    it('does not overflow very high day values to the next month', async () => {
      render(<DateInput {...props} value="2022-11-6" />);
      const dayInput = screen.getByRole('textbox', { name: /day/i });
      // add 6 to the 6 that's already there
      await userEvent.type(dayInput, '6');
      expect(props.onChange).toHaveBeenCalledWith(null);
    });

    it('only pastes in first 6 digits of a long number for year', async () => {
      render(<DateInput {...props} value="2010-01-01" />);
      const yearInput = screen.getByRole('textbox', { name: /year/i });
      yearInput.focus();
      await userEvent.type(yearInput, '{backspace}{backspace}{backspace}{backspace}');
      await userEvent.paste('12345678');
      expect(props.onChange).toHaveBeenCalledWith('123456-01-01');
    });

    it('only pastes in first 2 digits of a long number for day', async () => {
      render(<DateInput {...props} value="2010-01-01" />);
      const yearInput = screen.getByRole('textbox', { name: /day/i });
      yearInput.focus();
      await userEvent.type(yearInput, '{backspace}{backspace}');
      await userEvent.paste('123456');
      expect(props.onChange).toHaveBeenCalledWith('2010-01-12');
    });
  });

  describe('propagating focus and blur callbacks', () => {
    it('should propagate if focusing from or blurring to external component', async () => {
      render(
        <>
          <button type="button">starting point</button>
          <DateInput {...props} />
        </>,
      );
      await userEvent.tab();
      await userEvent.tab();
      await userEvent.tab({ shift: true });
      expect(props.onFocus).toHaveBeenCalledTimes(1);
      expect(props.onBlur).toHaveBeenCalledTimes(1);
    });

    it('should not propagate if switching between internals', async () => {
      render(
        <>
          <button type="button">external button</button>
          <DateInput {...props} />
        </>,
      );
      const externalButton = screen.getByRole('button', { name: 'external button' });

      const day = screen.getByRole('textbox', { name: /day/i });
      const month = screen.getByRole('combobox', { name: /month/i });
      const year = screen.getByRole('textbox', { name: /year/i });

      await userEvent.click(externalButton);
      await userEvent.click(day);
      expect(props.onFocus).toHaveBeenCalledTimes(1);
      expect(props.onBlur).toHaveBeenCalledTimes(0);

      await userEvent.click(month);
      // selectinput really likes to refocus, apparently.
      expect(props.onFocus).toHaveBeenCalledTimes(4);
      expect(props.onBlur).toHaveBeenCalledTimes(0);

      await userEvent.click(year);
      expect(props.onFocus).toHaveBeenCalledTimes(5);
      expect(props.onBlur).toHaveBeenCalledTimes(0);

      await userEvent.click(externalButton);
      expect(props.onFocus).toHaveBeenCalledTimes(5);
      expect(props.onBlur).toHaveBeenCalledTimes(1);
    });
  });

  describe('when selectProps is provided', () => {
    it('renders Select component with expected props', () => {
      render(
        <DateInput
          selectProps={{
            id: 'mock-id',
          }}
          {...props}
        />,
      );
      const monthSelect = screen.getByRole('combobox', { name: /month/i });
      expect(monthSelect).toHaveAttribute('id', 'mock-id');
    });
  });

  it('supports `Field` for labeling', () => {
    render(
      <Field label="Date of birth">
        <DateInput onChange={() => {}} />
      </Field>,
    );
    expect(screen.getAllByRole('group')[0]).toHaveAccessibleName(/^Date of birth/);
  });

  it('focuses day input when `Field` label is clicked', async () => {
    const label = 'Date of birth';

    render(
      <Field label={label}>
        <DateInput onChange={() => {}} />
      </Field>,
    );

    const dayInput = screen.getByRole('textbox', { name: /day/i });
    await userEvent.click(screen.getByText(label, { selector: 'label' })); // Have to use `getByText` due to the way `Field` handles group labelling
    expect(dayInput).toHaveFocus();
  });

  it('focuses month input when `Field` label is clicked and day is not present', async () => {
    const label = 'Date of birth';

    render(
      <Field label={label}>
        <DateInput mode="month-year" onChange={() => {}} />
      </Field>,
    );
    const monthTrigger = screen.getByRole('combobox');
    await userEvent.click(screen.getByText(label, { selector: 'label' })); // Have to use `getByText` due to the way `Field` handles group labelling
    expect(monthTrigger).toHaveAttribute('aria-expanded', 'true');
  });
});
