import * as React from 'react';
import { render } from '@testing-library/react';

import type { PanelExtension } from '@atlassian/clientside-extensions';
import type { PanelHandlerProps } from './PanelHandler';
import { PanelHandler } from './PanelHandler';

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

// onAction method of an Extension
const testRenderFn: PanelExtension.PanelRenderExtension = (api) => {
    api.onMount(onMountCallbackSpy);
    api.onUnmount(onUnmountCallbackSpy);
};

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

// eslint-disable-next-line @typescript-eslint/no-shadow
const TestComponent = ({ render, RootType }: PanelHandlerProps) => {
    return <PanelHandler render={render} RootType={RootType} />;
};

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

    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 render={testRenderFn} />);

        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 render={testRenderFn} />);

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

        // expect nothing to change
        rerender(<TestComponent render={testRenderFn} />);

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

        // expect the new value to be rendered when render function did changed
        rerender(<TestComponent render={changedTestRenderFn} />);

        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 render={changedTestRenderFn} />);

        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 render={testRenderFn} />);

        expect(onUnmountCallbackSpy).toHaveBeenCalledTimes(0);

        // the container rerenders with the same extension descriptor, so no need to call cleanup
        rerender(<TestComponent render={testRenderFn} />);

        expect(onUnmountCallbackSpy).toHaveBeenCalledTimes(0);

        // the extension descriptor changed, so cleanup is called
        rerender(<TestComponent render={changedTestRenderFn} />);

        expect(onUnmountCallbackSpy).toHaveBeenCalledTimes(1);

        // the container is destroyed, so cleanup is called
        unmount();

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

    it('should allow to specify a span element as a container', () => {
        const { asFragment } = render(<TestComponent render={testRenderFn} RootType="span" />);

        expect(asFragment().firstElementChild).toMatchInlineSnapshot(`<span />`);
    });
});
