import { describe, it, expect, vi, beforeEach, afterEach } from 'vitest';
import {
  TranscribeRequestSchema,
  MAX_AUDIO_SIZE_BYTES,
  SUPPORTED_AUDIO_TYPES,
  deriveFilename,
} from './types.js';
import type { TranscriptionProvider } from './types.js';
import { MockTranscriptionProvider } from './providers/mock.js';
import { OpenAITranscriptionProvider } from './providers/openai.js';
import { AzureTranscriptionProvider } from './providers/azure.js';
import { createSTTProvider, _resetSTT, getSTT, setSTT } from './factory.js';
import { toAzureLocale } from './providers/azure.js';

// ── Schema validation tests ─────────────────────────────────────

describe('TranscribeRequestSchema', () => {
  it('validates a minimal valid request', () => {
    const result = TranscribeRequestSchema.safeParse({
      audioUrl: 'https://blob.example.com/audio.mp3',
    });
    expect(result.success).toBe(true);
    if (result.success) {
      expect(result.data.audioUrl).toBe('https://blob.example.com/audio.mp3');
      expect(result.data.responseFormat).toBe('text');
    }
  });

  it('validates a full request with all optional fields', () => {
    const result = TranscribeRequestSchema.safeParse({
      audioUrl: 'https://blob.example.com/audio.wav',
      model: 'whisper-1',
      language: 'en',
      prompt: 'Technical meeting about software architecture.',
      responseFormat: 'verbose_json',
      productId: 'notelett',
    });
    expect(result.success).toBe(true);
    if (result.success) {
      expect(result.data.language).toBe('en');
      expect(result.data.responseFormat).toBe('verbose_json');
      expect(result.data.productId).toBe('notelett');
    }
  });

  it('rejects missing audioUrl', () => {
    const result = TranscribeRequestSchema.safeParse({});
    expect(result.success).toBe(false);
  });

  it('rejects invalid URL', () => {
    const result = TranscribeRequestSchema.safeParse({
      audioUrl: 'not-a-url',
    });
    expect(result.success).toBe(false);
  });

  it('rejects invalid responseFormat', () => {
    const result = TranscribeRequestSchema.safeParse({
      audioUrl: 'https://example.com/a.mp3',
      responseFormat: 'xml',
    });
    expect(result.success).toBe(false);
  });

  it('rejects language longer than 5 chars', () => {
    const result = TranscribeRequestSchema.safeParse({
      audioUrl: 'https://example.com/a.mp3',
      language: 'toolong',
    });
    expect(result.success).toBe(false);
  });

  it('rejects prompt longer than 1000 chars', () => {
    const result = TranscribeRequestSchema.safeParse({
      audioUrl: 'https://example.com/a.mp3',
      prompt: 'x'.repeat(1001),
    });
    expect(result.success).toBe(false);
  });
});

// ── Constants tests ─────────────────────────────────────────────

describe('transcription constants', () => {
  it('MAX_AUDIO_SIZE_BYTES is 25MB', () => {
    expect(MAX_AUDIO_SIZE_BYTES).toBe(25 * 1024 * 1024);
  });

  it('SUPPORTED_AUDIO_TYPES includes common audio formats', () => {
    expect(SUPPORTED_AUDIO_TYPES.has('audio/mpeg')).toBe(true);
    expect(SUPPORTED_AUDIO_TYPES.has('audio/wav')).toBe(true);
    expect(SUPPORTED_AUDIO_TYPES.has('audio/webm')).toBe(true);
    expect(SUPPORTED_AUDIO_TYPES.has('audio/ogg')).toBe(true);
    expect(SUPPORTED_AUDIO_TYPES.has('audio/flac')).toBe(true);
    expect(SUPPORTED_AUDIO_TYPES.has('audio/mp4')).toBe(true);
  });

  it('SUPPORTED_AUDIO_TYPES does not include text types', () => {
    expect(SUPPORTED_AUDIO_TYPES.has('text/plain')).toBe(false);
    expect(SUPPORTED_AUDIO_TYPES.has('application/json')).toBe(false);
  });
});

// ── deriveFilename tests ────────────────────────────────────────

describe('deriveFilename', () => {
  it('extracts filename from URL path', () => {
    expect(deriveFilename('https://blob.example.com/uploads/meeting.wav?sv=2024', null)).toBe(
      'meeting.wav'
    );
  });

  it('extracts filename with various extensions', () => {
    expect(deriveFilename('https://x.com/a.mp3', null)).toBe('a.mp3');
    expect(deriveFilename('https://x.com/b.m4a', null)).toBe('b.m4a');
    expect(deriveFilename('https://x.com/c.webm', null)).toBe('c.webm');
    expect(deriveFilename('https://x.com/d.flac', null)).toBe('d.flac');
  });

  it('falls back to content-type mapping', () => {
    expect(deriveFilename('https://x.com/blob123', 'audio/mpeg')).toBe('audio.mp3');
    expect(deriveFilename('https://x.com/blob123', 'audio/wav')).toBe('audio.wav');
    expect(deriveFilename('https://x.com/blob123', 'audio/webm')).toBe('audio.webm');
    expect(deriveFilename('https://x.com/blob123', 'audio/ogg')).toBe('audio.ogg');
  });

  it('handles content-type with charset parameter', () => {
    expect(deriveFilename('https://x.com/blob', 'audio/mpeg; charset=utf-8')).toBe('audio.mp3');
  });

  it('defaults to audio.mp3 when no match', () => {
    expect(deriveFilename('https://x.com/blob', null)).toBe('audio.mp3');
    expect(deriveFilename('https://x.com/blob', 'application/octet-stream')).toBe('audio.mp3');
  });
});

