import { mockAnimationsApi } from 'jsdom-testing-mocks';
import {
  mockMatchMedia,
  mockResizeObserver,
  render,
  screen,
  userEvent,
  waitFor,
  within,
} from '../test-utils';

import { MoneyInput, CurrencyItem, CurrencyOptionItem, Field } from '..';
import { MoneyInputPropsWithInputAttributes } from './MoneyInput';
import messages from './MoneyInput.messages';

mockMatchMedia();
mockResizeObserver();
mockAnimationsApi();

describe('Money Input', () => {
  const popularCurrencies: CurrencyOptionItem[] = [
    {
      value: 'EUR',
      label: 'EUR',
      note: 'Euro',
      currency: 'eur',
      searchable: 'Spain, Germany, France, Austria, Estonia',
    },
    {
      value: 'USD',
      label: 'USD',
      note: 'United States dollar',
      currency: 'usd',
      searchable: 'Hong Kong, Saudi Arabia',
    },
    {
      value: 'GBP',
      label: 'GBP',
      note: 'British pound',
      currency: 'gbp',
      searchable: 'England, Scotland, Wales',
    },
  ];
  const otherCurrencies: CurrencyOptionItem[] = [
    {
      value: 'CAD',
      label: 'CAD',
      note: 'Canadian dollar',
      currency: 'cad',
    },
    {
      value: 'AUD',
      label: 'AUD',
      note: 'Australian dollar',
      currency: 'aud',
    },
  ];
  const currencies: CurrencyItem[] = [
    { header: 'Popular currencies' },
    ...popularCurrencies,
    { header: 'Some other currencies' },
    ...otherCurrencies,
  ];
  const initialProps = {
    currencies,
    selectedCurrency: popularCurrencies[1],
    amount: 1000,
    onAmountChange: jest.fn(),
    onCurrencyChange: jest.fn(),
    onSearchChange: jest.fn(),
  };

  const customRender = (
    overrides: Partial<MoneyInputPropsWithInputAttributes> = {},
    locale?: string,
  ) => render(<MoneyInput {...initialProps} {...overrides} />, { locale });

  beforeEach(jest.clearAllMocks);

  const getTrigger = () => screen.getByRole('combobox', { name: /Select currency/ });
  const getInput = () => screen.getAllByRole('textbox')[0];

  const openDropdown = async () => {
    await userEvent.click(getTrigger());
  };

  const getPopularGroup = () => screen.getByRole('group', { name: 'Popular currencies' });
  const getPopularCurrencies = () => within(getPopularGroup()).getAllByRole('option');
  const getOtherGroup = () => screen.getByRole('group', { name: 'Some other currencies' });
  const getOtherCurrencies = () => within(getOtherGroup()).getAllByRole('option');

  it('renders a select with all currencies grouped', async () => {
    customRender();
    await openDropdown();
    expect(getPopularGroup()).toBeInTheDocument();
    getPopularCurrencies().forEach((option, index) => {
      expect(option).toHaveTextContent(
        new RegExp(`${popularCurrencies[index].label}\\s*?${popularCurrencies[index].note}`),
      );
    });
    expect(getOtherGroup()).toBeInTheDocument();
    getOtherCurrencies().forEach((option, index) => {
      expect(option).toHaveTextContent(
        new RegExp(`${otherCurrencies[index].label}\\s*?${otherCurrencies[index].note}`),
      );
    });
  });

  it('shows the currently active currency as active and hides its note', () => {
    customRender({ selectedCurrency: popularCurrencies[0] });
    expect(getTrigger()).toHaveTextContent(popularCurrencies[0].label);
    expect(getTrigger()).not.toHaveTextContent(popularCurrencies[0].note || '');
  });

  it('calls onCurrencyChange when the user selects a different currency', async () => {
    customRender();
    await openDropdown();
    await userEvent.keyboard('eur');
    await waitFor(() => {
      expect(screen.getAllByRole('option')).toHaveLength(1);
    });
    await userEvent.keyboard('{Enter}');

    await waitFor(() => {
      expect(initialProps.onCurrencyChange).toHaveBeenCalledTimes(1);
    });
    expect(initialProps.onCurrencyChange).toHaveBeenCalledWith(popularCurrencies[0]);

    await waitFor(() => {
      expect(screen.queryByRole('listbox')).not.toBeInTheDocument();
    });
    await openDropdown();
    await userEvent.keyboard('gbp');
    await waitFor(() => {
      expect(screen.getAllByRole('option')).toHaveLength(1);
    });
    await userEvent.keyboard('{Enter}');

    await waitFor(() => {
      expect(initialProps.onCurrencyChange).toHaveBeenCalledTimes(2);
    });
    expect(initialProps.onCurrencyChange).toHaveBeenCalledWith(popularCurrencies[2]);
  });

  describe('sizing', () => {
    (
      [
        { label: 'large as default', prop: undefined, suffix: 'lg' },
        { label: 'large', prop: 'lg', suffix: 'lg' },
        { label: 'medium', prop: 'md', suffix: 'md' },
        { label: 'small', prop: 'sm', suffix: 'sm' },
      ] as const
    ).forEach((props) => {
      it(`should respect ${props.label} size`, () => {
        const { container } = customRender({ size: props.prop });
        expect(container.querySelector('.tw-money-input')).toHaveClass(
          `input-group-${props.suffix}`,
        );
        expect(getTrigger()).toHaveClass(`np-form-control--size-${props.suffix}`);
      });
    });
  });

  describe('when searching', () => {
    it('hides headers', async () => {
      customRender();
      await openDropdown();
      await userEvent.keyboard('dollar');
      expect(screen.queryByRole('group', { name: 'Popular currencies' })).not.toBeInTheDocument();
      expect(
        screen.queryByRole('group', { name: 'Some other currencies' }),
      ).not.toBeInTheDocument();
    });

    it('searches by label', async () => {
      customRender();
      await openDropdown();
      await userEvent.keyboard('gb');
      const options = screen.getAllByRole('option');
      expect(options).toHaveLength(1);
      expect(options[0]).toHaveAccessibleName(new RegExp(popularCurrencies[2].label));
    });

    it('searches by note', async () => {
      customRender();
      await openDropdown();
      await userEvent.keyboard('dollar');
      const options = screen.getAllByRole('option');
      expect(options).toHaveLength(3);
      expect(options[0]).toHaveAccessibleName(new RegExp(popularCurrencies[1].note!));
      expect(options[1]).toHaveAccessibleName(new RegExp(otherCurrencies[0].note!));
      expect(options[2]).toHaveAccessibleName(new RegExp(otherCurrencies[1].note!));
    });

    it('searches by searchable string', async () => {
      customRender();
      await openDropdown();
      await userEvent.keyboard('hon');
      const options = screen.getAllByRole('option');
      expect(options).toHaveLength(1);
      expect(options[0]).toHaveAccessibleName(new RegExp(popularCurrencies[1].label));
    });

    describe('custom action', () => {
      const actionProps = {
        onCustomAction: jest.fn(),
        customActionLabel: 'Custom action label',
      };

      it('does not shows when onCustomAction is not set', async () => {
        customRender({ customActionLabel: actionProps.customActionLabel });
        await openDropdown();
        expect(
          screen.queryByRole('button', { name: actionProps.customActionLabel }),
        ).not.toBeInTheDocument();
      });

      it('shows when onCustomAction and customActionLabel are set', async () => {
        customRender(actionProps);
        await openDropdown();
        expect(
          screen.getByRole('button', { name: actionProps.customActionLabel }),
        ).toBeInTheDocument();
      });

      it('triggers onCustomAction but not onCurrencyChange', async () => {
        customRender(actionProps);
        await openDropdown();
        await userEvent.click(screen.getByRole('button', { name: actionProps.customActionLabel }));
        expect(actionProps.onCustomAction).toHaveBeenCalledTimes(1);
        expect(initialProps.onCurrencyChange).not.toHaveBeenCalled();
      });
    });

    it('sorts by labels first', async () => {
      customRender();
      await openDropdown();
      await userEvent.keyboard('au');
      const options = screen.getAllByRole('option');
      expect(options).toHaveLength(3);
      expect(options[0]).toHaveAccessibleName(new RegExp(otherCurrencies[1].label));
      expect(options[1]).toHaveAccessibleName(new RegExp(popularCurrencies[0].label));
      expect(options[2]).toHaveAccessibleName(new RegExp(popularCurrencies[1].label));
    });
  });

  describe('amount formatting', () => {
    it('formats the number you input after you blur it', async () => {
      customRender();
      const input = getInput();
      await userEvent.clear(input);
      await userEvent.type(input, '1234567.6543');
      await userEvent.tab();
      expect(input).toHaveValue('1,234,567.65');
    });

    it('uses custom decimals when specified', async () => {
      customRender({ decimals: 4 });
      const input = getInput();
      await userEvent.clear(input);
      await userEvent.type(input, '1234.56789');
      await userEvent.tab();
      expect(input).toHaveValue('1,234.5679');
    });

    it('uses custom decimals of 0', async () => {
      customRender({ decimals: 0 });
      const input = getInput();
      await userEvent.clear(input);
      await userEvent.type(input, '1234.56');
      await userEvent.tab();
      expect(input).toHaveValue('1,235');
    });

    it('ignores custom decimals override for zero-decimal currencies like JPY', async () => {
      const jpyCurrency: CurrencyOptionItem = {
        value: 'JPY',
        label: 'JPY',
        note: 'Japanese yen',
        currency: 'jpy',
      };
      customRender({
        decimals: 4,
        selectedCurrency: jpyCurrency,
        currencies: [jpyCurrency],
        amount: 1234,
      });
      const input = getInput();
      expect(input).toHaveValue('1,234');
      await userEvent.clear(input);
      await userEvent.type(input, '5678.9999');
      await userEvent.tab();
      expect(input).toHaveValue('5,679');
    });
  });

  it('calls onAmountChange with parsed and formatted value', async () => {
    customRender();
    expect(initialProps.onAmountChange).not.toHaveBeenCalled();
    await userEvent.type(getInput(), '.6543');
    expect(initialProps.onAmountChange).toHaveBeenCalledTimes(5);
    expect(initialProps.onAmountChange).toHaveBeenCalledWith(1000.65);
  });

  it('calls onAmountChange with value rounded to custom decimals', async () => {
    customRender({ decimals: 4, amount: null });
    await userEvent.type(getInput(), '12.34567');
    expect(initialProps.onAmountChange).toHaveBeenCalledWith(12.3457);
  });

  it('calls onAmountChange when input value is empty', async () => {
    customRender();
    await userEvent.clear(getInput());
    expect(initialProps.onAmountChange).toHaveBeenCalledTimes(1);
    expect(initialProps.onAmountChange).toHaveBeenCalledWith(null);
  });

  it('renders addon when element is passed through props', () => {
    const addonElement = <span data-testid="test-addon" />;
    customRender({ addon: addonElement });
    expect(screen.getByTestId('test-addon')).toBeInTheDocument();
  });

  describe('fixed currency', () => {
    const EEK = { value: 'EEK', label: 'EEK', currency: 'eek' };

    it('shows fixed currency view if currencies array is empty but select currency exists', () => {
      customRender({
        currencies: [],
        selectedCurrency: EEK,
      });
      expect(screen.queryByRole('combobox', { name: /Select currency/ })).not.toBeInTheDocument();
      expect(screen.getByText(EEK.label)).toBeInTheDocument();
    });

    it('shows fixed currency view if only one currency available and selected', () => {
      customRender({
        currencies: [EEK],
        selectedCurrency: EEK,
      });
      expect(screen.queryByRole('combobox', { name: /Select currency/ })).not.toBeInTheDocument();
      expect(screen.getByText(EEK.label)).toBeInTheDocument();
    });

    it('shows fixed currency view when no function is passed to onCurrencyChange prop', () => {
      customRender({ onCurrencyChange: undefined });
      expect(screen.queryByRole('combobox', { name: /Select currency/ })).not.toBeInTheDocument();
      expect(screen.getByText(popularCurrencies[1].label)).toBeInTheDocument();
    });

    describe('currency keyline and flag', () => {
      (
        [
          ['lg', true],
          ['md', true],
          ['sm', false],
        ] as const
      ).forEach(([size, isShown]) => {
        it(`${isShown ? 'shows' : `doesn't show`} for '${size}' input`, () => {
          const { container } = customRender({
            currencies: [EEK],
            selectedCurrency: EEK,
            size,
          });
          expect(container.querySelector('.wds-flag-eek'))[isShown ? 'toBeTruthy' : 'toBeFalsy']();
        });
      });
    });

    describe('disabled state', () => {
      it('should be enable by default', () => {
        customRender({
          currencies: [EEK],
          selectedCurrency: EEK,
        });
        expect(getInput()).toBeEnabled();
      });

      it('should be disabled when there is no onAmountChange prop', () => {
        customRender({
          currencies: [EEK],
          selectedCurrency: EEK,
          onAmountChange: undefined,
        });

        expect(getInput()).toBeDisabled();
      });
    });
  });

  describe('search placeholder', () => {
    it('uses default value', async () => {
      customRender();
      await openDropdown();
      expect(
        screen.getByRole('combobox', { name: messages.searchPlaceholder.defaultMessage }),
      ).toBeInTheDocument();
    });

    it('allows for custom values', async () => {
      const searchPlaceholder = 'custom placeholder';
      customRender({ searchPlaceholder });
      await openDropdown();
      expect(screen.getByRole('combobox', { name: searchPlaceholder })).toBeInTheDocument();
    });
  });

  it('calls onSearchChange', async () => {
    const searchQuery = 'aus';
    customRender();
    await openDropdown();
    await userEvent.keyboard(searchQuery);

    expect(initialProps.onSearchChange).toHaveBeenCalledTimes(searchQuery.length);
    expect(initialProps.onSearchChange).toHaveBeenCalledWith({
      filteredOptions: [popularCurrencies[0], otherCurrencies[1]],
      searchQuery,
    });

    await userEvent.keyboard('{Backspace}{Backspace}{Backspace}');
    expect(initialProps.onSearchChange).toHaveBeenCalledWith({
      filteredOptions: currencies,
      searchQuery: '',
    });
  });

  it('ensures namespaced classNames can be provided and used', () => {
    const classNames = { 'tw-money-input': 'customClass' };
    customRender({ classNames });
    expect(screen.getAllByRole('group')[0]).toHaveClass(classNames['tw-money-input']);
  });

  describe('placeholder', () => {
    it('shows a formatted placeholder when provided', () => {
      customRender({ placeholder: 12345767.6543 });
      expect(getInput()).toHaveAttribute('placeholder', '12,345,767.65');
    });

    it('allows a placeholder of 0', () => {
      customRender({ placeholder: 0 });
      expect(getInput()).toHaveAttribute('placeholder', '0');
    });
  });

  it('should respect selectProps', async () => {
    customRender({
      selectProps: {
        className: 'selectClassName',
      },
    });
    expect(screen.getAllByRole('group')[1]).toHaveClass('selectClassName');
  });

  describe('text input', () => {
    it.each([
      ['asd', ''],
      ['1a2s3d', '123'],
      ['±!@#$^*_+?><', ''],
      ['1±!@#$^*,_+?><2', '1,2'],
      ['12,3', '12,3'],
      ['12.3', '12.3'],
    ] as const)(
      "ignores the letters when typed '%s' and shows '%s'",
      async (testValue: string, expectedValue: string) => {
        customRender({ amount: null });
        await userEvent.type(getInput(), testValue);
        expect(getInput()).toHaveValue(expectedValue);
      },
    );
  });

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

    expect(screen.getAllByLabelText('Prioritized label')[0]).toHaveClass('np-form-control');
  });

  it('supports `Field` for labeling', () => {
    render(
      <Field label="Recipient gets">
        <MoneyInput {...initialProps} />
      </Field>,
    );

    expect(screen.getAllByRole('group')[0]).toHaveAccessibleName(/^Recipient gets/);
  });

  it('focuses money input when `Field` label is clicked', async () => {
    const label = 'Recipient gets';

    render(
      <Field label={label}>
        <MoneyInput {...initialProps} />
      </Field>,
    );

    const input = screen.getByRole('textbox');
    await userEvent.click(screen.getByLabelText(label));
    expect(input).toHaveFocus();
  });

  describe('ids', () => {
    it('renders Select component with generated id when id is not provided', () => {
      customRender();
      expect(getTrigger()).toHaveAttribute('id');
    });

    it('passes the id given to the input element', () => {
      customRender({ id: 'some-id' });
      expect(getInput()).toHaveAttribute('id', 'some-id');
    });

    it('should guarantee id and connect the input with the selected currency via withId HoC', () => {
      customRender();
      expect(getInput().getAttribute('id')).toBeTruthy();
      expect(getInput()).toHaveAttribute('aria-describedby', getTrigger().getAttribute('id'));
    });

    it('should have unique id for the select filter with predefined id', async () => {
      const fieldId = 'myFieldId';
      const searchPlaceholder = 'Type a currency or country';
      render(
        <Field label="Multiple currencies" id={fieldId}>
          <MoneyInput {...initialProps} searchPlaceholder={searchPlaceholder} />
        </Field>,
      );
      await openDropdown();
      expect(screen.getByLabelText(searchPlaceholder)).toHaveAttribute(
        'id',
        `${fieldId}SelectedCurrencySearch`,
      );
    });

    it('should have unique id for the select filter without predefined id', async () => {
      const searchPlaceholder = 'Type a currency or country';
      render(
        <Field label="Multiple currencies">
          <MoneyInput {...initialProps} searchPlaceholder={searchPlaceholder} />
        </Field>,
      );
      await openDropdown();
      expect(screen.getByLabelText(searchPlaceholder)).toHaveAttribute(
        'id',
        expect.stringMatching(/^:.*?:SelectedCurrencySearch$/),
      );
    });
  });

  it('should have AT label for the currency dropdown', () => {
    customRender();
    expect(getTrigger()).toHaveAttribute('aria-label', messages.selectCurrencyLabel.defaultMessage);
  });

  it('should have a listbox label', async () => {
    customRender();
    const trigger = getTrigger();
    await openDropdown();
    const triggerLabel = trigger.getAttribute('aria-label');
    expect(triggerLabel).toBeTruthy();
    expect(screen.getByRole('listbox', { name: triggerLabel ?? '' })).toBeInTheDocument();
  });

  it('renders custom action button in dropdown footer and calls onCustomAction', async () => {
    const onCustomAction = jest.fn();
    render(
      <MoneyInput
        currencies={[
          { value: 'USD', label: 'US Dollar', currency: 'usd' },
          { value: 'EUR', label: 'Euro', currency: 'eur' },
        ]}
        selectedCurrency={{ value: 'USD', label: 'US Dollar', currency: 'usd' }}
        amount={100}
        customActionLabel="Go to Wishes"
        onCurrencyChange={jest.fn()}
        onAmountChange={jest.fn()}
        onCustomAction={onCustomAction}
      />,
    );

    const trigger = screen.getByRole('combobox', { name: /select currency/i });
    await userEvent.click(trigger);

    const customActionBtn = await screen.findByRole('button', { name: /go to wishes/i });
    expect(customActionBtn).toBeInTheDocument();

    await userEvent.click(customActionBtn);
    expect(onCustomAction).toHaveBeenCalled();
  });
});
