import 'package:adhd_scheduler_core/scheduler_core.dart'; import 'package:test/test.dart'; void main() { group('Repository interfaces', () { final createdAt = DateTime(2026, 6, 21, 8); final window = SchedulingWindow( start: DateTime(2026, 6, 21, 9), end: DateTime(2026, 6, 21, 17), ); test('task repository supports in-memory fakes for domain services', () async { final backlogTask = task( id: 'backlog', title: 'Backlog task', status: TaskStatus.backlog, createdAt: createdAt, ); final scheduledTask = task( id: 'scheduled', title: 'Scheduled task', status: TaskStatus.planned, createdAt: createdAt, scheduledStart: DateTime(2026, 6, 21, 10), scheduledEnd: DateTime(2026, 6, 21, 10, 30), ); final repository = InMemoryTaskRepository([backlogTask]); await repository.save(scheduledTask); expect(await repository.findById('backlog'), same(backlogTask)); expect(await repository.findByStatus(TaskStatus.backlog), [backlogTask]); expect( (await repository.findScheduledInWindow(window)).map((task) => task.id), ['scheduled'], ); }); test('project repository stores project profiles by stable id', () async { final project = ProjectProfile( id: 'home', name: 'Home', colorKey: 'project-home', ); final repository = InMemoryProjectRepository(); await repository.save(project); expect(await repository.findById('home'), same(project)); expect(await repository.findAll(), [project]); }); test('locked block repository separates blocks from overrides', () async { final block = LockedBlock( id: 'work', name: 'Work', startTime: ClockTime(hour: 9, minute: 0), endTime: ClockTime(hour: 17, minute: 0), recurrence: LockedBlockRecurrence.weekly( weekdays: {LockedWeekday.monday}, ), createdAt: createdAt, updatedAt: createdAt, ); final override = LockedBlockOverride.remove( id: 'holiday', lockedBlockId: block.id, date: CivilDate(2026, 6, 22), createdAt: createdAt, ); final repository = InMemoryLockedBlockRepository(); await repository.saveBlock(block); await repository.saveOverride(override); expect(await repository.findBlockById('work'), same(block)); expect(await repository.findOverrideById('holiday'), same(override)); expect(await repository.findAllBlocks(), [block]); expect(await repository.findAllOverrides(), [override]); }); test('scheduling snapshot repository preserves scheduler input shape', () async { final scheduledTask = task( id: 'scheduled', title: 'Scheduled task', status: TaskStatus.planned, createdAt: createdAt, scheduledStart: DateTime(2026, 6, 21, 10), scheduledEnd: DateTime(2026, 6, 21, 10, 30), ); final lockedInterval = TimeInterval( start: DateTime(2026, 6, 21, 12), end: DateTime(2026, 6, 21, 13), label: 'Lunch', ); final snapshot = SchedulingStateSnapshot( id: 'snapshot-1', capturedAt: DateTime(2026, 6, 21, 8, 30), window: window, tasks: [scheduledTask], lockedIntervals: [lockedInterval], notices: [ SchedulingNotice('Saved schedule'), ], operationName: 'load-day', ); final repository = InMemorySchedulingSnapshotRepository(); await repository.save(snapshot); final saved = await repository.findById('snapshot-1'); expect(saved, same(snapshot)); expect(saved!.input.tasks, [scheduledTask]); expect(saved.input.lockedIntervals, [lockedInterval]); expect(saved.result.tasks, [scheduledTask]); expect(saved.result.notices.single.message, 'Saved schedule'); expect( (await repository.findInWindow(window)).map((snapshot) => snapshot.id), ['snapshot-1'], ); }); }); runCoreRepositoryConformance( createTaskRepository: () => InMemoryTaskRepository( [ task( id: 'backlog-a', title: 'Backlog A', status: TaskStatus.backlog, createdAt: DateTime.utc(2026, 6, 20), ), task( id: 'backlog-b', title: 'Backlog B', status: TaskStatus.backlog, createdAt: DateTime.utc(2026, 6, 21), ).copyWith(projectId: 'home', parentTaskId: 'parent'), task( id: 'scheduled', title: 'Scheduled', status: TaskStatus.planned, createdAt: DateTime.utc(2026, 6, 21), scheduledStart: DateTime.utc(2026, 6, 22, 10), scheduledEnd: DateTime.utc(2026, 6, 22, 10, 30), ).copyWith(projectId: 'home'), ], ), createProjectRepository: () => InMemoryProjectRepository([ ProjectProfile(id: 'home', name: 'Home', colorKey: 'home-blue'), ProjectProfile(id: 'work', name: 'Work', colorKey: 'work-green'), ]), createLockedBlockRepository: () => InMemoryLockedBlockRepository( initialBlocks: [ LockedBlock( id: 'work', name: 'Work', startTime: ClockTime(hour: 9, minute: 0), endTime: ClockTime(hour: 17, minute: 0), recurrence: LockedBlockRecurrence.weekly( weekdays: {LockedWeekday.monday}, ), createdAt: DateTime.utc(2026, 6, 20), updatedAt: DateTime.utc(2026, 6, 20), ), ], initialOverrides: [ LockedBlockOverride.remove( id: 'holiday', lockedBlockId: 'work', date: CivilDate(2026, 6, 22), createdAt: DateTime.utc(2026, 6, 20), ), ], ), ); } void runCoreRepositoryConformance({ required TaskRepository Function() createTaskRepository, required ProjectRepository Function() createProjectRepository, required LockedBlockRepository Function() createLockedBlockRepository, }) { group('Core repository conformance', () { const ownerId = 'owner-1'; test('task queries are owner-scoped, ordered, paged, and revisioned', () async { final repository = createTaskRepository(); final firstPage = await repository.findByOwner( ownerId: ownerId, page: const RepositoryPageRequest(limit: 2), ); final secondPage = await repository.findByOwner( ownerId: ownerId, page: RepositoryPageRequest(cursor: firstPage.nextCursor, limit: 2), ); final homeTasks = await repository.findByProjectId( ownerId: ownerId, projectId: 'home', ); final children = await repository.findByParentTaskId( ownerId: ownerId, parentTaskId: 'parent', ); final backlog = await repository.findBacklogCandidates( const BacklogCandidateQuery(ownerId: ownerId, projectId: 'home'), ); final localDay = await repository.findForLocalDay( ownerId: ownerId, localDate: CivilDate(2026, 6, 22), window: SchedulingWindow( start: DateTime.utc(2026, 6, 22), end: DateTime.utc(2026, 6, 23), ), ); final record = await repository.findRecordById('scheduled'); final updated = record!.value.copyWith( title: 'Updated scheduled', updatedAt: DateTime.utc(2026, 6, 22, 12), ); expect(firstPage.items.map((task) => task.id), [ 'backlog-a', 'backlog-b', ]); expect(firstPage.hasMore, isTrue); expect(secondPage.items.map((task) => task.id), ['scheduled']); expect(homeTasks.items.map((task) => task.id), [ 'backlog-b', 'scheduled', ]); expect(children.items.map((task) => task.id), ['backlog-b']); expect(backlog.items.map((task) => task.id), ['backlog-b']); expect(localDay.items.map((task) => task.id), ['scheduled']); expect(record.ownerId, ownerId); expect(record.revision, 1); final stale = await repository.saveIfRevision( task: updated, ownerId: ownerId, expectedRevision: 99, ); final invalidRevision = await repository.saveIfRevision( task: updated, ownerId: ownerId, expectedRevision: 0, ); final wrongOwner = await repository.saveIfRevision( task: updated, ownerId: 'other-owner', expectedRevision: 1, ); expect(stale.failure!.code, RepositoryFailureCode.staleRevision); expect(stale.failure!.actualRevision, 1); expect( invalidRevision.failure!.code, RepositoryFailureCode.invalidRevision, ); expect(wrongOwner.failure!.code, RepositoryFailureCode.ownerMismatch); final saved = await repository.saveIfRevision( task: updated, ownerId: ownerId, expectedRevision: 1, ); expect(saved.isSuccess, isTrue); expect(saved.revision, 2); expect((await repository.findRecordById('scheduled'))!.revision, 2); expect( () => firstPage.items.add(record.value), throwsUnsupportedError, ); }); test('project queries hide archived records by default and honor revisions', () async { final repository = createProjectRepository(); final record = await repository.findRecordById('home'); final archived = await repository.archive( projectId: 'home', ownerId: ownerId, expectedRevision: record!.revision, archivedAt: DateTime.utc(2026, 6, 25), ); final visible = await repository.findByOwner(ownerId: ownerId); final includingArchived = await repository.findByOwner( ownerId: ownerId, includeArchived: true, ); expect(archived.isSuccess, isTrue); expect(visible.items.map((project) => project.id), ['work']); expect(includingArchived.items.map((project) => project.id), [ 'home', 'work', ]); expect( (await repository.findRecordById('home'))!.archivedAt, DateTime.utc(2026, 6, 25), ); expect( (await repository.insert( project: ProjectProfile(id: 'home', name: 'Home', colorKey: 'again'), ownerId: ownerId, )) .failure! .code, RepositoryFailureCode.duplicateId, ); }); test('locked queries find date-scoped overrides and archive by revision', () async { final repository = createLockedBlockRepository(); final blockRecord = await repository.findBlockRecordById('work'); final overrides = await repository.findOverridesForDate( ownerId: ownerId, date: CivilDate(2026, 6, 22), ); final archived = await repository.archiveBlock( blockId: 'work', ownerId: ownerId, expectedRevision: blockRecord!.revision, archivedAt: DateTime.utc(2026, 6, 25), ); final visible = await repository.findBlocksByOwner(ownerId: ownerId); final includingArchived = await repository.findBlocksByOwner( ownerId: ownerId, includeArchived: true, ); expect(overrides.items.map((override) => override.id), ['holiday']); expect(archived.isSuccess, isTrue); expect(visible.items, isEmpty); expect(includingArchived.items.map((block) => block.id), ['work']); expect( (await repository.findBlockRecordById('work'))!.archivedAt, DateTime.utc(2026, 6, 25), ); }); }); } Task task({ required String id, required String title, required TaskStatus status, required DateTime createdAt, DateTime? scheduledStart, DateTime? scheduledEnd, }) { return Task( id: id, title: title, projectId: 'inbox', type: TaskType.flexible, status: status, durationMinutes: 30, scheduledStart: scheduledStart, scheduledEnd: scheduledEnd, createdAt: createdAt, updatedAt: createdAt, ); }