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

vi.mock('next/cache', () => ({ revalidatePath: vi.fn() }))
vi.mock('next/headers', () => ({ cookies: vi.fn().mockResolvedValue({}) }))
vi.mock('iron-session', () => ({
  getIronSession: vi.fn(),
}))
vi.mock('@/lib/session', () => ({
  sessionOptions: { cookieName: 'test', password: 'test' },
}))

vi.mock('@/lib/prisma', () => ({
  prisma: {
    sprint: {
      findUnique: vi.fn(),
      update: vi.fn(),
    },
    sprintRun: {
      findFirst: vi.fn(),
      findUnique: vi.fn(),
      create: vi.fn(),
      update: vi.fn(),
    },
    story: {
      findMany: vi.fn(),
      updateMany: vi.fn(),
    },
    pbi: {
      updateMany: vi.fn(),
    },
    task: {
      updateMany: vi.fn(),
      findUnique: vi.fn().mockResolvedValue(null),
    },
    claudeQuestion: {
      findMany: vi.fn(),
    },
    claudeJob: {
      create: vi.fn(),
      updateMany: vi.fn(),
    },
    product: {
      findUnique: vi.fn().mockResolvedValue(null),
    },
    $transaction: vi.fn(),
  },
}))

import { prisma } from '@/lib/prisma'
import { getIronSession } from 'iron-session'
import {
  startSprintRunAction,
  resumeSprintAction,
  cancelSprintRunAction,
} from '@/actions/sprint-runs'

const mockSession = getIronSession as ReturnType<typeof vi.fn>

type Mocked = {
  sprint: { findUnique: ReturnType<typeof vi.fn>; update: ReturnType<typeof vi.fn> }
  sprintRun: {
    findFirst: ReturnType<typeof vi.fn>
    findUnique: ReturnType<typeof vi.fn>
    create: ReturnType<typeof vi.fn>
    update: ReturnType<typeof vi.fn>
  }
  story: {
    findMany: ReturnType<typeof vi.fn>
    updateMany: ReturnType<typeof vi.fn>
  }
  pbi: { updateMany: ReturnType<typeof vi.fn> }
  task: { updateMany: ReturnType<typeof vi.fn> }
  claudeQuestion: { findMany: ReturnType<typeof vi.fn> }
  claudeJob: {
    create: ReturnType<typeof vi.fn>
    updateMany: ReturnType<typeof vi.fn>
  }
  $transaction: ReturnType<typeof vi.fn>
}
const mockPrisma = prisma as unknown as Mocked

const SPRINT_OK = {
  id: 'sprint-1',
  status: 'OPEN',
  product_id: 'prod-1',
  product: { id: 'prod-1', pr_strategy: 'SPRINT' },
}

const STORY_OK = {
  id: 'story-1',
  pbi_id: 'pbi-1',
  priority: 1,
  sort_order: 1,
  pbi: {
    id: 'pbi-1',
    code: 'PBI-1',
    title: 'PBI',
    status: 'READY',
    priority: 1,
    sort_order: 1,
  },
  tasks: [
    { id: 'task-1', code: 'T-1', title: 'T1', priority: 1, sort_order: 1, implementation_plan: 'plan' },
    { id: 'task-2', code: 'T-2', title: 'T2', priority: 1, sort_order: 2, implementation_plan: 'plan' },
  ],
}

beforeEach(() => {
  vi.clearAllMocks()
  mockSession.mockResolvedValue({ userId: 'user-1', isDemo: false })
  mockPrisma.$transaction.mockImplementation(
    async (run: (tx: typeof prisma) => Promise<unknown>) => run(prisma),
  )
})

