import * as faker from 'faker';

import {
    ErrorHandlingType,
    SimpleWebRequest,
    SimpleWebRequestOptions,
    WebErrorResponse,
    WebRequestPriority,
} from '../src/SimpleWebRequest';

import { asyncTick, DETAILED_RESPONSE } from './helpers';

describe('SimpleWebRequest', () => {
    beforeEach(() => {
        jasmine.Ajax.install();
    });
    afterEach(() => {
        jasmine.Ajax.uninstall();
    });

    it('performs GET request', () => {
        const requestOptions = { contentType: 'json' };
        const requestHeaders = { 'Accept': 'application/json' };
        const statusCode = 200;
        const onSuccess = jasmine.createSpy('onSuccess');
        const method = 'GET';
        const url = faker.internet.url();
        const responseParsingException = undefined;
        const response = {
            ...DETAILED_RESPONSE,
            requestOptions: { ...requestOptions, priority: WebRequestPriority.Normal },
            requestHeaders,
            method,
            url,
            responseParsingException,
        };

        let request: any;
        setTimeout(() => {
            request = jasmine.Ajax.requests.mostRecent();
            request.respondWith({ responseText: JSON.stringify(''), status: statusCode });
        }, 0);

        return new SimpleWebRequest<string>(method, url, requestOptions)
            .start()
            .then(onSuccess)
            .then(() => {
                expect(request.url).toEqual(url);
                expect(request.method).toEqual(method);
                expect(request.status).toEqual(statusCode);
                expect(onSuccess).toHaveBeenCalledWith(response);
            });
    });

    it('sends json POST request', () => {
        const sendData = {
            title: faker.name.title(),
            text: faker.lorem.text(),
        };
        const requestOptions = { sendData };
        const statusCode = 201;
        const onSuccess = jasmine.createSpy('onSuccess');
        const method = 'POST';
        const body = { ...sendData, id: faker.random.uuid() };
        const url = faker.internet.url();
        const responseParsingException = undefined;
        const response = {
            ...DETAILED_RESPONSE,
            requestOptions: { ...requestOptions, priority: WebRequestPriority.Normal },
            statusCode,
            method,
            body,
            url,
            responseParsingException,
        };

        let request: any;
        setTimeout(() => {
            request = jasmine.Ajax.requests.mostRecent();
            request.respondWith({ responseText: JSON.stringify(body), status: statusCode });
        }, 0);

        return new SimpleWebRequest<string>(method, url, requestOptions)
            .start()
            .then(onSuccess)
            .then(() => {
                expect(request.url).toEqual(url);
                expect(request.method).toEqual(method);
                expect(request.status).toEqual(statusCode);
                expect(onSuccess).toHaveBeenCalledWith(response);
            });
    });

    it('allows to set request headers', () => {
        const headers = {
            'X-Requested-With': 'XMLHttpRequest',
            'Max-Forwards': '10',
        };
        const method = 'POST';
        const url = faker.internet.url();

        let request: any;
        setTimeout(() => {
            request = jasmine.Ajax.requests.mostRecent();
            request.respondWith({ status: 200 });
        }, 0);

        return new SimpleWebRequest<string>(method, url, {}, () => headers).start().then(() => {
            expect(request.requestHeaders['X-Requested-With']).toEqual(headers['X-Requested-With']);
            expect(request.requestHeaders['Max-Forwards']).toEqual(headers['Max-Forwards']);
        });
    });

    it('forbids to set Accept header', () => {
        spyOn(console, 'error');

        const headers = {
            'Accept': 'application/xml',
        };
        const method = 'GET';
        const url = faker.internet.url();
        const error = `Don't set Accept with options.headers -- use it with the options.acceptType property`;
        const request = new SimpleWebRequest<string>(method, url, {}, () => headers);

        expect(() => (request as any)._fire()).toThrowError(error);
        expect(console.error).toHaveBeenCalledWith(error);
    });

    it('forbids to set Content-Type header', () => {
        spyOn(console, 'error');

        const headers = {
            'Content-Type': 'application/xml',
        };
        const method = 'GET';
        const url = faker.internet.url();
        const error = `Don't set Content-Type with options.headers -- use it with the options.contentType property`;
        const request = new SimpleWebRequest<string>(method, url, {}, () => headers);

        expect(() => (request as any)._fire()).toThrowError(error);
        expect(console.error).toHaveBeenCalledWith(error);
    });

    describe('blocking', () => {
        let maxRequests = 0;

        beforeEach(() => {
            maxRequests = SimpleWebRequestOptions.MaxSimultaneousRequests;
            SimpleWebRequestOptions.MaxSimultaneousRequests = 0;
            jasmine.clock().install();
        });

        afterEach(() => {
            SimpleWebRequestOptions.MaxSimultaneousRequests = maxRequests;
            jasmine.clock().uninstall();
        });

        it('executes the requests by priority and age', () => {
            const url = faker.internet.url();
            const method = 'GET';
            const onSuccessLow1 = jasmine.createSpy('onSuccessLow1');
            const onSuccessCritical1 = jasmine.createSpy('onSuccessCritical1');
            const onSuccessLow2 = jasmine.createSpy('onSuccessLow2');
            const onSuccessCritical2 = jasmine.createSpy('onSuccessCritical2');
            const status = 200;

            const p1 = new SimpleWebRequest<string>(method, url, { priority: WebRequestPriority.Low })
                .start().then(onSuccessLow1);
            jasmine.clock().tick(10);

            const p2 = new SimpleWebRequest<string>(method, url, { priority: WebRequestPriority.Critical })
                .start().then(onSuccessCritical1);
            jasmine.clock().tick(10);

            const p3 = new SimpleWebRequest<string>(method, url, { priority: WebRequestPriority.Low })
                .start().then(onSuccessLow2);
            jasmine.clock().tick(10);

            SimpleWebRequestOptions.MaxSimultaneousRequests = 1;
            // add a new request to kick the queue
            const p4 = new SimpleWebRequest<string>(method, url, { priority: WebRequestPriority.Critical })
                .start().then(onSuccessCritical2);

            asyncTick().then(() => {
                // only one is executed
                expect(jasmine.Ajax.requests.count()).toBe(1);
                jasmine.Ajax.requests.mostRecent().respondWith({status});
            });

            return p2.then(() => {
                // they're executed in correct order
                expect(onSuccessCritical1).toHaveBeenCalled();
                expect(jasmine.Ajax.requests.count()).toBe(2);
                jasmine.Ajax.requests.mostRecent().respondWith({status});

                return p4;
            }).then(() => {
                expect(onSuccessCritical2).toHaveBeenCalled();
                expect(jasmine.Ajax.requests.count()).toBe(3);
                jasmine.Ajax.requests.mostRecent().respondWith({status});

                return p1;
            }).then(() => {
                expect(onSuccessLow1).toHaveBeenCalled();
                expect(jasmine.Ajax.requests.count()).toBe(4);
                jasmine.Ajax.requests.mostRecent().respondWith({status});

                return p3;
            }).then(() => {
                expect(onSuccessLow2).toHaveBeenCalled();
            });
        });

        it('blocks the request with custom promise', () => {
            SimpleWebRequestOptions.MaxSimultaneousRequests = 1;
            const url = faker.internet.url();
            const method = 'GET';
            let blockResolver: () => void = () => undefined;
            const blockPromise = new Promise<void>((res, rej) => { blockResolver = res; });
            const onSuccess1 = jasmine.createSpy('onSuccess1');
            const p1 = new SimpleWebRequest<string>(method, url, {}, undefined, () => blockPromise).start().then(onSuccess1);

            asyncTick().then(() => {
                expect(jasmine.Ajax.requests.count()).toBe(0);
                blockResolver();

                return asyncTick();
            }).then(() => {
                const request = jasmine.Ajax.requests.mostRecent();
                request.respondWith({ status: 200 });
            });

            return p1.then(() => {
                expect(onSuccess1).toHaveBeenCalled();
            });
        });

        it('after the request is unblocked, it\'s returned to the queue with correct priority', () => {
            const url = faker.internet.url();
            const method = 'GET';
            let blockResolver: () => void = () => undefined;
            const blockPromise = new Promise<void>((res, rej) => { blockResolver = res; });
            const onSuccessHigh = jasmine.createSpy('onSuccessHigh');
            const onSuccessLow = jasmine.createSpy('onSuccessLow');
            const onSuccessCritical = jasmine.createSpy('onSuccessCritical');

            const p1 = new SimpleWebRequest<string>(method, url, { priority: WebRequestPriority.High }, undefined, () => blockPromise)
                .start()
                .then(onSuccessHigh);
            jasmine.clock().tick(10);
            const p2 = new SimpleWebRequest<string>(method, url, { priority: WebRequestPriority.Low }).start().then(onSuccessLow);
            jasmine.clock().tick(10);

            SimpleWebRequestOptions.MaxSimultaneousRequests = 1;
            // add a new request to kick the queue
            const p3 = new SimpleWebRequest<string>(method, url, { priority: WebRequestPriority.Critical }).start().then(onSuccessCritical);

            // unblock the request
            blockResolver();

            // have to do an awkward async tick to get the blocking blocker to resolve before the request goes out
            asyncTick().then(() => {
                expect(jasmine.Ajax.requests.count()).toBe(1);
                jasmine.Ajax.requests.mostRecent().respondWith({ status: 200 });
            });

            return p3.then(() => {
                // first the critical one gets sent
                expect(onSuccessCritical).toHaveBeenCalled();
                expect(jasmine.Ajax.requests.count()).toBe(2);

                // then the high, which was returned to the queue at after getting unblocked
                jasmine.Ajax.requests.mostRecent().respondWith({ status: 200 });

                return p1;
            }).then(() => {
                expect(onSuccessHigh).toHaveBeenCalled();
                expect(jasmine.Ajax.requests.count()).toBe(3);

                // and the low priority one gets sent last
                jasmine.Ajax.requests.mostRecent().respondWith({ status: 200 });

                return p2;
            }).then(() => {
                expect(onSuccessLow).toHaveBeenCalled();
            });
        });

        it('checks the blocked function again, once the request is on top of the queue', () => {
            const url = faker.internet.url();
            const method = 'GET';
            let blockResolver: () => void = () => undefined;
            const blockPromise = new Promise<void>((res, rej) => { blockResolver = res; });
            const onSuccessCritical = jasmine.createSpy('onSuccessCritical');
            const onSuccessHigh = jasmine.createSpy('onSuccessHigh');
            const onSuccessHigh2 = jasmine.createSpy('onSuccessHigh2');
            const blockSpy = jasmine.createSpy('blockSpy').and.callFake(() => blockPromise);

            const p1 = new SimpleWebRequest<string>(method, url, { priority: WebRequestPriority.Critical }, undefined, blockSpy)
                .start()
                .then(onSuccessCritical);
            jasmine.clock().tick(10);

            const p2 = new SimpleWebRequest<string>(method, url, { priority: WebRequestPriority.High }).start().then(onSuccessHigh);
            jasmine.clock().tick(10);

            SimpleWebRequestOptions.MaxSimultaneousRequests = 1;
            // add a new request to kick the queue
            const p3 = new SimpleWebRequest<string>(method, url, { priority: WebRequestPriority.High }).start().then(onSuccessHigh2);

            asyncTick().then(() => {
                expect(blockSpy).toHaveBeenCalled();
                expect(jasmine.Ajax.requests.count()).toBe(1);
                jasmine.Ajax.requests.mostRecent().respondWith({ status: 200 });
            });

            return p2.then(() => {
                expect(onSuccessHigh).toHaveBeenCalled();
                expect(jasmine.Ajax.requests.count()).toBe(2);

                // unblock the request, it will go back to the queue after the currently executed request
                blockResolver();

                jasmine.Ajax.requests.mostRecent().respondWith({ status: 200 });

                return p3;
            }).then(() => {
                expect(onSuccessHigh2).toHaveBeenCalled();
                expect(jasmine.Ajax.requests.count()).toBe(3);

                jasmine.Ajax.requests.mostRecent().respondWith({ status: 200 });

                return p1;
            }).then(() => {
                expect(onSuccessCritical).toHaveBeenCalled();
            });
        });

        it('fails the request, if the blocking promise rejects', done => {
            SimpleWebRequestOptions.MaxSimultaneousRequests = 1;
            const url = faker.internet.url();
            const method = 'GET';
            let blockRejecter: (err: any) => void = () => undefined;
            const blockPromise = new Promise<void>((res, rej) => { blockRejecter = rej; });
            const errorString = 'Terrible error';
            new SimpleWebRequest<string>(method, url, { priority: WebRequestPriority.Critical }, undefined, () => blockPromise)
                .start()
                .then(() => fail(), (err: WebErrorResponse) => {
                    expect(err.statusCode).toBe(0);
                    expect(err.statusText).toBe('_blockRequestUntil rejected: ' + errorString);
                    done();
                });

            blockRejecter(errorString);
        });

        it('does not attempt to fire aborted request, if it was aborted while blocked', () => {
            SimpleWebRequestOptions.MaxSimultaneousRequests = 1;
            const url = faker.internet.url();
            const method = 'GET';
            let blockResolver: () => void = () => undefined;
            const blockPromise = new Promise<void>((res, rej) => { blockResolver = res; });
            const req = new SimpleWebRequest<string>(method, url, { priority: WebRequestPriority.Critical }, undefined, () => blockPromise);
            const p1 = req.start();

            return asyncTick().then(() => {
                expect(jasmine.Ajax.requests.count()).toBe(0);
                req.abort();
                return asyncTick();
            }).then(() => {
                blockResolver();
                return p1;
            }).then(() => {
                fail();
            }, () => {
                expect(jasmine.Ajax.requests.count()).toBe(0);
            });
        });
    });

    describe('retries', () => {
        beforeEach(() => {
            jasmine.clock().install();
        });

        afterEach(() => {
            jasmine.clock().uninstall();
        });

        it('fails the request with "timedOut: true" if it times out without retries', done => {
            const url = faker.internet.url();
            const method = 'GET';
            new SimpleWebRequest<string>(method, url, { timeout: 10, customErrorHandler: () => ErrorHandlingType.DoNotRetry })
                .start()
                .then(() => {
                    expect(false).toBeTruthy();
                    done();
                })
                .catch(errResp => {
                    expect(errResp.timedOut).toBeTruthy();
                    done();
                });

            asyncTick().then(() => {
                jasmine.clock().tick(10);
            });
        });

        it('timedOut flag is reset on retry', done => {
            const url = faker.internet.url();
            const method = 'GET';
            const req = new SimpleWebRequest<string>(method, url, {
                timeout: 10,
                retries: 1,
                customErrorHandler: () => ErrorHandlingType.RetryCountedWithBackoff,
            });
            const requestPromise = req.start();

            requestPromise
                .then(() => {
                    expect(false).toBeTruthy();
                    done();
                })
                .catch(errResp => {
                    expect(errResp.canceled).toBeTruthy();
                    expect(errResp.timedOut).toBeFalsy();
                    done();
                });

            // first try will time out, the second one will be aborted
            asyncTick().then(() => {
                jasmine.clock().tick(10);
                req.abort();
            });
        });
    });

    // @TODO Add more unit tests
});
