import { describe, expect, it } from 'vitest'
import type { RequestRow } from './request-types'
import {
  distinctProjects,
  distinctPlanSlugs,
  distinctSessions,
  deliveryEvents,
  groupShipped,
  latestRequestEvent,
  matchesProject,
  matchesPlanSlug,
  matchesSession,
  matchesSearch,
  ordinal,
  priorityVariant,
  queuePositionLabel,
  shippedBucket,
  sortByAskedOrder,
  stageActiveStep,
  summaryCounts,
  waitReason,
  sessionFilterLabel,
} from './request-board'

function row(overrides: Partial<RequestRow>): RequestRow {
  return {
    id: 'req-a',
    title: 'Title',
    project: 'overdeck',
    state: 'asked',
    priority: 'NORMAL',
    origin: 'owner',
    asked_at: '2026-08-14T00:00:00.000Z',
    updated_at: '2026-08-14T00:00:00.000Z',
    worker: null,
    detail: null,
    proof_url: null,
    plan_ref: null,
    ...overrides,
  }
}

describe('priorityVariant', () => {
  it('maps known priorities and falls back for unknown/numeric values', () => {
    expect(priorityVariant('FIRE')).toBe('error')
    expect(priorityVariant('HIGH')).toBe('warning')
    expect(priorityVariant('NORMAL')).toBe('neutral')
    expect(priorityVariant('3')).toBe('info')
    expect(priorityVariant('unknown')).toBe('info')
  })
})

describe('request attribution filters', () => {
  it('lists plan slugs and friendly sessions, falling back to a short session id label', () => {
    const rows = [row({ plan_ref: 'task-lifecycle', session_name: 'memory-organizer-fluttering-owl', session_id: 'session-a' }), row({ id: 'b', plan_ref: 'other-plan', session_id: '1234567890abcdef' }), row({ id: 'c' })]
    expect(distinctPlanSlugs(rows)).toEqual(['other-plan', 'task-lifecycle'])
    expect(distinctSessions(rows)).toEqual(['1234567890abcdef', 'memory-organizer-fluttering-owl'])
    expect(sessionFilterLabel('1234567890abcdef')).toBe('12345678…cdef')
    expect(matchesPlanSlug(rows[0]!, 'task-lifecycle')).toBe(true)
    expect(matchesSession(rows[1]!, '1234567890abcdef')).toBe(true)
    expect(matchesSession(rows[2]!, '1234567890abcdef')).toBe(false)
  })
})

describe('sortByAskedOrder', () => {
  it('sorts ascending by asked_at, tiebreaking by id so ties stay stable', () => {
    const rows = [
      row({ id: 'b', asked_at: '2026-08-14T00:00:00.000Z' }),
      row({ id: 'a', asked_at: '2026-08-14T00:00:00.000Z' }),
      row({ id: 'c', asked_at: '2026-08-10T00:00:00.000Z' }),
    ]
    expect(sortByAskedOrder(rows).map((r) => r.id)).toEqual(['c', 'a', 'b'])
  })
})

describe('ordinal / queuePositionLabel', () => {
  it('handles the 11-13 teen exceptions', () => {
    expect(ordinal(1)).toBe('1st')
    expect(ordinal(2)).toBe('2nd')
    expect(ordinal(3)).toBe('3rd')
    expect(ordinal(4)).toBe('4th')
    expect(ordinal(11)).toBe('11th')
    expect(ordinal(12)).toBe('12th')
    expect(ordinal(13)).toBe('13th')
    expect(ordinal(21)).toBe('21st')
    expect(ordinal(22)).toBe('22nd')
    expect(ordinal(23)).toBe('23rd')
    expect(ordinal(111)).toBe('111th')
  })
  it('renders a zero-based index as a one-based ordinal label', () => {
    expect(queuePositionLabel(0)).toBe('1st in line')
    expect(queuePositionLabel(12)).toBe('13th in line')
  })
})

describe('waitReason', () => {
  it('never fabricates a specific queue type', () => {
    expect(waitReason(0)).toBe('Next up once a lane opens')
    expect(waitReason(1)).toBe('1 request ahead in line')
    expect(waitReason(2)).toBe('2 requests ahead in line')
  })
})

describe('stageActiveStep', () => {
  it('is always null — the store has no stage column to derive from', () => {
    expect(stageActiveStep(row({ state: 'in_flight', worker: 'codex', detail: 'building' }))).toBeNull()
  })
})

