/**
 * Anthropic Adapter Tests
 * Verifies model detection, message translation, tool-based structured output, and configuration
 */

import { describe, it, expect, vi, beforeEach } from 'vitest'
import { AnthropicAdapter, isAnthropicModel } from '../server/anthropic'
import type { LLMGenerateRequest } from '../../types'

// Mock Anthropic SDK
// Source uses client.messages.stream(params, options) which returns { finalMessage() }
const mockMessagesStream = vi.fn()

vi.mock('@anthropic-ai/sdk', () => ({
  default: vi.fn().mockImplementation(() => ({
    messages: {
      stream: mockMessagesStream
    }
  }))
}))

/**
 * Helper to set up mockMessagesStream to return a stream object
 * whose finalMessage() resolves to the given response.
 */
function mockStreamResponse(response: unknown) {
  mockMessagesStream.mockReturnValueOnce({
    finalMessage: () => Promise.resolve(response)
  })
}

/**
 * Helper for persistent (non-once) stream mock.
 */
function mockStreamResponsePersistent(response: unknown) {
  mockMessagesStream.mockReturnValue({
    finalMessage: () => Promise.resolve(response)
  })
}

/**
 * Helper to set up mockMessagesStream to throw when finalMessage() is called.
 */
function mockStreamError(error: Error) {
  mockMessagesStream.mockReturnValueOnce({
    finalMessage: () => Promise.reject(error)
  })
}

