import { describe, it, expect } from 'vitest'
import {
  combineDateAndTime,
  generateOccurrenceDates,
  type ScheduleEntry,
} from './scheduleOccurrences'

// All dates in this file use the local-time Date constructor so they
// are independent of the machine's timezone. Months are 0-indexed:
//   new Date(2026, 3, 13) === 13 April 2026 local time

const d = (year: number, month1to12: number, day: number): Date =>
  new Date(year, month1to12 - 1, day)

// Reference calendar used throughout these tests:
// April 2026 — Wed 1, Sun 5/12/19/26, Mon 6/13/20/27 (only 4 Mondays)
// March 2026 — Sun 1, Mon 2/9/16/23/30 (five Mondays, last = 30)

describe('generateOccurrenceDates – weekly', () => {
  it('yields the next N sundays in the window', () => {
    const entry: ScheduleEntry = { frequency: 'weekly', day: 'sunday' }
    const result = generateOccurrenceDates(entry, d(2026, 4, 6), d(2026, 4, 27))
    expect(result.map((x) => x.getDate())).toEqual([12, 19, 26])
  })

  it('includes the start day itself if it matches the target weekday', () => {
    const entry: ScheduleEntry = { frequency: 'weekly', day: 'sunday' }
    // 2026-04-05 is a Sunday
    const result = generateOccurrenceDates(entry, d(2026, 4, 5), d(2026, 4, 19))
    expect(result.map((x) => x.getDate())).toEqual([5, 12, 19])
  })

  it('returns an empty array when the window is shorter than a week and misses the target', () => {
    const entry: ScheduleEntry = { frequency: 'weekly', day: 'sunday' }
    // Mon -> Sat, no Sunday in range
    const result = generateOccurrenceDates(entry, d(2026, 4, 6), d(2026, 4, 11))
    expect(result).toEqual([])
  })

  it('works for weekdays other than sunday', () => {
    const entry: ScheduleEntry = { frequency: 'weekly', day: 'wednesday' }
    // Apr 1 is a Wednesday
    const result = generateOccurrenceDates(entry, d(2026, 4, 1), d(2026, 4, 30))
    expect(result.map((x) => x.getDate())).toEqual([1, 8, 15, 22, 29])
  })
})

describe('generateOccurrenceDates – biweekly', () => {
  it('yields dates aligned with the anchor, every two weeks', () => {
    const entry: ScheduleEntry = {
      frequency: 'biweekly',
      day: 'sunday',
      biweeklyAnchor: d(2026, 4, 5), // a Sunday
    }
    const result = generateOccurrenceDates(entry, d(2026, 4, 6), d(2026, 5, 31))
    // from anchor: 4/5, 4/19, 5/3, 5/17, 5/31 — window starts after 4/5
    expect(result.map((x) => `${x.getMonth() + 1}-${x.getDate()}`)).toEqual([
      '4-19',
      '5-3',
      '5-17',
      '5-31',
    ])
  })

  it('skips the off-parity week when the naive first-occurrence lands on it', () => {
    const entry: ScheduleEntry = {
      frequency: 'biweekly',
      day: 'sunday',
      biweeklyAnchor: d(2026, 4, 5),
    }
    // Start on 4/12 — that's a Sunday but off-parity (7 days from anchor).
    // Next valid occurrence should be 4/19, not 4/12.
    const result = generateOccurrenceDates(entry, d(2026, 4, 12), d(2026, 4, 26))
    expect(result.map((x) => x.getDate())).toEqual([19])
  })

  it('handles anchor in the future by rolling backward in parity', () => {
    const entry: ScheduleEntry = {
      frequency: 'biweekly',
      day: 'sunday',
      biweeklyAnchor: d(2026, 6, 7), // future Sunday
    }
    const result = generateOccurrenceDates(entry, d(2026, 4, 6), d(2026, 5, 31))
    // anchor parity backward: 6/7 - 14 = 5/24, - 14 = 5/10, -14 = 4/26, -14 = 4/12
    expect(result.map((x) => `${x.getMonth() + 1}-${x.getDate()}`)).toEqual([
      '4-12',
      '4-26',
      '5-10',
      '5-24',
    ])
  })

  it('returns empty when biweeklyAnchor is missing', () => {
    const entry: ScheduleEntry = { frequency: 'biweekly', day: 'sunday' }
    const result = generateOccurrenceDates(entry, d(2026, 4, 1), d(2026, 4, 30))
    expect(result).toEqual([])
  })
})

