import { beforeEach, describe, expect, it, vi } from 'vitest'
import { TASK_STATUS } from '@/lib/task/types'
import { buildMockRequest } from '../../../helpers/request'

type AuthState = {
  authenticated: boolean
}

type RouteContext = {
  params: Promise<{ taskId: string }>
}

type EmptyRouteContext = {
  params: Promise<Record<string, string>>
}

type ReplayEvent = Awaited<ReturnType<typeof import('@/lib/task/publisher').listEventsAfter>>[number]
type TaskLifecycleReplayEvent = Awaited<ReturnType<typeof import('@/lib/task/publisher').listTaskLifecycleEvents>>[number]

type TaskRecord = {
  id: string
  userId: string
  projectId: string
  type: string
  targetType: string
  targetId: string
  status: string
  errorCode: string | null
  errorMessage: string | null
}

const authState = vi.hoisted<AuthState>(() => ({
  authenticated: true,
}))

const queryTasksMock = vi.hoisted(() => vi.fn())
const dismissFailedTasksMock = vi.hoisted(() => vi.fn())
const getTaskByIdMock = vi.hoisted(() => vi.fn())
const cancelTaskMock = vi.hoisted(() => vi.fn())
const removeTaskJobMock = vi.hoisted(() => vi.fn(async () => true))
const publishTaskEventMock = vi.hoisted(() => vi.fn(async () => undefined))
const queryTaskTargetStatesMock = vi.hoisted(() => vi.fn())
const withPrismaRetryMock = vi.hoisted(() => vi.fn(async <T>(fn: () => Promise<T>) => await fn()))
const listEventsAfterMock = vi.hoisted(() =>
  vi.fn<typeof import('@/lib/task/publisher').listEventsAfter>(async () => []),
)
const listTaskLifecycleEventsMock = vi.hoisted(() =>
  vi.fn<typeof import('@/lib/task/publisher').listTaskLifecycleEvents>(async () => []),
)
const addChannelListenerMock = vi.hoisted(() =>
  vi.fn<(channel: string, listener: (message: string) => void) => Promise<() => Promise<void>>>(
    async () => async () => undefined,
  ),
)
const subscriberState = vi.hoisted(() => ({
  listener: null as ((message: string) => void) | null,
}))

vi.mock('@/lib/api-auth', () => {
  const unauthorized = () => new Response(
    JSON.stringify({ error: { code: 'UNAUTHORIZED' } }),
    { status: 401, headers: { 'content-type': 'application/json' } },
  )

  return {
    isErrorResponse: (value: unknown) => value instanceof Response,
    requireUserAuth: async () => {
      if (!authState.authenticated) return unauthorized()
      return { session: { user: { id: 'user-1' } } }
    },
    requireProjectAuthLight: async (projectId: string) => {
      if (!authState.authenticated) return unauthorized()
      return {
        session: { user: { id: 'user-1' } },
        project: { id: projectId, userId: 'user-1' },
      }
    },
  }
})

vi.mock('@/lib/task/service', () => ({
  queryTasks: queryTasksMock,
  dismissFailedTasks: dismissFailedTasksMock,
  getTaskById: getTaskByIdMock,
  cancelTask: cancelTaskMock,
}))

vi.mock('@/lib/task/queues', () => ({
  removeTaskJob: removeTaskJobMock,
}))

vi.mock('@/lib/task/publisher', () => ({
  publishTaskEvent: publishTaskEventMock,
  getProjectChannel: vi.fn((projectId: string) => `project:${projectId}`),
  listEventsAfter: listEventsAfterMock,
  listTaskLifecycleEvents: listTaskLifecycleEventsMock,
}))

vi.mock('@/lib/task/state-service', () => ({
  queryTaskTargetStates: queryTaskTargetStatesMock,
}))

vi.mock('@/lib/prisma-retry', () => ({
  withPrismaRetry: withPrismaRetryMock,
}))

vi.mock('@/lib/sse/shared-subscriber', () => ({
  getSharedSubscriber: vi.fn(() => ({
    addChannelListener: addChannelListenerMock,
  })),
}))

vi.mock('@/lib/prisma', () => ({
  prisma: {
    task: {
      findMany: vi.fn(async () => []),
    },
  },
}))

