import {
  act,
  cleanup,
  fireEvent,
  render,
  waitFor,
} from '@testing-library/react';
import React from 'react';
import { BrowserRouter as Router } from 'react-router-dom';
import { afterEach, describe, expect, it, test, vi } from 'vitest';
import { noop } from '../../../helpers/utils';
import { ConfirmationConfig } from '../../ConfirmDialog';
import { IconName } from '../../Icons';
import {
  Action,
  ActionProps,
  isContextAction,
  isNavigationAction,
} from './Action';

const defaultContextProps: ActionProps = {
  action: {
    label: 'action-label',
    onActionSelected: noop,
  },
};

const defaultNavigationProps: ActionProps = {
  action: {
    label: 'action-label',
    path: '#',
  },
};

describe('Action', () => {
  afterEach(() => {
    cleanup();
  });

  it('renders the component without crashing', () => {
    const { container } = render(<Action {...defaultContextProps} />);

    expect(container).toBeTruthy();
  });

  describe('Navigation Action', () => {
    it('renders a label', () => {
      const mockLabel = defaultNavigationProps.action.label;
      const { getByTestId } = render(
        <Router>
          <Action {...defaultNavigationProps} />
        </Router>,
      );

      const label = getByTestId('label');

      expect(label.textContent).toBe(mockLabel);
    });

    it(`displays a 'ChevronRight' as default icon`, () => {
      const { container } = render(
        <Router>
          <Action {...defaultNavigationProps} />
        </Router>,
      );

      const actionContainer = container.querySelector('.action-container');
      const icon = container.querySelector('svg');

      expect(icon).toBeInTheDocument();
      expect(actionContainer?.classList.contains('hasIcon')).toBe(true);
    });

    it(`displays a 'External' as default icon when "openInNewTab: true"`, () => {
      const { container } = render(
        <Router>
          <Action action={{ label: 'test', path: '/', openInNewTab: true }} />
        </Router>,
      );

      const actionContainer = container.querySelector('.action-container');
      const icon = container.querySelector('svg');

      expect(icon).toBeInTheDocument();
      expect(actionContainer?.classList.contains('hasIcon')).toBe(true);
    });

    it(`disables link when property isDisabled is 'true'`, () => {
      const { container } = render(
        <Router>
          <Action
            action={{ ...defaultNavigationProps.action, isDisabled: true }}
          />
        </Router>,
      );

      const actionContainer = container.querySelector('.action-container');

      expect(actionContainer?.classList.contains('isDisabled')).toBe(true);
    });

    it(`changes the icon if one provided by props`, () => {
      const iconName = IconName.External;
      const props = {
        action: { ...defaultNavigationProps.action, icon: iconName },
      };
      const { container } = render(
        <Router>
          <Action {...props} />
        </Router>,
      );

      const icon = container.querySelector('svg');

      expect(icon).toBeInTheDocument();
    });

    it(`renders a link`, () => {
      const { container } = render(
        <Router>
          <Action {...defaultNavigationProps} />
        </Router>,
      );

      const link = container.querySelector('a');

      expect(link).toBeInTheDocument();
    });
  });

  describe('Context Action', () => {
    describe('icon', () => {
      it('allows icon to be changed', () => {
        const iconProp: IconName = IconName.Delete;
        const { container } = render(
          <Action action={{ ...defaultContextProps.action, icon: iconProp }} />,
        );

        const icon = container.querySelector('svg');

        expect(icon).toBeInTheDocument();
      });

      it(`renders no icon by default`, () => {
        const { container } = render(
          <Action
            action={{
              ...defaultContextProps.action,
            }}
          />,
        );

        const icon = container.querySelector('svg');

        expect(icon).not.toBeInTheDocument();
      });

      it(`allows an icon for context action`, () => {
        const { container } = render(
          <Action
            action={{
              ...defaultContextProps.action,
              icon: IconName.ChevronUp,
            }}
          />,
        );

        const icon = container.querySelector('svg');

        expect(icon).toBeInTheDocument();
      });
    });

    it('renders a label', () => {
      const mockLabel = defaultContextProps.action.label;
      const { getByTestId } = render(
        <Action
          {...defaultContextProps}
          action={{
            label: mockLabel,
            onActionSelected: noop,
          }}
        />,
      );

      const label = getByTestId('label');

      expect(label.textContent).toBe(mockLabel);
    });

    it('raises the onActionSelected event', () => {
      const spy = vi.fn();
      const { getByTestId } = render(
        <Action
          {...defaultContextProps}
          action={{
            label: 'test-label',
            onActionSelected: spy,
          }}
        />,
      );

      const action = getByTestId('action');
      fireEvent.click(action);

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

    it(`adds a context background color`, () => {
      const { container } = render(
        <Action
          {...defaultContextProps}
          action={{
            label: 'test-label',
            onActionSelected: noop,
          }}
        />,
      );

      const action = container.querySelector('.action-container');

      expect(action?.classList.contains('context')).toBe(true);
    });

    describe('confirmation', () => {
      const mockLabel = 'test-label';

      it(`'Simple' mode changes the background color when clicked for the first time`, () => {
        const spy = vi.fn();

        const { getByTestId } = render(
          <Action
            {...defaultContextProps}
            action={{
              label: 'test-label',
              confirmationMode: 'Simple',
              onActionSelected: spy,
            }}
          />,
        );

        const action = getByTestId('action');
        expect(action.classList.contains('hasConfirm')).toBe(false);

        fireEvent.click(action);

        expect(action.classList.contains('hasConfirm')).toBe(true);
      });

      it(`'Simple' mode renders a confirmation message, raises the onActionSelected event after confirmation, and resets the action label`, () => {
        const confirmationMsg = 'Click again to confirm';
        const spy = vi.fn();

        const { getByTestId } = render(
          <Action
            {...defaultContextProps}
            action={{
              label: mockLabel,
              confirmationMode: 'Simple',
              onActionSelected: spy,
            }}
          />,
        );

        const action = getByTestId('action');
        let label = getByTestId('label');

        expect(label.textContent).toBe(mockLabel);

        fireEvent.click(action);

        const confirmLabel = getByTestId('confirm');
        expect(confirmLabel.textContent).toBe(confirmationMsg);

        fireEvent.click(action);

        label = getByTestId('label');

        expect(spy).toHaveBeenCalledTimes(1);
        expect(label.textContent).toBe(mockLabel);
      });

      it(`'Simple' mode renders a confirmation message, and doesn't raise the 'onActionSelected' event on 'mouseleave' when confirmation is required`, async () => {
        const confirmationMsg = 'Click again to confirm';
        const spy = vi.fn();
        vi.useFakeTimers();

        const { getByTestId } = render(
          <Action
            {...defaultContextProps}
            action={{
              label: mockLabel,
              confirmationMode: 'Simple',
              onActionSelected: spy,
            }}
          />,
        );

        const action = getByTestId('action');
        let label = getByTestId('label');

        expect(label.textContent).toBe(mockLabel);

        fireEvent.click(action);

        const confirmLabel = getByTestId('confirm');

        expect(confirmLabel.textContent).toBe(confirmationMsg);
        expect(action.classList.contains('hasConfirm')).toBe(true);

        fireEvent.mouseLeave(action);

        vi.runAllTimers();

        await waitFor(() => {
          label = getByTestId('label');
          expect(label.textContent).toBe(mockLabel);
        });

        expect(spy).not.toHaveBeenCalled();
        expect(action.classList.contains('hasConfirm')).toBe(false);

        vi.useRealTimers();
      });

      it(`'Advanced' mode changes the background color when clicked for the first time`, () => {
        const spy = vi.fn();

        const { getByTestId } = render(
          <Action
            {...defaultContextProps}
            action={{
              label: 'test-label',
              confirmationMode: 'Advanced',
              onActionSelected: spy,
            }}
          />,
        );

        const action = getByTestId('action');
        expect(action.classList.contains('hasConfirm')).toBe(false);

        fireEvent.click(action);

        expect(action.classList.contains('hasConfirm')).toBe(true);
      });

      it(`'Advanced' mode renders a confirmation pop up, raises the 'onActionSelected' event after the 'Confirm' button is clicked, and finally dismisses the confirmation pop up`, () => {
        const spy = vi.fn();

        const { queryByText, getByText, getByTestId } = render(
          <Action
            {...defaultContextProps}
            action={{
              label: mockLabel,
              confirmationMode: 'Advanced',
              onActionSelected: spy,
            }}
          />,
        );

        let confirmButton = queryByText('Confirm');
        expect(confirmButton).not.toBeInTheDocument();

        const action = getByTestId('action');
        fireEvent.click(action);

        confirmButton = getByText('Confirm');
        expect(confirmButton).toBeInTheDocument();

        fireEvent.click(confirmButton);

        confirmButton = queryByText('Confirm');
        expect(confirmButton).not.toBeInTheDocument();
        expect(spy).toHaveBeenCalledTimes(1);
      });

      it(`'Advanced' mode renders a confirmation pop up, does not raise the 'onActionSelected' event after the 'Cancel' button is clicked, and finally dismisses the confirmation pop up`, () => {
        const spy = vi.fn();

        const { queryByText, getByText, getByTestId } = render(
          <Action
            {...defaultContextProps}
            action={{
              label: mockLabel,
              confirmationMode: 'Advanced',
              onActionSelected: spy,
            }}
          />,
        );

        let cancelButton = queryByText('Cancel');
        expect(cancelButton).not.toBeInTheDocument();

        const action = getByTestId('action');
        fireEvent.click(action);

        cancelButton = getByText('Cancel');
        expect(cancelButton).toBeInTheDocument();

        fireEvent.click(cancelButton);

        cancelButton = queryByText('Cancel');
        expect(cancelButton).not.toBeInTheDocument();
      });

      it(`'Advanced' mode renders a confirmation pop up, does not raise the 'onActionSelected' event when the mouse leaves component, and finally dismisses the confirmation pop up`, async () => {
        const spy = vi.fn();
        vi.useFakeTimers();

        const { queryByText, getByText, getByTestId } = render(
          <Action
            {...defaultContextProps}
            action={{
              label: mockLabel,
              confirmationMode: 'Advanced',
              onActionSelected: spy,
            }}
          />,
        );

        let confirmButton = queryByText('Confirm');
        expect(confirmButton).not.toBeInTheDocument();

        const action = getByTestId('action');
        fireEvent.click(action);

        confirmButton = getByText('Confirm');
        expect(confirmButton).toBeInTheDocument();

        fireEvent.mouseLeave(action);

        vi.runAllTimers();

        // TODO: Debug why this isn't passing
        // await waitFor(() => {
        //   confirmButton = queryByText('Confirm');
        //   expect(confirmButton).not.toBeInTheDocument();
        // });
        expect(spy).not.toHaveBeenCalled();

        vi.useRealTimers();
      });

      it(`'Advanced' mode accepts a 'confirmationConfig' object`, () => {
        const mockConfig: ConfirmationConfig = {
          title: 'test-title',
          body: 'test-body',
          cancelButtonText: 'test-cancel',
          confirmButtonText: 'test-confirm',
        };
        const { getByText, getByTestId } = render(
          <Action
            {...defaultContextProps}
            action={{
              label: mockLabel,
              confirmationMode: 'Advanced',
              confirmationConfig: mockConfig,
              onActionSelected: noop,
            }}
          />,
        );

        const action = getByTestId('action');
        fireEvent.click(action);

        expect(getByText(mockConfig.title as string)).toBeInTheDocument();
        expect(getByText(mockConfig.body as string)).toBeInTheDocument();
        expect(
          getByText(mockConfig.cancelButtonText as string),
        ).toBeInTheDocument();
        expect(
          getByText(mockConfig.confirmButtonText as string),
        ).toBeInTheDocument();
      });

      it(`'Simple' raises onConfirmOpen if set`, () => {
        const confirmToggleSpy = vi.fn();

        const { getByTestId } = render(
          <Action
            {...defaultContextProps}
            action={{
              label: mockLabel,
              confirmationMode: 'Simple',
              confirmationConfig: { onConfirmOpen: confirmToggleSpy },
              onActionSelected: noop,
            }}
          />,
        );

        const action = getByTestId('action');

        // toggle confirmation on
        fireEvent.click(action);

        expect(confirmToggleSpy).toHaveBeenCalledTimes(1);

        // toggle confirmation off
        fireEvent.click(action);

        expect(confirmToggleSpy).toHaveBeenCalledTimes(2);
        expect(confirmToggleSpy).toHaveBeenNthCalledWith(1, true);
        expect(confirmToggleSpy).toHaveBeenNthCalledWith(2, false);
      });

      it(`'Simple' raises onConfirmOpen when 'mouseleave' if set`, () => {
        const confirmToggleSpy = vi.fn();

        vi.useFakeTimers();

        const { getByTestId } = render(
          <Action
            {...defaultContextProps}
            action={{
              label: mockLabel,
              confirmationMode: 'Simple',
              confirmationConfig: { onConfirmOpen: confirmToggleSpy },
              onActionSelected: noop,
            }}
          />,
        );

        const action = getByTestId('action');

        act(() => {
          fireEvent.click(action);
        });

        act(() => {
          fireEvent.mouseLeave(action);
        });

        act(() => {
          vi.runAllTimers();
        });

        expect(confirmToggleSpy).toHaveBeenCalledTimes(2);
        expect(confirmToggleSpy).toHaveBeenNthCalledWith(1, true);
        expect(confirmToggleSpy).toHaveBeenNthCalledWith(2, false);

        vi.useRealTimers();
      });

      it(`'Advanced' raises onConfirmOpen when 'Cancel' is clicked, if set`, () => {
        const confirmToggleSpy = vi.fn();

        const { getByTestId, getByText } = render(
          <Action
            {...defaultContextProps}
            action={{
              label: mockLabel,
              confirmationMode: 'Advanced',
              confirmationConfig: { onConfirmOpen: confirmToggleSpy },
              onActionSelected: noop,
            }}
          />,
        );

        const action = getByTestId('action');

        // toggle confirmation on
        fireEvent.click(action);

        const cancelButton = getByText('Cancel');
        fireEvent.click(cancelButton);

        expect(confirmToggleSpy).toHaveBeenCalledTimes(2);
        expect(confirmToggleSpy).toHaveBeenNthCalledWith(1, true);
        expect(confirmToggleSpy).toHaveBeenNthCalledWith(2, false);
      });

      it(`'Advanced' raises onConfirmOpen when 'Confirm' is clicked, if set`, () => {
        const confirmToggleSpy = vi.fn();

        const { getByTestId, getByText } = render(
          <Action
            {...defaultContextProps}
            action={{
              label: mockLabel,
              confirmationMode: 'Advanced',
              confirmationConfig: { onConfirmOpen: confirmToggleSpy },
              onActionSelected: noop,
            }}
          />,
        );

        const action = getByTestId('action');

        // toggle confirmation on
        fireEvent.click(action);

        const confirmButton = getByText('Confirm');
        fireEvent.click(confirmButton);

        expect(confirmToggleSpy).toHaveBeenCalledTimes(2);
        expect(confirmToggleSpy).toHaveBeenNthCalledWith(1, true);
        expect(confirmToggleSpy).toHaveBeenNthCalledWith(2, false);
      });
    });
  });
});

describe('isNavigationAction', () => {
  test('returns true for a NavigationActionData object', () => {
    const action = { label: 'Navigation Action', path: '/home' };
    expect(isNavigationAction(action)).toBe(true);
  });

  test('returns false for a ContextActionData object', () => {
    const action = { label: 'Context Action', onActionSelected: vi.fn() };
    expect(isNavigationAction(action)).toBe(false);
  });

  test('returns false in case `path` is set to undefined', () => {
    const action = {
      label: 'Context Action',
      path: undefined,
      onActionSelected: noop,
    };
    expect(isNavigationAction(action)).toBe(false);
  });
});

describe('isContextAction', () => {
  test('returns true for a ContextActionData object', () => {
    const action = { label: 'Context Action', onActionSelected: vi.fn() };
    expect(isContextAction(action)).toBe(true);
  });

  test('returns false for a NavigationActionData object', () => {
    const action = { label: 'Navigation Action', path: '/home' };
    expect(isContextAction(action)).toBe(false);
  });

  test('returns false in case `onActionSelected` is set to undefined', () => {
    const action = {
      label: 'Navigation Action',
      path: '/home',
      onActionSelected: undefined,
    };
    expect(isContextAction(action)).toBe(false);
  });
});
