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

import {
  DebugEmlUploadError,
  uploadDebugEmlSuperAgentRun,
  uploadDebugEmlSuperAgentRunStream,
} from '@/services/debugEmlService'
import { setUnauthorizedHandler } from '@/services/httpClient'

const jsonHeaders = {
  headers: {
    get: (name: string) => (name.toLowerCase() === 'content-type' ? 'application/json' : null),
  },
}

function mockJsonResponse(payload: unknown, status = 201): Response {
  return {
    ok: status >= 200 && status < 300,
    status,
    json: async () => payload,
    text: async () => JSON.stringify(payload),
    ...jsonHeaders,
  } as Response
}

function mockSseResponse(chunks: string[], status = 200): Response {
  const encoder = new TextEncoder()
  const stream = new ReadableStream<Uint8Array>({
    start(controller) {
      for (const chunk of chunks) {
        controller.enqueue(encoder.encode(chunk))
      }
      controller.close()
    },
  })
  return new Response(stream, {
    status,
    headers: {
      'Content-Type': 'text/event-stream',
    },
  })
}

describe('debugEmlService', () => {
  beforeEach(() => {
    vi.restoreAllMocks()
    setUnauthorizedHandler(null)
  })

  it('uploads an eml file with FormData and debug key header', async () => {
    const fetchMock = vi.spyOn(globalThis, 'fetch').mockResolvedValue(
      mockJsonResponse({
        debug_run_id: '90001',
        source_message_id: '30001',
        source_provider: 'DEBUG_EML_UPLOAD',
        external_message_id: 'debug-eml-run-90001-abcdef123456',
        external_conversation_id: 'debug-thread-90001',
        original_eml_oss_url: 'https://oss.example/raw.eml',
        original_eml_sha256: 'sha256',
        uploaded_media: [],
        html_body_with_oss_urls: '<strong>raw</strong>',
        html_body_sanitized: '<strong>safe</strong>',
        html_sanitize_required: true,
        html_render_mode: 'SANITIZED_HTML',
        agentbus_like_payload: { schema_version: 'debug-eml-upload-v1' },
        superagent_session_id: 'session-1',
        superagent_run_id: 'run-1',
        superagent_raw_answer: '{"ok":true}',
        superagent_parsed_json: { ok: true },
        warnings: [],
        status: 'SUPERAGENT_SUCCEEDED',
      }),
    )
    const file = new File(['From: guest@example.test'], 'booking.eml', { type: 'message/rfc822' })

    const result = await uploadDebugEmlSuperAgentRun({
      hotelId: 'HOTEL-TEST',
      debugUploadKey: ' manual-debug-key ',
      runLabel: 'frontend-smoke',
      file,
    })

    expect(fetchMock).toHaveBeenCalledTimes(1)
    const [url, init] = fetchMock.mock.calls[0]!
    expect(url).toBe('/api/system/debug/eml-superagent-runs')
    expect(init).toMatchObject({
      method: 'POST',
      headers: {
        Accept: 'application/json',
        'X-TH-Hotel-Debug-Upload-Key': 'manual-debug-key',
      },
    })
    expect(init?.headers).not.toHaveProperty('Content-Type')
    expect(init?.body).toBeInstanceOf(FormData)
    const formData = init?.body as FormData
    expect(formData.get('hotel_id')).toBe('HOTEL-TEST')
    expect(formData.get('run_label')).toBe('frontend-smoke')
    expect(formData.get('file')).toBe(file)
    expect(result.debug_run_id).toBe('90001')
  })

  it('omits hotel id when debug upload does not choose a hotel', async () => {
    const fetchMock = vi.spyOn(globalThis, 'fetch').mockResolvedValue(
      mockJsonResponse({
        debug_run_id: '90002',
        source_message_id: null,
        uploaded_media: [],
        warnings: [],
        status: 'CREATED',
      }),
    )
    const file = new File(['From: guest@example.test'], 'booking.eml', { type: 'message/rfc822' })

    await uploadDebugEmlSuperAgentRun({
      debugUploadKey: 'manual-debug-key',
      file,
    })

    const [, init] = fetchMock.mock.calls[0]!
    const formData = init?.body as FormData
    expect(formData.has('hotel_id')).toBe(false)
    expect(formData.get('file')).toBe(file)
  })

  it('throws a typed safe error for backend error_code responses', async () => {
    const unauthorizedHandler = vi.fn()
    setUnauthorizedHandler(unauthorizedHandler)
    vi.spyOn(globalThis, 'fetch').mockResolvedValue(
      mockJsonResponse(
        {
          error_code: 'DEBUG_UPLOAD_KEY_INVALID',
          message: 'Debug 上传访问口令缺失或错误。',
        },
        401,
      ),
    )
    const file = new File(['From: guest@example.test'], 'booking.eml', { type: 'message/rfc822' })

    await expect(
      uploadDebugEmlSuperAgentRun({
        hotelId: 'HOTEL-TEST',
        debugUploadKey: 'wrong-key',
        file,
      }),
    ).rejects.toMatchObject({
      status: 401,
      errorCode: 'DEBUG_UPLOAD_KEY_INVALID',
      message: 'Debug 上传访问口令缺失或错误。',
    } satisfies Partial<DebugEmlUploadError>)
    expect(unauthorizedHandler).not.toHaveBeenCalled()
  })

  it('runs the shared unauthorized handler for auth session errors', async () => {
    const unauthorizedHandler = vi.fn()
    setUnauthorizedHandler(unauthorizedHandler)
    vi.spyOn(globalThis, 'fetch').mockResolvedValue(
      mockJsonResponse(
        {
          error_code: 'AUTH_SESSION_INVALID',
          message: '登录已失效。',
        },
        401,
      ),
    )
    const file = new File(['From: guest@example.test'], 'booking.eml', { type: 'message/rfc822' })

    await expect(
      uploadDebugEmlSuperAgentRun({
        hotelId: 'HOTEL-TEST',
        debugUploadKey: 'manual-debug-key',
        file,
      }),
    ).rejects.toMatchObject({
      status: 401,
      errorCode: 'AUTH_SESSION_INVALID',
    } satisfies Partial<DebugEmlUploadError>)

    expect(unauthorizedHandler).toHaveBeenCalledOnce()
  })

  it('streams debug stages, public SuperAgent trace, and final result from backend SSE', async () => {
    const fetchMock = vi.spyOn(globalThis, 'fetch').mockResolvedValue(
      mockSseResponse([
        'event: debug_stage\n',
        'data: {"debug_run_id":"90003","status":"PARSING_EML","safe_summary":"阶段：解析 EML 邮件。"}\n\n',
        'event: superagent_trace\n',
        'data: {"event":"reasoning.summary","run_id":"run-stream-1","text":"正在分析 Debug 邮件。","ts":"2026-07-11T10:00:01Z"}\n\n',
        'event: superagent_trace\n',
        'data: {"event":"tool.call.started","run_id":"run-stream-1","tool_name":"th_hotel_query_case_context","input_summary":"{\\"group_code\\":\\"G001\\"}"}\n\n',
        'event: debug_heartbeat\n',
        'data: {"debug_run_id":"90003","status":"CALLING_SUPERAGENT"}\n\n',
        'event: superagent_result\n',
        'data: {"debug_run_id":"90003","source_message_id":"30003","uploaded_media":[],"superagent_session_id":"session-stream-1","superagent_run_id":"run-stream-1","superagent_raw_answer":"{\\"route_code\\":\\"S10\\"}","superagent_parsed_json":{"route_code":"S10"},"superagent_trace_events":[{"event":"reasoning.summary","text":"正在分析 Debug 邮件。"}],"warnings":[],"status":"SUPERAGENT_SUCCEEDED"}\n\n',
        'event: done\n',
        'data: {"status":"done"}\n\n',
      ]),
    )
    const stages: unknown[] = []
    const traces: unknown[] = []
    const file = new File(['From: guest@example.test'], 'booking.eml', { type: 'message/rfc822' })

    const result = await uploadDebugEmlSuperAgentRunStream(
      {
        hotelId: 'HOTEL-TEST',
        debugUploadKey: 'manual-debug-key',
        runLabel: 'stream-smoke',
        file,
      },
      {
        onStage: (stage) => stages.push(stage),
        onTrace: (trace) => traces.push(trace),
      },
    )

    expect(fetchMock).toHaveBeenCalledTimes(1)
    const [url, init] = fetchMock.mock.calls[0]!
    expect(url).toBe('/api/system/debug/eml-superagent-runs/stream')
    expect(init).toMatchObject({
      method: 'POST',
      headers: {
        Accept: 'text/event-stream',
        'X-TH-Hotel-Debug-Upload-Key': 'manual-debug-key',
      },
    })
    expect(stages).toMatchObject([
      {
        debug_run_id: '90003',
        status: 'PARSING_EML',
        safe_summary: '阶段：解析 EML 邮件。',
      },
    ])
    expect(traces).toMatchObject([
      {
        event: 'reasoning.summary',
        run_id: 'run-stream-1',
        text: '正在分析 Debug 邮件。',
      },
      {
        event: 'tool.call.started',
        tool_name: 'th_hotel_query_case_context',
        input_summary: '{"group_code":"G001"}',
      },
    ])
    expect(result.superagent_run_id).toBe('run-stream-1')
    expect(result.superagent_parsed_json).toEqual({ route_code: 'S10' })
    expect(result.superagent_trace_events).toMatchObject([
      {
        event: 'reasoning.summary',
        text: '正在分析 Debug 邮件。',
      },
    ])
  })

  it('throws a typed error when the backend SSE stream emits debug_error', async () => {
    vi.spyOn(globalThis, 'fetch').mockResolvedValue(
      mockSseResponse([
        'event: debug_stage\n',
        'data: {"debug_run_id":"90004","status":"CALLING_SUPERAGENT","safe_summary":"阶段：调用 SuperAgent Open API。"}\n\n',
        'event: debug_error\n',
        'data: {"debug_run_id":"90004","error_code":"SUPERAGENT_OPEN_API_FAILED","message":"SuperAgent 调用失败。","status":"SUPERAGENT_FAILED"}\n\n',
        'event: done\n',
        'data: {"status":"done"}\n\n',
      ]),
    )
    const file = new File(['From: guest@example.test'], 'booking.eml', { type: 'message/rfc822' })

    await expect(
      uploadDebugEmlSuperAgentRunStream({
        hotelId: 'HOTEL-TEST',
        debugUploadKey: 'manual-debug-key',
        file,
      }),
    ).rejects.toMatchObject({
      errorCode: 'SUPERAGENT_OPEN_API_FAILED',
      message: 'SuperAgent 调用失败。',
    } satisfies Partial<DebugEmlUploadError>)
  })
})
