import { ProxyConfiguration, Request } from '@crawlee/core';

const sessionId = 538909250932;

describe('ProxyConfiguration', () => {
    test('newUrl() should return proxy URL', async () => {
        const proxyConfiguration = new ProxyConfiguration({ proxyUrls: ['http://proxy.com:1111'] });
        expect(proxyConfiguration).toBeInstanceOf(ProxyConfiguration);
        expect(await proxyConfiguration.newUrl(sessionId)).toBe('http://proxy.com:1111');
    });

    test('newProxyInfo() should return ProxyInfo object', async () => {
        const proxyConfiguration = new ProxyConfiguration({ proxyUrls: ['http://proxy.com:1111'] });
        const url = 'http://proxy.com:1111';

        const proxyInfo = {
            sessionId: `${sessionId}`,
            url,
            hostname: 'proxy.com',
            username: '',
            password: '',
            port: '1111',
        };
        expect(await proxyConfiguration.newProxyInfo(sessionId)).toEqual(proxyInfo);
    });

    test('newProxyInfo() works with special characters', async () => {
        const url = 'http://user%40name:pass%40word@proxy.com:1111';
        const proxyConfiguration = new ProxyConfiguration({ proxyUrls: [url] });

        const proxyInfo = {
            sessionId: `${sessionId}`,
            url,
            hostname: 'proxy.com',
            username: 'user@name',
            password: 'pass@word',
            port: '1111',
        };
        expect(await proxyConfiguration.newProxyInfo(sessionId)).toEqual(proxyInfo);
    });

    test('should throw on invalid newUrlFunction', async () => {
        const newUrlFunction = () => {
            return 'http://proxy.com:1111*invalid_url';
        };
        const proxyConfiguration = new ProxyConfiguration({
            newUrlFunction,
        });
        try {
            await proxyConfiguration.newUrl();
            throw new Error('wrong error');
        } catch (err) {
            expect((err as Error).message).toMatch('The provided newUrlFunction did not return');
        }
    });

    test('newUrlFunction should correctly generate URLs', async () => {
        const customUrls = [
            'http://proxy.com:1111',
            'http://proxy.com:2222',
            'http://proxy.com:3333',
            'http://proxy.com:4444',
            'http://proxy.com:5555',
            'http://proxy.com:6666',
        ];
        const newUrlFunction = () => {
            return customUrls.pop() ?? null;
        };
        const proxyConfiguration = new ProxyConfiguration({
            newUrlFunction,
        });

        // through newUrl()
        expect(await proxyConfiguration.newUrl()).toEqual('http://proxy.com:6666');
        expect(await proxyConfiguration.newUrl()).toEqual('http://proxy.com:5555');
        expect(await proxyConfiguration.newUrl()).toEqual('http://proxy.com:4444');

        // through newProxyInfo()
        expect((await proxyConfiguration.newProxyInfo())!.url).toEqual('http://proxy.com:3333');
        expect((await proxyConfiguration.newProxyInfo())!.url).toEqual('http://proxy.com:2222');
        expect((await proxyConfiguration.newProxyInfo())!.url).toEqual('http://proxy.com:1111');
    });

    test('async newUrlFunction should work correctly', async () => {
        const customUrls = [
            'http://proxy.com:1111',
            'http://proxy.com:2222',
            'http://proxy.com:3333',
            'http://proxy.com:4444',
            'http://proxy.com:5555',
            'http://proxy.com:6666',
        ];
        const newUrlFunction = async () => {
            await new Promise((r) => setTimeout(r, 5));
            return customUrls.pop() ?? null;
        };
        const proxyConfiguration = new ProxyConfiguration({
            newUrlFunction,
        });

        // through newUrl()
        expect(await proxyConfiguration.newUrl()).toEqual('http://proxy.com:6666');
        expect(await proxyConfiguration.newUrl()).toEqual('http://proxy.com:5555');
        expect(await proxyConfiguration.newUrl()).toEqual('http://proxy.com:4444');

        // through newProxyInfo()
        expect((await proxyConfiguration.newProxyInfo())!.url).toEqual('http://proxy.com:3333');
        expect((await proxyConfiguration.newProxyInfo())!.url).toEqual('http://proxy.com:2222');
        expect((await proxyConfiguration.newProxyInfo())!.url).toEqual('http://proxy.com:1111');
    });

    describe('With proxyUrls options', () => {
        test('should rotate custom URLs correctly', async () => {
            const proxyConfiguration = new ProxyConfiguration({
                proxyUrls: ['http://proxy.com:1111', 'http://proxy.com:2222', 'http://proxy.com:3333'],
            });

            // @ts-expect-error private property
            const proxyUrls = proxyConfiguration.proxyUrls!;
            expect(await proxyConfiguration.newUrl()).toEqual(proxyUrls[0]);
            expect(await proxyConfiguration.newUrl()).toEqual(proxyUrls[1]);
            expect(await proxyConfiguration.newUrl()).toEqual(proxyUrls[2]);
            expect(await proxyConfiguration.newUrl()).toEqual(proxyUrls[0]);
            expect(await proxyConfiguration.newUrl()).toEqual(proxyUrls[1]);
            expect(await proxyConfiguration.newUrl()).toEqual(proxyUrls[2]);
        });

        test('newProxyInfo() should return correctly rotated URL', async () => {
            const proxyConfiguration = new ProxyConfiguration({
                proxyUrls: ['http://proxy.com:1111', 'http://proxy.com:2222', 'http://proxy.com:3333'],
            });

            // @ts-expect-error TODO private property?
            const proxyUrls = proxyConfiguration.proxyUrls!;
            expect((await proxyConfiguration.newProxyInfo())!.url).toEqual(proxyUrls[0]);
            expect((await proxyConfiguration.newProxyInfo())!.url).toEqual(proxyUrls[1]);
            expect((await proxyConfiguration.newProxyInfo())!.url).toEqual(proxyUrls[2]);
            expect((await proxyConfiguration.newProxyInfo())!.url).toEqual(proxyUrls[0]);
            expect((await proxyConfiguration.newProxyInfo())!.url).toEqual(proxyUrls[1]);
            expect((await proxyConfiguration.newProxyInfo())!.url).toEqual(proxyUrls[2]);
        });

        test('should rotate custom URLs with sessions correctly', async () => {
            const sessions = ['session_01', 'session_02', 'session_03', 'session_04', 'session_05', 'session_06'];
            const proxyConfiguration = new ProxyConfiguration({
                proxyUrls: ['http://proxy.com:1111', 'http://proxy.com:2222', 'http://proxy.com:3333'],
            });

            // @ts-expect-error TODO private property?
            const proxyUrls = proxyConfiguration.proxyUrls!;
            // should use same proxy URL
            expect(await proxyConfiguration.newUrl(sessions[0])).toEqual(proxyUrls[0]);
            expect(await proxyConfiguration.newUrl(sessions[0])).toEqual(proxyUrls[0]);
            expect(await proxyConfiguration.newUrl(sessions[0])).toEqual(proxyUrls[0]);

            // should rotate different proxies
            expect(await proxyConfiguration.newUrl(sessions[1])).toEqual(proxyUrls[1]);
            expect(await proxyConfiguration.newUrl(sessions[2])).toEqual(proxyUrls[2]);
            expect(await proxyConfiguration.newUrl(sessions[3])).toEqual(proxyUrls[0]);
            expect(await proxyConfiguration.newUrl(sessions[4])).toEqual(proxyUrls[1]);
            expect(await proxyConfiguration.newUrl(sessions[5])).toEqual(proxyUrls[2]);

            // should remember already used session
            expect(await proxyConfiguration.newUrl(sessions[1])).toEqual(proxyUrls[1]);
            expect(await proxyConfiguration.newUrl(sessions[3])).toEqual(proxyUrls[0]);
        });

        test('should throw cannot combine custom methods', async () => {
            const proxyUrls = ['http://proxy.com:1111', 'http://proxy.com:2222', 'http://proxy.com:3333'];
            const newUrlFunction = () => {
                return proxyUrls[Math.floor(Math.random() * proxyUrls.length)];
            };
            try {
                const proxyConfiguration = new ProxyConfiguration({
                    proxyUrls,
                    newUrlFunction,
                });
                throw new Error('wrong error');
            } catch (err) {
                expect((err as Error).message).toMatch('Cannot combine custom proxies "options.proxyUrls"');
            }
        });

        test('should throw proxyUrls array is empty', async () => {
            try {
                const proxyConfiguration = new ProxyConfiguration({
                    proxyUrls: [],
                });
                throw new Error('wrong error');
            } catch (err) {
                expect((err as Error).message).toMatch('Expected property array `proxyUrls` to not be empty');
            }
        });

        test('should throw invalid custom URL form', async () => {
            try {
                const proxyConfiguration = new ProxyConfiguration({
                    proxyUrls: ['http://proxy.com:1111*invalid_url'],
                });
                throw new Error('wrong error');
            } catch (err) {
                expect((err as Error).message).toMatch('to be a URL, got `http://proxy.com:1111*invalid_url`');
            }
        });
    });

    describe('with tieredProxyUrls', () => {
        test('without Request rotates the urls uniformly', async () => {
            const proxyConfiguration = new ProxyConfiguration({
                tieredProxyUrls: [
                    ['http://proxy.com:1111', 'http://proxy.com:2222'],
                    ['http://proxy.com:3333', 'http://proxy.com:4444'],
                ],
            });

            // @ts-expect-error protected property
            const tieredProxyUrls = proxyConfiguration.tieredProxyUrls!;
            expect(await proxyConfiguration.newUrl()).toEqual(tieredProxyUrls[0][0]);
            expect(await proxyConfiguration.newUrl()).toEqual(tieredProxyUrls[0][1]);
            expect(await proxyConfiguration.newUrl()).toEqual(tieredProxyUrls[1][0]);
            expect(await proxyConfiguration.newUrl()).toEqual(tieredProxyUrls[1][1]);
            expect(await proxyConfiguration.newUrl()).toEqual(tieredProxyUrls[0][0]);
        });

        test('rotating a request results in higher-level proxies', async () => {
            const proxyConfiguration = new ProxyConfiguration({
                tieredProxyUrls: [['http://proxy.com:1111'], ['http://proxy.com:2222'], ['http://proxy.com:3333']],
            });

            const request = new Request({
                url: 'http://example.com',
            });

            // @ts-expect-error protected property
            const tieredProxyUrls = proxyConfiguration.tieredProxyUrls!;
            expect(await proxyConfiguration.newUrl('session-id', { request })).toEqual(tieredProxyUrls[0][0]);
            expect(await proxyConfiguration.newUrl('session-id', { request })).toEqual(tieredProxyUrls[1][0]);
            expect(await proxyConfiguration.newUrl('session-id', { request })).toEqual(tieredProxyUrls[2][0]);

            // we still get the same (higher) proxy tier even with a new request
            const request2 = new Request({
                url: 'http://example.com/another-resource',
            });

            expect(await proxyConfiguration.newUrl('session-id', { request: request2 })).toEqual(tieredProxyUrls[2][0]);
        });

        test('upshifts and downshifts properly', async () => {
            const tieredProxyUrls = [['http://proxy.com:1111'], ['http://proxy.com:2222'], ['http://proxy.com:3333']];

            const proxyConfiguration = new ProxyConfiguration({
                tieredProxyUrls,
            });

            const request = new Request({
                url: 'http://example.com',
            });

            let gotToTheHighestProxy = false;
            for (let i = 0; i < 10; i++) {
                const lastProxyUrl = await proxyConfiguration.newUrl('session-id', { request });
                if (lastProxyUrl === tieredProxyUrls[2][0]) {
                    gotToTheHighestProxy = true;
                    break;
                }
            }

            expect(gotToTheHighestProxy).toBe(true);

            // Even the highest-tier proxies didn't help - we should try going down
            let gotToTheLowestProxy = false;

            for (let i = 0; i < 20; i++) {
                const lastProxyUrl = await proxyConfiguration.newUrl('session-id', { request });
                if (lastProxyUrl === tieredProxyUrls[0][0]) {
                    gotToTheLowestProxy = true;
                    break;
                }
            }

            expect(gotToTheLowestProxy).toBe(true);
        });

        test('successful requests make the proxy tier drop eventually', async () => {
            const tieredProxyUrls = [['http://proxy.com:1111'], ['http://proxy.com:2222'], ['http://proxy.com:3333']];

            const proxyConfiguration = new ProxyConfiguration({
                tieredProxyUrls,
            });

            const failingRequest = new Request({
                url: 'http://example.com',
            });
            let gotToTheHighestProxy = false;

            for (let i = 0; i < 10; i++) {
                const lastProxyUrl = await proxyConfiguration.newUrl('session-id', { request: failingRequest });

                if (lastProxyUrl === tieredProxyUrls[2][0]) {
                    gotToTheHighestProxy = true;
                    break;
                }
            }

            expect(gotToTheHighestProxy).toBe(true);

            let gotToTheLowestProxy = false;

            for (let i = 0; i < 100; i++) {
                const lastProxyUrl = await proxyConfiguration.newUrl('session-id', {
                    request: new Request({ url: `http://example.com/${i}` }),
                });

                if (lastProxyUrl === tieredProxyUrls[0][0]) {
                    gotToTheLowestProxy = true;
                    break;
                }
            }

            expect(gotToTheLowestProxy).toBe(true);
        });
    });
});
