import { DateTime } from 'luxon'
import type { WeeklyRule } from '../../schemas/availability-schedule.schema.js'
import { computeSlots, SlotComputeInput } from './slot-computer.js'

// Pure slot-math tests, with a focus on the DST boundary (DoD: "display is
// correct across a DST boundary"). All assertions compute the expected UTC with
// Luxon so they stay correct regardless of the machine's local tz.

const TZ = 'Europe/Copenhagen'

// 09:00–17:00 every day, in minutes from local midnight.
const allWeek = (start = 540, end = 1020): WeeklyRule[] =>
  Array.from({ length: 7 }, (_, dow) => ({ dayOfWeek: dow, intervals: [{ startMinute: start, endMinute: end }] }))

function baseInput(over: Partial<SlotComputeInput> = {}): SlotComputeInput {
  return {
    durationMinutes: 60,
    slotIntervalMinutes: 60,
    bufferBeforeMinutes: 0,
    bufferAfterMinutes: 0,
    minimumNoticeMinutes: 0,
    maximumDaysInFuture: 365,
    scheduleTimezone: TZ,
    weeklyRules: allWeek(),
    dateOverrides: [],
    busy: [],
    now: new Date('2026-06-01T00:00:00Z'),
    fromUtc: new Date('2026-06-01T00:00:00Z'),
    toUtc: new Date('2026-06-02T00:00:00Z'),
    ...over,
  }
}

const iso = (s: string, tz = TZ) => DateTime.fromISO(s, { zone: tz }).toUTC().toISO()
const isoSet = (slots: { startUtc: Date }[]) => slots.map((s) => s.startUtc.toISOString().replace('.000Z', 'Z'))

describe('computeSlots', () => {
  it('generates one slot per step within the daily window', () => {
    // 09:00–17:00, 60-min slots → 09,10,11,12,13,14,15,16 = 8 slots.
    const slots = computeSlots(baseInput())
    expect(slots).toHaveLength(8)
    expect(slots[0].startUtc.toISOString()).toBe(new Date(iso('2026-06-01T09:00')!).toISOString())
    expect(slots[7].startUtc.toISOString()).toBe(new Date(iso('2026-06-01T16:00')!).toISOString())
  })

  it('respects slotIntervalMinutes independent of duration', () => {
    // 30-min slots, 60-min duration, 09:00–11:00 window → 09:00, 09:30, 10:00.
    const slots = computeSlots(
      baseInput({ durationMinutes: 60, slotIntervalMinutes: 30, weeklyRules: allWeek(540, 660) }),
    )
    expect(slots.map((s) => DateTime.fromJSDate(s.startUtc).setZone(TZ).toFormat('HH:mm'))).toEqual([
      '09:00', '09:30', '10:00',
    ])
  })

  it('drops slots inside the minimum-notice window', () => {
    const now = new Date('2026-06-01T09:30:00Z') // 11:30 local CEST
    const slots = computeSlots(baseInput({ now, minimumNoticeMinutes: 120 }))
    // Earliest allowed = 11:30Z. First remaining slot is 12:00 local = 10:00Z...
    // 09:30Z + 120min = 11:30Z; first slot start >= 11:30Z is 14:00 local (12:00Z).
    expect(slots[0].startUtc.toISOString()).toBe(new Date('2026-06-01T12:00:00Z').toISOString())
  })

  it('removes slots overlapping a busy interval expanded by buffers', () => {
    // Busy 10:00–10:30 local (08:00–08:30Z) with 30-min buffers blocks 09:00..11:00.
    const busy = [{ startUtc: new Date(iso('2026-06-01T10:00')!), endUtc: new Date(iso('2026-06-01T10:30')!) }]
    const slots = computeSlots(baseInput({ bufferBeforeMinutes: 30, bufferAfterMinutes: 30, busy }))
    const local = slots.map((s) => DateTime.fromJSDate(s.startUtc).setZone(TZ).toFormat('HH:mm'))
    // 10:00 overlaps directly; 09:00's +30 after-buffer (ends 08:30Z) overlaps the
    // busy block. 11:00's -30 before-buffer ends exactly at 08:30Z — touching, not
    // overlapping — so it stays available.
    expect(local).not.toContain('10:00')
    expect(local).not.toContain('09:00')
    expect(local).toContain('11:00')
    expect(local).toContain('12:00')
  })

  it('honours the booking horizon (maximumDaysInFuture)', () => {
    const slots = computeSlots(
      baseInput({
        maximumDaysInFuture: 1,
        fromUtc: new Date('2026-06-01T00:00:00Z'),
        toUtc: new Date('2026-06-10T00:00:00Z'),
      }),
    )
    // now=2026-06-01T00:00Z, horizon +1 day = 2026-06-02T00:00Z. Only 1st's slots.
    const days = new Set(slots.map((s) => DateTime.fromJSDate(s.startUtc).setZone(TZ).toFormat('yyyy-MM-dd')))
    expect([...days]).toEqual(['2026-06-01'])
  })

  // ── DST ────────────────────────────────────────────────────────────────────
  it('maps the same local time to different UTC across the spring-forward boundary', () => {
    // Copenhagen DST starts 2026-03-29. 09:00 local: 28th = CET (+1) → 08:00Z;
    // 30th = CEST (+2) → 07:00Z.
    const slots = computeSlots(
      baseInput({
        durationMinutes: 60,
        slotIntervalMinutes: 60,
        weeklyRules: allWeek(540, 600), // 09:00–10:00
        now: new Date('2026-03-01T00:00:00Z'),
        fromUtc: new Date('2026-03-28T00:00:00Z'),
        toUtc: new Date('2026-03-31T00:00:00Z'),
      }),
    )
    const set = isoSet(slots)
    expect(set).toContain('2026-03-28T08:00:00Z') // before DST (+1)
    expect(set).toContain('2026-03-30T07:00:00Z') // after DST (+2)
  })

  it('never offers a slot at a non-existent spring-forward local time', () => {
    // The gap is 02:00→03:00 local on 2026-03-29. Offer 02:00–03:00 that day;
    // 30-min slots at 02:00 and 02:30 do not exist and must be dropped.
    const slots = computeSlots(
      baseInput({
        durationMinutes: 30,
        slotIntervalMinutes: 30,
        weeklyRules: [],
        dateOverrides: [{ date: '2026-03-29', isUnavailable: false, intervals: [{ startMinute: 120, endMinute: 180 }] }],
        now: new Date('2026-03-01T00:00:00Z'),
        fromUtc: new Date('2026-03-29T00:00:00Z'),
        toUtc: new Date('2026-03-30T00:00:00Z'),
      }),
    )
    expect(slots).toHaveLength(0)
  })

  it('applies date overrides: unavailable day yields no slots', () => {
    const slots = computeSlots(
      baseInput({
        dateOverrides: [{ date: '2026-06-01', isUnavailable: true, intervals: [] }],
      }),
    )
    expect(slots).toHaveLength(0)
  })

  it('date override intervals replace the weekly rule for that date', () => {
    const slots = computeSlots(
      baseInput({
        weeklyRules: allWeek(540, 1020), // 09–17 normally
        dateOverrides: [{ date: '2026-06-01', isUnavailable: false, intervals: [{ startMinute: 780, endMinute: 840 }] }], // 13:00–14:00
      }),
    )
    expect(slots).toHaveLength(1)
    expect(DateTime.fromJSDate(slots[0].startUtc).setZone(TZ).toFormat('HH:mm')).toBe('13:00')
  })
})
