/* eslint-disable jest/no-commented-out-tests */
/* eslint-disable react/jsx-boolean-value */
import * as React from 'react';
import { render, fireEvent, waitFor } from '@testing-library/react';

import type { ModalExtension } from '@atlassian/clientside-extensions';
import type { FunctionComponent } from 'react';

import { ModalHandler, ModalWithActionHandler } from './ModalHandler';
import type { ModalHandlerProps, ModalWithActionHandlerProps } from './ModalHandler';

const onMountCallbackSpy = jest.fn();
const onUnmountCallbackSpy = jest.fn();

const testRenderFn: ModalExtension.ModalRenderExtension = (api) => {
    api.onMount(onMountCallbackSpy);
    api.onUnmount(onUnmountCallbackSpy);
};

// When an attribute of the extension changes, the useExtensions hook returns a completly new descriptor object
// This render function is used to replicate that behaviour.
const changedTestRenderFn: ModalExtension.ModalRenderExtension = (api) => {
    api.onMount(onMountCallbackSpy);
    api.onUnmount(onUnmountCallbackSpy);
};

const emptyRenderer = (): null => null;
const emptyCallback = (): null => null;

const TestComponent: FunctionComponent<Partial<ModalHandlerProps>> = ({
    render: renderProp = testRenderFn,
    isOpen = true,
    onClose = emptyCallback,
}) => {
    return <ModalHandler render={renderProp} isOpen={isOpen} onClose={onClose} />;
};

const TestComponentWithActionHandler: FunctionComponent<Partial<ModalWithActionHandlerProps>> = ({
    render: renderProp = emptyRenderer,
    children,
}) => {
    return <ModalWithActionHandler render={renderProp}>{children}</ModalWithActionHandler>;
};

TestComponentWithActionHandler.defaultProps = {
    render: testRenderFn,
} as Partial<ModalHandlerProps>;