// ── MockTranscriptionProvider tests ─────────────────────────────

describe('MockTranscriptionProvider', () => {
  it('is always configured', () => {
    const provider = new MockTranscriptionProvider();
    expect(provider.isConfigured()).toBe(true);
  });

  it('returns deterministic transcription with filename and size', async () => {
    const provider = new MockTranscriptionProvider();
    const audio = new ArrayBuffer(16000);
    const result = await provider.transcribe({
      audio,
      filename: 'test.mp3',
    });

    expect(result.text).toContain('Mock transcription');
    expect(result.text).toContain('test.mp3');
    expect(result.text).toContain('16KB');
    expect(result.model).toBe('mock-stt');
    expect(result.durationSeconds).toBeGreaterThan(0);
  });

  it('uses provided language', async () => {
    const provider = new MockTranscriptionProvider();
    const result = await provider.transcribe({
      audio: new ArrayBuffer(100),
      filename: 'a.wav',
      language: 'es',
    });
    expect(result.language).toBe('es');
  });

  it('defaults language to en', async () => {
    const provider = new MockTranscriptionProvider();
    const result = await provider.transcribe({
      audio: new ArrayBuffer(100),
      filename: 'a.wav',
    });
    expect(result.language).toBe('en');
  });
});

// ── OpenAITranscriptionProvider tests ───────────────────────────

describe('OpenAITranscriptionProvider', () => {
  it('is not configured without OPENAI_API_KEY', () => {
    const provider = new OpenAITranscriptionProvider({ apiKey: '' });
    expect(provider.isConfigured()).toBe(false);
  });

  it('is configured with apiKey', () => {
    const provider = new OpenAITranscriptionProvider({ apiKey: 'sk-test' });
    expect(provider.isConfigured()).toBe(true);
  });

  it('throws when transcribing without config', async () => {
    const provider = new OpenAITranscriptionProvider({ apiKey: '' });
    await expect(
      provider.transcribe({ audio: new ArrayBuffer(10), filename: 'a.mp3' })
    ).rejects.toThrow('not configured');
  });
});

// ── toAzureLocale tests ─────────────────────────────────────────

describe('toAzureLocale', () => {
  it('maps common 2-letter codes to correct Azure locales', () => {
    expect(toAzureLocale('en')).toBe('en-US');
    expect(toAzureLocale('es')).toBe('es-ES');
    expect(toAzureLocale('fr')).toBe('fr-FR');
    expect(toAzureLocale('de')).toBe('de-DE');
    expect(toAzureLocale('pt')).toBe('pt-BR');
    expect(toAzureLocale('ja')).toBe('ja-JP');
    expect(toAzureLocale('zh')).toBe('zh-CN');
    expect(toAzureLocale('hi')).toBe('hi-IN');
  });

  it('passes through full BCP-47 locales unchanged', () => {
    expect(toAzureLocale('en-US')).toBe('en-US');
    expect(toAzureLocale('pt-BR')).toBe('pt-BR');
    expect(toAzureLocale('zh-TW')).toBe('zh-TW');
  });

  it('falls back to code-CODE for unmapped languages', () => {
    expect(toAzureLocale('xx')).toBe('xx-XX');
  });

  it('is case-insensitive for 2-letter codes', () => {
    expect(toAzureLocale('EN')).toBe('en-US');
    expect(toAzureLocale('Es')).toBe('es-ES');
  });
});

// ── AzureTranscriptionProvider tests ────────────────────────────

describe('AzureTranscriptionProvider', () => {
  it('is not configured without both key and region', () => {
    expect(new AzureTranscriptionProvider({ speechKey: '', speechRegion: '' }).isConfigured()).toBe(
      false
    );
    expect(
      new AzureTranscriptionProvider({ speechKey: 'k', speechRegion: '' }).isConfigured()
    ).toBe(false);
    expect(
      new AzureTranscriptionProvider({ speechKey: '', speechRegion: 'eastus' }).isConfigured()
    ).toBe(false);
  });

  it('is configured with both key and region', () => {
    const provider = new AzureTranscriptionProvider({ speechKey: 'k', speechRegion: 'eastus' });
    expect(provider.isConfigured()).toBe(true);
  });

  it('throws when transcribing without config', async () => {
    const provider = new AzureTranscriptionProvider({ speechKey: '', speechRegion: '' });
    await expect(
      provider.transcribe({ audio: new ArrayBuffer(10), filename: 'a.wav' })
    ).rejects.toThrow('not configured');
  });
});

