/**
 * Unit tests for TcpMonitorStrategy.
 *
 * Focused coverage:
 *  - SSRF guard rejects a loopback target and the rejection's message
 *    survives the catch block (the bug fixed alongside this test: the
 *    previous catch had `if (status !== 'down')` which dropped messages
 *    from upstream throws).
 *  - A successful connect still returns UP with timing populated.
 */
import { TcpMonitorStrategy } from '../tcp.strategy';
import type { MonitorForCheck } from '@/types';

// Mock dns for the SSRF guard so tests are deterministic.
jest.mock('dns', () => ({
    promises: {
        resolve4: jest.fn().mockResolvedValue(['93.184.216.34']),
        resolve6: jest.fn().mockRejectedValue(new Error('ENODATA')),
    },
}));

// Mock 'net' so the connect call is a no-op success unless we override.
const mockConnect = jest.fn();
const mockSetTimeout = jest.fn();
const mockOn = jest.fn();
const mockDestroy = jest.fn();
jest.mock('net', () => ({
    Socket: jest.fn().mockImplementation(() => ({
        setTimeout: mockSetTimeout,
        on: mockOn,
        connect: mockConnect,
        destroy: mockDestroy,
    })),
}));

const mockMonitor: MonitorForCheck = {
    id: 1,
    name: 'tcp-test',
    type: 'tcp',
    url: '93.184.216.34',
    hostname: null,
    port: 80,
    method: 'GET',
    headers: null,
    body: null,
    timeoutSeconds: 5,
    acceptedStatusCodes: '["200-299"]',
    keyword: null,
    keywordShouldContain: true,
    ignoreTlsErrors: false,
    intervalSeconds: 60,
    retries: 3,
    region: 'Global',
};

beforeEach(() => {
    mockConnect.mockReset();
    mockSetTimeout.mockReset();
    mockOn.mockReset();
    mockDestroy.mockReset();
});

describe('TcpMonitorStrategy', () => {
    it('returns DOWN with a "Blocked" errorMessage when target is loopback (SSRF)', async () => {
        const strategy = new TcpMonitorStrategy();
        const result = await strategy.check({ ...mockMonitor, url: '127.0.0.1', port: 3306 });
        expect(result.status).toBe('down');
        expect(result.errorMessage).toMatch(/Blocked|private/i);
        // The SSRF guard must reject BEFORE any socket activity.
        expect(mockConnect).not.toHaveBeenCalled();
    });

    it('returns DOWN with a "Blocked" errorMessage when target is RFC1918 link-local', async () => {
        const strategy = new TcpMonitorStrategy();
        const result = await strategy.check({ ...mockMonitor, url: '169.254.169.254', port: 80 });
        expect(result.status).toBe('down');
        expect(result.errorMessage).toMatch(/Blocked|private/i);
        expect(mockConnect).not.toHaveBeenCalled();
    });

    it('returns UP when the socket connects successfully', async () => {
        // Simulate a successful connect by having socket.connect immediately
        // invoke the success callback (3rd arg).
        mockConnect.mockImplementation((_port, _host, cb) => {
            if (cb) cb();
        });

        const strategy = new TcpMonitorStrategy();
        const result = await strategy.check(mockMonitor);
        expect(result.status).toBe('up');
        expect(mockConnect).toHaveBeenCalledWith(80, '93.184.216.34', expect.any(Function));
        expect(typeof result.connectTimeMs).toBe('number');
    });

    it('returns DOWN with the socket error message when connection refused', async () => {
        // Capture handlers registered via socket.on and invoke 'error' once
        // connect runs.
        const handlers: Record<string, (arg?: unknown) => void> = {};
        mockOn.mockImplementation((event: string, fn: (arg?: unknown) => void) => {
            handlers[event] = fn;
        });
        mockConnect.mockImplementation(() => {
            // Defer to next tick so handlers are attached
            process.nextTick(() => handlers['error']?.(new Error('ECONNREFUSED')));
        });

        const strategy = new TcpMonitorStrategy();
        const result = await strategy.check(mockMonitor);
        expect(result.status).toBe('down');
        expect(result.errorMessage).toBe('ECONNREFUSED');
    });
});
