/* eslint-disable no-console */
import { HappyEmoji } from '@transferwise/icons';
import { ThemeProvider } from '@wise/components-theming';
import React from 'react';

import { Sentiment, Size, Theme, Variant } from '../common';
import {
  render,
  cleanup,
  screen,
  userEvent as user,
  fireEvent,
  mockMatchMedia,
} from '../test-utils';

import Alert, { AlertAction, AlertArrowPosition, AlertType } from './Alert';

jest.mock('react', () => {
  const originReact = jest.requireActual<typeof import('react')>('react');
  const mUseReference = jest.fn();
  return {
    ...originReact,
    useRef: mUseReference,
  };
});

mockMatchMedia();

describe('Alert', () => {
  let component: HTMLElement;
  let container: HTMLElement;
  let alert: HTMLElement;
  let closeButton: HTMLElement;
  let action: AlertAction;
  const userEvent = user.setup({
    advanceTimers: jest.advanceTimersByTimeAsync,
  });

  const classForType = (type: AlertType) => `alert-${type}`;

  const message = 'Your card is on its way.';

  const origWarn = console.warn;
  let mockedWarn: jest.Mock;

  beforeAll(() => {
    mockedWarn = jest.fn();
    console.warn = mockedWarn;
  });

  afterEach(() => {
    jest.clearAllMocks();
  });

  afterAll(() => {
    console.warn = origWarn;
  });

  describe('defaults', () => {
    beforeEach(() => {
      container = render(
        <ThemeProvider theme={Theme.LIGHT}>
          <Alert message={message} />
        </ThemeProvider>,
      ).container;
      component = screen.getByTestId('alert');
    });

    it('the message is rendered', () => {
      expect(screen.getByText(message)).toBeInTheDocument();
    });

    it('will be of type neutral', () => {
      expect(component).toHaveClass(classForType(Sentiment.NEUTRAL));
      expect(screen.getByTestId('info-icon')).toBeInTheDocument();
    });

    it('is not dismissible', () => {
      expect(container.querySelector('button')).not.toBeInTheDocument();
    });

    it('has no arrow', () => {
      expect(component).not.toHaveClass('arrow');
    });
  });

  describe('deprecated props', () => {
    it('renders arrows but logs a warning', () => {
      render(<Alert arrow={AlertArrowPosition.BOTTOM} message={message} />);
      component = screen.getByTestId('alert');

      expect(component).toHaveClass('arrow');
      expect(component).toHaveClass('arrow-bottom');
      expect(mockedWarn).toHaveBeenCalledWith(
        expect.stringContaining(
          "Alert component doesn't support 'arrow' anymore, use 'InlinePrompt' instead.",
        ),
      );
    });

    it('renders children but logs a warning', () => {
      render(<Alert>{message}</Alert>);

      expect(screen.getByText(message)).toBeInTheDocument();
      expect(mockedWarn).toHaveBeenCalledWith(
        expect.stringContaining(
          "Alert component doesn't support 'children' anymore, use 'message' instead.",
        ),
      );
    });

    it('dismissible is ignored and a warning is logged', () => {
      ({ container } = render(<Alert dismissible message={message} />));

      expect(container.querySelector('button')).not.toBeInTheDocument();
      expect(mockedWarn).toHaveBeenCalledWith(
        expect.stringContaining(
          "Alert component doesn't support 'dismissible' anymore, use 'onDismiss' instead.",
        ),
      );
    });

    it('size is ignored and a warning is logged', () => {
      const { container: small } = render(<Alert size={Size.SMALL} message={message} />);
      const { container: large } = render(<Alert size={Size.LARGE} message={message} />);

      expect(small.innerHTML).toStrictEqual(large.innerHTML);
      expect(mockedWarn).toHaveBeenCalledWith(
        expect.stringContaining(
          "Alert component doesn't support 'size' anymore, please remove that prop.",
        ),
      );
    });

    it('maps type SUCCESS to type POSITIVE and logs a warning', () => {
      render(<Alert type={Sentiment.SUCCESS} message={message} />);

      const success = screen.getByTestId('alert');

      expect(success).toHaveClass(classForType(Sentiment.POSITIVE));
      expect(screen.getByTestId('check-icon')).toBeInTheDocument();
      expect(mockedWarn).toHaveBeenCalledWith(
        "Alert component has deprecated 'success' value for the 'type' prop. Please use 'positive' instead.",
      );
    });

    it('maps type INFO to type NEUTRAL and logs a warning', () => {
      render(<Alert type={Sentiment.INFO} message={message} />);

      const info = screen.getByTestId('alert');

      expect(info).toHaveClass(classForType(Sentiment.NEUTRAL));
      expect(screen.getByTestId('info-icon')).toBeInTheDocument();
      expect(mockedWarn).toHaveBeenCalledWith(
        "Alert component has deprecated 'info' value for the 'type' prop. Please use 'neutral' instead.",
      );
    });
  });

  it('maps type ERROR to type NEGATIVE and logs a warning', () => {
    render(<Alert type={Sentiment.ERROR} message={message} />);

    const error = screen.getByTestId('alert');

    expect(error).toHaveClass(classForType(Sentiment.NEGATIVE));
    expect(screen.getByTestId('cross-icon')).toBeInTheDocument();
    expect(mockedWarn).toHaveBeenCalledWith(
      expect.stringContaining(
        "Alert component has deprecated 'error' value for the 'type' prop. Please use 'negative' instead.",
      ),
    );
  });

  describe('action', () => {
    it('sets text and href', () => {
      action = {
        href: 'fluffykittens.com',
        text: 'Learn more',
      };
      render(<Alert action={action} message={message} />);

      const element = screen.getByText(action.text?.toString() ?? '');

      expect(element).toHaveAttribute('href', action.href);
      expect(element).not.toHaveAttribute('aria-label');
      expect(element).not.toHaveAttribute('target');
    });

    it('sets text and onClick', async () => {
      action = {
        onClick: jest.fn(),
        text: 'Learn more',
      };
      render(<Alert action={action} message={message} />);

      await userEvent.click(screen.getByText('Learn more'));

      expect(action.onClick).toHaveBeenCalled();
    });

    it('adds additional attributes', () => {
      action = {
        'aria-label': 'Learn more about fluffy kittens',
        href: 'fluffykittens.com',
        text: 'Learn more',
        target: '_blank',
      };
      render(<Alert action={action} message={message} />);

      const element = screen.getByText(action.text?.toString() ?? '');

      expect(element).toHaveAttribute('aria-label', action['aria-label']);
      expect(element).toHaveAttribute('target', action.target);
    });
  });

  describe('markdown support', () => {
    const input = 'That is one **bold cat**';
    const output = 'That is one <strong>bold cat</strong>';

    it('converts message to markdown', () => {
      render(<Alert message={input} />);

      expect(screen.getByTestId('alert')).toContainHTML(output);
    });

    it('does not convert children to markdown', () => {
      render(<Alert>{input}</Alert>);

      expect(screen.getByText(input)).toBeInTheDocument();
      expect(screen.getByTestId('alert')).not.toContainHTML(output);
    });
  });

  describe('className', () => {
    it('applies provided classes', () => {
      render(<Alert className="cats" message={message} />);

      expect(screen.getByTestId('alert')).toHaveClass('cats');
    });
  });

  describe('custom icon', () => {
    it('uses any provided icon in preference to the default', () => {
      const icon = <HappyEmoji />;

      render(<Alert icon={icon} message={message} />);
      component = screen.getByTestId('alert');
      expect(screen.getByTestId('happy-emoji-icon')).toBeInTheDocument();
    });
  });

  describe('StatusIcon label override', () => {
    it('should accept the accessible name override for the icon', () => {
      const customIconLabel = 'Custom icon label';
      render(
        <Alert type={Sentiment.NEGATIVE} message={message} statusIconLabel={customIconLabel} />,
      );
      expect(screen.getByLabelText(customIconLabel)).toBeInTheDocument();
    });
  });

  describe('onDismiss', () => {
    it('renders the close button if onDismiss is provided', async () => {
      render(<Alert message={message} onDismiss={jest.fn()} />);
      const button = await screen.findByRole('button', { name: 'Close' });
      expect(button).toBeEnabled();
    });

    it('calls onDismiss when the close button is clicked', async () => {
      const onDismiss = jest.fn();
      render(<Alert message={message} onDismiss={onDismiss} />);
      const button = await screen.findByRole('button', { name: 'Close' });
      await userEvent.click(button);

      expect(onDismiss).toHaveBeenCalledTimes(1);
    });
  });

  describe('types', () => {
    const getComponentWithType = (type: AlertType) => {
      render(<Alert type={type} message={message} />);
      return screen.getByTestId('alert');
    };

    it('renders neutral', () => {
      component = getComponentWithType(Sentiment.NEUTRAL);

      expect(component).toHaveClass(classForType(Sentiment.NEUTRAL));
      expect(screen.getByTestId('info-icon')).toBeInTheDocument();
    });

    it('renders positive', () => {
      component = getComponentWithType(Sentiment.POSITIVE);

      expect(component).toHaveClass(classForType(Sentiment.POSITIVE));
      expect(screen.getByTestId('check-icon')).toBeInTheDocument();
    });

    it('renders negative', () => {
      component = getComponentWithType(Sentiment.NEGATIVE);

      expect(component).toHaveClass(classForType(Sentiment.NEGATIVE));
      expect(screen.getByTestId('cross-icon')).toBeInTheDocument();
    });

    it('renders warning', () => {
      component = getComponentWithType(Sentiment.WARNING);

      expect(component).toHaveClass(classForType(Sentiment.WARNING));
      expect(screen.getByTestId('alert-icon')).toBeInTheDocument();
    });

    it('renders error alerts with aria-role alert', () => {
      component = getComponentWithType(Sentiment.NEGATIVE);
      expect(screen.getByRole('alert')).toBeInTheDocument();
    });
    it('renders neutral alerts with aria-role status', () => {
      component = getComponentWithType(Sentiment.NEUTRAL);
      expect(screen.getByRole('status')).toBeInTheDocument();
    });
  });

  describe('on touch devices', () => {
    const { location } = window;

    beforeAll(() => {
      jest.spyOn(window, 'open').mockImplementation();
      // @ts-expect-error value gets set right after its deletion
      delete window.location;
      // @ts-expect-error TypeScript doesn't allow assigning a plain object to window.location
      window.location = {
        ...window.location,
        assign: jest.fn(),
      };
    });

    afterAll(() => {
      // @ts-expect-error TypeScript doesn't allow assigning a plain object to window.location
      window.location = location;
    });

    describe('when target is not blank', () => {
      beforeEach(() => {
        action = {
          'aria-label': 'Learn more about fluffy kittens',
          href: '/test',
          text: 'Learn more',
        };

        render(<Alert action={action} message={message} onDismiss={jest.fn()} />);

        alert = screen.getByTestId('alert');
        closeButton = screen.getByLabelText('Close');

        jest.spyOn(React, 'useRef').mockReturnValue({
          current: closeButton,
        });
      });

      it('loads action on tap', () => {
        fireEvent.touchStart(alert);
        expect(window.location.assign).not.toHaveBeenCalled();
        fireEvent.touchEnd(alert);
        expect(window.location.assign).toHaveBeenCalledWith(action.href);
      });

      it('doesn`t redirect on touch move', () => {
        fireEvent.touchStart(alert);
        expect(window.location.assign).not.toHaveBeenCalled();
        fireEvent.touchMove(alert);
        expect(window.location.assign).not.toHaveBeenCalled();
        fireEvent.touchEnd(alert);
        expect(window.location.assign).not.toHaveBeenCalled();
      });
    });

    describe('when target is blank', () => {
      beforeEach(() => {
        action = {
          'aria-label': 'Learn more about fluffy kittens',
          href: '/test',
          text: 'Learn more',
          target: '_blank',
        };

        render(<Alert action={action} message={message} onDismiss={jest.fn()} />);

        alert = screen.getByTestId('alert');
        closeButton = screen.getByLabelText('Close');

        jest.spyOn(React, 'useRef').mockReturnValue({
          current: closeButton,
        });
      });

      afterEach(() => {
        cleanup();
      });

      it('opens a new window on tap', () => {
        fireEvent.touchStart(alert);
        expect(window.open).not.toHaveBeenCalled();
        fireEvent.touchEnd(alert);
        expect(window.open).toHaveBeenCalledWith(action.href);
      });
    });

    describe('when action is not provided', () => {
      beforeEach(() => {
        render(<Alert message={message} onDismiss={jest.fn()} />);

        alert = screen.getByTestId('alert');
        closeButton = screen.getByLabelText('Close');

        jest.spyOn(React, 'useRef').mockReturnValue({
          current: closeButton,
        });
      });

      afterEach(() => {
        cleanup();
      });

      it('opens a new window on tap', () => {
        fireEvent.touchStart(alert);
        expect(window.open).not.toHaveBeenCalled();
        fireEvent.touchEnd(alert);
        expect(window.open).not.toHaveBeenCalled();
      });
    });

    it('when close button is clicked redirection is not triggered', () => {
      jest.spyOn(React, 'useRef').mockReturnValue({
        current: closeButton,
      });

      fireEvent.touchStart(closeButton);
      expect(window.open).not.toHaveBeenCalled();
      fireEvent.touchEnd(closeButton);
      expect(window.open).not.toHaveBeenCalled();
    });
  });

  describe('with personal theme', () => {
    it('renders StatusIcon for Personal theme', () => {
      render(
        <ThemeProvider theme={Theme.PERSONAL}>
          <Alert message={message} type={Sentiment.POSITIVE} />
        </ThemeProvider>,
      );

      expect(screen.getByTestId('status-icon')).toBeInTheDocument();
    });
  });

  describe('`active` prop backward compatibility', () => {
    it('should render wrapper and alert if `active` is unset', () => {
      render(<Alert message={message} />);
      expect(screen.getByRole('status')).toBeInTheDocument();
      expect(screen.getByText(message)).toBeInTheDocument();
    });
  });

  it('should disable all focusable elements inside until announced', () => {
    action = {
      href: 'fluffykittens.com',
      text: 'Learn more',
    };
    const { container } = render(<Alert action={action} message={message} onDismiss={jest.fn()} />);

    expect(screen.getByText(action.text as string)).toHaveAttribute('tabIndex', '-1');
    expect(container.querySelector('button')).toHaveAttribute('tabIndex', '-1');
  });
});