const baseTask: TaskRecord = {
  id: 'task-1',
  userId: 'user-1',
  projectId: 'project-1',
  type: 'IMAGE_CHARACTER',
  targetType: 'CharacterAppearance',
  targetId: 'appearance-1',
  status: TASK_STATUS.FAILED,
  errorCode: null,
  errorMessage: null,
}

describe('api contract - task infra routes (behavior)', () => {
  const emptyRouteContext: EmptyRouteContext = { params: Promise.resolve({}) }

  beforeEach(() => {
    vi.clearAllMocks()
    authState.authenticated = true
    subscriberState.listener = null

    queryTasksMock.mockResolvedValue([baseTask])
    dismissFailedTasksMock.mockResolvedValue(1)
    getTaskByIdMock.mockResolvedValue(baseTask)
    cancelTaskMock.mockResolvedValue({
      task: {
        ...baseTask,
        status: TASK_STATUS.FAILED,
        errorCode: 'TASK_CANCELLED',
        errorMessage: 'Task cancelled by user',
      },
      cancelled: true,
    })
    queryTaskTargetStatesMock.mockResolvedValue([
      {
        targetType: 'CharacterAppearance',
        targetId: 'appearance-1',
        active: true,
        status: TASK_STATUS.PROCESSING,
        taskId: 'task-1',
        updatedAt: new Date().toISOString(),
      },
    ])
    addChannelListenerMock.mockImplementation(async (_channel: string, listener: (message: string) => void) => {
      subscriberState.listener = listener
      return async () => undefined
    })
    listTaskLifecycleEventsMock.mockResolvedValue([])
  })

  it('GET /api/tasks: unauthenticated -> 401; authenticated -> 200 with caller-owned tasks', async () => {
    const { GET } = await import('@/app/api/tasks/route')

    authState.authenticated = false
    const unauthorizedReq = buildMockRequest({
      path: '/api/tasks',
      method: 'GET',
      query: { projectId: 'project-1', limit: 20 },
    })
    const unauthorizedRes = await GET(unauthorizedReq, emptyRouteContext)
    expect(unauthorizedRes.status).toBe(401)

    authState.authenticated = true
    const req = buildMockRequest({
      path: '/api/tasks',
      method: 'GET',
      query: { projectId: 'project-1', limit: 20, targetId: 'appearance-1' },
    })
    const res = await GET(req, emptyRouteContext)
    expect(res.status).toBe(200)

    const payload = await res.json() as { tasks: TaskRecord[] }
    expect(payload.tasks).toHaveLength(1)
    expect(payload.tasks[0]?.id).toBe('task-1')
    expect(queryTasksMock).toHaveBeenCalledWith(expect.objectContaining({
      projectId: 'project-1',
      targetId: 'appearance-1',
      limit: 20,
    }))
  })

  it('POST /api/tasks/dismiss: invalid params -> 400; success -> dismissed count', async () => {
    const { POST } = await import('@/app/api/tasks/dismiss/route')

    const invalidReq = buildMockRequest({
      path: '/api/tasks/dismiss',
      method: 'POST',
      body: { taskIds: [] },
    })
    const invalidRes = await POST(invalidReq, emptyRouteContext)
    expect(invalidRes.status).toBe(400)

    const req = buildMockRequest({
      path: '/api/tasks/dismiss',
      method: 'POST',
      body: { taskIds: ['task-1', 'task-2'] },
    })
    const res = await POST(req, emptyRouteContext)
    expect(res.status).toBe(200)

    const payload = await res.json() as { success: boolean; dismissed: number }
    expect(payload.success).toBe(true)
    expect(payload.dismissed).toBe(1)
    expect(dismissFailedTasksMock).toHaveBeenCalledWith(['task-1', 'task-2'], 'user-1')
  })

  it('POST /api/task-target-states: validates payload and returns queried states', async () => {
    const { POST } = await import('@/app/api/task-target-states/route')

    const invalidReq = buildMockRequest({
      path: '/api/task-target-states',
      method: 'POST',
      body: { projectId: 'project-1' },
    })
    const invalidRes = await POST(invalidReq, emptyRouteContext)
    expect(invalidRes.status).toBe(400)

    const req = buildMockRequest({
      path: '/api/task-target-states',
      method: 'POST',
      body: {
        projectId: 'project-1',
        targets: [
          {
            targetType: 'CharacterAppearance',
            targetId: 'appearance-1',
            types: ['IMAGE_CHARACTER'],
          },
        ],
      },
    })
    const res = await POST(req, emptyRouteContext)
    expect(res.status).toBe(200)

    const payload = await res.json() as { states: Array<Record<string, unknown>> }
    expect(payload.states).toHaveLength(1)
    expect(withPrismaRetryMock).toHaveBeenCalledTimes(1)
    expect(queryTaskTargetStatesMock).toHaveBeenCalledWith({
      projectId: 'project-1',
      userId: 'user-1',
      targets: [
        {
          targetType: 'CharacterAppearance',
          targetId: 'appearance-1',
          types: ['IMAGE_CHARACTER'],
        },
      ],
    })
  })

  it('GET /api/tasks/[taskId]: enforces ownership and returns task detail', async () => {
    const route = await import('@/app/api/tasks/[taskId]/route')

    authState.authenticated = false
    const unauthorizedReq = buildMockRequest({ path: '/api/tasks/task-1', method: 'GET' })
    const unauthorizedRes = await route.GET(unauthorizedReq, { params: Promise.resolve({ taskId: 'task-1' }) })
    expect(unauthorizedRes.status).toBe(401)

    authState.authenticated = true
    getTaskByIdMock.mockResolvedValueOnce({ ...baseTask, userId: 'other-user' })
    const notFoundReq = buildMockRequest({ path: '/api/tasks/task-1', method: 'GET' })
    const notFoundRes = await route.GET(notFoundReq, { params: Promise.resolve({ taskId: 'task-1' }) })
    expect(notFoundRes.status).toBe(404)

    const req = buildMockRequest({ path: '/api/tasks/task-1', method: 'GET' })
    const res = await route.GET(req, { params: Promise.resolve({ taskId: 'task-1' }) })
    expect(res.status).toBe(200)

    const payload = await res.json() as { task: TaskRecord }
    expect(payload.task.id).toBe('task-1')
  })

  it('GET /api/tasks/[taskId]?includeEvents=1: returns lifecycle events for refresh replay', async () => {
    const route = await import('@/app/api/tasks/[taskId]/route')
    const replayEvents: TaskLifecycleReplayEvent[] = [
      {
        id: '11',
        type: 'task.lifecycle',
        taskId: 'task-1',
        projectId: 'project-1',
        userId: 'user-1',
        ts: new Date().toISOString(),
        taskType: 'IMAGE_CHARACTER',
        targetType: 'CharacterAppearance',
        targetId: 'appearance-1',
        episodeId: null,
        payload: {
          lifecycleType: 'task.processing',
          stepId: 'clip_1_phase1',
          stepTitle: '分镜规划',
          stepIndex: 1,
          stepTotal: 3,
          message: 'running',
        },
      },
    ]
    listTaskLifecycleEventsMock.mockResolvedValueOnce(replayEvents)

    const req = buildMockRequest({
      path: '/api/tasks/task-1',
      method: 'GET',
      query: { includeEvents: '1', eventsLimit: '1200' },
    })
    const res = await route.GET(req, { params: Promise.resolve({ taskId: 'task-1' }) })
    expect(res.status).toBe(200)

    const payload = await res.json() as { task: TaskRecord; events: Array<Record<string, unknown>> }
    expect(payload.task.id).toBe('task-1')
    expect(payload.events).toHaveLength(1)
    expect(payload.events[0]?.id).toBe('11')
    expect(listTaskLifecycleEventsMock).toHaveBeenCalledWith('task-1', 1200)
  })

  it('DELETE /api/tasks/[taskId]: cancellation publishes cancelled event payload', async () => {
    const { DELETE } = await import('@/app/api/tasks/[taskId]/route')

    const req = buildMockRequest({ path: '/api/tasks/task-1', method: 'DELETE' })
    const res = await DELETE(req, { params: Promise.resolve({ taskId: 'task-1' }) } as RouteContext)
    expect(res.status).toBe(200)

    expect(removeTaskJobMock).toHaveBeenCalledWith('task-1')
    expect(publishTaskEventMock).toHaveBeenCalledWith(expect.objectContaining({
      taskId: 'task-1',
      projectId: 'project-1',
      payload: expect.objectContaining({
        cancelled: true,
        stage: 'cancelled',
      }),
    }))
  })

  it('GET /api/sse: missing projectId -> 400; unauthenticated with projectId -> 401', async () => {
    const { GET } = await import('@/app/api/sse/route')

    const invalidReq = buildMockRequest({ path: '/api/sse', method: 'GET' })
    const invalidRes = await GET(invalidReq, emptyRouteContext)
    expect(invalidRes.status).toBe(400)

    authState.authenticated = false
    const unauthorizedReq = buildMockRequest({
      path: '/api/sse',
      method: 'GET',
      query: { projectId: 'project-1' },
    })
    const unauthorizedRes = await GET(unauthorizedReq, emptyRouteContext)
    expect(unauthorizedRes.status).toBe(401)
  })

  it('GET /api/sse: authenticated replay request returns SSE stream and replays missed events', async () => {
    const { GET } = await import('@/app/api/sse/route')

    listEventsAfterMock.mockResolvedValueOnce([
      {
        id: '4',
        type: 'task.lifecycle',
        taskId: 'task-1',
        projectId: 'project-1',
        userId: 'user-1',
        ts: new Date().toISOString(),
        taskType: 'IMAGE_CHARACTER',
        targetType: 'CharacterAppearance',
        targetId: 'appearance-1',
        episodeId: null,
        payload: { lifecycleType: 'task.created' },
      } satisfies ReplayEvent,
    ])

    const req = buildMockRequest({
      path: '/api/sse',
      method: 'GET',
      query: { projectId: 'project-1' },
      headers: { 'last-event-id': '3' },
    })
    const res = await GET(req, emptyRouteContext)

    expect(res.status).toBe(200)
    expect(res.headers.get('content-type')).toContain('text/event-stream')
    expect(listEventsAfterMock).toHaveBeenCalledWith('project-1', 3, 5000)
    expect(addChannelListenerMock).toHaveBeenCalledWith('project:project-1', expect.any(Function))

    const reader = res.body?.getReader()
    expect(reader).toBeTruthy()
    const firstChunk = await reader!.read()
    expect(firstChunk.done).toBe(false)
    const decoded = new TextDecoder().decode(firstChunk.value)
    expect(decoded).toContain('event:')
    await reader!.cancel()
  })

  it('GET /api/sse: channel lifecycle stream includes terminal completed event', async () => {
    const { GET } = await import('@/app/api/sse/route')
    listEventsAfterMock.mockResolvedValueOnce([])

    const req = buildMockRequest({
      path: '/api/sse',
      method: 'GET',
      query: { projectId: 'project-1' },
      headers: { 'last-event-id': '10' },
    })
    const res = await GET(req, emptyRouteContext)
    expect(res.status).toBe(200)

    const listener = subscriberState.listener
    expect(listener).toBeTruthy()

    listener!(JSON.stringify({
      id: '11',
      type: 'task.lifecycle',
      taskId: 'task-1',
      projectId: 'project-1',
      userId: 'user-1',
      ts: new Date().toISOString(),
      taskType: 'IMAGE_CHARACTER',
      targetType: 'CharacterAppearance',
      targetId: 'appearance-1',
      episodeId: null,
      payload: { lifecycleType: 'processing', progress: 60 },
    }))
    listener!(JSON.stringify({
      id: '12',
      type: 'task.lifecycle',
      taskId: 'task-1',
      projectId: 'project-1',
      userId: 'user-1',
      ts: new Date().toISOString(),
      taskType: 'IMAGE_CHARACTER',
      targetType: 'CharacterAppearance',
      targetId: 'appearance-1',
      episodeId: null,
      payload: { lifecycleType: 'completed', progress: 100 },
    }))

    const reader = res.body?.getReader()
    expect(reader).toBeTruthy()
    const chunk1 = await reader!.read()
    const chunk2 = await reader!.read()
    const merged = `${new TextDecoder().decode(chunk1.value)}${new TextDecoder().decode(chunk2.value)}`

    expect(merged).toContain('"lifecycleType":"processing"')
    expect(merged).toContain('"lifecycleType":"completed"')
    expect(merged).toContain('"taskId":"task-1"')
    await reader!.cancel()
  })
})
