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

import { type DependencyEdge, schedule, type SchedulableIssue, selectFocus } from './scheduler-v0.js'

function issue(number: number, over: Partial<SchedulableIssue> = {}): SchedulableIssue {
  return { number, title: `#${number}`, labels: [], estimateDays: 2, priority: 2, ...over }
}

describe('schedule', () => {
  it('lays open issues out serially with cumulative start/finish', () => {
    const plan = schedule([issue(1, { estimateDays: 2 }), issue(2, { estimateDays: 3 })], [])
    expect(plan.cycle).toBeNull()
    const byN = Object.fromEntries(plan.items.map((i) => [i.number, i]))
    // total span = sum of durations, no overlap (single serial worker)
    const spans = plan.items.map((i) => [i.startDay, i.endDay])
    expect(spans).toContainEqual([0, expect.any(Number)])
    expect(byN[1].durationDays + byN[2].durationDays).toBe(5)
    expect(Math.max(...plan.items.map((i) => i.endDay))).toBe(5)
  })

  it('respects dependencies: a dependency is scheduled before its dependent', () => {
    // #2 depends on #1
    const edges: DependencyEdge[] = [{ issue: 2, dependsOn: 1 }]
    const plan = schedule([issue(2, { priority: 1 }), issue(1, { priority: 4 })], edges)
    const order = plan.items.map((i) => i.number)
    expect(order.indexOf(1)).toBeLessThan(order.indexOf(2))
    // #2 records #1 as a blocker; #1 records #2 as blocked
    const two = plan.items.find((i) => i.number === 2)!
    expect(two.blockedBy).toEqual([1])
    const one = plan.items.find((i) => i.number === 1)!
    expect(one.blocks).toEqual([2])
  })

  it('breaks ties among ready roots by priority, then estimate', () => {
    const plan = schedule(
      [issue(1, { priority: 3 }), issue(2, { priority: 1 }), issue(3, { priority: 2 })],
      [],
    )
    expect(plan.items.map((i) => i.number)).toEqual([2, 3, 1])
  })

  it('detects a dependency cycle instead of looping', () => {
    const edges: DependencyEdge[] = [
      { issue: 1, dependsOn: 2 },
      { issue: 2, dependsOn: 1 },
    ]
    const plan = schedule([issue(1), issue(2)], edges)
    expect(plan.items).toHaveLength(0)
    expect(plan.cycle).not.toBeNull()
    expect(plan.cycle!.sort()).toEqual([1, 2])
  })

  it('drops dependencies on out-of-scope (closed) issues', () => {
    // #2 depends on #9 which isn't in scope → treated as satisfied
    const plan = schedule([issue(2)], [{ issue: 2, dependsOn: 9 }])
    expect(plan.cycle).toBeNull()
    expect(plan.items.find((i) => i.number === 2)!.blockedBy).toEqual([])
  })

  it('marks the longest dependency chain critical', () => {
    // chain 1→2→3 (each 3d = 9d) vs standalone 4 (2d): 1,2,3 critical, 4 not
    const edges: DependencyEdge[] = [
      { issue: 2, dependsOn: 1 },
      { issue: 3, dependsOn: 2 },
    ]
    const issues = [
      issue(1, { estimateDays: 3 }),
      issue(2, { estimateDays: 3 }),
      issue(3, { estimateDays: 3 }),
      issue(4, { estimateDays: 2 }),
    ]
    const plan = schedule(issues, edges)
    const crit = plan.items.filter((i) => i.critical).map((i) => i.number).sort()
    expect(crit).toEqual([1, 2, 3])
    expect(plan.items.find((i) => i.number === 4)!.critical).toBe(false)
  })

  it('defaults a null estimate to DEFAULT_ESTIMATE_DAYS', () => {
    const plan = schedule([issue(1, { estimateDays: null })], [])
    expect(plan.items[0].durationDays).toBe(2)
  })
})

describe('selectFocus', () => {
  it('picks the first three of the ordered plan as now/next/later', () => {
    const plan = schedule([issue(1, { priority: 1 }), issue(2, { priority: 2 }), issue(3, { priority: 3 })], [])
    const focus = selectFocus(plan)
    expect(focus.now?.number).toBe(1)
    expect(focus.next?.number).toBe(2)
    expect(focus.later?.number).toBe(3)
  })

  it('returns nulls past the end of a short plan', () => {
    const focus = selectFocus(schedule([issue(1)], []))
    expect(focus.now?.number).toBe(1)
    expect(focus.next).toBeNull()
    expect(focus.later).toBeNull()
  })
})
