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

vi.mock('@/lib/prisma', () => ({
  prisma: {
    task: {
      findFirst: vi.fn(),
      update: vi.fn(),
      findMany: vi.fn(),
    },
    story: {
      findUniqueOrThrow: vi.fn(),
      findMany: vi.fn(),
      update: vi.fn(),
    },
    pbi: {
      findUniqueOrThrow: vi.fn(),
      findMany: vi.fn(),
      update: vi.fn(),
    },
    sprint: {
      findUniqueOrThrow: vi.fn(),
      update: vi.fn(),
    },
    claudeJob: {
      findFirst: vi.fn(),
      updateMany: vi.fn(),
    },
    sprintRun: {
      findUnique: vi.fn(),
      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 patchTask } from '@/app/api/tasks/[id]/route'

const mockPrisma = prisma as unknown as {
  task: {
    findFirst: ReturnType<typeof vi.fn>
    update: ReturnType<typeof vi.fn>
    findMany: ReturnType<typeof vi.fn>
  }
  story: {
    findUniqueOrThrow: ReturnType<typeof vi.fn>
    findMany: ReturnType<typeof vi.fn>
    update: ReturnType<typeof vi.fn>
  }
  pbi: {
    findUniqueOrThrow: ReturnType<typeof vi.fn>
    findMany: ReturnType<typeof vi.fn>
    update: ReturnType<typeof vi.fn>
  }
  sprint: {
    findUniqueOrThrow: ReturnType<typeof vi.fn>
    update: ReturnType<typeof vi.fn>
  }
  claudeJob: {
    findFirst: ReturnType<typeof vi.fn>
    updateMany: ReturnType<typeof vi.fn>
  }
  sprintRun: {
    findUnique: ReturnType<typeof vi.fn>
    update: ReturnType<typeof vi.fn>
  }
  $transaction: ReturnType<typeof vi.fn>
}
const mockAuth = authenticateApiRequest as ReturnType<typeof vi.fn>

function makeTask(overrides: { userId?: string; membersLength?: number } = {}) {
  const { userId = 'user-1', membersLength = 0 } = overrides
  return {
    id: 'task-1',
    story: {
      product: {
        user_id: userId,
        members: Array.from({ length: membersLength }, (_, i) => ({ id: `member-${i}` })),
      },
    },
  }
}

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

describe('PATCH /api/tasks/:id', () => {
  beforeEach(() => {
    vi.clearAllMocks()
    mockAuth.mockResolvedValue({ userId: 'user-1', isDemo: false })
    mockPrisma.task.findFirst.mockResolvedValue(makeTask())
    mockPrisma.task.update.mockResolvedValue({
      id: 'task-1',
      status: 'DONE',
      implementation_plan: null,
      title: 'Task',
      story_id: 'story-1',
    })
    // Default sibling state: only this task, already DONE → no story-promotion
    mockPrisma.task.findMany.mockResolvedValue([{ status: 'DONE' }])
    mockPrisma.story.findUniqueOrThrow.mockResolvedValue({
      id: 'story-1',
      status: 'DONE',
      pbi_id: 'pbi-1',
      sprint_id: null,
    })
    mockPrisma.story.findMany.mockResolvedValue([{ status: 'DONE' }])
    mockPrisma.pbi.findUniqueOrThrow.mockResolvedValue({ id: 'pbi-1', status: 'DONE' })
    // Pass-through for $transaction so tests behave as if Prisma ran the run-fn directly.
    mockPrisma.$transaction.mockImplementation(async (run: (tx: typeof prisma) => Promise<unknown>) => {
      return run(prisma)
    })
  })

  // TC-T-06
  it('returns 422 for invalid status value', async () => {
    const res = await patchTask(...makeRequest({ status: 'INVALID' }))
    expect(res.status).toBe(422)
  })

  // TC-T-07
  it('returns 422 when body has no recognized fields', async () => {
    const res = await patchTask(...makeRequest({}))
    expect(res.status).toBe(422)
  })

  it('returns 422 when body has only unrecognized fields', async () => {
    const res = await patchTask(...makeRequest({ unknown_field: 'value' }))
    expect(res.status).toBe(422)
  })

  // TC-T-08
  it('updates status only and returns 200 with updated task', async () => {
    mockPrisma.task.update.mockResolvedValue({ id: 'task-1', status: 'IN_PROGRESS', implementation_plan: null })

    const res = await patchTask(...makeRequest({ status: 'in_progress' }))
    const data = await res.json()

    expect(res.status).toBe(200)
    expect(data).toMatchObject({ id: 'task-1', status: 'in_progress' })
    expect(mockPrisma.task.update).toHaveBeenCalledWith(
      expect.objectContaining({
        data: { status: 'IN_PROGRESS' },
      })
    )
  })

  // TC-T-09
  it('updates implementation_plan only and returns 200', async () => {
    const plan = 'Step 1: implement. Step 2: test.'
    mockPrisma.task.update.mockResolvedValue({ id: 'task-1', status: 'TO_DO', implementation_plan: plan })

    const res = await patchTask(...makeRequest({ implementation_plan: plan }))
    const data = await res.json()

    expect(res.status).toBe(200)
    expect(data.implementation_plan).toBe(plan)
    expect(mockPrisma.task.update).toHaveBeenCalledWith(
      expect.objectContaining({
        data: { implementation_plan: plan },
      })
    )
  })

  // TC-T-10
  it('updates both status and implementation_plan and returns 200', async () => {
    const plan = 'Full plan here.'
    // First update writes the implementation_plan; second is the helper's status write.
    mockPrisma.task.update
      .mockResolvedValueOnce({ id: 'task-1', status: 'TO_DO', implementation_plan: plan })
      .mockResolvedValueOnce({
        id: 'task-1',
        title: 'Task',
        status: 'DONE',
        story_id: 'story-1',
        implementation_plan: plan,
      })

    const res = await patchTask(...makeRequest({ status: 'done', implementation_plan: plan }))
    const data = await res.json()

    expect(res.status).toBe(200)
    expect(data).toMatchObject({ status: 'done', implementation_plan: plan })
    // implementation_plan written via direct update; status written via helper update.
    expect(mockPrisma.task.update).toHaveBeenCalledWith(
      expect.objectContaining({ data: { implementation_plan: plan } }),
    )
    expect(mockPrisma.task.update).toHaveBeenCalledWith(
      expect.objectContaining({ data: { status: 'DONE' } }),
    )
  })

  // TC-T-11
  it('allows update when user is a team member (not product owner)', async () => {
    mockAuth.mockResolvedValue({ userId: 'user-1', isDemo: false })
    mockPrisma.task.findFirst.mockResolvedValue(makeTask({ userId: 'user-2', membersLength: 1 }))
    mockPrisma.task.update.mockResolvedValue({ id: 'task-1', status: 'DONE', implementation_plan: null })

    const res = await patchTask(...makeRequest({ status: 'done' }))
    expect(res.status).toBe(200)
  })

  it('the three patchable status values are accepted (review is rejected)', async () => {
    for (const apiStatus of ['todo', 'in_progress', 'done'] as const) {
      const dbStatus = { todo: 'TO_DO', in_progress: 'IN_PROGRESS', done: 'DONE' }[apiStatus]
      mockPrisma.task.update.mockResolvedValue({ id: 'task-1', status: dbStatus, implementation_plan: null })
      const res = await patchTask(...makeRequest({ status: apiStatus }))
      expect(res.status).toBe(200)
    }
    const reviewRes = await patchTask(...makeRequest({ status: 'review' }))
    expect(reviewRes.status).toBe(422)
  })

  it('promotes story to DONE when last sibling task transitions to DONE', async () => {
    mockPrisma.task.update.mockResolvedValue({
      id: 'task-1',
      status: 'DONE',
      implementation_plan: null,
      title: 'Task',
      story_id: 'story-1',
    })
    mockPrisma.task.findMany.mockResolvedValue([{ status: 'DONE' }, { status: 'DONE' }])
    mockPrisma.story.findUniqueOrThrow.mockResolvedValue({
      id: 'story-1',
      status: 'IN_SPRINT',
      pbi_id: 'pbi-1',
      sprint_id: null,
    })
    mockPrisma.story.findMany.mockResolvedValue([{ status: 'DONE' }])
    mockPrisma.pbi.findUniqueOrThrow.mockResolvedValue({ id: 'pbi-1', status: 'READY' })

    const res = await patchTask(...makeRequest({ status: 'done' }))
    expect(res.status).toBe(200)
    expect(mockPrisma.story.update).toHaveBeenCalledWith({
      where: { id: 'story-1' },
      data: { status: 'DONE' },
    })
  })

  // TC-T-12
  it('updates verify_only alone and returns 200 with verify_only in response', async () => {
    mockPrisma.task.update.mockResolvedValue({ id: 'task-1', status: 'TO_DO', implementation_plan: null, verify_only: true })

    const res = await patchTask(...makeRequest({ verify_only: true }))
    const data = await res.json()

    expect(res.status).toBe(200)
    expect(data.verify_only).toBe(true)
    expect(mockPrisma.task.update).toHaveBeenCalledWith(
      expect.objectContaining({ data: { verify_only: true } }),
    )
  })

  it('combines verify_only and implementation_plan into one update call', async () => {
    const plan = 'Verify only: check test results.'
    mockPrisma.task.update.mockResolvedValue({ id: 'task-1', status: 'TO_DO', implementation_plan: plan, verify_only: true })

    const res = await patchTask(...makeRequest({ implementation_plan: plan, verify_only: true }))
    const data = await res.json()

    expect(res.status).toBe(200)
    expect(data).toMatchObject({ implementation_plan: plan, verify_only: true })
    expect(mockPrisma.task.update).toHaveBeenCalledTimes(1)
    expect(mockPrisma.task.update).toHaveBeenCalledWith(
      expect.objectContaining({ data: { implementation_plan: plan, verify_only: true } }),
    )
  })

  it('returns 400 for malformed JSON', async () => {
    const req = new Request('http://localhost/api/tasks/task-1', {
      method: 'PATCH',
      headers: { Authorization: 'Bearer test-token', 'Content-Type': 'application/json' },
      body: '{not json',
    })
    const res = await patchTask(req, { params: Promise.resolve({ id: 'task-1' }) })
    expect(res.status).toBe(400)
  })
})