describe('generateOccurrenceDates – monthlyByWeekday', () => {
  it('yields the 3rd sunday each month', () => {
    const entry: ScheduleEntry = {
      frequency: 'monthlyByWeekday',
      day: 'sunday',
      weekOfMonth: 'third',
    }
    const result = generateOccurrenceDates(entry, d(2026, 4, 1), d(2026, 6, 30))
    // Apr 19, May 17, Jun 21 — let me verify May/June:
    // May 2026: Sun 3, 10, 17, 24, 31 → third = 17
    // Jun 2026: Sun 7, 14, 21, 28 → third = 21
    expect(result.map((x) => `${x.getMonth() + 1}-${x.getDate()}`)).toEqual([
      '4-19',
      '5-17',
      '6-21',
    ])
  })

  it('yields the last sunday each month', () => {
    const entry: ScheduleEntry = {
      frequency: 'monthlyByWeekday',
      day: 'sunday',
      weekOfMonth: 'last',
    }
    const result = generateOccurrenceDates(entry, d(2026, 4, 1), d(2026, 6, 30))
    // Apr 26, May 31, Jun 28
    expect(result.map((x) => `${x.getMonth() + 1}-${x.getDate()}`)).toEqual([
      '4-26',
      '5-31',
      '6-28',
    ])
  })

  it('treats "fourth" as the 4th occurrence, not the last', () => {
    // March 2026 has 5 Mondays (2, 9, 16, 23, 30).
    // "fourth" should be 23, NOT 30.
    const entry: ScheduleEntry = {
      frequency: 'monthlyByWeekday',
      day: 'monday',
      weekOfMonth: 'fourth',
    }
    const result = generateOccurrenceDates(entry, d(2026, 3, 1), d(2026, 3, 31))
    expect(result.map((x) => x.getDate())).toEqual([23])
  })

  it('yields the 5th thursday only in months that have one', () => {
    const entry: ScheduleEntry = {
      frequency: 'monthlyByWeekday',
      day: 'thursday',
      weekOfMonth: 'fifth',
    }
    // 2026: Jul has 5 Thursdays (2,9,16,23,30), Aug 4 (6,13,20,27), Sep 4, Oct 5 (1,8,15,22,29), Nov 4, Dec 5 (3,10,17,24,31)
    const result = generateOccurrenceDates(entry, d(2026, 7, 1), d(2026, 12, 31))
    expect(result.map((x) => `${x.getMonth() + 1}-${x.getDate()}`)).toEqual([
      '7-30',
      '10-29',
      '12-31',
    ])
  })

  it('"fifth" and "last" differ in months with only 4 occurrences', () => {
    // Aug 2026 Thursdays: 6, 13, 20, 27 — last = 27, fifth = none
    const last = generateOccurrenceDates(
      { frequency: 'monthlyByWeekday', day: 'thursday', weekOfMonth: 'last' },
      d(2026, 8, 1),
      d(2026, 8, 31),
    )
    const fifth = generateOccurrenceDates(
      { frequency: 'monthlyByWeekday', day: 'thursday', weekOfMonth: 'fifth' },
      d(2026, 8, 1),
      d(2026, 8, 31),
    )
    expect(last.map((x) => x.getDate())).toEqual([27])
    expect(fifth).toEqual([])
  })

  it('works when the start-of-window is after that month\'s occurrence', () => {
    // 3rd Sunday of April 2026 is 4/19. Start the window on 4/20.
    // April should be skipped; May should still appear.
    const entry: ScheduleEntry = {
      frequency: 'monthlyByWeekday',
      day: 'sunday',
      weekOfMonth: 'third',
    }
    const result = generateOccurrenceDates(entry, d(2026, 4, 20), d(2026, 5, 31))
    expect(result.map((x) => `${x.getMonth() + 1}-${x.getDate()}`)).toEqual([
      '5-17',
    ])
  })

  it('returns empty when weekOfMonth is missing', () => {
    const entry: ScheduleEntry = {
      frequency: 'monthlyByWeekday',
      day: 'sunday',
    }
    const result = generateOccurrenceDates(entry, d(2026, 4, 1), d(2026, 4, 30))
    expect(result).toEqual([])
  })
})

describe('generateOccurrenceDates – daily', () => {
  it('yields every day in the window, inclusive of both endpoints', () => {
    const entry: ScheduleEntry = { frequency: 'daily' }
    const result = generateOccurrenceDates(entry, d(2026, 4, 10), d(2026, 4, 14))
    expect(result.map((x) => x.getDate())).toEqual([10, 11, 12, 13, 14])
  })

  it('yields a single day when after == before (same day)', () => {
    const entry: ScheduleEntry = { frequency: 'daily' }
    const result = generateOccurrenceDates(entry, d(2026, 4, 10), d(2026, 4, 10))
    expect(result.map((x) => x.getDate())).toEqual([10])
  })

  it('returns empty when before < after', () => {
    const entry: ScheduleEntry = { frequency: 'daily' }
    const result = generateOccurrenceDates(entry, d(2026, 4, 10), d(2026, 4, 9))
    expect(result).toEqual([])
  })

  it('crosses a month boundary', () => {
    const entry: ScheduleEntry = { frequency: 'daily' }
    const result = generateOccurrenceDates(entry, d(2026, 4, 29), d(2026, 5, 2))
    expect(result.map((x) => `${x.getMonth() + 1}-${x.getDate()}`)).toEqual([
      '4-29',
      '4-30',
      '5-1',
      '5-2',
    ])
  })
})

