import { describe, it, expect, vi, beforeEach, afterEach } from 'vitest'
import {
  parseOffset,
  applyOffset,
  computeRetryNextRunAt,
  computeNextRunAt,
  getCurrentPayload,
  getCurrentItemLabel,
  isScheduleComplete,
  getTotalSteps,
  isValidOffset
} from '../utils'
import type { TaskScheduleConfig } from '../types'

describe('parseOffset', () => {
  it('parses positive day offset', () => {
    expect(parseOffset('+3d')).toEqual({ value: 3, unit: 'd' })
  })

  it('parses negative day offset', () => {
    expect(parseOffset('-14d')).toEqual({ value: -14, unit: 'd' })
  })

  it('parses positive hour offset', () => {
    expect(parseOffset('+2h')).toEqual({ value: 2, unit: 'h' })
  })

  it('parses negative hour offset', () => {
    expect(parseOffset('-2h')).toEqual({ value: -2, unit: 'h' })
  })

  it('parses positive minute offset', () => {
    expect(parseOffset('+30m')).toEqual({ value: 30, unit: 'm' })
  })

  it('parses negative minute offset', () => {
    expect(parseOffset('-45m')).toEqual({ value: -45, unit: 'm' })
  })

  it('parses single digit offsets', () => {
    expect(parseOffset('+1d')).toEqual({ value: 1, unit: 'd' })
    expect(parseOffset('-1h')).toEqual({ value: -1, unit: 'h' })
    expect(parseOffset('+5m')).toEqual({ value: 5, unit: 'm' })
  })

  it('parses large offsets', () => {
    expect(parseOffset('+365d')).toEqual({ value: 365, unit: 'd' })
    expect(parseOffset('-1000h')).toEqual({ value: -1000, unit: 'h' })
    expect(parseOffset('+9999m')).toEqual({ value: 9999, unit: 'm' })
  })

  it('throws on invalid format - no sign', () => {
    expect(() => parseOffset('3d')).toThrow('Invalid offset format')
  })

  it('throws on invalid format - invalid unit', () => {
    expect(() => parseOffset('+3w')).toThrow('Invalid offset format')
  })

  it('throws on invalid format - invalid unit s', () => {
    expect(() => parseOffset('+30s')).toThrow('Invalid offset format')
  })

  it('throws on invalid format - non-numeric', () => {
    expect(() => parseOffset('+abcd')).toThrow('Invalid offset format')
  })

  it('throws on invalid format - spelled out', () => {
    expect(() => parseOffset('3 days')).toThrow('Invalid offset format')
  })

  it('throws on empty string', () => {
    expect(() => parseOffset('')).toThrow('Invalid offset format')
  })

  it('throws on whitespace', () => {
    expect(() => parseOffset(' +3d ')).toThrow('Invalid offset format')
  })

  it('throws on decimal values', () => {
    expect(() => parseOffset('+3.5d')).toThrow('Invalid offset format')
  })
})

describe('applyOffset', () => {
  const anchor = new Date('2026-01-15T10:00:00Z')

  it('applies positive day offset', () => {
    const result = applyOffset(anchor, '+3d')
    expect(result.toISOString()).toBe('2026-01-18T10:00:00.000Z')
  })

  it('applies negative day offset', () => {
    const result = applyOffset(anchor, '-7d')
    expect(result.toISOString()).toBe('2026-01-08T10:00:00.000Z')
  })

  it('applies positive hour offset', () => {
    const result = applyOffset(anchor, '+5h')
    expect(result.toISOString()).toBe('2026-01-15T15:00:00.000Z')
  })

  it('applies negative hour offset', () => {
    const result = applyOffset(anchor, '-2h')
    expect(result.toISOString()).toBe('2026-01-15T08:00:00.000Z')
  })

  it('applies positive minute offset', () => {
    const result = applyOffset(anchor, '+30m')
    expect(result.toISOString()).toBe('2026-01-15T10:30:00.000Z')
  })

  it('applies negative minute offset', () => {
    const result = applyOffset(anchor, '-45m')
    expect(result.toISOString()).toBe('2026-01-15T09:15:00.000Z')
  })

  it('does not mutate original date', () => {
    const original = new Date('2026-01-15T10:00:00Z')
    applyOffset(original, '+3d')
    expect(original.toISOString()).toBe('2026-01-15T10:00:00.000Z')
  })

  it('handles month boundary crossing (forward)', () => {
    const endOfMonth = new Date('2026-01-31T12:00:00Z')
    const result = applyOffset(endOfMonth, '+3d')
    expect(result.toISOString()).toBe('2026-02-03T12:00:00.000Z')
  })

  it('handles month boundary crossing (backward)', () => {
    const startOfMonth = new Date('2026-02-01T12:00:00Z')
    const result = applyOffset(startOfMonth, '-3d')
    expect(result.toISOString()).toBe('2026-01-29T12:00:00.000Z')
  })

  it('handles year boundary crossing', () => {
    const endOfYear = new Date('2025-12-30T12:00:00Z')
    const result = applyOffset(endOfYear, '+5d')
    expect(result.toISOString()).toBe('2026-01-04T12:00:00.000Z')
  })

  it('handles hour overflow into next day', () => {
    const lateNight = new Date('2026-01-15T22:00:00Z')
    const result = applyOffset(lateNight, '+5h')
    expect(result.toISOString()).toBe('2026-01-16T03:00:00.000Z')
  })

  it('handles minute overflow into next hour', () => {
    const nearHour = new Date('2026-01-15T10:45:00Z')
    const result = applyOffset(nearHour, '+30m')
    expect(result.toISOString()).toBe('2026-01-15T11:15:00.000Z')
  })

  it('handles large day offset', () => {
    const result = applyOffset(anchor, '+365d')
    expect(result.toISOString()).toBe('2027-01-15T10:00:00.000Z')
  })
})

