import { describe, it, expect } from 'vitest';
import * as net from 'node:net';
import { preflightAndConnect, SshSsrfError } from './ssrf.js';
import type { LookupFn } from '../net/ssrf-strict.js';

describe('ssh/ssrf preflightAndConnect', () => {
  it('rejects forbidden_address when resolved IP is private and allowPrivate=false', async () => {
    const lookup: LookupFn = async () => [{ address: '10.0.0.5', family: 4 }];
    await expect(
      preflightAndConnect({
        host: 'evil.lan',
        port: 22,
        allowPrivate: false,
        timeoutMs: 500,
        lookup,
      }),
    ).rejects.toMatchObject({ code: 'forbidden_address' });
  });

  it('rejects dns_failed when lookup throws', async () => {
    const lookup: LookupFn = async () => {
      throw new Error('ENOTFOUND');
    };
    try {
      await preflightAndConnect({
        host: 'noexist',
        port: 22,
        allowPrivate: false,
        timeoutMs: 500,
        lookup,
      });
      throw new Error('expected throw');
    } catch (err) {
      expect(err).toBeInstanceOf(SshSsrfError);
      expect((err as SshSsrfError).code).toBe('dns_failed');
    }
  });

  it('rejects invalid_host on bad port', async () => {
    await expect(
      preflightAndConnect({
        host: '8.8.8.8',
        port: 0,
        allowPrivate: false,
        timeoutMs: 500,
      }),
    ).rejects.toMatchObject({ code: 'invalid_host' });
  });

  it('rejects invalid_host on bad timeoutMs', async () => {
    await expect(
      preflightAndConnect({
        host: '8.8.8.8',
        port: 22,
        allowPrivate: false,
        timeoutMs: 0,
      }),
    ).rejects.toMatchObject({ code: 'invalid_host' });
  });

  it('rejects connect_timeout when target is unreachable', async () => {
    await expect(
      preflightAndConnect({
        host: '192.0.2.1', // TEST-NET-1, not routable
        port: 22,
        allowPrivate: false,
        timeoutMs: 100,
      }),
    ).rejects.toMatchObject({ code: 'connect_timeout' });
  });

  it('succeeds against a local TCP echo (allowPrivate=true)', async () => {
    const server = net.createServer();
    await new Promise<void>((resolve) => server.listen(0, '127.0.0.1', () => resolve()));
    const addr = server.address();
    if (typeof addr !== 'object' || !addr) throw new Error('no address');
    try {
      const result = await preflightAndConnect({
        host: '127.0.0.1',
        port: addr.port,
        allowPrivate: true,
        timeoutMs: 1000,
      });
      expect(result.resolvedIp).toBe('127.0.0.1');
      expect(result.family).toBe(4);
      expect(result.socket).toBeInstanceOf(net.Socket);
      result.socket.destroy();
    } finally {
      await new Promise<void>((resolve) => server.close(() => resolve()));
    }
  });

  it('rejects forbidden_address for localhost when allowPrivate=false', async () => {
    await expect(
      preflightAndConnect({
        host: '127.0.0.1',
        port: 22,
        allowPrivate: false,
        timeoutMs: 500,
      }),
    ).rejects.toMatchObject({ code: 'forbidden_address' });
  });
});
