import React, { useRef } from 'react';
import { render, renderHook, waitFor } from '@testing-library/react';
import { ENV_PROD } from '@theorchard/constants';
import { FeatureFlagDictionary, Identity, Permission, Resource } from '@theorchard/suite-identity';
import Datadog from '../../datadog';
import Segment from '../../segment';
import { SuiteAuthClient } from '../../types';
import * as appLocale from '../locale';
import * as persistedData from '../persistedData';
import * as authFlow from '../useAuthFlow';
import { useSuiteApplication } from '../useSuiteApplication';

const mockAppConfig = {};

const options = {
    apollo: {},
    plugins: [],
};

const mockIdentity: Identity = {
    id: '1234',
    email: 'test@test.com',
    name: 'test',
    profileId: 1234,
    profileType: 'SuiteProfile',
    features: {},
};

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

const mockAuthResult = ({
    user,
    loading = false,
    error,
}: {
    error?: Error;
    user?: Identity;
    loading?: boolean;
} = {}) => {
    const client = {} as unknown as SuiteAuthClient;

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

    return client;
};

const NO_VENDOR = 'NO VENDOR';

const TestComponent = () => {
    const data = useSuiteApplication(options);
    const count = useRef(0);
    count.current++;

    return (
        <div>
            <div>{data.data.identity?.resources?.[0]?.name ?? NO_VENDOR}</div>
            <div>Render count: {count.current}</div>
        </div>
    );
};

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

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

    test('calls Datadog.setUser', async () => {
        vi.spyOn(Datadog, 'setUser');

        renderHook(() => useSuiteApplication(options));

        await waitFor(() => {
            expect(Datadog.setUser).toHaveBeenCalledWith(mockIdentity);
        });
        expect(Datadog.setUser).toHaveBeenCalledTimes(1);
    });

    test('calls Segment.setUser', async () => {
        vi.spyOn(Segment, 'setUser');

        renderHook(() => useSuiteApplication(options));

        await waitFor(() => {
            expect(Segment.setUser).toHaveBeenCalledWith(mockIdentity);
        });
        expect(Segment.setUser).toHaveBeenCalledTimes(1);
    });

    test('sets application locale', async () => {
        vi.spyOn(appLocale, 'setApplicationLocale');

        renderHook(() => useSuiteApplication(options));

        await waitFor(() => {
            expect(appLocale.setApplicationLocale).toHaveBeenCalledWith(
                mockIdentity.locale,
                mockIdentity.numberFormat,
                mockIdentity.dateFormat
            );
        });
        expect(appLocale.setApplicationLocale).toHaveBeenCalledTimes(1);
    });

    describe('identity details', () => {
        const cachedVendorName = 'Old vendor name';
        const fetchedVendorName = 'New vendor name';

        const cachedFeatures = { feature: true };
        const cachedResources = [{ type: 'Vendor', id: '1', name: cachedVendorName }];

        const fetchedFeatures = { feature: false };
        const fetchedResources = [{ type: 'Vendor', id: '2', name: fetchedVendorName }];

        const cachedPermissions = [
            {
                action: 'read',
                resourceType: 'resource',
                tenants: [{ uuid: '1', __typename: 'Vendor' }],
            },
        ];
        const fetchedPermissions = [
            {
                action: 'write',
                resourceType: 'resource',
                tenants: [{ uuid: '2', __typename: 'Vendor' }],
            },
        ];

        const expectIdentityDetailsToBePersisted = () => {
            expect(persistedData.setPersistedFeatures).toHaveBeenCalledTimes(1);
            expect(persistedData.setPersistedResources).toHaveBeenCalledTimes(1);
            expect(persistedData.setPersistedPermissions).toHaveBeenCalledTimes(1);
            expect(persistedData.setPersistedFeatures).toHaveBeenCalledWith(
                mockIdentity.id,
                fetchedFeatures
            );
            expect(persistedData.setPersistedResources).toHaveBeenCalledWith(
                mockIdentity.id,
                fetchedResources
            );
            expect(persistedData.setPersistedPermissions).toHaveBeenCalledWith(
                mockIdentity.id,
                fetchedPermissions
            );
        };

        const mockIdentityDetails = (
            authClient: SuiteAuthClient,
            {
                cachedFeatures,
                cachedResources,
                cachedPermissions,
                fetchedFeatures,
                fetchedResources,
                fetchedPermissions,
            }: {
                fetchedFeatures?: FeatureFlagDictionary;
                fetchedResources?: Resource[];
                fetchedPermissions?: Permission[];
                cachedFeatures?: FeatureFlagDictionary;
                cachedResources?: Resource[];
                cachedPermissions?: Permission[];
            }
        ) => {
            vi.clearAllMocks();
            vi.spyOn(persistedData, 'getPersistedFeatures').mockReturnValue(cachedFeatures);
            vi.spyOn(persistedData, 'getPersistedResources').mockReturnValue(cachedResources);
            vi.spyOn(persistedData, 'getPersistedPermissions').mockReturnValue(cachedPermissions);
            vi.spyOn(persistedData, 'setPersistedFeatures').mockImplementation();
            vi.spyOn(persistedData, 'setPersistedResources').mockImplementation();
            vi.spyOn(persistedData, 'setPersistedPermissions').mockImplementation();

            authClient.getFeatureFlags = vi.fn().mockResolvedValue(fetchedFeatures);
            authClient.getIdentityResources = vi.fn().mockResolvedValue(fetchedResources);
            authClient.getIdentityPermissions = vi.fn().mockResolvedValue(fetchedPermissions);
        };

        describe('feature flags and resources not cached', () => {
            beforeEach(() => {
                mockIdentityDetails(mockAuthClient, {
                    fetchedFeatures,
                    fetchedResources,
                    fetchedPermissions,
                });
            });

            test('returns fetched identity data', async () => {
                const { findByText } = render(<TestComponent />);

                const element = await findByText(fetchedVendorName);
                expect(element).toBeInTheDocument();

                expectIdentityDetailsToBePersisted();
            });
        });

        describe('feature flags and resources cached', () => {
            beforeEach(() => {
                mockIdentityDetails(mockAuthClient, {
                    cachedFeatures,
                    cachedResources,
                    cachedPermissions,
                    fetchedFeatures: cachedFeatures,
                    fetchedResources: cachedResources,
                    fetchedPermissions: cachedPermissions,
                });
            });

            test('returns cached identity data', async () => {
                const { findByText } = render(<TestComponent />);

                const element = await findByText(cachedVendorName);
                expect(element).toBeInTheDocument();

                expect(persistedData.setPersistedFeatures).toHaveBeenCalledTimes(0);
                expect(persistedData.setPersistedResources).toHaveBeenCalledTimes(0);
                expect(persistedData.setPersistedPermissions).toHaveBeenCalledTimes(0);
            });
        });

        describe('feature flags and resources not cached or fetched', () => {
            beforeEach(() => {
                mockIdentityDetails(mockAuthClient, {
                    cachedFeatures: undefined,
                    cachedResources: undefined,
                    cachedPermissions: undefined,
                    fetchedFeatures: undefined,
                    fetchedResources: undefined,
                    fetchedPermissions: undefined,
                });
            });

            test('returns empty identity data', async () => {
                const { findByText } = render(<TestComponent />);

                const element = await findByText(NO_VENDOR);
                expect(element).toBeInTheDocument();
            });
        });

        describe('feature flags and resources updated', () => {
            beforeEach(() => {
                mockIdentityDetails(mockAuthClient, {
                    cachedFeatures,
                    cachedResources,
                    cachedPermissions,
                    fetchedFeatures,
                    fetchedResources,
                    fetchedPermissions,
                });
            });

            test('returns fetched identity data', async () => {
                const { findByText } = render(<TestComponent />);

                const element = await findByText(fetchedVendorName);
                expect(element).toBeInTheDocument();

                expectIdentityDetailsToBePersisted();
            });
        });
    });

    describe('sessionExpiration', () => {
        const fetchedVendorName = 'New vendor name';
        const fetchedFeatures = { feature: false };
        const fetchedResources = [{ type: 'Vendor', id: '2', name: fetchedVendorName }];
        const fetchedPermissions = undefined;

        const mockIdentityDetails = (
            authClient: SuiteAuthClient,
            {
                cachedFeatures,
                cachedResources,
                cachedPermissions,
                fetchedFeatures,
                fetchedResources,
                fetchedPermissions,
            }: {
                fetchedFeatures?: FeatureFlagDictionary;
                fetchedResources?: Resource[];
                fetchedPermissions?: Permission[];
                cachedFeatures?: FeatureFlagDictionary;
                cachedResources?: Resource[];
                cachedPermissions?: Permission[];
            }
        ) => {
            vi.spyOn(persistedData, 'getPersistedFeatures').mockReturnValue(cachedFeatures);
            vi.spyOn(persistedData, 'getPersistedResources').mockReturnValue(cachedResources);
            vi.spyOn(persistedData, 'getPersistedPermissions').mockReturnValue(cachedPermissions);
            vi.spyOn(persistedData, 'setPersistedFeatures').mockImplementation();
            vi.spyOn(persistedData, 'setPersistedResources').mockImplementation();
            vi.spyOn(persistedData, 'setPersistedPermissions').mockImplementation();

            authClient.getFeatureFlags = vi.fn().mockResolvedValue(fetchedFeatures);
            authClient.getIdentityResources = vi.fn().mockResolvedValue(fetchedResources);
            authClient.getIdentityPermissions = vi.fn().mockResolvedValue(fetchedPermissions);
        };

        beforeEach(() => {
            mockAuthClient.setSessionExpirationHandler = vi.fn();
            mockAuthClient.endSession = vi.fn();
            mockIdentityDetails(mockAuthClient, {
                fetchedFeatures,
                fetchedResources,
                fetchedPermissions,
            });
        });

        test('sets expiration handler when modal=true', async () => {
            renderHook(() =>
                useSuiteApplication({
                    ...options,
                    sessionExpiration: {
                        modal: () => true,
                    },
                })
            );

            await waitFor(() => {
                expect(mockAuthClient.setSessionExpirationHandler).toHaveBeenCalled();
            });
        });

        test('does not set expiration handler by default', async () => {
            renderHook(() =>
                useSuiteApplication({
                    ...options,
                })
            );

            expect(mockAuthClient.setSessionExpirationHandler).not.toHaveBeenCalled();
        });

        describe('with feature flag', () => {
            const ff = 'test-session-expiration-ff';
            beforeEach(() => {
                mockIdentityDetails(mockAuthClient, {
                    fetchedFeatures: { [ff]: true },
                    fetchedResources: [],
                    fetchedPermissions: undefined,
                });
            });

            test('sets expiration handler when enabled', async () => {
                renderHook(() =>
                    useSuiteApplication({
                        ...options,
                        sessionExpiration: {
                            modal: (user) => Boolean(user.features[ff]),
                        },
                    })
                );

                await waitFor(() => {
                    expect(mockAuthClient.setSessionExpirationHandler).toHaveBeenCalled();
                });
            });

            test('does not set expiration handle when disabled', async () => {
                renderHook(() =>
                    useSuiteApplication({
                        ...options,
                        sessionExpiration: {
                            modal: (user) => Boolean(user.features['disabled_feature_flag']),
                        },
                    })
                );

                expect(mockAuthClient.setSessionExpirationHandler).not.toHaveBeenCalled();
            });
        });

        describe('test-expiration', () => {
            const originalWindowLocation: any = window.location;

            beforeEach(() => {
                window.location.search = '?test-expiration=1';
                vi.useFakeTimers();
            });
            afterEach(() => {
                window.location = originalWindowLocation;
                vi.useRealTimers();
            });

            test('session expiration can be triggered in non-prod envs', async () => {
                renderHook(() =>
                    useSuiteApplication({
                        ...options,
                        sessionExpiration: {
                            modal: () => true,
                        },
                    })
                );

                await vi.runAllTimersAsync();
                expect(mockAuthClient.setSessionExpirationHandler).toHaveBeenCalled();
                expect(mockAuthClient.endSession).toHaveBeenCalled();
            });

            test('session expiration can not be triggered in prod', async () => {
                (mockAppConfig as any).environment = ENV_PROD;
                renderHook(() =>
                    useSuiteApplication({
                        ...options,
                        sessionExpiration: {
                            modal: () => true,
                        },
                    })
                );

                await vi.runAllTimersAsync();
                expect(mockAuthClient.endSession).not.toHaveBeenCalled();
                delete (mockAppConfig as any).environment;
            });
        });
    });
});