describe('computeRetryNextRunAt', () => {
  it('computes 1 minute delay for retry 0', () => {
    const before = new Date()
    const result = computeRetryNextRunAt(0)
    const after = new Date()

    // (0 + 1)^2 = 1 minute
    const expectedMinDelay = 1 * 60 * 1000 // 1 minute in ms
    const diff = result.getTime() - before.getTime()

    expect(diff).toBeGreaterThanOrEqual(expectedMinDelay - 100) // Allow 100ms tolerance
    expect(diff).toBeLessThanOrEqual(expectedMinDelay + (after.getTime() - before.getTime()) + 100)
  })

  it('computes 4 minute delay for retry 1', () => {
    const before = new Date()
    const result = computeRetryNextRunAt(1)

    // (1 + 1)^2 = 4 minutes
    const expectedMinDelay = 4 * 60 * 1000 // 4 minutes in ms
    const diff = result.getTime() - before.getTime()

    expect(diff).toBeGreaterThanOrEqual(expectedMinDelay - 100)
    expect(diff).toBeLessThanOrEqual(expectedMinDelay + 1000) // Allow 1s tolerance
  })

  it('computes 9 minute delay for retry 2', () => {
    const before = new Date()
    const result = computeRetryNextRunAt(2)

    // (2 + 1)^2 = 9 minutes
    const expectedMinDelay = 9 * 60 * 1000 // 9 minutes in ms
    const diff = result.getTime() - before.getTime()

    expect(diff).toBeGreaterThanOrEqual(expectedMinDelay - 100)
    expect(diff).toBeLessThanOrEqual(expectedMinDelay + 1000)
  })

  it('computes 16 minute delay for retry 3', () => {
    const before = new Date()
    const result = computeRetryNextRunAt(3)

    // (3 + 1)^2 = 16 minutes
    const expectedMinDelay = 16 * 60 * 1000 // 16 minutes in ms
    const diff = result.getTime() - before.getTime()

    expect(diff).toBeGreaterThanOrEqual(expectedMinDelay - 100)
    expect(diff).toBeLessThanOrEqual(expectedMinDelay + 1000)
  })

  it('computes 25 minute delay for retry 4', () => {
    const before = new Date()
    const result = computeRetryNextRunAt(4)

    // (4 + 1)^2 = 25 minutes
    const expectedMinDelay = 25 * 60 * 1000 // 25 minutes in ms
    const diff = result.getTime() - before.getTime()

    expect(diff).toBeGreaterThanOrEqual(expectedMinDelay - 100)
    expect(diff).toBeLessThanOrEqual(expectedMinDelay + 1000)
  })

  it('returns a Date object', () => {
    const result = computeRetryNextRunAt(0)
    expect(result).toBeInstanceOf(Date)
  })

  it('returns a future date', () => {
    const now = new Date()
    const result = computeRetryNextRunAt(0)
    expect(result.getTime()).toBeGreaterThan(now.getTime())
  })
})

