import * as React from 'react';
import { shallow, mount } from 'enzyme';
import { MockConnectHost } from './MockConnectHost';
import { MockConnectIframeProvider } from './MockConnectIframeProvider';
import { MockIframeLoadingIndicator } from './MockIframeLoadingIndicator';
import { MockIframeTimeoutIndicator } from './MockIframeTimeoutIndicator';
import { MockIframeFailedToLoadIndicator } from './MockIframeFailedToLoadIndicator';
import { ConnectIframe, ConnectIframeDefinitions, LoadingState } from '../ConnectIframe';
import { analytics } from '../../../adaptors/analytics/AnalyticsAdaptor';
import IFrameLifecycleEventManager from '../IFrameLifecycleEventManager';
import { AnalyticsCategories, AnalyticsActions } from '../../../adaptors/analytics/AnalyticsConstants';

function flushPromises() {
  return new Promise(resolve => setImmediate(resolve));
}

jest.mock('../IFrameLifecycleEventManager', () => {
    return require('./MockIframeLifecycleEventManager');
});

jest.mock('../../../adaptors/analytics/AnalyticsAdaptor', () => {
    const { mockAnalytics } = require('./MockAnalyticsAdaptor');
    return { analytics: mockAnalytics };
});

const props:ConnectIframeDefinitions.Props = {
    appKey: 'appKey',
    moduleKey: 'moduleKey',
    url: 'url',
    width: 'width',
    height: 'height',
    options: {},
    connectHost: new MockConnectHost(),
    connectIframeProvider: new MockConnectIframeProvider()
};

