import React from 'react';
import { ApolloClient, NormalizedCacheObject } from '@apollo/client';
import { AppContext, SuiteAuthenticatedContext, SuiteInitContext, SuitePlugin } from '../../types';
import { PluginManager } from '../pluginManager';

describe('PluginManager', () => {
    describe('onInit', () => {
        const context = {
            config: {
                appName: 'test',
                appTitle: 'test',
                brand: 'orchard',
                environment: 'prod',
            },
        } as SuiteInitContext;

        const plugin1Data = { plugin1: 'says hi' };
        const plugin3Data = { plugin2: 'says hey' };
        const plugin1 = { onInit: vi.fn().mockResolvedValue(plugin1Data) };
        const plugin2 = {};
        const plugin3 = {
            name: 'plugin3',
            onInit: vi.fn().mockResolvedValue(plugin3Data),
        };

        const plugins: SuitePlugin[] = [plugin1, plugin2, plugin3];

        beforeEach(() => {
            plugin1.onInit.mockClear();
            plugin3.onInit.mockClear();
        });

        test('calls "onInit" on all plugins', async () => {
            await PluginManager.onInit(plugins, context);

            expect(plugin1.onInit).toHaveBeenCalledTimes(1);
            expect(plugin3.onInit).toHaveBeenCalledTimes(1);
        });

        test('returns merged data from all calls', async () => {
            const data = await PluginManager.onInit(plugins, context);

            expect(data).toEqual({ ...plugin1Data, ...plugin3Data });
        });

        test('data is passed down', async () => {
            const pluginManager = new PluginManager(plugins);
            await pluginManager.onInit(context);

            expect(plugin3.onInit).toHaveBeenCalledWith(context, plugin1Data);
        });

        describe('on error', () => {
            test('throws PluginError', async () => {
                plugin1.onInit.mockImplementationOnce(() => {
                    throw new Error('boom');
                });

                await expect(PluginManager.onInit(plugins, context)).rejects.toThrow(
                    'The plugin [0] failed while executing the "onInit" callback.'
                );
            });

            describe('with named plugin', () => {
                beforeEach(() => {
                    plugin3.onInit.mockImplementationOnce(() => {
                        throw new Error('boom');
                    });
                });

                test('throws PluginError with name in message', async () =>
                    await expect(PluginManager.onInit(plugins, context)).rejects.toThrow(
                        'The plugin "plugin3" failed while executing the "onInit" callback.'
                    ));
            });
        });
    });

    describe('onGraphqlClientCreate', () => {
        const context = {
            config: {
                appName: 'test',
                appTitle: 'test',
                brand: 'orchard',
                environment: 'prod',
            },
        } as SuiteAuthenticatedContext;

        const plugin1Client = {} as ApolloClient<NormalizedCacheObject>;
        const plugin3Client = {} as ApolloClient<NormalizedCacheObject>;
        const plugin1 = {
            onGraphqlClientCreate: vi.fn().mockResolvedValue(plugin1Client),
        };
        const plugin2 = {};
        const plugin3 = {
            onGraphqlClientCreate: vi.fn().mockResolvedValue(plugin3Client),
        };

        const plugins: SuitePlugin[] = [plugin1, plugin2, plugin3];

        beforeEach(() => {
            plugin1.onGraphqlClientCreate.mockClear();
            plugin3.onGraphqlClientCreate.mockClear();
        });

        test('calls "onGraphqlClientCreate" on all plugins', async () => {
            const pluginManager = new PluginManager(plugins);
            await pluginManager.onGraphqlClientCreate(context);

            expect(plugin1.onGraphqlClientCreate).toHaveBeenCalledTimes(1);
            expect(plugin3.onGraphqlClientCreate).toHaveBeenCalledTimes(1);
        });

        test('returns client from last call', async () => {
            const pluginManager = new PluginManager(plugins);
            const data = await pluginManager.onGraphqlClientCreate(context);

            expect(data).toEqual(plugin3Client);
        });

        test('client is passed down', async () => {
            const pluginManager = new PluginManager(plugins);
            await pluginManager.onGraphqlClientCreate(context);

            expect(plugin3.onGraphqlClientCreate).toHaveBeenCalledWith(context, plugin1Client);
        });

        describe('on error', () => {
            beforeEach(() => {
                plugin1.onGraphqlClientCreate.mockImplementation(() => {
                    throw new Error('boom');
                });
            });

            test('throws PluginError', async () => {
                const pluginManager = new PluginManager(plugins);

                await expect(pluginManager.onGraphqlClientCreate(context)).rejects.toThrow(
                    'The plugin [0] failed while executing the "onGraphqlClientCreate" callback.'
                );
            });
        });
    });

    describe('onAppDataLoad', () => {
        const context = {
            config: {
                appName: 'test',
                appTitle: 'test',
                brand: 'orchard',
                environment: 'prod',
            },
        } as AppContext;

        const plugin1Data = { plugin1: 'says hi' };
        const plugin3Data = { plugin3: 'says hey' };
        const plugin1 = {
            onAppDataLoad: vi.fn().mockResolvedValue(plugin1Data),
        };
        const plugin2 = {};
        const plugin3 = {
            onAppDataLoad: vi.fn().mockResolvedValue(plugin3Data),
        };

        const plugins: SuitePlugin[] = [plugin1, plugin2, plugin3];

        beforeEach(() => {
            plugin1.onAppDataLoad.mockClear();
            plugin3.onAppDataLoad.mockClear();
        });

        test('calls "onAppDataLoad" on all plugins', async () => {
            const pluginManager = new PluginManager(plugins);
            await pluginManager.onAppDataLoad(context);

            expect(plugin1.onAppDataLoad).toHaveBeenCalledTimes(1);
            expect(plugin3.onAppDataLoad).toHaveBeenCalledTimes(1);
        });

        test('returns merged data', async () => {
            const pluginManager = new PluginManager(plugins);
            const data = await pluginManager.onAppDataLoad(context);

            expect(data).toEqual({
                ...plugin1Data,
                ...plugin3Data,
            });
        });

        test('data is passed down', async () => {
            const pluginManager = new PluginManager(plugins);
            await pluginManager.onAppDataLoad(context);

            expect(plugin3.onAppDataLoad).toHaveBeenCalledWith({
                ...context,
                appData: plugin1Data,
            });
        });

        describe('on error', () => {
            beforeEach(() => {
                plugin1.onAppDataLoad.mockImplementation(() => {
                    throw new Error('boom');
                });
            });

            test('throws PluginError', async () => {
                const pluginManager = new PluginManager(plugins);

                await expect(pluginManager.onAppDataLoad(context)).rejects.toThrow(
                    'The plugin [0] failed while executing the "onAppDataLoad" callback.'
                );
            });
        });
    });

    describe('onLoaded', () => {
        const context = {
            config: {
                appName: 'test',
                appTitle: 'test',
                brand: 'orchard',
                environment: 'prod',
            },
        } as AppContext;

        const plugin1 = { onLoaded: vi.fn() };
        const plugin2 = {};
        const plugin3 = { onLoaded: vi.fn() };

        const plugins: SuitePlugin[] = [plugin1, plugin2, plugin3];

        beforeEach(() => {
            plugin1.onLoaded.mockClear();
            plugin3.onLoaded.mockClear();
        });

        test('calls "onLoaded" on all plugins', async () => {
            const pluginManager = new PluginManager(plugins);
            await pluginManager.onLoaded(context);

            expect(plugin1.onLoaded).toHaveBeenCalledTimes(1);
            expect(plugin3.onLoaded).toHaveBeenCalledTimes(1);
        });

        describe('on error', () => {
            beforeEach(() => {
                plugin1.onLoaded.mockImplementation(() => {
                    throw new Error('boom');
                });
            });

            test('throws PluginError', async () => {
                const pluginManager = new PluginManager(plugins);

                await expect(pluginManager.onLoaded(context)).rejects.toThrow(
                    'The plugin [0] failed while executing the "onLoaded" callback.'
                );
            });
        });
    });

    describe('getWrappers', () => {
        const plugin1Text = 'Plugin One';
        const plugin2Text = 'Plugin Two';
        const pluginHighPrioText = 'First!';

        const plugin1 = { Wrapper: () => <>{plugin1Text}</> };
        const plugin2 = { Wrapper: () => <>{plugin2Text}</> };
        const highPrioPlugin = {
            priority: 'high',
            Wrapper: () => <>{pluginHighPrioText}</>,
        };

        const plugins = [plugin1, {}, highPrioPlugin, plugin2];

        test('returns high and normal priority wrappers', () => {
            const { highPriority, normalPriority } = PluginManager.getWrappers(plugins);

            expect(highPriority).toStrictEqual([highPrioPlugin.Wrapper]);
            expect(normalPriority).toStrictEqual([plugin1.Wrapper, plugin2.Wrapper]);
        });
    });
});
