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

const {
  mockGetSession,
  mockFindFirstTask,
  mockFindManyTask,
  mockFindFirstProduct,
  mockFindFirstSprint,
  mockFindFirstJob,
  mockCreateJob,
  mockUpdateJob,
  mockExecuteRaw,
  mockTransaction,
} = vi.hoisted(() => ({
  mockGetSession: vi.fn(),
  mockFindFirstTask: vi.fn(),
  mockFindManyTask: vi.fn(),
  mockFindFirstProduct: vi.fn(),
  mockFindFirstSprint: vi.fn(),
  mockFindFirstJob: vi.fn(),
  mockCreateJob: vi.fn(),
  mockUpdateJob: vi.fn(),
  mockExecuteRaw: vi.fn().mockResolvedValue(undefined),
  mockTransaction: vi.fn(),
}))

vi.mock('next/cache', () => ({ revalidatePath: vi.fn() }))

vi.mock('@/lib/auth', () => ({
  getSession: mockGetSession,
}))

vi.mock('@/lib/prisma', () => ({
  prisma: {
    task: { findFirst: mockFindFirstTask, findMany: mockFindManyTask },
    product: { findFirst: mockFindFirstProduct },
    sprint: { findFirst: mockFindFirstSprint },
    claudeJob: {
      findFirst: mockFindFirstJob,
      create: mockCreateJob,
      update: mockUpdateJob,
    },
    $executeRaw: mockExecuteRaw,
    $transaction: mockTransaction,
  },
}))

import {
  enqueueClaudeJobAction,
  enqueueAllTodoJobsAction,
  cancelClaudeJobAction,
  previewEnqueueAllAction,
  enqueueClaudeJobsBatchAction,
} from '@/actions/claude-jobs'

const SESSION_USER = { userId: 'user-1', isDemo: false }

const SESSION_DEMO = { userId: 'demo-1', isDemo: true }
const TASK_ID = 'task-cuid-1'
const JOB_ID = 'job-cuid-1'
const PRODUCT_ID = 'product-cuid-1'

const MOCK_TASK = { id: TASK_ID, story: { product_id: PRODUCT_ID } }
const MOCK_JOB_QUEUED = { id: JOB_ID, status: 'QUEUED' as const, task_id: TASK_ID, product_id: PRODUCT_ID }

beforeEach(() => {
  vi.clearAllMocks()
  mockExecuteRaw.mockResolvedValue(undefined)
})

describe('enqueueClaudeJobAction', () => {
  it('happy path: creates job with QUEUED status', async () => {
    mockGetSession.mockResolvedValue(SESSION_USER)
    mockFindFirstTask.mockResolvedValue(MOCK_TASK)
    mockFindFirstJob.mockResolvedValue(null)
    mockCreateJob.mockResolvedValue({ id: JOB_ID })

    const result = await enqueueClaudeJobAction(TASK_ID)

    expect(result).toEqual({ success: true, jobId: JOB_ID })
    expect(mockCreateJob).toHaveBeenCalledWith(
      expect.objectContaining({ data: expect.objectContaining({ status: 'QUEUED', task_id: TASK_ID }) })
    )
  })

  it('blocks demo user', async () => {
    mockGetSession.mockResolvedValue(SESSION_DEMO)

    const result = await enqueueClaudeJobAction(TASK_ID)

    expect(result).toMatchObject({ error: 'Niet beschikbaar in demo-modus' })
    expect(mockCreateJob).not.toHaveBeenCalled()
  })

  it('returns error when task not found', async () => {
    mockGetSession.mockResolvedValue(SESSION_USER)
    mockFindFirstTask.mockResolvedValue(null)

    const result = await enqueueClaudeJobAction(TASK_ID)

    expect(result).toMatchObject({ error: 'Task niet gevonden' })
    expect(mockCreateJob).not.toHaveBeenCalled()
  })

  it('idempotency: returns existing jobId when QUEUED job exists', async () => {
    mockGetSession.mockResolvedValue(SESSION_USER)
    mockFindFirstTask.mockResolvedValue(MOCK_TASK)
    mockFindFirstJob.mockResolvedValue({ id: JOB_ID })

    const result = await enqueueClaudeJobAction(TASK_ID)

    expect(result).toMatchObject({ error: 'Er loopt al een agent voor deze task', jobId: JOB_ID })
    expect(mockCreateJob).not.toHaveBeenCalled()
  })

  it('allows new enqueue after terminal (DONE) job', async () => {
    mockGetSession.mockResolvedValue(SESSION_USER)
    mockFindFirstTask.mockResolvedValue(MOCK_TASK)
    mockFindFirstJob.mockResolvedValue(null) // no active job
    mockCreateJob.mockResolvedValue({ id: 'new-job-id' })

    const result = await enqueueClaudeJobAction(TASK_ID)

    expect(result).toEqual({ success: true, jobId: 'new-job-id' })
  })
})