// ── Factory tests ───────────────────────────────────────────────

describe('STT factory', () => {
  beforeEach(() => {
    _resetSTT();
  });

  afterEach(() => {
    _resetSTT();
    vi.unstubAllEnvs();
  });

  it('createSTTProvider returns OpenAI provider', () => {
    const provider = createSTTProvider('openai');
    expect(provider).toBeInstanceOf(OpenAITranscriptionProvider);
  });

  it('createSTTProvider returns Azure provider', () => {
    const provider = createSTTProvider('azure');
    expect(provider).toBeInstanceOf(AzureTranscriptionProvider);
  });

  it('createSTTProvider returns Mock provider', () => {
    const provider = createSTTProvider('mock');
    expect(provider).toBeInstanceOf(MockTranscriptionProvider);
  });

  it('createSTTProvider throws for unknown type', () => {
    expect(() => createSTTProvider('invalid' as 'openai')).toThrow('Unknown STT_PROVIDER');
  });

  it('getSTT auto-detects mock when no credentials', () => {
    vi.stubEnv('OPENAI_API_KEY', '');
    vi.stubEnv('AZURE_SPEECH_KEY', '');
    vi.stubEnv('AZURE_SPEECH_REGION', '');
    vi.stubEnv('STT_PROVIDER', '');
    _resetSTT();
    const provider = getSTT();
    expect(provider).toBeInstanceOf(MockTranscriptionProvider);
  });

  it('getSTT auto-detects openai when OPENAI_API_KEY set', () => {
    vi.stubEnv('OPENAI_API_KEY', 'sk-test');
    vi.stubEnv('AZURE_SPEECH_KEY', '');
    vi.stubEnv('AZURE_SPEECH_REGION', '');
    vi.stubEnv('STT_PROVIDER', '');
    _resetSTT();
    const provider = getSTT();
    expect(provider).toBeInstanceOf(OpenAITranscriptionProvider);
  });

  it('getSTT auto-detects azure when AZURE_SPEECH_KEY + REGION set', () => {
    vi.stubEnv('OPENAI_API_KEY', 'sk-test');
    vi.stubEnv('AZURE_SPEECH_KEY', 'my-key');
    vi.stubEnv('AZURE_SPEECH_REGION', 'eastus');
    vi.stubEnv('STT_PROVIDER', '');
    _resetSTT();
    const provider = getSTT();
    expect(provider).toBeInstanceOf(AzureTranscriptionProvider);
  });

  it('getSTT respects explicit STT_PROVIDER over auto-detect', () => {
    vi.stubEnv('OPENAI_API_KEY', 'sk-test');
    vi.stubEnv('AZURE_SPEECH_KEY', 'my-key');
    vi.stubEnv('AZURE_SPEECH_REGION', 'eastus');
    vi.stubEnv('STT_PROVIDER', 'mock');
    _resetSTT();
    const provider = getSTT();
    expect(provider).toBeInstanceOf(MockTranscriptionProvider);
  });

  it('getSTT returns singleton', () => {
    vi.stubEnv('STT_PROVIDER', 'mock');
    _resetSTT();
    const p1 = getSTT();
    const p2 = getSTT();
    expect(p1).toBe(p2);
  });

  it('setSTT overrides the singleton', () => {
    vi.stubEnv('STT_PROVIDER', 'openai');
    _resetSTT();
    const mock = new MockTranscriptionProvider();
    setSTT(mock);
    expect(getSTT()).toBe(mock);
  });

  it('TranscriptionProvider interface is satisfied by all providers', () => {
    const providers: TranscriptionProvider[] = [
      new MockTranscriptionProvider(),
      new OpenAITranscriptionProvider({ apiKey: 'test' }),
      new AzureTranscriptionProvider({ speechKey: 'k', speechRegion: 'eastus' }),
    ];
    for (const p of providers) {
      expect(typeof p.transcribe).toBe('function');
      expect(typeof p.isConfigured).toBe('function');
    }
  });
});

// ── Model passthrough tests ─────────────────────────────────────

describe('model passthrough', () => {
  it('MockTranscriptionProvider ignores model (returns mock-stt)', async () => {
    const provider = new MockTranscriptionProvider();
    const result = await provider.transcribe({
      audio: new ArrayBuffer(100),
      filename: 'a.wav',
      model: 'whisper-large-v3',
    });
    expect(result.model).toBe('mock-stt');
  });

  it('TranscriptionInput accepts optional model field', () => {
    const input = {
      audio: new ArrayBuffer(10),
      filename: 'a.mp3',
      model: 'whisper-1',
    };
    expect(input.model).toBe('whisper-1');
  });
});