describe('startSprintRunAction — happy path', () => {
  it('maakt SprintRun + 2 ClaudeJobs voor 2 TO_DO tasks', async () => {
    mockPrisma.sprint.findUnique.mockResolvedValue(SPRINT_OK)
    mockPrisma.sprintRun.findFirst.mockResolvedValue(null)
    mockPrisma.story.findMany.mockResolvedValue([STORY_OK])
    mockPrisma.claudeQuestion.findMany.mockResolvedValue([])
    mockPrisma.sprintRun.create.mockResolvedValue({ id: 'run-1' })
    mockPrisma.claudeJob.create.mockResolvedValue({ id: 'job-x' })

    const result = await startSprintRunAction({ sprint_id: 'sprint-1' })

    expect(result).toEqual({ ok: true, sprint_run_id: 'run-1', jobs_count: 2 })
    expect(mockPrisma.sprintRun.create).toHaveBeenCalledWith({
      data: expect.objectContaining({
        sprint_id: 'sprint-1',
        started_by_id: 'user-1',
        status: 'QUEUED',
        pr_strategy: 'SPRINT',
      }),
    })
    expect(mockPrisma.claudeJob.create).toHaveBeenCalledTimes(2)
  })
})

describe('startSprintRunAction — pre-flight blockers', () => {
  it('blokkeert wanneer task geen implementation_plan heeft', async () => {
    mockPrisma.sprint.findUnique.mockResolvedValue(SPRINT_OK)
    mockPrisma.sprintRun.findFirst.mockResolvedValue(null)
    mockPrisma.story.findMany.mockResolvedValue([
      {
        ...STORY_OK,
        tasks: [
          { id: 'task-1', code: 'T-1', title: 'T1', priority: 1, sort_order: 1, implementation_plan: null },
        ],
      },
    ])
    mockPrisma.claudeQuestion.findMany.mockResolvedValue([])

    const result = await startSprintRunAction({ sprint_id: 'sprint-1' })

    expect(result).toMatchObject({ ok: false, error: 'PRE_FLIGHT_BLOCKED' })
    if (result.ok === false && 'blockers' in result) {
      expect(result.blockers).toContainEqual({
        type: 'task_no_plan',
        id: 'task-1',
        label: 'T-1: T1',
      })
    }
    expect(mockPrisma.sprintRun.create).not.toHaveBeenCalled()
  })

  it('blokkeert wanneer er een open ClaudeQuestion in scope is', async () => {
    mockPrisma.sprint.findUnique.mockResolvedValue(SPRINT_OK)
    mockPrisma.sprintRun.findFirst.mockResolvedValue(null)
    mockPrisma.story.findMany.mockResolvedValue([STORY_OK])
    mockPrisma.claudeQuestion.findMany.mockResolvedValue([
      { id: 'q-1', question: 'Welke route?' },
    ])

    const result = await startSprintRunAction({ sprint_id: 'sprint-1' })

    expect(result).toMatchObject({ ok: false, error: 'PRE_FLIGHT_BLOCKED' })
    if (result.ok === false && 'blockers' in result) {
      expect(result.blockers).toContainEqual({
        type: 'open_question',
        id: 'q-1',
        label: 'Welke route?',
      })
    }
  })

  it('blokkeert wanneer een PBI BLOCKED of FAILED is', async () => {
    mockPrisma.sprint.findUnique.mockResolvedValue(SPRINT_OK)
    mockPrisma.sprintRun.findFirst.mockResolvedValue(null)
    mockPrisma.story.findMany.mockResolvedValue([
      { ...STORY_OK, pbi: { ...STORY_OK.pbi, status: 'BLOCKED' } },
    ])
    mockPrisma.claudeQuestion.findMany.mockResolvedValue([])

    const result = await startSprintRunAction({ sprint_id: 'sprint-1' })

    expect(result).toMatchObject({ ok: false, error: 'PRE_FLIGHT_BLOCKED' })
    if (result.ok === false && 'blockers' in result) {
      expect(result.blockers).toContainEqual({
        type: 'pbi_blocked',
        id: 'pbi-1',
        label: 'PBI-1: PBI',
      })
    }
  })
})

