import type { LanguageModelV3CallOptions } from '@ai-sdk/provider'
import type { Model, Provider, ProviderType } from '@renderer/types'
import { beforeEach, describe, expect, it, vi } from 'vitest'

vi.mock('i18next', () => ({
  default: { t: (key: string, opts?: Record<string, unknown>) => `${key}${opts ? JSON.stringify(opts) : ''}` }
}))

vi.mock('@renderer/config/models', () => ({
  isAnthropicModel: vi.fn(() => false),
  isGeminiModel: vi.fn(() => false)
}))

vi.mock('@renderer/config/models/openai', () => ({
  isOpenAILLMModel: vi.fn(() => false)
}))

const mockExtractPdfText = vi.fn()

vi.mock('@shared/utils/pdf', () => ({
  extractPdfText: (...args: unknown[]) => mockExtractPdfText(...args)
}))

vi.stubGlobal('window', {
  ...globalThis.window,
  api: {
    pdf: {
      extractText: mockExtractPdfText
    }
  },
  toast: {
    warning: vi.fn(),
    error: vi.fn()
  }
})

import { isAnthropicModel, isGeminiModel } from '@renderer/config/models'
import { isOpenAILLMModel } from '@renderer/config/models/openai'

import { createPdfCompatibilityPlugin } from '../pdfCompatibilityPlugin'

function makeProvider(id: string, type: ProviderType): Provider {
  return { id, name: id, type, apiKey: 'test', apiHost: 'https://test.com', isSystem: false, models: [] } as Provider
}

function makeModel(overrides: Partial<Model> = {}): Model {
  return { id: 'test-model', provider: 'test', name: 'Test', group: 'test', ...overrides } as Model
}

function makePdfFilePart(filename = 'test.pdf') {
  return {
    type: 'file' as const,
    data: 'base64pdfdata',
    mediaType: 'application/pdf',
    filename
  }
}

function makeImageFilePart() {
  return {
    type: 'file' as const,
    data: 'base64imgdata',
    mediaType: 'image/png',
    filename: 'test.png'
  }
}

function makeTextPart(text: string) {
  return { type: 'text' as const, text }
}

async function runMiddleware(provider: Provider, params: LanguageModelV3CallOptions, model: Model = makeModel()) {
  const plugin = createPdfCompatibilityPlugin(provider, model)
  const context: {
    middlewares: Array<{ transformParams: (opts: Record<string, unknown>) => Promise<LanguageModelV3CallOptions> }>
  } = { middlewares: [] }
  void plugin.configureContext!(context as never)
  const middleware = context.middlewares[0]
  return middleware.transformParams({ params, type: 'generate', model: {} })
}

