import { describe, it, expect, beforeEach, afterEach, vi } from 'vitest';

// Mock dns module before importing SSRFProtection
vi.mock('dns/promises', () => ({
  lookup: vi.fn(),
}));

import { SSRFProtection } from '../../../src/utils/ssrf-protection';
import * as dns from 'dns/promises';

/**
 * Unit tests for SSRFProtection with configurable security modes
 *
 * SECURITY: These tests verify SSRF protection blocks malicious URLs in all modes
 * See: https://github.com/czlonkowski/n8n-mcp/issues/265 (HIGH-03)
 */
describe('SSRFProtection', () => {
  const originalEnv = process.env.WEBHOOK_SECURITY_MODE;

  beforeEach(() => {
    // Clear all mocks before each test
    vi.clearAllMocks();
    // Default mock: simulate real DNS behavior - return the hostname as IP if it looks like an IP
    vi.mocked(dns.lookup).mockImplementation(async (hostname: any) => {
      // Handle special hostname "localhost"
      if (hostname === 'localhost') {
        return { address: '127.0.0.1', family: 4 } as any;
      }

      // If hostname is an IP address, return it as-is (simulating real DNS behavior)
      const ipv4Regex = /^(\d{1,3}\.){3}\d{1,3}$/;
      const ipv6Regex = /^([0-9a-fA-F]{0,4}:)+[0-9a-fA-F]{0,4}$/;

      if (ipv4Regex.test(hostname)) {
        return { address: hostname, family: 4 } as any;
      }
      if (ipv6Regex.test(hostname) || hostname === '::1') {
        return { address: hostname, family: 6 } as any;
      }

      // For actual hostnames, return a public IP by default
      return { address: '8.8.8.8', family: 4 } as any;
    });
  });

  afterEach(() => {
    // Restore original environment
    if (originalEnv) {
      process.env.WEBHOOK_SECURITY_MODE = originalEnv;
    } else {
      delete process.env.WEBHOOK_SECURITY_MODE;
    }
    vi.restoreAllMocks();
  });

  describe('Strict Mode (default)', () => {
    beforeEach(() => {
      delete process.env.WEBHOOK_SECURITY_MODE; // Use default strict
    });

    it('should block localhost', async () => {
      const localhostURLs = [
        'http://localhost:3000/webhook',
        'http://127.0.0.1/webhook',
        'http://[::1]/webhook',
      ];

      for (const url of localhostURLs) {
        const result = await SSRFProtection.validateWebhookUrl(url);
        expect(result.valid, `URL ${url} should be blocked but was valid`).toBe(false);
        expect(result.reason, `URL ${url} should have a reason`).toBeDefined();
      }
    });

    it('should block AWS metadata endpoint', async () => {
      const result = await SSRFProtection.validateWebhookUrl('http://169.254.169.254/latest/meta-data');
      expect(result.valid).toBe(false);
      expect(result.reason).toContain('Cloud metadata');
    });

    it('should block GCP metadata endpoint', async () => {
      const result = await SSRFProtection.validateWebhookUrl('http://metadata.google.internal/computeMetadata/v1/');
      expect(result.valid).toBe(false);
      expect(result.reason).toContain('Cloud metadata');
    });

    it('should block Alibaba Cloud metadata endpoint', async () => {
      const result = await SSRFProtection.validateWebhookUrl('http://100.100.100.200/latest/meta-data');
      expect(result.valid).toBe(false);
      expect(result.reason).toContain('Cloud metadata');
    });

    it('should block Oracle Cloud metadata endpoint', async () => {
      const result = await SSRFProtection.validateWebhookUrl('http://192.0.0.192/opc/v2/instance/');
      expect(result.valid).toBe(false);
      expect(result.reason).toContain('Cloud metadata');
    });

    it('should block private IP ranges', async () => {
      const privateIPs = [
        'http://10.0.0.1/webhook',
        'http://192.168.1.1/webhook',
        'http://172.16.0.1/webhook',
        'http://172.31.255.255/webhook',
      ];

      for (const url of privateIPs) {
        const result = await SSRFProtection.validateWebhookUrl(url);
        expect(result.valid).toBe(false);
        expect(result.reason).toContain('Private IP');
      }
    });

    it('should allow public URLs', async () => {
      const publicURLs = [
        'https://hooks.example.com/webhook',
        'https://api.external.com/callback',
        'http://public-service.com:8080/hook',
      ];

      for (const url of publicURLs) {
        const result = await SSRFProtection.validateWebhookUrl(url);
        expect(result.valid).toBe(true);
        expect(result.reason).toBeUndefined();
      }
    });

    it('should block non-HTTP protocols', async () => {
      const invalidProtocols = [
        'file:///etc/passwd',
        'ftp://internal-server/file',
        'gopher://old-service',
      ];

      for (const url of invalidProtocols) {
        const result = await SSRFProtection.validateWebhookUrl(url);
        expect(result.valid).toBe(false);
        expect(result.reason).toContain('protocol');
      }
    });
  });

  describe('Moderate Mode', () => {
    beforeEach(() => {
      process.env.WEBHOOK_SECURITY_MODE = 'moderate';
    });

    it('should allow localhost', async () => {
      const localhostURLs = [
        'http://localhost:5678/webhook',
        'http://127.0.0.1:5678/webhook',
        'http://[::1]:5678/webhook',
      ];

      for (const url of localhostURLs) {
        const result = await SSRFProtection.validateWebhookUrl(url);
        expect(result.valid).toBe(true);
      }
    });

    it('should still block private IPs', async () => {
      const privateIPs = [
        'http://10.0.0.1/webhook',
        'http://192.168.1.1/webhook',
        'http://172.16.0.1/webhook',
      ];

      for (const url of privateIPs) {
        const result = await SSRFProtection.validateWebhookUrl(url);
        expect(result.valid).toBe(false);
        expect(result.reason).toContain('Private IP');
      }
    });

    it('should still block cloud metadata', async () => {
      const metadataURLs = [
        'http://169.254.169.254/latest/meta-data',
        'http://metadata.google.internal/computeMetadata/v1/',
      ];

      for (const url of metadataURLs) {
        const result = await SSRFProtection.validateWebhookUrl(url);
        expect(result.valid).toBe(false);
        expect(result.reason).toContain('metadata');
      }
    });

    it('should allow public URLs', async () => {
      const result = await SSRFProtection.validateWebhookUrl('https://api.example.com/webhook');
      expect(result.valid).toBe(true);
    });
  });

  describe('Permissive Mode', () => {
    beforeEach(() => {
      process.env.WEBHOOK_SECURITY_MODE = 'permissive';
    });

    it('should allow localhost', async () => {
      const result = await SSRFProtection.validateWebhookUrl('http://localhost:5678/webhook');
      expect(result.valid).toBe(true);
    });

    it('should allow private IPs', async () => {
      const privateIPs = [
        'http://10.0.0.1/webhook',
        'http://192.168.1.1/webhook',
        'http://172.16.0.1/webhook',
      ];

      for (const url of privateIPs) {
        const result = await SSRFProtection.validateWebhookUrl(url);
        expect(result.valid).toBe(true);
      }
    });

    it('should still block cloud metadata', async () => {
      const metadataURLs = [
        'http://169.254.169.254/latest/meta-data',
        'http://metadata.google.internal/computeMetadata/v1/',
        'http://169.254.170.2/v2/metadata',
      ];

      for (const url of metadataURLs) {
        const result = await SSRFProtection.validateWebhookUrl(url);
        expect(result.valid).toBe(false);
        expect(result.reason).toContain('metadata');
      }
    });

    it('should allow public URLs', async () => {
      const result = await SSRFProtection.validateWebhookUrl('https://api.example.com/webhook');
      expect(result.valid).toBe(true);
    });
  });

  describe('DNS Rebinding Prevention', () => {
    it('should block hostname resolving to private IP (strict mode)', async () => {
      delete process.env.WEBHOOK_SECURITY_MODE; // strict

      // Mock DNS lookup to return private IP
      vi.mocked(dns.lookup).mockResolvedValue({ address: '10.0.0.1', family: 4 } as any);

      const result = await SSRFProtection.validateWebhookUrl('http://evil.example.com/webhook');
      expect(result.valid).toBe(false);
      expect(result.reason).toContain('Private IP');
    });

    it('should block hostname resolving to private IP (moderate mode)', async () => {
      process.env.WEBHOOK_SECURITY_MODE = 'moderate';

      // Mock DNS lookup to return private IP
      vi.mocked(dns.lookup).mockResolvedValue({ address: '192.168.1.100', family: 4 } as any);

      const result = await SSRFProtection.validateWebhookUrl('http://internal.company.com/webhook');
      expect(result.valid).toBe(false);
      expect(result.reason).toContain('Private IP');
    });

    it('should allow hostname resolving to private IP (permissive mode)', async () => {
      process.env.WEBHOOK_SECURITY_MODE = 'permissive';

      // Mock DNS lookup to return private IP
      vi.mocked(dns.lookup).mockResolvedValue({ address: '192.168.1.100', family: 4 } as any);

      const result = await SSRFProtection.validateWebhookUrl('http://internal.company.com/webhook');
      expect(result.valid).toBe(true);
    });

    it('should block hostname resolving to cloud metadata (all modes)', async () => {
      const modes = ['strict', 'moderate', 'permissive'];

      for (const mode of modes) {
        process.env.WEBHOOK_SECURITY_MODE = mode;

        // Mock DNS lookup to return cloud metadata IP
        vi.mocked(dns.lookup).mockResolvedValue({ address: '169.254.169.254', family: 4 } as any);

        const result = await SSRFProtection.validateWebhookUrl('http://evil-domain.com/webhook');
        expect(result.valid).toBe(false);
        expect(result.reason).toContain('metadata');
      }
    });

    it('should block hostname resolving to localhost IP (strict mode)', async () => {
      delete process.env.WEBHOOK_SECURITY_MODE; // strict

      // Mock DNS lookup to return localhost IP
      vi.mocked(dns.lookup).mockResolvedValue({ address: '127.0.0.1', family: 4 } as any);

      const result = await SSRFProtection.validateWebhookUrl('http://suspicious-domain.com/webhook');
      expect(result.valid).toBe(false);
      expect(result.reason).toBeDefined();
    });
  });

  describe('IPv6 Protection', () => {
    it('should block IPv6 localhost (strict mode)', async () => {
      delete process.env.WEBHOOK_SECURITY_MODE; // strict

      // Mock DNS to return IPv6 localhost
      vi.mocked(dns.lookup).mockResolvedValue({ address: '::1', family: 6 } as any);

      const result = await SSRFProtection.validateWebhookUrl('http://ipv6-test.com/webhook');
      expect(result.valid).toBe(false);
      // Updated: IPv6 localhost is now caught by the localhost check, not IPv6 check
      expect(result.reason).toContain('Localhost');
    });

    it('should block IPv6 link-local (strict mode)', async () => {
      delete process.env.WEBHOOK_SECURITY_MODE; // strict

      // Mock DNS to return IPv6 link-local
      vi.mocked(dns.lookup).mockResolvedValue({ address: 'fe80::1', family: 6 } as any);

      const result = await SSRFProtection.validateWebhookUrl('http://ipv6-local.com/webhook');
      expect(result.valid).toBe(false);
      expect(result.reason).toContain('IPv6 private');
    });

    it('should block IPv6 unique local (strict mode)', async () => {
      delete process.env.WEBHOOK_SECURITY_MODE; // strict

      // Mock DNS to return IPv6 unique local
      vi.mocked(dns.lookup).mockResolvedValue({ address: 'fc00::1', family: 6 } as any);

      const result = await SSRFProtection.validateWebhookUrl('http://ipv6-internal.com/webhook');
      expect(result.valid).toBe(false);
      expect(result.reason).toContain('IPv6 private');
    });

    it('should block IPv6 unique local fd00::/8 (strict mode)', async () => {
      delete process.env.WEBHOOK_SECURITY_MODE; // strict

      // Mock DNS to return IPv6 unique local fd00::/8
      vi.mocked(dns.lookup).mockResolvedValue({ address: 'fd00::1', family: 6 } as any);

      const result = await SSRFProtection.validateWebhookUrl('http://ipv6-fd00.com/webhook');
      expect(result.valid).toBe(false);
      expect(result.reason).toContain('IPv6 private');
    });

    it('should block IPv6 unspecified address (strict mode)', async () => {
      delete process.env.WEBHOOK_SECURITY_MODE; // strict

      // Mock DNS to return IPv6 unspecified address
      vi.mocked(dns.lookup).mockResolvedValue({ address: '::', family: 6 } as any);

      const result = await SSRFProtection.validateWebhookUrl('http://ipv6-unspecified.com/webhook');
      expect(result.valid).toBe(false);
      expect(result.reason).toContain('IPv6 private');
    });

    it('should block IPv4-mapped IPv6 addresses (strict mode)', async () => {
      delete process.env.WEBHOOK_SECURITY_MODE; // strict

      // Mock DNS to return IPv4-mapped IPv6 address
      vi.mocked(dns.lookup).mockResolvedValue({ address: '::ffff:127.0.0.1', family: 6 } as any);

      const result = await SSRFProtection.validateWebhookUrl('http://ipv4-mapped.com/webhook');
      expect(result.valid).toBe(false);
      expect(result.reason).toContain('IPv6 private');
    });
  });

  describe('DNS Resolution Failures', () => {
    it('should handle DNS resolution failure gracefully', async () => {
      // Mock DNS lookup to fail
      vi.mocked(dns.lookup).mockRejectedValue(new Error('ENOTFOUND'));

      const result = await SSRFProtection.validateWebhookUrl('http://non-existent-domain.invalid/webhook');
      expect(result.valid).toBe(false);
      expect(result.reason).toBe('DNS resolution failed');
    });
  });

  describe('Edge Cases', () => {
    it('should handle malformed URLs', async () => {
      const malformedURLs = [
        'not-a-url',
        'http://',
        '://missing-protocol.com',
      ];

      for (const url of malformedURLs) {
        const result = await SSRFProtection.validateWebhookUrl(url);
        expect(result.valid).toBe(false);
        expect(result.reason).toBe('Invalid URL format');
      }
    });

    it('should handle URL with special characters safely', async () => {
      const result = await SSRFProtection.validateWebhookUrl('https://example.com/webhook?param=value&other=123');
      expect(result.valid).toBe(true);
    });
  });

  /**
   * Sync URL validation — verifies the sync guard that runs inside
   * validateInstanceContext and must not make any DNS calls.
   */
  describe('validateUrlSync', () => {
    beforeEach(() => {
      delete process.env.WEBHOOK_SECURITY_MODE;
    });

    it('should reject URL with trailing fragment', () => {
      const result = SSRFProtection.validateUrlSync('http://169.254.169.254#');
      expect(result.valid).toBe(false);
      expect(result.reason).toBe('URL fragments are not allowed');
    });

    it('should reject HTTPS variant with trailing fragment', () => {
      const result = SSRFProtection.validateUrlSync('https://169.254.169.254#');
      expect(result.valid).toBe(false);
      expect(result.reason).toBe('URL fragments are not allowed');
    });

    it('should reject fragment with content after the hash', () => {
      const result = SSRFProtection.validateUrlSync('http://n8n.example.com#trailing');
      expect(result.valid).toBe(false);
      expect(result.reason).toBe('URL fragments are not allowed');
    });

    it('should reject URLs with userinfo', () => {
      const result = SSRFProtection.validateUrlSync('http://user:pass@n8n.example.com');
      expect(result.valid).toBe(false);
      expect(result.reason).toBe('Userinfo in URL is not allowed');
    });

    it('should reject URLs with username only', () => {
      const result = SSRFProtection.validateUrlSync('http://user@n8n.example.com');
      expect(result.valid).toBe(false);
      expect(result.reason).toBe('Userinfo in URL is not allowed');
    });

    it('should reject AWS/Azure metadata endpoint in all modes', () => {
      for (const mode of ['strict', 'moderate', 'permissive']) {
        process.env.WEBHOOK_SECURITY_MODE = mode;
        const result = SSRFProtection.validateUrlSync('http://169.254.169.254');
        expect(result.valid, `mode=${mode}`).toBe(false);
        expect(result.reason).toBe('Cloud metadata endpoint blocked');
      }
    });

    it('should reject all cloud metadata endpoints in all modes', () => {
      const metadataUrls = [
        'http://169.254.170.2',         // AWS ECS
        'http://metadata.google.internal', // GCP
        'http://metadata',                 // GCP short
        'http://100.100.100.200',          // Alibaba
        'http://192.0.0.192',              // Oracle
      ];
      for (const mode of ['strict', 'moderate', 'permissive']) {
        process.env.WEBHOOK_SECURITY_MODE = mode;
        for (const url of metadataUrls) {
          const result = SSRFProtection.validateUrlSync(url);
          expect(result.valid, `url=${url} mode=${mode}`).toBe(false);
          expect(result.reason).toBe('Cloud metadata endpoint blocked');
        }
      }
    });

    it('should reject private IPv4 literals in strict mode', () => {
      delete process.env.WEBHOOK_SECURITY_MODE; // strict default
      const privateUrls = [
        'http://10.0.0.1',
        'http://192.168.1.1',
        'http://172.16.0.1',
        'http://172.31.255.255',
      ];
      for (const url of privateUrls) {
        const result = SSRFProtection.validateUrlSync(url);
        expect(result.valid, `url=${url}`).toBe(false);
        expect(result.reason).toContain('Private IP');
      }
    });

    it('should reject private IPv4 literals in moderate mode', () => {
      process.env.WEBHOOK_SECURITY_MODE = 'moderate';
      const result = SSRFProtection.validateUrlSync('http://10.0.0.1');
      expect(result.valid).toBe(false);
      expect(result.reason).toContain('Private IP');
    });

    it('should allow private IPv4 literals in permissive mode', () => {
      process.env.WEBHOOK_SECURITY_MODE = 'permissive';
      const result = SSRFProtection.validateUrlSync('http://10.0.0.1');
      expect(result.valid).toBe(true);
    });

    it('should reject localhost literals in strict mode', () => {
      delete process.env.WEBHOOK_SECURITY_MODE;
      const localhostUrls = [
        'http://localhost',
        'http://127.0.0.1',
        'http://0.0.0.0',
      ];
      for (const url of localhostUrls) {
        const result = SSRFProtection.validateUrlSync(url);
        expect(result.valid, `url=${url}`).toBe(false);
      }
    });

    it('should allow localhost literals in moderate and permissive modes', () => {
      for (const mode of ['moderate', 'permissive']) {
        process.env.WEBHOOK_SECURITY_MODE = mode;
        const result = SSRFProtection.validateUrlSync('http://localhost:5678');
        expect(result.valid, `mode=${mode}`).toBe(true);
      }
    });

    it('should reject non-http(s) protocols', () => {
      const badProtocols = [
        'file:///etc/passwd',
        'gopher://example.com',
        'ftp://example.com',
        'data:text/plain;base64,aGVsbG8=',
      ];
      for (const url of badProtocols) {
        const result = SSRFProtection.validateUrlSync(url);
        expect(result.valid, `url=${url}`).toBe(false);
        expect(result.reason).toContain('protocol');
      }
    });

    it('should reject malformed URLs', () => {
      const malformed = ['not-a-url', 'http://', '://missing-protocol.com', ''];
      for (const url of malformed) {
        const result = SSRFProtection.validateUrlSync(url);
        expect(result.valid, `url=${url}`).toBe(false);
      }
    });

    it('should accept valid public URLs', () => {
      const validUrls = [
        'https://n8n.example.com',
        'https://n8n.example.com/api/v1',
        'https://n8n.example.com:8443',
        'http://n8n.example.com/path?query=1',
      ];
      for (const url of validUrls) {
        const result = SSRFProtection.validateUrlSync(url);
        expect(result.valid, `url=${url}`).toBe(true);
        expect(result.reason).toBeUndefined();
      }
    });

    it('should not perform DNS resolution', () => {
      // Spin through a representative set; dns.lookup must never be called.
      SSRFProtection.validateUrlSync('https://n8n.example.com');
      SSRFProtection.validateUrlSync('http://169.254.169.254');
      SSRFProtection.validateUrlSync('http://10.0.0.1');
      SSRFProtection.validateUrlSync('http://localhost');
      SSRFProtection.validateUrlSync('http://evil.example.com#');
      expect(vi.mocked(dns.lookup)).toHaveBeenCalledTimes(0);
    });

    it('should reject non-string input safely', () => {
      // @ts-expect-error testing runtime guard
      const result = SSRFProtection.validateUrlSync(null);
      expect(result.valid).toBe(false);
      expect(result.reason).toBe('URL fragments are not allowed');
    });
  });
});
