import { expect, test } from 'bun:test'; import { addDays, endOfDay, localDate, scheduled, schedulesOverlap, shade } from './calendar'; import { calendarSettingsSchema, dateSchema, habitInput, scheduleSchema } from './contracts'; test('fixed calendar recurrence uses anchors and Monday weeks independently of completions', () => { expect(scheduled({ type: 'daily' }, '2026-09-04')).toBe(true); expect(scheduled({ type: 'interval', every: 3, anchor: '2026-09-04' }, '2026-09-07')).toBe(true); expect(scheduled({ type: 'interval', every: 3, anchor: '2026-09-04' }, '2026-09-01')).toBe(false); expect(scheduled({ type: 'interval', every: 3, anchor: '2026-09-04' }, '2026-09-06')).toBe(false); expect(scheduled({ type: 'weekdays', days: [1] }, '2026-09-07')).toBe(true); expect(scheduled({ type: 'weekdays', days: [1] }, '2026-09-08')).toBe(false); const rule = { type: 'weekly' as const, every: 2, weekday: 3, anchor: '2026-08-31' }; expect(scheduled(rule, '2026-09-02')).toBe(true); expect(scheduled(rule, '2026-09-09')).toBe(false); expect(scheduled(rule, '2026-09-16')).toBe(true); }); test('real dates, strict settings, and exclusive methods are validated', () => { for (const d of ['2026-02-29', '2026-13-01', '2026-9-1', 'garbage']) expect(dateSchema.safeParse(d).success).toBe(false); expect(addDays('2024-02-28', 1)).toBe('2024-02-29'); expect(habitInput.safeParse({ name: 'mixed', method: 'manual', target: 8 }).success).toBe(false); expect(scheduleSchema.safeParse({ type: 'weekdays', days: [1, 1] }).success).toBe(false); expect(calendarSettingsSchema.safeParse({ daySpacing: 4 }).success).toBe(false); }); test('local midnight handles spring, autumn, and fractional timezone offsets', () => { expect(endOfDay('2026-03-29', 'Europe/Belgrade') - endOfDay('2026-03-28', 'Europe/Belgrade')).toBe(23 * 3600000); expect(endOfDay('2026-10-25', 'Europe/Belgrade') - endOfDay('2026-10-24', 'Europe/Belgrade')).toBe(25 * 3600000); expect(new Date(endOfDay('2026-09-04', 'Asia/Kathmandu')).toISOString()).toBe('2026-09-04T18:15:00.000Z'); expect(localDate(Date.parse('2026-09-03T22:00Z'), 'Europe/Belgrade')).toBe('2026-09-04'); }); test('eight positive count shades, full completion, neutral and empty stay distinct', () => { const settings = calendarSettingsSchema.parse({}); const values = Array.from({ length: 8 }, (_, i) => shade((i + 1) / 8, 8, settings, true)); expect(new Set(values.map(v => v.color)).size).toBe(8); expect(values.map(v => v.level)).toEqual([1,2,3,4,5,6,7,8]); expect(shade(1, 1, settings, true).color).toBe(settings.mainColor); expect(shade(0, 8, settings, true).color).toBe(settings.emptyColor); expect(shade(null, 8, settings, true).color).toBe(settings.notDueColor); expect(shade(0.75, 4, settings, false).level).toBe(3); }); test('white and pale main colors still produce distinct positive shades', () => { for (const mainColor of ['#ffffff', '#fefdfc', '#ffff00', '#000000']) { const settings = calendarSettingsSchema.parse({ mainColor, shadeCount: 20 }); const colors = Array.from({ length: 20 }, (_, i) => shade((i + 1) / 20, 20, settings, true).color); expect(new Set(colors).size).toBe(20); expect(colors.at(-1)).toBe(mainColor); } }); test('schedule intersection detects incompatible anchored day and week cycles', () => { expect(schedulesOverlap({ type: 'interval', every: 7, anchor: '2026-09-04' }, { type: 'weekdays', days: [1] })).toBe(false); expect(schedulesOverlap({ type: 'interval', every: 3, anchor: '2026-09-04' }, { type: 'weekdays', days: [1] })).toBe(true); expect(schedulesOverlap({ type: 'weekly', every: 2, weekday: 3, anchor: '2026-08-31' }, { type: 'weekly', every: 2, weekday: 3, anchor: '2026-09-07' })).toBe(false); expect(schedulesOverlap({ type: 'weekly', every: 2, weekday: 3, anchor: '2026-08-31' }, { type: 'weekly', every: 3, weekday: 3, anchor: '2026-09-07' })).toBe(true); expect(schedulesOverlap({ type: 'daily' }, { type: 'weekly', every: 520, weekday: 0, anchor: '2026-09-04' })).toBe(true); });