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<Record<string, unknown>>) {
  return {
    story: {
      findMany: vi.fn().mockResolvedValue(stories),
    },
  } as unknown as Parameters<typeof partitionByEligibility>[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 an OPEN sprint', () => {
    expect(
      isEligibleForSprint({
        sprint_id: 'abc',
        status: 'IN_SPRINT' as StoryStatus,
        sprint: { status: 'OPEN' },
      }),
    ).toBe(false)
  })

  it('returns false when story is DONE (sprint_id irrelevant)', () => {
    expect(
      isEligibleForSprint({
        sprint_id: 'abc',
        status: 'DONE' as StoryStatus,
        sprint: { status: 'CLOSED' },
      }),
    ).toBe(false)
  })

  it('returns true when story is in a CLOSED sprint (released back to planning)', () => {
    expect(
      isEligibleForSprint({
        sprint_id: 'abc',
        status: 'IN_SPRINT' as StoryStatus,
        sprint: { status: 'CLOSED' },
      }),
    ).toBe(true)
  })

  it('returns true when story is in an ARCHIVED sprint', () => {
    expect(
      isEligibleForSprint({
        sprint_id: 'abc',
        status: 'IN_SPRINT' as StoryStatus,
        sprint: { status: 'ARCHIVED' },
      }),
    ).toBe(true)
  })

  it('returns true when story is in a FAILED sprint', () => {
    expect(
      isEligibleForSprint({
        sprint_id: 'abc',
        status: 'IN_SPRINT' as StoryStatus,
        sprint: { status: 'FAILED' },
      }),
    ).toBe(true)
  })

  it('returns false when sprint_id is set but sprint relation is missing (defensive)', () => {
    // Zonder sprint-data weten we niet of die OPEN is, dus blijven we
    // conservatief — niet eligible.
    expect(
      isEligibleForSprint({
        sprint_id: 'abc',
        status: 'IN_SPRINT' 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('frees stories from a CLOSED sprint — they become eligible again', async () => {
    const prisma = mockPrisma([
      {
        id: 's1',
        sprint_id: 'sprint-closed',
        status: 'IN_SPRINT',
        sprint: { id: 'sprint-closed', code: 'SP-C', status: 'CLOSED' },
      },
    ])
    const result = await partitionByEligibility(prisma, ['s1'])
    expect(result.eligible).toEqual(['s1'])
    expect(result.crossSprint).toEqual([])
    expect(result.notEligible).toEqual([])
  })

  it('frees stories from ARCHIVED and FAILED sprints', async () => {
    const prisma = mockPrisma([
      {
        id: 's1',
        sprint_id: 'sprint-arch',
        status: 'IN_SPRINT',
        sprint: { id: 'sprint-arch', code: 'SP-A', status: 'ARCHIVED' },
      },
      {
        id: 's2',
        sprint_id: 'sprint-fail',
        status: 'IN_SPRINT',
        sprint: { id: 'sprint-fail', code: 'SP-F', status: 'FAILED' },
      },
    ])
    const result = await partitionByEligibility(prisma, ['s1', 's2'])
    expect(result.eligible).toEqual(['s1', 's2'])
    expect(result.notEligible).toEqual([])
  })

  it('a DONE story in a CLOSED sprint is notEligible because DONE (sprint inactive)', async () => {
    // Volgorde: niet-actieve sprint blokkeert niet meer, dus de DONE-check
    // bepaalt de reason. Vroeger werd dit 'IN_OTHER_SPRINT' — dat was misleidend
    // omdat de sprint helemaal niet meer actief was.
    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: 'DONE' }])
    expect(result.eligible).toEqual([])
  })

  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)
  })
})
