import { describe, it, expect, vi } from 'vitest' import type { StoryStatus } from '@prisma/client' import { getBlockingSprintMap, isEligibleForSprint, partitionByEligibility, } from '@/lib/sprint-conflicts' function mockPrisma(stories: Array>) { return { story: { findMany: vi.fn().mockResolvedValue(stories), }, } as unknown as Parameters[0] } describe('isEligibleForSprint', () => { it('returns true for OPEN story without sprint', () => { expect( isEligibleForSprint({ sprint_id: null, status: 'OPEN' as StoryStatus }), ).toBe(true) }) it('returns true for IN_SPRINT story without sprint_id (edge: restoration)', () => { expect( isEligibleForSprint({ sprint_id: null, status: 'IN_SPRINT' as StoryStatus, }), ).toBe(true) }) it('returns false for DONE story without sprint', () => { expect( isEligibleForSprint({ sprint_id: null, status: 'DONE' as StoryStatus }), ).toBe(false) }) it('returns false when story is in any sprint (open status)', () => { expect( isEligibleForSprint({ sprint_id: 'abc', status: 'OPEN' as StoryStatus, }), ).toBe(false) }) it('returns false when story is in any sprint (done status)', () => { expect( isEligibleForSprint({ sprint_id: 'abc', status: 'DONE' as StoryStatus, }), ).toBe(false) }) }) describe('partitionByEligibility', () => { it('returns empty partition for empty input', async () => { const prisma = mockPrisma([]) const result = await partitionByEligibility(prisma, []) expect(result).toEqual({ eligible: [], notEligible: [], crossSprint: [] }) }) it('classifies all eligible when stories are free + OPEN', async () => { const prisma = mockPrisma([ { id: 's1', sprint_id: null, status: 'OPEN', sprint: null }, { id: 's2', sprint_id: null, status: 'IN_SPRINT', sprint: null }, ]) const result = await partitionByEligibility(prisma, ['s1', 's2']) expect(result.eligible).toEqual(['s1', 's2']) expect(result.notEligible).toEqual([]) expect(result.crossSprint).toEqual([]) }) it('marks DONE stories as notEligible with reason=DONE', async () => { const prisma = mockPrisma([ { id: 's1', sprint_id: null, status: 'DONE', sprint: null }, ]) const result = await partitionByEligibility(prisma, ['s1']) expect(result.eligible).toEqual([]) expect(result.notEligible).toEqual([{ storyId: 's1', reason: 'DONE' }]) }) it('marks stories in other OPEN sprint as crossSprint + notEligible', async () => { const prisma = mockPrisma([ { id: 's1', sprint_id: 'sprint-other', status: 'IN_SPRINT', sprint: { id: 'sprint-other', code: 'SP-2', status: 'OPEN' }, }, ]) const result = await partitionByEligibility(prisma, ['s1']) expect(result.crossSprint).toEqual([ { storyId: 's1', sprintId: 'sprint-other', sprintName: 'SP-2' }, ]) expect(result.notEligible).toEqual([ { storyId: 's1', reason: 'IN_OTHER_SPRINT' }, ]) expect(result.eligible).toEqual([]) }) it('classifies story in CLOSED sprint with status=OPEN as eligible (status reset already happened)', async () => { const prisma = mockPrisma([ { id: 's1', sprint_id: null, status: 'OPEN', sprint: null, }, ]) const result = await partitionByEligibility(prisma, ['s1']) expect(result.eligible).toEqual(['s1']) }) it('does NOT mark crossSprint for stories in CLOSED other sprint', async () => { const prisma = mockPrisma([ { id: 's1', sprint_id: 'sprint-closed', status: 'DONE', sprint: { id: 'sprint-closed', code: 'SP-C', status: 'CLOSED' }, }, ]) const result = await partitionByEligibility(prisma, ['s1']) expect(result.crossSprint).toEqual([]) expect(result.notEligible).toEqual([ { storyId: 's1', reason: 'IN_OTHER_SPRINT' }, ]) }) it('respects excludeSprintId — story in same sprint is eligible', async () => { const prisma = mockPrisma([ { id: 's1', sprint_id: 'sprint-active', status: 'IN_SPRINT', sprint: { id: 'sprint-active', code: 'SP-A', status: 'OPEN' }, }, ]) const result = await partitionByEligibility(prisma, ['s1'], 'sprint-active') expect(result.eligible).toEqual(['s1']) expect(result.crossSprint).toEqual([]) }) }) describe('getBlockingSprintMap', () => { it('returns empty map for empty input', async () => { const prisma = mockPrisma([]) const result = await getBlockingSprintMap(prisma, 'p1', []) expect(result.size).toBe(0) }) it('returns blocking sprint info for stories in OPEN sprints', async () => { const prisma = mockPrisma([ { id: 's1', sprint_id: 'sprint-x', sprint: { id: 'sprint-x', code: 'SP-X', status: 'OPEN' }, }, ]) const result = await getBlockingSprintMap(prisma, 'p1', ['s1']) expect(result.get('s1')).toEqual({ sprintId: 'sprint-x', sprintName: 'SP-X', }) }) it('excludes the active sprint from blocking', async () => { const prisma = mockPrisma([ { id: 's1', sprint_id: 'sprint-active', sprint: { id: 'sprint-active', code: 'SP-A', status: 'OPEN' }, }, ]) const result = await getBlockingSprintMap( prisma, 'p1', ['s1'], 'sprint-active', ) expect(result.size).toBe(0) }) it('does not include CLOSED sprints (filtered at DB query level)', async () => { // The prisma mock receives WHERE sprint.status='OPEN' so CLOSED stories // are already filtered out before reaching this function's mapping logic. const prisma = mockPrisma([]) const result = await getBlockingSprintMap(prisma, 'p1', ['s1']) expect(result.size).toBe(0) }) })