describe('enqueueAllTodoJobsAction', () => {
  it('happy path: scopes to active sprint + assignee, queues all queueable tasks', async () => {
    mockGetSession.mockResolvedValue(SESSION_USER)
    mockFindFirstProduct.mockResolvedValue({ id: PRODUCT_ID })
    mockFindFirstSprint.mockResolvedValue({ id: 'sprint-1' })
    mockFindManyTask.mockResolvedValue([{ id: 'task-a' }, { id: 'task-b' }])
    mockTransaction.mockResolvedValue([
      { id: 'job-a', task_id: 'task-a' },
      { id: 'job-b', task_id: 'task-b' },
    ])

    const result = await enqueueAllTodoJobsAction(PRODUCT_ID)

    expect(result).toEqual({ success: true, count: 2 })
    expect(mockFindManyTask).toHaveBeenCalledWith(
      expect.objectContaining({
        where: expect.objectContaining({
          status: 'TO_DO',
          story: { sprint_id: 'sprint-1', assignee_id: SESSION_USER.userId },
        }),
      })
    )
    expect(mockExecuteRaw).toHaveBeenCalledTimes(2)
  })

  it('returns count=0 when product has no active sprint', async () => {
    mockGetSession.mockResolvedValue(SESSION_USER)
    mockFindFirstProduct.mockResolvedValue({ id: PRODUCT_ID })
    mockFindFirstSprint.mockResolvedValue(null)

    const result = await enqueueAllTodoJobsAction(PRODUCT_ID)

    expect(result).toEqual({ success: true, count: 0 })
    expect(mockFindManyTask).not.toHaveBeenCalled()
    expect(mockTransaction).not.toHaveBeenCalled()
  })

  it('returns count=0 when no queueable tasks in sprint+assignee scope', async () => {
    mockGetSession.mockResolvedValue(SESSION_USER)
    mockFindFirstProduct.mockResolvedValue({ id: PRODUCT_ID })
    mockFindFirstSprint.mockResolvedValue({ id: 'sprint-1' })
    mockFindManyTask.mockResolvedValue([])

    const result = await enqueueAllTodoJobsAction(PRODUCT_ID)

    expect(result).toEqual({ success: true, count: 0 })
    expect(mockTransaction).not.toHaveBeenCalled()
    expect(mockExecuteRaw).not.toHaveBeenCalled()
  })

  it('blocks demo user', async () => {
    mockGetSession.mockResolvedValue(SESSION_DEMO)

    const result = await enqueueAllTodoJobsAction(PRODUCT_ID)

    expect(result).toMatchObject({ error: 'Niet beschikbaar in demo-modus' })
    expect(mockTransaction).not.toHaveBeenCalled()
  })

  it('returns error when product not accessible', async () => {
    mockGetSession.mockResolvedValue(SESSION_USER)
    mockFindFirstProduct.mockResolvedValue(null)

    const result = await enqueueAllTodoJobsAction(PRODUCT_ID)

    expect(result).toMatchObject({ error: 'Geen toegang tot dit product' })
    expect(mockTransaction).not.toHaveBeenCalled()
  })
})

const makePbiTask = (id: string, status: string, pbiStatus = 'READY') => ({
  id,
  title: `Task ${id}`,
  status,
  story: { id: 'story-1', title: 'Story 1', code: 'ST-1', pbi: { id: 'pbi-1', status: pbiStatus, priority: 1, sort_order: 1.0 } },
})

