import type { LoadSignal, LoadSnapshot } from '@crawlee/core';
import { AutoscaledPool } from '@crawlee/core';
import { sleep } from '@crawlee/utils';

import log from '@apify/log';

describe('AutoscaledPool', () => {
    let logLevel: number;
    beforeAll(() => {
        logLevel = log.getLevel();
        log.setLevel(log.LEVELS.ERROR);
    });

    afterAll(() => {
        log.setLevel(logLevel);
    });

    test('should work with concurrency 1', async () => {
        const range = [...Array(10).keys()];
        const result: number[] = [];

        let isFinished = false;

        const runTaskFunction = async () => {
            if (range.length === 1) {
                isFinished = true;
            }

            return new Promise((resolve) => {
                const item = range.shift()!;
                result.push(item);
                setTimeout(resolve, 5);
            });
        };

        const pool = new AutoscaledPool({
            minConcurrency: 1,
            maxConcurrency: 1,
            runTaskFunction,
            isFinishedFunction: async () => Promise.resolve(isFinished),
            isTaskReadyFunction: async () => Promise.resolve(!isFinished),
        });
        await pool.run();

        expect(result).toEqual([...Array(10).keys()]);
    });

    test('should work with concurrency 10', async () => {
        const range = [...Array(100).keys()];
        const result: number[] = [];

        let isFinished = false;

        const runTaskFunction = async () => {
            if (range.length === 1) {
                isFinished = true;
            }

            return new Promise((resolve) => {
                const item = range.shift()!;
                result.push(item);
                setTimeout(resolve, 5);
            });
        };

        const pool = new AutoscaledPool({
            minConcurrency: 10,
            maxConcurrency: 10,
            runTaskFunction,
            isFinishedFunction: async () => Promise.resolve(isFinished),
            isTaskReadyFunction: async () => Promise.resolve(!isFinished),
        });

        await pool.run();

        expect(result).toEqual([...Array(100).keys()]);
    });

    test('enables setting concurrency', async () => {
        const range = [...Array(100).keys()];
        const result: number[] = [];

        let isFinished = false;

        const runTaskFunction = async () => {
            if (range.length === 1) {
                isFinished = true;
            }

            return new Promise((resolve) => {
                const item = range.shift()!;
                result.push(item);
                setTimeout(resolve, 5);
            });
        };

        const pool = new AutoscaledPool({
            // Test initial concurrency setting
            minConcurrency: 3,
            maxConcurrency: 13,
            desiredConcurrency: 9,
            runTaskFunction,
            isFinishedFunction: async () => isFinished,
            isTaskReadyFunction: async () => !isFinished,
        });

        expect(pool.minConcurrency).toBe(3);
        expect(pool.maxConcurrency).toBe(13);
        expect(pool.desiredConcurrency).toBe(9);

        const promise = pool.run();

        // Test setting concurrency
        pool.minConcurrency = 4;
        pool.maxConcurrency = 14;
        pool.desiredConcurrency = 7;

        expect(pool.minConcurrency).toBe(4);
        expect(pool.maxConcurrency).toBe(14);
        expect(pool.desiredConcurrency).toBe(7);

        await promise;

        expect(result).toEqual([...Array(100).keys()]);
    });

    describe('should scale correctly', () => {
        class MockSystemStatus {
            okNow: boolean;
            okLately: boolean;
            getCurrentStatus: () => { isSystemIdle: boolean };
            getHistoricalStatus: () => { isSystemIdle: boolean };

            constructor(okNow: boolean, okLately: boolean) {
                this.okNow = okNow;
                this.okLately = okLately;
                this.getCurrentStatus = () => ({ isSystemIdle: this.okNow });
                this.getHistoricalStatus = () => ({ isSystemIdle: this.okLately });
            }
        }

        let pool: AutoscaledPool;
        let systemStatus: MockSystemStatus;
        const cb = () => {};
        beforeEach(() => {
            systemStatus = new MockSystemStatus(true, true);
            pool = new AutoscaledPool({
                minConcurrency: 1,
                maxConcurrency: 100,
                runTaskFunction: async () => {},
                isFinishedFunction: async () => false,
                isTaskReadyFunction: async () => true,
            });
            // @ts-expect-error Mock
            pool.systemStatus = systemStatus;
        });

        test('works with low values', () => {
            // @ts-expect-error Calling private method
            pool._autoscale(cb);
            expect(pool.desiredConcurrency).toBe(2);

            // @ts-expect-error Calling private method
            pool._autoscale(cb);
            expect(pool.desiredConcurrency).toBe(2); // because currentConcurrency is not high enough;

            // @ts-expect-error Overwriting readonly private prop
            pool._currentConcurrency = 2;
            // @ts-expect-error Calling private method
            pool._autoscale(cb);
            expect(pool.desiredConcurrency).toBe(3);

            systemStatus.okNow = false; // this should have no effect
            // @ts-expect-error Overwriting readonly private prop
            pool._currentConcurrency = 3;
            // @ts-expect-error Calling private method
            pool._autoscale(cb);
            expect(pool.desiredConcurrency).toBe(4);

            systemStatus.okLately = false;
            // @ts-expect-error Calling private method
            pool._autoscale(cb);
            expect(pool.desiredConcurrency).toBe(3);
        });

        test('works with high values', () => {
            // Should not scale because current concurrency is too low.
            pool.desiredConcurrency = 50;
            // @ts-expect-error Overwriting readonly private prop
            pool._currentConcurrency = Math.floor(pool.desiredConcurrency * pool.desiredConcurrencyRatio) - 1;
            systemStatus.okLately = true;
            // @ts-expect-error Calling private method
            pool._autoscale(cb);
            expect(pool.desiredConcurrency).toBe(50);

            // Should scale because we bumped up current concurrency.
            // @ts-expect-error Overwriting readonly private prop
            pool._currentConcurrency = Math.floor(pool.desiredConcurrency * pool.desiredConcurrencyRatio);
            // @ts-expect-error Accessing private prop
            let newConcurrency = pool.desiredConcurrency + Math.ceil(pool.desiredConcurrency * pool.scaleUpStepRatio);
            // @ts-expect-error Calling private method
            pool._autoscale(cb);
            expect(pool.desiredConcurrency).toEqual(newConcurrency);

            // Should scale down.
            systemStatus.okLately = false;
            // @ts-expect-error Accessing private prop
            newConcurrency = pool.desiredConcurrency - Math.ceil(pool.desiredConcurrency * pool.scaleDownStepRatio);
            // @ts-expect-error Calling private method
            pool._autoscale(cb);
            expect(pool.desiredConcurrency).toEqual(newConcurrency);
        });

        test('works at minConcurrency when currently overloaded', async () => {
            let limit = 5;
            let concurrencyLog: number[] = [];
            let count = 0;
            // @ts-expect-error Overwriting readonly private prop
            pool.systemStatus.okNow = false;
            // @ts-expect-error Overwriting readonly private prop
            pool.runTaskFunction = async () => {
                await sleep(10);
                count++;
            };
            // @ts-expect-error Overwriting readonly private prop
            pool.isFinishedFunction = async () => count >= limit;
            // @ts-expect-error Overwriting readonly private prop
            pool.isTaskReadyFunction = async () => count < limit;
            pool.desiredConcurrency = 10;
            // @ts-expect-error Overwriting readonly private prop
            pool._currentConcurrency = pool.currentConcurrency;

            Object.defineProperty(pool, 'currentConcurrency', {
                get() {
                    return this._currentConcurrency;
                },
                set(v) {
                    concurrencyLog.push(v);
                    this._currentConcurrency = v;
                },
            });

            expect(pool.currentConcurrency).toBe(0);

            await pool.run();
            expect(concurrencyLog.some((n) => n > 1)).toBe(false);

            limit = 50;
            concurrencyLog = [];
            count = 0;
            pool.minConcurrency = 5;

            await pool.run();
            expect(concurrencyLog.some((n) => n > 5)).toBe(false);
        });
    });

    describe('should throw', () => {
        // Turn off unnecessary error logging.
        let originalLevel: number;
        beforeAll(() => {
            originalLevel = log.getLevel();
            log.setLevel(log.LEVELS.OFF);
        });

        afterAll(() => {
            log.setLevel(originalLevel);
        });

        test('when some of the promises throws', async () => {
            let counter = 0;
            const runTaskFunction = async () => {
                counter++;
                await sleep(1);
                if (counter > 20) throw new Error('some-promise-error');
            };

            const pool = new AutoscaledPool({
                maxConcurrency: 5,
                minConcurrency: 5,
                runTaskFunction,
                isFinishedFunction: async () => counter > 200,
                isTaskReadyFunction: async () => true,
            });

            await expect(pool.run()).rejects.toThrow('some-promise-error');
        });

        test('when runTaskFunction throws', async () => {
            const runTaskFunction = async () => {
                await sleep(3);
                throw new Error('some-runtask-error');
            };

            const pool = new AutoscaledPool({
                maxConcurrency: 1,
                runTaskFunction,
                isFinishedFunction: async () => false,
                isTaskReadyFunction: async () => true,
            });

            await expect(pool.run()).rejects.toThrow('some-runtask-error');
        });

        test('when isFinishedFunction throws', async () => {
            let count = 0;
            const pool = new AutoscaledPool({
                maxConcurrency: 1,
                runTaskFunction: async () => {
                    count++;
                },
                isFinishedFunction: async () => {
                    throw new Error('some-finished-error');
                },
                isTaskReadyFunction: async () => {
                    return count < 1;
                },
            });

            await expect(pool.run()).rejects.toThrow('some-finished-error');
        });

        test('when isTaskReadyFunction throws', async () => {
            let count = 0;
            const pool = new AutoscaledPool({
                maxConcurrency: 1,
                runTaskFunction: async () => {
                    count++;
                },
                isFinishedFunction: async () => false,
                isTaskReadyFunction: async () => {
                    if (count > 1) throw new Error('some-ready-error');
                    else return true;
                },
            });

            await expect(pool.run()).rejects.toThrow('some-ready-error');
        });
    });

    test('should not handle tasks added after isFinishedFunction returned true', async () => {
        const isFinished = async () => count > 10;
        let count = 0;

        // Run the pool and close it after 3s.
        const pool = new AutoscaledPool({
            minConcurrency: 3,
            runTaskFunction: async () =>
                sleep(1).then(() => {
                    count++;
                }),
            isFinishedFunction: isFinished,
            isTaskReadyFunction: async () => !(await isFinished()),
        });

        // @ts-expect-error Overwriting readonly private prop
        pool.maybeRunIntervalMillis = 5;

        await pool.run();
        await sleep(10);
        expect(count).toBeGreaterThanOrEqual(11);
        // Check finished tasks.
        expect(count).toBeLessThanOrEqual(13);
    });

    test('should break and resume when the task queue is empty for a while', async () => {
        const finished: number[] = [];
        let isFinished = false;
        let isTaskReady = true;

        let counter = 0;
        const pool = new AutoscaledPool({
            maxConcurrency: 1,
            runTaskFunction: async () => {
                await sleep(1);
                if (counter === 10) {
                    isTaskReady = false;
                    setTimeout(() => {
                        isTaskReady = true;
                    }, 10);
                }
                if (counter === 19) {
                    isTaskReady = false;
                    isFinished = true;
                }
                counter++;
                finished.push(Date.now());
            },
            isFinishedFunction: async () => isFinished,
            isTaskReadyFunction: async () => !isFinished && isTaskReady,
        });
        // @ts-expect-error Overwriting readonly private prop
        pool.maybeRunIntervalMillis = 1;
        await pool.run();

        // Check finished tasks.
        expect(finished).toHaveLength(20);
        expect(finished[11] - finished[10]).toBeGreaterThan(9);
    });

    test('should work with loggingIntervalSecs = null', async () => {
        const pool = new AutoscaledPool({
            minConcurrency: 1,
            maxConcurrency: 100,
            runTaskFunction: async () => Promise.resolve(),
            isFinishedFunction: async () => Promise.resolve(false),
            isTaskReadyFunction: async () => Promise.resolve(true),
            loggingIntervalSecs: null,
        });
        // @ts-expect-error Calling private method
        pool._autoscale(() => {});
        expect(pool.desiredConcurrency).toBe(2);
    });

    test('should abort', async () => {
        let finished = false;
        let aborted = false;
        const pool = new AutoscaledPool({
            runTaskFunction: async () => {
                if (!aborted) {
                    await pool.abort();
                    aborted = true;
                }
            },
            isFinishedFunction: async () => {
                finished = true;
                return true;
            },
            isTaskReadyFunction: async () => !aborted,
        });
        await pool.run();
        expect(finished).toBe(false);
    });

    test('should only finish after tasks complete', async () => {
        let started = false;
        let completed = false;

        const pool = new AutoscaledPool({
            runTaskFunction: async () => {
                started = true;
                await sleep(100);
                completed = true;
            },

            isFinishedFunction: async () => {
                return started;
            },

            isTaskReadyFunction: async () => {
                return !started;
            },
        });

        await pool.run();
        expect(started).toBe(true);
        expect(completed).toBe(true);
    });

    test('should pause and resume', async () => {
        let count = 0;
        const results: number[] = [];
        let pauseResolve: (value: unknown) => void;
        const pausePromise = new Promise((res) => {
            pauseResolve = res;
        });

        const pool = new AutoscaledPool({
            maybeRunIntervalSecs: 0.01,
            minConcurrency: 10,
            runTaskFunction: async () => {
                results.push(count++);
                if (count === 20) {
                    void pool.pause().then(pauseResolve);
                }
            },
            isFinishedFunction: async () => !(count < 50),
            isTaskReadyFunction: async () => count < 50,
        });

        let finished = false;
        const runPromise = pool.run();
        void runPromise.then(() => {
            finished = true;
        });
        await pausePromise;
        expect(count).toBe(20);
        expect(finished).toBe(false);
        expect(results).toHaveLength(count);
        results.forEach((r, i) => expect(r).toEqual(i));

        pool.resume();
        await runPromise;
        expect(count).toBe(50);
        expect(finished).toBe(true);
        expect(results).toHaveLength(count);
        results.forEach((r, i) => expect(r).toEqual(i));
    });

    test('should timeout after taskTimeoutSecs', async () => {
        const runTaskFunction = async () => {
            await sleep(1e3);
            return 1;
        };

        const pool = new AutoscaledPool({
            minConcurrency: 1,
            maxConcurrency: 1,
            runTaskFunction,
            taskTimeoutSecs: 0.1,
            isFinishedFunction: async () => false,
            isTaskReadyFunction: async () => true,
        });

        const now = Date.now();
        await expect(pool.run()).rejects.toThrow('runTaskFunction timed out after 0.1 seconds.');
        expect(Date.now() - now).toBeGreaterThanOrEqual(100);
    });

    test('should not timeout if taskTimeoutSecs === 0', async () => {
        let finished = false;

        const runTaskFunction = async () => {
            await sleep(1e3);
            finished = true;
            return 1;
        };
        const pool = new AutoscaledPool({
            minConcurrency: 1,
            maxConcurrency: 1,
            runTaskFunction,
            taskTimeoutSecs: 0,
            isFinishedFunction: async () => finished,
            isTaskReadyFunction: async () => !finished,
        });

        const now = Date.now();
        await expect(pool.run()).resolves.toBeUndefined();
        expect(Date.now() - now).toBeGreaterThanOrEqual(1e3);
    }, 10e3);

    test('should not timeout if taskTimeoutSecs not explicitly set', async () => {
        let finished = false;

        const runTaskFunction = async () => {
            await sleep(1e3);
            finished = true;
            return 1;
        };

        const pool = new AutoscaledPool({
            minConcurrency: 1,
            maxConcurrency: 1,
            runTaskFunction,
            isFinishedFunction: async () => finished,
            isTaskReadyFunction: async () => !finished,
        });

        const now = Date.now();
        await expect(pool.run()).resolves.toBeUndefined();
        expect(Date.now() - now).toBeGreaterThanOrEqual(1e3);
    }, 10e3);

    describe('custom load signals', () => {
        /** Creates a minimal fake LoadSignal with static pre-seeded snapshots. */
        function createFakeLoadSignal(name: string, { overloadedRatio = 0.3, isOverloaded = false } = {}): LoadSignal {
            const now = Date.now();
            const snapshots: LoadSnapshot[] = Array.from({ length: 5 }, (_, i) => ({
                createdAt: new Date(now - (5 - i) * 100),
                isOverloaded,
            }));

            return {
                name,
                overloadedRatio,
                async start() {},
                async stop() {},
                getSample(sampleDurationMillis?: number) {
                    if (!sampleDurationMillis) return snapshots;
                    const cutoff = Date.now() - sampleDurationMillis;
                    return snapshots.filter((s) => +s.createdAt >= cutoff);
                },
            };
        }

        test('overloaded signal prevents concurrency from scaling up', async () => {
            const signal = createFakeLoadSignal('proxyHealth', { isOverloaded: true });

            let count = 0;
            const pool = new AutoscaledPool({
                minConcurrency: 1,
                maxConcurrency: 10,
                runTaskFunction: async () => {
                    count++;
                    await sleep(10);
                },
                isFinishedFunction: async () => count >= 50,
                isTaskReadyFunction: async () => count < 50,
                systemStatusOptions: { loadSignals: [signal] },
            });

            await pool.run();

            expect(pool.desiredConcurrency).toBe(1);
        });

        test('signal info appears in SystemStatus.getCurrentStatus()', async () => {
            const signal = createFakeLoadSignal('navTimeout', { overloadedRatio: 0.2, isOverloaded: true });

            let count = 0;
            const pool = new AutoscaledPool({
                minConcurrency: 1,
                maxConcurrency: 10,
                runTaskFunction: async () => {
                    count++;
                    await sleep(10);
                },
                isFinishedFunction: async () => count >= 10,
                isTaskReadyFunction: async () => count < 10,
                systemStatusOptions: { loadSignals: [signal] },
            });

            // @ts-expect-error Accessing private prop
            const status = pool.systemStatus.getCurrentStatus();
            expect(status.loadSignalInfo?.navTimeout?.isOverloaded).toBe(true);

            await pool.run();
        });
    });
});