describe('ConnectIframe', () => {
    beforeEach(() => {
        analytics.trigger.mockClear();
        props.connectHost.createExtension.mockClear();
        props.connectHost.destroy.mockClear();
        props.connectIframeProvider.onHideInlineDialog.mockClear();
        props.connectIframeProvider.onStoreNestedIframeJSON.mockClear();
        props.connectIframeProvider.buildIframeStyles.mockClear();
    });

    describe('#resize', () => {
        it('renders correct height and width', async() => {
            const wrapper:ConnectIframe = mount(<ConnectIframe {...props} />) as ConnectIframe;
            await flushPromises();
            wrapper.instance().resize('newWidth', 'newHeight');
            wrapper.update();
            expect(wrapper.find('div').props().style.width).toBe('auto');
            expect(wrapper.find('div').props().style.height).toBe('auto');
            expect(wrapper.find('iframe').props().width).toBe('newWidth');
            expect(wrapper.find('iframe').props().height).toBe('newHeight');
        });
    });

    describe('#sizeToParent', () => {
        it('renders correct height and width', async() => {
            const wrapper:ConnectIframe = mount(<ConnectIframe {...props} />) as ConnectIframe;
            await flushPromises();
            wrapper.instance().sizeToParent();
            wrapper.update();
            expect(wrapper.find('div').props().style.width).toBe('100%');
            expect(wrapper.find('div').props().style.height).toBe('100%');
            expect(wrapper.find('iframe').props().width).toBe('100%');
            expect(wrapper.find('iframe').props().height).toBe('100%');
        });
    });

    describe('#iframeContainer', () => {
        it('renders iframe without div container', async() => {
            const wrapper:ConnectIframe = shallow(<ConnectIframe {...props} iframeContainer={({children}) => children}/>) as ConnectIframe;
            await flushPromises();
            expect(wrapper.find('div').length).toBe(0);
            expect(wrapper.find('iframe').length).toBe(1);
        });
    });

    describe('#isSubIframe', () => {
        describe('in a non-nested iframe', () => {
            it('returns false', () => {
                const wrapper:ConnectIframe = shallow(<ConnectIframe {...props} />) as ConnectIframe;
                expect(wrapper.instance()._isSubIframe()).toBe(false);
            });
        });

        describe('in a nested iframe', () => {
            it('returns true', () => {
                const wrapper:ConnectIframe = shallow(<ConnectIframe {...props} />) as ConnectIframe;
                wrapper.setState({hostFrameOffset: 2});
                expect(wrapper.instance()._isSubIframe()).toBe(true);
            });
        });
    });

    describe('#hideInlineDialog', () => {
        it('does not cause error if not in an inline dialog', () => {
            const wrapper:ConnectIframe = shallow(<ConnectIframe {...props} connectIframeProvider={{}} />) as ConnectIframe;
            wrapper.instance().hideInlineDialog();
            expect(props.connectIframeProvider.onHideInlineDialog).not.toHaveBeenCalled();
        });

        it('invokes the provider method if registered', () => {
            const wrapper:ConnectIframe = shallow(<ConnectIframe {...props} />) as ConnectIframe;
            wrapper.instance().hideInlineDialog();
            expect(props.connectIframeProvider.onHideInlineDialog).toHaveBeenCalledTimes(1);
        });
    });

    describe('#render', () => {
        it('renders a provided timeout indicator', async() => {
            const wrapper:ConnectIframe = shallow(<ConnectIframe {...props} timeoutIndicator={MockIframeTimeoutIndicator as typeof React.PureComponent} />) as ConnectIframe;
            await flushPromises();
            wrapper.instance().iframeTimeoutCallback();
            wrapper.update();
            expect(wrapper.html()).toEqual(expect.stringContaining(MockIframeTimeoutIndicator.getHtml()));
        });

        it('renders a provided failed to load indicator', async() => {
            const wrapper:ConnectIframe = shallow(<ConnectIframe {...props} failedToLoadIndicator={MockIframeFailedToLoadIndicator as typeof React.PureComponent} />) as ConnectIframe;
            await flushPromises();
            wrapper.instance().iframeFailedToLoadCallback();
            wrapper.update();
            expect(wrapper.html()).toEqual(expect.stringContaining(MockIframeFailedToLoadIndicator.getHtml()));
        });

        describe('Loading indicator', () => {
            let wrapper:ConnectIframe;

            beforeEach(() => {
                wrapper = shallow(<ConnectIframe {...props} loadingIndicator={MockIframeLoadingIndicator as typeof React.PureComponent} />) as ConnectIframe;
            });

          it('displays a provided loading indicator while loading', async() => {
                await flushPromises();
                wrapper.update();
                expect(wrapper.html()).toEqual(expect.stringContaining(MockIframeLoadingIndicator.getHtml());
            });

            it('does not display a provided loading indicator once loaded', async() => {
                await flushPromises();
                wrapper.instance().iframeEstablishedCallback();
                wrapper.update();
                expect(wrapper.html()).not.toEqual(expect.stringContaining(MockIframeLoadingIndicator.getHtml());
            });
        });

        it('Hides the iframe while loading', async() => {
            const wrapper = shallow(<ConnectIframe {...props} connectIframeProvider={{}} />);
            await flushPromises();
            wrapper.update();
            expect(JSON.stringify(wrapper.find('iframe').props().style)).toEqual(JSON.stringify({opacity: 0}));
        });

        it('Shows the iframe once loaded', async() => {
            const wrapper = shallow(<ConnectIframe {...props} />);
            await flushPromises();
            wrapper.instance().iframeEstablishedCallback();
            wrapper.update();
            expect(JSON.stringify(wrapper.find('iframe').props().style)).toEqual(JSON.stringify({}));
        });

        it('Using product-provided iframe styles', () => {
          const wrapper = shallow(<ConnectIframe {...props} />);
          wrapper.instance().componentDidMount();
          expect(props.connectIframeProvider.buildIframeStyles).toHaveBeenCalledTimes(1);
          expect(JSON.stringify(wrapper.find('iframe').props().style)).toEqual(JSON.stringify({key: 'value'}));
        });

        it('uses the iframe atttributes provided by simple-xdm', async() => {
            const wrapper = shallow(<ConnectIframe {...props} />);
            await flushPromises();
            wrapper.update();
            expect(wrapper.find('iframe').props().id).toEqual('mock-iframe-id');
            expect(wrapper.find('iframe').props().src).toEqual(props.url);
            expect(wrapper.find('iframe').props().name).toEqual('mock-iframe-name');
        });
    });

    describe('#componentWillUnmount', () => {
        let wrapper:ConnectIframe;

        beforeEach(() => {
            wrapper = shallow(<ConnectIframe {...props} />) as ConnectIframe;
        });

        it('invokes all registered callbacks', () => {
            const cb1 = jest.fn();
            const cb2 = jest.fn();
            wrapper.instance().registerUnmountCallback(cb1);
            wrapper.instance().registerUnmountCallback(cb2);
            wrapper.instance().componentWillUnmount();
            expect(cb1).toHaveBeenCalledTimes(1);
            expect(cb2).toHaveBeenCalledTimes(1);
        });
    });

    describe('#componentDidMount', () => {
        let wrapper:ConnectIframe;

        describe('in a non-nested iframe', () => {
            beforeEach(() => {
                wrapper = shallow(<ConnectIframe {...props} />) as ConnectIframe;
            });

            it('it does not store nested iframe JSON', async() => {
                await flushPromises();
                wrapper.update();
                expect(props.connectIframeProvider.onStoreNestedIframeJSON).not.toHaveBeenCalled();
            });

            it('builds an xdm bridge', async() => {
                await flushPromises();
                wrapper.update();
                expect(props.connectHost.createExtension).toHaveBeenCalledTimes(1);
            });

            it('triggers an analytics event', async() => {
                await flushPromises();
                wrapper.update();
                expect(analytics.trigger).toHaveBeenCalledTimes(1);
                expect(analytics.trigger).toHaveBeenCalledWith(
                    AnalyticsCategories.iframe,
                    AnalyticsActions.create,
                    'appKey',
                    expect.anything()
                );
            });
        });

        describe('in a nested iframe', () => {
            beforeEach(() => {
                const nestedProps = Object.assign({}, props, {hostFrameOffset: 2});
                wrapper = shallow(<ConnectIframe {...nestedProps} children />) as ConnectIframe;
            });

            it('stores the nested iframe JSON', async() => {
                await flushPromises();
                wrapper.update();
                expect(props.connectIframeProvider.onStoreNestedIframeJSON).toHaveBeenCalledTimes(1);
            });
        });
    });

    describe('#componentDidUpdate', () => {
        let wrapper:ConnectIframe;

        describe('in a non-nested iframe', () => {
            beforeEach(() => {
                wrapper = shallow(<ConnectIframe {...props} />) as ConnectIframe;
            });

            it('it does not store nested iframe JSON', async() => {
                await flushPromises();
                wrapper.update();
                expect(props.connectIframeProvider.onStoreNestedIframeJSON).not.toHaveBeenCalled();
            });

            it('builds an xdm bridge', async() => {
                await flushPromises();
                wrapper.update();
                expect(props.connectHost.createExtension).toHaveBeenCalledTimes(1);
            });

            it('triggers an analytics event', async() => {
                await flushPromises();
                wrapper.update();
                expect(analytics.trigger).toHaveBeenCalledTimes(1);
                expect(analytics.trigger).toHaveBeenCalledWith(
                    AnalyticsCategories.iframe,
                    AnalyticsActions.create,
                    'appKey',
                    expect.anything()
                );
            });
        });

        describe('in a nested iframe', () => {
            beforeEach(() => {
                const nestedProps = Object.assign({}, props, {hostFrameOffset: 2});
                wrapper = shallow(<ConnectIframe {...nestedProps} children />) as ConnectIframe;
            });

            it('stores the nested iframe JSON', async() => {
                await flushPromises();
                wrapper.update();
                expect(props.connectIframeProvider.onStoreNestedIframeJSON).toHaveBeenCalledTimes(1);
            });
        });
    });

    it('resets on appKey change', async() => {
        const wrapper:ConnectIframe = mount(<ConnectIframe {...props} />) as ConnectIframe;
        await flushPromises();
        wrapper.update();
        const firstIFrameLifecycleEventManager:IFrameLifecycleEventManager =
            wrapper.instance().getIFrameLifecycleEventManager();
        wrapper.setProps(Object.assign({}, props, {appKey: props.appKey+'1'}));
        await flushPromises();
            wrapper.update();
            const secondIFrameLifecycleEventManager:IFrameLifecycleEventManager =
                wrapper.instance().getIFrameLifecycleEventManager();
            expect(props.connectHost.destroy).toHaveBeenCalledTimes(1);
            expect(props.connectHost.createExtension).toHaveBeenCalledTimes(2);
            expect(firstIFrameLifecycleEventManager.unregister).toHaveBeenCalledTimes(1);
            expect(secondIFrameLifecycleEventManager.unregister).not.toHaveBeenCalled();
        });
    });

    it('resets on moduleKey change', async() => {
        const wrapper:ConnectIframe = mount(<ConnectIframe {...props} />) as ConnectIframe;
        await flushPromises();
        wrapper.update();
        const firstIFrameLifecycleEventManager:IFrameLifecycleEventManager =
            wrapper.instance().getIFrameLifecycleEventManager();
        wrapper.setProps(Object.assign({}, props, {moduleKey: props.moduleKey+'1'}));
        await flushPromises();
        wrapper.update();
        const secondIFrameLifecycleEventManager:IFrameLifecycleEventManager =
            wrapper.instance().getIFrameLifecycleEventManager();
        expect(props.connectHost.destroy).toHaveBeenCalledTimes(1);
        expect(props.connectHost.createExtension).toHaveBeenCalledTimes(2);
        expect(firstIFrameLifecycleEventManager.unregister).toHaveBeenCalledTimes(1);
        expect(secondIFrameLifecycleEventManager.unregister).not.toHaveBeenCalled();
    });

    it('does not reset on width change', async() => {
        const wrapper:ConnectIframe = mount(<ConnectIframe {...props} />) as ConnectIframe;
        await flushPromises();
        wrapper.update();
        const firstIFrameLifecycleEventManager:IFrameLifecycleEventManager =
            wrapper.instance().getIFrameLifecycleEventManager();
        wrapper.setProps(Object.assign({}, props, {width: props.width+'1'}));
        expect(props.connectHost.destroy).not.toHaveBeenCalled();
        expect(props.connectHost.createExtension).toHaveBeenCalledTimes(1);
    });

    it('does not reset on height change', async() => {
        const wrapper:ConnectIframe = mount(<ConnectIframe {...props} />) as ConnectIframe;
        await flushPromises();
        wrapper.update();
        const firstIFrameLifecycleEventManager:IFrameLifecycleEventManager =
            wrapper.instance().getIFrameLifecycleEventManager();
        wrapper.setProps(Object.assign({}, props, {height: props.height+'1'}));
        expect(props.connectHost.destroy).not.toHaveBeenCalled();
        expect(props.connectHost.createExtension).toHaveBeenCalledTimes(1);
    });

    it('creates only a single xdm bridge', async() => {
        const wrapper:ConnectIframe = mount(<ConnectIframe {...props} />) as ConnectIframe;
        await flushPromises();
        wrapper.update();
        expect(props.connectHost.createExtension).toHaveBeenCalledTimes(1);
    });

    describe('iframe context resolver', () => {
        it('runs if required', async() => {
            let customProps = Object.assign({}, props, {connectIframeProvider: new MockConnectIframeProvider()});
            customProps.connectIframeProvider.resolveIframeContext =
                jest.fn(iframeContext =>
                    new Promise(resolve => resolve(Object.assign({}, iframeContext, {url: 'resolved-url'}))));
            const wrapper:ConnectIframe = mount(<ConnectIframe {...customProps} />) as ConnectIframe;
            await flushPromises();
            wrapper.update();
            expect(customProps.connectIframeProvider.resolveIframeContext).toHaveBeenCalledTimes(1);
            expect(wrapper.find('iframe').props().src).toEqual('resolved-url');
        });

        it('does not break the component if not registered', async() => {
            let customProps = Object.assign({}, props, {connectIframeProvider: new MockConnectIframeProvider()});
            customProps.connectIframeProvider.resolveIframeContext = undefined;
            const wrapper:ConnectIframe = mount(<ConnectIframe {...customProps} />) as ConnectIframe;
            await flushPromises();
            wrapper.update();
            expect(wrapper.find('iframe').props().src).toEqual(customProps.url);
        });
    });
});