import { describe, expect, it } from 'vitest' import type { EntityObject } from '../../../../chrono_manager/src/models/entity' import { daysVisibleRange } from '../../../src/utils/date' import { expandEntityInstances } from '../../../src/utils/recurrence' function localIso(year: number, month: number, day: number, hour = 0): string { return new Date(year, month, day, hour).toISOString() } function eventEntity( identifier: string, startsOn: string | null, endsOn: string | null, timeless = false, ): EntityObject { return { identifier, properties: { startsOn, endsOn, timeless, }, } as unknown as EntityObject } describe('entity instance expansion', () => { it('normalizes timestamps and duration once', () => { const [instance] = expandEntityInstances( eventEntity('meeting', localIso(2026, 5, 15, 9), localIso(2026, 5, 15, 10)), ) expect(instance.duration).toBe(60 * 60 * 1000) expect(new Date(instance.start).getHours()).toBe(9) expect(new Date(instance.dayStart).getHours()).toBe(0) expect(instance.instanceId).toBeNull() expect(instance.multiDay).toBe(false) }) it('produces no instance for an invalid start timestamp', () => { expect(expandEntityInstances(eventEntity('invalid', 'not-a-date', null))).toEqual([]) }) it('marks an overnight event as multi-day', () => { const [instance] = expandEntityInstances( eventEntity('overnight', localIso(2026, 5, 15, 23), localIso(2026, 5, 16, 1)), ) expect(instance.multiDay).toBe(true) expect(instance.timeless).toBe(true) }) it('honors an exclusive midnight end', () => { const [instance] = expandEntityInstances( eventEntity('all-day', localIso(2026, 5, 20), localIso(2026, 5, 21), true), ) const end = new Date(instance.end) expect([end.getDate(), end.getHours()]).toEqual([21, 0]) expect(new Date(instance.dayStart).getDate()).toBe(20) expect(instance.multiDay).toBe(false) }) it('treats UTC timestamps on timeless events as floating calendar dates', () => { const [instance] = expandEntityInstances( eventEntity('utc-all-day', '2025-03-27T00:00:00+00:00', '2025-03-28T00:00:00+00:00', true), ) const start = new Date(instance.start) expect([start.getMonth(), start.getDate(), start.getHours()]).toEqual([2, 27, 0]) expect(new Date(instance.end).getDate()).toBe(28) }) it('gives a zero-duration event a queryable minimum duration', () => { const [instance] = expandEntityInstances( eventEntity('instant', localIso(2026, 5, 15, 9), null), ) expect(instance.duration).toBe(1) }) it('omits a non-recurring event outside the requested range', () => { const instances = expandEntityInstances( eventEntity('outside', localIso(2026, 6, 1, 9), localIso(2026, 6, 1, 10)), daysVisibleRange(new Date(2026, 5, 15), 2), ) expect(instances).toEqual([]) }) })