describe('computeNextRunAt', () => {
  describe('relative schedule', () => {
    const config: TaskScheduleConfig = {
      type: 'relative',
      anchorAt: '2026-01-15T10:00:00Z',
      items: [
        { offset: '+1d', payload: {} },
        { offset: '+3d', payload: {} },
        { offset: '+7d', payload: {} }
      ]
    }

    it('computes first step correctly', () => {
      const result = computeNextRunAt(config, 0)
      expect(result?.toISOString()).toBe('2026-01-16T10:00:00.000Z')
    })

    it('computes second step correctly', () => {
      const result = computeNextRunAt(config, 1)
      expect(result?.toISOString()).toBe('2026-01-18T10:00:00.000Z')
    })

    it('computes third step correctly', () => {
      const result = computeNextRunAt(config, 2)
      expect(result?.toISOString()).toBe('2026-01-22T10:00:00.000Z')
    })

    it('returns null when sequence complete', () => {
      const result = computeNextRunAt(config, 3)
      expect(result).toBeNull()
    })

    it('returns null for step beyond sequence', () => {
      const result = computeNextRunAt(config, 10)
      expect(result).toBeNull()
    })

    it('handles negative offsets', () => {
      const countdownConfig: TaskScheduleConfig = {
        type: 'relative',
        anchorAt: '2026-06-01T00:00:00Z',
        items: [
          { offset: '-7d', payload: {} },
          { offset: '-1d', payload: {} }
        ]
      }
      expect(computeNextRunAt(countdownConfig, 0)?.toISOString()).toBe('2026-05-25T00:00:00.000Z')
      expect(computeNextRunAt(countdownConfig, 1)?.toISOString()).toBe('2026-05-31T00:00:00.000Z')
    })
  })

  describe('absolute schedule', () => {
    const config: TaskScheduleConfig = {
      type: 'absolute',
      items: [
        { runAt: '2026-01-15T09:00:00Z', payload: {} },
        { runAt: '2026-02-01T09:00:00Z', payload: {} },
        { runAt: '2026-03-15T14:30:00Z', payload: {} }
      ]
    }

    it('returns first item datetime', () => {
      const result = computeNextRunAt(config, 0)
      expect(result?.toISOString()).toBe('2026-01-15T09:00:00.000Z')
    })

    it('returns second item datetime', () => {
      const result = computeNextRunAt(config, 1)
      expect(result?.toISOString()).toBe('2026-02-01T09:00:00.000Z')
    })

    it('returns third item datetime', () => {
      const result = computeNextRunAt(config, 2)
      expect(result?.toISOString()).toBe('2026-03-15T14:30:00.000Z')
    })

    it('returns null when sequence complete', () => {
      const result = computeNextRunAt(config, 3)
      expect(result).toBeNull()
    })

    it('returns null for step beyond sequence', () => {
      const result = computeNextRunAt(config, 100)
      expect(result).toBeNull()
    })
  })

  describe('recurring schedule', () => {
    it('computes next run for daily interval-based schedule', () => {
      const config: TaskScheduleConfig = {
        type: 'recurring',
        interval: 'daily',
        time: '09:00',
        timezone: 'UTC',
        payload: {}
      }
      // Test that it returns a date (exact value depends on current time)
      const result = computeNextRunAt(config, 0)
      expect(result).toBeInstanceOf(Date)
    })

    it('computes next run for weekly interval-based schedule', () => {
      const config: TaskScheduleConfig = {
        type: 'recurring',
        interval: 'weekly',
        time: '14:30',
        timezone: 'UTC',
        payload: {}
      }
      const result = computeNextRunAt(config, 0)
      expect(result).toBeInstanceOf(Date)
    })

    it('computes next run for monthly interval-based schedule', () => {
      const config: TaskScheduleConfig = {
        type: 'recurring',
        interval: 'monthly',
        time: '08:00',
        timezone: 'UTC',
        payload: {}
      }
      const result = computeNextRunAt(config, 0)
      expect(result).toBeInstanceOf(Date)
    })

    it('returns null if endAt has passed', () => {
      const config: TaskScheduleConfig = {
        type: 'recurring',
        interval: 'daily',
        time: '09:00',
        timezone: 'UTC',
        payload: {},
        endAt: '2020-01-01T00:00:00Z' // Past date
      }
      const result = computeNextRunAt(config, 0, new Date('2025-01-01'))
      expect(result).toBeNull()
    })

    it('returns next run when before endAt', () => {
      const config: TaskScheduleConfig = {
        type: 'recurring',
        interval: 'daily',
        time: '09:00',
        timezone: 'UTC',
        payload: {},
        endAt: '2030-01-01T00:00:00Z' // Future date
      }
      const result = computeNextRunAt(config, 0, new Date('2025-01-01T08:00:00Z'))
      expect(result).toBeInstanceOf(Date)
    })

    it('computes next run for cron expression', () => {
      const config: TaskScheduleConfig = {
        type: 'recurring',
        cron: '0 9 * * 1',
        timezone: 'UTC',
        payload: {}
      }
      // Wednesday Jan 15, 2025 at 10:00 UTC → next Monday at 09:00 UTC = Jan 20
      const fromTime = new Date('2025-01-15T10:00:00Z')
      const result = computeNextRunAt(config, 0, fromTime)
      expect(result).toBeInstanceOf(Date)
      expect(result!.toISOString()).toBe('2025-01-20T09:00:00.000Z')
    })

    it('computes next run for multi-time cron (3x daily)', () => {
      const config: TaskScheduleConfig = {
        type: 'recurring',
        cron: '0 4,16,22 * * *',
        timezone: 'UTC',
        payload: {}
      }
      // At 05:00 UTC → next is 16:00 same day
      const result1 = computeNextRunAt(config, 0, new Date('2025-03-25T05:00:00Z'))
      expect(result1!.toISOString()).toBe('2025-03-25T16:00:00.000Z')

      // At 17:00 UTC → next is 22:00 same day
      const result2 = computeNextRunAt(config, 0, new Date('2025-03-25T17:00:00Z'))
      expect(result2!.toISOString()).toBe('2025-03-25T22:00:00.000Z')

      // At 23:00 UTC → next is 04:00 next day
      const result3 = computeNextRunAt(config, 0, new Date('2025-03-25T23:00:00Z'))
      expect(result3!.toISOString()).toBe('2025-03-26T04:00:00.000Z')
    })

    it('respects timezone for cron expression', () => {
      const config: TaskScheduleConfig = {
        type: 'recurring',
        cron: '0 9 * * *',
        timezone: 'America/New_York',
        payload: {}
      }
      // 12:00 UTC on Jan 15 = 07:00 EST → next 09:00 EST = 14:00 UTC
      const fromTime = new Date('2025-01-15T12:00:00Z')
      const result = computeNextRunAt(config, 0, fromTime)
      expect(result).toBeInstanceOf(Date)
      expect(result!.toISOString()).toBe('2025-01-15T14:00:00.000Z')
    })

    it('returns null for cron when endAt has passed', () => {
      const config: TaskScheduleConfig = {
        type: 'recurring',
        cron: '0 9 * * *',
        timezone: 'UTC',
        payload: {},
        endAt: '2025-01-10T00:00:00Z'
      }
      const result = computeNextRunAt(config, 0, new Date('2025-01-15T08:00:00Z'))
      expect(result).toBeNull()
    })

    it('returns null for cron when next run exceeds endAt', () => {
      const config: TaskScheduleConfig = {
        type: 'recurring',
        cron: '0 9 * * *',
        timezone: 'UTC',
        payload: {},
        endAt: '2025-01-16T08:00:00Z'
      }
      // At 10:00 Jan 15 → next would be 09:00 Jan 16, but endAt is 08:00 Jan 16
      const result = computeNextRunAt(config, 0, new Date('2025-01-15T10:00:00Z'))
      expect(result).toBeNull()
    })

    describe('timezone handling', () => {
      it('converts time correctly for America/New_York timezone', () => {
        // Schedule at 09:00 New York time
        const config: TaskScheduleConfig = {
          type: 'recurring',
          interval: 'daily',
          time: '09:00',
          timezone: 'America/New_York',
          payload: {}
        }
        // Test at 12:00 UTC on Jan 15, 2026 (07:00 EST)
        // 09:00 EST = 14:00 UTC (EST is UTC-5 in winter)
        const fromTime = new Date('2026-01-15T12:00:00Z')
        const result = computeNextRunAt(config, 0, fromTime)

        expect(result).toBeInstanceOf(Date)
        // The result should be 14:00 UTC (09:00 EST)
        expect(result?.getUTCHours()).toBe(14)
        expect(result?.getUTCMinutes()).toBe(0)
      })

      it('converts time correctly for Europe/London timezone', () => {
        // Schedule at 10:00 London time
        const config: TaskScheduleConfig = {
          type: 'recurring',
          interval: 'daily',
          time: '10:00',
          timezone: 'Europe/London',
          payload: {}
        }
        // Test at 08:00 UTC on Jan 15, 2026 (08:00 GMT in winter, no DST)
        // 10:00 GMT = 10:00 UTC
        const fromTime = new Date('2026-01-15T08:00:00Z')
        const result = computeNextRunAt(config, 0, fromTime)

        expect(result).toBeInstanceOf(Date)
        // The result should be 10:00 UTC (10:00 GMT)
        expect(result?.getUTCHours()).toBe(10)
        expect(result?.getUTCMinutes()).toBe(0)
      })

      it('converts time correctly for Asia/Tokyo timezone', () => {
        // Schedule at 09:00 Tokyo time
        const config: TaskScheduleConfig = {
          type: 'recurring',
          interval: 'daily',
          time: '09:00',
          timezone: 'Asia/Tokyo',
          payload: {}
        }
        // Test at 22:00 UTC on Jan 15, 2026 (07:00 JST next day)
        // 09:00 JST = 00:00 UTC (JST is UTC+9)
        const fromTime = new Date('2026-01-15T22:00:00Z')
        const result = computeNextRunAt(config, 0, fromTime)

        expect(result).toBeInstanceOf(Date)
        // The result should be 00:00 UTC on Jan 16 (09:00 JST)
        expect(result?.getUTCHours()).toBe(0)
        expect(result?.getUTCMinutes()).toBe(0)
        expect(result?.getUTCDate()).toBe(16)
      })

      it('defaults to UTC when timezone not provided', () => {
        const config: TaskScheduleConfig = {
          type: 'recurring',
          interval: 'daily',
          time: '09:00',
          timezone: 'UTC',
          payload: {}
        }
        // Test at 08:00 UTC
        const fromTime = new Date('2026-01-15T08:00:00Z')
        const result = computeNextRunAt(config, 0, fromTime)

        expect(result).toBeInstanceOf(Date)
        // Should return 09:00 UTC same day
        expect(result?.getUTCHours()).toBe(9)
        expect(result?.getUTCMinutes()).toBe(0)
        expect(result?.getUTCDate()).toBe(15)
      })

      it('moves to next day if time has passed in timezone', () => {
        // Schedule at 09:00 New York time
        const config: TaskScheduleConfig = {
          type: 'recurring',
          interval: 'daily',
          time: '09:00',
          timezone: 'America/New_York',
          payload: {}
        }
        // Test at 16:00 UTC on Jan 15, 2026 (11:00 EST - after scheduled time)
        // Should schedule for 09:00 EST next day = 14:00 UTC Jan 16
        const fromTime = new Date('2026-01-15T16:00:00Z')
        const result = computeNextRunAt(config, 0, fromTime)

        expect(result).toBeInstanceOf(Date)
        expect(result?.getUTCHours()).toBe(14)
        expect(result?.getUTCDate()).toBe(16)
      })

      describe('with fake timers', () => {
        beforeEach(() => {
          vi.useFakeTimers()
        })

        afterEach(() => {
          vi.useRealTimers()
        })

        it('computes next run in America/New_York timezone with deterministic time', () => {
          // Set system time to 2026-01-15T13:00:00Z (8 AM EST)
          vi.setSystemTime(new Date('2026-01-15T13:00:00Z'))

          const config: TaskScheduleConfig = {
            type: 'recurring',
            interval: 'daily',
            time: '09:00', // 9 AM in New York
            timezone: 'America/New_York',
            payload: {}
          }

          // 9 AM EST = 14:00 UTC (during standard time, EST is UTC-5)
          const result = computeNextRunAt(config, 0)
          expect(result?.toISOString()).toBe('2026-01-15T14:00:00.000Z')
        })

        it('computes next run in Europe/London timezone with deterministic time', () => {
          // Set system time to 2026-01-15T08:30:00Z (8:30 AM GMT)
          vi.setSystemTime(new Date('2026-01-15T08:30:00Z'))

          const config: TaskScheduleConfig = {
            type: 'recurring',
            interval: 'daily',
            time: '10:00', // 10 AM in London
            timezone: 'Europe/London',
            payload: {}
          }

          // 10 AM GMT = 10:00 UTC (during winter, no DST offset)
          const result = computeNextRunAt(config, 0)
          expect(result?.toISOString()).toBe('2026-01-15T10:00:00.000Z')
        })

        it('handles DST transition for America/New_York (spring forward)', () => {
          // March 8, 2026 is when DST starts in the US (clocks spring forward at 2 AM)
          // Set time to just before DST transition
          vi.setSystemTime(new Date('2026-03-08T06:00:00Z')) // 1 AM EST

          const config: TaskScheduleConfig = {
            type: 'recurring',
            interval: 'daily',
            time: '09:00', // 9 AM in New York
            timezone: 'America/New_York',
            payload: {}
          }

          // After DST, 9 AM EDT = 13:00 UTC (EDT is UTC-4)
          const result = computeNextRunAt(config, 0)
          expect(result?.toISOString()).toBe('2026-03-08T13:00:00.000Z')
        })

        it('handles DST transition for America/New_York (fall back)', () => {
          // November 1, 2026 is when DST ends in the US (clocks fall back at 2 AM)
          // Set time to just after DST ends
          vi.setSystemTime(new Date('2026-11-01T07:00:00Z')) // 2 AM EST (after fall back)

          const config: TaskScheduleConfig = {
            type: 'recurring',
            interval: 'daily',
            time: '09:00', // 9 AM in New York
            timezone: 'America/New_York',
            payload: {}
          }

          // After DST ends, 9 AM EST = 14:00 UTC (EST is UTC-5)
          const result = computeNextRunAt(config, 0)
          expect(result?.toISOString()).toBe('2026-11-01T14:00:00.000Z')
        })

        it('defaults to UTC behavior when timezone is set to UTC', () => {
          // Set system time to 2026-01-15T07:30:00Z
          vi.setSystemTime(new Date('2026-01-15T07:30:00Z'))

          const config: TaskScheduleConfig = {
            type: 'recurring',
            interval: 'daily',
            time: '09:00',
            timezone: 'UTC',
            payload: {}
          }

          // 9 AM UTC should be exactly 09:00:00Z
          const result = computeNextRunAt(config, 0)
          expect(result?.toISOString()).toBe('2026-01-15T09:00:00.000Z')
        })

        it('stores result as UTC regardless of input timezone', () => {
          vi.setSystemTime(new Date('2026-01-15T00:00:00Z'))

          const config: TaskScheduleConfig = {
            type: 'recurring',
            interval: 'daily',
            time: '15:00', // 3 PM in Tokyo
            timezone: 'Asia/Tokyo',
            payload: {}
          }

          // 15:00 JST = 06:00 UTC (JST is UTC+9)
          const result = computeNextRunAt(config, 0)

          // Verify the result is stored as a proper UTC ISO string
          expect(result?.toISOString()).toBe('2026-01-15T06:00:00.000Z')
          // Verify it's a Date object that can be used directly
          expect(result).toBeInstanceOf(Date)
          expect(result?.getUTCHours()).toBe(6)
          expect(result?.getUTCMinutes()).toBe(0)
        })
      })
    })
  })
})

