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

vi.mock('@/lib/prisma', () => ({
  prisma: {
    story: {
      findFirst: vi.fn(),
    },
    task: {
      update: vi.fn(),
    },
    $transaction: vi.fn(),
  },
}))

vi.mock('@/lib/api-auth', () => ({
  authenticateApiRequest: vi.fn(),
}))

import { prisma } from '@/lib/prisma'
import { authenticateApiRequest } from '@/lib/api-auth'
import { PATCH as patchReorder } from '@/app/api/stories/[id]/tasks/reorder/route'

const mockPrisma = prisma as unknown as {
  story: { findFirst: ReturnType<typeof vi.fn> }
  task: { update: ReturnType<typeof vi.fn> }
  $transaction: ReturnType<typeof vi.fn>
}
const mockAuth = authenticateApiRequest as ReturnType<typeof vi.fn>

function makeStory(taskIds: string[]) {
  return {
    id: 'story-1',
    product_id: 'prod-1',
    tasks: taskIds.map(id => ({ id })),
  }
}

function makeRequest(body: unknown, storyId = 'story-1'): [Request, { params: Promise<{ id: string }> }] {
  return [
    new Request(`http://localhost/api/stories/${storyId}/tasks/reorder`, {
      method: 'PATCH',
      headers: { Authorization: 'Bearer test-token', 'Content-Type': 'application/json' },
      body: JSON.stringify(body),
    }),
    { params: Promise.resolve({ id: storyId }) },
  ]
}

describe('PATCH /api/stories/:id/tasks/reorder', () => {
  beforeEach(() => {
    vi.clearAllMocks()
    mockAuth.mockResolvedValue({ userId: 'user-1', isDemo: false })
    mockPrisma.$transaction.mockResolvedValue([])
    mockPrisma.task.update.mockResolvedValue({ id: 'task-1', sort_order: 1 })
  })

  // TC-RO-06 — body validation fires before story lookup
  it('returns 422 when task_ids is an empty array', async () => {
    const res = await patchReorder(...makeRequest({ task_ids: [] }))
    expect(res.status).toBe(422)
    expect(mockPrisma.story.findFirst).not.toHaveBeenCalled()
  })

  // TC-RO-07
  it('returns 422 when task_ids is not an array', async () => {
    const res = await patchReorder(...makeRequest({ task_ids: 'task-1' }))
    expect(res.status).toBe(422)
    expect(mockPrisma.story.findFirst).not.toHaveBeenCalled()
  })

  it('returns 422 when task_ids is missing entirely', async () => {
    const res = await patchReorder(...makeRequest({}))
    expect(res.status).toBe(422)
  })

  // TC-RO-08
  it('returns 422 when task_ids contains an ID not belonging to the story', async () => {
    mockPrisma.story.findFirst.mockResolvedValue(makeStory(['task-1', 'task-2']))

    const res = await patchReorder(...makeRequest({ task_ids: ['task-1', 'task-from-other-story'] }))
    const data = await res.json()

    expect(res.status).toBe(422)
    expect(data.error).toContain('task-from-other-story')
  })

  // TC-RO-09
  it('reorders tasks and returns 200 with success: true', async () => {
    mockPrisma.story.findFirst.mockResolvedValue(makeStory(['task-1', 'task-2', 'task-3']))

    const res = await patchReorder(...makeRequest({ task_ids: ['task-3', 'task-1', 'task-2'] }))
    const data = await res.json()

    expect(res.status).toBe(200)
    expect(data).toEqual({ success: true })
    expect(mockPrisma.$transaction).toHaveBeenCalled()
  })

  it('updates each task with its new sort_order index', async () => {
    mockPrisma.story.findFirst.mockResolvedValue(makeStory(['task-1', 'task-2']))

    await patchReorder(...makeRequest({ task_ids: ['task-2', 'task-1'] }))

    expect(mockPrisma.task.update).toHaveBeenCalledWith(
      expect.objectContaining({ where: { id: 'task-2' }, data: { sort_order: 1 } })
    )
    expect(mockPrisma.task.update).toHaveBeenCalledWith(
      expect.objectContaining({ where: { id: 'task-1' }, data: { sort_order: 2 } })
    )
  })
})