describe('generateOccurrenceDates – monthlyByDate', () => {
  it('yields the 13th of every month in the window', () => {
    const entry: ScheduleEntry = { frequency: 'monthlyByDate', dayOfMonth: 13 }
    const result = generateOccurrenceDates(entry, d(2026, 4, 1), d(2026, 6, 30))
    expect(result.map((x) => `${x.getMonth() + 1}-${x.getDate()}`)).toEqual([
      '4-13',
      '5-13',
      '6-13',
    ])
  })

  it('skips months without a 31st', () => {
    const entry: ScheduleEntry = { frequency: 'monthlyByDate', dayOfMonth: 31 }
    // 2026: Jan=31, Feb=28, Mar=31, Apr=30, May=31, Jun=30, Jul=31,
    // Aug=31, Sep=30, Oct=31, Nov=30, Dec=31
    const result = generateOccurrenceDates(entry, d(2026, 1, 1), d(2026, 12, 31))
    expect(result.map((x) => `${x.getMonth() + 1}-${x.getDate()}`)).toEqual([
      '1-31',
      '3-31',
      '5-31',
      '7-31',
      '8-31',
      '10-31',
      '12-31',
    ])
  })

  it('skips a month when the window starts after that month\'s target day', () => {
    const entry: ScheduleEntry = { frequency: 'monthlyByDate', dayOfMonth: 13 }
    // Start 2026-04-14, after April's 13th — April should be skipped.
    const result = generateOccurrenceDates(entry, d(2026, 4, 14), d(2026, 6, 30))
    expect(result.map((x) => `${x.getMonth() + 1}-${x.getDate()}`)).toEqual([
      '5-13',
      '6-13',
    ])
  })

  it('works for the 1st of the month', () => {
    const entry: ScheduleEntry = { frequency: 'monthlyByDate', dayOfMonth: 1 }
    const result = generateOccurrenceDates(entry, d(2026, 4, 1), d(2026, 6, 1))
    expect(result.map((x) => `${x.getMonth() + 1}-${x.getDate()}`)).toEqual([
      '4-1',
      '5-1',
      '6-1',
    ])
  })

  it('returns empty when dayOfMonth is missing', () => {
    const entry: ScheduleEntry = { frequency: 'monthlyByDate' }
    const result = generateOccurrenceDates(entry, d(2026, 4, 1), d(2026, 6, 30))
    expect(result).toEqual([])
  })

  it('returns empty for out-of-range dayOfMonth', () => {
    const entry: ScheduleEntry = { frequency: 'monthlyByDate', dayOfMonth: 32 }
    const result = generateOccurrenceDates(entry, d(2026, 4, 1), d(2026, 6, 30))
    expect(result).toEqual([])
  })
})

describe('generateOccurrenceDates – edge cases', () => {
  it('returns empty when before < after', () => {
    const entry: ScheduleEntry = { frequency: 'weekly', day: 'sunday' }
    const result = generateOccurrenceDates(entry, d(2026, 4, 30), d(2026, 4, 1))
    expect(result).toEqual([])
  })

  it('returns empty for an unknown day value', () => {
    const entry = {
      frequency: 'weekly',
      day: 'someday',
    } as unknown as ScheduleEntry
    const result = generateOccurrenceDates(entry, d(2026, 4, 1), d(2026, 4, 30))
    expect(result).toEqual([])
  })
})

describe('combineDateAndTime', () => {
  it('copies HH:mm from timeSource onto the calendar date', () => {
    const date = d(2026, 4, 19)
    const time = new Date(1970, 0, 1, 10, 30)
    const combined = combineDateAndTime(date, time)
    expect(combined.getFullYear()).toBe(2026)
    expect(combined.getMonth()).toBe(3)
    expect(combined.getDate()).toBe(19)
    expect(combined.getHours()).toBe(10)
    expect(combined.getMinutes()).toBe(30)
    expect(combined.getSeconds()).toBe(0)
    expect(combined.getMilliseconds()).toBe(0)
  })

  it('zeroes seconds and milliseconds even when the source has them', () => {
    const date = d(2026, 4, 19)
    const time = new Date(1970, 0, 1, 18, 45, 42, 123)
    const combined = combineDateAndTime(date, time)
    expect(combined.getHours()).toBe(18)
    expect(combined.getMinutes()).toBe(45)
    expect(combined.getSeconds()).toBe(0)
    expect(combined.getMilliseconds()).toBe(0)
  })

  it('ignores the year/month/day of the timeSource', () => {
    const date = d(2026, 4, 19)
    const time = new Date(1999, 11, 31, 8, 0)
    const combined = combineDateAndTime(date, time)
    expect(combined.getFullYear()).toBe(2026)
    expect(combined.getMonth()).toBe(3)
    expect(combined.getDate()).toBe(19)
    expect(combined.getHours()).toBe(8)
  })
})
