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

type TaskEventRow = {
  id: number
  taskId: string
  projectId: string
  userId: string
  eventType: string
  payload: Record<string, unknown> | null
  createdAt: Date
}

type TaskMeta = {
  id: string
  type: string
  targetType: string
  targetId: string
  episodeId: string | null
}

const taskEventFindManyMock = vi.hoisted(() =>
  vi.fn<(...args: unknown[]) => Promise<TaskEventRow[]>>(async () => []),
)
const taskEventCreateMock = vi.hoisted(() =>
  vi.fn<(...args: unknown[]) => Promise<TaskEventRow | null>>(async () => null),
)
const taskFindManyMock = vi.hoisted(() =>
  vi.fn<(...args: unknown[]) => Promise<TaskMeta[]>>(async () => []),
)
const redisPublishMock = vi.hoisted(() => vi.fn(async () => 1))

vi.mock('@/lib/prisma', () => ({
  prisma: {
    taskEvent: {
      findMany: taskEventFindManyMock,
      create: taskEventCreateMock,
    },
    task: {
      findMany: taskFindManyMock,
    },
  },
}))

vi.mock('@/lib/redis', () => ({
  redis: {
    publish: redisPublishMock,
  },
}))

import { listEventsAfter, listTaskLifecycleEvents, publishTaskStreamEvent } from '@/lib/task/publisher'

describe('task publisher replay', () => {
  beforeEach(() => {
    taskEventFindManyMock.mockReset()
    taskEventCreateMock.mockReset()
    taskFindManyMock.mockReset()
    redisPublishMock.mockReset()
  })

  it('replays persisted lifecycle + stream rows in chronological order', async () => {
    taskEventFindManyMock.mockResolvedValueOnce([
      {
        id: 12,
        taskId: 'task-1',
        projectId: 'project-1',
        userId: 'user-1',
        eventType: 'task.stream',
        payload: {
          stepId: 'step-1',
          stream: {
            kind: 'text',
            seq: 2,
            lane: 'main',
            delta: 'world',
          },
        },
        createdAt: new Date('2026-02-27T00:00:02.000Z'),
      },
      {
        id: 11,
        taskId: 'task-1',
        projectId: 'project-1',
        userId: 'user-1',
        eventType: 'task.processing',
        payload: {
          lifecycleType: 'task.processing',
          stepId: 'step-1',
          stepTitle: '阶段1',
        },
        createdAt: new Date('2026-02-27T00:00:01.000Z'),
      },
      {
        id: 10,
        taskId: 'task-1',
        projectId: 'project-1',
        userId: 'user-1',
        eventType: 'task.ignored',
        payload: {},
        createdAt: new Date('2026-02-27T00:00:00.000Z'),
      },
    ])
    taskFindManyMock.mockResolvedValueOnce([
      {
        id: 'task-1',
        type: 'script_to_storyboard_run',
        targetType: 'episode',
        targetId: 'episode-1',
        episodeId: 'episode-1',
      },
    ])

    const events = await listTaskLifecycleEvents('task-1', 50)

    expect(taskEventFindManyMock).toHaveBeenCalledWith(expect.objectContaining({
      where: { taskId: 'task-1' },
      orderBy: { id: 'desc' },
      take: 50,
    }))
    expect(events).toHaveLength(2)
    expect(events.map((event) => event.id)).toEqual(['11', '12'])
    expect(events.map((event) => event.type)).toEqual(['task.lifecycle', 'task.stream'])
    expect((events[1]?.payload as { stream?: { delta?: string } }).stream?.delta).toBe('world')
  })

  it('persists stream rows when persist=true', async () => {
    taskEventCreateMock.mockResolvedValueOnce({
      id: 99,
      taskId: 'task-1',
      projectId: 'project-1',
      userId: 'user-1',
      eventType: 'task.stream',
      payload: {
        stream: {
          kind: 'text',
          seq: 1,
          lane: 'main',
          delta: 'hello',
        },
      },
      createdAt: new Date('2026-02-27T00:00:03.000Z'),
    })
    redisPublishMock.mockResolvedValueOnce(1)

    const message = await publishTaskStreamEvent({
      taskId: 'task-1',
      projectId: 'project-1',
      userId: 'user-1',
      taskType: 'story_to_script_run',
      targetType: 'episode',
      targetId: 'episode-1',
      episodeId: 'episode-1',
      payload: {
        stepId: 'step-1',
        stream: {
          kind: 'text',
          seq: 1,
          lane: 'main',
          delta: 'hello',
        },
      },
      persist: true,
    })

    expect(taskEventCreateMock).toHaveBeenCalledWith(expect.objectContaining({
      data: expect.objectContaining({
        taskId: 'task-1',
        eventType: 'task.stream',
      }),
    }))
    expect(redisPublishMock).toHaveBeenCalledTimes(1)
    expect(message?.id).toBe('99')
    expect(message?.type).toBe('task.stream')
  })

  it('replays lifecycle + stream rows in listEventsAfter', async () => {
    taskEventFindManyMock.mockResolvedValueOnce([
      {
        id: 101,
        taskId: 'task-1',
        projectId: 'project-1',
        userId: 'user-1',
        eventType: 'task.stream',
        payload: {
          stepId: 'step-1',
          stream: {
            kind: 'text',
            seq: 3,
            lane: 'main',
            delta: 'chunk',
          },
        },
        createdAt: new Date('2026-02-27T00:00:03.000Z'),
      },
      {
        id: 102,
        taskId: 'task-1',
        projectId: 'project-1',
        userId: 'user-1',
        eventType: 'task.processing',
        payload: {
          lifecycleType: 'task.processing',
          stepId: 'step-1',
        },
        createdAt: new Date('2026-02-27T00:00:04.000Z'),
      },
    ])
    taskFindManyMock.mockResolvedValueOnce([
      {
        id: 'task-1',
        type: 'story_to_script_run',
        targetType: 'episode',
        targetId: 'episode-1',
        episodeId: 'episode-1',
      },
    ])

    const events = await listEventsAfter('project-1', 100, 20)

    expect(taskEventFindManyMock).toHaveBeenCalledWith(expect.objectContaining({
      where: {
        projectId: 'project-1',
        id: { gt: 100 },
      },
      orderBy: { id: 'asc' },
    }))
    expect(events).toHaveLength(2)
    expect(events.map((event) => event.id)).toEqual(['101', '102'])
    expect(events.map((event) => event.type)).toEqual(['task.stream', 'task.lifecycle'])
    expect((events[0]?.payload as { stream?: { delta?: string } }).stream?.delta).toBe('chunk')
  })
})
