import { shallow } from 'enzyme';

import { MoneyInput, Title, Input, SelectInput } from '..';
import { mockMatchMedia, mockResizeObserver } from '../test-utils';

mockMatchMedia();
mockResizeObserver();

jest.mock('../inputs/contexts', () => ({
  ...jest.requireActual('../inputs/contexts'),
  withInputAttributes: (Component) => Component,
}));

jest.mock('./currencyFormatting', () => ({
  parseAmount: jest.fn(),
  formatAmount: jest.fn(),
}));

const numberFormatting = require('./currencyFormatting');

const defaultLocale = 'en-GB';
jest.mock('react-intl', () => ({
  injectIntl: (Component) =>
    function (props) {
      return (
        <Component {...props} intl={{ locale: defaultLocale, formatMessage: (id) => String(id) }} />
      );
    },
  defineMessages: (translations) => translations,
  useIntl: () => ({ locale: defaultLocale, formatMessage: (id) => String(id) }),
}));

describe('Money Input', () => {
  let component;
  let props;

  let currencies;

  const EEK = { value: 'EEK', label: 'EEK', currency: 'eek' };

  beforeEach(() => {
    currencies = [
      {
        header: 'Popular currencies',
      },
      {
        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',
      },
      {
        header: 'Some other currencies',
      },
      {
        value: 'CAD',
        label: 'CAD',
        note: 'Canadian dollar',
        currency: 'cad',
      },
      {
        value: 'AUD',
        label: 'AUD',
        note: 'Australian dollar',
        currency: 'aud',
      },
    ];
    props = {
      currencies,
      selectedCurrency: currencies[1],
      amount: 1000,
      onAmountChange: jest.fn(),
      onCurrencyChange: jest.fn(),
    };
    component = shallow(<MoneyInput {...props} />)
      .dive()
      .dive();
    jest.clearAllMocks();
  });

  function currencySelect() {
    return component.find(SelectInput);
  }

  function fixedCurrencyDisplay() {
    return component.find('.tw-money-input__fixed-currency');
  }

  function searchCurrencies(query) {
    currencySelect().prop('onFilterChange')({
      query,
      queryNormalized: query.trim().replace(/\s+/gu, ' ').normalize('NFKC').toLowerCase(),
    });
    component.update();
  }

  function displayedCurrencies() {
    return currencySelect().prop('items');
  }

  function getFooter() {
    return currencySelect().prop('renderFooter');
  }

  function amountInput() {
    return component.find(Input);
  }

  function focusAmount() {
    amountInput().simulate('focus');
  }

  function blurAmount() {
    amountInput().simulate('blur');
  }

  function enterAmount(amount) {
    focusAmount();
    amountInput().simulate('change', { target: { value: amount } });
  }

  function addon() {
    return component.find('.input-group-addon').first();
  }

  it('renders a select with all the currencies as options', () => {
    expect(currencySelect()).toHaveLength(1);
    expect(displayedCurrencies()).toStrictEqual([
      {
        type: 'group',
        label: 'Popular currencies',
        options: [
          {
            type: 'option',
            value: {
              value: 'EUR',
              label: 'EUR',
              note: 'Euro',
              currency: 'eur',
              searchable: 'Spain, Germany, France, Austria, Estonia',
            },
            filterMatchers: ['EUR', 'EUR', 'Euro', 'Spain, Germany, France, Austria, Estonia'],
          },
          {
            type: 'option',
            value: {
              value: 'USD',
              label: 'USD',
              note: 'United States dollar',
              currency: 'usd',
              searchable: 'Hong Kong, Saudi Arabia',
            },
            filterMatchers: ['USD', 'USD', 'United States dollar', 'Hong Kong, Saudi Arabia'],
          },
          {
            type: 'option',
            value: {
              value: 'GBP',
              label: 'GBP',
              note: 'British pound',
              currency: 'gbp',
              searchable: 'England, Scotland, Wales',
            },
            filterMatchers: ['GBP', 'GBP', 'British pound', 'England, Scotland, Wales'],
          },
        ],
      },
      {
        type: 'group',
        label: 'Some other currencies',
        options: [
          {
            type: 'option',
            value: {
              value: 'CAD',
              label: 'CAD',
              note: 'Canadian dollar',
              currency: 'cad',
            },
            filterMatchers: ['CAD', 'CAD', 'Canadian dollar', ''],
          },
          {
            type: 'option',
            value: {
              value: 'AUD',
              label: 'AUD',
              note: 'Australian dollar',
              currency: 'aud',
            },
            filterMatchers: ['AUD', 'AUD', 'Australian dollar', ''],
          },
        ],
      },
    ]);
  });

  it('renders Select component with generated id when id is not provided', () => {
    const select = component.find('SelectInput');

    expect(select.prop('id')).toBeTruthy();
  });

  it('shows the currently active currency as active and hides its note', () => {
    expect(currencySelect().prop('value')).toStrictEqual({
      value: 'EUR',
      label: 'EUR',
      note: 'Euro',
      currency: 'eur',
      searchable: 'Spain, Germany, France, Austria, Estonia',
    });
  });

  it('calls onCurrencyChange when the user selects a different currency', () => {
    expect(props.onCurrencyChange).not.toHaveBeenCalled();

    currencySelect().prop('onChange')(props.currencies[2]);
    expect(props.onCurrencyChange).toHaveBeenCalledTimes(1);
    expect(props.onCurrencyChange).toHaveBeenLastCalledWith(props.currencies[2]);
  });

  it('can be different sizes and defaults to lg', () => {
    expect(component.find('.input-group').hasClass('input-group-lg')).toBe(true);
    expect(currencySelect().prop('size')).toBe('lg');
    ['sm', 'md', 'lg'].forEach((size) => {
      component.setProps({ size });
      expect(currencySelect().prop('size')).toBe(size);
      expect(component.find('.input-group').hasClass(`input-group-${size}`)).toBe(true);
    });
  });

  describe('when searching', () => {
    it('hides headers', () => {
      currencies = [
        { header: 'A currency' },
        { value: 'GBP', label: 'British pound', currency: 'gbp' },
        { header: 'Another currency' },
        { value: 'EUR', label: 'Euro', currency: 'eur' },
      ];
      component.setProps({ currencies });
      expect(displayedCurrencies()).toStrictEqual([
        {
          type: 'group',
          label: 'A currency',
          options: [
            {
              type: 'option',
              value: { value: 'GBP', label: 'British pound', currency: 'gbp' },
              filterMatchers: ['GBP', 'British pound', '', ''],
            },
          ],
        },
        {
          type: 'group',
          label: 'Another currency',
          options: [
            {
              type: 'option',
              value: { value: 'EUR', label: 'Euro', currency: 'eur' },
              filterMatchers: ['EUR', 'Euro', '', ''],
            },
          ],
        },
      ]);

      searchCurrencies('O');
      expect(displayedCurrencies()).toStrictEqual([
        {
          type: 'option',
          value: { value: 'GBP', label: 'British pound', currency: 'gbp' },
          filterMatchers: ['GBP', 'British pound', '', ''],
        },
        {
          type: 'option',
          value: { value: 'EUR', label: 'Euro', currency: 'eur' },
          filterMatchers: ['EUR', 'Euro', '', ''],
        },
      ]);
    });

    it('searches by label', () => {
      currencies = [
        { value: 'GBP', label: 'British pound', currency: 'gbp' },
        { value: 'EUR', label: 'Euro', currency: 'eur' },
      ];
      component.setProps({ currencies });
      expect(displayedCurrencies()).toStrictEqual([
        {
          type: 'option',
          value: { value: 'GBP', label: 'British pound', currency: 'gbp' },
          filterMatchers: ['GBP', 'British pound', '', ''],
        },
        {
          type: 'option',
          value: { value: 'EUR', label: 'Euro', currency: 'eur' },
          filterMatchers: ['EUR', 'Euro', '', ''],
        },
      ]);

      searchCurrencies('P');
      expect(displayedCurrencies()).toStrictEqual([
        {
          type: 'option',
          value: { value: 'GBP', label: 'British pound', currency: 'gbp' },
          filterMatchers: ['GBP', 'British pound', '', ''],
        },
      ]);
    });

    it('searches by note', () => {
      currencies = [
        { value: 'GBP', label: 'British pound', currency: 'gbp', note: 'Queen money' },
        { value: 'EUR', label: 'Euro', currency: 'eur', note: 'Other money' },
      ];
      component.setProps({ currencies });
      expect(displayedCurrencies()).toStrictEqual([
        {
          type: 'option',
          value: { value: 'GBP', label: 'British pound', currency: 'gbp', note: 'Queen money' },
          filterMatchers: ['GBP', 'British pound', 'Queen money', ''],
        },
        {
          type: 'option',
          value: { value: 'EUR', label: 'Euro', currency: 'eur', note: 'Other money' },
          filterMatchers: ['EUR', 'Euro', 'Other money', ''],
        },
      ]);

      searchCurrencies('Other');
      expect(displayedCurrencies()).toStrictEqual([
        {
          type: 'option',
          value: { value: 'EUR', label: 'Euro', currency: 'eur', note: 'Other money' },
          filterMatchers: ['EUR', 'Euro', 'Other money', ''],
        },
      ]);
    });

    it('searches by searchable string', () => {
      currencies = [
        {
          value: 'GBP',
          label: 'British pound',
          currency: 'gbp',
          searchable: 'Great Britain, United Kingdom',
        },
        {
          value: 'EUR',
          label: 'Euro',
          currency: 'eur',
          searchable: 'Europe',
        },
      ];
      component.setProps({ currencies });
      expect(displayedCurrencies()).toStrictEqual([
        {
          type: 'option',
          value: {
            value: 'GBP',
            label: 'British pound',
            currency: 'gbp',
            searchable: 'Great Britain, United Kingdom',
          },
          filterMatchers: ['GBP', 'British pound', '', 'Great Britain, United Kingdom'],
        },
        {
          type: 'option',
          value: {
            value: 'EUR',
            label: 'Euro',
            currency: 'eur',
            searchable: 'Europe',
          },
          filterMatchers: ['EUR', 'Euro', '', 'Europe'],
        },
      ]);

      searchCurrencies('Britain');
      expect(displayedCurrencies()).toStrictEqual([
        {
          type: 'option',
          value: {
            value: 'GBP',
            label: 'British pound',
            currency: 'gbp',
            searchable: 'Great Britain, United Kingdom',
          },
          filterMatchers: ['GBP', 'British pound', '', 'Great Britain, United Kingdom'],
        },
      ]);
    });

    it('shows custom action option on every search when onCustomAction is passed to MoneyInput', () => {
      currencies = [
        {
          value: 'GBP',
          label: 'British pound',
          currency: 'gbp',
          searchable: 'Great Britain, United Kingdom',
        },
        {
          value: 'EUR',
          label: 'Euro',
          currency: 'eur',
          searchable: 'Europe',
        },
      ];
      component.setProps({
        currencies,
        onCustomAction: jest.fn(),
        customActionLabel: 'I am a label',
      });
      expect(displayedCurrencies()).toStrictEqual([
        {
          type: 'option',
          value: {
            value: 'GBP',
            label: 'British pound',
            currency: 'gbp',
            searchable: 'Great Britain, United Kingdom',
          },
          filterMatchers: ['GBP', 'British pound', '', 'Great Britain, United Kingdom'],
        },
        {
          type: 'option',
          value: {
            value: 'EUR',
            label: 'Euro',
            currency: 'eur',
            searchable: 'Europe',
          },
          filterMatchers: ['EUR', 'Euro', '', 'Europe'],
        },
      ]);

      searchCurrencies('Britain');
      expect(displayedCurrencies()).toStrictEqual([
        {
          type: 'option',
          value: {
            value: 'GBP',
            label: 'British pound',
            currency: 'gbp',
            searchable: 'Great Britain, United Kingdom',
          },
          filterMatchers: ['GBP', 'British pound', '', 'Great Britain, United Kingdom'],
        },
      ]);

      searchCurrencies('Random search string');
      expect(displayedCurrencies()).toHaveLength(0);
      const footerNode = shallow(getFooter()()).find('div');
      expect(footerNode).not.toBeNull();
    });

    it('sorts labels to first', () => {
      searchCurrencies('au');
      expect(displayedCurrencies()).toStrictEqual([
        {
          type: 'option',
          value: {
            value: 'AUD',
            label: 'AUD',
            note: 'Australian dollar',
            currency: 'aud',
          },
          filterMatchers: ['AUD', 'AUD', 'Australian dollar', ''],
        },
        {
          type: 'option',
          value: {
            value: 'EUR',
            label: 'EUR',
            note: 'Euro',
            currency: 'eur',
            searchable: 'Spain, Germany, France, Austria, Estonia',
          },
          filterMatchers: ['EUR', 'EUR', 'Euro', 'Spain, Germany, France, Austria, Estonia'],
        },
        {
          type: 'option',
          value: {
            value: 'USD',
            label: 'USD',
            note: 'United States dollar',
            currency: 'usd',
            searchable: 'Hong Kong, Saudi Arabia',
          },
          filterMatchers: ['USD', 'USD', 'United States dollar', 'Hong Kong, Saudi Arabia'],
        },
      ]);
    });
  });

  describe('amount formatting', () => {
    beforeEach(() => {
      jest
        .spyOn(numberFormatting, 'formatAmount')
        .mockImplementation(
          (number, currency, locale) => `formatted ${number}, ${locale}, ${currency}`,
        );
    });

    it('formats the number you input after you blur it', () => {
      component.setProps({ numberFormatPrecision: 3 });
      jest.spyOn(numberFormatting, 'parseAmount').mockImplementation(Number.parseFloat);
      enterAmount('123.45');
      expect(amountInput().prop('value')).toBe('123.45');

      blurAmount();
      expect(amountInput().prop('value')).toBe('formatted 123.45, en-GB, eur');
    });
  });

  it('parses the number you input and calls onAmountChange with it', () => {
    const onAmountChange = jest.fn();
    let assertions = 0;
    component.setProps({ onAmountChange, numberFormatPrecision: 1 });
    jest.spyOn(numberFormatting, 'parseAmount').mockImplementation((amount, currency, locale) => {
      expect(amount).toBe('500.1234');
      expect(locale).toBe(defaultLocale);
      expect(currency).toBe('eur');
      assertions += 1;
      return 500.1;
    });
    expect(onAmountChange).not.toHaveBeenCalled();

    enterAmount('500.1234');
    expect(onAmountChange).toHaveBeenCalledTimes(1);
    expect(onAmountChange).toHaveBeenLastCalledWith(500.1);
    expect(assertions).toBe(1);
  });

  it('does call onAmountChange when input value is empty', () => {
    const testValue = '';
    const onAmountChange = jest.fn();
    component.setProps({ onAmountChange });
    jest.spyOn(numberFormatting, 'parseAmount').mockImplementation();
    enterAmount(testValue);
    expect(numberFormatting.parseAmount).not.toHaveBeenCalled();
    expect(onAmountChange).toHaveBeenLastCalledWith(null);
  });

  it.each(['cannot parse this yo', '  '])(
    "does not call onAmountChange with a parsed number if unable to parse value '%s'",
    (testValue) => {
      const onAmountChange = jest.fn();
      component.setProps({ onAmountChange });
      jest.spyOn(numberFormatting, 'parseAmount').mockImplementation(() => NaN);
      enterAmount(testValue);
      expect(onAmountChange).not.toHaveBeenCalled();
      expect(numberFormatting.parseAmount).toHaveBeenLastCalledWith(
        testValue,
        'eur',
        defaultLocale,
      );
    },
  );

  it('passes the id given to the input element', () => {
    component.setProps({ id: 'some-id' });
    expect(amountInput().prop('id')).toBe('some-id');
  });

  it('renders addon when element is passed through props', () => {
    const addonElement = <span id="test-addon" />;
    component.setProps({ addon: addonElement });
    expect(addon()).toHaveLength(1);

    const passedInAddon = () => addon().children().first();
    expect(passedInAddon().prop('id')).toBe('test-addon');
    expect(passedInAddon().type()).toBe('span');
  });

  it('shows fixed currency view if currencies array is empty but select currency exists', () => {
    expect(fixedCurrencyDisplay()).toHaveLength(0);

    component.setProps({
      currencies: [],
      selectedCurrency: EEK,
    });

    expect(currencySelect()).toHaveLength(0);
    expect(fixedCurrencyDisplay()).toHaveLength(1);
    expect(component.find(Title).props().children).toBe('EEK');
  });

  it('shows fixed currency view if only one currency available and selected', () => {
    expect(fixedCurrencyDisplay()).toHaveLength(0);

    component.setProps({
      currencies: [EEK],
      selectedCurrency: EEK,
    });

    expect(currencySelect()).toHaveLength(0);
    expect(fixedCurrencyDisplay()).toHaveLength(1);
    expect(component.find(Title).props().children).toBe('EEK');
  });

  it('shows fixed currency view when no function is passed to onCurrencyChange prop', () => {
    component.setProps({ onCurrencyChange: undefined });
    expect(currencySelect()).toHaveLength(0);
    expect(fixedCurrencyDisplay()).toHaveLength(1);
  });

  it('does not shows fixed currency keyline and flag if small input', () => {
    component.setProps({
      currencies: [EEK],
      selectedCurrency: EEK,
      size: 'sm',
    });

    ['md', 'lg'].forEach((size) => {
      component.setProps({ size });
      expect(component.find('.money-input-currency-flag')).toHaveLength(1);
    });

    component.setProps({ size: 'sm' });
    expect(component.find('.money-input-currency-flag')).toHaveLength(0);
  });

  it('amount input will be disabled when there is no onAmountChange prop', () => {
    component.setProps({
      currencies: [EEK],
      selectedCurrency: EEK,
    });

    expect(amountInput().prop('disabled')).toBe(false);
    expect(component.find('.tw-money-input__fixed-currency').hasClass('disabled')).toBe(false);

    component.setProps({ onAmountChange: undefined });
    expect(amountInput().prop('disabled')).toBe(true);
    expect(component.find('.tw-money-input__fixed-currency').hasClass('disabled')).toBe(true);
  });

  it('uses the passed in search placeholder', () => {
    component.setProps({ searchPlaceholder: 'a placeholder' });
    expect(currencySelect().prop('filterPlaceholder')).toBe('a placeholder');
  });

  it('clears the search value when selecting an option', () => {
    const onSearchChange = jest.fn();
    component.setProps({ onSearchChange });
    searchCurrencies('eur');
    expect(onSearchChange).toHaveBeenLastCalledWith({
      searchQuery: 'eur',
      filteredOptions: [props.currencies[1]],
    });

    currencySelect().prop('onChange')(props.currencies[1]);
    component.update();
    expect(onSearchChange).toHaveBeenLastCalledWith({
      searchQuery: '',
      filteredOptions: [...props.currencies],
    });
  });

  it('shows custom action last when onCustomAction prop is passed to MoneyInput', () => {
    currencies = [
      {
        value: 'GBP',
        label: 'British pound',
        currency: 'gbp',
        searchable: 'Great Britain, United Kingdom',
      },
      {
        value: 'EUR',
        label: 'EUR',
        currency: 'eur',
        searchable: 'Europe',
      },
    ];
    const onCustomActionMock = jest.fn();
    component.setProps({
      currencies,
      onCustomAction: onCustomActionMock,
      customActionLabel: 'Label boy',
    });

    const options = displayedCurrencies();

    expect(options).toHaveLength(2);
    expect(options[0].value).toStrictEqual(currencies[0]);
    expect(options[1].value).toStrictEqual(currencies[1]);

    const footerNode = shallow(getFooter()()).find('div');

    expect(footerNode.prop('role')).toBe('button');
    expect(onCustomActionMock).toHaveBeenCalledTimes(0);
    footerNode.simulate('click');
    expect(onCustomActionMock).toHaveBeenCalledTimes(1);
  });

  it('calls onCustomAction callback when custom action is selected', () => {
    const onCustomAction = jest.fn();
    component.setProps({ onCustomAction });
    expect(onCustomAction).not.toHaveBeenCalled();
    expect(props.onCurrencyChange).not.toHaveBeenCalled();

    const footerNode = shallow(getFooter()()).find('div');
    expect(onCustomAction).toHaveBeenCalledTimes(0);
    footerNode.simulate('click');
    expect(onCustomAction).toHaveBeenCalledTimes(1);
    expect(props.onCurrencyChange).toHaveBeenCalledTimes(0);
  });

  it('ensures namespaced classNames can be provided and used', () => {
    const styles = { 'tw-money-input': 'tw-money-input_TWISAWESOME125' };
    expect(component.find('.tw-money-input').exists()).toBe(true);

    component.setProps({ classNames: styles });
    expect(component.find('.tw-money-input').exists()).toBe(false);
    expect(component.find('.tw-money-input_TWISAWESOME125').exists()).toBe(true);
  });

  describe('placeholder', () => {
    beforeEach(() => {
      jest
        .spyOn(numberFormatting, 'formatAmount')
        .mockImplementation(
          (number, currency, locale) => `formatted ${number}, ${locale}, ${currency}`,
        );
    });

    it('shows a formatted placeholder when provided', () => {
      component.setProps({ placeholder: 12345.67, numberFormatPrecision: 3 });
      expect(amountInput().prop('placeholder')).toBe('formatted 12345.67, en-GB, eur');
    });

    it('allows a placeholder of 0', () => {
      component.setProps({ placeholder: 0, numberFormatPrecision: 3 });
      expect(amountInput().prop('placeholder')).toBe('formatted 0, en-GB, eur');
    });
  });

  describe('onSearchChange()', () => {
    it('notifies the consumer when the search field is changed', () => {
      const getCurrencyByValue = (searchValue) =>
        currencies.find(({ value }) => value === searchValue);

      const onSearchChange = jest.fn();
      const onCustomAction = jest.fn();
      component.setProps({
        onSearchChange,
        customActionLabel: 'customActionLabel',
        onCustomAction,
      });

      searchCurrencies('e');
      expect(onSearchChange).toHaveBeenLastCalledWith({
        searchQuery: 'e',
        filteredOptions: [
          getCurrencyByValue('EUR'),
          getCurrencyByValue('USD'),
          getCurrencyByValue('GBP'),
        ],
      });

      searchCurrencies('');
      expect(onSearchChange).toHaveBeenLastCalledWith({
        searchQuery: '',
        filteredOptions: [...currencies],
      });

      searchCurrencies('eur');
      expect(onSearchChange).toHaveBeenLastCalledWith({
        searchQuery: 'eur',
        filteredOptions: [getCurrencyByValue('EUR')],
      });

      const footerNode = shallow(getFooter()()).find('div');
      footerNode.simulate('click');
      expect(onCustomAction).toHaveBeenCalledTimes(1);
      expect(onSearchChange).toHaveBeenLastCalledWith({
        searchQuery: '',
        filteredOptions: [...currencies],
      });
    });
  });

  describe('when passed selectProps', () => {
    beforeEach(() => {
      component = shallow(
        <MoneyInput
          selectProps={{
            buttonProps: {
              'aria-label': 'mock-button-label',
            },
          }}
          {...props}
        />,
      )
        .dive()
        .dive();
    });

    it('renders Select component with expected props', () => {
      const select = component.find('SelectInput');

      expect(select.prop('buttonProps')).toStrictEqual({
        'aria-label': 'mock-button-label',
      });
    });
  });
});
