/**
 * OpenAI Adapter Tests
 * Verifies model options handling and configuration
 */

import { describe, it, expect, vi, beforeEach } from 'vitest'
import { OpenAIAdapter } from '../server/openai'
import { LLMResponseParseError } from '../../errors'
import type { LLMGenerateRequest } from '../../types'

// Mock OpenAI SDK
vi.mock('openai', () => {
  return {
    default: vi.fn().mockImplementation(() => ({
      chat: {
        completions: {
          create: vi.fn()
        }
      }
    }))
  }
})

describe('OpenAIAdapter', () => {
  const mockOpenAICreate = vi.fn()

  beforeEach(async () => {
    vi.clearAllMocks()

    // Get the mock OpenAI constructor
    const OpenAI = (await import('openai')).default as unknown as { mockImplementation: (fn: () => unknown) => void }
    OpenAI.mockImplementation(() => ({
      chat: {
        completions: {
          create: mockOpenAICreate
        }
      }
    }))
  })

  const baseRequest: LLMGenerateRequest = {
    messages: [
      { role: 'system', content: 'You are a helpful assistant' },
      { role: 'user', content: 'Hello' }
    ],
    responseSchema: {
      type: 'object',
      properties: {
        response: { type: 'string' }
      }
    },
    maxOutputTokens: 1000
  }

  const mockOpenAIResponse = {
    choices: [
      {
        message: {
          content: JSON.stringify({ response: 'Hello! How can I help you?' })
        },
        finish_reason: 'stop'
      }
    ],
    usage: {
      prompt_tokens: 10,
      completion_tokens: 20,
      total_tokens: 30
    }
  }

  describe('Model Options - Basic Configuration', () => {
    it('creates adapter without model options', async () => {
      const adapter = new OpenAIAdapter({
        apiKey: 'test-key',
        model: 'gpt-5-mini'
      })

      mockOpenAICreate.mockResolvedValueOnce(mockOpenAIResponse)

      await adapter.generate(baseRequest)

      // Verify no model options were passed to OpenAI
      const createParams = mockOpenAICreate.mock.calls[0][0]
      expect(createParams).not.toHaveProperty('reasoning_effort')
      expect(createParams).not.toHaveProperty('verbosity')
    })

    it('creates adapter with reasoning_effort option', async () => {
      const adapter = new OpenAIAdapter({
        apiKey: 'test-key',
        model: 'gpt-5',
        modelOptions: {
          reasoning_effort: 'low'
        }
      })

      mockOpenAICreate.mockResolvedValueOnce(mockOpenAIResponse)

      await adapter.generate(baseRequest)

      // Verify reasoning_effort was passed to OpenAI
      const createParams = mockOpenAICreate.mock.calls[0][0]
      expect(createParams).toHaveProperty('reasoning_effort', 'low')
    })

    it('creates adapter with verbosity option', async () => {
      const adapter = new OpenAIAdapter({
        apiKey: 'test-key',
        model: 'gpt-5-mini',
        modelOptions: {
          verbosity: 'low'
        }
      })

      mockOpenAICreate.mockResolvedValueOnce(mockOpenAIResponse)

      await adapter.generate(baseRequest)

      // Verify verbosity was passed to OpenAI
      const createParams = mockOpenAICreate.mock.calls[0][0]
      expect(createParams).toHaveProperty('verbosity', 'low')
    })

    it('creates adapter with both reasoning_effort and verbosity options', async () => {
      const adapter = new OpenAIAdapter({
        apiKey: 'test-key',
        model: 'gpt-5',
        modelOptions: {
          reasoning_effort: 'medium',
          verbosity: 'high'
        }
      })

      mockOpenAICreate.mockResolvedValueOnce(mockOpenAIResponse)

      await adapter.generate(baseRequest)

      // Verify both options were passed to OpenAI
      const createParams = mockOpenAICreate.mock.calls[0][0]
      expect(createParams).toHaveProperty('reasoning_effort', 'medium')
      expect(createParams).toHaveProperty('verbosity', 'high')
    })
  })

  describe('Model Options - Option Values', () => {
    it('supports minimal reasoning effort', async () => {
      const adapter = new OpenAIAdapter({
        apiKey: 'test-key',
        model: 'gpt-5',
        modelOptions: {
          reasoning_effort: 'minimal'
        }
      })

      mockOpenAICreate.mockResolvedValueOnce(mockOpenAIResponse)

      await adapter.generate(baseRequest)

      const createParams = mockOpenAICreate.mock.calls[0][0]
      expect(createParams.reasoning_effort).toBe('minimal')
    })

    it('supports low reasoning effort', async () => {
      const adapter = new OpenAIAdapter({
        apiKey: 'test-key',
        model: 'gpt-5',
        modelOptions: {
          reasoning_effort: 'low'
        }
      })

      mockOpenAICreate.mockResolvedValueOnce(mockOpenAIResponse)

      await adapter.generate(baseRequest)

      const createParams = mockOpenAICreate.mock.calls[0][0]
      expect(createParams.reasoning_effort).toBe('low')
    })

    it('supports medium reasoning effort', async () => {
      const adapter = new OpenAIAdapter({
        apiKey: 'test-key',
        model: 'gpt-5',
        modelOptions: {
          reasoning_effort: 'medium'
        }
      })

      mockOpenAICreate.mockResolvedValueOnce(mockOpenAIResponse)

      await adapter.generate(baseRequest)

      const createParams = mockOpenAICreate.mock.calls[0][0]
      expect(createParams.reasoning_effort).toBe('medium')
    })

    it('supports high reasoning effort', async () => {
      const adapter = new OpenAIAdapter({
        apiKey: 'test-key',
        model: 'gpt-5',
        modelOptions: {
          reasoning_effort: 'high'
        }
      })

      mockOpenAICreate.mockResolvedValueOnce(mockOpenAIResponse)

      await adapter.generate(baseRequest)

      const createParams = mockOpenAICreate.mock.calls[0][0]
      expect(createParams.reasoning_effort).toBe('high')
    })

    it('supports low verbosity', async () => {
      const adapter = new OpenAIAdapter({
        apiKey: 'test-key',
        model: 'gpt-5-mini',
        modelOptions: {
          verbosity: 'low'
        }
      })

      mockOpenAICreate.mockResolvedValueOnce(mockOpenAIResponse)

      await adapter.generate(baseRequest)

      const createParams = mockOpenAICreate.mock.calls[0][0]
      expect(createParams.verbosity).toBe('low')
    })

    it('supports medium verbosity', async () => {
      const adapter = new OpenAIAdapter({
        apiKey: 'test-key',
        model: 'gpt-5-mini',
        modelOptions: {
          verbosity: 'medium'
        }
      })

      mockOpenAICreate.mockResolvedValueOnce(mockOpenAIResponse)

      await adapter.generate(baseRequest)

      const createParams = mockOpenAICreate.mock.calls[0][0]
      expect(createParams.verbosity).toBe('medium')
    })

    it('supports high verbosity', async () => {
      const adapter = new OpenAIAdapter({
        apiKey: 'test-key',
        model: 'gpt-5-mini',
        modelOptions: {
          verbosity: 'high'
        }
      })

      mockOpenAICreate.mockResolvedValueOnce(mockOpenAIResponse)

      await adapter.generate(baseRequest)

      const createParams = mockOpenAICreate.mock.calls[0][0]
      expect(createParams.verbosity).toBe('high')
    })
  })

  describe('Model Options - Backward Compatibility', () => {
    it('works with existing code that does not provide model options', async () => {
      const adapter = new OpenAIAdapter({
        apiKey: 'test-key',
        model: 'gpt-5-mini'
        // No modelOptions field
      })

      mockOpenAICreate.mockResolvedValueOnce(mockOpenAIResponse)

      const response = await adapter.generate(baseRequest)

      // Should complete successfully
      expect(response.output).toEqual({ response: 'Hello! How can I help you?' })
      expect(response.usage).toEqual({
        inputTokens: 10,
        outputTokens: 20,
        totalTokens: 30
      })

      // No model options should be in the API call
      const createParams = mockOpenAICreate.mock.calls[0][0]
      expect(createParams).not.toHaveProperty('reasoning')
      expect(createParams).not.toHaveProperty('text')
    })

    it('handles empty model options object', async () => {
      const adapter = new OpenAIAdapter({
        apiKey: 'test-key',
        model: 'gpt-5-mini',
        modelOptions: {}
      })

      mockOpenAICreate.mockResolvedValueOnce(mockOpenAIResponse)

      await adapter.generate(baseRequest)

      // No model options should be in the API call
      const createParams = mockOpenAICreate.mock.calls[0][0]
      expect(createParams).not.toHaveProperty('reasoning_effort')
      expect(createParams).not.toHaveProperty('verbosity')
    })
  })

  describe('Model Options - Integration with Other Parameters', () => {
    it('combines model options with temperature and maxOutputTokens', async () => {
      const adapter = new OpenAIAdapter({
        apiKey: 'test-key',
        model: 'gpt-5',
        modelOptions: {
          reasoning_effort: 'low',
          verbosity: 'low'
        }
      })

      mockOpenAICreate.mockResolvedValueOnce(mockOpenAIResponse)

      await adapter.generate({
        ...baseRequest,
        temperature: 0.7,
        topP: 0.9,
        maxOutputTokens: 2000
      })

      const createParams = mockOpenAICreate.mock.calls[0][0]

      // Standard parameters
      expect(createParams.temperature).toBe(0.7)
      expect(createParams.top_p).toBe(0.9)
      expect(createParams.max_completion_tokens).toBe(2000)

      // Model options
      expect(createParams.reasoning_effort).toBe('low')
      expect(createParams.verbosity).toBe('low')
    })

    it('preserves standard OpenAI parameters when model options present', async () => {
      const adapter = new OpenAIAdapter({
        apiKey: 'test-key',
        model: 'gpt-5',
        modelOptions: {
          reasoning_effort: 'high'
        }
      })

      mockOpenAICreate.mockResolvedValueOnce(mockOpenAIResponse)

      await adapter.generate(baseRequest)

      const createParams = mockOpenAICreate.mock.calls[0][0]

      // Standard parameters should still be present
      expect(createParams.model).toBe('gpt-5')
      expect(createParams.messages).toHaveLength(2)
      expect(createParams.max_completion_tokens).toBe(1000)
      expect(createParams.response_format).toEqual({
        type: 'json_schema',
        json_schema: {
          name: 'llm_response',
          schema: baseRequest.responseSchema
        }
      })
    })
  })

  describe('Model Options - Multiple Calls', () => {
    it('applies same model options to multiple generate calls', async () => {
      const adapter = new OpenAIAdapter({
        apiKey: 'test-key',
        model: 'gpt-5',
        modelOptions: {
          reasoning_effort: 'low',
          verbosity: 'low'
        }
      })

      mockOpenAICreate.mockResolvedValue(mockOpenAIResponse)

      // First call
      await adapter.generate(baseRequest)
      const firstCallParams = mockOpenAICreate.mock.calls[0][0]
      expect(firstCallParams.reasoning_effort).toBe('low')
      expect(firstCallParams.verbosity).toBe('low')

      // Second call
      await adapter.generate(baseRequest)
      const secondCallParams = mockOpenAICreate.mock.calls[1][0]
      expect(secondCallParams.reasoning_effort).toBe('low')
      expect(secondCallParams.verbosity).toBe('low')
    })
  })

  describe('JSON Parse Error Handling', () => {
    it('throws LLMResponseParseError when response is invalid JSON', async () => {
      const adapter = new OpenAIAdapter({
        apiKey: 'test-key',
        model: 'gpt-5-mini'
      })

      const invalidJsonResponse = {
        choices: [
          {
            message: {
              content: '{"response": "incomplete json' // Invalid JSON - unterminated string
            },
            finish_reason: 'stop'
          }
        ],
        usage: {
          prompt_tokens: 10,
          completion_tokens: 20,
          total_tokens: 30
        }
      }

      mockOpenAICreate.mockResolvedValueOnce(invalidJsonResponse)

      await expect(adapter.generate(baseRequest)).rejects.toThrow(LLMResponseParseError)
    })

    it('includes truncated raw content in LLMResponseParseError context', async () => {
      const adapter = new OpenAIAdapter({
        apiKey: 'test-key',
        model: 'gpt-5-mini'
      })

      const invalidJsonResponse = {
        choices: [
          {
            message: {
              content: '{"response": "incomplete'
            },
            finish_reason: 'stop'
          }
        ],
        usage: {
          prompt_tokens: 10,
          completion_tokens: 20,
          total_tokens: 30
        }
      }

      mockOpenAICreate.mockResolvedValueOnce(invalidJsonResponse)

      try {
        await adapter.generate(baseRequest)
        expect.fail('Should have thrown')
      } catch (error) {
        expect(error).toBeInstanceOf(LLMResponseParseError)
        const parseError = error as LLMResponseParseError
        expect(parseError.context).toHaveProperty('rawContent')
        expect(parseError.context).toHaveProperty('parseError')
        expect(parseError.message).toContain('Failed to parse LLM response as JSON')
      }
    })

    it('parses valid JSON successfully', async () => {
      const adapter = new OpenAIAdapter({
        apiKey: 'test-key',
        model: 'gpt-5-mini'
      })

      mockOpenAICreate.mockResolvedValueOnce(mockOpenAIResponse)

      const result = await adapter.generate(baseRequest)
      expect(result.output).toEqual({ response: 'Hello! How can I help you?' })
    })
  })

  describe('Error Handling with Better Stack Logging', () => {
    let mockBslogExternalServiceError: ReturnType<typeof vi.fn>
    let mockIsSystemError: ReturnType<typeof vi.fn>

    beforeEach(() => {
      // Mock Better Stack logger functions
      mockBslogExternalServiceError = vi.fn()
      mockIsSystemError = vi.fn()

      // Mock the betterstack-logger module
      vi.doMock('../../../utils/betterstack-logger.js', () => ({
        bslogExternalServiceError: mockBslogExternalServiceError,
        isSystemError: mockIsSystemError
      }))
    })

    it('logs system errors (5xx) to Better Stack', async () => {
      const adapter = new OpenAIAdapter({
        apiKey: 'test-key',
        model: 'gpt-5-mini',
        modelOptions: {
          reasoning_effort: 'low'
        }
      })

      const systemError = new Error('OpenAI service unavailable')
      Object.assign(systemError, { status: 503 })

      mockIsSystemError.mockReturnValue(true)
      mockOpenAICreate.mockRejectedValueOnce(systemError)

      await expect(adapter.generate(baseRequest)).rejects.toThrow('OpenAI service unavailable')

      // Verify error was re-thrown
      expect(mockOpenAICreate).toHaveBeenCalledTimes(1)
    })

    it('does not log user errors (4xx) to Better Stack', async () => {
      const adapter = new OpenAIAdapter({
        apiKey: 'test-key',
        model: 'gpt-5-mini'
      })

      const userError = new Error('Invalid API key')
      Object.assign(userError, { status: 401 })

      mockIsSystemError.mockReturnValue(false)
      mockOpenAICreate.mockRejectedValueOnce(userError)

      await expect(adapter.generate(baseRequest)).rejects.toThrow('Invalid API key')

      // Verify error was re-thrown
      expect(mockOpenAICreate).toHaveBeenCalledTimes(1)
    })

    it('includes rich context in Better Stack logs', async () => {
      const adapter = new OpenAIAdapter({
        apiKey: 'test-key',
        model: 'gpt-5',
        modelOptions: {
          reasoning_effort: 'medium'
        }
      })

      const systemError = new Error('Rate limit exceeded')
      Object.assign(systemError, { status: 529 })

      mockIsSystemError.mockReturnValue(true)
      mockOpenAICreate.mockRejectedValueOnce(systemError)

      await expect(adapter.generate(baseRequest)).rejects.toThrow('Rate limit exceeded')

      // Verify error was re-thrown
      expect(mockOpenAICreate).toHaveBeenCalledTimes(1)
    })
  })
})
