import { describe, it, expect } from 'vitest' import { intervalHitsToday, isScheduledToday, getDueTimeToday, isPastTime, isExtendedToday, formatDueTimeLabel, msUntilExpiry, toLocalISODate, } from '../common/scheduleUtils' import type { ChoreSchedule } from '../common/models' // --------------------------------------------------------------------------- // toLocalISODate // --------------------------------------------------------------------------- describe('toLocalISODate', () => { it('formats a date as YYYY-MM-DD', () => { const d = new Date(2025, 0, 5) // Jan 5, 2025 local expect(toLocalISODate(d)).toBe('2025-01-05') }) it('pads month and day', () => { const d = new Date(2025, 2, 9) // Mar 9 expect(toLocalISODate(d)).toBe('2025-03-09') }) }) // --------------------------------------------------------------------------- // intervalHitsToday // --------------------------------------------------------------------------- describe('intervalHitsToday', () => { it('anchor day hits on itself', () => { const wednesday = new Date(2025, 0, 8) // Jan 8, 2025 is a Wednesday expect(intervalHitsToday('2025-01-08', 2, wednesday)).toBe(true) }) it('anchor=Wednesday(2025-01-08), interval=2, Thursday does NOT hit', () => { const thursday = new Date(2025, 0, 9) // 1 day after anchor, 1 % 2 !== 0 expect(intervalHitsToday('2025-01-08', 2, thursday)).toBe(false) }) it('anchor=Wednesday(2025-01-08), interval=2, Friday hits (2 days after anchor)', () => { const friday = new Date(2025, 0, 10) // 2 days after anchor, 2 % 2 === 0 expect(intervalHitsToday('2025-01-08', 2, friday)).toBe(true) }) it('interval=1 hits every day', () => { const monday = new Date(2025, 0, 6) // anchor is that same Monday expect(intervalHitsToday('2025-01-06', 1, monday)).toBe(true) const tuesday = new Date(2025, 0, 7) expect(intervalHitsToday('2025-01-06', 1, tuesday)).toBe(true) }) it('returns false for a date before the anchor', () => { const beforeAnchor = new Date(2025, 0, 7) // Jan 7 is before Jan 8 anchor expect(intervalHitsToday('2025-01-08', 2, beforeAnchor)).toBe(false) }) it('empty anchor string treats localDate as the anchor (always hits)', () => { const today = new Date(2025, 0, 8) expect(intervalHitsToday('', 2, today)).toBe(true) }) }) // --------------------------------------------------------------------------- // isScheduledToday // --------------------------------------------------------------------------- describe('isScheduledToday', () => { const daysSchedule: ChoreSchedule = { child_id: 'c1', task_id: 't1', mode: 'days', day_configs: [ { day: 1, hour: 8, minute: 0 }, // Monday { day: 3, hour: 9, minute: 30 }, // Wednesday ], interval_days: 2, anchor_date: '', interval_has_deadline: true, interval_hour: 0, interval_minute: 0, } it('returns true on a scheduled weekday', () => { const monday = new Date(2025, 0, 6) // Jan 6, 2025 = Monday expect(isScheduledToday(daysSchedule, monday)).toBe(true) }) it('returns false on an unscheduled weekday', () => { const tuesday = new Date(2025, 0, 7) // Tuesday expect(isScheduledToday(daysSchedule, tuesday)).toBe(false) }) it('interval mode delegates to intervalHitsToday', () => { const intervalSchedule: ChoreSchedule = { child_id: 'c1', task_id: 't1', mode: 'interval', day_configs: [], interval_days: 2, anchor_date: '2025-01-08', // Wednesday interval_has_deadline: true, interval_hour: 8, interval_minute: 0, } const wednesday = new Date(2025, 0, 8) expect(isScheduledToday(intervalSchedule, wednesday)).toBe(true) const thursday = new Date(2025, 0, 9) expect(isScheduledToday(intervalSchedule, thursday)).toBe(false) }) }) // --------------------------------------------------------------------------- // getDueTimeToday // --------------------------------------------------------------------------- describe('getDueTimeToday', () => { const daysSchedule: ChoreSchedule = { child_id: 'c1', task_id: 't1', mode: 'days', day_configs: [{ day: 1, hour: 8, minute: 30 }], interval_days: 2, anchor_date: '', interval_has_deadline: true, interval_hour: 0, interval_minute: 0, } it('returns correct time for a scheduled day', () => { const monday = new Date(2025, 0, 6) expect(getDueTimeToday(daysSchedule, monday)).toEqual({ hour: 8, minute: 30 }) }) it('returns null for an unscheduled day', () => { const tuesday = new Date(2025, 0, 7) expect(getDueTimeToday(daysSchedule, tuesday)).toBeNull() }) it('interval mode returns interval time on hit days', () => { const intervalSchedule: ChoreSchedule = { child_id: 'c1', task_id: 't1', mode: 'interval', day_configs: [], interval_days: 2, anchor_date: '2025-01-08', interval_has_deadline: true, interval_hour: 14, interval_minute: 45, } const wednesday = new Date(2025, 0, 8) expect(getDueTimeToday(intervalSchedule, wednesday)).toEqual({ hour: 14, minute: 45 }) }) it('interval mode returns null on non-hit days', () => { const intervalSchedule: ChoreSchedule = { child_id: 'c1', task_id: 't1', mode: 'interval', day_configs: [], interval_days: 2, anchor_date: '2025-01-08', interval_has_deadline: true, interval_hour: 14, interval_minute: 45, } const thursday = new Date(2025, 0, 9) expect(getDueTimeToday(intervalSchedule, thursday)).toBeNull() }) it('interval mode with interval_has_deadline=false returns null (Anytime)', () => { const intervalSchedule: ChoreSchedule = { child_id: 'c1', task_id: 't1', mode: 'interval', day_configs: [], interval_days: 2, anchor_date: '2025-01-08', interval_has_deadline: false, interval_hour: 14, interval_minute: 45, } const wednesday = new Date(2025, 0, 8) // Even on a hit day, Anytime means no deadline → null expect(getDueTimeToday(intervalSchedule, wednesday)).toBeNull() }) }) // --------------------------------------------------------------------------- // isPastTime // --------------------------------------------------------------------------- describe('isPastTime', () => { it('returns true when current time equals due time', () => { const now = new Date(2025, 0, 6, 8, 30, 0) expect(isPastTime(8, 30, now)).toBe(true) }) it('returns true when current time is past due time', () => { const now = new Date(2025, 0, 6, 9, 0, 0) expect(isPastTime(8, 30, now)).toBe(true) }) it('returns false when current time is before due time', () => { const now = new Date(2025, 0, 6, 8, 0, 0) expect(isPastTime(8, 30, now)).toBe(false) }) }) // --------------------------------------------------------------------------- // isExtendedToday // --------------------------------------------------------------------------- describe('isExtendedToday', () => { it('returns true when extension date matches today', () => { const today = new Date(2025, 0, 15) expect(isExtendedToday('2025-01-15', today)).toBe(true) }) it('returns false when extension date does not match today', () => { const today = new Date(2025, 0, 15) expect(isExtendedToday('2025-01-14', today)).toBe(false) }) it('returns false for null extension date', () => { const today = new Date(2025, 0, 15) expect(isExtendedToday(null, today)).toBe(false) }) it('returns false for undefined extension date', () => { const today = new Date(2025, 0, 15) expect(isExtendedToday(undefined, today)).toBe(false) }) }) // --------------------------------------------------------------------------- // formatDueTimeLabel // --------------------------------------------------------------------------- describe('formatDueTimeLabel', () => { it('formats midnight as 12:00 AM', () => { expect(formatDueTimeLabel(0, 0)).toBe('12:00 AM') }) it('formats noon as 12:00 PM', () => { expect(formatDueTimeLabel(12, 0)).toBe('12:00 PM') }) it('formats 8:30 AM', () => { expect(formatDueTimeLabel(8, 30)).toBe('8:30 AM') }) it('formats 14:45 as 2:45 PM', () => { expect(formatDueTimeLabel(14, 45)).toBe('2:45 PM') }) it('pads single-digit minutes', () => { expect(formatDueTimeLabel(9, 5)).toBe('9:05 AM') }) }) // --------------------------------------------------------------------------- // msUntilExpiry // --------------------------------------------------------------------------- describe('msUntilExpiry', () => { it('returns positive ms when due time is in the future', () => { const now = new Date(2025, 0, 6, 8, 0, 0, 0) const ms = msUntilExpiry(8, 30, now) expect(ms).toBe(30 * 60 * 1000) // 30 minutes }) it('returns 0 when due time is in the past', () => { const now = new Date(2025, 0, 6, 9, 0, 0, 0) expect(msUntilExpiry(8, 30, now)).toBe(0) }) it('returns 0 when due time equals current time', () => { const now = new Date(2025, 0, 6, 8, 30, 0, 0) expect(msUntilExpiry(8, 30, now)).toBe(0) }) it('correctly handles seconds and milliseconds offset', () => { // 8:29:30.500 → due at 8:30:00.000 = 29500ms remaining const now = new Date(2025, 0, 6, 8, 29, 30, 500) expect(msUntilExpiry(8, 30, now)).toBe(29500) }) })