import { describe, expect, it } from 'vitest'
import {
  AuthError,
  getAdapter,
  QuotaExhaustedError,
  RateLimitError,
  TransientError,
  type AIRequest,
} from './index.js'
import {
  classifyGoogleError,
  extractGeminiSystem,
  makeGoogleAdapter,
  mapContentToGemini,
  mapGeminiResponse,
  mapMessagesToGemini,
  registerGoogleAdapter,
  type GoogleGenAIClient,
} from './google.js'

describe('google adapter', () => {
  const req: AIRequest = {
    model: 'gemini-2.5-flash',
    messages: [
      { role: 'system', content: 'be helpful' },
      {
        role: 'user',
        content: [
          { type: 'text', text: 'look' },
          {
            type: 'image',
            image: { mediaType: 'image/jpeg', data: 'imgdata' },
          },
        ],
      },
    ],
    maxTokens: 256,
  }

  it('extracts system prompt and maps messages with image parts', () => {
    expect(extractGeminiSystem(req.messages)).toBe('be helpful')
    expect(mapMessagesToGemini(req.messages)).toEqual([
      {
        role: 'user',
        parts: [{ text: 'look' }, { inlineData: { mimeType: 'image/jpeg', data: 'imgdata' } }],
      },
    ])
  })

  it('maps image blocks to gemini inlineData wire shape', () => {
    const wire = mapContentToGemini(req.messages[1]!.content as never)
    expect(wire).toEqual([
      { text: 'look' },
      { inlineData: { mimeType: 'image/jpeg', data: 'imgdata' } },
    ])
  })

  it('maps response usage without cost via injected client', async () => {
    const calls: unknown[] = []
    const client: GoogleGenAIClient = {
      models: {
        async generateContent(params) {
          calls.push(params)
          return {
            text: 'done',
            usageMetadata: { promptTokenCount: 9, candidatesTokenCount: 4 },
          }
        },
      },
    }

    const adapter = makeGoogleAdapter({
      apiKey: 'k',
      createClient: () => client,
    })
    const result = await adapter.call(req)
    expect(result.content).toBe('done')
    expect(result.usage).toEqual({ promptTokens: 9, completionTokens: 4 })
    expect(Object.keys(result.usage)).toEqual(['promptTokens', 'completionTokens'])
    expect(adapter.supportsImage).toBe(true)
    expect(calls[0]).toMatchObject({
      model: 'gemini-2.5-flash',
      config: { systemInstruction: 'be helpful', maxOutputTokens: 256 },
    })
  })

  it('classifies provider HTTP errors with retryable flags', () => {
    expect(classifyGoogleError(429, 'rate', 'google')).toBeInstanceOf(RateLimitError)
    expect(classifyGoogleError(429, 'RESOURCE_EXHAUSTED quota', 'google')).toBeInstanceOf(
      QuotaExhaustedError,
    )
    expect(classifyGoogleError(500, 'boom', 'google')).toBeInstanceOf(TransientError)
    expect(classifyGoogleError(401, 'bad key', 'google')).toBeInstanceOf(AuthError)
    expect(classifyGoogleError(401, 'bad key', 'google').retryable).toBe(false)
  })

  it('mapGeminiResponse omits cost fields', () => {
    const mapped = mapGeminiResponse(
      {
        text: 'x',
        usageMetadata: { promptTokenCount: 1, candidatesTokenCount: 2 },
      },
      'google',
      'gemini-2.5-flash',
    )
    expect(mapped.usage).toEqual({ promptTokens: 1, completionTokens: 2 })
    expect('cost' in mapped.usage).toBe(false)
  })

  it('registerGoogleAdapter wires getAdapter("google") — closes the live FatalError throw', () => {
    registerGoogleAdapter()
    const adapter = getAdapter('google', { apiKey: 'k' })
    expect(adapter.provider).toBe('google')
    expect(typeof adapter.call).toBe('function')
    // seam-level read: supportsImage is visible through the AIAdapter type
    // returned by getAdapter(), not only on the concrete factory return.
    expect(adapter.supportsImage).toBe(true)
  })

  it('throws classified errors from injected client failures', async () => {
    const client: GoogleGenAIClient = {
      models: {
        async generateContent() {
          const err = new Error('quota exceeded') as Error & { status?: number }
          err.status = 429
          throw err
        },
      },
    }
    const adapter = makeGoogleAdapter({ apiKey: 'k', createClient: () => client })
    await expect(adapter.call(req)).rejects.toBeInstanceOf(QuotaExhaustedError)
  })
})
