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

/// Tests Task Lifecycle behavior for the Scheduler Core package.
library;

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

/// Entry point for this executable Dart file.
/// It wires the local command, test, or application behavior needed by this repository without exposing additional public API.
void main() {
  group('Task transition service', () {
    final now = DateTime(2026, 6, 25, 9);
    const service = TaskTransitionService();

    test('completion records completedAt actual interval and activity', () {
      final task = scheduledTask(id: 'task-1', now: now);
      final actualStart = DateTime(2026, 6, 25, 9, 5);
      final actualEnd = DateTime(2026, 6, 25, 9, 20);

      final result = service.apply(
        task: task,
        transitionCode: TaskTransitionCode.complete,
        operationId: 'op-complete',
        activityId: 'activity-complete',
        occurredAt: now.add(const Duration(minutes: 30)),
        actualStart: actualStart,
        actualEnd: actualEnd,
      );

      expect(result.outcomeCode, TaskTransitionOutcomeCode.applied);
      expect(result.task.status, TaskStatus.completed);
      expect(result.task.completedAt, now.add(const Duration(minutes: 30)));
      expect(result.task.actualStart, actualStart);
      expect(result.task.actualEnd, actualEnd);
      expect(result.activities.single.id, 'activity-complete');
      expect(result.activities.single.operationId, 'op-complete');
      expect(result.activities.single.code, TaskActivityCode.completed);
      expect(result.task.stats.completedLateCount, 1);
      expect(
        result.activities.single.metadata,
        containsPair('previousStatus', 'planned'),
      );
      expect(
        result.activities.single.metadata,
        containsPair('nextStatus', 'completed'),
      );
      expect(
        result.activities.single.metadata,
        containsPair('pushesBeforeCompletion', 0),
      );
    });

    test('late-by-zero completion is not counted late', () {
      final task = scheduledTask(id: 'task-1', now: now);

      final result = service.apply(
        task: task,
        transitionCode: TaskTransitionCode.complete,
        operationId: 'op-complete',
        activityId: 'activity-complete',
        occurredAt: task.scheduledEnd,
      );

      expect(result.task.completedAt, task.scheduledEnd);
      expect(result.task.stats.completedLateCount, 0);
    });

    test('completion accounting uses actual locked overlap boundaries', () {
      final task = scheduledTask(
        id: 'task-1',
        now: now,
        start: DateTime(2026, 6, 25, 9),
        durationMinutes: 90,
      );

      final result = service.apply(
        task: task,
        transitionCode: TaskTransitionCode.complete,
        operationId: 'op-complete',
        activityId: 'activity-complete',
        occurredAt: DateTime(2026, 6, 25, 10, 30),
        actualStart: DateTime(2026, 6, 25, 9, 15),
        actualEnd: DateTime(2026, 6, 25, 10, 15),
        lockedIntervals: [
          TimeInterval(
            start: DateTime(2026, 6, 25, 8),
            end: DateTime(2026, 6, 25, 9, 15),
          ),
          TimeInterval(
            start: DateTime(2026, 6, 25, 9, 30),
            end: DateTime(2026, 6, 25, 9, 45),
          ),
          TimeInterval(
            start: DateTime(2026, 6, 25, 9, 40),
            end: DateTime(2026, 6, 25, 10),
          ),
        ],
      );

      expect(result.task.stats.completedDuringLockedHoursCount, 1);
      expect(result.task.stats.completedDuringLockedHoursMinutes, 30);
    });

    test('completion without actual interval does not fabricate locked minutes',
        () {
      final task = scheduledTask(id: 'task-1', now: now);

      final result = service.apply(
        task: task,
        transitionCode: TaskTransitionCode.complete,
        operationId: 'op-complete',
        activityId: 'activity-complete',
        occurredAt: DateTime(2026, 6, 25, 9, 20),
        lockedIntervals: [
          TimeInterval(
            start: DateTime(2026, 6, 25, 9),
            end: DateTime(2026, 6, 25, 10),
          ),
        ],
      );

      expect(result.task.actualStart, isNull);
      expect(result.task.stats.completedDuringLockedHoursCount, 0);
      expect(result.task.stats.completedDuringLockedHoursMinutes, 0);
    });

    test('completion captures pushes present at completion', () {
      final task = scheduledTask(id: 'task-1', now: now).copyWith(
        stats: const TaskStatistics(
          manuallyPushedCount: 2,
          autoPushedCount: 1,
        ),
      );

      final result = service.apply(
        task: task,
        transitionCode: TaskTransitionCode.complete,
        operationId: 'op-complete',
        activityId: 'activity-complete',
        occurredAt: task.scheduledEnd,
      );

      expect(result.activities.single.metadata,
          containsPair('pushesBeforeCompletion', 3));
      expect(result.task.stats.completedAfterPushCount, 1);
      expect(result.task.stats.totalPushesBeforeCompletion, 3);
    });

    test('duplicate operation ids do not create activities or mutate again',
        () {
      final task = scheduledTask(id: 'task-1', now: now);
      final first = service.apply(
        task: task,
        transitionCode: TaskTransitionCode.manualPush,
        operationId: 'op-push',
        activityId: 'activity-push',
        occurredAt: now,
      );

      final duplicate = service.apply(
        task: first.task,
        transitionCode: TaskTransitionCode.manualPush,
        operationId: 'op-push',
        activityId: 'activity-push-again',
        occurredAt: now.add(const Duration(minutes: 5)),
        existingActivities: first.activities,
      );

      expect(first.task.stats.manuallyPushedCount, 1);
      expect(first.activities.single.code, TaskActivityCode.manuallyPushed);
      expect(
          duplicate.outcomeCode, TaskTransitionOutcomeCode.duplicateOperation);
      expect(duplicate.duplicateOfActivityId, 'activity-push');
      expect(duplicate.activities, isEmpty);
      expect(duplicate.task.stats.manuallyPushedCount, 1);
    });

    test('terminal states no-op for same transition and reject reopening', () {
      final completed = scheduledTask(id: 'done', now: now).copyWith(
        status: TaskStatus.completed,
        completedAt: now,
      );

      final repeatComplete = service.apply(
        task: completed,
        transitionCode: TaskTransitionCode.complete,
        operationId: 'op-repeat',
        activityId: 'activity-repeat',
        occurredAt: now,
      );
      final activateCompleted = service.apply(
        task: completed,
        transitionCode: TaskTransitionCode.activate,
        operationId: 'op-activate',
        activityId: 'activity-activate',
        occurredAt: now,
      );

      expect(repeatComplete.outcomeCode, TaskTransitionOutcomeCode.noOp);
      expect(repeatComplete.task, completed);
      expect(repeatComplete.activities, isEmpty);
      expect(activateCompleted.outcomeCode,
          TaskTransitionOutcomeCode.invalidState);
      expect(activateCompleted.task.status, TaskStatus.completed);
    });

    test('critical missed records miss and backlog movement activities', () {
      final task = requiredTask(
        id: 'meds',
        type: TaskType.critical,
        now: now,
      );

      final result = service.apply(
        task: task,
        transitionCode: TaskTransitionCode.miss,
        operationId: 'op-missed',
        activityId: 'activity-missed',
        occurredAt: now,
      );

      expect(result.outcomeCode, TaskTransitionOutcomeCode.applied);
      expect(result.task.status, TaskStatus.backlog);
      expect(result.task.scheduledStart, isNull);
      expect(result.task.stats.missedCount, 1);
      expect(result.task.stats.movedToBacklogCount, 1);
      expect(result.activities.map((activity) => activity.code), [
        TaskActivityCode.missed,
        TaskActivityCode.movedToBacklog,
      ]);
    });

    test(
        'restore and activation use activity records without movement statuses',
        () {
      final backlog = Task.quickCapture(
        id: 'backlog',
        title: 'Backlog',
        createdAt: now,
      );

      final restored = service.apply(
        task: backlog,
        transitionCode: TaskTransitionCode.restoreFromBacklog,
        operationId: 'op-restore',
        activityId: 'activity-restore',
        occurredAt: now,
      );
      final activated = service.apply(
        task: restored.task,
        transitionCode: TaskTransitionCode.activate,
        operationId: 'op-activate',
        activityId: 'activity-activate',
        occurredAt: now,
      );

      expect(restored.task.status, TaskStatus.planned);
      expect(restored.task.stats.restoredFromBacklogCount, 1);
      expect(restored.activities.single.code,
          TaskActivityCode.restoredFromBacklog);
      expect(activated.task.status, TaskStatus.active);
      expect(activated.activities.single.code, TaskActivityCode.activated);
      expect(TaskStatus.values.map((status) => status.name),
          isNot(contains('pushed')));
    });
  });

  group('Task activity accounting service', () {
    final now = DateTime(2026, 6, 25, 9);
    const service = TaskActivityAccountingService();

    test('applies activity counters once by activity id', () {
      final task = scheduledTask(id: 'task-1', now: now);
      final activity = TaskActivity(
        id: 'activity-push',
        operationId: 'op-push',
        code: TaskActivityCode.manuallyPushed,
        taskId: task.id,
        projectId: task.projectId,
        occurredAt: now,
      );

      final first = service.apply(task: task, activities: [activity]);
      final replay = service.apply(
        task: first.task,
        activities: [activity],
        alreadyAppliedActivityIds: first.appliedActivityIds,
      );

      expect(first.task.stats.manuallyPushedCount, 1);
      expect(first.appliedActivityIds, ['activity-push']);
      expect(replay.task.stats.manuallyPushedCount, 1);
      expect(replay.appliedActivityIds, isEmpty);
    });
  });

  group('Action services use canonical transitions', () {
    final now = DateTime(2026, 6, 25, 9);

    test('flexible done returns completion activity', () {
      const service = FlexibleTaskActionService();
      final task = scheduledTask(id: 'flex', now: now);

      final result = service.apply(
        task: task,
        action: FlexibleTaskQuickAction.done,
        updatedAt: now,
        operationId: 'op-flex-done',
      );

      expect(result.transitionOutcomeCode, TaskTransitionOutcomeCode.applied);
      expect(result.task.completedAt, now);
      expect(result.activities.single.code, TaskActivityCode.completed);
      expect(result.activities.single.operationId, 'op-flex-done');
    });

    test('required missed returns canonical activities', () {
      const service = RequiredTaskActionService();
      final task = requiredTask(
        id: 'critical',
        type: TaskType.critical,
        now: now,
      );

      final result = service.apply(
        task: task,
        action: RequiredTaskAction.missed,
        updatedAt: now,
        operationId: 'op-required-missed',
      );

      expect(result.transitionOutcomeCode, TaskTransitionOutcomeCode.applied);
      expect(result.task.status, TaskStatus.backlog);
      expect(result.activities.map((activity) => activity.code), [
        TaskActivityCode.missed,
        TaskActivityCode.movedToBacklog,
      ]);
    });

    test('push destination returns movement activities for changed tasks', () {
      const service = FlexibleTaskActionService();
      final first = scheduledTask(id: 'first', now: now);
      final second = scheduledTask(
        id: 'second',
        now: now,
        start: DateTime(2026, 6, 25, 9, 20),
      );

      final result = service.applyPushDestination(
        destination: PushDestination.nextAvailableSlot,
        input: SchedulingInput(
          tasks: [first, second],
          window: SchedulingWindow(
            start: DateTime(2026, 6, 25, 9),
            end: DateTime(2026, 6, 25, 11),
          ),
        ),
        taskId: first.id,
        updatedAt: now,
        operationId: 'op-push',
      );

      expect(
          result.schedulingResult.outcomeCode, SchedulingOutcomeCode.success);
      expect(result.activities.map((activity) => activity.code), [
        TaskActivityCode.manuallyPushed,
        TaskActivityCode.automaticallyPushed,
      ]);
      expect(
          result.activities.map((activity) => activity.operationId).toSet(), {
        'op-push',
      });
    });

    test('duplicate push destination operation id returns no-op result', () {
      const service = FlexibleTaskActionService();
      final task = scheduledTask(id: 'first', now: now);
      final previousActivity = TaskActivity(
        id: 'activity-push',
        operationId: 'op-push',
        code: TaskActivityCode.manuallyPushed,
        taskId: task.id,
        projectId: task.projectId,
        occurredAt: now,
      );

      final result = service.applyPushDestination(
        destination: PushDestination.nextAvailableSlot,
        input: SchedulingInput(
          tasks: [task],
          window: SchedulingWindow(
            start: DateTime(2026, 6, 25, 9),
            end: DateTime(2026, 6, 25, 11),
          ),
        ),
        taskId: task.id,
        updatedAt: now,
        operationId: 'op-push',
        existingActivities: [previousActivity],
      );

      expect(result.schedulingResult.outcomeCode, SchedulingOutcomeCode.noOp);
      expect(result.schedulingResult.tasks.single, task);
      expect(result.activities, isEmpty);
    });

    test('surprise logging applies completion accounting', () {
      const service = SurpriseTaskLogService();
      final result = service.log(
        request: SurpriseTaskLogRequest(
          id: 'surprise',
          title: 'Unexpected call',
          createdAt: now,
          startedAt: DateTime(2026, 6, 25, 9, 10),
          timeUsedMinutes: 40,
        ),
        input: SchedulingInput(
          tasks: const [],
          window: SchedulingWindow(
            start: DateTime(2026, 6, 25, 9),
            end: DateTime(2026, 6, 25, 11),
          ),
          lockedIntervals: [
            TimeInterval(
              start: DateTime(2026, 6, 25, 9, 30),
              end: DateTime(2026, 6, 25, 10),
            ),
          ],
        ),
        updatedAt: now,
      );

      expect(result.activities.single.code, TaskActivityCode.completed);
      expect(result.surpriseTask.completedAt, now);
      expect(result.surpriseTask.stats.completedDuringLockedHoursCount, 1);
      expect(result.surpriseTask.stats.completedDuringLockedHoursMinutes, 20);
    });
  });
}

/// 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 DateTime now,
  DateTime? start,
  int durationMinutes = 15,
}) {
  final scheduledStart = start ?? DateTime(2026, 6, 25, 9);
  return Task(
    id: id,
    title: id,
    projectId: 'project',
    type: TaskType.flexible,
    status: TaskStatus.planned,
    priority: PriorityLevel.medium,
    durationMinutes: durationMinutes,
    scheduledStart: scheduledStart,
    scheduledEnd: scheduledStart.add(Duration(minutes: durationMinutes)),
    createdAt: now,
    updatedAt: now,
  );
}

/// Runs the `requiredTask` 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 requiredTask({
  required String id,
  required TaskType type,
  required DateTime now,
}) {
  return Task(
    id: id,
    title: id,
    projectId: 'required',
    type: type,
    status: TaskStatus.planned,
    priority: PriorityLevel.high,
    durationMinutes: 30,
    scheduledStart: DateTime(2026, 6, 25, 9),
    scheduledEnd: DateTime(2026, 6, 25, 9, 30),
    createdAt: now,
    updatedAt: now,
  );
}
