import { describe, it, expect, vi, beforeEach, afterEach } from 'vitest'

import type { ChatStreamEvent } from './useChatStream'

const chatApiMock = vi.hoisted(() => ({
  getConversation: vi.fn(),
  listMessages: vi.fn(),
  sendMessage: vi.fn(),
  cancelMessage: vi.fn(),
  retryMessage: vi.fn(),
}))

vi.mock('@/api/chat', () => ({ chatApi: chatApiMock }))

// startChatStream 的 mock：记录每次调用，暴露 emit 让测试驱动事件。
interface StreamInstance {
  url: string
  since: number
  emit: (ev: ChatStreamEvent) => void
  stopped: boolean
}
const streamInstances = vi.hoisted(() => [] as StreamInstance[])

vi.mock('@/composables/useChatStream', () => ({
  startChatStream: vi.fn(
    (
      url: string,
      since: number,
      handlers: { onEvent: (ev: ChatStreamEvent) => void; onError: (e: Error) => void },
    ) => {
      const inst: StreamInstance = {
        url,
        since,
        emit: (ev) => handlers.onEvent(ev),
        stopped: false,
      }
      streamInstances.push(inst)
      return {
        stop: () => {
          inst.stopped = true
        },
      }
    },
  ),
}))

import { useConversationSession } from './useConversationSession'

function conv(id: string) {
  return { id, user_id: 'u1', model_id: 'm1', system_prompt: '', created_at: '', updated_at: '' }
}

function msg(id: number, status = 'ok', role = 'assistant') {
  return {
    id,
    conversation_id: 'c1',
    role,
    content: `msg-${id}`,
    status,
    created_at: '',
    updated_at: '',
  }
}

beforeEach(() => {
  streamInstances.length = 0
  vi.clearAllMocks()
})

afterEach(() => {
  vi.restoreAllMocks()
})

describe('useConversationSession', () => {
  it('openConversation 将 DESC 消息排为 id 升序', async () => {
    chatApiMock.getConversation.mockResolvedValue({
      conversation: conv('c1'),
      messages: [msg(3), msg(2), msg(1)],
    })
    const s = useConversationSession()
    await s.openConversation('c1')
    expect(s.messages.value.map((m) => m.id)).toEqual([1, 2, 3])
    expect(s.isStreaming.value).toBe(false)
  })

  it('openConversation 发现 pending 消息时续接流', async () => {
    chatApiMock.getConversation.mockResolvedValue({
      conversation: conv('c1'),
      messages: [msg(2, 'pending'), msg(1)],
    })
    const s = useConversationSession()
    await s.openConversation('c1')
    expect(s.streamingMessageId.value).toBe(2)
    expect(streamInstances).toHaveLength(1)
    expect(streamInstances[0].since).toBe(1)
  })

  it('sendMessage 追加两条占位消息并累积 text_delta 至 done', async () => {
    chatApiMock.getConversation.mockResolvedValue({ conversation: conv('c1'), messages: [] })
    chatApiMock.sendMessage.mockResolvedValue({
      user_message_id: 10,
      assistant_message_id: 11,
      stream_url: '/stream',
    })
    const s = useConversationSession()
    await s.openConversation('c1')
    await s.sendMessage('hello')

    expect(s.messages.value.map((m) => [m.id, m.role, m.status])).toEqual([
      [10, 'user', 'ok'],
      [11, 'assistant', 'pending'],
    ])
    expect(s.isStreaming.value).toBe(true)

    const stream = streamInstances[0]
    stream.emit({ id: 1, event: 'text_delta', data: { text: 'foo' } })
    stream.emit({ id: 2, event: 'text_delta', data: { text: 'bar' } })
    stream.emit({ id: 3, event: 'done', data: {} })

    const assistant = s.messages.value[1]
    expect(assistant.content).toBe('foobar')
    expect(assistant.status).toBe('ok')
    expect(s.isStreaming.value).toBe(false)
  })

  it('error 事件标记消息失败并结束流式', async () => {
    chatApiMock.getConversation.mockResolvedValue({ conversation: conv('c1'), messages: [] })
    chatApiMock.sendMessage.mockResolvedValue({
      user_message_id: 1,
      assistant_message_id: 2,
      stream_url: '/stream',
    })
    const s = useConversationSession()
    await s.openConversation('c1')
    await s.sendMessage('x')
    streamInstances[0].emit({ id: 1, event: 'error', data: { message: 'boom' } })
    expect(s.messages.value[1].status).toBe('error')
    expect(s.messages.value[1].error_message).toBe('boom')
    expect(s.isStreaming.value).toBe(false)
  })

  it('closeCurrent 终止流并清空状态', async () => {
    chatApiMock.getConversation.mockResolvedValue({
      conversation: conv('c1'),
      messages: [msg(2, 'pending')],
    })
    const s = useConversationSession()
    await s.openConversation('c1')
    expect(streamInstances[0].stopped).toBe(false)
    s.closeCurrent()
    expect(streamInstances[0].stopped).toBe(true)
    expect(s.currentConversation.value).toBeNull()
    expect(s.messages.value).toEqual([])
  })

  it('双实例互不干扰', async () => {
    chatApiMock.getConversation
      .mockResolvedValueOnce({ conversation: conv('c1'), messages: [msg(1)] })
      .mockResolvedValueOnce({ conversation: conv('c2'), messages: [msg(5), msg(4)] })
    const a = useConversationSession()
    const b = useConversationSession()
    await a.openConversation('c1')
    await b.openConversation('c2')
    expect(a.messages.value.map((m) => m.id)).toEqual([1])
    expect(b.messages.value.map((m) => m.id)).toEqual([4, 5])
    b.closeCurrent()
    expect(a.currentConversation.value?.id).toBe('c1')
  })
})