describe('previewEnqueueAllAction', () => {
  it('blocks demo user', async () => {
    mockGetSession.mockResolvedValue(SESSION_DEMO)

    const result = await previewEnqueueAllAction(PRODUCT_ID)

    expect(result).toMatchObject({ error: 'Niet beschikbaar in demo-modus' })
    expect(mockFindManyTask).not.toHaveBeenCalled()
  })

  it('returns error when product not accessible', async () => {
    mockGetSession.mockResolvedValue(SESSION_USER)
    mockFindFirstProduct.mockResolvedValue(null)

    const result = await previewEnqueueAllAction(PRODUCT_ID)

    expect(result).toMatchObject({ error: 'Geen toegang tot dit product' })
    expect(mockFindManyTask).not.toHaveBeenCalled()
  })

  it('returns empty tasks when no active sprint', async () => {
    mockGetSession.mockResolvedValue(SESSION_USER)
    mockFindFirstProduct.mockResolvedValue({ id: PRODUCT_ID })
    mockFindFirstSprint.mockResolvedValue(null)

    const result = await previewEnqueueAllAction(PRODUCT_ID)

    expect(result).toEqual({ tasks: [], blockerIndex: null, blockerReason: null })
    expect(mockFindManyTask).not.toHaveBeenCalled()
  })

  it('returns all tasks with no blocker when only TO_DO tasks', async () => {
    mockGetSession.mockResolvedValue(SESSION_USER)
    mockFindFirstProduct.mockResolvedValue({ id: PRODUCT_ID })
    mockFindFirstSprint.mockResolvedValue({ id: 'sprint-1' })
    mockFindManyTask.mockResolvedValue([
      makePbiTask('t1', 'TO_DO'),
      makePbiTask('t2', 'TO_DO'),
    ])

    const result = await previewEnqueueAllAction(PRODUCT_ID)

    expect(result).toMatchObject({ blockerIndex: null, blockerReason: null })
    if (!('error' in result)) expect(result.tasks).toHaveLength(2)
  })

  it('detects REVIEW task as blocker at correct index', async () => {
    mockGetSession.mockResolvedValue(SESSION_USER)
    mockFindFirstProduct.mockResolvedValue({ id: PRODUCT_ID })
    mockFindFirstSprint.mockResolvedValue({ id: 'sprint-1' })
    mockFindManyTask.mockResolvedValue([
      makePbiTask('t1', 'TO_DO'),
      makePbiTask('t2', 'TO_DO'),
      makePbiTask('t3', 'REVIEW'),
      makePbiTask('t4', 'TO_DO'),
    ])

    const result = await previewEnqueueAllAction(PRODUCT_ID)

    expect(result).toMatchObject({ blockerIndex: 2, blockerReason: 'task-review' })
    if (!('error' in result)) expect(result.tasks).toHaveLength(3)
  })

  it('detects BLOCKED PBI as blocker at first task of that PBI', async () => {
    mockGetSession.mockResolvedValue(SESSION_USER)
    mockFindFirstProduct.mockResolvedValue({ id: PRODUCT_ID })
    mockFindFirstSprint.mockResolvedValue({ id: 'sprint-1' })
    mockFindManyTask.mockResolvedValue([
      makePbiTask('t1', 'TO_DO', 'BLOCKED'),
      makePbiTask('t2', 'TO_DO', 'BLOCKED'),
    ])

    const result = await previewEnqueueAllAction(PRODUCT_ID)

    expect(result).toMatchObject({ blockerIndex: 0, blockerReason: 'pbi-blocked' })
    if (!('error' in result)) expect(result.tasks).toHaveLength(1)
  })

  it('queries without TO_DO filter to expose REVIEW tasks', async () => {
    mockGetSession.mockResolvedValue(SESSION_USER)
    mockFindFirstProduct.mockResolvedValue({ id: PRODUCT_ID })
    mockFindFirstSprint.mockResolvedValue({ id: 'sprint-1' })
    mockFindManyTask.mockResolvedValue([])

    await previewEnqueueAllAction(PRODUCT_ID)

    expect(mockFindManyTask).toHaveBeenCalledWith(
      expect.objectContaining({
        where: expect.not.objectContaining({ status: 'TO_DO' }),
      })
    )
  })
})

const makeBatchTask = (id: string, hasActiveJob = false) => ({
  id,
  claude_jobs: hasActiveJob ? [{ id: 'job-active' }] : [],
})

