import { describe, it, expect, vi, beforeEach, afterEach } from 'vitest';
import {
  validateWebhookUrl,
  sanitizeVariables,
  resolveVariables,
  buildAIAdapter,
} from '../../src/server';
import type { LocalConfig } from '../../src/server';
import type { AgentOptions } from '../../src/types';

// ---------------------------------------------------------------------------
// SSRF validation (validateWebhookUrl)
// ---------------------------------------------------------------------------

describe('validateWebhookUrl()', () => {
  it('accepts valid HTTPS URLs', () => {
    expect(() => validateWebhookUrl('https://api.example.com/hook')).not.toThrow();
  });

  it('accepts valid HTTP URLs', () => {
    expect(() => validateWebhookUrl('http://api.example.com/hook')).not.toThrow();
  });

  it('rejects non-HTTP(S) schemes', () => {
    expect(() => validateWebhookUrl('ftp://example.com/hook')).toThrow('Invalid webhook URL scheme');
    expect(() => validateWebhookUrl('file:///etc/passwd')).toThrow('Invalid webhook URL scheme');
    expect(() => validateWebhookUrl('javascript:alert(1)')).toThrow();
  });

  it('blocks localhost', () => {
    expect(() => validateWebhookUrl('https://localhost/hook')).toThrow('private/internal address');
  });

  it('blocks 127.0.0.x range', () => {
    expect(() => validateWebhookUrl('https://127.0.0.1/hook')).toThrow('private/internal address');
    expect(() => validateWebhookUrl('https://127.0.0.99/hook')).toThrow('private/internal address');
  });

  it('blocks 10.x.x.x range', () => {
    expect(() => validateWebhookUrl('https://10.0.0.1/hook')).toThrow('private/internal address');
    expect(() => validateWebhookUrl('https://10.255.255.255/hook')).toThrow('private/internal address');
  });

  it('blocks 172.16-31.x.x range', () => {
    expect(() => validateWebhookUrl('https://172.16.0.1/hook')).toThrow('private/internal address');
    expect(() => validateWebhookUrl('https://172.31.255.255/hook')).toThrow('private/internal address');
  });

  it('allows 172.15.x.x (not in private range)', () => {
    expect(() => validateWebhookUrl('https://172.15.0.1/hook')).not.toThrow();
  });

  it('blocks 192.168.x.x range', () => {
    expect(() => validateWebhookUrl('https://192.168.1.1/hook')).toThrow('private/internal address');
  });

  it('blocks 169.254.x.x (link-local)', () => {
    expect(() => validateWebhookUrl('https://169.254.0.1/hook')).toThrow('private/internal address');
  });

  it('blocks 0.x.x.x range', () => {
    expect(() => validateWebhookUrl('https://0.0.0.0/hook')).toThrow('private/internal address');
    expect(() => validateWebhookUrl('https://0.1.2.3/hook')).toThrow('private/internal address');
  });

  it('blocks IPv6 loopback (bracketed literal)', () => {
    expect(() => validateWebhookUrl('http://[::1]:8080/hook')).toThrow('private/internal address');
    expect(() => validateWebhookUrl('http://[::]/hook')).toThrow('private/internal address');
  });

  it('blocks IPv6 unique-local (fc00::/7) and link-local (fe80::/10)', () => {
    expect(() => validateWebhookUrl('http://[fc00::1]/hook')).toThrow('private/internal address');
    expect(() => validateWebhookUrl('http://[fd12:3456::1]/hook')).toThrow('private/internal address');
    expect(() => validateWebhookUrl('http://[fe80::1]/hook')).toThrow('private/internal address');
  });

  it('blocks ip6-localhost / ip6-loopback aliases', () => {
    expect(() => validateWebhookUrl('http://ip6-localhost/hook')).toThrow('private/internal address');
    expect(() => validateWebhookUrl('http://ip6-loopback/hook')).toThrow('private/internal address');
  });

  it('blocks metadata.google.internal', () => {
    expect(() => validateWebhookUrl('https://metadata.google.internal/hook')).toThrow(
      'private/internal address',
    );
  });

  it('blocks bare `metadata` and metadata.azure.com', () => {
    expect(() => validateWebhookUrl('http://metadata/hook')).toThrow('private/internal address');
    expect(() => validateWebhookUrl('https://metadata.azure.com/hook')).toThrow('private/internal address');
  });

  it('throws on malformed URL', () => {
    expect(() => validateWebhookUrl('not-a-url')).toThrow();
  });
});

// ---------------------------------------------------------------------------
// sanitizeVariables
// ---------------------------------------------------------------------------