describe('getCurrentPayload', () => {
  it('returns payload for recurring schedule', () => {
    const config: TaskScheduleConfig = {
      type: 'recurring',
      interval: 'daily',
      time: '09:00',
      timezone: 'UTC',
      payload: { key: 'value', reportType: 'daily' }
    }
    expect(getCurrentPayload(config, 0)).toEqual({ key: 'value', reportType: 'daily' })
    // Recurring schedules always return the same payload regardless of step
    expect(getCurrentPayload(config, 100)).toEqual({ key: 'value', reportType: 'daily' })
  })

  it('returns correct item payload for relative schedule', () => {
    const config: TaskScheduleConfig = {
      type: 'relative',
      anchorAt: '2026-01-15T10:00:00Z',
      items: [
        { offset: '+1d', payload: { step: 1, tone: 'friendly' } },
        { offset: '+3d', payload: { step: 2, tone: 'casual' } },
        { offset: '+7d', payload: { step: 3, tone: 'urgent' } }
      ]
    }
    expect(getCurrentPayload(config, 0)).toEqual({ step: 1, tone: 'friendly' })
    expect(getCurrentPayload(config, 1)).toEqual({ step: 2, tone: 'casual' })
    expect(getCurrentPayload(config, 2)).toEqual({ step: 3, tone: 'urgent' })
  })

  it('returns correct item payload for absolute schedule', () => {
    const config: TaskScheduleConfig = {
      type: 'absolute',
      items: [
        { runAt: '2026-01-15T09:00:00Z', payload: { phase: 'alpha' } },
        { runAt: '2026-02-01T09:00:00Z', payload: { phase: 'beta' } }
      ]
    }
    expect(getCurrentPayload(config, 0)).toEqual({ phase: 'alpha' })
    expect(getCurrentPayload(config, 1)).toEqual({ phase: 'beta' })
  })

  it('returns null for completed relative sequence', () => {
    const config: TaskScheduleConfig = {
      type: 'relative',
      anchorAt: '2026-01-15T10:00:00Z',
      items: [{ offset: '+1d', payload: { test: true } }]
    }
    expect(getCurrentPayload(config, 1)).toBeNull()
    expect(getCurrentPayload(config, 10)).toBeNull()
  })

  it('returns null for completed absolute sequence', () => {
    const config: TaskScheduleConfig = {
      type: 'absolute',
      items: [{ runAt: '2026-01-15T09:00:00Z', payload: { test: true } }]
    }
    expect(getCurrentPayload(config, 1)).toBeNull()
    expect(getCurrentPayload(config, 10)).toBeNull()
  })

  it('handles empty payload object', () => {
    const config: TaskScheduleConfig = {
      type: 'relative',
      anchorAt: '2026-01-15T10:00:00Z',
      items: [{ offset: '+1d', payload: {} }]
    }
    expect(getCurrentPayload(config, 0)).toEqual({})
  })
})

