'use strict';

const proxyquire = require('proxyquire').noCallThru();
const { EventEmitter } = require('events');

// Mock socket object
const createMockSocket = () => ({
    on: () => {},
    write: () => {},
    end: () => {},
    destroy: () => {}
});

// Real EventEmitter-backed mock socket, for tests that need listener tracking / emit.
const createMockEmitterSocket = () => Object.assign(new EventEmitter(), { write() {}, end() {}, destroy() {} });

// Asserts proxyConnection returned the socket and guarded it with an error listener.
const assertEarlyErrorHandler = (test, socket, mockSocket) => {
    test.equal(socket, mockSocket);
    test.ok(socket.listenerCount('error') >= 1, 'proxy socket carries an error listener before being returned');
    test.doesNotThrow(() => socket.emit('error', new Error('early proxy error')), 'an early proxy socket error does not throw');
};

// Mock logger
const createMockLogger = () => {
    const logs = { info: [], error: [] };
    return {
        info: msg => logs.info.push(msg),
        error: msg => logs.error.push(msg),
        _logs: logs
    };
};

// ============================================
// HTTP Proxy Tests
// ============================================

module.exports['Proxy Connection: HTTP proxy success'] = async test => {
    const mockSocket = createMockSocket();
    const logger = createMockLogger();

    const { proxyConnection } = proxyquire('../lib/proxy-connection', {
        'nodemailer/lib/smtp-connection/http-proxy-client': (url, port, host, cb) => {
            cb(null, mockSocket);
        },
        socks: { SocksClient: {} },
        dns: { promises: { resolve: async () => ['127.0.0.1'] } },
        net: { isIP: () => true }
    });

    const socket = await proxyConnection(logger, 'http://proxy.example.com:8080', '192.168.1.1', 993);

    test.equal(socket, mockSocket);
    test.equal(logger._logs.info.length, 1);
    test.ok(logger._logs.info[0].msg.includes('HTTP proxy'));
    test.done();
};

module.exports['Proxy Connection: HTTPS proxy success'] = async test => {
    const mockSocket = createMockSocket();
    const logger = createMockLogger();

    const { proxyConnection } = proxyquire('../lib/proxy-connection', {
        'nodemailer/lib/smtp-connection/http-proxy-client': (url, port, host, cb) => {
            cb(null, mockSocket);
        },
        socks: { SocksClient: {} },
        dns: { promises: { resolve: async () => ['127.0.0.1'] } },
        net: { isIP: () => true }
    });

    const socket = await proxyConnection(logger, 'https://proxy.example.com:8080', '192.168.1.1', 993);

    test.equal(socket, mockSocket);
    test.equal(logger._logs.info.length, 1);
    test.done();
};

module.exports['Proxy Connection: HTTP proxy with password hides it in logs'] = async test => {
    const mockSocket = createMockSocket();
    const logger = createMockLogger();

    const { proxyConnection } = proxyquire('../lib/proxy-connection', {
        'nodemailer/lib/smtp-connection/http-proxy-client': (url, port, host, cb) => {
            cb(null, mockSocket);
        },
        socks: { SocksClient: {} },
        dns: { promises: { resolve: async () => ['127.0.0.1'] } },
        net: { isIP: () => true }
    });

    await proxyConnection(logger, 'http://user:secret123@proxy.example.com:8080', '192.168.1.1', 993);

    test.equal(logger._logs.info.length, 1);
    test.ok(!logger._logs.info[0].proxyUrl.includes('secret123'));
    test.ok(logger._logs.info[0].proxyUrl.includes('(hidden)'));
    test.done();
};

module.exports['Proxy Connection: HTTP proxy failure'] = async test => {
    const logger = createMockLogger();
    const testError = new Error('Connection refused');

    const { proxyConnection } = proxyquire('../lib/proxy-connection', {
        'nodemailer/lib/smtp-connection/http-proxy-client': (url, port, host, cb) => {
            cb(testError);
        },
        socks: { SocksClient: {} },
        dns: { promises: { resolve: async () => ['127.0.0.1'] } },
        net: { isIP: () => true }
    });

    try {
        await proxyConnection(logger, 'http://proxy.example.com:8080', '192.168.1.1', 993);
        test.ok(false, 'Should have thrown');
    } catch (err) {
        test.equal(err, testError);
        test.equal(logger._logs.error.length, 1);
        test.ok(logger._logs.error[0].msg.includes('Failed'));
    }

    test.done();
};

