/* eslint-disable @typescript-eslint/no-non-null-assertion */
import expect from 'app/test/unexpected';
import sinon, { SinonSpy, SinonStub } from 'sinon';

import refreshTokenMiddleware from 'app/components/user/middlewares/refreshTokenMiddleware';
import { browserHistory } from 'app/services/history';
import * as authentication from 'app/services/api/authentication';
import { InternalServerError, Middleware } from 'app/services/request';
import { updateToken } from 'app/components/accounts/actions';
import { MiddlewareRequestOptions } from '../../../services/request/PromiseMiddlewareLayer';

const refreshToken = 'foo';
const expiredToken =
    'eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJuYmYiOjE0NzA3NjE0NDMsImV4cCI6MTQ3MDc2MTQ0MywiaWF0IjoxNDcwNzYxNDQzLCJqdGkiOiJpZDEyMzQ1NiJ9.gWdnzfQQvarGpkbldUvB8qdJZSVkvdNtCbhbbl2yJW8';
// valid till 2100 year
const validToken =
    'eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJuYmYiOjE0NzA3NjE5NzcsImV4cCI6NDEwMjQ0NDgwMCwiaWF0IjoxNDcwNzYxOTc3LCJqdGkiOiJpZDEyMzQ1NiJ9.M4KY4QgHOUzhpAZjWoHJbGsEJPR-RBsJ1c1BKyxvAoU';