describe('getCurrentItemLabel', () => {
  it('returns undefined for recurring schedule', () => {
    const config: TaskScheduleConfig = {
      type: 'recurring',
      interval: 'daily',
      time: '09:00',
      timezone: 'UTC',
      payload: {}
    }
    expect(getCurrentItemLabel(config, 0)).toBeUndefined()
    expect(getCurrentItemLabel(config, 100)).toBeUndefined()
  })

  it('returns label for relative schedule item', () => {
    const config: TaskScheduleConfig = {
      type: 'relative',
      anchorAt: '2026-01-15T10:00:00Z',
      items: [
        { offset: '+1d', payload: {}, label: 'Thank you email' },
        { offset: '+3d', payload: {}, label: 'Follow up' },
        { offset: '+7d', payload: {} } // No label
      ]
    }
    expect(getCurrentItemLabel(config, 0)).toBe('Thank you email')
    expect(getCurrentItemLabel(config, 1)).toBe('Follow up')
    expect(getCurrentItemLabel(config, 2)).toBeUndefined()
  })

  it('returns label for absolute schedule item', () => {
    const config: TaskScheduleConfig = {
      type: 'absolute',
      items: [
        { runAt: '2026-01-15T09:00:00Z', payload: {}, label: 'Phase 1 Launch' },
        { runAt: '2026-02-01T09:00:00Z', payload: {} } // No label
      ]
    }
    expect(getCurrentItemLabel(config, 0)).toBe('Phase 1 Launch')
    expect(getCurrentItemLabel(config, 1)).toBeUndefined()
  })

  it('returns undefined for out of range step in relative schedule', () => {
    const config: TaskScheduleConfig = {
      type: 'relative',
      anchorAt: '2026-01-15T10:00:00Z',
      items: [{ offset: '+1d', payload: {}, label: 'Test' }]
    }
    expect(getCurrentItemLabel(config, 1)).toBeUndefined()
    expect(getCurrentItemLabel(config, 10)).toBeUndefined()
  })

  it('returns undefined for out of range step in absolute schedule', () => {
    const config: TaskScheduleConfig = {
      type: 'absolute',
      items: [{ runAt: '2026-01-15T09:00:00Z', payload: {}, label: 'Test' }]
    }
    expect(getCurrentItemLabel(config, 1)).toBeUndefined()
  })
})