describe('enqueueClaudeJobsBatchAction', () => {
  it('happy path: 3 taskIds → 3 jobs in input order', async () => {
    mockGetSession.mockResolvedValue(SESSION_USER)
    mockFindFirstProduct.mockResolvedValue({ id: PRODUCT_ID })
    mockFindFirstSprint.mockResolvedValue({ id: 'sprint-1' })
    mockFindManyTask.mockResolvedValue([
      makeBatchTask('t1'),
      makeBatchTask('t2'),
      makeBatchTask('t3'),
    ])
    mockTransaction.mockResolvedValue([
      { id: 'job-1', task_id: 't1' },
      { id: 'job-2', task_id: 't2' },
      { id: 'job-3', task_id: 't3' },
    ])

    const result = await enqueueClaudeJobsBatchAction(PRODUCT_ID, ['t1', 't2', 't3'])

    expect(result).toEqual({ success: true, count: 3 })
    expect(mockExecuteRaw).toHaveBeenCalledTimes(3)
  })

  it('blocks demo user', async () => {
    mockGetSession.mockResolvedValue(SESSION_DEMO)

    const result = await enqueueClaudeJobsBatchAction(PRODUCT_ID, ['t1'])

    expect(result).toMatchObject({ error: 'Niet beschikbaar in demo-modus' })
    expect(mockTransaction).not.toHaveBeenCalled()
  })

  it('returns error when product not accessible', async () => {
    mockGetSession.mockResolvedValue(SESSION_USER)
    mockFindFirstProduct.mockResolvedValue(null)

    const result = await enqueueClaudeJobsBatchAction(PRODUCT_ID, ['t1'])

    expect(result).toMatchObject({ error: 'Geen toegang tot dit product' })
    expect(mockTransaction).not.toHaveBeenCalled()
  })

  it('returns error when task belongs to another user (IDOR)', async () => {
    mockGetSession.mockResolvedValue(SESSION_USER)
    mockFindFirstProduct.mockResolvedValue({ id: PRODUCT_ID })
    mockFindFirstSprint.mockResolvedValue({ id: 'sprint-1' })
    // Only 1 of 2 tasks authorized (other-user's task filtered out)
    mockFindManyTask.mockResolvedValue([makeBatchTask('t1')])

    const result = await enqueueClaudeJobsBatchAction(PRODUCT_ID, ['t1', 't-other-user'])

    expect(result).toMatchObject({ error: 'Een of meer taken zijn niet toegankelijk voor deze gebruiker' })
    expect(mockTransaction).not.toHaveBeenCalled()
  })

  it('skips tasks with active jobs (idempotent)', async () => {
    mockGetSession.mockResolvedValue(SESSION_USER)
    mockFindFirstProduct.mockResolvedValue({ id: PRODUCT_ID })
    mockFindFirstSprint.mockResolvedValue({ id: 'sprint-1' })
    mockFindManyTask.mockResolvedValue([
      makeBatchTask('t1'),
      makeBatchTask('t2', true), // has active job — skip
      makeBatchTask('t3'),
    ])
    mockTransaction.mockResolvedValue([
      { id: 'job-1', task_id: 't1' },
      { id: 'job-3', task_id: 't3' },
    ])

    const result = await enqueueClaudeJobsBatchAction(PRODUCT_ID, ['t1', 't2', 't3'])

    expect(result).toEqual({ success: true, count: 2 })
    expect(mockExecuteRaw).toHaveBeenCalledTimes(2)
  })

  it('returns count=0 for empty taskIds', async () => {
    mockGetSession.mockResolvedValue(SESSION_USER)

    const result = await enqueueClaudeJobsBatchAction(PRODUCT_ID, [])

    expect(result).toEqual({ success: true, count: 0 })
    expect(mockFindFirstProduct).not.toHaveBeenCalled()
  })
})

describe('cancelClaudeJobAction', () => {
  it('happy path: cancels QUEUED job', async () => {
    mockGetSession.mockResolvedValue(SESSION_USER)
    mockFindFirstJob.mockResolvedValue(MOCK_JOB_QUEUED)
    mockUpdateJob.mockResolvedValue({})

    const result = await cancelClaudeJobAction(JOB_ID)

    expect(result).toEqual({ success: true })
    expect(mockUpdateJob).toHaveBeenCalledWith(
      expect.objectContaining({
        where: { id: JOB_ID },
        data: expect.objectContaining({ status: 'CANCELLED' }),
      })
    )
  })

  it('demo user is blocked', async () => {
    mockGetSession.mockResolvedValue(SESSION_DEMO)

    const result = await cancelClaudeJobAction(JOB_ID)

    expect(result).toMatchObject({ error: 'Niet beschikbaar in demo-modus' })
    expect(mockUpdateJob).not.toHaveBeenCalled()
  })

  it('returns error when job not found (ownership check)', async () => {
    mockGetSession.mockResolvedValue(SESSION_USER)
    mockFindFirstJob.mockResolvedValue(null)

    const result = await cancelClaudeJobAction(JOB_ID)

    expect(result).toMatchObject({ error: 'Job niet gevonden' })
    expect(mockUpdateJob).not.toHaveBeenCalled()
  })

  it('returns error when cancelling terminal (DONE) job', async () => {
    mockGetSession.mockResolvedValue(SESSION_USER)
    mockFindFirstJob.mockResolvedValue({ ...MOCK_JOB_QUEUED, status: 'DONE' as const })

    const result = await cancelClaudeJobAction(JOB_ID)

    expect(result).toMatchObject({ error: 'Alleen actieve jobs kunnen geannuleerd worden' })
    expect(mockUpdateJob).not.toHaveBeenCalled()
  })

  it('returns error when cancelling FAILED job', async () => {
    mockGetSession.mockResolvedValue(SESSION_USER)
    mockFindFirstJob.mockResolvedValue({ ...MOCK_JOB_QUEUED, status: 'FAILED' as const })

    const result = await cancelClaudeJobAction(JOB_ID)

    expect(result).toMatchObject({ error: 'Alleen actieve jobs kunnen geannuleerd worden' })
  })
})
