import {deepStrictEqual, fail} from 'assert';
import {FEATURES} from '../features';
import {CacheStorage} from '../cache-storage';
import {Logger} from '../logger';

const proxy = 'http://example.com/proxy';

const createMockContext = (origin: string, opts = {}) => {
    const context = {
        location: {
            href: origin
        },
        document: {
            createElement(_name: string) {
                let _href = '';
                return {
                    set href(value: string) {
                        _href = value;
                    },
                    get href() {
                        return _href;
                    },
                    get protocol() {
                        return new URL(_href).protocol;
                    },
                    get hostname() {
                        return new URL(_href).hostname;
                    },
                    get port() {
                        return new URL(_href).port;
                    }
                };
            }
        }
    };

    CacheStorage.setContext(context as Window);
    Logger.create({id: 'test', enabled: false});
    return CacheStorage.create('test', {
        imageTimeout: 0,
        useCORS: false,
        allowTaint: false,
        proxy,
        ...opts
    });
};

const images: ImageMock[] = [];
const xhr: XMLHttpRequestMock[] = [];
const sleep = async (timeout: number) => await new Promise((resolve) => setTimeout(resolve, timeout));

class ImageMock {
    src?: string;
    crossOrigin?: string;
    onload?: () => void;
    constructor() {
        images.push(this);
    }
}

class XMLHttpRequestMock {
    sent: boolean;
    status: number;
    timeout: number;
    method?: string;
    url?: string;
    response?: string;
    onload?: () => void;
    ontimeout?: () => void;
    constructor() {
        this.sent = false;
        this.status = 500;
        this.timeout = 5000;
        xhr.push(this);
    }

    async load(status: number, response: string) {
        this.response = response;
        this.status = status;
        if (this.onload) {
            this.onload();
        }
        await sleep(0);
    }

    open(method: string, url: string) {
        this.method = method;
        this.url = url;
    }
    send() {
        this.sent = true;
    }
}

Object.defineProperty(global, 'Image', {value: ImageMock, writable: true});
Object.defineProperty(global, 'XMLHttpRequest', {
    value: XMLHttpRequestMock,
    writable: true
});

const setFeatures = (opts: {[key: string]: boolean} = {}) => {
    const defaults: {[key: string]: boolean} = {
        SUPPORT_SVG_DRAWING: true,
        SUPPORT_CORS_IMAGES: true,
        SUPPORT_CORS_XHR: true,
        SUPPORT_RESPONSE_TYPE: false
    };

    Object.keys(defaults).forEach((key) => {
        Object.defineProperty(FEATURES, key, {
            value: typeof opts[key] === 'boolean' ? opts[key] : defaults[key],
            writable: true
        });
    });
};