describe('isScheduleComplete', () => {
  it('returns false for indefinite recurring schedule', () => {
    const config: TaskScheduleConfig = {
      type: 'recurring',
      interval: 'daily',
      time: '09:00',
      timezone: 'UTC',
      payload: {}
    }
    expect(isScheduleComplete(config, 0)).toBe(false)
    expect(isScheduleComplete(config, 100)).toBe(false)
    expect(isScheduleComplete(config, 1000)).toBe(false)
  })

  it('returns false for recurring with null endAt', () => {
    const config: TaskScheduleConfig = {
      type: 'recurring',
      interval: 'daily',
      time: '09:00',
      timezone: 'UTC',
      payload: {},
      endAt: null
    }
    expect(isScheduleComplete(config, 0)).toBe(false)
  })

  it('returns true when relative sequence is complete', () => {
    const config: TaskScheduleConfig = {
      type: 'relative',
      anchorAt: '2026-01-15T10:00:00Z',
      items: [
        { offset: '+1d', payload: {} },
        { offset: '+3d', payload: {} }
      ]
    }
    expect(isScheduleComplete(config, 0)).toBe(false)
    expect(isScheduleComplete(config, 1)).toBe(false)
    expect(isScheduleComplete(config, 2)).toBe(true)
    expect(isScheduleComplete(config, 10)).toBe(true)
  })

  it('returns true when absolute sequence is complete', () => {
    const config: TaskScheduleConfig = {
      type: 'absolute',
      items: [
        { runAt: '2026-01-15T09:00:00Z', payload: {} },
        { runAt: '2026-02-01T09:00:00Z', payload: {} }
      ]
    }
    expect(isScheduleComplete(config, 0)).toBe(false)
    expect(isScheduleComplete(config, 1)).toBe(false)
    expect(isScheduleComplete(config, 2)).toBe(true)
  })

  it('returns true for single-item relative sequence at step 1', () => {
    const config: TaskScheduleConfig = {
      type: 'relative',
      anchorAt: '2026-01-15T10:00:00Z',
      items: [{ offset: '+1d', payload: {} }]
    }
    expect(isScheduleComplete(config, 0)).toBe(false)
    expect(isScheduleComplete(config, 1)).toBe(true)
  })

  it('returns true for single-item absolute sequence at step 1', () => {
    const config: TaskScheduleConfig = {
      type: 'absolute',
      items: [{ runAt: '2026-01-15T09:00:00Z', payload: {} }]
    }
    expect(isScheduleComplete(config, 1)).toBe(true)
  })

  // Note: Testing recurring with endAt in the past is time-dependent
  // In production, this would compare endAt against current time
  it('handles recurring schedule with future endAt', () => {
    const config: TaskScheduleConfig = {
      type: 'recurring',
      interval: 'daily',
      time: '09:00',
      timezone: 'UTC',
      payload: {},
      endAt: '2099-12-31T23:59:59Z' // Far future
    }
    expect(isScheduleComplete(config, 0)).toBe(false)
  })
})