describe('refreshTokenMiddleware', () => {
    let middleware: Middleware;
    let getState: SinonStub;
    let dispatch: SinonSpy<[any], any>;

    const email = 'test@email.com';

    beforeEach(() => {
        sinon.stub(authentication, 'requestToken').named('authentication.requestToken');
        sinon.stub(authentication, 'logout').named('authentication.logout');
        sinon.stub(browserHistory, 'push');

        getState = sinon.stub().named('store.getState');
        dispatch = sinon
            .spy((arg) => (typeof arg === 'function' ? arg(dispatch, getState) : arg))
            .named('store.dispatch');

        middleware = refreshTokenMiddleware({ getState, dispatch } as any);
    });

    afterEach(() => {
        (authentication.requestToken as any).restore();
        (authentication.logout as any).restore();
        (browserHistory.push as any).restore();
    });

    function assertRelogin() {
        expect(dispatch, 'to have a call satisfying', [
            {
                type: 'auth:setCredentials',
                payload: {
                    login: email,
                    returnUrl: expect.it('to be a string'),
                },
            },
        ]);

        expect(browserHistory.push, 'to have a call satisfying', ['/login']);
    }

    it('must be till 2100 to test with validToken', () => expect(new Date().getFullYear(), 'to be less than', 2100));

    describe('#before', () => {
        describe('when token expired', () => {
            beforeEach(() => {
                const account = {
                    id: 42,
                    email,
                    token: expiredToken,
                    refreshToken,
                };
                getState.returns({
                    accounts: {
                        active: account.id,
                        available: [account],
                    },
                    auth: {
                        credentials: {},
                    },
                    user: {},
                });
            });

            it('should request new token', () => {
                const data = {
                    url: 'foo',
                    options: {
                        headers: {},
                    },
                };

                (authentication.requestToken as any).returns(Promise.resolve(validToken));

                return middleware.before!(data).then((resp) => {
                    expect(resp, 'to satisfy', data);

                    expect(authentication.requestToken, 'to have a call satisfying', [refreshToken]);
                });
            });

            it('should not apply to refresh-token request', () => {
                const data: MiddlewareRequestOptions = {
                    url: '/refresh-token',
                    options: {
                        headers: {},
                    },
                };
                const resp = middleware.before!(data);

                return expect(resp, 'to be fulfilled with', data).then(() =>
                    expect(authentication.requestToken, 'was not called'),
                );
            });

            it('should not auto refresh token if options.token specified', () => {
                const data: MiddlewareRequestOptions = {
                    url: 'foo',
                    options: {
                        token: 'foo',
                        headers: {},
                    },
                };
                middleware.before!(data);

                expect(authentication.requestToken, 'was not called');
            });

            it('should update user with new token', () => {
                const data = {
                    url: 'foo',
                    options: {
                        headers: {},
                    },
                };

                (authentication.requestToken as any).returns(Promise.resolve(validToken));

                return middleware.before!(data).then(() =>
                    expect(dispatch, 'to have a call satisfying', [updateToken(validToken)]),
                );
            });

            it('should relogin if token can not be parsed', () => {
                const account = {
                    id: 42,
                    email,
                    token: 'realy bad token',
                    refreshToken,
                };
                getState.returns({
                    accounts: {
                        active: account.id,
                        available: [account],
                    },
                    auth: {
                        credentials: {},
                    },
                    user: {},
                });

                const req: MiddlewareRequestOptions = {
                    url: 'foo',
                    options: { headers: {} },
                };

                return expect(middleware.before!(req), 'to be rejected with', {
                    message: 'Invalid token',
                }).then(() => {
                    expect(authentication.requestToken, 'was not called');

                    assertRelogin();
                });
            });

            it('should relogin if token request failed', () => {
                (authentication.requestToken as any).returns(Promise.reject());

                return expect(middleware.before!({ url: 'foo', options: { headers: {} } }), 'to be rejected').then(() =>
                    assertRelogin(),
                );
            });

            it('should not logout if request failed with 5xx', () => {
                const resp = new InternalServerError({}, { status: 500 });

                (authentication.requestToken as any).returns(Promise.reject(resp));

                return expect(
                    middleware.before!({ url: 'foo', options: { headers: {} } }),
                    'to be rejected with',
                    resp,
                ).then(() => expect(dispatch, 'to have no calls satisfying', [{ payload: { isGuest: true } }]));
            });
        });

        it('should not be applied if no token', () => {
            getState.returns({
                accounts: {
                    active: null,
                    available: [],
                },
                user: {},
            });

            const data: MiddlewareRequestOptions = {
                url: 'foo',
                options: {
                    headers: {},
                },
            };
            const resp = middleware.before!(data);

            return expect(resp, 'to be fulfilled with', data).then(() =>
                expect(authentication.requestToken, 'was not called'),
            );
        });
    });

    describe('#catch', () => {
        const expiredResponse = {
            name: 'Unauthorized',
            message: 'Token expired',
            code: 0,
            status: 401,
            type: 'yii\\web\\UnauthorizedHttpException',
        };

        const badTokenReponse = {
            name: 'Unauthorized',
            message: 'You are requesting with an invalid credential.',
            code: 0,
            status: 401,
            type: 'yii\\web\\UnauthorizedHttpException',
        };

        const incorrectTokenReponse = {
            name: 'Unauthorized',
            message: 'Incorrect token',
            code: 0,
            status: 401,
            type: 'yii\\web\\UnauthorizedHttpException',
        };

        let restart: SinonStub;

        beforeEach(() => {
            getState.returns({
                accounts: {
                    active: 1,
                    available: [
                        {
                            id: 1,
                            email,
                            token: 'old token',
                            refreshToken,
                        },
                    ],
                },
                user: {},
            });

            restart = sinon.stub().named('restart');

            (authentication.requestToken as any).returns(Promise.resolve(validToken));
        });

        function assertNewTokenRequest() {
            expect(authentication.requestToken, 'to have a call satisfying', [refreshToken]);
            expect(restart, 'was called');
            expect(dispatch, 'was called');
        }

        it('should request new token if expired', () =>
            expect(
                middleware.catch!(expiredResponse, { url: '', options: { headers: {} } }, restart),
                'to be fulfilled',
            ).then(assertNewTokenRequest));

        it('should request new token if invalid credential', () =>
            expect(
                middleware.catch!(badTokenReponse, { url: '', options: { headers: {} } }, restart),
                'to be fulfilled',
            ).then(assertNewTokenRequest));

        it('should request new token if token is incorrect', () =>
            expect(
                middleware.catch!(incorrectTokenReponse, { url: '', options: { headers: {} } }, restart),
                'to be fulfilled',
            ).then(assertNewTokenRequest));

        it('should relogin if no refreshToken', () => {
            getState.returns({
                accounts: {
                    active: 1,
                    available: [
                        {
                            id: 1,
                            email,
                            refreshToken: null,
                        },
                    ],
                },
                auth: {
                    credentials: {},
                },
                user: {},
            });

            return expect(
                middleware.catch!(incorrectTokenReponse, { url: '', options: { headers: {} } }, restart),
                'to be rejected',
            ).then(() => {
                assertRelogin();
            });
        });

        it('should pass the request through if options.token specified', () => {
            const promise = middleware.catch!(
                expiredResponse,
                {
                    url: '',
                    options: {
                        token: 'foo',
                        headers: {},
                    },
                },
                restart,
            );

            return expect(promise, 'to be rejected with', expiredResponse).then(() => {
                expect(restart, 'was not called');
                expect(authentication.requestToken, 'was not called');
            });
        });

        it('should pass the rest of failed requests through', () => {
            const resp = {};

            const promise = middleware.catch!(
                resp,
                {
                    url: '',
                    options: {
                        headers: {},
                    },
                },
                restart,
            );

            return expect(promise, 'to be rejected with', resp).then(() => {
                expect(restart, 'was not called');
                expect(authentication.requestToken, 'was not called');
            });
        });
    });
});
