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' }),
}))
vi.mock('@/lib/rate-limit', () => ({
  enforceUserRateLimit: vi.fn().mockReturnValue(null),
}))
vi.mock('@/lib/code-server', () => ({
  createWithCodeRetry: vi.fn(),
  generateNextSprintCode: vi.fn(),
}))
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: { findFirst: vi.fn() },
      story: {
        findMany: vi.fn(),
        updateMany: vi.fn(),
      },
      task: {
        findMany: vi.fn(),
        updateMany: vi.fn(),
      },
      $transaction: vi.fn(async (fn: (tx: typeof txClient) => unknown) => fn(txClient)),
      __txClient: txClient,
    },
  }
})

import { prisma } from '@/lib/prisma'
import { commitSprintMembershipAction } from '@/actions/sprints'

type Mocked = {
  sprint: { findFirst: 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

beforeEach(() => {
  vi.clearAllMocks()
  mockPrisma.sprint.findFirst.mockReset().mockResolvedValue({
    id: 'sprint-active',
    product_id: 'product-1',
  })
  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.story.updateMany.mockReset().mockResolvedValue({ count: 0 })
  mockPrisma.__txClient.task.updateMany.mockReset().mockResolvedValue({ count: 0 })
})

describe('commitSprintMembershipAction', () => {
  it('happy path: eligible adds + valid removes → transactie commits', async () => {
    // adds-partition: alle eligible (sprint_id=null + niet DONE)
    mockPrisma.story.findMany
      // partition lookup
      .mockResolvedValueOnce([
        { id: 's-add-1', sprint_id: null, status: 'OPEN', sprint: null },
      ])
      // removes-filter (sprint_id == activeSprintId)
      .mockResolvedValueOnce([{ id: 's-rem-1' }])
      // affectedStories
      .mockResolvedValueOnce([
        { pbi_id: 'pbiA' },
        { pbi_id: 'pbiB' },
      ])
    mockPrisma.task.findMany.mockResolvedValueOnce([{ id: 't1' }])

    const result = await commitSprintMembershipAction({
      activeSprintId: 'sprint-active',
      adds: ['s-add-1'],
      removes: ['s-rem-1'],
    })

    expect('success' in result).toBe(true)
    if ('success' in result) {
      expect(result.affectedStoryIds.sort()).toEqual(['s-add-1', 's-rem-1'])
      expect(result.affectedPbiIds.sort()).toEqual(['pbiA', 'pbiB'])
      expect(result.affectedTaskIds).toEqual(['t1'])
      expect(result.conflicts.notEligible).toEqual([])
      expect(result.conflicts.alreadyRemoved).toEqual([])
    }
    expect(mockPrisma.$transaction).toHaveBeenCalledTimes(1)
    expect(mockPrisma.__txClient.story.updateMany).toHaveBeenCalledTimes(2)
    expect(mockPrisma.__txClient.task.updateMany).toHaveBeenCalledTimes(2)
  })

  it('add met status=DONE → conflicts.notEligible, story niet ge-update', async () => {
    mockPrisma.story.findMany
      .mockResolvedValueOnce([
        { id: 's-done', sprint_id: null, status: 'DONE', sprint: null },
      ])
      // removes-filter (geen removes)
      .mockResolvedValueOnce([])

    const result = await commitSprintMembershipAction({
      activeSprintId: 'sprint-active',
      adds: ['s-done'],
      removes: [],
    })

    expect('success' in result).toBe(true)
    if ('success' in result) {
      expect(result.affectedStoryIds).toEqual([])
      expect(result.conflicts.notEligible).toEqual([
        { storyId: 's-done', reason: 'DONE' },
      ])
    }
    // Geen transaction omdat er niets te commiten valt.
    expect(mockPrisma.$transaction).not.toHaveBeenCalled()
  })

  it('add met sprint_id in andere OPEN sprint → conflicts.notEligible IN_OTHER_SPRINT', async () => {
    mockPrisma.story.findMany
      .mockResolvedValueOnce([
        {
          id: 's-elsewhere',
          sprint_id: 'sprint-other',
          status: 'IN_SPRINT',
          sprint: { id: 'sprint-other', code: 'SP-O', status: 'OPEN' },
        },
      ])
      .mockResolvedValueOnce([])

    const result = await commitSprintMembershipAction({
      activeSprintId: 'sprint-active',
      adds: ['s-elsewhere'],
      removes: [],
    })

    if ('success' in result) {
      expect(result.conflicts.notEligible).toEqual([
        { storyId: 's-elsewhere', reason: 'IN_OTHER_SPRINT' },
      ])
    }
  })

  it('remove voor story die niet in actieve sprint zit → conflicts.alreadyRemoved', async () => {
    mockPrisma.story.findMany
      // adds-partition (geen adds)
      .mockResolvedValueOnce([])
      // removes-filter — race scenario: story zit niet meer in active sprint
      .mockResolvedValueOnce([])

    const result = await commitSprintMembershipAction({
      activeSprintId: 'sprint-active',
      adds: [],
      removes: ['s-was-removed'],
    })

    if ('success' in result) {
      expect(result.affectedStoryIds).toEqual([])
      expect(result.conflicts.alreadyRemoved).toEqual(['s-was-removed'])
    }
  })

  it('transactie: story.status=IN_SPRINT bij add, =OPEN bij remove', async () => {
    mockPrisma.story.findMany
      .mockResolvedValueOnce([
        { id: 's-add', sprint_id: null, status: 'OPEN', sprint: null },
      ])
      .mockResolvedValueOnce([{ id: 's-rem' }])
      .mockResolvedValueOnce([{ pbi_id: 'pbiA' }])
    mockPrisma.task.findMany.mockResolvedValueOnce([])

    await commitSprintMembershipAction({
      activeSprintId: 'sprint-active',
      adds: ['s-add'],
      removes: ['s-rem'],
    })

    const calls = mockPrisma.__txClient.story.updateMany.mock.calls
    // Add: status=IN_SPRINT + sprint_id=sprint-active
    expect(calls[0][0].data).toEqual({
      sprint_id: 'sprint-active',
      status: 'IN_SPRINT',
    })
    // Remove: status=OPEN + sprint_id=null
    expect(calls[1][0].data).toEqual({ sprint_id: null, status: 'OPEN' })
  })

  it('task.sprint_id wordt in dezelfde transactie ge-update', async () => {
    mockPrisma.story.findMany
      .mockResolvedValueOnce([
        { id: 's-add', sprint_id: null, status: 'OPEN', sprint: null },
      ])
      .mockResolvedValueOnce([])
      .mockResolvedValueOnce([{ pbi_id: 'pbiA' }])
    mockPrisma.task.findMany.mockResolvedValueOnce([])

    await commitSprintMembershipAction({
      activeSprintId: 'sprint-active',
      adds: ['s-add'],
      removes: [],
    })

    expect(mockPrisma.__txClient.task.updateMany).toHaveBeenCalledWith(
      expect.objectContaining({
        where: { story_id: { in: ['s-add'] } },
        data: { sprint_id: 'sprint-active' },
      }),
    )
  })

  it('return: affectedStoryIds + affectedPbiIds + affectedTaskIds + conflicts', async () => {
    mockPrisma.story.findMany
      .mockResolvedValueOnce([
        { id: 's-add', sprint_id: null, status: 'OPEN', sprint: null },
      ])
      .mockResolvedValueOnce([{ id: 's-rem' }])
      .mockResolvedValueOnce([
        { pbi_id: 'pbiA' },
        { pbi_id: 'pbiB' },
      ])
    mockPrisma.task.findMany.mockResolvedValueOnce([
      { id: 't1' },
      { id: 't2' },
    ])

    const result = await commitSprintMembershipAction({
      activeSprintId: 'sprint-active',
      adds: ['s-add'],
      removes: ['s-rem'],
    })

    expect(result).toMatchObject({
      success: true,
      affectedStoryIds: expect.arrayContaining(['s-add', 's-rem']),
      affectedPbiIds: expect.arrayContaining(['pbiA', 'pbiB']),
      affectedTaskIds: expect.arrayContaining(['t1', 't2']),
    })
  })

  it('rejects when sprint is not accessible', async () => {
    mockPrisma.sprint.findFirst.mockResolvedValue(null)

    const result = await commitSprintMembershipAction({
      activeSprintId: 'sprint-active',
      adds: [],
      removes: [],
    })

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