describe('AnthropicAdapter', () => {
  beforeEach(() => {
    vi.clearAllMocks()
  })

  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 mockAnthropicResponse = {
    content: [
      {
        type: 'tool_use',
        id: 'tool_123',
        name: 'structured_output',
        input: { response: 'Hello! How can I help you?' }
      }
    ],
    usage: {
      input_tokens: 10,
      output_tokens: 20
    }
  }

  describe('isAnthropicModel', () => {
    it('returns true for claude models with anthropic provider', () => {
      expect(isAnthropicModel('claude-opus-4-8', 'anthropic')).toBe(true)
      expect(isAnthropicModel('claude-sonnet-4-6', 'anthropic')).toBe(true)
      expect(isAnthropicModel('claude-haiku-4-5-20251001', 'anthropic')).toBe(true)
      expect(isAnthropicModel('claude-haiku-4-5', 'anthropic')).toBe(true)
    })

    it('returns false for claude models with other providers', () => {
      expect(isAnthropicModel('claude-opus-4-8', 'openrouter')).toBe(false)
      expect(isAnthropicModel('claude-sonnet-4-6', 'google')).toBe(false)
    })

    it('returns false for non-claude models', () => {
      expect(isAnthropicModel('gpt-5', 'anthropic')).toBe(false)
      expect(isAnthropicModel('gemini-3-flash-preview', 'anthropic')).toBe(false)
    })
  })

  describe('Basic Configuration', () => {
    it('creates adapter without model options', async () => {
      const adapter = new AnthropicAdapter({
        apiKey: 'test-key',
        model: 'claude-sonnet-4-6'
      })

      mockStreamResponse(mockAnthropicResponse)

      await adapter.generate(baseRequest)

      expect(mockMessagesStream).toHaveBeenCalledTimes(1)
    })

    it('creates adapter with model options', async () => {
      const adapter = new AnthropicAdapter({
        apiKey: 'test-key',
        model: 'claude-opus-4-8',
        modelOptions: {}
      })

      mockStreamResponse(mockAnthropicResponse)

      await adapter.generate(baseRequest)

      expect(mockMessagesStream).toHaveBeenCalledTimes(1)
    })
  })

  describe('Message Translation', () => {
    it('converts system messages to separate system field', async () => {
      const adapter = new AnthropicAdapter({
        apiKey: 'test-key',
        model: 'claude-sonnet-4-6'
      })

      mockStreamResponse(mockAnthropicResponse)

      await adapter.generate({
        messages: [
          { role: 'system', content: 'You are helpful' },
          { role: 'user', content: 'Hello' }
        ],
        responseSchema: baseRequest.responseSchema,
        maxOutputTokens: 1000
      })

      const callParams = mockMessagesStream.mock.calls[0][0]

      // System message should be in separate system field
      expect(callParams.system).toBe('You are helpful')

      // Messages should only have user message
      expect(callParams.messages).toHaveLength(1)
      expect(callParams.messages[0].role).toBe('user')
    })

    it('combines multiple system messages', async () => {
      const adapter = new AnthropicAdapter({
        apiKey: 'test-key',
        model: 'claude-sonnet-4-6'
      })

      mockStreamResponse(mockAnthropicResponse)

      await adapter.generate({
        messages: [
          { role: 'system', content: 'You are helpful' },
          { role: 'system', content: 'Be concise' },
          { role: 'user', content: 'Hello' }
        ],
        responseSchema: baseRequest.responseSchema,
        maxOutputTokens: 1000
      })

      const callParams = mockMessagesStream.mock.calls[0][0]
      expect(callParams.system).toBe('You are helpful\n\nBe concise')
    })

    it('handles requests without system messages', async () => {
      const adapter = new AnthropicAdapter({
        apiKey: 'test-key',
        model: 'claude-sonnet-4-6'
      })

      mockStreamResponse(mockAnthropicResponse)

      await adapter.generate({
        messages: [{ role: 'user', content: 'Hello' }],
        responseSchema: baseRequest.responseSchema,
        maxOutputTokens: 1000
      })

      const callParams = mockMessagesStream.mock.calls[0][0]
      expect(callParams.system).toBeUndefined()
    })

    it('preserves user and assistant roles', async () => {
      const adapter = new AnthropicAdapter({
        apiKey: 'test-key',
        model: 'claude-sonnet-4-6'
      })

      mockStreamResponse(mockAnthropicResponse)

      await adapter.generate({
        messages: [
          { role: 'user', content: 'Hello' },
          { role: 'assistant', content: 'Hi there!' },
          { role: 'user', content: 'How are you?' }
        ],
        responseSchema: baseRequest.responseSchema,
        maxOutputTokens: 1000
      })

      const callParams = mockMessagesStream.mock.calls[0][0]
      expect(callParams.messages[0].role).toBe('user')
      expect(callParams.messages[1].role).toBe('assistant')
      expect(callParams.messages[2].role).toBe('user')
    })
  })

  describe('Tool-based Structured Output', () => {
    it('configures tool_use for structured output', async () => {
      const adapter = new AnthropicAdapter({
        apiKey: 'test-key',
        model: 'claude-sonnet-4-6'
      })

      mockStreamResponse(mockAnthropicResponse)

      await adapter.generate(baseRequest)

      const callParams = mockMessagesStream.mock.calls[0][0]

      // Should have structured_output tool
      expect(callParams.tools).toHaveLength(1)
      expect(callParams.tools[0].name).toBe('structured_output')
      expect(callParams.tools[0].input_schema).toEqual(baseRequest.responseSchema)

      // Should force tool use
      expect(callParams.tool_choice).toEqual({ type: 'tool', name: 'structured_output' })
    })

    it('extracts response from tool_use block', async () => {
      const adapter = new AnthropicAdapter({
        apiKey: 'test-key',
        model: 'claude-sonnet-4-6'
      })

      mockStreamResponse({
        content: [
          {
            type: 'tool_use',
            id: 'tool_456',
            name: 'structured_output',
            input: { answer: 'Paris', confidence: 0.95 }
          }
        ],
        usage: { input_tokens: 15, output_tokens: 25 }
      })

      const response = await adapter.generate<{ answer: string; confidence: number }>({
        messages: [{ role: 'user', content: 'What is the capital of France?' }],
        responseSchema: {
          type: 'object',
          properties: {
            answer: { type: 'string' },
            confidence: { type: 'number' }
          }
        },
        maxOutputTokens: 500
      })

      expect(response.output.answer).toBe('Paris')
      expect(response.output.confidence).toBe(0.95)
    })

    it('throws error when no tool_use block in response', async () => {
      const adapter = new AnthropicAdapter({
        apiKey: 'test-key',
        model: 'claude-sonnet-4-6'
      })

      mockStreamResponse({
        content: [{ type: 'text', text: 'Hello!' }],
        usage: { input_tokens: 10, output_tokens: 5 }
      })

      await expect(adapter.generate(baseRequest)).rejects.toThrow(
        'Anthropic response missing tool_use block for structured output'
      )
    })
  })

  describe('Usage Metadata', () => {
    it('calculates total tokens from input + output', async () => {
      const adapter = new AnthropicAdapter({
        apiKey: 'test-key',
        model: 'claude-sonnet-4-6'
      })

      mockStreamResponse({
        content: [mockAnthropicResponse.content[0]],
        usage: {
          input_tokens: 100,
          output_tokens: 50
        }
      })

      const response = await adapter.generate(baseRequest)

      expect(response.usage).toEqual({
        inputTokens: 100,
        outputTokens: 50,
        totalTokens: 150
      })
    })

    it('extracts token usage correctly', async () => {
      const adapter = new AnthropicAdapter({
        apiKey: 'test-key',
        model: 'claude-sonnet-4-6'
      })

      mockStreamResponse(mockAnthropicResponse)

      const response = await adapter.generate(baseRequest)

      expect(response.usage).toEqual({
        inputTokens: 10,
        outputTokens: 20,
        totalTokens: 30
      })
    })
  })

  describe('MaxTokens Configuration', () => {
    it('passes maxOutputTokens to API', async () => {
      const adapter = new AnthropicAdapter({
        apiKey: 'test-key',
        model: 'claude-sonnet-4-6'
      })

      mockStreamResponse(mockAnthropicResponse)

      await adapter.generate({
        ...baseRequest,
        maxOutputTokens: 2000
      })

      const callParams = mockMessagesStream.mock.calls[0][0]
      expect(callParams.max_tokens).toBe(2000)
    })

    it('uses default 4000 when maxOutputTokens not specified', async () => {
      const adapter = new AnthropicAdapter({
        apiKey: 'test-key',
        model: 'claude-sonnet-4-6'
      })

      mockStreamResponse(mockAnthropicResponse)

      await adapter.generate({
        messages: baseRequest.messages,
        responseSchema: baseRequest.responseSchema
        // No maxOutputTokens
      })

      const callParams = mockMessagesStream.mock.calls[0][0]
      expect(callParams.max_tokens).toBe(4000)
    })
  })

  describe('Temperature and TopP', () => {
    it('passes temperature to API', async () => {
      const adapter = new AnthropicAdapter({
        apiKey: 'test-key',
        model: 'claude-sonnet-4-6'
      })

      mockStreamResponse(mockAnthropicResponse)

      await adapter.generate({
        ...baseRequest,
        temperature: 0.7
      })

      const callParams = mockMessagesStream.mock.calls[0][0]
      expect(callParams.temperature).toBe(0.7)
    })

    it('passes topP to API', async () => {
      const adapter = new AnthropicAdapter({
        apiKey: 'test-key',
        model: 'claude-sonnet-4-6'
      })

      mockStreamResponse(mockAnthropicResponse)

      await adapter.generate({
        ...baseRequest,
        topP: 0.9
      })

      const callParams = mockMessagesStream.mock.calls[0][0]
      expect(callParams.top_p).toBe(0.9)
    })

    it('omits sampling parameters for Opus 4.8', async () => {
      const adapter = new AnthropicAdapter({
        apiKey: 'test-key',
        model: 'claude-opus-4-8'
      })

      mockStreamResponse(mockAnthropicResponse)

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

      const callParams = mockMessagesStream.mock.calls[0][0]
      expect(callParams).not.toHaveProperty('temperature')
      expect(callParams).not.toHaveProperty('top_p')
      expect(callParams).not.toHaveProperty('top_k')
    })
  })

  describe('Error Handling', () => {
    it('propagates API errors', async () => {
      const adapter = new AnthropicAdapter({
        apiKey: 'test-key',
        model: 'claude-sonnet-4-6'
      })

      const apiError = new Error('API rate limit exceeded')
      mockStreamError(apiError)

      await expect(adapter.generate(baseRequest)).rejects.toThrow('API rate limit exceeded')
    })
  })

  describe('Multiple Calls', () => {
    it('reuses client across multiple generate calls', async () => {
      const adapter = new AnthropicAdapter({
        apiKey: 'test-key',
        model: 'claude-sonnet-4-6'
      })

      mockStreamResponsePersistent(mockAnthropicResponse)

      // First call
      await adapter.generate(baseRequest)
      expect(mockMessagesStream).toHaveBeenCalledTimes(1)

      // Second call
      await adapter.generate(baseRequest)
      expect(mockMessagesStream).toHaveBeenCalledTimes(2)
    })
  })

  describe('Model Selection', () => {
    it.each(['claude-opus-4-8', 'claude-sonnet-4-6', 'claude-haiku-4-5-20251001', 'claude-haiku-4-5'] as const)(
      'supports %s model',
      async (model) => {
        const adapter = new AnthropicAdapter({
          apiKey: 'test-key',
          model
        })

        mockStreamResponse(mockAnthropicResponse)

        await adapter.generate(baseRequest)

        const callParams = mockMessagesStream.mock.calls[0][0]
        expect(callParams.model).toBe(model)
      }
    )
  })
})
