import moize from '../src';

type Type = {
    number: number;
};

const method = (one: number, two: Type) => one + two.number;
const promiseMethodResolves = (one: number, two: Type) =>
    new Promise((resolve) => setTimeout(() => resolve(one + two.number), 1000));
const promiseMethodRejects =
    // eslint-disable-next-line @typescript-eslint/no-unused-vars
    (one: number, two: Type) =>
        new Promise((resolve, reject) =>
            setTimeout(() => reject(new Error('boom')), 1000)
        );

describe('moize.updateCacheForKey', () => {
    describe('success', () => {
        it('will refresh the cache', () => {
            const moized = moize.maxSize(2)(method, {
                updateCacheForKey(args) {
                    return args[1].number % 2 === 0;
                },
            });

            const mutated = { number: 5 };

            const result = moized(6, mutated);

            expect(result).toBe(11);

            mutated.number = 11;

            const mutatedResult = moized(6, mutated);

            // Result was not recalculated because `updateCacheForKey` returned `false` and the values are
            // seen as unchanged.
            expect(mutatedResult).toBe(result);

            mutated.number = 10;

            const refreshedResult = moized(6, mutated);

            // Result was recalculated because `updateCacheForKey` returned `true`.
            expect(refreshedResult).not.toBe(result);
            expect(refreshedResult).toBe(16);

            const { keys, values } = moized.cacheSnapshot;

            expect(keys).toEqual([[6, mutated]]);
            expect(values).toEqual([16]);
        });

        it('will refresh the cache based on external values', async () => {
            const mockMethod = jest.fn(method);

            let lastUpdate = Date.now();

            const moized = moize.maxSize(2)(mockMethod, {
                updateCacheForKey() {
                    const now = Date.now();
                    const last = lastUpdate;

                    lastUpdate = now;

                    return last + 1000 < now;
                },
            });

            const mutated = { number: 5 };

            moized(6, mutated);
            moized(6, mutated);
            moized(6, mutated);

            expect(mockMethod).toHaveBeenCalledTimes(1);

            await new Promise((resolve) => setTimeout(resolve, 2000));

            moized(6, mutated);

            expect(mockMethod).toHaveBeenCalledTimes(2);
        });

        it('will refresh the cache when used with promises', async () => {
            const moized = moize.maxSize(2)(promiseMethodResolves, {
                isPromise: true,
                updateCacheForKey(args) {
                    return args[1].number % 2 === 0;
                },
            });

            const mutated = { number: 5 };

            const result = await moized(6, mutated);

            expect(result).toBe(11);

            mutated.number = 11;

            const mutatedResult = await moized(6, mutated);

            // Result was not recalculated because `updateCacheForKey` returned `false` and the values are
            // seen as unchanged.
            expect(mutatedResult).toBe(result);

            mutated.number = 10;

            const refreshedResult = await moized(6, mutated);

            // Result was recalculated because `updateCacheForKey` returned `true`.
            expect(refreshedResult).not.toBe(result);
            expect(refreshedResult).toBe(16);

            const { keys, values } = moized.cacheSnapshot;

            expect(keys).toEqual([[6, mutated]]);
            expect(values).toEqual([Promise.resolve(16)]);
        });

        it('will refresh the cache when used with custom key transformers', () => {
            type ConditionalIncrement = {
                force?: boolean;
            };

            let count = 0;

            // eslint-disable-next-line @typescript-eslint/no-unused-vars
            const increment = (_?: ConditionalIncrement) => ++count;

            const moized = moize.maxSize(2)(increment, {
                isSerialized: true,
                updateCacheForKey: (args: [ConditionalIncrement]) =>
                    args[0] && args[0].force === true,
                serializer: () => ['always same'],
            });

            expect(moized()).toBe(1);
            expect(moized()).toBe(1);
            expect(moized({ force: true })).toBe(2);
            expect(moized()).toBe(2);
        });

        it('will refresh the cache with shorthand', () => {
            const moized = moize.updateCacheForKey(
                (args) => args[1].number % 2 === 0
            )(method);

            const mutated = { number: 5 };

            const result = moized(6, mutated);

            expect(result).toBe(11);

            mutated.number = 11;

            const mutatedResult = moized(6, mutated);

            // Result was not recalculated because `updateCacheForKey` returned `false` and the values are
            // seen as unchanged.
            expect(mutatedResult).toBe(result);

            mutated.number = 10;

            const refreshedResult = moized(6, mutated);

            // Result was recalculated because `updateCacheForKey` returned `true`.
            expect(refreshedResult).not.toBe(result);
            expect(refreshedResult).toBe(16);

            const { keys, values } = moized.cacheSnapshot;

            expect(keys).toEqual([[6, mutated]]);
            expect(values).toEqual([16]);
        });

        it('will refresh the cache with composed shorthand', () => {
            const moizer = moize.compose(
                moize.maxSize(2),
                moize.updateCacheForKey((args) => args[1].number % 2 === 0)
            );
            const moized = moizer(method);

            const mutated = { number: 5 };

            const result = moized(6, mutated);

            expect(result).toBe(11);

            mutated.number = 11;

            const mutatedResult = moized(6, mutated);

            // Result was not recalculated because `updateCacheForKey` returned `false` and the values are
            // seen as unchanged.
            expect(mutatedResult).toBe(result);

            mutated.number = 10;

            const refreshedResult = moized(6, mutated);

            // Result was recalculated because `updateCacheForKey` returned `true`.
            expect(refreshedResult).not.toBe(result);
            expect(refreshedResult).toBe(16);

            const { keys, values } = moized.cacheSnapshot;

            expect(keys).toEqual([[6, mutated]]);
            expect(values).toEqual([16]);
        });
    });

    describe('fail', () => {
        it('surfaces the error if the function fails', () => {
            const moized = moize.maxSize(2)(
                // eslint-disable-next-line @typescript-eslint/no-unused-vars
                (_1: number, _2: Type) => {
                    throw new Error('boom');
                },
                {
                    updateCacheForKey(args) {
                        return args[1].number % 2 === 0;
                    },
                }
            );

            const mutated = { number: 5 };

            expect(() => moized(6, mutated)).toThrow(new Error('boom'));
        });

        it('surfaces the error if the promise rejects', async () => {
            const moized = moize.maxSize(2)(promiseMethodRejects, {
                isPromise: true,
                updateCacheForKey(args) {
                    return args[1].number % 2 === 0;
                },
            });

            const mutated = { number: 5 };

            await expect(moized(6, mutated)).rejects.toEqual(new Error('boom'));
        });

        it('should have nothing in cache if promise is rejected and key was never present', async () => {
            const moized = moize.maxSize(2)(promiseMethodRejects, {
                isPromise: true,
                updateCacheForKey(args) {
                    return args[1].number % 2 === 0;
                },
            });

            const mutated = { number: 5 };

            await expect(moized(6, mutated)).rejects.toEqual(new Error('boom'));

            expect(moized.keys()).toEqual([]);
            expect(moized.values()).toEqual([]);
        });

        // For some reason, this is causing `jest` to crash instead of handle the rejection
        it.skip('should have nothing in cache if promise is rejected and key was present', async () => {
            const moized = moize.maxSize(2)(promiseMethodRejects, {
                isPromise: true,
                updateCacheForKey(args) {
                    return args[1].number % 2 === 0;
                },
            });

            const mutated = { number: 5 };

            moized.set([6, mutated], Promise.resolve(11));

            expect(moized.get([6, mutated])).toEqual(Promise.resolve(11));

            mutated.number = 10;

            await expect(moized(6, mutated)).rejects.toEqual(new Error('boom'));

            expect(moized.keys()).toEqual([]);
            expect(moized.values()).toEqual([]);
        });
    });

    describe('infrastructure', () => {
        it('should have all the static properties of a standard moized method', () => {
            const moized = moize.maxSize(2)(promiseMethodResolves, {
                updateCacheForKey(args) {
                    return args[1].number % 2 === 0;
                },
            });
            const standardMoized = moize.maxSize(2)(promiseMethodResolves);

            expect(Object.getOwnPropertyNames(moized)).toEqual(
                Object.getOwnPropertyNames(standardMoized)
            );
        });
    });

    describe('edge cases', () => {
        it('should retain the original function name', () => {
            function myNamedFunction() {}

            const memoized = moize(myNamedFunction, {
                updateCacheForKey: () => false,
            });

            expect(memoized.name).toBe('moized(myNamedFunction)');
        });
    });
});
