/**
 * Unit tests for SslMonitorStrategy (new 2026-05-30).
 *
 * A cert-watch monitor: open a TLS connection (SNI-correct, connecting to
 * the SSRF-validated IP), read the peer certificate, and go DOWN when the
 * cert is expired, not-yet-valid, or expires within `tlsExpiryWarningDays`.
 * Ties into Deliverable 1 — populates tlsExpiresAt / tlsIssuer / tlsValid so
 * the existing dashboard card + detail page render the cert.
 */
import { SslMonitorStrategy } from '../ssl.strategy';
import type { MonitorForCheck } from '@/types';
import EventEmitter from 'events';
import tls from 'tls';

jest.mock('dns', () => ({
    promises: {
        resolve4: jest.fn().mockResolvedValue(['93.184.216.34']),
        resolve6: jest.fn().mockRejectedValue(new Error('ENODATA')),
    },
}));

const mockConnect = jest.fn();
jest.mock('tls', () => ({
    connect: jest.fn((...args: unknown[]) => mockConnect(...args)),
}));

const base: MonitorForCheck = {
    id: 1,
    name: 'ssl-test',
    type: 'ssl',
    url: 'example.com',
    hostname: 'example.com',
    port: 443,
    method: 'GET',
    headers: null,
    body: null,
    timeoutSeconds: 10,
    acceptedStatusCodes: '["200-299"]',
    keyword: null,
    keywordShouldContain: true,
    ignoreTlsErrors: false,
    intervalSeconds: 60,
    retries: 2,
    region: 'Global',
    tlsExpiryWarningDays: 14,
};

// Build a fake TLSSocket that emits secureConnect with a controllable cert.
function fakeSocket(cert: Record<string, unknown>, authorized = true) {
    // eslint-disable-next-line @typescript-eslint/no-explicit-any
    const socket = new EventEmitter() as any;
    socket.authorized = authorized;
    socket.getPeerCertificate = jest.fn().mockReturnValue(cert);
    socket.setTimeout = jest.fn();
    socket.destroy = jest.fn();
    socket.end = jest.fn();
    return socket;
}

function wire(socket: EventEmitter, fireSecure = true, err?: Error) {
    mockConnect.mockImplementation((..._args: unknown[]) => {
        process.nextTick(() => {
            if (err) socket.emit('error', err);
            else if (fireSecure) socket.emit('secureConnect');
        });
        return socket;
    });
}

const days = (n: number) => new Date(Date.now() + n * 86400_000).toUTCString();

beforeEach(() => { mockConnect.mockReset(); });

describe('SslMonitorStrategy', () => {
    it('returns UP with expiry + issuer when the cert is valid and far from expiry', async () => {
        const socket = fakeSocket({ valid_to: days(90), valid_from: days(-10), issuer: { O: 'Lets Encrypt' } });
        wire(socket);
        const result = await new SslMonitorStrategy().check(base);
        expect(result.status).toBe('up');
        expect(result.tlsExpiresAt).toBeInstanceOf(Date);
        expect(result.tlsIssuer).toBe('Lets Encrypt');
        expect(result.tlsValid).toBe(true);
    });

    it('returns DOWN with TLS_INVALID when the cert is already expired', async () => {
        const socket = fakeSocket({ valid_to: days(-1), valid_from: days(-90), issuer: { O: 'CA' } });
        wire(socket);
        const result = await new SslMonitorStrategy().check(base);
        expect(result.status).toBe('down');
        expect(result.errorClass).toBe('TLS_INVALID');
        expect(result.errorMessage).toMatch(/expired/i);
    });

    it('returns DOWN when the cert expires within the warning window', async () => {
        const socket = fakeSocket({ valid_to: days(5), valid_from: days(-10), issuer: { O: 'CA' } });
        wire(socket);
        const result = await new SslMonitorStrategy().check({ ...base, tlsExpiryWarningDays: 14 });
        expect(result.status).toBe('down');
        expect(result.errorClass).toBe('TLS_INVALID');
        expect(result.errorMessage).toMatch(/expires in|days/i);
        // Still surfaces the expiry so the UI can render it.
        expect(result.tlsExpiresAt).toBeInstanceOf(Date);
    });

    it('returns DOWN with TLS_INVALID on a TLS handshake error', async () => {
        const socket = fakeSocket({});
        wire(socket, false, Object.assign(new Error('self signed certificate'), { code: 'DEPTH_ZERO_SELF_SIGNED_CERT' }));
        const result = await new SslMonitorStrategy().check(base);
        expect(result.status).toBe('down');
        expect(result.errorClass).toBe('TLS_INVALID');
    });

    it('returns DOWN with DNS_FAIL when the host is private/blocked (SSRF guard)', async () => {
        const result = await new SslMonitorStrategy().check({ ...base, url: '127.0.0.1', hostname: '127.0.0.1' });
        expect(result.status).toBe('down');
        expect(result.errorMessage).toMatch(/Blocked|private/i);
        expect(mockConnect).not.toHaveBeenCalled();
    });

    it('connects to the SSRF-validated IP but sets servername to the hostname (SNI)', async () => {
        const socket = fakeSocket({ valid_to: days(90), valid_from: days(-10), issuer: { O: 'CA' } });
        wire(socket);
        await new SslMonitorStrategy().check(base);
        const opts = mockConnect.mock.calls[0][0];
        expect(opts.host).toBe('93.184.216.34');
        expect(opts.servername).toBe('example.com');
        expect(opts.port).toBe(443);
    });

    it('defaults the port to 443 when none is configured', async () => {
        const socket = fakeSocket({ valid_to: days(90), valid_from: days(-10), issuer: { O: 'CA' } });
        wire(socket);
        await new SslMonitorStrategy().check({ ...base, port: null });
        expect(mockConnect.mock.calls[0][0].port).toBe(443);
    });
});

// Touch the real tls import so the mock factory is referenced.
void tls;