module.exports['Proxy Connection: HTTP proxy failure hides password'] = async test => {
    const logger = createMockLogger();

    const { proxyConnection } = proxyquire('../lib/proxy-connection', {
        'nodemailer/lib/smtp-connection/http-proxy-client': (url, port, host, cb) => {
            cb(new Error('Failed'));
        },
        socks: { SocksClient: {} },
        dns: { promises: { resolve: async () => ['127.0.0.1'] } },
        net: { isIP: () => true }
    });

    try {
        await proxyConnection(logger, 'http://user:secret@proxy.example.com:8080', '192.168.1.1', 993);
    } catch (err) {
        // Error expected - we just need to verify logging
        err.expected = true;
        test.ok(!logger._logs.error[0].proxyUrl.includes('secret'));
        test.ok(logger._logs.error[0].proxyUrl.includes('(hidden)'));
    }

    test.done();
};

// ============================================
// SOCKS Proxy Tests
// ============================================

module.exports['Proxy Connection: SOCKS5 proxy success'] = async test => {
    const mockSocket = createMockSocket();
    const logger = createMockLogger();

    const { proxyConnection } = proxyquire('../lib/proxy-connection', {
        'nodemailer/lib/smtp-connection/http-proxy-client': () => {},
        socks: {
            SocksClient: {
                createConnection: async opts => {
                    test.equal(opts.proxy.type, 5);
                    test.equal(opts.command, 'connect');
                    return { socket: mockSocket };
                }
            }
        },
        dns: { promises: { resolve: async () => ['127.0.0.1'] } },
        net: { isIP: () => true }
    });

    const socket = await proxyConnection(logger, 'socks5://proxy.example.com:1080', '192.168.1.1', 993);

    test.equal(socket, mockSocket);
    test.equal(logger._logs.info.length, 1);
    test.ok(logger._logs.info[0].msg.includes('SOCKS proxy'));
    test.done();
};

module.exports['Proxy Connection: SOCKS proxy (defaults to SOCKS5)'] = async test => {
    const mockSocket = createMockSocket();
    const logger = createMockLogger();

    const { proxyConnection } = proxyquire('../lib/proxy-connection', {
        'nodemailer/lib/smtp-connection/http-proxy-client': () => {},
        socks: {
            SocksClient: {
                createConnection: async opts => {
                    test.equal(opts.proxy.type, 5); // Default to SOCKS5
                    return { socket: mockSocket };
                }
            }
        },
        dns: { promises: { resolve: async () => ['127.0.0.1'] } },
        net: { isIP: () => true }
    });

    await proxyConnection(logger, 'socks://proxy.example.com:1080', '192.168.1.1', 993);
    test.done();
};

module.exports['Proxy Connection: SOCKS4 proxy'] = async test => {
    const mockSocket = createMockSocket();
    const logger = createMockLogger();

    const { proxyConnection } = proxyquire('../lib/proxy-connection', {
        'nodemailer/lib/smtp-connection/http-proxy-client': () => {},
        socks: {
            SocksClient: {
                createConnection: async opts => {
                    test.equal(opts.proxy.type, 4);
                    return { socket: mockSocket };
                }
            }
        },
        dns: { promises: { resolve: async () => ['127.0.0.1'] } },
        net: { isIP: () => true }
    });

    await proxyConnection(logger, 'socks4://proxy.example.com:1080', '192.168.1.1', 993);
    test.done();
};

module.exports['Proxy Connection: SOCKS4a proxy'] = async test => {
    const mockSocket = createMockSocket();
    const logger = createMockLogger();

    const { proxyConnection } = proxyquire('../lib/proxy-connection', {
        'nodemailer/lib/smtp-connection/http-proxy-client': () => {},
        socks: {
            SocksClient: {
                createConnection: async opts => {
                    test.equal(opts.proxy.type, 4);
                    return { socket: mockSocket };
                }
            }
        },
        dns: { promises: { resolve: async () => ['127.0.0.1'] } },
        net: { isIP: () => true }
    });

    await proxyConnection(logger, 'socks4a://proxy.example.com:1080', '192.168.1.1', 993);
    test.done();
};