describe('startSprintRunAction — SPRINT_BATCH', () => {
  const SPRINT_BATCH = {
    ...SPRINT_OK,
    product: {
      id: 'prod-1',
      pr_strategy: 'SPRINT_BATCH',
      repo_url: 'https://github.com/example/main',
    },
  }

  it('blokkeert task met afwijkende repo_url', async () => {
    mockPrisma.sprint.findUnique.mockResolvedValue(SPRINT_BATCH)
    mockPrisma.sprintRun.findFirst.mockResolvedValue(null)
    mockPrisma.story.findMany.mockResolvedValue([
      {
        ...STORY_OK,
        tasks: [
          {
            id: 'task-1',
            code: 'T-1',
            title: 'In main repo',
            priority: 1,
            sort_order: 1,
            implementation_plan: 'plan',
            repo_url: null,
          },
          {
            id: 'task-2',
            code: 'T-2',
            title: 'Cross-repo',
            priority: 1,
            sort_order: 2,
            implementation_plan: 'plan',
            repo_url: 'https://github.com/example/other',
          },
        ],
      },
    ])
    mockPrisma.claudeQuestion.findMany.mockResolvedValue([])

    const result = await startSprintRunAction({ sprint_id: 'sprint-1' })

    expect(result).toMatchObject({ ok: false, error: 'PRE_FLIGHT_BLOCKED' })
    if (result.ok === false && 'blockers' in result) {
      expect(result.blockers).toContainEqual({
        type: 'task_cross_repo',
        id: 'task-2',
        label: 'T-2: Cross-repo',
      })
    }
    expect(mockPrisma.sprintRun.create).not.toHaveBeenCalled()
  })

  it('staat tasks toe wanneer repo_url leeg is of gelijk aan product.repo_url', async () => {
    mockPrisma.sprint.findUnique.mockResolvedValue(SPRINT_BATCH)
    mockPrisma.sprintRun.findFirst.mockResolvedValue(null)
    mockPrisma.story.findMany.mockResolvedValue([
      {
        ...STORY_OK,
        tasks: [
          {
            id: 'task-1',
            code: 'T-1',
            title: 'No override',
            priority: 1,
            sort_order: 1,
            implementation_plan: 'plan',
            repo_url: null,
          },
          {
            id: 'task-2',
            code: 'T-2',
            title: 'Same repo',
            priority: 1,
            sort_order: 2,
            implementation_plan: 'plan',
            repo_url: 'https://github.com/example/main',
          },
        ],
      },
    ])
    mockPrisma.claudeQuestion.findMany.mockResolvedValue([])
    mockPrisma.sprintRun.create.mockResolvedValue({ id: 'run-batch' })
    mockPrisma.claudeJob.create.mockResolvedValue({ id: 'job-sprint' })

    const result = await startSprintRunAction({ sprint_id: 'sprint-1' })

    expect(result).toMatchObject({ ok: true, sprint_run_id: 'run-batch' })
    // Eén SPRINT_IMPLEMENTATION-job, niet per-task
    expect(mockPrisma.claudeJob.create).toHaveBeenCalledTimes(1)
    expect(mockPrisma.claudeJob.create).toHaveBeenCalledWith({
      data: expect.objectContaining({
        kind: 'SPRINT_IMPLEMENTATION',
        sprint_run_id: 'run-batch',
        product_id: 'prod-1',
      }),
    })
  })
})

describe('startSprintRunAction — guards', () => {
  it('weigert wanneer Sprint niet ACTIVE is', async () => {
    mockPrisma.sprint.findUnique.mockResolvedValue({ ...SPRINT_OK, status: 'CLOSED' })

    const result = await startSprintRunAction({ sprint_id: 'sprint-1' })
    expect(result).toMatchObject({ ok: false, error: 'SPRINT_NOT_ACTIVE' })
  })

  it('weigert wanneer er al een actieve SprintRun is', async () => {
    mockPrisma.sprint.findUnique.mockResolvedValue(SPRINT_OK)
    mockPrisma.sprintRun.findFirst.mockResolvedValue({ id: 'run-existing', status: 'RUNNING' })

    const result = await startSprintRunAction({ sprint_id: 'sprint-1' })
    expect(result).toMatchObject({ ok: false, error: 'SPRINT_RUN_ALREADY_ACTIVE' })
  })

  it('weigert demo-sessie', async () => {
    mockSession.mockResolvedValue({ userId: 'demo', isDemo: true })

    const result = await startSprintRunAction({ sprint_id: 'sprint-1' })
    expect(result).toMatchObject({ ok: false, code: 403 })
  })
})

