import { symbolToken, useDependencies } from '@servicetitan/react-ioc';
import { fireEvent, renderHook, waitFor } from '@testing-library/react';
import axios, { AxiosRequestConfig, CanceledError } from 'axios';
import { HttpResponse, http } from 'msw';
import { FC, PropsWithChildren } from 'react';
import mock from 'xhr-mock';
import { server } from '../__mocks__/server';
import { TokenServerAuth, TokenServerAuthOptions, withMicroservice } from '../with-microservice';

class Page {
    get iframe() {
        return document.getElementsByTagName('iframe')[0];
    }
}

describe(`[ajax-handlers] ${withMicroservice.name}`, () => {
    const page = new Page();

    let options: TokenServerAuthOptions | undefined;
    let requestCounts: Record<string, number>; // tracks how many times each request is made

    beforeAll(() => {
        mock.teardown();
        server.listen();
        server.events.on('request:start', ({ request }) => {
            const path = new URL(request.url).pathname;
            requestCounts[path] = (requestCounts[path] ?? 0) + 1;
        });
    });
    afterAll(() => {
        server.close();
        server.events.removeAllListeners('request:start');
    });

    beforeEach(() => {
        options = undefined;
        requestCounts = {};
        server.use(
            http.get('/bff/authInfo', () => {
                return HttpResponse.text(undefined, { status: 500 });
            })
        );
    });
    afterEach(() => {
        server.resetHandlers();
    });

    const UnwrappedComponent: FC<PropsWithChildren> = ({ children }) => <div>{children}</div>;
    const BASE_URL_TOKEN = symbolToken<string>('BASE_URL_TOKEN', false);

    const defaultBaseURL = '/mfe';
    const defaultEndpoint = '/user';

    const createComponentWithMicroservice = ({ baseURL = defaultBaseURL }: { baseURL?: string }) =>
        withMicroservice({
            authAdapter: options ? context => new TokenServerAuth(context, options) : 'token',
            baseURL,
            component: UnwrappedComponent,
            tokens: [BASE_URL_TOKEN],
        });

    const useGetUser = ({
        axiosRequestConfig,
    }: { axiosRequestConfig?: AxiosRequestConfig } = {}) => {
        const [baseURL] = useDependencies(BASE_URL_TOKEN);

        const request = async () => {
            const options = axiosRequestConfig ?? {
                baseURL,
                url: defaultEndpoint,
                method: 'GET',
            };

            const result = await axios.request(options);
            return result.data;
        };

        return request;
    };

    async function simulateIframeMessage(
        baseURL: string,
        isLoggedIn: boolean,
        shouldFireEvent: boolean
    ) {
        await waitFor(() => {
            expect(page.iframe?.src).toMatch(
                new RegExp(`http://localhost${baseURL}/bff/silent-login\\?initiator=.+`)
            );
        });

        if (shouldFireEvent) {
            // Simulate the iframe postMessage
            const initiator = page.iframe.src.match(/initiator=(.+)/)?.[1];
            fireEvent(
                window,
                new MessageEvent('message', {
                    data: {
                        source: 'bff-silent-login',
                        isLoggedIn,
                        initiator,
                    },
                    origin: 'http://localhost',
                })
            );
        } else {
            jest.runOnlyPendingTimers();
        }
    }

    it('authenticates before making a request', async () => {
        const hook = renderHook(() => useGetUser(), {
            wrapper: createComponentWithMicroservice({}),
        });

        await waitFor(() => {
            expect(hook.result.current).not.toBeNull();
        });

        await waitFor(() => {
            expect(page.iframe?.src).toMatch(
                new RegExp(`http://localhost${defaultBaseURL}/bff/silent-login\\?initiator=.+`)
            );
        });
    });

    describe('when an private api request is made', () => {
        const originalLocation = window.location;
        let isLoggedIn: boolean;
        let shouldFireEvent: boolean;

        interface SubjectOptions {
            baseURL?: string;
            isLoggedIn?: boolean;
            shouldFireEvent?: boolean;
            expectAuthTimes?: number;
        }
        const subject = async (options: SubjectOptions = {}) => {
            const baseURL = options.baseURL ?? defaultBaseURL;

            const hook = renderHook(() => useGetUser(), {
                wrapper: createComponentWithMicroservice({
                    baseURL,
                }),
            });

            await waitFor(() => {
                expect(hook.result.current).not.toBeNull();
            });

            const getUser = hook.result.current;
            const getUserPromise = getUser();

            const repeatTimes = options.expectAuthTimes ?? 1;
            for (let i = 0; i < repeatTimes; i++) {
                // eslint-disable-next-line no-await-in-loop
                await simulateIframeMessage(
                    baseURL,
                    options.isLoggedIn ?? isLoggedIn,
                    options.shouldFireEvent ?? shouldFireEvent
                );
            }

            return getUserPromise;
        };

        beforeAll(() => {
            Object.defineProperty(window, 'location', {
                configurable: true,
                value: { ...originalLocation, reload: jest.fn() },
            });
        });
        afterAll(() => {
            Object.defineProperty(window, 'location', {
                configurable: true,
                value: originalLocation,
            });
        });

        beforeEach(() => {
            jest.clearAllMocks();
            server.use(http.get('/mfe/user', () => new HttpResponse('foo', { status: 200 })));

            shouldFireEvent = true;
            isLoggedIn = true;
        });

        function itRaisesCanceledError() {
            it('raises canceled request', async () => {
                await expect(subject()).rejects.toBeInstanceOf(CanceledError);
            });
        }

        it('makes request successfully', async () => {
            const response = await subject();

            expect(response).toBe('foo');
            expect(requestCounts['/mfe/user']).toBe(1);
        });

        describe('when iframe message is never sent', () => {
            beforeEach(() => {
                jest.spyOn(console, 'error').mockImplementation(jest.fn()); // suppress AjaxHandlers.PreAuthenticate error
                jest.useFakeTimers();
                shouldFireEvent = false;
            });
            afterEach(() => {
                jest.useRealTimers();
            });

            itRaisesCanceledError();
        });

        describe('when request returns 401 once', () => {
            beforeEach(() => {
                server.use(
                    http.get('/mfe/user', () => new HttpResponse(null, { status: 401 }), {
                        once: true,
                    })
                );
            });

            it('re-authenticates', async () => {
                const response = await subject({ expectAuthTimes: 2 });
                expect(response).toBe('foo');
                expect(requestCounts['/bff/authInfo']).toBe(2);
                expect(requestCounts['/mfe/user']).toBe(2);
            });
        });

        describe('when user is not authenticated', () => {
            beforeEach(() => {
                isLoggedIn = false;
                window.sessionStorage.clear();
            });

            itRaisesCanceledError();

            it('reloads the page', async () => {
                await expect(subject()).rejects.toThrow();

                expect(window.location.reload).toHaveBeenCalled();
            });

            describe('with a custom error handler', () => {
                const onError = jest.fn();

                beforeEach(() => (options = { onError }));

                test('calls error handler instead of reloading page', async () => {
                    await expect(subject()).rejects.toThrow();

                    expect(window.location.reload).not.toHaveBeenCalled();
                    expect(onError).toHaveBeenCalled();
                });
            });
        });

        describe('when two instances of TokenServerAuth both authenticate at the same time with difference results', () => {
            beforeEach(() => {
                server.use(http.get('/mfe2/user', () => new HttpResponse('bar', { status: 200 })));
            });

            test('each instance receives the correct result', async () => {
                const responses = await Promise.allSettled([
                    subject(),
                    subject({ baseURL: '/mfe2', isLoggedIn: false }),
                ]);
                expect((responses[0] as PromiseFulfilledResult<string>).value).toBe('foo');
                expect((responses[1] as PromiseRejectedResult).reason).toBeInstanceOf(
                    CanceledError
                );
            });
        });
    });
});
