import { renderHook, act, waitFor } from '@testing-library/react';
import { useAuth } from './useAuth';
import '@testing-library/jest-dom';
import { AuthenticationConfiguration } from '../types';
import * as authUtils from '../utils/authUtils';

jest.mock('../utils/authUtils');

const mockGetCookieValue = authUtils.getCookieValue as jest.MockedFunction<typeof authUtils.getCookieValue>;
const mockRefreshAccessToken = authUtils.refreshAccessToken as jest.MockedFunction<typeof authUtils.refreshAccessToken>;

describe('useAuth', () => {
    const authConfig: AuthenticationConfiguration = {
        authUrl: 'https://auth.example.com',
        clientId: 'example-client-id',
        redirectUrl: 'https://example.com/callback',
        scope: 'offline',
    };

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

    it('should initialize with token and authentication state', async () => {
        mockGetCookieValue.mockReturnValue('initialAuthToken');

        const { result } = renderHook(() => useAuth(authConfig));

        await waitFor(() => {
            expect(result.current.authToken).toBe('initialAuthToken');
            expect(result.current.isAuthenticated).toBe(true);
            expect(result.current.alwaysUseResourceRequestProxy).toBe(false);
        });
    });
    it('should do nothing if missing crucial props', async () => {
        const authConfigMissingProps = {
            clientId: 'example-client-id',
            // Missing redirectUrl, authUrl, and scope
        };
        // @ts-ignore
        const { result } = renderHook(() => useAuth(authConfigMissingProps));

        result.current.login();

        await waitFor(() => {
            expect(result.current.authToken).toBe(undefined);
            expect(result.current.isAuthenticated).toBe(false);
        });
    });

    it('should log error and return when popup fails to open', async () => {
        const consoleSpy = jest.spyOn(console, 'error').mockImplementation(() => {});
        jest.spyOn(window, 'open').mockImplementation(() => null);

        const { result } = renderHook(() => useAuth(authConfig));

        result.current.login();

        await waitFor(() => {
            expect(consoleSpy).toHaveBeenCalledWith('Popup failed to open');
        });
        consoleSpy.mockRestore();
    });

    it('should log error when popup closes before authentication', async () => {
        jest.useFakeTimers();

        const popupMock = {
            close: jest.fn(),
        } as unknown as Window;

        Object.defineProperty(popupMock, 'closed', {
            get: jest.fn(() => false),
            set: jest.fn(),
        });

        const consoleSpy = jest.spyOn(console, 'error').mockImplementation(() => {});
        jest.spyOn(window, 'open').mockImplementation(() => popupMock);
        mockGetCookieValue.mockReturnValueOnce(null);

        const { result } = renderHook(() => useAuth(authConfig));

        result.current.login();
        (Object.getOwnPropertyDescriptor(popupMock, 'closed')!.get as jest.Mock).mockReturnValue(true);
        jest.advanceTimersByTime(1000);

        await waitFor(() => {
            expect(consoleSpy).toHaveBeenCalledWith('Popup closed before authentication');
        });

        consoleSpy.mockRestore();
    });

    it('should login successfully and update state', async () => {
        jest.useFakeTimers();

        const popupMock = {
            closed: false,
            close: jest.fn(),
        } as unknown as Window;

        jest.spyOn(window, 'open').mockImplementation(() => popupMock);
        mockGetCookieValue.mockReturnValueOnce(null).mockReturnValueOnce('newAuthToken').mockReturnValueOnce('newRefreshToken');

        const { result } = renderHook(() => useAuth(authConfig));

        result.current.login();

        expect(window.open).toHaveBeenCalledWith(
            `${authConfig.authUrl}?client_id=${authConfig.clientId}&scope=offline&redirect_uri=${encodeURIComponent(authConfig.redirectUrl)}&response_type=code&state=state`,
            'Login',
            'width=600,height=600',
        );

        act(() => {
            mockGetCookieValue.mockReturnValue('newAuthToken');
            jest.advanceTimersByTime(1000);
            (popupMock as any).closed = true;
        });

        await waitFor(() => {
            expect(result.current.authToken).toBe('newAuthToken');
            expect(result.current.isAuthenticated).toBe(true);
        });

        expect(popupMock.close).toHaveBeenCalled();
    });

    it('splits the scope from dxp-console and sends in expected format (space delimited)', async () => {
        jest.useFakeTimers();

        const popupMock = {
            closed: false,
            close: jest.fn(),
        } as unknown as Window;

        jest.spyOn(window, 'open').mockImplementation(() => popupMock);
        mockGetCookieValue.mockReturnValueOnce(null).mockReturnValueOnce('newAuthToken').mockReturnValueOnce('newRefreshToken');

        const { result } = renderHook(() => useAuth({ ...authConfig, scope: 'offline;asset:read' }));

        result.current.login();

        expect(window.open).toHaveBeenCalledWith(
            `${authConfig.authUrl}?client_id=${authConfig.clientId}&scope=offline asset:read&redirect_uri=${encodeURIComponent(authConfig.redirectUrl)}&response_type=code&state=state`,
            'Login',
            'width=600,height=600',
        );
    });

    it('Works with an empty scope', async () => {
        jest.useFakeTimers();

        const popupMock = {
            closed: false,
            close: jest.fn(),
        } as unknown as Window;

        jest.spyOn(window, 'open').mockImplementation(() => popupMock);
        mockGetCookieValue.mockReturnValueOnce(null).mockReturnValueOnce('newAuthToken').mockReturnValueOnce('newRefreshToken');

        const { result } = renderHook(() => useAuth({ ...authConfig, scope: '' }));

        result.current.login();

        expect(window.open).toHaveBeenCalledWith(
            `${authConfig.authUrl}?client_id=${authConfig.clientId}&scope=&redirect_uri=${encodeURIComponent(authConfig.redirectUrl)}&response_type=code&state=state`,
            'Login',
            'width=600,height=600',
        );
    });

    it('should refresh access token and update state', async () => {
        mockGetCookieValue.mockReturnValue('initialRefreshToken');
        mockRefreshAccessToken.mockResolvedValue('newAuthToken');

        const { result } = renderHook(() => useAuth(authConfig));

        await waitFor(() => {
            expect(mockRefreshAccessToken).toHaveBeenCalledWith(authConfig);
            expect(result.current.authToken).toBe('newAuthToken');
            expect(result.current.isAuthenticated).toBe(true);
        });
    });

    it('should handle token refresh failure', async () => {
        mockGetCookieValue.mockReturnValue('initialRefreshToken');
        mockRefreshAccessToken.mockRejectedValue(new Error('Failed to refresh token'));

        const { result } = renderHook(() => useAuth(authConfig));

        await waitFor(() => {
            expect(mockRefreshAccessToken).toHaveBeenCalledWith(authConfig);
            expect(result.current.isAuthenticated).toBe(false);
        });
    });

    it('should update state when the page gains focus', async () => {
        const { result } = renderHook(() => useAuth(authConfig));

        mockGetCookieValue.mockReturnValueOnce('focusedAuthToken');

        act(() => {
            window.dispatchEvent(new Event('focus'));
        });

        await waitFor(() => {
            expect(result.current.authToken).toBe('focusedAuthToken');
            expect(result.current.isAuthenticated).toBe(true);
        });
    });

    it('should update state when the document becomes visible', async () => {
        mockGetCookieValue.mockReturnValueOnce(null);
        const { result } = renderHook(() => useAuth(authConfig));

        mockGetCookieValue.mockReturnValue('visibleAuthToken');

        Object.defineProperty(document, 'hidden', { configurable: true, value: false });

        act(() => {
            document.dispatchEvent(new Event('visibilitychange'));
        });

        await waitFor(() => {
            expect(result.current.authToken).toBe('visibleAuthToken');
            expect(result.current.isAuthenticated).toBe(true);
        });
    });

    it('should clean up event listeners on unmount', async () => {
        const removeEventListenerSpy = jest.spyOn(window, 'removeEventListener');

        const { unmount } = renderHook(() => useAuth(authConfig));

        unmount();

        expect(removeEventListenerSpy).toHaveBeenCalledWith('focus', expect.any(Function));
    });

    it('should log error when authConfig is misconfigured', async () => {
        const consoleSpy = jest.spyOn(console, 'error').mockImplementation(() => {});

        const invalidConfig = { clientId: 'example-client-id', scope: 'offline' };

        // @ts-ignore
        const { result } = renderHook(() => useAuth(invalidConfig));

        result.current.login();

        await waitFor(() => {
            expect(consoleSpy).toHaveBeenCalledWith('Auth config is misconfigured');
        });

        consoleSpy.mockRestore();
    });

    it('should handle missing refreshToken when logging in', async () => {
        jest.useFakeTimers();

        const popupMock = {
            closed: false,
            close: jest.fn(),
        } as unknown as Window;

        jest.spyOn(window, 'open').mockImplementation(() => popupMock);
        mockGetCookieValue.mockReturnValueOnce(null).mockReturnValueOnce('newAuthToken').mockReturnValueOnce(null);

        const { result } = renderHook(() => useAuth(authConfig));

        result.current.login();

        act(() => {
            mockGetCookieValue.mockReturnValue('newAuthToken');
            jest.advanceTimersByTime(1000);
            (popupMock as any).closed = true;
        });

        await waitFor(() => {
            expect(result.current.authToken).toBe(null);
            expect(result.current.isAuthenticated).toBe(false);
        });

        expect(popupMock.close).toHaveBeenCalled();
    });

    it('should throw an error when getCookieValue throws unexpectedly', () => {
        const getCookieValueSpy = jest.spyOn(authUtils, 'getCookieValue').mockImplementation(() => {
            throw new Error('Unexpected error');
        });
        const consoleSpy = jest.spyOn(console, 'error').mockImplementation(() => {});

        expect(() => renderHook(() => useAuth(authConfig))).toThrow('Unexpected error');

        consoleSpy.mockRestore();
        getCookieValueSpy.mockRestore();
    });

    describe('alwaysUseResourceRequestProxy', () => {
        const authConfigAlwaysUseResourceRequestProxy = { ...authConfig, alwaysUseResourceRequestProxy: true };
        it('should initialize with alwaysUseResourceRequestProxy true', async () => {
            const { result } = renderHook(() => useAuth(authConfigAlwaysUseResourceRequestProxy));

            await waitFor(() => {
                expect(result.current.authToken).toBe(null);
                expect(result.current.isAuthenticated).toBe(true);
                expect(result.current.alwaysUseResourceRequestProxy).toBe(true);
            });

            // Check that refreshAccessToken is not called when authConfig changes
            // Set up a spy and test that refreshAccessToken is not called when authConfig changes
            const refreshAccessTokenSpy = jest.spyOn(authUtils, 'refreshAccessToken');

            // Change authConfig and rerender
            const newConfig = { ...authConfigAlwaysUseResourceRequestProxy, clientId: 'changed-client-id' };
            const { rerender } = renderHook((props) => useAuth(props), { initialProps: authConfigAlwaysUseResourceRequestProxy });
            rerender(newConfig);

            // refreshAccessToken should not be called
            expect(refreshAccessTokenSpy).not.toHaveBeenCalled();
        });

        it('should not attach event listeners when alwaysUseResourceRequestProxy is true', async () => {
            mockGetCookieValue.mockReturnValueOnce(null);
            const { result } = renderHook(() => useAuth(authConfigAlwaysUseResourceRequestProxy));

            mockGetCookieValue.mockReturnValue('visibleAuthToken');

            Object.defineProperty(document, 'hidden', { configurable: true, value: false });

            act(() => {
                document.dispatchEvent(new Event('visibilitychange'));
            });

            await waitFor(() => {
                expect(result.current.authToken).toBe(null);
            });
        });
    });
});
