import { describe, expect, it } from 'vitest'
import { compareOptional, formatDurationMs, formatRelativeTime, formatTraceAxisTime, traceAxisTicks } from './format'

describe('formatDurationMs', () => {
  it.each([
    [61, '61ms'],
    [999, '999ms'],
    [1000, '1s'],
    [45_000, '45s'],
  ])('formats %dms as %s', (durationMs, expected) => {
    expect(formatDurationMs(durationMs)).toBe(expected)
  })
})

describe('formatRelativeTime', () => {
  const now = Date.UTC(2026, 6, 18, 12)

  it.each([
    ['just now', now - 59_999, 'just now'],
    ['minutes', now - 2 * 60_000, '2m ago'],
    ['hours', now - 4 * 3_600_000, '4h ago'],
    ['days', now - 2 * 86_400_000, '2d ago'],
    ['future minutes', now + 3 * 60_000, 'in 3m'],
    ['future hours', now + 5 * 3_600_000, 'in 5h'],
    ['future days', now + 2 * 86_400_000, 'in 2d'],
  ])('formats %s', (_label, epochMs, expected) => {
    expect(formatRelativeTime(epochMs, now)).toBe(expected)
  })
})

describe('compareOptional', () => {
  it('sorts missing values last in both directions', () => {
    expect(compareOptional(null, 1, 'asc')).toBe(1)
    expect(compareOptional(null, 1, 'desc')).toBe(1)
    expect(compareOptional(1, null, 'asc')).toBe(-1)
    expect(compareOptional(1, 2, 'desc')).toBe(1)
  })
})

describe('formatTraceAxisTime', () => {
  const base = Date.UTC(2026, 7, 9, 14, 30, 45)

  it('shows seconds for short spans', () => {
    expect(formatTraceAxisTime(base, base, base + 45_000)).toBe('14:30:45')
  })

  it('shows hours and minutes for sub-day spans', () => {
    expect(formatTraceAxisTime(base, base, base + 3_600_000)).toBe('14:30')
  })

  it('shows month and day for multi-day spans', () => {
    expect(formatTraceAxisTime(base, base, base + 172_800_000)).toBe('Aug 9, 14:30')
  })

  it('formats UTC midnight as 00:xx, not 24:xx', () => {
    const midnight = Date.UTC(2026, 7, 10, 0, 0, 0)
    expect(formatTraceAxisTime(midnight, midnight, midnight + 3_600_000)).toBe('00:00')
    expect(formatTraceAxisTime(midnight, midnight, midnight + 45_000)).toBe('00:00:00')
  })

  it('shows seconds for >=60s spans when tick step is under one minute', () => {
    const base = Date.UTC(2026, 7, 9, 14, 30, 0)
    expect(formatTraceAxisTime(base, base, base + 90_000, 30_000)).toBe('14:30:00')
    expect(formatTraceAxisTime(base + 60_000, base, base + 90_000, 30_000)).toBe('14:31:00')
  })

  it('keeps adjacent labels distinct across 60–120s spans', () => {
    const start = Date.UTC(2026, 7, 9, 14, 30, 0)
    const end = start + 61_000
    const labels = traceAxisTicks(start, end, 8).map((tick) => tick.label)
    for (let index = 1; index < labels.length; index += 1) {
      expect(labels[index]).not.toBe(labels[index - 1])
    }
  })

  it('includes date context when a sub-day span crosses UTC midnight', () => {
    const start = Date.UTC(2026, 7, 9, 22, 0, 0)
    const end = Date.UTC(2026, 7, 10, 2, 0, 0)
    expect(formatTraceAxisTime(start, start, end)).toMatch(/Aug 9/)
    expect(formatTraceAxisTime(end, start, end)).toMatch(/Aug 10/)
  })

  it('includes year when a span crosses a UTC calendar year', () => {
    const start = Date.UTC(2025, 11, 31, 22, 0, 0)
    const end = Date.UTC(2026, 0, 1, 2, 0, 0)
    expect(formatTraceAxisTime(start, start, end)).toMatch(/2025/)
    expect(formatTraceAxisTime(end, start, end)).toMatch(/2026/)
  })

  it('shows millisecond precision for sub-second spans', () => {
    const start = Date.UTC(2026, 7, 9, 14, 30, 45, 0)
    const end = start + 500
    expect(formatTraceAxisTime(start, start, end)).toMatch(/45\.\d{3}/)
    expect(formatTraceAxisTime(end, start, end)).toMatch(/45\.5\d{2}/)
  })
})

