import 'package:adhd_scheduler_core/scheduler_core.dart'; import 'package:test/test.dart'; void main() { group('Child task ownership', () { final now = DateTime(2026, 6, 19, 12); test('child references parent through parentTaskId', () { final parent = task(id: 'parent', title: 'Clean kitchen', createdAt: now); final child = task( id: 'child', title: 'Clear sink', createdAt: now, parentTaskId: parent.id, ); final view = ChildTaskView(tasks: [parent, child]); expect(child.parentTaskId, parent.id); expect(view.isChildOf(child: child, parent: parent), isTrue); expect(view.parentOf(child), same(parent)); }); test('parent can query and aggregate direct children', () { final parent = task(id: 'parent', title: 'Clean kitchen', createdAt: now); final plannedChild = task( id: 'planned-child', title: 'Clear sink', createdAt: now, parentTaskId: parent.id, ); final completedChild = task( id: 'completed-child', title: 'Wipe counter', createdAt: now, status: TaskStatus.completed, parentTaskId: parent.id, ); final unrelatedChild = task( id: 'unrelated-child', title: 'Sort drawer', createdAt: now, parentTaskId: 'other-parent', ); final view = ChildTaskView( tasks: [parent, plannedChild, unrelatedChild, completedChild], ); expect(view.childrenOf(parent).map((child) => child.id), [ 'planned-child', 'completed-child', ]); final summary = view.summaryFor(parent); expect(summary.totalCount, 2); expect(summary.plannedCount, 1); expect(summary.completedCount, 1); expect(summary.allChildrenCompleted, isFalse); }); test('parent can remain incomplete while children are planned or completed', () { final parent = task(id: 'parent', title: 'Clean kitchen', createdAt: now); final completedChild = task( id: 'completed-child', title: 'Clear sink', createdAt: now, status: TaskStatus.completed, parentTaskId: parent.id, ); final view = ChildTaskView(tasks: [parent, completedChild]); expect(parent.status, TaskStatus.planned); expect(view.summaryFor(parent).allChildrenCompleted, isTrue); expect(view.parentShouldAutoComplete(parent), isTrue); }); test('child ownership does not block normal scheduling behavior', () { const engine = SchedulingEngine(); final parent = task( id: 'parent', title: 'Clean kitchen', createdAt: now, status: TaskStatus.backlog, ); final child = task( id: 'child', title: 'Clear sink', createdAt: now, status: TaskStatus.backlog, durationMinutes: 15, parentTaskId: parent.id, ); final result = engine.insertBacklogTaskIntoNextAvailableSlot( input: SchedulingInput( tasks: [parent, child], window: SchedulingWindow( start: DateTime(2026, 6, 19, 9), end: DateTime(2026, 6, 19, 10), ), ), taskId: child.id, updatedAt: now, ); final scheduledChild = taskById(result.tasks, child.id); final unchangedParent = taskById(result.tasks, parent.id); expect(scheduledChild.status, TaskStatus.planned); expect(scheduledChild.parentTaskId, parent.id); expect(scheduledChild.scheduledStart, DateTime(2026, 6, 19, 9)); expect(unchangedParent.status, TaskStatus.backlog); expect(unchangedParent.scheduledStart, isNull); }); }); group('Child task entry defaults', () { final now = DateTime(2026, 6, 19, 12); test('child creation supports explicit row fields', () { final parent = task( id: 'parent', title: 'Clean kitchen', createdAt: now, ).copyWith(projectId: 'home'); final child = ChildTaskEntry( id: 'child', title: ' Clear sink ', createdAt: now, priority: PriorityLevel.high, reward: RewardLevel.high, difficulty: DifficultyLevel.hard, durationMinutes: 20, ).toTask(parent: parent); expect(child.id, 'child'); expect(child.title, 'Clear sink'); expect(child.parentTaskId, parent.id); expect(child.projectId, parent.projectId); expect(child.priority, PriorityLevel.high); expect(child.reward, RewardLevel.high); expect(child.difficulty, DifficultyLevel.hard); expect(child.durationMinutes, 20); expect(child.status, TaskStatus.backlog); }); test('child inherits parent project unless project override is supplied', () { final parent = task( id: 'parent', title: 'Plan trip', createdAt: now, ).copyWith(projectId: 'travel'); final inherited = ChildTaskEntry( id: 'inherited', title: 'Book train', createdAt: now, ).toTask(parent: parent); final overridden = ChildTaskEntry( id: 'overridden', title: 'Ask partner', createdAt: now, projectId: 'relationships', ).toTask(parent: parent); expect(inherited.projectId, 'travel'); expect(overridden.projectId, 'relationships'); }); test('missing reward remains not set and distinct from very low reward', () { final parent = task(id: 'parent', title: 'Clean kitchen', createdAt: now); final noReward = ChildTaskEntry( id: 'no-reward', title: 'Clear sink', createdAt: now, ).toTask(parent: parent); final veryLowReward = ChildTaskEntry( id: 'very-low', title: 'Wipe counter', createdAt: now, reward: RewardLevel.veryLow, ).toTask(parent: parent); expect(noReward.reward, RewardLevel.notSet); expect(veryLowReward.reward, RewardLevel.veryLow); expect(noReward.reward, isNot(veryLowReward.reward)); }); test('children without priority preserve row insertion order', () { final parent = task(id: 'parent', title: 'Clean kitchen', createdAt: now); final first = ChildTaskEntry( id: 'first', title: 'Clear sink', createdAt: now, ).toTask(parent: parent); final second = ChildTaskEntry( id: 'second', title: 'Wipe counter', createdAt: now, ).toTask(parent: parent); final third = ChildTaskEntry( id: 'third', title: 'Sweep floor', createdAt: now, ).toTask(parent: parent); final view = ChildTaskView(tasks: [parent, first, second, third]); expect(first.priority, isNull); expect(second.priority, isNull); expect(third.priority, isNull); expect(view.childrenOf(parent).map((child) => child.id), [ 'first', 'second', 'third', ]); }); }); group('Child task edge-case regressions', () { final now = DateTime(2026, 6, 19, 12); test('parent with zero children does not auto-complete by accident', () { final parent = task(id: 'parent', title: 'Clean kitchen', createdAt: now); final view = ChildTaskView(tasks: [parent]); expect(view.childrenOf(parent), isEmpty); expect(view.summaryFor(parent).hasChildren, isFalse); expect(view.summaryFor(parent).allChildrenCompleted, isFalse); expect(view.parentShouldAutoComplete(parent), isFalse); }); test('parent with planned children remains incomplete', () { final parent = task(id: 'parent', title: 'Clean kitchen', createdAt: now); final child = task( id: 'child', title: 'Clear sink', createdAt: now, parentTaskId: parent.id, ); final view = ChildTaskView(tasks: [parent, child]); expect(parent.status, TaskStatus.planned); expect(view.summaryFor(parent).plannedCount, 1); expect(view.parentShouldAutoComplete(parent), isFalse); }); test('child completion does not force parent or sibling completion', () { const service = FlexibleTaskActionService(); final parent = scheduledTask( id: 'parent', title: 'Clean kitchen', createdAt: now, start: DateTime(2026, 6, 19, 9), ); final firstChild = scheduledTask( id: 'first-child', title: 'Clear sink', createdAt: now, start: DateTime(2026, 6, 19, 9, 30), parentTaskId: parent.id, ); final secondChild = scheduledTask( id: 'second-child', title: 'Wipe counter', createdAt: now, start: DateTime(2026, 6, 19, 10), parentTaskId: parent.id, ); final completedChild = service.apply( task: firstChild, action: FlexibleTaskQuickAction.done, updatedAt: now, ); expect(completedChild.task.status, TaskStatus.completed); expect(completedChild.task.parentTaskId, parent.id); expect(parent.status, TaskStatus.planned); expect(secondChild.status, TaskStatus.planned); }); test('parent completion does not force children before explicit action', () { const service = FlexibleTaskActionService(); final parent = scheduledTask( id: 'parent', title: 'Clean kitchen', createdAt: now, start: DateTime(2026, 6, 19, 9), ); final child = scheduledTask( id: 'child', title: 'Clear sink', createdAt: now, start: DateTime(2026, 6, 19, 9, 30), parentTaskId: parent.id, ); final completedParent = service.apply( task: parent, action: FlexibleTaskQuickAction.done, updatedAt: now, ); expect(completedParent.task.status, TaskStatus.completed); expect(child.status, TaskStatus.planned); expect(child.parentTaskId, parent.id); }); test('child keeps parent id when scheduled pushed backlogged and completed', () { const engine = SchedulingEngine(); const service = FlexibleTaskActionService(); final parent = task( id: 'parent', title: 'Clean kitchen', createdAt: now, status: TaskStatus.backlog, ); final backlogChild = task( id: 'child', title: 'Clear sink', createdAt: now, status: TaskStatus.backlog, durationMinutes: 15, parentTaskId: parent.id, ); final scheduledResult = engine.insertBacklogTaskIntoNextAvailableSlot( input: SchedulingInput( tasks: [parent, backlogChild], window: SchedulingWindow( start: DateTime(2026, 6, 19, 9), end: DateTime(2026, 6, 19, 10), ), ), taskId: backlogChild.id, updatedAt: now, ); final scheduledChild = taskById(scheduledResult.tasks, backlogChild.id); final pushedResult = engine.pushFlexibleTaskToNextAvailableSlot( input: SchedulingInput( tasks: [scheduledChild], window: SchedulingWindow( start: DateTime(2026, 6, 19, 9), end: DateTime(2026, 6, 19, 10), ), ), taskId: scheduledChild.id, updatedAt: now, ); final pushedChild = taskById(pushedResult.tasks, backlogChild.id); final movedToBacklog = engine.moveToBacklog(pushedChild, updatedAt: now); final completedChild = service.apply( task: pushedChild, action: FlexibleTaskQuickAction.done, updatedAt: now, ); expect(scheduledChild.parentTaskId, parent.id); expect(pushedChild.parentTaskId, parent.id); expect(movedToBacklog.parentTaskId, parent.id); expect(completedChild.task.parentTaskId, parent.id); }); test('priority sorting is stable within same-priority child groups', () { final parent = task(id: 'parent', title: 'Clean kitchen', createdAt: now); final low = childEntry( id: 'low', title: 'Low', createdAt: now, priority: PriorityLevel.low, ).toTask(parent: parent); final firstHigh = childEntry( id: 'first-high', title: 'First high', createdAt: now, priority: PriorityLevel.high, ).toTask(parent: parent); final secondHigh = childEntry( id: 'second-high', title: 'Second high', createdAt: now, priority: PriorityLevel.high, ).toTask(parent: parent); final noPriority = childEntry( id: 'no-priority', title: 'No priority', createdAt: now, ).toTask(parent: parent); final view = ChildTaskView( tasks: [parent, low, firstHigh, noPriority, secondHigh], ); expect(view.childrenOfSortedByPriority(parent).map((child) => child.id), [ 'first-high', 'second-high', 'low', 'no-priority', ]); }); test( 'ownership stays direct and does not become dependency graph traversal', () { final parent = task(id: 'parent', title: 'Clean kitchen', createdAt: now); final child = task( id: 'child', title: 'Clear sink', createdAt: now, parentTaskId: parent.id, ); final grandchild = task( id: 'grandchild', title: 'Find sponge', createdAt: now, parentTaskId: child.id, ); final view = ChildTaskView(tasks: [parent, child, grandchild]); expect(view.childrenOf(parent).map((task) => task.id), ['child']); expect(view.childrenOf(child).map((task) => task.id), ['grandchild']); }); }); group('Parent child completion rules', () { final now = DateTime(2026, 6, 19, 12); const service = ChildTaskCompletionService(); test('all children completed completes parent', () { final parent = task(id: 'parent', title: 'Clean kitchen', createdAt: now); final completedChild = task( id: 'completed-child', title: 'Clear sink', createdAt: now, status: TaskStatus.completed, parentTaskId: parent.id, ); final lastChild = task( id: 'last-child', title: 'Wipe counter', createdAt: now, parentTaskId: parent.id, ); final result = service.completeChild( tasks: [parent, completedChild, lastChild], childTaskId: lastChild.id, updatedAt: now, ); expect(taskById(result.tasks, 'last-child').status, TaskStatus.completed); expect(taskById(result.tasks, 'parent').status, TaskStatus.completed); expect(result.autoCompletedParent, isTrue); expect(result.completedParentId, parent.id); expect(result.forceCompletedChildIds, isEmpty); }); test('parent complete force-completes remaining direct children', () { final parent = task(id: 'parent', title: 'Clean kitchen', createdAt: now); final plannedChild = task( id: 'planned-child', title: 'Clear sink', createdAt: now, parentTaskId: parent.id, ); final completedChild = task( id: 'completed-child', title: 'Wipe counter', createdAt: now, status: TaskStatus.completed, parentTaskId: parent.id, ); final result = service.completeParent( tasks: [parent, plannedChild, completedChild], parentTaskId: parent.id, updatedAt: now, ); expect(taskById(result.tasks, 'parent').status, TaskStatus.completed); expect( taskById(result.tasks, 'planned-child').status, TaskStatus.completed, ); expect( taskById(result.tasks, 'completed-child').status, TaskStatus.completed, ); expect(result.forceCompletedChildIds, ['planned-child']); expect(result.autoCompletedParent, isFalse); expect(result.completedParentId, parent.id); }); test('partial child completion does not complete parent', () { final parent = task(id: 'parent', title: 'Clean kitchen', createdAt: now); final firstChild = task( id: 'first-child', title: 'Clear sink', createdAt: now, parentTaskId: parent.id, ); final secondChild = task( id: 'second-child', title: 'Wipe counter', createdAt: now, parentTaskId: parent.id, ); final result = service.completeChild( tasks: [parent, firstChild, secondChild], childTaskId: firstChild.id, updatedAt: now, ); expect( taskById(result.tasks, 'first-child').status, TaskStatus.completed); expect(taskById(result.tasks, 'parent').status, TaskStatus.planned); expect(result.autoCompletedParent, isFalse); expect(result.canCompleteParentExplicitly, isTrue); }); test('completing one child does not complete siblings', () { final parent = task(id: 'parent', title: 'Clean kitchen', createdAt: now); final firstChild = task( id: 'first-child', title: 'Clear sink', createdAt: now, parentTaskId: parent.id, ); final secondChild = task( id: 'second-child', title: 'Wipe counter', createdAt: now, parentTaskId: parent.id, ); final result = service.completeChild( tasks: [parent, firstChild, secondChild], childTaskId: firstChild.id, updatedAt: now, ); expect( taskById(result.tasks, 'first-child').status, TaskStatus.completed); expect(taskById(result.tasks, 'second-child').status, TaskStatus.planned); expect(result.forceCompletedChildIds, isEmpty); }); test('explicit parent-complete action records parent and child completion', () { final parent = task(id: 'parent', title: 'Clean kitchen', createdAt: now); final firstChild = task( id: 'first-child', title: 'Clear sink', createdAt: now, parentTaskId: parent.id, ); final secondChild = task( id: 'second-child', title: 'Wipe counter', createdAt: now, parentTaskId: parent.id, ); final result = service.completeParent( tasks: [parent, firstChild, secondChild], parentTaskId: parent.id, updatedAt: now, ); expect(result.completedParentId, parent.id); expect(result.forceCompletedChildIds, ['first-child', 'second-child']); expect(result.changedTaskIds, ['parent', 'first-child', 'second-child']); expect(result.canCompleteParentExplicitly, isFalse); }); }); } Task task({ required String id, required String title, required DateTime createdAt, TaskStatus status = TaskStatus.planned, int? durationMinutes, String? parentTaskId, PriorityLevel? priority = PriorityLevel.medium, String projectId = 'home', }) { return Task( id: id, title: title, projectId: projectId, type: TaskType.flexible, status: status, priority: priority, durationMinutes: durationMinutes, parentTaskId: parentTaskId, createdAt: createdAt, updatedAt: createdAt, ); } Task scheduledTask({ required String id, required String title, required DateTime createdAt, required DateTime start, String? parentTaskId, }) { return task( id: id, title: title, createdAt: createdAt, durationMinutes: 15, parentTaskId: parentTaskId, ).copyWith( status: TaskStatus.planned, scheduledStart: start, scheduledEnd: start.add(const Duration(minutes: 15)), ); } ChildTaskEntry childEntry({ required String id, required String title, required DateTime createdAt, PriorityLevel? priority, }) { return ChildTaskEntry( id: id, title: title, createdAt: createdAt, priority: priority, ); } Task taskById(List tasks, String id) { return tasks.singleWhere((task) => task.id == id); }