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

vi.mock('next/cache', () => ({ revalidatePath: vi.fn() }))
vi.mock('next/headers', () => ({
  cookies: vi.fn().mockResolvedValue({
    set: vi.fn(),
    get: vi.fn(),
    delete: vi.fn(),
  }),
}))
vi.mock('iron-session', () => ({
  getIronSession: vi.fn().mockResolvedValue({ userId: 'user-1', isDemo: false }),
}))
vi.mock('@/lib/session', () => ({
  sessionOptions: { cookieName: 'test', password: 'test' },
}))
vi.mock('@/lib/product-access', () => ({
  productAccessFilter: vi.fn().mockReturnValue({}),
  getAccessibleProduct: vi.fn().mockResolvedValue({
    id: 'product-1',
    user_id: 'user-1',
  }),
}))
vi.mock('@/lib/rate-limit', () => ({
  enforceUserRateLimit: vi.fn().mockReturnValue(null),
}))
vi.mock('@/lib/code-server', () => ({
  createWithCodeRetry: vi.fn(async (_gen, fn) => fn('SP-1')),
  generateNextSprintCode: vi.fn().mockResolvedValue('SP-1'),
}))
vi.mock('@/lib/active-sprint', () => ({
  setActiveSprintInSettings: vi.fn().mockResolvedValue(undefined),
}))
vi.mock('@/lib/prisma', () => {
  const txClient = {
    sprint: { create: vi.fn() },
    story: { updateMany: vi.fn() },
    task: { updateMany: vi.fn() },
  }
  return {
    prisma: {
      sprint: {
        create: vi.fn(),
        findFirst: vi.fn(),
        update: vi.fn(),
      },
      story: {
        findMany: vi.fn(),
        updateMany: vi.fn(),
      },
      task: {
        findMany: vi.fn(),
        updateMany: vi.fn(),
      },
      pbi: { findMany: vi.fn() },
      user: {
        findUnique: vi.fn(),
        update: vi.fn(),
      },
      $transaction: vi.fn(async (fn: (tx: typeof txClient) => unknown) => fn(txClient)),
      __txClient: txClient,
    },
  }
})

import { prisma } from '@/lib/prisma'
import {
  createSprintWithSelectionAction,
  type CreateSprintWithSelectionInput,
} from '@/actions/sprints'

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

function baseInput(
  overrides: Partial<CreateSprintWithSelectionInput> = {},
): CreateSprintWithSelectionInput {
  return {
    productId: 'product-1',
    metadata: { goal: 'Sprint 1' },
    pbiIntent: {},
    storyOverrides: {},
    ...overrides,
  }
}

beforeEach(() => {
  vi.clearAllMocks()
  mockPrisma.sprint.create.mockReset()
  mockPrisma.story.findMany.mockReset()
  mockPrisma.story.updateMany.mockReset()
  mockPrisma.task.findMany.mockReset()
  mockPrisma.task.updateMany.mockReset()
  mockPrisma.$transaction.mockImplementation(
    async (fn: (tx: typeof mockPrisma.__txClient) => unknown) =>
      fn(mockPrisma.__txClient),
  )
  mockPrisma.__txClient.sprint.create
    .mockReset()
    .mockResolvedValue({ id: 'sprint-1', code: 'SP-1' })
  mockPrisma.__txClient.story.updateMany
    .mockReset()
    .mockResolvedValue({ count: 0 })
  mockPrisma.__txClient.task.updateMany
    .mockReset()
    .mockResolvedValue({ count: 0 })
})

