import React, { useState } from 'react';
import { act, render, screen, waitFor } from '@testing-library/react';
import userEvent from '@testing-library/user-event';
import { axe } from 'jest-axe';

import { getLgIds as getLgModalIds } from '@leafygreen-ui/modal';

import { getLgIds } from '..';
import { ConfirmationModal, Variant } from '..';

const lgIds = getLgIds();

const WrappedModal = ({
  open: initialOpen,
  ...props
}: Partial<React.ComponentProps<typeof ConfirmationModal>>) => {
  const [open, setOpen] = useState(initialOpen);

  return (
    <ConfirmationModal
      title="Title text"
      buttonText="Confirm"
      open={open}
      onConfirm={() => setOpen(false)}
      onCancel={() => setOpen(false)}
      {...props}
    >
      {props.children ?? 'Content text'}
    </ConfirmationModal>
  );
};

function renderModal(
  props: Partial<React.ComponentProps<typeof ConfirmationModal>> = {},
) {
  return render(<WrappedModal {...props} />);
}

describe('packages/confirmation-modal', () => {
  // Mock dialog methods for JSDOM environment
  beforeAll(() => {
    HTMLDialogElement.prototype.show = jest.fn(function mock(
      this: HTMLDialogElement,
    ) {
      this.open = true;
    });

    HTMLDialogElement.prototype.showModal = jest.fn(function mock(
      this: HTMLDialogElement,
    ) {
      this.open = true;
    });

    HTMLDialogElement.prototype.close = jest.fn(function mock(
      this: HTMLDialogElement,
    ) {
      this.open = false;
    });
  });

  describe('a11y', () => {
    test('does not have basic accessibility issues', async () => {
      const { container, getByText } = renderModal({ open: true });
      const results = await axe(container);
      expect(results).toHaveNoViolations();

      let newResults = null as any;
      act(() => void userEvent.click(getByText('Confirm')));
      await act(async () => {
        newResults = await axe(container);
      });
      expect(newResults).toHaveNoViolations();
    });
  });

  test('does not render if closed', () => {
    const { queryByText } = renderModal();
    expect(queryByText('Content text')).toBeNull();
  });

  test('renders if open', () => {
    const { getByText } = renderModal({ open: true });
    expect(getByText('Title text')).toBeVisible();
    expect(getByText('Content text')).toBeVisible();
    expect(getByText('Confirm')).toBeVisible();
    expect(getByText('Cancel')).toBeVisible();
  });

  describe('initial focus', () => {
    test(`focuses confirm button when variant is ${Variant.Default}`, async () => {
      renderModal({
        open: true,
        variant: Variant.Default,
      });
      const confirmButton = screen.getByRole('button', { name: 'Confirm' });

      await waitFor(() => {
        expect(confirmButton).toHaveFocus();
      });
    });

    test(`focuses cancel button when variant is ${Variant.Danger}`, async () => {
      renderModal({
        open: true,
        variant: Variant.Danger,
      });
      const cancelButton = screen.getByRole('button', { name: 'Cancel' });

      await waitFor(() => {
        expect(cancelButton).toHaveFocus();
      });
    });

    test('focuses text input when requiredInputText is provided', async () => {
      renderModal({
        open: true,
        requiredInputText: 'Confirm',
      });
      const textInput = screen.getByLabelText(
        'Type "Confirm" to confirm your action',
        { selector: 'input' },
      );

      await waitFor(() => {
        expect(textInput).toHaveFocus();
      });
    });

    test('uses custom initialFocus prop when provided', async () => {
      const customFocusRef = React.createRef<HTMLButtonElement>();

      renderModal({
        open: true,
        initialFocus: customFocusRef,
        children: (
          <>
            Content text
            <button ref={customFocusRef}>Custom action</button>
          </>
        ),
      });

      const customButton = screen.getByRole('button', {
        name: 'Custom action',
      });
      const confirmButton = screen.getByRole('button', { name: 'Confirm' });
      const cancelButton = screen.getByRole('button', { name: 'Cancel' });

      await waitFor(() => {
        expect(customButton).toHaveFocus();
        expect(confirmButton).not.toHaveFocus();
        expect(cancelButton).not.toHaveFocus();
      });
    });

    test('focuses cancel button when confirm button is disabled via confirmButtonProps', async () => {
      renderModal({
        open: true,
        variant: Variant.Default,
        confirmButtonProps: {
          disabled: true,
        },
      });
      const cancelButton = screen.getByRole('button', { name: 'Cancel' });
      const confirmButton = screen.getByRole('button', { name: 'Confirm' });

      await waitFor(() => {
        expect(cancelButton).toHaveFocus();
        expect(confirmButton).not.toHaveFocus();
      });
    });

    test('focuses cancel button when confirm button is disabled via submitDisabled', async () => {
      renderModal({
        open: true,
        variant: Variant.Default,
        submitDisabled: true,
      });
      const cancelButton = screen.getByRole('button', { name: 'Cancel' });
      const confirmButton = screen.getByRole('button', { name: 'Confirm' });

      await waitFor(() => {
        expect(cancelButton).toHaveFocus();
        expect(confirmButton).not.toHaveFocus();
      });
    });
  });

  describe('button text', () => {
    // TODO: remove - buttonText is deprecated
    test('renders from "buttonText"', () => {
      const { getByText } = renderModal({
        open: true,
        buttonText: 'custom button text',
      });

      expect(getByText('custom button text')).toBeVisible();
    });

    test('renders from "confirmButtonProps"', () => {
      const { getByText } = renderModal({
        open: true,
        buttonText: undefined,
        confirmButtonProps: {
          children: 'custom confirm',
        },
      });

      expect(getByText('custom confirm')).toBeVisible();
    });

    // TODO: remove - buttonText is deprecated
    test('overrides deprecated "buttonText" with "confirmButtonProps"', () => {
      const { getByText } = renderModal({
        open: true,
        buttonText: 'custom button text',
        confirmButtonProps: {
          children: 'custom confirm',
        },
      });

      expect(getByText('custom button text')).toBeVisible();
    });
  });

  describe('on confirm', () => {
    // TODO: remove test  - onConfirm is deprecated
    test('fires `onConfirm` on confirmation', () => {
      const confirmSpy = jest.fn();
      const cancelSpy = jest.fn();

      const { getByText } = renderModal({
        open: true,
        onConfirm: confirmSpy,
        onCancel: cancelSpy,
      });

      const button = getByText('Confirm');
      expect(button).toBeVisible();

      userEvent.click(button);
      expect(confirmSpy).toHaveBeenCalledTimes(1);
      expect(cancelSpy).not.toHaveBeenCalled();
    });

    test('fires `onClick` from "confirmButtonProps"', () => {
      const confirmSpy = jest.fn();

      const { getByText } = renderModal({
        open: true,
        onConfirm: undefined,
        confirmButtonProps: {
          onClick: confirmSpy,
        },
      });

      const button = getByText('Confirm');
      expect(button).toBeVisible();

      userEvent.click(button);
      expect(confirmSpy).toHaveBeenCalledTimes(1);
    });
  });

  describe('on cancel', () => {
    // TODO: remove test - OnCancel is deprecated
    test('fires `onCancel` on cancel', () => {
      const confirmSpy = jest.fn();
      const cancelSpy = jest.fn();

      const { getByText } = renderModal({
        open: true,
        onConfirm: confirmSpy,
        onCancel: cancelSpy,
      });

      const button = getByText('Cancel');
      expect(button).toBeVisible();

      userEvent.click(button);
      expect(confirmSpy).not.toHaveBeenCalled();
      expect(cancelSpy).toHaveBeenCalledTimes(1);
    });

    test('fires `onClick` from "cancelButtonProps"', () => {
      const confirmSpy = jest.fn();
      const cancelSpy = jest.fn();

      const { getByText } = renderModal({
        open: true,
        onCancel: undefined,
        cancelButtonProps: {
          onClick: cancelSpy,
        },
      });

      const button = getByText('Cancel');
      expect(button).toBeVisible();

      userEvent.click(button);
      expect(confirmSpy).not.toHaveBeenCalled();
      expect(cancelSpy).toHaveBeenCalledTimes(1);
    });

    test('closes modal when `handleClose` sets state to false', async () => {
      const confirmSpy = jest.fn();
      const handleCloseSpy = jest.fn((_e?: MouseEvent | KeyboardEvent) => {});

      const TestWrapper = () => {
        const [open, setOpen] = useState(true);

        const handleClose = (e?: MouseEvent | KeyboardEvent) => {
          handleCloseSpy(e);
          setOpen(false);
        };

        return (
          <ConfirmationModal
            title="Title text"
            buttonText="Confirm"
            open={open}
            onConfirm={confirmSpy}
            cancelButtonProps={{
              onClick: handleClose,
            }}
          >
            Content text
          </ConfirmationModal>
        );
      };

      const { getByText, getByRole } = render(<TestWrapper />);

      const button = getByText('Cancel');
      const modal = getByRole('dialog');
      expect(button).toBeVisible();
      expect(modal).toBeVisible();

      // Click the button - the event is not passed to the `onCancel` callback
      // but we verify the function is called
      userEvent.click(button);
      expect(confirmSpy).not.toHaveBeenCalled();
      expect(handleCloseSpy).toHaveBeenCalledTimes(1);
      expect(handleCloseSpy).toHaveBeenCalledWith(undefined);

      // Verify that the function signature accepts MouseEvent by calling it directly
      const mockMouseEvent = new MouseEvent('click', { bubbles: true });
      handleCloseSpy.mockClear();
      handleCloseSpy(mockMouseEvent);
      expect(handleCloseSpy).toHaveBeenCalledWith(mockMouseEvent);

      await waitFor(() => expect(modal).not.toBeVisible());
    });
  });

  describe('button id props', () => {
    test('propagates to the buttons', () => {
      const { getByText } = renderModal({
        open: true,
        confirmButtonProps: { id: 'my-confirm-btn', children: 'Confirm' },
        cancelButtonProps: { id: 'my-cancel-btn' },
      });

      const confirmButton = getByText('Confirm').closest('button');
      expect(confirmButton).toHaveAttribute('id', 'my-confirm-btn');

      const cancelButton = getByText('Cancel').closest('button');
      expect(cancelButton).toHaveAttribute('id', 'my-cancel-btn');
    });
  });

  describe('closes when', () => {
    test('escape key is pressed', async () => {
      const { getByRole } = renderModal({ open: true });
      const modal = getByRole('dialog');

      userEvent.keyboard('{Escape}');

      await waitFor(() => expect(modal).not.toBeVisible());
    });

    test('x icon is clicked', async () => {
      const { getByLabelText, getByRole } = renderModal({ open: true });
      const modal = getByRole('dialog');

      const x = getByLabelText('Close modal');
      userEvent.click(x);

      await waitFor(() => expect(modal).not.toBeVisible());
    });
  });

  describe('requiring text confirmation', () => {
    test('can only click confirmation button when text confirmation is entered', () => {
      const { getByText, getByLabelText } = renderModal({
        open: true,
        requiredInputText: 'Confirm',
      });

      const confirmationButton = getByText('Confirm').closest('button');
      expect(confirmationButton).toHaveAttribute('aria-disabled', 'true');

      const cancelButton = getByText('Cancel').closest('button');
      expect(cancelButton).not.toHaveAttribute('aria-disabled', 'true');

      const textInput = getByLabelText('Type "Confirm" to confirm your action');

      expect(textInput).toBeVisible();
      expect(textInput).toBe(document.activeElement);

      // Should still be disabled after partial entry
      userEvent.clear(textInput);
      userEvent.type(textInput, 'Confir');
      expect(confirmationButton).toHaveAttribute('aria-disabled', 'true');

      userEvent.clear(textInput);
      userEvent.type(textInput, 'Confirm');
      expect(confirmationButton).not.toHaveAttribute('aria-disabled', 'true');

      // Should be disabled again
      userEvent.clear(textInput);
      userEvent.type(textInput, 'Confirm?');
      expect(confirmationButton).toHaveAttribute('aria-disabled', 'true');

      // Case matters
      userEvent.clear(textInput);
      userEvent.type(textInput, 'confirm');
      expect(confirmationButton).toHaveAttribute('aria-disabled', 'true');
    });

    const requiredInputTextCases = [
      {
        describeCase:
          'when requiredInputText is provided, confirm button is reset',
        requiredInputText: 'Confirm',
        disabledAfterReopeningModal: true,
      },
      {
        describeCase:
          'when requiredInputText is undefined, confirm button is not reset',
        requiredInputText: undefined,
        disabledAfterReopeningModal: false,
      },
    ];

    const buttonClickCases = [
      { testCase: 'on cancel', testId: lgIds.cancel },
      { testCase: 'on confirm', testId: lgIds.confirm },
      { testCase: 'on modal close', testId: getLgModalIds(lgIds.root).close },
    ];

    describe.each(requiredInputTextCases)(
      '$describeCase',
      ({ requiredInputText, disabledAfterReopeningModal }) => {
        test.each(buttonClickCases)('$testCase', async ({ testId }) => {
          const { findByTestId, getByLabelText, getByRole, rerender } =
            renderModal({
              open: true,
              requiredInputText,
            });

          const modal = getByRole('dialog');
          const confirmationButton = await findByTestId(lgIds.confirm);
          const buttonToClick = await findByTestId(testId);
          expect(confirmationButton).toHaveAttribute(
            'aria-disabled',
            disabledAfterReopeningModal.toString(),
          );

          let textInput;

          if (requiredInputText) {
            textInput = getByLabelText(
              `Type "${requiredInputText}" to confirm your action`,
            );
            userEvent.clear(textInput);
            userEvent.type(textInput, requiredInputText);
            expect(confirmationButton).not.toHaveAttribute(
              'aria-disabled',
              'true',
            );
          }

          userEvent.click(buttonToClick);

          await waitFor(() => expect(modal).not.toBeVisible());

          rerender(
            <ConfirmationModal
              title="Title text"
              open={true}
              requiredInputText={requiredInputText}
            >
              Content text
            </ConfirmationModal>,
          );

          const rerenderedConfirmationButton = await findByTestId(
            lgIds.confirm,
          );

          if (requiredInputText) {
            textInput = getByLabelText(
              `Type "${requiredInputText}" to confirm your action`,
            );
            expect(textInput).toHaveValue('');
          }

          expect(rerenderedConfirmationButton).toHaveAttribute(
            'aria-disabled',
            disabledAfterReopeningModal.toString(),
          );
        });
      },
    );
  });

  describe('confirm is not disabled when', () => {
    test('By default', () => {
      const { getByText } = renderModal({
        open: true,
      });

      const confirmationButton = getByText('Confirm').closest('button');
      expect(confirmationButton).toHaveAttribute('aria-disabled', 'false');
    });

    // TODO: remove this test - submitDisabled is deprecated
    test('"submitDisabled" prop is false and "confirmButtonProps" has "disabled: true"', async () => {
      const { getByText, getByRole } = renderModal({
        open: true,
        submitDisabled: false,
        confirmButtonProps: {
          disabled: true,
        },
      });

      const confirmationButton = getByText('Confirm').closest('button');
      expect(confirmationButton).toHaveAttribute('aria-disabled', 'false');

      const modal = getByRole('dialog');
      const button = getByText('Confirm');
      expect(button).toBeVisible();

      // Modal doesn't close when button is clicked
      userEvent.click(button);
      await waitFor(() => expect(modal).not.toBeVisible());
    });

    test('"confirmButtonProps" has "disabled: false"', async () => {
      const { getByText, getByRole } = renderModal({
        open: true,
        confirmButtonProps: {
          disabled: false,
        },
      });

      const confirmationButton = getByText('Confirm').closest('button');
      expect(confirmationButton).toHaveAttribute('aria-disabled', 'false');

      const modal = getByRole('dialog');
      const button = getByText('Confirm');
      expect(button).toBeVisible();

      // Modal doesn't close when button is clicked
      userEvent.click(button);
      await waitFor(() => expect(modal).not.toBeVisible());
    });
  });

  describe('confirm is disabled when', () => {
    test('"confirmButtonProps" includes "disabled"', () => {
      const { getByText } = renderModal({
        open: true,
        confirmButtonProps: {
          disabled: true,
        },
      });

      const confirmationButton = getByText('Confirm').closest('button');
      expect(confirmationButton).toHaveAttribute('aria-disabled', 'true');

      const button = getByText('Confirm');
      expect(button).toBeVisible();

      // Modal doesn't close when button is clicked
      userEvent.click(button);
      expect(button).toBeVisible();
    });

    // TODO: remove this test - submitDisabled is deprecated
    test('"submitDisabled" prop is set', () => {
      const { getByText } = renderModal({
        open: true,
        submitDisabled: true,
      });

      const confirmationButton = getByText('Confirm').closest('button');
      expect(confirmationButton).toHaveAttribute('aria-disabled', 'true');

      const button = getByText('Confirm');
      expect(button).toBeVisible();

      // Modal doesn't close when button is clicked
      userEvent.click(button);
      expect(button).toBeVisible();
    });

    // TODO: remove this test - submitDisabled is deprecated
    test('"submitDisabled" prop is true and "confirmButtonProps" has "disabled: false"', () => {
      const { getByText } = renderModal({
        open: true,
        submitDisabled: true,
        confirmButtonProps: {
          disabled: false,
        },
      });

      const confirmationButton = getByText('Confirm').closest('button');
      expect(confirmationButton).toHaveAttribute('aria-disabled', 'true');

      const button = getByText('Confirm');
      expect(button).toBeVisible();

      // Modal doesn't close when button is clicked
      userEvent.click(button);
      expect(button).toBeVisible();
    });

    test('"confirmButtonProps" has "disabled: true" and the "requiredInputText" prop is also set', () => {
      const { getByText, getByLabelText } = renderModal({
        open: true,
        confirmButtonProps: {
          disabled: true,
        },
        requiredInputText: 'Confirm',
      });

      const confirmationButton = getByText('Confirm').closest('button');
      expect(confirmationButton).toHaveAttribute('aria-disabled', 'true');

      const textInput = getByLabelText('Type "Confirm" to confirm your action');
      expect(textInput).toBeVisible();

      userEvent.clear(textInput);
      userEvent.type(textInput, 'Confirm');
      expect(confirmationButton).toHaveAttribute('aria-disabled', 'true');
    });

    // TODO: remove this test - submitDisabled is deprecated
    test('"submitDisabled" prop is set and the "requiredInputText" prop is also set', () => {
      const { getByText, getByLabelText } = renderModal({
        open: true,
        submitDisabled: true,
        requiredInputText: 'Confirm',
      });

      const confirmationButton = getByText('Confirm').closest('button');
      expect(confirmationButton).toHaveAttribute('aria-disabled', 'true');

      const textInput = getByLabelText('Type "Confirm" to confirm your action');
      expect(textInput).toBeVisible();

      userEvent.clear(textInput);
      userEvent.type(textInput, 'Confirm');
      expect(confirmationButton).toHaveAttribute('aria-disabled', 'true');
    });
  });

  describe('testid attribute', () => {
    it('propagates to the dom element', () => {
      const { getByTestId } = renderModal({
        open: true,
        'data-testid': 'my-modal',
      });

      const modal = getByTestId('my-modal');
      expect(modal).toBeInTheDocument();
    });

    it('propagates to the buttons', () => {
      const { getByTestId } = renderModal({
        open: true,
        confirmButtonProps: { 'data-testid': 'my-confirm-btn' },
        cancelButtonProps: { 'data-testid': 'my-cancel-btn' },
      });

      const confirmButton = getByTestId('my-confirm-btn');
      expect(confirmButton).toBeInTheDocument();

      const cancelButton = getByTestId('my-cancel-btn');
      expect(cancelButton).toBeInTheDocument();
    });
  });

  // eslint-disable-next-line jest/no-disabled-tests
  test.skip('types behave as expected', () => {
    <>
      <ConfirmationModal
        title="Title text"
        buttonText="Confirm"
        onConfirm={() => {}}
        onCancel={() => {}}
        open={true}
        submitDisabled={false}
      >
        Hey
      </ConfirmationModal>

      <ConfirmationModal
        title="Title text"
        confirmButtonProps={{
          children: 'confirm',
          onClick: () => {},
          disabled: true,
        }}
      >
        Hey
      </ConfirmationModal>

      <ConfirmationModal
        title="Title text"
        confirmButtonProps={{
          // @ts-expect-error - variant does exist in confirmButtonProps
          variant: 'primary',
        }}
      >
        Hey
      </ConfirmationModal>

      <ConfirmationModal
        title="Title text"
        cancelButtonProps={{
          variant: 'primary',
          disabled: true,
          isLoading: true,
        }}
      >
        Hey
      </ConfirmationModal>

      <ConfirmationModal
        title="Title text"
        cancelButtonProps={{
          // @ts-expect-error - children does exist in cancelButtonProps
          children: 'new cancel text',
          variant: 'primary',
          disabled: true,
          isLoading: true,
        }}
      >
        Hey
      </ConfirmationModal>

      <ConfirmationModal
        title="Title text"
        cancelButtonProps={{
          variant: 'primary',
          disabled: true,
          id: 'my-cancel-btn',
        }}
      >
        Hey
      </ConfirmationModal>

      <ConfirmationModal
        title="Title text"
        // @ts-expect-error - href is not valid on a button
        href="https://mongodb.design"
        cancelButtonProps={{
          variant: 'primary',
          disabled: true,
        }}
      >
        Hey
      </ConfirmationModal>
    </>;
  });
});
