import React from 'react';
import createLoader from '../amd-loader';
import ManifestLoader from '../manifest';
import { DependencyError } from '../module-registry';

describe('loader', () => {
    const scriptUrl = 'cdn.theorchard.com/module/module.js';
    const styleUrl = 'cdn.theorchard.com/module/module.css';

    let mockEvent = { type: 'load', event: {} };
    const addEventListener = jest
        .fn()
        .mockImplementation(
            (type: string, listener: (event: unknown) => void) =>
                type === mockEvent.type && listener(mockEvent.event)
        );

    jest.spyOn(document, 'createElement').mockImplementation(
        () => ({ addEventListener } as unknown as HTMLElement)
    );
    document.head.append = jest.fn();

    const mockModule = { default: () => <div /> };
    const manifest = {
        get: jest.fn().mockImplementation(async (name: string) => {
            window.define(name, [], () => mockModule);

            return await Promise.resolve({
                scripts: [scriptUrl],
                stylesheets: [styleUrl],
            });
        }),
    };

    const loader = createLoader(manifest as unknown as ManifestLoader);

    beforeAll(async () => await loader('main'));

    test('creates script elements', () => {
        expect(document.head.append).toHaveBeenCalledWith(
            expect.objectContaining({
                src: scriptUrl,
            })
        );
    });

    test('creates link elements', () => {
        expect(document.head.append).toHaveBeenCalledWith(
            expect.objectContaining({
                href: styleUrl,
            })
        );
    });

    test('resolves to loaded module', async () => {
        const name = 'index';

        const loadedModule = await loader(name);
        expect(loadedModule).toEqual(mockModule);

        (document.head.append as jest.Mock).mockReset();
        await loader(name);
        expect(document.head.append).not.toHaveBeenCalled();
    });

    describe('multiple calls for the same module', () => {
        test('returns cached instance', async () => {
            const name = 'webpack';

            const promise = loader(name);

            return await promise.then(async first => {
                (document.head.append as jest.Mock).mockReset();

                return await loader(name).then(second => {
                    expect(second === first).toEqual(true);
                    expect(document.head.append).not.toHaveBeenCalled();
                });
            });
        });
    });

    describe('module requests unknown library dependency', () => {
        const name = 'uniqe';

        test('throws error', async () => {
            const lib = 'windows-me-sdk';
            window.define(name, [lib], () => mockModule);

            let error = null;
            try {
                await loader(name);
            } catch (e) {
                error = e;
            }

            expect(error).toEqual(new DependencyError(lib));
        });
    });

    describe('module failes to load', () => {
        const error = '404';

        beforeEach(() => {
            mockEvent = { type: 'error', event: error };
        });

        test('rejects with error', async () =>
            await expect(loader('somemodule')).rejects.toEqual(error));
    });

    describe('parallel requests for same module', () => {
        const name = 'uniq';

        const firstRequest = loader(name);
        const secondRequest = loader(name);

        beforeEach(() => {
            mockEvent = { type: 'load', event: {} };
        });

        test('returns the same request promise', () =>
            expect(firstRequest === secondRequest).toEqual(true));

        test('request cache is cleared when done', async () =>
            await firstRequest.then(() => {
                const thirdRequest = loader(name);
                expect(thirdRequest === firstRequest).toEqual(false);
            }));
    });
});