module.exports['Proxy Connection: SOCKS proxy with authentication'] = async test => {
    const mockSocket = createMockSocket();
    const logger = createMockLogger();

    const { proxyConnection } = proxyquire('../lib/proxy-connection', {
        'nodemailer/lib/smtp-connection/http-proxy-client': () => {},
        socks: {
            SocksClient: {
                createConnection: async opts => {
                    test.equal(opts.proxy.userId, 'testuser');
                    test.equal(opts.proxy.password, 'testpass');
                    return { socket: mockSocket };
                }
            }
        },
        dns: { promises: { resolve: async () => ['127.0.0.1'] } },
        net: { isIP: () => true }
    });

    await proxyConnection(logger, 'socks5://testuser:testpass@proxy.example.com:1080', '192.168.1.1', 993);
    test.done();
};

module.exports['Proxy Connection: SOCKS proxy hides password in logs'] = async test => {
    const mockSocket = createMockSocket();
    const logger = createMockLogger();

    const { proxyConnection } = proxyquire('../lib/proxy-connection', {
        'nodemailer/lib/smtp-connection/http-proxy-client': () => {},
        socks: {
            SocksClient: {
                createConnection: async () => ({ socket: mockSocket })
            }
        },
        dns: { promises: { resolve: async () => ['127.0.0.1'] } },
        net: { isIP: () => true }
    });

    await proxyConnection(logger, 'socks5://user:secretpass@proxy.example.com:1080', '192.168.1.1', 993);

    test.ok(!logger._logs.info[0].proxyUrl.includes('secretpass'));
    test.ok(logger._logs.info[0].proxyUrl.includes('(hidden)'));
    test.done();
};

module.exports['Proxy Connection: SOCKS proxy default port'] = async test => {
    const mockSocket = createMockSocket();
    const logger = createMockLogger();

    const { proxyConnection } = proxyquire('../lib/proxy-connection', {
        'nodemailer/lib/smtp-connection/http-proxy-client': () => {},
        socks: {
            SocksClient: {
                createConnection: async opts => {
                    test.equal(opts.proxy.port, 1080); // Default SOCKS port
                    return { socket: mockSocket };
                }
            }
        },
        dns: { promises: { resolve: async () => ['127.0.0.1'] } },
        net: { isIP: () => true }
    });

    await proxyConnection(logger, 'socks5://proxy.example.com', '192.168.1.1', 993);
    test.done();
};

module.exports['Proxy Connection: SOCKS proxy failure'] = async test => {
    const logger = createMockLogger();
    const testError = new Error('SOCKS connection failed');

    const { proxyConnection } = proxyquire('../lib/proxy-connection', {
        'nodemailer/lib/smtp-connection/http-proxy-client': () => {},
        socks: {
            SocksClient: {
                createConnection: async () => {
                    throw testError;
                }
            }
        },
        dns: { promises: { resolve: async () => ['127.0.0.1'] } },
        net: { isIP: () => true }
    });

    try {
        await proxyConnection(logger, 'socks5://proxy.example.com:1080', '192.168.1.1', 993);
        test.ok(false, 'Should have thrown');
    } catch (err) {
        test.equal(err, testError);
        test.equal(logger._logs.error.length, 1);
        test.ok(logger._logs.error[0].msg.includes('Failed'));
    }

    test.done();
};

module.exports['Proxy Connection: SOCKS proxy failure hides password'] = async test => {
    const logger = createMockLogger();

    const { proxyConnection } = proxyquire('../lib/proxy-connection', {
        'nodemailer/lib/smtp-connection/http-proxy-client': () => {},
        socks: {
            SocksClient: {
                createConnection: async () => {
                    throw new Error('Failed');
                }
            }
        },
        dns: { promises: { resolve: async () => ['127.0.0.1'] } },
        net: { isIP: () => true }
    });

    try {
        await proxyConnection(logger, 'socks5://user:secret@proxy.example.com:1080', '192.168.1.1', 993);
    } catch (err) {
        // Error expected - we just need to verify logging
        err.expected = true;
        test.ok(!logger._logs.error[0].proxyUrl.includes('secret'));
        test.ok(logger._logs.error[0].proxyUrl.includes('(hidden)'));
    }

    test.done();
};

// ============================================
// DNS Resolution Tests
// ============================================

