// SPDX-FileCopyrightText: 2026 FocusFlow contributors
// SPDX-License-Identifier: AGPL-3.0-only

/// Tests Child Tasks behavior for the Scheduler Core package.
library;

import 'package:scheduler_core/scheduler_core.dart';
import 'package:test/test.dart';

/// Runs the `and` top-level operation.
/// This function is part of the file-level API and gives callers a direct way to use the associated scheduler behavior.
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 break-up command', () {
    final now = DateTime(2026, 6, 19, 12);
    const service = ChildTaskBreakUpService();

    test('creates ordered direct children for one parent', () {
      final parent = task(
        id: 'parent',
        title: 'Clean kitchen',
        createdAt: now,
      ).copyWith(projectId: 'home');
      final request = ChildTaskBreakUpRequest(
        parentTaskId: parent.id,
        operationId: 'break-parent-1',
        entries: [
          childEntry(
            id: 'first',
            title: 'Clear sink',
            createdAt: now,
          ),
          childEntry(
            id: 'second',
            title: 'Wipe counter',
            createdAt: now,
            priority: PriorityLevel.high,
          ),
        ],
      );

      final result = service.breakUp(tasks: [parent], request: request);

      expect(result.parentTask, parent);
      expect(result.operationId, 'break-parent-1');
      expect(result.createdChildTaskIds, ['first', 'second']);
      expect(result.childTasks.map((child) => child.id), ['first', 'second']);
      expect(
          result.childTasks.every((child) => child.parentTaskId == parent.id),
          isTrue);
      expect(result.childTasks.every((child) => child.projectId == 'home'),
          isTrue);
      expect(result.tasks.map((task) => task.id), [
        'parent',
        'first',
        'second',
      ]);
      expect(result.activities, isEmpty);
    });

    test('rejects empty child sets duplicate ids and reused task ids', () {
      final parent = task(id: 'parent', title: 'Clean kitchen', createdAt: now);
      final existingChild = task(
        id: 'existing',
        title: 'Existing child',
        createdAt: now,
        parentTaskId: parent.id,
      );

      expect(
        () => service.breakUp(
          tasks: [parent],
          request: ChildTaskBreakUpRequest(
            parentTaskId: parent.id,
            operationId: 'empty',
            entries: const [],
          ),
        ),
        throwsArgumentError,
      );
      expect(
        () => service.breakUp(
          tasks: [parent],
          request: ChildTaskBreakUpRequest(
            parentTaskId: parent.id,
            operationId: 'duplicate',
            entries: [
              childEntry(id: 'same', title: 'One', createdAt: now),
              childEntry(id: 'same', title: 'Two', createdAt: now),
            ],
          ),
        ),
        throwsArgumentError,
      );
      expect(
        () => service.breakUp(
          tasks: [parent, existingChild],
          request: ChildTaskBreakUpRequest(
            parentTaskId: parent.id,
            operationId: 'reused',
            entries: [
              childEntry(id: existingChild.id, title: 'Again', createdAt: now),
            ],
          ),
        ),
        throwsArgumentError,
      );
    });

    test('rejects self-parenting and invalid child row fields', () {
      final parent = task(id: 'parent', title: 'Clean kitchen', createdAt: now);

      expect(
        () => service.breakUp(
          tasks: [parent],
          request: ChildTaskBreakUpRequest(
            parentTaskId: parent.id,
            operationId: 'self',
            entries: [
              childEntry(id: parent.id, title: 'Self', createdAt: now),
            ],
          ),
        ),
        throwsArgumentError,
      );
      expect(
        () => service.breakUp(
          tasks: [parent],
          request: ChildTaskBreakUpRequest(
            parentTaskId: parent.id,
            operationId: 'blank-title',
            entries: [
              childEntry(id: 'child', title: '   ', createdAt: now),
            ],
          ),
        ),
        throwsArgumentError,
      );
      expect(
        () => service.breakUp(
          tasks: [parent],
          request: ChildTaskBreakUpRequest(
            parentTaskId: parent.id,
            operationId: 'bad-duration',
            entries: [
              ChildTaskEntry(
                id: 'child',
                title: 'Clear sink',
                createdAt: now,
                durationMinutes: 0,
              ),
            ],
          ),
        ),
        throwsA(isA<DomainValidationException>()),
      );
    });

    test('request snapshots entry lists', () {
      final entries = [
        childEntry(id: 'child', title: 'Clear sink', createdAt: now),
      ];

      final request = ChildTaskBreakUpRequest(
        parentTaskId: 'parent',
        operationId: 'break',
        entries: entries,
      );
      entries.clear();

      expect(request.entries.map((entry) => entry.id), ['child']);
      expect(
          () => request.entries.add(
                childEntry(id: 'other', title: 'Other', createdAt: now),
              ),
          throwsUnsupportedError);
    });
  });

  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 = scheduledTask(
        id: 'last-child',
        title: 'Wipe counter',
        createdAt: now,
        start: DateTime(2026, 6, 19, 11),
        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);
      expect(result.activities.map((activity) => activity.taskId), [
        'last-child',
        'parent',
      ]);
      expect(result.activities.last.metadata['completionSource'],
          'autoParentAfterLastChild');
      expect(taskById(result.tasks, 'last-child').stats.completedLateCount, 1);
    });

    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);
      expect(result.activities.map((activity) => activity.taskId), [
        'parent',
        'planned-child',
      ]);
      expect(result.activities.first.metadata['completionSource'], 'parent');
      expect(
        result.activities.last.metadata['completionSource'],
        'parentForce',
      );
    });

    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);
    });

    test('parent can be completed from a direct child', () {
      final parent = task(id: 'parent', title: 'Clean kitchen', createdAt: now);
      final initiatingChild = task(
        id: 'initiating-child',
        title: 'Clear sink',
        createdAt: now,
        parentTaskId: parent.id,
      );
      final sibling = task(
        id: 'sibling',
        title: 'Wipe counter',
        createdAt: now,
        parentTaskId: parent.id,
      );

      final result = service.completeParentFromChild(
        tasks: [parent, initiatingChild, sibling],
        childTaskId: initiatingChild.id,
        updatedAt: now,
        operationId: 'complete-from-child',
      );

      expect(taskById(result.tasks, parent.id).status, TaskStatus.completed);
      expect(
        taskById(result.tasks, initiatingChild.id).status,
        TaskStatus.completed,
      );
      expect(taskById(result.tasks, sibling.id).status, TaskStatus.completed);
      expect(result.forceCompletedChildIds, ['initiating-child', 'sibling']);
      expect(result.activities.map((activity) => activity.taskId), [
        'parent',
        'initiating-child',
        'sibling',
      ]);
      expect(
        result.activities.first.metadata['initiatingChildTaskId'],
        initiatingChild.id,
      );
    });

    test('completion retry with existing activities does not duplicate facts',
        () {
      final parent = task(id: 'parent', title: 'Clean kitchen', createdAt: now);
      final child = task(
        id: 'child',
        title: 'Clear sink',
        createdAt: now,
        parentTaskId: parent.id,
      );
      final first = service.completeParent(
        tasks: [parent, child],
        parentTaskId: parent.id,
        updatedAt: now,
        operationId: 'complete-parent-once',
      );

      final retry = service.completeParent(
        tasks: first.tasks,
        parentTaskId: parent.id,
        updatedAt: now.add(const Duration(minutes: 10)),
        operationId: 'complete-parent-once',
        existingActivities: first.activities,
      );

      expect(retry.changedTaskIds, isEmpty);
      expect(retry.activities, isEmpty);
      expect(retry.transitionOutcomeCodes, [
        TaskTransitionOutcomeCode.duplicateOperation,
      ]);
      expect(taskById(retry.tasks, parent.id).completedAt, now);
      expect(taskById(retry.tasks, child.id).completedAt, now);
    });

    test('force completion preserves already terminal child facts', () {
      final completedAt = now.subtract(const Duration(hours: 1));
      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,
      ).copyWith(
        updatedAt: completedAt,
        completedAt: completedAt,
      );
      final cancelledChild = task(
        id: 'cancelled-child',
        title: 'Wipe counter',
        createdAt: now,
        status: TaskStatus.cancelled,
        parentTaskId: parent.id,
      );

      final result = service.completeParent(
        tasks: [parent, completedChild, cancelledChild],
        parentTaskId: parent.id,
        updatedAt: now,
      );

      expect(result.forceCompletedChildIds, isEmpty);
      expect(
          taskById(result.tasks, completedChild.id).completedAt, completedAt);
      expect(
        taskById(result.tasks, cancelledChild.id).status,
        TaskStatus.cancelled,
      );
      expect(result.activities.map((activity) => activity.taskId), ['parent']);
    });
  });
}

/// Runs the `task` top-level operation.
/// This function is part of the file-level API and gives callers a direct way to use the associated scheduler behavior.
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,
  );
}

/// Runs the `scheduledTask` top-level operation.
/// This function is part of the file-level API and gives callers a direct way to use the associated scheduler behavior.
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)),
  );
}

/// Runs the `childEntry` top-level operation.
/// This function is part of the file-level API and gives callers a direct way to use the associated scheduler behavior.
ChildTaskEntry childEntry({
  required String id,
  required String title,
  required DateTime createdAt,
  PriorityLevel? priority,
}) {
  return ChildTaskEntry(
    id: id,
    title: title,
    createdAt: createdAt,
    priority: priority,
  );
}

/// Runs the `taskById` top-level operation.
/// This function is part of the file-level API and gives callers a direct way to use the associated scheduler behavior.
Task taskById(List<Task> tasks, String id) {
  return tasks.singleWhere((task) => task.id == id);
}