describe('cache-storage', () => {
    beforeEach(() => setFeatures());
    afterEach(() => {
        xhr.splice(0, xhr.length);
        images.splice(0, images.length);
    });
    it('addImage adds images to cache', async () => {
        const cache = createMockContext('http://example.com', {proxy: null});
        await cache.addImage('http://example.com/test.jpg');
        await cache.addImage('http://example.com/test2.jpg');

        deepStrictEqual(images.length, 2);
        deepStrictEqual(images[0].src, 'http://example.com/test.jpg');
        deepStrictEqual(images[1].src, 'http://example.com/test2.jpg');
    });

    it('addImage should not add duplicate entries', async () => {
        const cache = createMockContext('http://example.com');
        await cache.addImage('http://example.com/test.jpg');
        await cache.addImage('http://example.com/test.jpg');

        deepStrictEqual(images.length, 1);
        deepStrictEqual(images[0].src, 'http://example.com/test.jpg');
    });

    describe('svg', () => {
        it('should add svg images correctly', async () => {
            const cache = createMockContext('http://example.com');
            await cache.addImage('http://example.com/test.svg');
            await cache.addImage('http://example.com/test2.svg');

            deepStrictEqual(images.length, 2);
            deepStrictEqual(images[0].src, 'http://example.com/test.svg');
            deepStrictEqual(images[1].src, 'http://example.com/test2.svg');
        });

        it('should omit svg images if not supported', async () => {
            setFeatures({SUPPORT_SVG_DRAWING: false});
            const cache = createMockContext('http://example.com');
            await cache.addImage('http://example.com/test.svg');
            await cache.addImage('http://example.com/test2.svg');

            deepStrictEqual(images.length, 0);
        });
    });

    describe('cross-origin', () => {
        it('addImage should not add images it cannot load/render', async () => {
            const cache = createMockContext('http://example.com', {
                proxy: undefined
            });
            await cache.addImage('http://html2canvas.hertzen.com/test.jpg');
            deepStrictEqual(images.length, 0);
        });

        it('addImage should add images if tainting enabled', async () => {
            const cache = createMockContext('http://example.com', {
                allowTaint: true,
                proxy: undefined
            });
            await cache.addImage('http://html2canvas.hertzen.com/test.jpg');
            deepStrictEqual(images.length, 1);
            deepStrictEqual(images[0].src, 'http://html2canvas.hertzen.com/test.jpg');
            deepStrictEqual(images[0].crossOrigin, undefined);
        });

        it('addImage should add images if cors enabled', async () => {
            const cache = createMockContext('http://example.com', {useCORS: true});
            await cache.addImage('http://html2canvas.hertzen.com/test.jpg');
            deepStrictEqual(images.length, 1);
            deepStrictEqual(images[0].src, 'http://html2canvas.hertzen.com/test.jpg');
            deepStrictEqual(images[0].crossOrigin, 'anonymous');
        });

        it('addImage should not add images if cors enabled but not supported', async () => {
            setFeatures({SUPPORT_CORS_IMAGES: false});

            const cache = createMockContext('http://example.com', {
                useCORS: true,
                proxy: undefined
            });
            await cache.addImage('http://html2canvas.hertzen.com/test.jpg');
            deepStrictEqual(images.length, 0);
        });

        it('addImage should not add images to proxy if cors enabled', async () => {
            const cache = createMockContext('http://example.com', {useCORS: true});
            await cache.addImage('http://html2canvas.hertzen.com/test.jpg');
            deepStrictEqual(images.length, 1);
            deepStrictEqual(images[0].src, 'http://html2canvas.hertzen.com/test.jpg');
            deepStrictEqual(images[0].crossOrigin, 'anonymous');
        });

        it('addImage should use proxy ', async () => {
            const cache = createMockContext('http://example.com');
            await cache.addImage('http://html2canvas.hertzen.com/test.jpg');
            deepStrictEqual(xhr.length, 1);
            deepStrictEqual(
                xhr[0].url,
                `${proxy}?url=${encodeURIComponent('http://html2canvas.hertzen.com/test.jpg')}&responseType=text`
            );
            await xhr[0].load(200, '<data response>');

            deepStrictEqual(images.length, 1);
            deepStrictEqual(images[0].src, '<data response>');
        });

        it('proxy should respect imageTimeout', async () => {
            const cache = createMockContext('http://example.com', {
                imageTimeout: 10
            });
            await cache.addImage('http://html2canvas.hertzen.com/test.jpg');

            deepStrictEqual(xhr.length, 1);
            deepStrictEqual(
                xhr[0].url,
                `${proxy}?url=${encodeURIComponent('http://html2canvas.hertzen.com/test.jpg')}&responseType=text`
            );
            deepStrictEqual(xhr[0].timeout, 10);
            if (xhr[0].ontimeout) {
                xhr[0].ontimeout();
            }
            try {
                await cache.match('http://html2canvas.hertzen.com/test.jpg');
                fail('Expected result to timeout');
            } catch (e) {}
        });
    });

    it('match should return cache entry', async () => {
        const cache = createMockContext('http://example.com');
        await cache.addImage('http://example.com/test.jpg');

        if (images[0].onload) {
            images[0].onload();
        }

        const response = await cache.match('http://example.com/test.jpg');

        deepStrictEqual(response.src, 'http://example.com/test.jpg');
    });

    it('image should respect imageTimeout', async () => {
        const cache = createMockContext('http://example.com', {imageTimeout: 10});
        cache.addImage('http://example.com/test.jpg');

        try {
            await cache.match('http://example.com/test.jpg');
            fail('Expected result to timeout');
        } catch (e) {}
    });
});