describe('getTotalSteps', () => {
  it('returns null for recurring schedule (indefinite)', () => {
    const config: TaskScheduleConfig = {
      type: 'recurring',
      interval: 'daily',
      time: '09:00',
      timezone: 'UTC',
      payload: {}
    }
    expect(getTotalSteps(config)).toBeNull()
  })

  it('returns null for recurring schedule with endAt', () => {
    const config: TaskScheduleConfig = {
      type: 'recurring',
      interval: 'daily',
      time: '09:00',
      timezone: 'UTC',
      payload: {},
      endAt: '2026-12-31T23:59:59Z'
    }
    // Even with endAt, we don't compute total steps for recurring
    expect(getTotalSteps(config)).toBeNull()
  })

  it('returns correct count for relative schedule', () => {
    const config: TaskScheduleConfig = {
      type: 'relative',
      anchorAt: '2026-01-15T10:00:00Z',
      items: [
        { offset: '+1d', payload: {} },
        { offset: '+3d', payload: {} },
        { offset: '+7d', payload: {} }
      ]
    }
    expect(getTotalSteps(config)).toBe(3)
  })

  it('returns correct count for absolute schedule', () => {
    const config: TaskScheduleConfig = {
      type: 'absolute',
      items: [
        { runAt: '2026-01-15T09:00:00Z', payload: {} },
        { runAt: '2026-02-01T09:00:00Z', payload: {} }
      ]
    }
    expect(getTotalSteps(config)).toBe(2)
  })

  it('returns 1 for single-item schedule', () => {
    const relativeConfig: TaskScheduleConfig = {
      type: 'relative',
      anchorAt: '2026-01-15T10:00:00Z',
      items: [{ offset: '+1d', payload: {} }]
    }
    const absoluteConfig: TaskScheduleConfig = {
      type: 'absolute',
      items: [{ runAt: '2026-01-15T09:00:00Z', payload: {} }]
    }
    expect(getTotalSteps(relativeConfig)).toBe(1)
    expect(getTotalSteps(absoluteConfig)).toBe(1)
  })
})