describe('shippedBucket / groupShipped', () => {
  // Built from local-calendar arithmetic (never a hardcoded UTC string) so the fixture is
  // correct in every timezone the test runs in — bucketing is defined in local calendar time.
  const now = new Date(2026, 7, 14, 12, 0, 0)
  const today = new Date(2026, 7, 14, 1, 0, 0)
  const yesterday = new Date(2026, 7, 13, 23, 0, 0)
  const thisWeek = new Date(2026, 7, 9, 0, 0, 0)
  const earlier = new Date(2026, 6, 1, 0, 0, 0)
  it('buckets Today / Yesterday / This week / Earlier from updated_at', () => {
    expect(shippedBucket(today.toISOString(), now)).toBe('Today')
    expect(shippedBucket(yesterday.toISOString(), now)).toBe('Yesterday')
    expect(shippedBucket(thisWeek.toISOString(), now)).toBe('This week')
    expect(shippedBucket(earlier.toISOString(), now)).toBe('Earlier')
  })
  it('groups rows into buckets by their updated_at', () => {
    const rows = [
      row({ id: 'today', state: 'shipped', updated_at: today.toISOString() }),
      row({ id: 'yesterday', state: 'shipped', updated_at: yesterday.toISOString() }),
      row({ id: 'earlier', state: 'shipped', updated_at: earlier.toISOString() }),
    ]
    const groups = groupShipped(rows, now)
    expect(groups.Today.map((r) => r.id)).toEqual(['today'])
    expect(groups.Yesterday.map((r) => r.id)).toEqual(['yesterday'])
    expect(groups['This week']).toEqual([])
    expect(groups.Earlier.map((r) => r.id)).toEqual(['earlier'])
  })
})

describe('latestRequestEvent / deliveryEvents', () => {
  it('chooses the newest authoritative receipt or transition before the detail fallback', () => {
    const request = row({
      detail: 'Fallback detail',
      receipt_trail: [
        { id: 'old', request_id: 'req-a', at: '2026-08-14T01:00:00Z', kind: 'claimed', line: 'Work started' },
        { id: 'new', request_id: 'req-a', at: '2026-08-14T02:00:00Z', kind: 'progress', line: 'Tests passed' },
      ],
      transition_trail: [{ id: 'transition', request_id: 'req-a', at: '2026-08-14T03:00:00Z', from_state: 'asked', to_state: 'in_flight', actor: 'hook', reason: 'Claimed' }],
    })
    expect(latestRequestEvent(request)).toEqual({ id: 'transition', at: '2026-08-14T03:00:00Z', label: 'Claimed', kind: 'transition' })
  })

  it('returns no event when the store recorded none', () => {
    expect(latestRequestEvent(row({ detail: null, receipt_trail: [], transition_trail: [] }))).toBeNull()
  })

  it('projects only landed and deployed receipts newest first', () => {
    const events = deliveryEvents([row({
      receipt_trail: [
        { id: 'progress', request_id: 'req-a', at: '2026-08-14T01:00:00Z', kind: 'progress', line: 'Tests passed' },
        { id: 'landed', request_id: 'req-a', at: '2026-08-14T02:00:00Z', kind: 'landed', line: 'Landed' },
        { id: 'deployed', request_id: 'req-a', at: '2026-08-14T03:00:00Z', kind: 'deployed', line: 'Deployed' },
      ],
    })])
    expect(events.map((event) => event.id)).toEqual(['deployed', 'landed'])
  })
})

describe('summaryCounts', () => {
  it('counts per column honestly, shipped-today scoped by bucket', () => {
    const now = new Date(2026, 7, 14, 12, 0, 0)
    const rows = [
      row({ id: '1', state: 'in_flight' }),
      row({ id: '2', state: 'in_flight' }),
      row({ id: '3', state: 'asked' }),
      row({ id: '4', state: 'blocked_needs_owner' }),
      row({ id: 'recovery', state: 'orphaned' }),
      row({ id: 'canceled', state: 'canceled' }),
      row({ id: '5', state: 'shipped', updated_at: new Date(2026, 7, 14, 1, 0, 0).toISOString() }),
      row({ id: '6', state: 'shipped', updated_at: new Date(2026, 6, 1, 0, 0, 0).toISOString() }),
    ]
    expect(summaryCounts(rows, now)).toEqual({ inFlight: 2, queued: 1, blocked: 1, needsRecovery: 1, shippedToday: 1 })
  })
})

describe('distinctProjects', () => {
  it('dedupes and sorts', () => {
    const rows = [row({ project: 'zebra' }), row({ project: 'ack' }), row({ project: 'zebra' })]
    expect(distinctProjects(rows)).toEqual(['ack', 'zebra'])
  })
})

describe('matchesSearch / matchesProject', () => {
  it('matches title, project, or detail case-insensitively; empty query matches all', () => {
    const r = row({ title: 'Ship Requests Board', project: 'overdeck', detail: 'four columns' })
    expect(matchesSearch(r, '')).toBe(true)
    expect(matchesSearch(r, 'requests')).toBe(true)
    expect(matchesSearch(r, 'OVERDECK')).toBe(true)
    expect(matchesSearch(r, 'four columns')).toBe(true)
    expect(matchesSearch(r, 'nonexistent')).toBe(false)
  })
  it('filters by exact project or passes through when null', () => {
    const r = row({ project: 'overdeck' })
    expect(matchesProject(r, null)).toBe(true)
    expect(matchesProject(r, 'overdeck')).toBe(true)
    expect(matchesProject(r, 'other')).toBe(false)
  })
})