describe('sanitizeVariables()', () => {
  it('converts all values to strings', () => {
    const result = sanitizeVariables({ name: 'Alice', age: 30 as unknown });
    expect(result.name).toBe('Alice');
    expect(result.age).toBe('30');
  });

  it('strips __proto__ key (prototype pollution defense)', () => {
    const result = sanitizeVariables({ __proto__: 'evil', safe: 'ok' });
    expect(result.__proto__).toBeUndefined();
    expect(result.safe).toBe('ok');
  });

  it('strips constructor key', () => {
    const result = sanitizeVariables({ constructor: 'evil', safe: 'ok' });
    expect(result.constructor).toBeUndefined();
    expect(result.safe).toBe('ok');
  });

  it('strips prototype key', () => {
    const result = sanitizeVariables({ prototype: 'evil', safe: 'ok' });
    expect(result.prototype).toBeUndefined();
    expect(result.safe).toBe('ok');
  });

  it('handles null/undefined values gracefully', () => {
    const result = sanitizeVariables({
      a: null as unknown,
      b: undefined as unknown,
    });
    expect(result.a).toBe('');
    expect(result.b).toBe('');
  });

  it('returns a plain object with no inherited props', () => {
    const result = sanitizeVariables({ key: 'value' });
    expect(Object.getPrototypeOf(result)).toBeNull();
  });
});

// ---------------------------------------------------------------------------
// resolveVariables
// ---------------------------------------------------------------------------

describe('resolveVariables()', () => {
  it('replaces {key} placeholders', () => {
    const result = resolveVariables('Hello {name}, welcome to {company}!', {
      name: 'Alice',
      company: 'Patter',
    });
    expect(result).toBe('Hello Alice, welcome to Patter!');
  });

  it('replaces all occurrences of same placeholder', () => {
    const result = resolveVariables('{x} and {x}', { x: 'Y' });
    expect(result).toBe('Y and Y');
  });

  it('leaves unmatched placeholders as-is', () => {
    const result = resolveVariables('Hello {name}!', {});
    expect(result).toBe('Hello {name}!');
  });

  it('handles empty template', () => {
    const result = resolveVariables('', { name: 'test' });
    expect(result).toBe('');
  });
});

// ---------------------------------------------------------------------------
// buildAIAdapter
// ---------------------------------------------------------------------------

describe('buildAIAdapter()', () => {
  it('returns OpenAI adapter by default', () => {
    const config: LocalConfig = {
      openaiKey: 'test-key',
      phoneNumber: '+15551234567',
      webhookUrl: 'example.com',
    };
    const agent: AgentOptions = {
      systemPrompt: 'Test prompt',
    };
    const adapter = buildAIAdapter(config, agent, 'Resolved prompt');
    expect(adapter).toBeDefined();
    // It should be an OpenAIRealtimeAdapter (has sendText method)
    expect(typeof (adapter as Record<string, unknown>).sendAudio).toBe('function');
  });

  it('returns ElevenLabs adapter when engine is ElevenLabsConvAI', async () => {
    const { ElevenLabsConvAI } = await import('../../src/index');
    const config: LocalConfig = {
      phoneNumber: '+15551234567',
      webhookUrl: 'example.com',
    };
    const agent: AgentOptions = {
      systemPrompt: 'Test',
      provider: 'elevenlabs_convai',
      engine: new ElevenLabsConvAI({ apiKey: 'el-key', agentId: 'agent-id' }),
    };
    const adapter = buildAIAdapter(config, agent);
    expect(adapter).toBeDefined();
  });

  it('buildAIAdapter throws without engine for elevenlabs_convai', () => {
    const config: LocalConfig = {
      phoneNumber: '+15551234567',
      webhookUrl: 'example.com',
    };
    const agent: AgentOptions = {
      systemPrompt: 'Test',
      provider: 'elevenlabs_convai',
    };
    expect(() => buildAIAdapter(config, agent)).toThrow(/engine/);
  });

  it('injects transfer_call and end_call tools alongside agent tools', () => {
    const config: LocalConfig = {
      openaiKey: 'test-key',
      phoneNumber: '+15551234567',
      webhookUrl: 'example.com',
    };
    const agent: AgentOptions = {
      systemPrompt: 'Test',
      tools: [
        {
          name: 'my_tool',
          description: 'desc',
          parameters: { type: 'object', properties: {} },
          webhookUrl: 'https://example.com/hook',
        },
      ],
    };
    // The adapter is constructed with tools; we can verify it doesn't throw
    const adapter = buildAIAdapter(config, agent, 'prompt');
    expect(adapter).toBeDefined();
  });
});

// ---------------------------------------------------------------------------
// Error middleware / JSON response shape (tested via status codes in route handlers)
// ---------------------------------------------------------------------------

describe('Error response shape', () => {
  it('validateWebhookUrl produces Error with descriptive message', () => {
    try {
      validateWebhookUrl('ftp://evil.com');
    } catch (err) {
      expect(err).toBeInstanceOf(Error);
      expect((err as Error).message).toContain('Invalid webhook URL scheme');
    }
  });
});