module.exports['Proxy Connection: Resolves hostname to IP'] = async test => {
    const mockSocket = createMockSocket();
    const logger = createMockLogger();
    let resolvedHost = null;

    const { proxyConnection } = proxyquire('../lib/proxy-connection', {
        'nodemailer/lib/smtp-connection/http-proxy-client': (url, port, host, cb) => {
            resolvedHost = host;
            cb(null, mockSocket);
        },
        socks: { SocksClient: {} },
        dns: {
            promises: {
                resolve: async hostname => {
                    if (hostname === 'mail.example.com') {
                        return ['93.184.216.34'];
                    }
                    return [];
                }
            }
        },
        net: { isIP: host => /^\d+\.\d+\.\d+\.\d+$/.test(host) }
    });

    await proxyConnection(logger, 'http://proxy.example.com:8080', 'mail.example.com', 993);

    test.equal(resolvedHost, '93.184.216.34');
    test.done();
};

module.exports['Proxy Connection: Skips DNS for IP addresses'] = async test => {
    const mockSocket = createMockSocket();
    const logger = createMockLogger();
    let dnsResolveCalled = false;

    const { proxyConnection } = proxyquire('../lib/proxy-connection', {
        'nodemailer/lib/smtp-connection/http-proxy-client': (url, port, host, cb) => {
            cb(null, mockSocket);
        },
        socks: { SocksClient: {} },
        dns: {
            promises: {
                resolve: async () => {
                    dnsResolveCalled = true;
                    return ['127.0.0.1'];
                }
            }
        },
        net: { isIP: () => true } // Pretend it's already an IP
    });

    await proxyConnection(logger, 'http://proxy.example.com:8080', '192.168.1.1', 993);

    test.equal(dnsResolveCalled, false);
    test.done();
};

module.exports['Proxy Connection: SOCKS resolves proxy hostname'] = async test => {
    const mockSocket = createMockSocket();
    const logger = createMockLogger();
    let proxyHostResolved = null;

    const { proxyConnection } = proxyquire('../lib/proxy-connection', {
        'nodemailer/lib/smtp-connection/http-proxy-client': () => {},
        socks: {
            SocksClient: {
                createConnection: async opts => {
                    proxyHostResolved = opts.proxy.host;
                    return { socket: mockSocket };
                }
            }
        },
        dns: {
            promises: {
                resolve: async hostname => {
                    if (hostname === 'proxy.example.com') {
                        return ['10.0.0.1'];
                    }
                    return ['127.0.0.1'];
                }
            }
        },
        net: { isIP: host => /^\d+\.\d+\.\d+\.\d+$/.test(host) }
    });

    await proxyConnection(logger, 'socks5://proxy.example.com:1080', '192.168.1.1', 993);

    test.equal(proxyHostResolved, '10.0.0.1');
    test.done();
};

// ============================================
// Edge Cases
// ============================================

module.exports['Proxy Connection: Unknown protocol returns undefined'] = async test => {
    const logger = createMockLogger();

    const { proxyConnection } = proxyquire('../lib/proxy-connection', {
        'nodemailer/lib/smtp-connection/http-proxy-client': () => {},
        socks: { SocksClient: {} },
        dns: { promises: { resolve: async () => ['127.0.0.1'] } },
        net: { isIP: () => true }
    });

    const result = await proxyConnection(logger, 'ftp://proxy.example.com:21', '192.168.1.1', 993);

    test.equal(result, undefined);
    test.done();
};

module.exports['Proxy Connection: HTTP proxy with no socket returned'] = async test => {
    const logger = createMockLogger();

    const { proxyConnection } = proxyquire('../lib/proxy-connection', {
        'nodemailer/lib/smtp-connection/http-proxy-client': (url, port, host, cb) => {
            cb(null, null); // No socket
        },
        socks: { SocksClient: {} },
        dns: { promises: { resolve: async () => ['127.0.0.1'] } },
        net: { isIP: () => true }
    });

    const socket = await proxyConnection(logger, 'http://proxy.example.com:8080', '192.168.1.1', 993);

    test.equal(socket, null);
    // No log when socket is null
    test.equal(logger._logs.info.length, 0);
    test.done();
};