describe('isValidOffset', () => {
  it('returns true for valid day offset', () => {
    expect(isValidOffset('+3d')).toBe(true)
    expect(isValidOffset('-14d')).toBe(true)
    expect(isValidOffset('+365d')).toBe(true)
  })

  it('returns true for valid hour offset', () => {
    expect(isValidOffset('+2h')).toBe(true)
    expect(isValidOffset('-24h')).toBe(true)
  })

  it('returns true for valid minute offset', () => {
    expect(isValidOffset('+30m')).toBe(true)
    expect(isValidOffset('-45m')).toBe(true)
  })

  it('returns false for missing sign', () => {
    expect(isValidOffset('3d')).toBe(false)
    expect(isValidOffset('2h')).toBe(false)
  })

  it('returns false for invalid unit', () => {
    expect(isValidOffset('+3w')).toBe(false)
    expect(isValidOffset('+30s')).toBe(false)
    expect(isValidOffset('+1y')).toBe(false)
  })

  it('returns false for non-numeric value', () => {
    expect(isValidOffset('+abcd')).toBe(false)
  })

  it('returns false for empty string', () => {
    expect(isValidOffset('')).toBe(false)
  })

  it('returns false for spelled out offset', () => {
    expect(isValidOffset('3 days')).toBe(false)
    expect(isValidOffset('+3 days')).toBe(false)
  })

  it('returns false for decimal values', () => {
    expect(isValidOffset('+3.5d')).toBe(false)
  })

  it('returns false for whitespace', () => {
    expect(isValidOffset(' +3d')).toBe(false)
    expect(isValidOffset('+3d ')).toBe(false)
  })
})