describe('pdfCompatibilityPlugin', () => {
  beforeEach(() => {
    vi.clearAllMocks()
    vi.mocked(isOpenAILLMModel).mockReturnValue(false)
    vi.mocked(isAnthropicModel).mockReturnValue(false)
    vi.mocked(isGeminiModel).mockReturnValue(false)
  })

  it('should pass through for OpenAI model routed via openai-response endpoint', async () => {
    vi.mocked(isOpenAILLMModel).mockReturnValue(true)
    const provider = makeProvider('moonshot', 'openai')
    const model = makeModel({ endpoint_type: 'openai-response' as const })

    const params = {
      prompt: [{ role: 'user' as const, content: [makeTextPart('Hello'), makePdfFilePart()] }]
    } as unknown as LanguageModelV3CallOptions

    const result = await runMiddleware(provider, params, model)
    expect(result).toEqual(params)
    expect(mockExtractPdfText).not.toHaveBeenCalled()
  })

  it('should convert PDF for OpenAI model on generic openai provider without endpoint_type', async () => {
    vi.mocked(isOpenAILLMModel).mockReturnValue(true)
    const provider = makeProvider('moonshot', 'openai')
    mockExtractPdfText.mockResolvedValue('Extracted PDF content')

    const params = {
      prompt: [{ role: 'user' as const, content: [makeTextPart('Hello'), makePdfFilePart('report.pdf')] }]
    } as unknown as LanguageModelV3CallOptions

    const result = await runMiddleware(provider, params)
    expect(mockExtractPdfText).toHaveBeenCalledWith('base64pdfdata')
    expect(result.prompt[0]).toMatchObject({
      role: 'user',
      content: [
        { type: 'text', text: 'Hello' },
        { type: 'text', text: 'report.pdf\nExtracted PDF content' }
      ]
    })
  })

  it('should pass through for Claude model routed via Anthropic endpoint (endpoint_type=anthropic)', async () => {
    vi.mocked(isAnthropicModel).mockReturnValue(true)
    const provider = makeProvider('my-aggregator', 'new-api')
    const claudeModel = { ...makeModel(), id: 'claude-opus-4-7', endpoint_type: 'anthropic' as const }

    const params = {
      prompt: [{ role: 'user' as const, content: [makeTextPart('Hello'), makePdfFilePart()] }]
    } as unknown as LanguageModelV3CallOptions

    const result = await runMiddleware(provider, params, claudeModel)
    expect(result).toEqual(params)
    expect(mockExtractPdfText).not.toHaveBeenCalled()
  })

  it('should convert PDF for Claude model routed via chat-completions endpoint (no endpoint_type)', async () => {
    // An Anthropic-named model wired through an OpenAI-compatible aggregator (e.g. NewAPI
    // without endpoint_type='anthropic') uses chat-completions, which does NOT accept the
    // 'file' part type. Native PDF must be downgraded to text in this case.
    vi.mocked(isAnthropicModel).mockReturnValue(true)
    const provider = makeProvider('my-aggregator', 'new-api')
    const claudeModel = { ...makeModel(), id: 'claude-opus-4-7' }
    mockExtractPdfText.mockResolvedValue('Extracted PDF content')

    const params = {
      prompt: [{ role: 'user' as const, content: [makeTextPart('Hello'), makePdfFilePart('report.pdf')] }]
    } as unknown as LanguageModelV3CallOptions

    const result = await runMiddleware(provider, params, claudeModel)
    expect(mockExtractPdfText).toHaveBeenCalledWith('base64pdfdata')
    expect(result.prompt[0]).toMatchObject({
      role: 'user',
      content: [
        { type: 'text', text: 'Hello' },
        { type: 'text', text: 'report.pdf\nExtracted PDF content' }
      ]
    })
  })

  it('should convert PDF for Gemini model accessed through non-Gemini provider (e.g., GitHub Copilot)', async () => {
    // A Gemini-named model through GitHub Copilot (type: 'openai') uses OpenAI-compatible
    // format which does NOT support native PDF file parts (would cause 400 "type has to be
    // either 'image_url' or 'text'" errors). PDF must be converted to text.
    // Mocking isGeminiModel(true) ensures this test would fail under the old
    // `isGeminiModel(model)` branch — i.e. it now actually guards the regression.
    vi.mocked(isGeminiModel).mockReturnValue(true)
    const provider = makeProvider('copilot', 'openai')
    const geminiModel = { ...makeModel(), id: 'gemini-3.1-pro-preview' }
    mockExtractPdfText.mockResolvedValue('Extracted PDF content')

    const params = {
      prompt: [{ role: 'user' as const, content: [makeTextPart('Hello'), makePdfFilePart('report.pdf')] }]
    } as unknown as LanguageModelV3CallOptions

    const result = await runMiddleware(provider, params, geminiModel)
    expect(mockExtractPdfText).toHaveBeenCalledWith('base64pdfdata')
    expect(result.prompt[0]).toMatchObject({
      role: 'user',
      content: [
        { type: 'text', text: 'Hello' },
        { type: 'text', text: 'report.pdf\nExtracted PDF content' }
      ]
    })
  })

  it('should pass through for Gemini model routed via Gemini endpoint (endpoint_type=gemini)', async () => {
    vi.mocked(isGeminiModel).mockReturnValue(true)
    const provider = makeProvider('my-aggregator', 'new-api')
    const geminiModel = { ...makeModel(), id: 'gemini-3.1-pro-preview', endpoint_type: 'gemini' as const }

    const params = {
      prompt: [{ role: 'user' as const, content: [makeTextPart('Hello'), makePdfFilePart()] }]
    } as unknown as LanguageModelV3CallOptions

    const result = await runMiddleware(provider, params, geminiModel)
    expect(result).toEqual(params)
    expect(mockExtractPdfText).not.toHaveBeenCalled()
  })

  it('should pass through for Gemini model on native Gemini provider', async () => {
    const provider = makeProvider('gemini', 'gemini')

    const params = {
      prompt: [{ role: 'user' as const, content: [makeTextPart('Hello'), makePdfFilePart()] }]
    } as unknown as LanguageModelV3CallOptions

    const result = await runMiddleware(provider, params)
    expect(result).toEqual(params)
    expect(mockExtractPdfText).not.toHaveBeenCalled()
  })

  it('should pass through unchanged when provider type supports native PDF (openai-response)', async () => {
    const provider = makeProvider('openai', 'openai-response')

    const params = {
      prompt: [{ role: 'user' as const, content: [makeTextPart('Hello'), makePdfFilePart()] }]
    } as unknown as LanguageModelV3CallOptions

    const result = await runMiddleware(provider, params)
    expect(result).toEqual(params)
    expect(mockExtractPdfText).not.toHaveBeenCalled()
  })

  it('should convert PDF for non-native provider types (new-api, gateway, openai)', async () => {
    const provider = makeProvider('moonshot', 'openai')
    mockExtractPdfText.mockResolvedValue('Extracted PDF content')

    const params = {
      prompt: [{ role: 'user' as const, content: [makeTextPart('Hello'), makePdfFilePart('report.pdf')] }]
    } as unknown as LanguageModelV3CallOptions

    const result = await runMiddleware(provider, params)
    expect(mockExtractPdfText).toHaveBeenCalledWith('base64pdfdata')
    expect(result.prompt[0]).toMatchObject({
      role: 'user',
      content: [
        { type: 'text', text: 'Hello' },
        { type: 'text', text: 'report.pdf\nExtracted PDF content' }
      ]
    })
  })

  it('should convert PDF for qiniu openai-compatible GPT models', async () => {
    const provider = makeProvider('qiniu', 'openai')
    const model = makeModel({ id: 'gpt-5.4', name: 'gpt-5.4' })
    mockExtractPdfText.mockResolvedValue('Extracted PDF content')

    const params = {
      prompt: [{ role: 'user' as const, content: [makeTextPart('Hello'), makePdfFilePart('report.pdf')] }]
    } as unknown as LanguageModelV3CallOptions

    const result = await runMiddleware(provider, params, model)
    expect(mockExtractPdfText).toHaveBeenCalledWith('base64pdfdata')
    expect(result.prompt[0]).toMatchObject({
      role: 'user',
      content: [
        { type: 'text', text: 'Hello' },
        { type: 'text', text: 'report.pdf\nExtracted PDF content' }
      ]
    })
  })

  it('should convert PDF FilePart to TextPart for ollama provider', async () => {
    const provider = makeProvider('ollama', 'ollama')
    mockExtractPdfText.mockResolvedValue('Extracted PDF content')

    const params = {
      prompt: [{ role: 'user' as const, content: [makeTextPart('Hello'), makePdfFilePart('report.pdf')] }]
    } as unknown as LanguageModelV3CallOptions

    const result = await runMiddleware(provider, params)
    expect(mockExtractPdfText).toHaveBeenCalledWith('base64pdfdata')
    expect(result.prompt[0]).toMatchObject({
      role: 'user',
      content: [
        { type: 'text', text: 'Hello' },
        { type: 'text', text: 'report.pdf\nExtracted PDF content' }
      ]
    })
  })

  it('should drop PDF part and warn when text extraction fails', async () => {
    const provider = makeProvider('ollama', 'ollama')
    mockExtractPdfText.mockRejectedValue(new Error('parse failed'))

    const params = {
      prompt: [{ role: 'user' as const, content: [makeTextPart('Hello'), makePdfFilePart('broken.pdf')] }]
    } as unknown as LanguageModelV3CallOptions

    const result = await runMiddleware(provider, params)
    expect(result.prompt[0]).toMatchObject({
      role: 'user',
      content: [{ type: 'text', text: 'Hello' }]
    })
    expect(window.toast.warning).toHaveBeenCalled()
  })

  it('should not convert non-PDF FileParts', async () => {
    const provider = makeProvider('ollama', 'ollama')

    const imagePart = makeImageFilePart()
    const params = {
      prompt: [{ role: 'user' as const, content: [makeTextPart('Hello'), imagePart] }]
    } as unknown as LanguageModelV3CallOptions

    const result = await runMiddleware(provider, params)
    expect(result.prompt[0]).toMatchObject({
      role: 'user',
      content: [{ type: 'text', text: 'Hello' }, imagePart]
    })
    expect(mockExtractPdfText).not.toHaveBeenCalled()
  })

  it('should handle mixed content: text + PDF + image — only PDF converted', async () => {
    const provider = makeProvider('ollama', 'ollama')
    mockExtractPdfText.mockResolvedValue('PDF text content')

    const imagePart = makeImageFilePart()
    const params = {
      prompt: [{ role: 'user' as const, content: [makeTextPart('Analyze'), makePdfFilePart('doc.pdf'), imagePart] }]
    } as unknown as LanguageModelV3CallOptions

    const result = await runMiddleware(provider, params)
    expect(result.prompt[0]).toMatchObject({
      role: 'user',
      content: [{ type: 'text', text: 'Analyze' }, { type: 'text', text: 'doc.pdf\nPDF text content' }, imagePart]
    })
  })

  it('should pass through when prompt is empty', async () => {
    const provider = makeProvider('ollama', 'ollama')
    const params = { prompt: [] } as unknown as LanguageModelV3CallOptions
    const result = await runMiddleware(provider, params)
    expect(result).toEqual(params)
  })

  it('should pass through messages with string content (system messages)', async () => {
    const provider = makeProvider('ollama', 'ollama')
    const params = {
      prompt: [{ role: 'system' as const, content: 'You are a helpful assistant' }]
    } as unknown as LanguageModelV3CallOptions

    const result = await runMiddleware(provider, params)
    expect(result.prompt[0]).toMatchObject({ role: 'system', content: 'You are a helpful assistant' })
  })
})
