import { describe, expect, it, vi } from 'vitest'
import {
  addSystemPrompt,
  extractMessageText,
  formatMessages,
  mergeStringFields,
  processSSEStream,
  removeLoading,
  serializeError
} from '../../src/utils/chat.utils'

describe('chat utils', () => {
  describe('formatMessages', () => {
    it('filters out completely empty messages', () => {
      const result = formatMessages([
        { role: 'user', content: '' },
        { role: 'assistant', content: 'hello' }
      ] as any)

      expect(result).toEqual([{ role: 'assistant', content: 'hello' }])
    })

    it('keeps messages that only contain tool calls', () => {
      const result = formatMessages([
        {
          role: 'assistant',
          content: '',
          tool_calls: [{ id: 'tool-1', type: 'function' }]
        }
      ] as any)

      expect(result).toEqual([
        {
          role: 'assistant',
          content: '',
          tool_calls: [{ id: 'tool-1', type: 'function' }]
        }
      ])
    })

    it('keeps messages that only contain tool_call_id', () => {
      const result = formatMessages([
        {
          role: 'tool',
          content: '',
          tool_call_id: 'tool-1'
        }
      ] as any)

      expect(result).toEqual([
        {
          role: 'tool',
          content: '',
          tool_call_id: 'tool-1'
        }
      ])
    })

    it('preserves multimodal content arrays', () => {
      const content = [
        { type: 'text', text: '请对比图片差异' },
        { type: 'image_url', image_url: { url: 'https://example.com/1.png' } }
      ]

      const result = formatMessages([
        {
          role: 'user',
          content
        }
      ] as any)

      expect(result).toEqual([
        {
          role: 'user',
          content
        }
      ])
    })

    it('preserves reasoning_content when present', () => {
      const result = formatMessages([
        {
          role: 'assistant',
          content: 'answer',
          reasoning_content: 'thoughts'
        }
      ] as any)

      expect(result).toEqual([
        {
          role: 'assistant',
          content: 'answer',
          reasoning_content: 'thoughts'
        }
      ])
    })

    it('keeps tool fields together with normal content', () => {
      const result = formatMessages([
        {
          role: 'assistant',
          content: 'tool output',
          tool_calls: [{ id: 'tool-1' }],
          reasoning_content: 'internal'
        }
      ] as any)

      expect(result[0]).toEqual({
        role: 'assistant',
        content: 'tool output',
        tool_calls: [{ id: 'tool-1' }],
        reasoning_content: 'internal'
      })
    })
  })

  describe('serializeError', () => {
    it('returns an empty string for undefined and null', () => {
      expect(serializeError(undefined)).toBe('')
      expect(serializeError(null)).toBe('')
    })

    it('serializes Error instances into structured json strings', () => {
      expect(serializeError(new TypeError('boom'))).toBe('{"name":"TypeError","message":"boom"}')
    })

    it('returns plain strings unchanged', () => {
      expect(serializeError('plain text')).toBe('plain text')
    })

    it('json-stringifies ordinary objects', () => {
      expect(serializeError({ code: 500, message: 'fail' })).toBe('{"code":500,"message":"fail"}')
    })

    it('falls back to String() for non-json-serializable values', () => {
      const value = 1n

      expect(serializeError(value)).toBe('1')
    })
  })

  describe('extractMessageText', () => {
    it('returns plain string content as-is', () => {
      expect(extractMessageText('hello')).toBe('hello')
    })

    it('joins text items from mixed content arrays', () => {
      const result = extractMessageText([
        'line 1',
        { type: 'text', text: 'line 2' },
        { type: 'text', content: 'line 3' },
        { type: 'image_url', image_url: { url: 'https://example.com/a.png' } }
      ])

      expect(result).toBe('line 1\nline 2\nline 3')
    })

    it('returns an empty string for unsupported content', () => {
      expect(extractMessageText({ foo: 'bar' })).toBe('')
    })
  })

  describe('mergeStringFields', () => {
    it('concatenates sibling string fields', () => {
      const result = mergeStringFields(
        {
          content: 'hello',
          title: 'foo'
        },
        {
          content: ' world',
          title: ' bar'
        }
      )

      expect(result).toEqual({
        content: 'hello world',
        title: 'foo bar'
      })
    })

    it('recursively merges nested object fields', () => {
      const result = mergeStringFields(
        {
          delta: {
            content: 'hello',
            metadata: {
              reason: 'a'
            }
          }
        },
        {
          delta: {
            content: ' world',
            metadata: {
              reason: 'b'
            }
          }
        }
      )

      expect(result).toEqual({
        delta: {
          content: 'hello world',
          metadata: {
            reason: 'ab'
          }
        }
      })
    })

    it('copies missing fields from the source object', () => {
      const result = mergeStringFields(
        {
          delta: {
            content: 'hello'
          }
        },
        {
          delta: {
            role: 'assistant'
          },
          finish_reason: 'stop'
        }
      )

      expect(result).toEqual({
        delta: {
          content: 'hello',
          role: 'assistant'
        },
        finish_reason: 'stop'
      })
    })

    it('overwrites falsy non-string values and recursively merges nested objects', () => {
      const result = mergeStringFields(
        {
          index: 0,
          done: false,
          meta: {
            step: 'a'
          }
        },
        {
          index: 1,
          done: true,
          meta: {
            step: 'b'
          }
        }
      )

      expect(result).toEqual({
        index: 1,
        done: true,
        meta: {
          step: 'ab'
        }
      })
    })
  })

  describe('processSSEStream', () => {
    it('parses standard SSE chunks and forwards them to the handler', () => {
      const handler = {
        onData: vi.fn(),
        onDone: vi.fn(),
        onError: vi.fn()
      }
      const data = [
        'data: {"choices":[{"delta":{"content":"hello"},"finish_reason":null}]}',
        '',
        'data: {"choices":[{"delta":{"content":" world"},"finish_reason":"stop"}]}',
        '',
        'data: [DONE]',
        ''
      ].join('\n\n')

      processSSEStream(data, handler as any)

      expect(handler.onData).toHaveBeenCalledTimes(2)
      expect(handler.onDone).toHaveBeenCalledWith('stop')
    })

    it('uses the latest finish reason before DONE', () => {
      const handler = {
        onData: vi.fn(),
        onDone: vi.fn(),
        onError: vi.fn()
      }
      const data = [
        'data: {"choices":[{"delta":{"content":"a"},"finish_reason":null}]}',
        '',
        'data: {"choices":[{"delta":{"content":"b"},"finish_reason":"tool_calls"}]}',
        '',
        'data: [DONE]',
        ''
      ].join('\n\n')

      processSSEStream(data, handler as any)

      expect(handler.onDone).toHaveBeenCalledWith('tool_calls')
    })

    it('ignores blank segments and malformed lines that do not start with data', () => {
      const handler = {
        onData: vi.fn(),
        onDone: vi.fn(),
        onError: vi.fn()
      }
      const data = ['event: ping', '', 'data: [DONE]', ''].join('\n\n')

      processSSEStream(data, handler as any)

      expect(handler.onData).not.toHaveBeenCalled()
      expect(handler.onDone).toHaveBeenCalledWith(undefined)
    })

    it('swallows JSON parse errors and continues parsing later chunks', () => {
      const handler = {
        onData: vi.fn(),
        onDone: vi.fn(),
        onError: vi.fn()
      }
      const consoleError = vi.spyOn(console, 'error').mockImplementation(() => {})
      const data = [
        'data: {"choices":[{"delta":{"content":"ok"},"finish_reason":null}]}',
        '',
        'data: {"choices":',
        '',
        'data: {"choices":[{"delta":{"content":"still ok"},"finish_reason":"stop"}]}',
        '',
        'data: [DONE]',
        ''
      ].join('\n\n')

      processSSEStream(data, handler as any)

      expect(handler.onData).toHaveBeenCalledTimes(2)
      expect(handler.onDone).toHaveBeenCalledWith('stop')
      expect(consoleError).toHaveBeenCalled()

      consoleError.mockRestore()
    })
  })

  describe('removeLoading', () => {
    it('removes the last loading item by default', () => {
      const messages = [
        {
          renderContent: [
            { type: 'text', content: 'hello' },
            { type: 'loading', content: '' },
            { type: 'agent-loading', content: '' }
          ]
        }
      ]

      removeLoading(messages as any)

      expect(messages[0].renderContent).toEqual([
        { type: 'text', content: 'hello' },
        { type: 'loading', content: '' }
      ])
    })

    it('removes the last loading item that matches the provided name', () => {
      const messages = [
        {
          renderContent: [
            { type: 'loading', content: 'tool-a' },
            { type: 'loading', content: 'tool-b' },
            { type: 'loading', content: 'tool-a' }
          ]
        }
      ]

      removeLoading(messages as any, 'tool-a')

      expect(messages[0].renderContent).toEqual([
        { type: 'loading', content: 'tool-a' },
        { type: 'loading', content: 'tool-b' }
      ])
    })

    it('does nothing when there is no renderContent', () => {
      const messages = [{}]

      expect(() => removeLoading(messages as any)).not.toThrow()
      expect(messages).toEqual([{}])
    })

    it('does nothing when no loading item matches the provided name', () => {
      const messages = [
        {
          renderContent: [
            { type: 'text', content: 'hello' },
            { type: 'loading', content: 'tool-a' }
          ]
        }
      ]

      removeLoading(messages as any, 'tool-b')

      expect(messages[0].renderContent).toEqual([
        { type: 'text', content: 'hello' },
        { type: 'loading', content: 'tool-a' }
      ])
    })
  })

  describe('addSystemPrompt', () => {
    it('adds a system prompt when the message list is empty', () => {
      const messages: any[] = []

      addSystemPrompt(messages, '你是一个助手')

      expect(messages).toEqual([{ role: 'system', content: '你是一个助手' }])
    })

    it('prepends a system prompt when the first message is not system', () => {
      const messages = [{ role: 'user', content: 'hello' }]

      addSystemPrompt(messages as any, '你是一个助手')

      expect(messages).toEqual([
        { role: 'system', content: '你是一个助手' },
        { role: 'user', content: 'hello' }
      ])
    })

    it('updates the existing system prompt when content has changed', () => {
      const messages = [
        { role: 'system', content: '旧提示词' },
        { role: 'user', content: 'hello' }
      ]

      addSystemPrompt(messages as any, '新提示词')

      expect(messages[0]).toEqual({ role: 'system', content: '新提示词' })
    })

    it('keeps the existing system prompt when it already matches', () => {
      const messages = [
        { role: 'system', content: '固定提示词' },
        { role: 'user', content: 'hello' }
      ]

      addSystemPrompt(messages as any, '固定提示词')

      expect(messages).toEqual([
        { role: 'system', content: '固定提示词' },
        { role: 'user', content: 'hello' }
      ])
    })
  })
})