describe('traceAxisTicks', () => {
  it('derives ticks from start and end bounds', () => {
    const start = 1_000
    const end = 31_000
    const ticks = traceAxisTicks(start, end, 8)
    expect(ticks[0]).toMatchObject({ pct: 0, epochMs: start, label: '00:00:01' })
    expect(ticks.at(-1)).toMatchObject({ pct: 100, epochMs: end })
    expect(ticks.length).toBeGreaterThan(1)
    expect(ticks.length).toBeLessThanOrEqual(8)
  })

  it('appends the exact end bound when span is not divisible by the step', () => {
    const start = 1_000
    const end = 31_500
    const ticks = traceAxisTicks(start, end, 8)
    expect(ticks[0]).toMatchObject({ pct: 0, epochMs: start, label: '00:00:01' })
    expect(ticks.at(-1)).toMatchObject({ pct: 100, epochMs: end, label: '00:00:31' })
    expect(ticks.some((tick) => tick.epochMs === 30_000)).toBe(false)
    expect(ticks.some((tick) => tick.epochMs === 25_000)).toBe(true)
  })

  it('suppresses interior ticks within one step of start or end', () => {
    const start = 0
    const end = 61_000
    const ticks = traceAxisTicks(start, end, 8)
    expect(ticks[0]).toMatchObject({ pct: 0, epochMs: start })
    expect(ticks.at(-1)).toMatchObject({ pct: 100, epochMs: end })
    expect(ticks.some((tick) => tick.epochMs === 60_000)).toBe(false)
    const interior = ticks.slice(1, -1)
    const stepMs = interior.length >= 2 ? interior[1]!.epochMs - interior[0]!.epochMs : 10_000
    for (const tick of interior) {
      expect(tick.epochMs - start).toBeGreaterThanOrEqual(stepMs)
      expect(end - tick.epochMs).toBeGreaterThanOrEqual(stepMs)
    }
  })

  it('steps across long spans without exceeding maxTicks', () => {
    const start = Date.UTC(2026, 0, 1)
    const end = start + 3 * 86_400_000
    const ticks = traceAxisTicks(start, end, 6)
    expect(ticks[0]?.label).toMatch(/Jan/)
    expect(ticks.length).toBeLessThanOrEqual(6)
  })

  it('honors maxTicks for spans longer than 49 days', () => {
    const start = Date.UTC(2026, 0, 1)
    const end = start + 70 * 86_400_000
    const ticks = traceAxisTicks(start, end, 8)
    expect(ticks[0]).toMatchObject({ pct: 0, epochMs: start })
    expect(ticks.at(-1)).toMatchObject({ pct: 100, epochMs: end })
    expect(ticks.length).toBeLessThanOrEqual(8)
  })

  it('aligns interior ticks to UTC wall-clock step boundaries', () => {
    const start = Date.UTC(2026, 7, 9, 14, 17, 23)
    const end = Date.UTC(2026, 7, 9, 20, 42, 11)
    const ticks = traceAxisTicks(start, end, 8)
    const interior = ticks.slice(1, -1)
    expect(interior.length).toBeGreaterThan(0)
    for (const tick of interior) {
      expect(tick.epochMs % 3_600_000).toBe(0)
    }
    expect(ticks[0]?.epochMs).toBe(start)
    expect(ticks.at(-1)?.epochMs).toBe(end)
  })

  it('labels cross-midnight sub-day spans with date context', () => {
    const start = Date.UTC(2026, 7, 9, 22, 0, 0)
    const end = Date.UTC(2026, 7, 10, 2, 0, 0)
    const ticks = traceAxisTicks(start, end, 8)
    expect(ticks.some((tick) => /Aug 9/.test(tick.label))).toBe(true)
    expect(ticks.some((tick) => /Aug 10/.test(tick.label))).toBe(true)
  })

  it('labels sub-second spans with distinct millisecond endpoints', () => {
    const start = Date.UTC(2026, 7, 9, 14, 30, 45, 0)
    const end = start + 500
    const ticks = traceAxisTicks(start, end, 8)
    expect(ticks[0]).toMatchObject({ pct: 0, epochMs: start })
    expect(ticks.at(-1)).toMatchObject({ pct: 100, epochMs: end })
    expect(ticks[0]!.label).not.toBe(ticks.at(-1)!.label)
    expect(ticks.length).toBeLessThanOrEqual(8)
  })

  it('includes year on date-bearing labels when span crosses a UTC calendar year', () => {
    const start = Date.UTC(2025, 11, 31, 22, 0, 0)
    const end = Date.UTC(2026, 0, 1, 2, 0, 0)
    const ticks = traceAxisTicks(start, end, 8)
    expect(ticks.some((tick) => /2025/.test(tick.label))).toBe(true)
    expect(ticks.some((tick) => /2026/.test(tick.label))).toBe(true)
    expect(ticks[0]!.label).not.toBe(ticks.at(-1)!.label)
  })

  it('selects oversized steps in O(1) time for very long spans', () => {
    const start = Date.UTC(2026, 0, 1)
    const end = start + 10_000 * 86_400_000
    const ticks = traceAxisTicks(start, end, 8)
    expect(ticks[0]).toMatchObject({ pct: 0, epochMs: start })
    expect(ticks.at(-1)).toMatchObject({ pct: 100, epochMs: end })
    expect(ticks.length).toBeLessThanOrEqual(8)
  })
})
