import React from 'react';
import { render, waitFor } from '@testing-library/react';
import { Identity } from '@theorchard/suite-identity';
import { MemoryRouter } from 'react-router-dom';
import * as pageIdleListenerModule from '../../components/pageIdleListener';
import Datadog from '../../datadog';
import Segment from '../../segment';
import { SuiteAppProps, SuiteAuthClient } from '../../types';
import { SuiteApplication } from '../suiteApplication';
import * as authFlow from '../useAuthFlow';

const mockAppConfig = { graphqlUrl: 'test-gql-url' };

vi.mock('@theorchard/suite-config', () => ({
    useAppConfig: () => mockAppConfig,
}));

const mockAuthResult = ({
    user,
    loading = false,
    error,
}: {
    error?: Error;
    user?: Identity;
    loading?: boolean;
} = {}) => {
    const client = {
        logout: vi.fn(),
        endSession: vi.fn(),
        setSessionExpirationHandler: vi.fn(),
    } as unknown as SuiteAuthClient;

    vi.spyOn(authFlow, 'useAuthFlow').mockReturnValue({
        client,
        user,
        loading,
        error,
    });

    return client;
};

describe('<SuiteApplication>', () => {
    const renderComponent = (props: SuiteAppProps) =>
        render(<SuiteApplication {...props} />, {
            wrapper: ({ children }) => <MemoryRouter>{children}</MemoryRouter>,
        });

    describe('when auth loading', () => {
        beforeEach(() => {
            mockAuthResult({ loading: true });
        });

        test('renders splash page', () => {
            const { container, getByTestId, getByText } = renderComponent({});
            expect(container).toMatchSnapshot();
            expect(getByTestId('SplashPage')).toBeInTheDocument();
            expect(getByText('Please wait')).toBeInTheDocument();
        });
    });

    describe('when auth fails', () => {
        const error = new Error('boom');

        beforeEach(() => {
            mockAuthResult({ error, loading: false });
        });

        test('renders fatal error page', async () => {
            const { container, findByTestId, getByText } = renderComponent({});

            expect(await findByTestId('FatalErrorPage')).toBeInTheDocument();
            expect(getByText('Failed to start the application')).toBeInTheDocument();
            expect(container).toMatchSnapshot();
        });
    });

    describe('when auth succeeds', () => {
        const identity = {
            id: '1234',
            email: 'test@test.com',
            name: 'leoDaVinci',
            firstName: 'Leonardo',
            lastName: 'Da Vinci',
            profileId: 1234,
            profileType: 'SuiteProfile',
            profileUUID: '12345',
            features: {},
        };

        let mockAuthClient: SuiteAuthClient;

        beforeEach(() => {
            mockAuthClient = mockAuthResult({
                loading: false,
                user: identity,
            });
        });

        describe('fetching feature flags', () => {
            beforeEach(() => {
                mockAuthClient.getFeatureFlags = vi.fn().mockImplementation(
                    async () =>
                        await new Promise(() => {
                            /*noop*/
                        })
                );
            });

            test('renders splash page', async () => {
                const { queryByTestId, getByText, findByTestId } = renderComponent({});

                expect(await findByTestId('SplashPage')).toBeInTheDocument();
                expect(getByText('Please wait')).toBeInTheDocument();
                expect(queryByTestId('MainNav')).toBeNull();
            });
        });

        describe('feature flags fetched', () => {
            beforeEach(() => {
                mockAuthClient.getFeatureFlags = vi.fn().mockResolvedValue({});
            });

            test('renders main nav and content', async () => {
                const { findByTestId, getByText } = renderComponent({});

                expect(await findByTestId('MainNavFooter')).toBeInTheDocument();
                expect(await findByTestId('Page')).toBeInTheDocument();
                expect(getByText('Leonardo Da Vinci')).toBeInTheDocument();
                expect(getByText('Oops! We lost this page.')).toBeInTheDocument();
            });

            test('renders content without main nav if mainNav=false', async () => {
                const { findByTestId, queryByTestId, getByText } = renderComponent({
                    mainNav: false,
                });

                expect(await findByTestId('Page')).toBeInTheDocument();
                expect(queryByTestId('MainNavFooter')).toBeNull();
                expect(getByText('Oops! We lost this page.')).toBeInTheDocument();
            });
        });

        describe('feature flags fails to load', () => {
            const error = new Error('Boom');

            beforeEach(() => {
                mockAuthClient.getFeatureFlags = vi.fn().mockRejectedValue(error);
            });

            test('renders error message', async () => {
                const { findByTestId, getByText, queryByTestId } = renderComponent({});

                expect(await findByTestId('FatalErrorPage')).toBeInTheDocument();
                expect(queryByTestId('MainNavFooter')).toBeNull();
                expect(getByText('Failed to start the application')).toBeInTheDocument();
            });
        });

        describe('fetching identity resources', () => {
            beforeEach(() => {
                mockAuthClient.getIdentityResources = vi.fn().mockImplementation(
                    async () =>
                        await new Promise(() => {
                            /*noop*/
                        })
                );
            });

            test('renders splash page', async () => {
                const { queryByTestId, getByText, findByTestId } = renderComponent({});

                expect(await findByTestId('SplashPage')).toBeInTheDocument();
                expect(getByText('Please wait')).toBeInTheDocument();
                expect(queryByTestId('MainNav')).toBeNull();
            });
        });

        describe('identity resources fetched', () => {
            beforeEach(() => {
                mockAuthClient.getIdentityResources = vi.fn().mockResolvedValue([]);
            });

            test('renders main nav and content', async () => {
                const { findByTestId, getByText } = renderComponent({});

                expect(await findByTestId('MainNavFooter')).toBeInTheDocument();
                expect(await findByTestId('BannersContainer')).toBeInTheDocument();
                expect(await findByTestId('Page')).toBeInTheDocument();
                expect(getByText('Leonardo Da Vinci')).toBeInTheDocument();
                expect(getByText('Oops! We lost this page.')).toBeInTheDocument();
            });

            describe('with mainNav=false', () => {
                test('renders without MainNav nor BannerContainer', async () => {
                    const { queryByTestId, findByTestId } = renderComponent({ mainNav: false });

                    expect(await findByTestId('Page')).toBeInTheDocument();
                    expect(queryByTestId('.MainNav')).toBeNull();
                    expect(queryByTestId('.BannersContainer')).toBeNull();
                });
            });

            describe('<SidebarPortal>', () => {
                test('is rendered', async () => {
                    const { findByTestId } = renderComponent({});
                    expect(await findByTestId('SuiteSidebarPortal')).toBeInTheDocument();
                });
            });
        });

        describe('identity resources fails to load', () => {
            const error = new Error('Boom');

            beforeEach(() => {
                mockAuthClient.getIdentityResources = vi.fn().mockRejectedValue(error);
            });

            test('renders error message', async () => {
                const { findByTestId, getByText, queryByTestId } = renderComponent({});

                expect(await findByTestId('FatalErrorPage')).toBeInTheDocument();
                expect(queryByTestId('MainNavFooter')).toBeNull();
                expect(getByText('Failed to start the application')).toBeInTheDocument();
            });
        });

        describe('sessionExpiration', () => {
            test('defaults to true', async () => {
                renderComponent({});
                await waitFor(() =>
                    expect(mockAuthClient.setSessionExpirationHandler).toHaveBeenCalled()
                );
            });

            test('can be overridden', async () => {
                const { findByTestId } = renderComponent({
                    sessionExpiration: { modal: () => false },
                });
                await findByTestId('MainNavFooter');
                expect(mockAuthClient.setSessionExpirationHandler).not.toHaveBeenCalled();
            });
        });
    });

    describe('<PageIdleListener>', () => {
        let mockAuthClient: SuiteAuthClient;

        beforeEach(() => {
            mockAuthClient = mockAuthResult();
        });

        test('is not rendered by default', () => {
            const { container } = renderComponent({});
            expect(container.querySelector('.PageIdleListener')).not.toBeInTheDocument();
        });

        test('is rendered when idleTimeoutSeconds > 0', () => {
            const { container } = renderComponent({ idleTimeoutSeconds: 1 });
            expect(container.querySelector('.PageIdleListener')).toBeInTheDocument();
        });

        describe('when user is idle', () => {
            beforeEach(() => {
                vi.spyOn(Segment, 'trackEvent').mockImplementation(vi.fn());
                vi.spyOn(Datadog, 'addAction').mockImplementation(vi.fn());

                // crossTab doesn't work in test environment
                const orig = pageIdleListenerModule.PageIdleListener;
                vi.spyOn(pageIdleListenerModule, 'PageIdleListener').mockImplementation((props) => {
                    return orig({ ...props, crossTab: false });
                });
            });

            afterEach(() => {
                vi.restoreAllMocks();
            });

            test('calls onIdle', async () => {
                renderComponent({ idleTimeoutSeconds: 1 });

                await waitFor(() => expect(Datadog.addAction).toHaveBeenCalledTimes(1), {
                    timeout: 2000,
                });

                expect(Datadog.addAction).toHaveBeenCalledWith('Session timed out', {
                    idleTimeoutSeconds: 1,
                });
                expect(Segment.trackEvent).toHaveBeenCalledTimes(1);
                expect(Segment.trackEvent).toHaveBeenCalledWith('Session timed out', {
                    idleTimeoutSeconds: 1,
                });
                expect(mockAuthClient.endSession).toHaveBeenCalledTimes(1);
                expect(mockAuthClient.endSession).toHaveBeenCalledWith('idle_timeout');
            });
        });
    });
});
