import { describe, expect, it } from 'vitest' import { type LaneSubject, previewCascade, resolveLanes } from '@/lib/timetable/lane' import type { Performance } from '@/types/timetable' function s(id: string, start: string, end: string, lane: number | null = null, cancelled = false): LaneSubject { return { id, start_at: start, end_at: end, lane, cancelled } } function p(id: string, start: string, end: string, lane = 0): Performance { return { id, engagement_id: 'e1', event_id: 'ev1', stage_id: 's1', lane, lane_resolved: lane, start_at: start, end_at: end, version: 0, notes: null, warnings: [], created_at: null, updated_at: null, deleted_at: null, } } describe('resolveLanes (Pass 2 only — implicit lanes)', () => { it('places non-overlapping items on lane 0', () => { const result = resolveLanes([ s('a', '2026-07-10T18:00:00Z', '2026-07-10T19:00:00Z'), s('b', '2026-07-10T19:00:00Z', '2026-07-10T20:00:00Z'), ]) expect(result.laneOf).toEqual({ a: 0, b: 0 }) expect(result.laneCount).toBe(1) }) it('stacks overlapping items into separate lanes', () => { const result = resolveLanes([ s('a', '2026-07-10T18:00:00Z', '2026-07-10T19:30:00Z'), s('b', '2026-07-10T19:00:00Z', '2026-07-10T20:00:00Z'), ]) expect(result.laneOf.a).toBe(0) expect(result.laneOf.b).toBe(1) expect(result.laneCount).toBe(2) }) it('Pass 1 — explicit lane is honoured', () => { const result = resolveLanes([ s('a', '2026-07-10T18:00:00Z', '2026-07-10T19:00:00Z', 2), s('b', '2026-07-10T19:00:00Z', '2026-07-10T20:00:00Z'), ]) expect(result.laneOf.a).toBe(2) expect(result.laneOf.b).toBe(0) }) it('Pass 1 — overlapping explicit lane bumps down', () => { const result = resolveLanes([ s('a', '2026-07-10T18:00:00Z', '2026-07-10T19:30:00Z', 0), s('b', '2026-07-10T19:00:00Z', '2026-07-10T20:00:00Z', 0), ]) expect(result.laneOf.a).toBe(0) expect(result.laneOf.b).toBe(1) }) it('cancelled items are excluded from collision checks', () => { const result = resolveLanes([ s('a', '2026-07-10T18:00:00Z', '2026-07-10T19:30:00Z'), s('b', '2026-07-10T19:00:00Z', '2026-07-10T20:00:00Z', null, true), ]) expect(result.laneOf.b).toBe(0) }) it('handles empty input', () => { const result = resolveLanes([]) expect(result.laneCount).toBe(1) expect(result.laneOf).toEqual({}) }) }) describe('previewCascade (drag preview)', () => { it('preserves wanted lane when target is free', () => { const cohort = [p('a', '2026-07-10T18:00:00Z', '2026-07-10T19:00:00Z', 0)] const result = previewCascade( { id: 'dragged', lane: 1, start_at: '2026-07-10T18:30:00Z', end_at: '2026-07-10T19:30:00Z' }, cohort, ) expect(result.laneOf.dragged).toBe(1) expect(result.laneOf.a).toBe(0) }) it('cascades existing item down when wanted lane is busy', () => { const cohort = [p('a', '2026-07-10T18:00:00Z', '2026-07-10T19:00:00Z', 0)] const result = previewCascade( { id: 'dragged', lane: 0, start_at: '2026-07-10T18:30:00Z', end_at: '2026-07-10T19:30:00Z' }, cohort, ) expect(result.laneOf.dragged).toBe(1) expect(result.laneOf.a).toBe(0) }) })