module.exports['Proxy Connection: DNS returns empty result'] = async test => {
    const mockSocket = createMockSocket();
    const logger = createMockLogger();
    let usedHost = null;

    const { proxyConnection } = proxyquire('../lib/proxy-connection', {
        'nodemailer/lib/smtp-connection/http-proxy-client': (url, port, host, cb) => {
            usedHost = host;
            cb(null, mockSocket);
        },
        socks: { SocksClient: {} },
        dns: {
            promises: {
                resolve: async () => [] // Empty result
            }
        },
        net: { isIP: () => false }
    });

    await proxyConnection(logger, 'http://proxy.example.com:8080', 'mail.example.com', 993);

    // Should keep original hostname when DNS returns empty
    test.equal(usedHost, 'mail.example.com');
    test.done();
};

module.exports['Proxy Connection: SOCKS with username only'] = async test => {
    const mockSocket = createMockSocket();
    const logger = createMockLogger();

    const { proxyConnection } = proxyquire('../lib/proxy-connection', {
        'nodemailer/lib/smtp-connection/http-proxy-client': () => {},
        socks: {
            SocksClient: {
                createConnection: async opts => {
                    test.equal(opts.proxy.userId, 'testuser');
                    test.equal(opts.proxy.password, ''); // Empty string from URL parsing
                    return { socket: mockSocket };
                }
            }
        },
        dns: { promises: { resolve: async () => ['127.0.0.1'] } },
        net: { isIP: () => true }
    });

    await proxyConnection(logger, 'socks5://testuser@proxy.example.com:1080', '192.168.1.1', 993);
    test.done();
};

// ============================================
// Early error handler (no unhandled 'error' before ImapFlow takes over)
// ============================================

module.exports['Proxy Connection: HTTP socket has early error handler before return'] = async test => {
    const mockSocket = createMockEmitterSocket();
    const logger = createMockLogger();

    const { proxyConnection } = proxyquire('../lib/proxy-connection', {
        'nodemailer/lib/smtp-connection/http-proxy-client': (url, port, host, cb) => {
            cb(null, mockSocket);
        },
        socks: { SocksClient: {} },
        dns: { promises: { resolve: async () => ['127.0.0.1'] } },
        net: { isIP: () => true }
    });

    const socket = await proxyConnection(logger, 'http://proxy.example.com:8080', '192.168.1.1', 993);

    assertEarlyErrorHandler(test, socket, mockSocket);
    test.done();
};

module.exports['Proxy Connection: SOCKS socket has early error handler before return'] = async test => {
    const mockSocket = createMockEmitterSocket();
    const logger = createMockLogger();

    const { proxyConnection } = proxyquire('../lib/proxy-connection', {
        'nodemailer/lib/smtp-connection/http-proxy-client': () => {},
        socks: {
            SocksClient: {
                createConnection: async () => ({ socket: mockSocket })
            }
        },
        dns: { promises: { resolve: async () => ['127.0.0.1'] } },
        net: { isIP: () => true }
    });

    const socket = await proxyConnection(logger, 'socks5://proxy.example.com:1080', '192.168.1.1', 993);

    assertEarlyErrorHandler(test, socket, mockSocket);
    test.done();
};

// detachEarlyErrorHandler must remove the guard once the caller takes ownership of the socket,
// so it no longer swallows/logs errors meant for the new owner's handlers.
module.exports['Proxy Connection: detachEarlyErrorHandler removes the early guard'] = async test => {
    const mockSocket = createMockEmitterSocket();
    const logger = createMockLogger();

    const { proxyConnection, detachEarlyErrorHandler } = proxyquire('../lib/proxy-connection', {
        'nodemailer/lib/smtp-connection/http-proxy-client': (url, port, host, cb) => {
            cb(null, mockSocket);
        },
        socks: { SocksClient: {} },
        dns: { promises: { resolve: async () => ['127.0.0.1'] } },
        net: { isIP: () => true }
    });

    const socket = await proxyConnection(logger, 'http://proxy.example.com:8080', '192.168.1.1', 993);
    test.ok(socket.listenerCount('error') >= 1, 'early error handler attached on return');
    test.ok(typeof socket._earlyErrorHandler === 'function', 'handler reference stored on the socket');

    detachEarlyErrorHandler(socket);

    test.equal(socket.listenerCount('error'), 0, 'early error handler removed after detach');
    test.equal(socket._earlyErrorHandler, null, 'stored handler reference cleared');

    // Detaching again (or on a bare socket) must be a safe no-op.
    test.doesNotThrow(() => detachEarlyErrorHandler(socket));
    test.doesNotThrow(() => detachEarlyErrorHandler(createMockEmitterSocket()));

    test.done();
};
