import JestMockPromise from 'jest-mock-promise';
import { Cache } from 'src/common/utils/decorators';
import fn = jest.fn;

describe('Cache decorator tests', () => {
    test('Cache must be used when method is called second time.', () => {
        const spyFn = fn();
        const returnedValue = 1;
        class CacheTestClass {
            @Cache
            public zeroParametersCacheableMethod() {
                spyFn();
                return returnedValue;
            }
        }

        const testClassInstance = new CacheTestClass();
        const firstCallResult = testClassInstance.zeroParametersCacheableMethod();
        const secondCallResult = testClassInstance.zeroParametersCacheableMethod();

        expect(spyFn.mock.calls.length).toBe(1);
        expect(firstCallResult).toBe(returnedValue);
        expect(secondCallResult).toBe(returnedValue);
    });

    test('Cache must not be used when method is called second time with other parameter.', () => {
        const spyFn = fn();
        class CacheTestClass {
            @Cache
            public oneParametersCacheableMethod(parameter: unknown) {
                spyFn();
                return parameter;
            }
        }

        const testClassInstance = new CacheTestClass();
        testClassInstance.oneParametersCacheableMethod(0);
        testClassInstance.oneParametersCacheableMethod(1);

        expect(spyFn.mock.calls.length).toBe(2);
    });

    test('Cache must be used when method is called second time with same parameter. Stored value must be correct.', () => {
        const spyFn = fn();
        const additionSeed = 1;
        const firstCallParameter = 2;
        const secondCallParameter = 3;
        class CacheTestClass {
            @Cache
            public oneParametersCacheableMethod(parameter: number) {
                spyFn();
                return additionSeed + parameter;
            }
        }

        const testClassInstance = new CacheTestClass();
        const firstCallResult = testClassInstance.oneParametersCacheableMethod(firstCallParameter);
        const secondCallResult =
            testClassInstance.oneParametersCacheableMethod(secondCallParameter);
        const spyCallsAfterSecondTimeMethodCall = spyFn.mock.calls.length;
        const thirdCallResult = testClassInstance.oneParametersCacheableMethod(firstCallParameter);
        const spyCallsAfterThirdTimeMethodCall = spyFn.mock.calls.length;

        expect(spyCallsAfterSecondTimeMethodCall).toBe(2);
        expect(spyCallsAfterThirdTimeMethodCall).toBe(2);
        expect(firstCallResult).toBe(additionSeed + firstCallParameter);
        expect(secondCallResult).toBe(additionSeed + secondCallParameter);
        expect(thirdCallResult).toBe(firstCallResult);
    });

    test('Cache decorator must works with async functions.', async () => {
        const spyFn = fn();
        const promise = new JestMockPromise();

        const returnedValue = 1;
        class CacheTestClass {
            @Cache
            public async asyncCacheableMethod() {
                spyFn();
                await promise;
                return returnedValue;
            }
        }

        const testClassInstance = new CacheTestClass();
        const firstCallPromise = testClassInstance.asyncCacheableMethod();
        const secondCallPromise = testClassInstance.asyncCacheableMethod();
        promise.resolve();

        const firstCallResult = await firstCallPromise;
        const secondCallResult = await secondCallPromise;

        expect(firstCallResult).toBe(returnedValue);
        expect(secondCallResult).toBe(returnedValue);
        expect(spyFn.mock.calls.length).toBe(1);
    });

    test('Cache decorator must works with async function which resolves before second call.', async () => {
        const spyFn = fn();
        const promise = new JestMockPromise();

        const returnedValue = 1;
        class CacheTestClass {
            @Cache
            public async asyncCacheableMethod() {
                spyFn();
                await promise;
                return returnedValue;
            }
        }

        const testClassInstance = new CacheTestClass();
        const firstCallPromise = testClassInstance.asyncCacheableMethod();
        promise.resolve();
        const secondCallPromise = testClassInstance.asyncCacheableMethod();

        const firstCallResult = await firstCallPromise;
        const secondCallResult = await secondCallPromise;

        expect(firstCallResult).toBe(returnedValue);
        expect(secondCallResult).toBe(returnedValue);
        expect(spyFn.mock.calls.length).toBe(1);
    });

    test('Cache must be used when async method is called second time with same parameter. Stored value must be correct.', async () => {
        const spyFn = fn();
        const promise = new JestMockPromise();
        const additionSeed = 1;
        const firstCallParameter = 2;
        const secondCallParameter = 3;
        class CacheTestClass {
            @Cache
            public async oneParametersAsyncCacheableMethod(parameter: number) {
                spyFn();
                const additionSeedValue = await promise;
                return additionSeedValue + parameter;
            }
        }

        const testClassInstance = new CacheTestClass();
        const firstCallPromise =
            testClassInstance.oneParametersAsyncCacheableMethod(firstCallParameter);
        const secondCallPromise =
            testClassInstance.oneParametersAsyncCacheableMethod(secondCallParameter);
        const spyCallsAfterSecondTimeMethodCall = spyFn.mock.calls.length;
        const thirdCallPromise =
            testClassInstance.oneParametersAsyncCacheableMethod(firstCallParameter);
        const spyCallsAfterThirdTimeMethodCall = spyFn.mock.calls.length;
        promise.resolve(additionSeed);

        const firstCallResult = await firstCallPromise;
        const secondCallResult = await secondCallPromise;
        const thirdCallResult = await thirdCallPromise;

        expect(spyCallsAfterSecondTimeMethodCall).toBe(2);
        expect(spyCallsAfterThirdTimeMethodCall).toBe(2);
        expect(firstCallResult).toBe(additionSeed + firstCallParameter);
        expect(secondCallResult).toBe(additionSeed + secondCallParameter);
        expect(thirdCallResult).toBe(firstCallResult);
    });

    test('Cache decorator must works with getters.', () => {
        const spyFn = fn();
        const returnedValue = 1;
        class CacheTestClass {
            @Cache
            public get cacheableGetter(): number {
                spyFn();
                return returnedValue;
            }
        }

        const testClassInstance = new CacheTestClass();
        const firstCallResult = testClassInstance.cacheableGetter;
        const secondCallResult = testClassInstance.cacheableGetter;

        expect(spyFn.mock.calls.length).toBe(1);
        expect(firstCallResult).toBe(returnedValue);
        expect(secondCallResult).toBe(returnedValue);
    });

    test('Cached value must be used before maxAge time is not up. ', () => {
        jest.useFakeTimers().setSystemTime(0);
        const spyFn = fn();
        const returnedValue = 1;
        const maxAge = 10;
        class CacheTestClass {
            @Cache({ maxAge })
            public cacheableMethod(): number {
                spyFn();
                return returnedValue;
            }
        }

        const testClassInstance = new CacheTestClass();
        const firstCallResult = testClassInstance.cacheableMethod();
        jest.useFakeTimers().setSystemTime(maxAge - 1);
        const secondCallResult = testClassInstance.cacheableMethod();

        expect(spyFn.mock.calls.length).toBe(1);
        expect(firstCallResult).toBe(returnedValue);
        expect(secondCallResult).toBe(returnedValue);
    });

    test('Cached value must not be used after maxAge time is up.', () => {
        jest.useFakeTimers().setSystemTime(0);
        const spyFn = fn();
        const returnedValue = 1;
        const maxAge = 10;
        class CacheTestClass {
            @Cache({ maxAge })
            public cacheableMethod(): number {
                spyFn();
                return returnedValue;
            }
        }

        const testClassInstance = new CacheTestClass();
        const firstCallResult = testClassInstance.cacheableMethod();
        jest.useFakeTimers().setSystemTime(maxAge + 1);
        const secondCallResult = testClassInstance.cacheableMethod();

        expect(spyFn.mock.calls.length).toBe(2);
        expect(firstCallResult).toBe(returnedValue);
        expect(secondCallResult).toBe(returnedValue);
    });

    test('Cached value must be used in async function before maxAge time is up but even if first promise has not resolved yet.', async () => {
        jest.useFakeTimers().setSystemTime(0);
        const spyFn = fn();
        const promise = new JestMockPromise();
        const returnedValue = 1;
        const maxAge = 10;
        class CacheTestClass {
            @Cache({ maxAge: 10 })
            public async cacheableMethod(): Promise<number> {
                spyFn();
                await promise;
                return returnedValue;
            }
        }

        const testClassInstance = new CacheTestClass();
        const firstCallPromise = testClassInstance.cacheableMethod();
        jest.useFakeTimers().setSystemTime(maxAge - 1);
        const secondCallPromise = testClassInstance.cacheableMethod();
        promise.resolve();
        const firstCallResult = await firstCallPromise;
        const secondCallResult = await secondCallPromise;

        expect(spyFn.mock.calls.length).toBe(1);
        expect(firstCallResult).toBe(returnedValue);
        expect(secondCallResult).toBe(returnedValue);
    });

    test('Cached value must not be used in async function after maxAge time is up but even if first promise has not resolved yet.', async () => {
        jest.useFakeTimers().setSystemTime(0);
        const spyFn = fn();
        const promise = new JestMockPromise();
        const returnedValue = 1;
        const maxAge = 10;
        class CacheTestClass {
            @Cache({ maxAge: 10 })
            public async cacheableMethod(): Promise<number> {
                spyFn();
                await promise;
                return returnedValue;
            }
        }

        const testClassInstance = new CacheTestClass();
        const firstCallPromise = testClassInstance.cacheableMethod();
        jest.useFakeTimers().setSystemTime(maxAge + 1);
        const secondCallPromise = testClassInstance.cacheableMethod();
        promise.resolve();
        const firstCallResult = await firstCallPromise;
        const secondCallResult = await secondCallPromise;

        expect(spyFn.mock.calls.length).toBe(2);
        expect(firstCallResult).toBe(returnedValue);
        expect(secondCallResult).toBe(returnedValue);
    });

    test('Conditional cache must not be saved if flag is passed.', () => {
        const spyFn = fn();
        const returnedValue = 1;
        class CacheTestClass {
            @Cache({ conditionalCache: true })
            public conditionalCacheableMethod() {
                spyFn();
                return {
                    notSave: true,
                    value: returnedValue
                };
            }
        }

        const testClassInstance = new CacheTestClass();
        const firstCallResult = testClassInstance.conditionalCacheableMethod();
        const secondCallResult = testClassInstance.conditionalCacheableMethod();

        expect(spyFn.mock.calls.length).toBe(2);
        expect(firstCallResult).toBe(returnedValue);
        expect(secondCallResult).toBe(returnedValue);
    });

    test('Conditional cache must be saved if flag is not passed.', () => {
        const spyFn = fn();
        const returnedValue = 1;
        class CacheTestClass {
            @Cache({ conditionalCache: true })
            public conditionalCacheableMethod() {
                spyFn();
                return {
                    notSave: false,
                    value: returnedValue
                };
            }
        }

        const testClassInstance = new CacheTestClass();
        const firstCallResult = testClassInstance.conditionalCacheableMethod();
        const secondCallResult = testClassInstance.conditionalCacheableMethod();

        expect(spyFn.mock.calls.length).toBe(1);
        expect(firstCallResult).toBe(returnedValue);
        expect(secondCallResult).toBe(returnedValue);
    });

    test('Conditional cache must be saved if flag is not passed in async method.', async () => {
        const spyFn = fn();
        const promise = new JestMockPromise();
        const returnedValue = 1;
        class CacheTestClass {
            @Cache({ conditionalCache: true })
            public async conditionalAsyncCacheableMethod() {
                spyFn();
                await promise;
                return {
                    notSave: false,
                    value: returnedValue
                };
            }
        }

        const testClassInstance = new CacheTestClass();
        const firstCallPromise = testClassInstance.conditionalAsyncCacheableMethod();
        promise.resolve();
        const secondCallPromise = testClassInstance.conditionalAsyncCacheableMethod();
        const firstCallResult = await firstCallPromise;
        const secondCallResult = await secondCallPromise;

        expect(spyFn.mock.calls.length).toBe(1);
        expect(firstCallResult).toBe(returnedValue);
        expect(secondCallResult).toBe(returnedValue);
    });

    test('Conditional cache must be saved before async method resolves, but must not be saved after promise resolving if fleg is passed.', async () => {
        const spyFn = fn();
        const promise = new JestMockPromise();
        const returnedValue = 1;
        class CacheTestClass {
            @Cache({ conditionalCache: true })
            public async conditionalAsyncCacheableMethod() {
                spyFn();
                await promise;
                return {
                    notSave: true,
                    value: returnedValue
                };
            }
        }

        const testClassInstance = new CacheTestClass();
        const firstCallPromise = testClassInstance.conditionalAsyncCacheableMethod();
        const secondCallPromise = testClassInstance.conditionalAsyncCacheableMethod();
        const spyCallsNumberAfterSecondMethodCall = spyFn.mock.calls.length;
        promise.resolve();
        const firstCallResult = await firstCallPromise;
        const secondCallResult = await secondCallPromise;

        const thirdCallPromise = testClassInstance.conditionalAsyncCacheableMethod();
        const spyCallsNumberAfterThirdMethodCall = spyFn.mock.calls.length;
        promise.resolve();
        const thirdCallResult = await thirdCallPromise;

        expect(spyCallsNumberAfterSecondMethodCall).toBe(1);
        expect(spyCallsNumberAfterThirdMethodCall).toBe(2);
        expect(firstCallResult).toBe(returnedValue);
        expect(secondCallResult).toBe(returnedValue);
        expect(thirdCallResult).toBe(returnedValue);
    });
});
