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

import PhoneNumberInput, { PhoneNumberInputProps } from './PhoneNumberInput';

mockMatchMedia();
mockResizeObserver();

describe('PhoneNumberInput', () => {
  afterEach(jest.clearAllMocks);

  const props = { onChange: jest.fn() };

  const customRender = (overrides: Partial<PhoneNumberInputProps> = {}, locale?: string) =>
    render(<PhoneNumberInput {...props} {...overrides} />, { locale });

  const getCountryCodeSelect = () => screen.getByRole('combobox');
  const getCountryCodeLabel = () => screen.getByText('Country code');
  const getPhoneNumberInput = () => screen.getByRole('textbox');
  const getPhoneNumberLabel = () => screen.getByText('Phone number');

  describe('defaults', () => {
    it('should set prefix control to default UK value', () => {
      customRender();
      expect(getCountryCodeSelect()).toHaveTextContent('+44');
    });

    it('should set number control to empty', () => {
      customRender();
      expect(getPhoneNumberInput()).toHaveValue('');
    });

    it('should not disable the select', () => {
      customRender();
      expect(getCountryCodeSelect()).toBeEnabled();
    });

    it('should not disable the input', () => {
      customRender();
      expect(getPhoneNumberInput()).toBeEnabled();
    });
  });

  it('should propagate `initialValue`', () => {
    const prefix = '+39';
    const number = '123456789';
    customRender({ initialValue: `${prefix}${number}` });
    expect(getCountryCodeSelect()).toHaveTextContent(prefix);
    expect(getPhoneNumberInput()).toHaveValue(number);
  });

  describe('id prop', () => {
    it('should render sensible default IDs', () => {
      customRender();
      const countryCodeSelectID = getCountryCodeSelect().getAttribute('id');
      expect(countryCodeSelectID).toMatch(/^country-code-select-[a-z0-9]{6}$/);
      const countryCodeLabelID = getCountryCodeLabel().getAttribute('id');
      expect(countryCodeLabelID).toMatch(/^country-code-label-[a-z0-9]{6}$/);
      const phoneNumberInputID = getPhoneNumberInput().getAttribute('id');
      expect(phoneNumberInputID).toMatch(/^phone-number-input-[a-z0-9]{6}$/);
      const phoneNumberLabelID = getPhoneNumberLabel().getAttribute('id');
      expect(phoneNumberLabelID).toMatch(/^phone-number-label-[a-z0-9]{6}$/);
    });

    it('should use the custom `id` as-is for the input but with slight modification for select', () => {
      const id = 'component-id';
      customRender({ id });
      expect(getPhoneNumberInput()).toHaveAttribute('id', id);
      expect(getPhoneNumberLabel()).toHaveAttribute('id', `${id}-phone-number-label`);
      expect(getCountryCodeSelect()).toHaveAttribute('id', `${id}-country-code-select`);
      expect(getCountryCodeLabel()).toHaveAttribute('id', `${id}-country-code-label`);
    });

    it('should set correct ARIA attributes on the country code select when id is provided', () => {
      const id = 'my-id';
      customRender({ id });
      expect(getCountryCodeSelect()).toHaveAttribute('aria-labelledby', `${id}-country-code-label`);
    });

    it('should set correct ARIA attributes on the country code select when id is not provided', () => {
      customRender();
      const selectLabelledBy = getCountryCodeSelect().getAttribute('aria-labelledby');
      expect(selectLabelledBy).toMatch(/^country-code-label-[a-z0-9]{6}$/);
    });

    it('should set correct ARIA attributes on the phone number input when id is provided', () => {
      const id = 'my-id';
      customRender({ id });
      expect(getPhoneNumberInput()).toHaveAttribute('aria-labelledby', `${id}-phone-number-label`);
    });

    it('should set correct ARIA attributes on the phone number input when id is not provided', () => {
      customRender();
      const inputLabelledBy = getPhoneNumberInput().getAttribute('aria-labelledby');
      expect(inputLabelledBy).toMatch(/^phone-number-label-[a-z0-9]{6}$/);
    });
  });

  describe('pasting', () => {
    const initialPrefix = '+48';
    const initialNumber = '987654321';

    const renderAndPaste = async (value: string) => {
      customRender({ initialValue: `${initialPrefix}${initialNumber}` });
      await userEvent.tab();
      await userEvent.tab();
      await userEvent.paste(value);
    };

    [
      { number: '+36303932551', countryCode: '+36', localNumber: '303932551' },
      { number: '+39123456781', countryCode: '+39', localNumber: '123456781' },
      { number: '+44 7700 900415', countryCode: '+44', localNumber: '7700900415' },
      { number: '+2975557308515', countryCode: '+297', localNumber: '5557308515' },
      { number: '+297-555-7217-360', countryCode: '+297', localNumber: '5557217360' },
      { number: '+213-555-5160-67', countryCode: '+213', localNumber: '555516067' },
      { number: '+246-387-5553', countryCode: '+246', localNumber: '3875553' },
      { number: '+852-940-5558--6', countryCode: '+852', localNumber: '94055586' },
      { number: '+228 253 5558     4', countryCode: '+228', localNumber: '25355584' },
    ].forEach(({ number, countryCode, localNumber }) => {
      it(`'${number}' number should update the value properly`, async () => {
        await renderAndPaste(number);

        expect(getCountryCodeSelect()).toHaveTextContent(countryCode);
        expect(getPhoneNumberInput()).toHaveValue(localNumber);
        expect(props.onChange).toHaveBeenCalledWith(number.replace(/[\s-]+/g, ''), countryCode);
      });
    });

    it('should not paste invalid characters', async () => {
      await renderAndPaste('+36asdasdasd');
      expect(getCountryCodeSelect()).toHaveTextContent(initialPrefix);
      expect(getPhoneNumberInput()).toHaveValue(initialNumber);
      expect(props.onChange).not.toHaveBeenCalled();
    });

    it('should not paste countries which are not in the select', async () => {
      await renderAndPaste('+9992342343423');
      expect(getCountryCodeSelect()).toHaveTextContent(initialPrefix);
      expect(getPhoneNumberInput()).toHaveValue(initialNumber);
      expect(props.onChange).not.toHaveBeenCalled();
    });

    it("should not paste numbers which doesn't start with the country code", async () => {
      await renderAndPaste('0+36303932551');
      expect(getCountryCodeSelect()).toHaveTextContent(initialPrefix);
      expect(getPhoneNumberInput()).toHaveValue(initialNumber);
      expect(props.onChange).not.toHaveBeenCalled();
    });

    it("should allow pasting numbers which don't contain a country code", async () => {
      const newNumber = '06303932551';
      await renderAndPaste(newNumber);
      expect(getCountryCodeSelect()).toHaveTextContent(initialPrefix);
      expect(getPhoneNumberInput()).toHaveValue(newNumber);
      expect(props.onChange).toHaveBeenCalledWith(`${initialPrefix}${newNumber}`, initialPrefix);
    });
  });

  describe('initialValue', () => {
    describe('when a model is supplied that could match more than one prefix', () => {
      const initialProps = { initialValue: '+1868123456789' };

      it('should set the select to the longest matching prefix', () => {
        customRender(initialProps);
        expect(getCountryCodeSelect()).toHaveTextContent('+1868');
      });

      it('should set the number input to the rest of the number', () => {
        customRender(initialProps);
        expect(getPhoneNumberInput()).toHaveValue('123456789');
      });
    });

    describe('when a model is supplied with no matching prefix', () => {
      const initialProps = { initialValue: '+999123456789' };

      it('should empty the select', () => {
        customRender(initialProps);
        expect(getCountryCodeSelect()).toHaveTextContent('Select an option...');
      });

      it('should put the whole value in the input without the plus', () => {
        customRender(initialProps);
        expect(getPhoneNumberInput()).toHaveValue('999123456789');
      });
    });

    describe('when an partial model is supplied (with a matching prefix)', () => {
      it('should set the select to the matching prefix and put the rest of the number in the suffix', () => {
        customRender({ initialValue: '+123' });
        expect(getCountryCodeSelect()).toHaveTextContent('+1');
        expect(getPhoneNumberInput()).toHaveValue('23');
      });
    });
  });

  describe('when disabled is true', () => {
    it('should disable both controls', () => {
      customRender({ disabled: true });
      expect(getCountryCodeSelect()).toBeDisabled();
      expect(getPhoneNumberInput()).toBeDisabled();
    });
  });

  describe('placeholders', () => {
    it('should use the provided placeholder', () => {
      const placeholder = 'custom placeholder';
      customRender({ placeholder });
      expect(getPhoneNumberInput()).toHaveAttribute('placeholder', placeholder);
    });

    it('should use the provided searchPlaceholder', async () => {
      const searchPlaceholder = 'search placeholder';
      customRender({ searchPlaceholder });
      await userEvent.click(getCountryCodeSelect());
      expect(screen.getByRole('combobox', { name: searchPlaceholder })).toBeInTheDocument();
    });
  });

  describe('when supplied with a locale', () => {
    describe('and a value', () => {
      it('should use the prefix of the supplied value', () => {
        customRender({ initialValue: '+12345678' }, 'es');
        expect(getCountryCodeSelect()).toHaveTextContent('+1');
      });
    });

    describe('and no value', () => {
      describe('and no country code', () => {
        it('should default the prefix to the local country', () => {
          customRender(undefined, 'es');
          expect(getCountryCodeSelect()).toHaveTextContent('+34');
        });
      });

      describe('and country code', () => {
        it('should override locale prefix with country specific prefix', () => {
          customRender({ countryCode: 'US' }, 'es');
          expect(getCountryCodeSelect()).toHaveTextContent('+1');
        });
      });
    });
  });

  describe('user input', () => {
    describe('valid number', () => {
      it('should trigger onChange handler', async () => {
        customRender();
        await userEvent.type(getPhoneNumberInput(), '123');
        expect(props.onChange).toHaveBeenCalledWith('+44123', '+44');
      });
    });

    describe('invalid number', () => {
      it('should trigger onChange with null value', async () => {
        customRender({ initialValue: '+1234' });
        await userEvent.type(getPhoneNumberInput(), '{Backspace}{Backspace}{Backspace}1');
        expect(props.onChange).toHaveBeenCalledWith(null, '+1');
      });
    });

    describe('when user insert invalid character', () => {
      it('should strip them', async () => {
        customRender({ initialValue: '+12345678' });
        await userEvent.type(getPhoneNumberInput(), '123--');
        expect(props.onChange).toHaveBeenCalledWith('+12345678123', '+1');
      });
    });

    describe('overlapping prefix and suffix numbers', () => {
      it("shouldn't change the prefix number on matching suffix input", async () => {
        customRender({ countryCode: 'eg' });
        await userEvent.type(getPhoneNumberInput(), '1111111');
        expect(getPhoneNumberInput()).toHaveValue('1111111');
        expect(props.onChange).toHaveBeenCalledWith('+201111111', '+20');
      });
    });

    describe('onChange deduplication', () => {
      it('should not call onChange when the composed phone number has not changed', () => {
        const onChangeMock = jest.fn();
        const { rerender } = render(
          <PhoneNumberInput initialValue="+441234567890" onChange={onChangeMock} />,
        );

        onChangeMock.mockClear();

        // Rerender with the same initialValue - internal state should not trigger onChange
        rerender(<PhoneNumberInput initialValue="+441234567890" onChange={onChangeMock} />);

        expect(onChangeMock).not.toHaveBeenCalled();
      });
    });
  });

  describe('when selectProps is supplied', () => {
    it('renders Select component with expected props', () => {
      customRender({ selectProps: { className: 'custom-class' } });
      expect(getCountryCodeSelect().parentElement).toHaveClass('custom-class');
    });
  });

  it('supports custom `aria-labelledby` attribute', () => {
    render(
      <>
        {/* eslint-disable-next-line jsx-a11y/label-has-associated-control */}
        <label id="prioritized-label">Prioritized label</label>
        <PhoneNumberInput aria-labelledby="prioritized-label" onChange={() => {}} />
      </>,
    );

    expect(
      within(screen.getByLabelText('Prioritized label')).getByRole('textbox'),
    ).toBeInTheDocument();
  });

  it('supports `Field` for labeling', () => {
    render(
      <Field label="Phone number">
        <PhoneNumberInput initialValue="+12345678" onChange={() => {}} />
      </Field>,
    );
    expect(screen.getAllByRole('group')[0]).toHaveAccessibleName(/^Phone number/);
  });

  it('focuses country code input when `Field` label is clicked', async () => {
    const label = 'Phone number';

    render(
      <Field label={label}>
        <PhoneNumberInput initialValue="+12345678" onChange={() => {}} />
      </Field>,
    );

    await userEvent.click(screen.getByText(label, { selector: 'label:not(.sr-only)' })); // Have to use `getByText` due to the way `Field` handles group labelling
    const countryCodeFilterInput = screen.getByRole('combobox');
    expect(countryCodeFilterInput).toHaveFocus();
  });

  it('focuses number input when `Field` label is clicked and country code is disabled', async () => {
    const label = 'Phone number';

    render(
      <Field label={label}>
        <PhoneNumberInput
          initialValue="+12345678"
          selectProps={{ disabled: true }}
          onChange={() => {}}
        />
      </Field>,
    );

    const phoneNumberInput = screen.getByRole('textbox');
    await userEvent.click(screen.getByText(label, { selector: 'label:not(.sr-only)' })); // Have to use `getByText` due to the way `Field` handles group labelling
    expect(phoneNumberInput).toHaveFocus();
  });
});
