import { describe, it, expect, beforeEach, afterEach } from '@jest/globals';
import { assertSafeToolProxyHttpUrl } from '../security/outbound-url.js';

const ORIGINAL_ENV = { ...process.env };

describe('assertSafeToolProxyHttpUrl - SSRF protection', () => {
  beforeEach(() => {
    process.env = { ...ORIGINAL_ENV, NODE_ENV: 'development' };
    delete process.env['TOOL_PROXY_HTTP_FETCH_ALLOW_INTERNAL'];
  });

  afterEach(() => {
    process.env = ORIGINAL_ENV;
  });

  describe('valid URLs (development)', () => {
    it('accepts https public URL', () => {
      const url = assertSafeToolProxyHttpUrl('https://example.com/api');
      expect(url.hostname).toBe('example.com');
      expect(url.protocol).toBe('https:');
    });

    it('accepts http public URL in development', () => {
      const url = assertSafeToolProxyHttpUrl('http://example.com/data');
      expect(url.hostname).toBe('example.com');
      expect(url.protocol).toBe('http:');
    });

    it('accepts URL with query string and fragment', () => {
      const url = assertSafeToolProxyHttpUrl('https://api.example.com/v1/users?id=123#section');
      expect(url.search).toBe('?id=123');
      expect(url.hash).toBe('#section');
    });
  });

  describe('production HTTPS-only enforcement', () => {
    beforeEach(() => {
      process.env['NODE_ENV'] = 'production';
    });

    it('accepts https in production', () => {
      const url = assertSafeToolProxyHttpUrl('https://example.com');
      expect(url.protocol).toBe('https:');
    });

    it('rejects http in production', () => {
      expect(() => assertSafeToolProxyHttpUrl('http://example.com')).toThrow(
        'Only HTTPS URLs are allowed in production'
      );
    });
  });

  describe('invalid / malformed URLs', () => {
    it('rejects malformed URL', () => {
      expect(() => assertSafeToolProxyHttpUrl('not-a-url')).toThrow('Invalid URL');
    });

    it('rejects empty string', () => {
      expect(() => assertSafeToolProxyHttpUrl('')).toThrow('Invalid URL');
    });

    it('rejects URL with only whitespace', () => {
      expect(() => assertSafeToolProxyHttpUrl('   ')).toThrow('Invalid URL');
    });
  });

  describe('embedded credentials (userinfo)', () => {
    it('rejects URL with username', () => {
      expect(() => assertSafeToolProxyHttpUrl('https://user@example.com')).toThrow(
        'URL must not contain embedded credentials'
      );
    });

    it('rejects URL with username and password', () => {
      expect(() => assertSafeToolProxyHttpUrl('https://user:pass@example.com')).toThrow(
        'URL must not contain embedded credentials'
      );
    });

    it('rejects http URL with credentials', () => {
      expect(() => assertSafeToolProxyHttpUrl('http://admin:secret@internal.lan')).toThrow(
        'URL must not contain embedded credentials'
      );
    });
  });

  describe('blocked metadata hosts', () => {
    it('rejects AWS metadata endpoint (169.254.169.254)', () => {
      expect(() => assertSafeToolProxyHttpUrl('http://169.254.169.254/latest/meta-data/')).toThrow(
        'Host is not allowed'
      );
    });

    it('rejects GCP metadata endpoint (metadata.google.internal)', () => {
      expect(() => assertSafeToolProxyHttpUrl('http://metadata.google.internal/computeMetadata/v1/')).toThrow(
        'Host is not allowed'
      );
    });

    it('rejects short metadata hostname', () => {
      expect(() => assertSafeToolProxyHttpUrl('http://metadata/')).toThrow('Host is not allowed');
    });

    it('rejects metadata endpoint with https', () => {
      expect(() => assertSafeToolProxyHttpUrl('https://169.254.169.254/')).toThrow(
        'Host is not allowed'
      );
    });
  });

  describe('private / loopback hosts (default deny)', () => {
    it('rejects localhost', () => {
      expect(() => assertSafeToolProxyHttpUrl('http://localhost:3000')).toThrow(
        'Private or loopback hosts are not allowed for this fetch'
      );
    });

    it('rejects 127.0.0.1', () => {
      expect(() => assertSafeToolProxyHttpUrl('http://127.0.0.1:8080')).toThrow(
        'Private or loopback hosts are not allowed for this fetch'
      );
    });

    it('rejects 127.* loopback range', () => {
      expect(() => assertSafeToolProxyHttpUrl('http://127.1.2.3/')).toThrow(
        'Private or loopback hosts are not allowed for this fetch'
      );
    });

    it('rejects 0.0.0.0', () => {
      expect(() => assertSafeToolProxyHttpUrl('http://0.0.0.0/')).toThrow(
        'Private or loopback hosts are not allowed for this fetch'
      );
    });

    it('rejects 10.x.x.x private range', () => {
      expect(() => assertSafeToolProxyHttpUrl('http://10.0.0.1/')).toThrow(
        'Private or loopback hosts are not allowed for this fetch'
      );
    });

    it('rejects 192.168.x.x private range', () => {
      expect(() => assertSafeToolProxyHttpUrl('http://192.168.1.1/')).toThrow(
        'Private or loopback hosts are not allowed for this fetch'
      );
    });

    it('rejects 172.16-31.x.x private range (172.16)', () => {
      expect(() => assertSafeToolProxyHttpUrl('http://172.16.0.1/')).toThrow(
        'Private or loopback hosts are not allowed for this fetch'
      );
    });

    it('rejects 172.16-31.x.x private range (172.31)', () => {
      expect(() => assertSafeToolProxyHttpUrl('http://172.31.255.254/')).toThrow(
        'Private or loopback hosts are not allowed for this fetch'
      );
    });

    it('rejects link-local 169.254.x.x', () => {
      expect(() => assertSafeToolProxyHttpUrl('http://169.254.1.1/')).toThrow(
        'Private or loopback hosts are not allowed for this fetch'
      );
    });

    it('rejects IPv6 loopback ::1', () => {
      expect(() => assertSafeToolProxyHttpUrl('http://[::1]:3000/')).toThrow(
        'Private or loopback hosts are not allowed for this fetch'
      );
    });

    it('rejects IPv6 link-local fe80::', () => {
      expect(() => assertSafeToolProxyHttpUrl('http://[fe80::1]/')).toThrow(
        'Private or loopback hosts are not allowed for this fetch'
      );
    });

    it('rejects IPv6 ULA fc00::', () => {
      expect(() => assertSafeToolProxyHttpUrl('http://[fc00::1]/')).toThrow(
        'Private or loopback hosts are not allowed for this fetch'
      );
    });

    it('rejects IPv6 ULA fd00::', () => {
      expect(() => assertSafeToolProxyHttpUrl('http://[fd00:1234::1]/')).toThrow(
        'Private or loopback hosts are not allowed for this fetch'
      );
    });
  });

  describe('ALLOW_INTERNAL_FETCH escape hatch', () => {
    beforeEach(() => {
      process.env['TOOL_PROXY_HTTP_FETCH_ALLOW_INTERNAL'] = 'true';
    });

    it('allows localhost when ALLOW_INTERNAL_FETCH=true', () => {
      const url = assertSafeToolProxyHttpUrl('http://localhost:3000');
      expect(url.hostname).toBe('localhost');
    });

    it('allows 127.0.0.1 when ALLOW_INTERNAL_FETCH=true', () => {
      const url = assertSafeToolProxyHttpUrl('http://127.0.0.1:8080');
      expect(url.hostname).toBe('127.0.0.1');
    });

    it('allows 10.x private when ALLOW_INTERNAL_FETCH=true', () => {
      const url = assertSafeToolProxyHttpUrl('http://10.0.0.1');
      expect(url.hostname).toBe('10.0.0.1');
    });

    it('still blocks metadata endpoint even with ALLOW_INTERNAL_FETCH=true', () => {
      expect(() => assertSafeToolProxyHttpUrl('http://169.254.169.254/')).toThrow(
        'Host is not allowed'
      );
    });

    it('still blocks metadata.google.internal with ALLOW_INTERNAL_FETCH=true', () => {
      expect(() => assertSafeToolProxyHttpUrl('http://metadata.google.internal/')).toThrow(
        'Host is not allowed'
      );
    });
  });

  describe('unsupported protocols', () => {
    it('rejects file:// protocol', () => {
      expect(() => assertSafeToolProxyHttpUrl('file:///etc/passwd')).toThrow(
        'Only HTTP and HTTPS URLs are allowed'
      );
    });

    it('rejects ftp:// protocol', () => {
      expect(() => assertSafeToolProxyHttpUrl('ftp://ftp.example.com/file.txt')).toThrow(
        'Only HTTP and HTTPS URLs are allowed'
      );
    });

    it('rejects gopher:// protocol', () => {
      expect(() => assertSafeToolProxyHttpUrl('gopher://gopher.example.com/')).toThrow(
        'Only HTTP and HTTPS URLs are allowed'
      );
    });

    it('rejects data: URIs', () => {
      expect(() => assertSafeToolProxyHttpUrl('data:text/plain,hello')).toThrow(
        'Only HTTP and HTTPS URLs are allowed'
      );
    });

    it('rejects javascript: URIs', () => {
      expect(() => assertSafeToolProxyHttpUrl('javascript:alert(1)')).toThrow(
        'Only HTTP and HTTPS URLs are allowed'
      );
    });
  });

  describe('case sensitivity', () => {
    it('normalizes hostname to lowercase for metadata check', () => {
      expect(() => assertSafeToolProxyHttpUrl('http://METADATA.GOOGLE.INTERNAL/')).toThrow(
        'Host is not allowed'
      );
    });

    it('normalizes hostname for localhost check', () => {
      expect(() => assertSafeToolProxyHttpUrl('http://LocalHost:3000')).toThrow(
        'Private or loopback hosts are not allowed for this fetch'
      );
    });

    it('accepts uppercase scheme', () => {
      const url = assertSafeToolProxyHttpUrl('HTTPS://example.com');
      expect(url.protocol).toBe('https:');
    });
  });

  describe('edge cases', () => {
    it('rejects URL with no hostname (e.g., relative path)', () => {
      expect(() => assertSafeToolProxyHttpUrl('/relative/path')).toThrow('Invalid URL');
    });

    it('accepts public IP (non-private)', () => {
      const url = assertSafeToolProxyHttpUrl('https://8.8.8.8/');
      expect(url.hostname).toBe('8.8.8.8');
    });

    it('accepts subdomain', () => {
      const url = assertSafeToolProxyHttpUrl('https://api.staging.example.com/v2');
      expect(url.hostname).toBe('api.staging.example.com');
    });

    it('accepts non-standard port', () => {
      const url = assertSafeToolProxyHttpUrl('https://example.com:8443/api');
      expect(url.port).toBe('8443');
    });

    it('rejects URL-encoded private IP (basic check - hostname not decoded)', () => {
      // URL parser normalizes hostname, but our checks happen post-parse
      // This test documents current behavior; URL('http://127.0.0.1') → hostname: '127.0.0.1'
      expect(() => assertSafeToolProxyHttpUrl('http://127.0.0.1/')).toThrow(
        'Private or loopback hosts are not allowed for this fetch'
      );
    });
  });
});