describe('createSprintWithSelectionAction', () => {
  it('resolves intent=all naar alle child-stories en weert overrides.remove', async () => {
    // Stap 1: stories voor PBI-A (intent=all). Plus eligibility-fetch.
    mockPrisma.story.findMany
      // resolve step (only for pbis with intent='all')
      .mockResolvedValueOnce([
        { id: 's1', pbi_id: 'pbiA' },
        { id: 's2', pbi_id: 'pbiA' },
        { id: 's3', pbi_id: 'pbiA' },
      ])
      // partitionByEligibility — alle eligible
      .mockResolvedValueOnce([
        { id: 's1', sprint_id: null, status: 'OPEN', sprint: null },
        { id: 's3', sprint_id: null, status: 'OPEN', sprint: null },
      ])
      // affectedStories
      .mockResolvedValueOnce([
        { pbi_id: 'pbiA' },
        { pbi_id: 'pbiA' },
      ])
    mockPrisma.task.findMany.mockResolvedValueOnce([{ id: 't1' }])

    const result = await createSprintWithSelectionAction(
      baseInput({
        pbiIntent: { pbiA: 'all' },
        storyOverrides: { pbiA: { add: [], remove: ['s2'] } },
      }),
    )

    expect('success' in result).toBe(true)
    if ('success' in result) {
      expect(result.affectedStoryIds).toEqual(['s1', 's3'])
      expect(result.conflicts.notEligible).toEqual([])
    }
  })

  it('voegt storyOverrides.add toe over PBI heen (zelfs intent=none)', async () => {
    // Geen PBI met intent=all → stap 1 wordt niet uitgevoerd.
    mockPrisma.story.findMany
      // partition
      .mockResolvedValueOnce([
        { id: 's10', sprint_id: null, status: 'OPEN', sprint: null },
      ])
      // affectedStories
      .mockResolvedValueOnce([{ pbi_id: 'pbiB' }])
    mockPrisma.task.findMany.mockResolvedValueOnce([])

    const result = await createSprintWithSelectionAction(
      baseInput({
        pbiIntent: { pbiB: 'none' },
        storyOverrides: { pbiB: { add: ['s10'], remove: [] } },
      }),
    )

    expect('success' in result).toBe(true)
    if ('success' in result) {
      expect(result.affectedStoryIds).toEqual(['s10'])
    }
  })

  it('eligibility-filter classificeert DONE en cross-sprint stories', async () => {
    mockPrisma.story.findMany
      // resolve
      .mockResolvedValueOnce([
        { id: 's1', pbi_id: 'pbiA' },
        { id: 's2', pbi_id: 'pbiA' },
        { id: 's3', pbi_id: 'pbiA' },
      ])
      // partition: s1=DONE, s2=eligible, s3=in andere OPEN sprint
      .mockResolvedValueOnce([
        { id: 's1', sprint_id: null, status: 'DONE', sprint: null },
        { id: 's2', sprint_id: null, status: 'OPEN', sprint: null },
        {
          id: 's3',
          sprint_id: 'sprint-other',
          status: 'IN_SPRINT',
          sprint: { id: 'sprint-other', code: 'SP-O', status: 'OPEN' },
        },
      ])
      // affectedStories
      .mockResolvedValueOnce([{ pbi_id: 'pbiA' }])
    mockPrisma.task.findMany.mockResolvedValueOnce([])

    const result = await createSprintWithSelectionAction(
      baseInput({ pbiIntent: { pbiA: 'all' } }),
    )

    expect('success' in result).toBe(true)
    if ('success' in result) {
      expect(result.affectedStoryIds).toEqual(['s2'])
      expect(result.conflicts.notEligible.map((n) => n.storyId).sort()).toEqual(
        ['s1', 's3'],
      )
      expect(result.conflicts.crossSprint.map((c) => c.storyId)).toEqual(['s3'])
    }
  })

  it('zet story.status=IN_SPRINT en task.sprint_id mee in dezelfde transactie', async () => {
    mockPrisma.story.findMany
      .mockResolvedValueOnce([{ id: 's1', pbi_id: 'pbiA' }])
      .mockResolvedValueOnce([
        { id: 's1', sprint_id: null, status: 'OPEN', sprint: null },
      ])
      .mockResolvedValueOnce([{ pbi_id: 'pbiA' }])
    mockPrisma.task.findMany.mockResolvedValueOnce([{ id: 't1' }])

    await createSprintWithSelectionAction(
      baseInput({ pbiIntent: { pbiA: 'all' } }),
    )

    expect(mockPrisma.$transaction).toHaveBeenCalledTimes(1)
    expect(mockPrisma.__txClient.story.updateMany).toHaveBeenCalledWith(
      expect.objectContaining({
        data: expect.objectContaining({
          sprint_id: 'sprint-1',
          status: 'IN_SPRINT',
        }),
      }),
    )
    expect(mockPrisma.__txClient.task.updateMany).toHaveBeenCalledWith(
      expect.objectContaining({
        data: { sprint_id: 'sprint-1' },
      }),
    )
  })

  it('returnt affectedStoryIds + affectedPbiIds + affectedTaskIds', async () => {
    mockPrisma.story.findMany
      .mockResolvedValueOnce([
        { id: 's1', pbi_id: 'pbiA' },
        { id: 's2', pbi_id: 'pbiB' },
      ])
      .mockResolvedValueOnce([
        { id: 's1', sprint_id: null, status: 'OPEN', sprint: null },
        { id: 's2', sprint_id: null, status: 'OPEN', sprint: null },
      ])
      .mockResolvedValueOnce([{ pbi_id: 'pbiA' }, { pbi_id: 'pbiB' }])
    mockPrisma.task.findMany.mockResolvedValueOnce([
      { id: 't1' },
      { id: 't2' },
    ])

    const result = await createSprintWithSelectionAction(
      baseInput({ pbiIntent: { pbiA: 'all', pbiB: 'all' } }),
    )

    expect('success' in result).toBe(true)
    if ('success' in result) {
      expect(result.affectedStoryIds.sort()).toEqual(['s1', 's2'])
      expect(result.affectedPbiIds.sort()).toEqual(['pbiA', 'pbiB'])
      expect(result.affectedTaskIds.sort()).toEqual(['t1', 't2'])
    }
  })

  it('returnt error wanneer geen eligible stories overblijven', async () => {
    mockPrisma.story.findMany
      .mockResolvedValueOnce([{ id: 's1', pbi_id: 'pbiA' }])
      // s1 is DONE → notEligible
      .mockResolvedValueOnce([
        { id: 's1', sprint_id: null, status: 'DONE', sprint: null },
      ])

    const result = await createSprintWithSelectionAction(
      baseInput({ pbiIntent: { pbiA: 'all' } }),
    )

    expect('error' in result).toBe(true)
    if ('error' in result) {
      expect(result.code).toBe(422)
    }
  })
})