describe('resumeSprintAction', () => {
  it('zet sprint en cascade-statuses terug en maakt nieuwe SprintRun', async () => {
    // Eerste findUnique (resume) ziet de sprint nog op FAILED;
    // de tweede call (binnen startSprintRunCore na de update) ziet ACTIVE.
    mockPrisma.sprint.findUnique
      .mockResolvedValueOnce({ ...SPRINT_OK, status: 'FAILED' })
      .mockResolvedValue(SPRINT_OK)
    mockPrisma.sprintRun.findFirst.mockResolvedValue(null)
    mockPrisma.story.findMany.mockImplementation(async (args: { select?: { pbi_id?: boolean } }) => {
      if (args.select?.pbi_id) return [{ pbi_id: 'pbi-1' }]
      return [STORY_OK]
    })
    mockPrisma.claudeQuestion.findMany.mockResolvedValue([])
    mockPrisma.sprintRun.create.mockResolvedValue({ id: 'run-2' })
    mockPrisma.claudeJob.create.mockResolvedValue({ id: 'job-x' })

    const result = await resumeSprintAction({ sprint_id: 'sprint-1' })

    expect(result).toMatchObject({ ok: true, sprint_run_id: 'run-2' })
    expect(mockPrisma.sprint.update).toHaveBeenCalledWith({
      where: { id: 'sprint-1' },
      data: { status: 'OPEN', completed_at: null },
    })
    expect(mockPrisma.story.updateMany).toHaveBeenCalledWith({
      where: { sprint_id: 'sprint-1', status: 'FAILED' },
      data: { status: 'IN_SPRINT' },
    })
    expect(mockPrisma.task.updateMany).toHaveBeenCalledWith({
      where: { story: { sprint_id: 'sprint-1' }, status: 'FAILED' },
      data: { status: 'TO_DO' },
    })
  })

  it('weigert als sprint niet FAILED is', async () => {
    mockPrisma.sprint.findUnique.mockResolvedValue({ ...SPRINT_OK, status: 'OPEN' })

    const result = await resumeSprintAction({ sprint_id: 'sprint-1' })
    expect(result).toMatchObject({ ok: false, error: 'SPRINT_NOT_FAILED' })
  })
})

describe('cancelSprintRunAction', () => {
  it('zet SprintRun op CANCELLED en cancelt openstaande jobs', async () => {
    mockPrisma.sprintRun.findUnique.mockResolvedValue({
      id: 'run-1',
      status: 'RUNNING',
      sprint_id: 'sprint-1',
    })

    const result = await cancelSprintRunAction({ sprint_run_id: 'run-1' })

    expect(result).toEqual({ ok: true })
    expect(mockPrisma.sprintRun.update).toHaveBeenCalledWith({
      where: { id: 'run-1' },
      data: expect.objectContaining({ status: 'CANCELLED' }),
    })
    expect(mockPrisma.claudeJob.updateMany).toHaveBeenCalledWith(expect.objectContaining({
      where: expect.objectContaining({
        sprint_run_id: 'run-1',
        status: { in: ['QUEUED', 'CLAIMED', 'RUNNING'] },
      }),
      data: expect.objectContaining({ status: 'CANCELLED' }),
    }))
  })

  it('weigert wanneer SprintRun al DONE is', async () => {
    mockPrisma.sprintRun.findUnique.mockResolvedValue({
      id: 'run-1',
      status: 'DONE',
      sprint_id: 'sprint-1',
    })

    const result = await cancelSprintRunAction({ sprint_run_id: 'run-1' })
    expect(result).toMatchObject({ ok: false, error: 'SPRINT_RUN_NOT_CANCELLABLE' })
  })
})