describe('ModalHandler', () => {
    beforeEach(() => {
        onMountCallbackSpy.mockReset();
        onUnmountCallbackSpy.mockReset();
    });

    describe('ModalHandler', () => {
        it('should receive an isOpen prop to set the modal as open', async () => {
            const { rerender, queryByRole } = render(<TestComponent isOpen={false} />);

            expect(queryByRole('dialog')).toBeFalsy();

            rerender(<TestComponent isOpen={true} />);
            await waitFor(() => {
                expect(queryByRole('dialog')).toBeTruthy();
            });
        });

        it('should provide a closeModal API method to extension that executes a provided onClose prop to close the modal', () => {
            const onCloseCallbackSpy = jest.fn();

            expect(onCloseCallbackSpy).toHaveBeenCalledTimes(0);

            render(<TestComponent render={(api) => api.closeModal()} onClose={onCloseCallbackSpy} />);

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

        it('should provide an onClose API method for extension to prevent the modal from closing unexpectedly (e.g: ESC key)', () => {
            const onCloseCallbackSpy = jest.fn();
            const fireEscKeyPress = () =>
                fireEvent.keyDown(document.body, {
                    key: 'Escape',
                    keyCode: 27,
                    charCode: 27,
                    which: 27,
                });

            expect(onCloseCallbackSpy).toHaveBeenCalledTimes(0);

            const { rerender, container } = render(
                <TestComponent render={(api) => api.onClose(() => false)} onClose={onCloseCallbackSpy} />,
            );

            fireEvent.click(container);

            expect(onCloseCallbackSpy).toHaveBeenCalledTimes(0);

            rerender(<TestComponent render={(api) => api.onClose(() => true)} onClose={onCloseCallbackSpy} />);

            fireEscKeyPress();

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

        it('should provide a setTitle API method that sets a title in the modal header', async () => {
            const TEST_TITLE = 'A modal with a test title';

            const { findByRole } = render(<TestComponent render={(api) => api.setTitle(TEST_TITLE)} />);

            expect((await findByRole('heading')).textContent).toBe(TEST_TITLE);
        });

        it('should provide a setActions API to specify the actions a user can perform when the modal is opened', async () => {
            const primaryActionSpy = jest.fn();
            const secondaryActionSpy = jest.fn();

            const actions: ModalExtension.ModalAction[] = [
                {
                    text: 'Primary action',
                    onClick: primaryActionSpy,
                    testId: 'primaryAction',
                },
                {
                    text: 'Secondary action',
                    onClick: secondaryActionSpy,
                    testId: 'secondaryAction',
                },
            ];

            const { findByTestId } = render(<TestComponent render={(api) => api.setActions(actions)} />);

            const primaryBtn = await findByTestId('primaryAction');
            const secondaryBtn = await findByTestId('secondaryAction');

            expect(primaryActionSpy).toHaveBeenCalledTimes(0);
            expect(secondaryActionSpy).toHaveBeenCalledTimes(0);

            fireEvent.click(primaryBtn);

            expect(primaryActionSpy).toHaveBeenCalledTimes(1);
            expect(secondaryActionSpy).toHaveBeenCalledTimes(0);

            fireEvent.click(secondaryBtn);

            expect(primaryActionSpy).toHaveBeenCalledTimes(1);
            expect(secondaryActionSpy).toHaveBeenCalledTimes(1);
        });

        it('should allow modal actions to be set as disabled', async () => {
            const primaryActionSpy = jest.fn();

            const primaryAction: ModalExtension.ModalAction = {
                text: 'Primary action',
                onClick: primaryActionSpy,
                testId: 'primaryAction',
            };
            const { findByTestId, rerender } = render(<TestComponent render={(api) => api.setActions([primaryAction])} />);

            expect(primaryActionSpy).toHaveBeenCalledTimes(0);

            fireEvent.click(await findByTestId('primaryAction'));

            expect(primaryActionSpy).toHaveBeenCalledTimes(1);

            primaryAction.isDisabled = true;
            rerender(<TestComponent render={(api) => api.setActions([primaryAction])} />);
            fireEvent.click(await findByTestId('primaryAction'));

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

        // TODO: find a way with AK team to test that a button is in loading state.
        // it.only('should allow modal actions to be set as loading', () => {});

        // TODO: find a way with AK team to test the size applied to a modal without querying for the actual size of the element.
        // it('should provide a setWidth API method to modify the width of the modal', () => {});

        // TODO: find a way with AK team to test the danger/warning appearance of a modal without involving CSS.
        // it('should provide a setAppearance API method that changes the appearance of the modal', () => {});

        it('should provide an onMount API method to render custom HTML in a container', async () => {
            const CUSTOM_CONTENT = 'Custom content';
            onMountCallbackSpy.mockImplementationOnce((container: HTMLElement) => {
                container.innerHTML = CUSTOM_CONTENT;
            });

            const { findByText } = render(<TestComponent />);

            expect(onMountCallbackSpy).toHaveBeenCalledTimes(1);
            expect(await findByText(CUSTOM_CONTENT)).toBeTruthy();
        });

        it('should only rerender the extension content if something in the extension descriptor changed', async () => {
            const CUSTOM_CONTENT = 'Custom content';
            const DIFFERENT_CONTENT = 'Different content';
            onMountCallbackSpy
                .mockImplementationOnce((container: HTMLElement) => {
                    container.innerHTML = CUSTOM_CONTENT;
                })
                .mockImplementationOnce((container: HTMLElement) => {
                    container.innerHTML = DIFFERENT_CONTENT;
                });

            const { findByText, rerender } = render(<TestComponent />);

            expect(onMountCallbackSpy).toHaveBeenCalledTimes(1);
            expect(await findByText(CUSTOM_CONTENT)).toBeTruthy();

            rerender(<TestComponent />);

            // expect nothing to change
            expect(onMountCallbackSpy).toHaveBeenCalledTimes(1);
            expect(await findByText(CUSTOM_CONTENT)).toBeTruthy();

            rerender(<TestComponent render={changedTestRenderFn} />);

            // expect the new value to be rendered
            expect(onMountCallbackSpy).toHaveBeenCalledTimes(2);
            expect(await findByText(DIFFERENT_CONTENT)).toBeTruthy();
        });

        it('should provide an onUnmount API method to be called with the container element when destroying the extension', () => {
            onMountCallbackSpy.mockImplementation((container: HTMLElement) => {
                expect(container).toHaveProperty('innerHTML');
            });

            const { unmount } = render(<TestComponent />);

            expect(onUnmountCallbackSpy).toHaveBeenCalledTimes(0);

            unmount();

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

        it('should only call cleanUp callback if the extension changes or the container is destroyed', () => {
            const { rerender, unmount } = render(<TestComponent />);

            expect(onUnmountCallbackSpy).toHaveBeenCalledTimes(0);

            rerender(<TestComponent />);

            // the container rerenders with the same extension render prop, so no need to call cleanup
            expect(onUnmountCallbackSpy).toHaveBeenCalledTimes(0);

            rerender(<TestComponent render={changedTestRenderFn} />);

            // the extension render prop changed, so cleanup is called
            expect(onUnmountCallbackSpy).toHaveBeenCalledTimes(1);

            unmount();

            // the container is destroyed, so cleanup is called
            expect(onUnmountCallbackSpy).toHaveBeenCalledTimes(2);
        });
    });

    describe('ModalWithActionHandler', () => {
        const modalButtonText = 'My Modal';

        it('should render a button to handle the modal', () => {
            const { queryByText } = render(<TestComponentWithActionHandler>{modalButtonText}</TestComponentWithActionHandler>);

            expect(queryByText(modalButtonText)).toBeTruthy();
        });

        it('should receive an isOpen prop to set the modal as open', async () => {
            const { queryByRole, queryByText } = render(<TestComponentWithActionHandler>{modalButtonText}</TestComponentWithActionHandler>);

            expect(queryByRole('dialog')).toBeFalsy();

            queryByText(modalButtonText)?.click();
            await waitFor(() => {
                expect(queryByRole('dialog')).toBeTruthy();
            });
        });
    });
});
