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

/// Tests Application Commands 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('V1 application commands', () {
    final day = CivilDate(2026, 6, 25);
    final tomorrow = CivilDate(2026, 6, 26);
    final createdAt = DateTime.utc(2026, 6, 20, 8);

    test('quick capture to backlog is atomic and idempotent by operation id',
        () async {
      final store = storeWithSettings();
      final useCases = commands(store);
      final context =
          appContext('capture-backlog', DateTime.utc(2026, 6, 25, 8));

      final result = await useCases.quickCaptureToBacklog(
        context: context,
        title: '  Write down idea  ',
      );
      final duplicate = await useCases.quickCaptureToBacklog(
        context: context,
        title: 'Write down idea again',
      );

      expect(result.isSuccess, isTrue);
      expect(result.requireValue.changedTasks.single.title, 'Write down idea');
      expect(
          result.requireValue.changedTasks.single.status, TaskStatus.backlog);
      expect(store.currentTasks, hasLength(1));
      expect(store.currentOperations.single.operationId, 'capture-backlog');
      expect(
          duplicate.failure!.code, ApplicationFailureCode.duplicateOperation);
      expect(store.currentTasks, hasLength(1));
    });

    test('quick capture to next available slot schedules through the app layer',
        () async {
      final store = storeWithSettings();

      final result = await commands(store).quickCaptureToNextAvailableSlot(
        context: appContext('capture-schedule', DateTime.utc(2026, 6, 25, 8)),
        localDate: day,
        title: 'Scheduled capture',
        durationMinutes: 30,
      );

      expect(result.isSuccess, isTrue);
      final captured = result.requireValue.changedTasks.single;
      expect(captured.status, TaskStatus.planned);
      expect(captured.scheduledStart, DateTime.utc(2026, 6, 25, 9));
      expect(captured.scheduledEnd, DateTime.utc(2026, 6, 25, 9, 30));
      expect(store.currentTaskActivities.single.code,
          TaskActivityCode.restoredFromBacklog);
    });

    test('schedules backlog item and persists movement activities', () async {
      final backlog = task(
        id: 'backlog',
        title: 'Backlog',
        type: TaskType.flexible,
        status: TaskStatus.backlog,
        createdAt: createdAt,
        durationMinutes: 30,
      );
      final planned = task(
        id: 'planned',
        title: 'Planned',
        type: TaskType.flexible,
        status: TaskStatus.planned,
        createdAt: createdAt,
        start: DateTime.utc(2026, 6, 25, 9),
        end: DateTime.utc(2026, 6, 25, 9, 30),
      );
      final store = storeWithSettings(tasks: [backlog, planned]);

      final result =
          await commands(store).scheduleBacklogItemToNextAvailableSlot(
        context: appContext('schedule-backlog', DateTime.utc(2026, 6, 25, 8)),
        localDate: day,
        taskId: backlog.id,
        expectedUpdatedAt: backlog.updatedAt,
      );

      expect(result.isSuccess, isTrue);
      expect(result.requireValue.changedTasks.map((task) => task.id).toSet(), {
        'backlog',
        'planned',
      });
      expect(
        taskById(store.currentTasks, 'backlog').status,
        TaskStatus.planned,
      );
      expect(
        taskById(store.currentTasks, 'backlog').scheduledStart,
        DateTime.utc(2026, 6, 25, 9),
      );
      expect(
        taskById(store.currentTasks, 'planned').scheduledStart,
        DateTime.utc(2026, 6, 25, 9, 30),
      );
      expect(store.currentTaskActivities.map((activity) => activity.code), [
        TaskActivityCode.restoredFromBacklog,
        TaskActivityCode.automaticallyPushed,
      ]);
    });

    test('schedules backlog item after duration is supplied', () async {
      final backlog = Task.quickCapture(
        id: 'backlog',
        title: 'Backlog',
        projectId: 'home',
        createdAt: createdAt,
        updatedAt: createdAt,
      );
      final store = storeWithSettings(tasks: [backlog]);

      final result =
          await commands(store).scheduleBacklogItemToNextAvailableSlot(
        context: appContext(
          'schedule-backlog-with-duration',
          DateTime.utc(2026, 6, 25, 8),
        ),
        localDate: day,
        taskId: backlog.id,
        durationMinutes: 25,
        expectedUpdatedAt: backlog.updatedAt,
      );

      expect(result.isSuccess, isTrue);
      final scheduled = taskById(store.currentTasks, 'backlog');
      expect(scheduled.durationMinutes, 25);
      expect(scheduled.status, TaskStatus.planned);
      expect(scheduled.scheduledStart, DateTime.utc(2026, 6, 25, 9));
      expect(scheduled.scheduledEnd, DateTime.utc(2026, 6, 25, 9, 25));
    });

    test('stale expected update leaves repositories unchanged', () async {
      final planned = task(
        id: 'planned',
        title: 'Planned',
        type: TaskType.flexible,
        status: TaskStatus.planned,
        createdAt: createdAt,
        start: DateTime.utc(2026, 6, 25, 9),
        end: DateTime.utc(2026, 6, 25, 9, 30),
      );
      final store = storeWithSettings(tasks: [planned]);

      final result = await commands(store).moveFlexibleToBacklog(
        context: appContext('stale-move', DateTime.utc(2026, 6, 25, 8)),
        taskId: planned.id,
        expectedUpdatedAt:
            planned.updatedAt.subtract(const Duration(minutes: 1)),
      );

      expect(result.failure!.code, ApplicationFailureCode.staleRevision);
      expect(
          taskById(store.currentTasks, planned.id).status, TaskStatus.planned);
      expect(store.currentOperations, isEmpty);
    });

    test('remove task deletes it from persistence', () async {
      final planned = task(
        id: 'remove-me',
        title: 'Remove me',
        type: TaskType.flexible,
        status: TaskStatus.planned,
        createdAt: createdAt,
        start: DateTime.utc(2026, 6, 25, 9),
        end: DateTime.utc(2026, 6, 25, 9, 30),
      );
      final store = storeWithSettings(tasks: [planned]);

      final result = await commands(store).removeTask(
        context: appContext('remove-task', DateTime.utc(2026, 6, 25, 8)),
        taskId: planned.id,
        expectedUpdatedAt: planned.updatedAt,
      );

      expect(result.isSuccess, isTrue);
      expect(store.currentTasks, isEmpty);
      expect(result.requireValue.changedTasks, isEmpty);
      expect(result.requireValue.readHint.affectedTaskIds, [planned.id]);
      expect(store.currentOperations.single.operationId, 'remove-task');
    });

    test('remove task rejects stale expected update', () async {
      final planned = task(
        id: 'stale-remove',
        title: 'Stale remove',
        type: TaskType.flexible,
        status: TaskStatus.planned,
        createdAt: createdAt,
        start: DateTime.utc(2026, 6, 25, 9),
        end: DateTime.utc(2026, 6, 25, 9, 30),
      );
      final store = storeWithSettings(tasks: [planned]);

      final result = await commands(store).removeTask(
        context: appContext('stale-remove', DateTime.utc(2026, 6, 25, 8)),
        taskId: planned.id,
        expectedUpdatedAt:
            planned.updatedAt.subtract(const Duration(minutes: 1)),
      );

      expect(result.failure!.code, ApplicationFailureCode.staleRevision);
      expect(taskById(store.currentTasks, planned.id), planned);
      expect(store.currentOperations, isEmpty);
    });

    test('commit persistence failure rolls back command-side task and activity',
        () async {
      final planned = task(
        id: 'planned',
        title: 'Planned',
        type: TaskType.flexible,
        status: TaskStatus.planned,
        createdAt: createdAt,
        start: DateTime.utc(2026, 6, 25, 9),
        end: DateTime.utc(2026, 6, 25, 9, 30),
      );
      final store = storeWithSettings(tasks: [planned])
        ..failNextCommitForTesting();

      final result = await commands(store).completeFlexibleTask(
        context: appContext('rollback-complete', DateTime.utc(2026, 6, 25, 9)),
        localDate: day,
        taskId: planned.id,
      );

      expect(result.failure!.code, ApplicationFailureCode.persistenceFailure);
      expect(
          taskById(store.currentTasks, planned.id).status, TaskStatus.planned);
      expect(store.currentTaskActivities, isEmpty);
      expect(store.currentProjectStatistics, isEmpty);
      expect(store.currentOperations, isEmpty);
    });

    test('push commands and backlog move persist expected command outputs',
        () async {
      final first = task(
        id: 'first',
        title: 'First',
        type: TaskType.flexible,
        status: TaskStatus.planned,
        createdAt: createdAt,
        start: DateTime.utc(2026, 6, 25, 9),
        end: DateTime.utc(2026, 6, 25, 9, 30),
      );
      final second = task(
        id: 'second',
        title: 'Second',
        type: TaskType.flexible,
        status: TaskStatus.planned,
        createdAt: createdAt,
        start: DateTime.utc(2026, 6, 25, 9, 30),
        end: DateTime.utc(2026, 6, 25, 10),
      );
      final tomorrowTask = task(
        id: 'tomorrow-existing',
        title: 'Tomorrow existing',
        type: TaskType.flexible,
        status: TaskStatus.planned,
        createdAt: createdAt,
        start: DateTime.utc(2026, 6, 26, 9),
        end: DateTime.utc(2026, 6, 26, 9, 30),
      );
      final store = storeWithSettings(tasks: [first, second, tomorrowTask]);
      final useCases = commands(store);

      final pushToday = await useCases.pushFlexibleToNextAvailableSlot(
        context: appContext('push-today', DateTime.utc(2026, 6, 25, 8)),
        localDate: day,
        taskId: first.id,
      );
      final pushTomorrow = await useCases.pushFlexibleToTomorrowTopOfQueue(
        context: appContext('push-tomorrow', DateTime.utc(2026, 6, 25, 8, 5)),
        targetDate: tomorrow,
        taskId: second.id,
      );
      final moveBacklog = await useCases.moveFlexibleToBacklog(
        context: appContext('move-backlog', DateTime.utc(2026, 6, 25, 8, 10)),
        taskId: first.id,
      );

      expect(pushToday.isSuccess, isTrue);
      expect(pushToday.requireValue.activities.first.code,
          TaskActivityCode.manuallyPushed);
      expect(pushTomorrow.isSuccess, isTrue);
      expect(
        taskById(store.currentTasks, second.id).scheduledStart,
        DateTime.utc(2026, 6, 26, 9),
      );
      expect(moveBacklog.isSuccess, isTrue);
      final movedBacklog = taskById(store.currentTasks, first.id);
      expect(movedBacklog.status, TaskStatus.backlog);
      expect(movedBacklog.backlogEnteredAt, DateTime.utc(2026, 6, 25, 8, 10));
      expect(
        movedBacklog.backlogEnteredAtProvenance,
        Task.backlogEnteredAtProvenanceMovedToBacklog,
      );
      expect(
        store.currentTaskActivities
            .map((activity) => activity.operationId)
            .toSet(),
        containsAll({'push-today', 'push-tomorrow', 'move-backlog'}),
      );
    });

    test('push next moves an overdue task after the command time', () async {
      final overdue = task(
        id: 'overdue',
        title: 'Overdue',
        type: TaskType.flexible,
        status: TaskStatus.planned,
        createdAt: createdAt,
        start: DateTime.utc(2026, 6, 24, 9),
        end: DateTime.utc(2026, 6, 24, 9, 30),
      );
      final afternoon = task(
        id: 'afternoon',
        title: 'Afternoon',
        type: TaskType.flexible,
        status: TaskStatus.planned,
        createdAt: createdAt,
        start: DateTime.utc(2026, 6, 25, 13),
        end: DateTime.utc(2026, 6, 25, 13, 30),
      );
      final store = storeWithSettings(tasks: [overdue, afternoon]);

      final result = await commands(store).pushFlexibleToNextAvailableSlot(
        context: appContext('push-overdue', DateTime.utc(2026, 6, 25, 12)),
        localDate: day,
        taskId: overdue.id,
      );

      expect(result.isSuccess, isTrue);
      final pushed = taskById(store.currentTasks, overdue.id);
      expect(pushed.scheduledStart, DateTime.utc(2026, 6, 25, 12));
      expect(pushed.scheduledEnd, DateTime.utc(2026, 6, 25, 12, 30));
      expect(pushed.stats.manuallyPushedCount, 1);
      expect(taskById(store.currentTasks, afternoon.id).scheduledStart,
          DateTime.utc(2026, 6, 25, 13));
      expect(
        store.currentTaskActivities
            .where((activity) => activity.operationId == 'push-overdue')
            .map((activity) => activity.code),
        [TaskActivityCode.manuallyPushed],
      );
    });

    test('completion commands persist activities and project statistics',
        () async {
      final flexible = task(
        id: 'flexible',
        title: 'Flexible',
        type: TaskType.flexible,
        status: TaskStatus.planned,
        createdAt: createdAt,
        start: DateTime.utc(2026, 6, 25, 9),
        end: DateTime.utc(2026, 6, 25, 9, 30),
      );
      final critical = task(
        id: 'critical',
        title: 'Critical',
        type: TaskType.critical,
        status: TaskStatus.planned,
        createdAt: createdAt,
        start: DateTime.utc(2026, 6, 25, 10),
        end: DateTime.utc(2026, 6, 25, 10, 30),
      );
      final store = storeWithSettings(tasks: [flexible, critical]);
      final useCases = commands(store);

      final done = await useCases.completeFlexibleTask(
        context:
            appContext('complete-flexible', DateTime.utc(2026, 6, 25, 9, 20)),
        localDate: day,
        taskId: flexible.id,
        actualStart: DateTime.utc(2026, 6, 25, 9),
        actualEnd: DateTime.utc(2026, 6, 25, 9, 20),
      );
      final missed = await useCases.applyRequiredTaskAction(
        context: appContext('miss-critical', DateTime.utc(2026, 6, 25, 10, 45)),
        localDate: day,
        taskId: critical.id,
        action: RequiredTaskAction.missed,
      );

      expect(done.isSuccess, isTrue);
      expect(taskById(store.currentTasks, flexible.id).status,
          TaskStatus.completed);
      expect(missed.isSuccess, isTrue);
      expect(
          taskById(store.currentTasks, critical.id).status, TaskStatus.backlog);
      expect(
        store.currentTaskActivities.map((activity) => activity.code).toSet(),
        {
          TaskActivityCode.completed,
          TaskActivityCode.missed,
          TaskActivityCode.movedToBacklog,
        },
      );
      expect(store.currentProjectStatistics.single.completedTaskCount, 1);
    });

    test('uncomplete task clears completion while preserving schedule',
        () async {
      final completed = task(
        id: 'completed',
        title: 'Completed',
        type: TaskType.flexible,
        status: TaskStatus.completed,
        createdAt: createdAt,
        start: DateTime.utc(2026, 6, 25, 9),
        end: DateTime.utc(2026, 6, 25, 9, 30),
        actualStart: DateTime.utc(2026, 6, 25, 9),
        actualEnd: DateTime.utc(2026, 6, 25, 9, 20),
        completedAt: DateTime.utc(2026, 6, 25, 9, 20),
      );
      final store = storeWithSettings(tasks: [completed]);

      final result = await commands(store).uncompleteTask(
        context: appContext(
          'uncomplete-task',
          DateTime.utc(2026, 6, 25, 9, 25),
        ),
        localDate: day,
        taskId: completed.id,
      );

      expect(result.isSuccess, isTrue);
      final reopened = taskById(store.currentTasks, completed.id);
      expect(reopened.status, TaskStatus.planned);
      expect(reopened.scheduledStart, completed.scheduledStart);
      expect(reopened.scheduledEnd, completed.scheduledEnd);
      expect(reopened.actualStart, isNull);
      expect(reopened.actualEnd, isNull);
      expect(reopened.completedAt, isNull);
      expect(reopened.updatedAt, DateTime.utc(2026, 6, 25, 9, 25));
      expect(store.currentTaskActivities, isEmpty);
    });

    test('surprise logging creates completed work and repairs flexible overlap',
        () async {
      final planned = task(
        id: 'planned',
        title: 'Planned',
        type: TaskType.flexible,
        status: TaskStatus.planned,
        createdAt: createdAt,
        start: DateTime.utc(2026, 6, 25, 9),
        end: DateTime.utc(2026, 6, 25, 10),
      );
      final store = storeWithSettings(tasks: [planned]);

      final result = await commands(store).logSurpriseTask(
        context: appContext('surprise-op', DateTime.utc(2026, 6, 25, 9, 30)),
        localDate: day,
        title: 'Unexpected call',
        startedAt: DateTime.utc(2026, 6, 25, 9),
        timeUsedMinutes: 30,
      );

      expect(result.isSuccess, isTrue);
      expect(taskById(store.currentTasks, 'surprise-op').status,
          TaskStatus.completed);
      expect(
        taskById(store.currentTasks, planned.id).scheduledStart,
        isNot(DateTime.utc(2026, 6, 25, 9)),
      );
      expect(
          store.currentTaskActivities.single.code, TaskActivityCode.completed);
      expect(store.currentProjectStatistics.single.completedTaskCount, 1);
    });

    test('protected Free Slot create update and remove are atomic commands',
        () async {
      final store = storeWithSettings();
      final useCases = commands(store);
      final create = await useCases.createProtectedFreeSlot(
        context: appContext('free-create', DateTime.utc(2026, 6, 25, 8)),
        localDate: day,
        title: 'Rest',
        start: DateTime.utc(2026, 6, 25, 10),
        end: DateTime.utc(2026, 6, 25, 11),
      );
      final freeSlotId = create.requireValue.changedTasks.single.id;
      final update = await useCases.updateProtectedFreeSlot(
        context: appContext('free-update', DateTime.utc(2026, 6, 25, 8, 5)),
        localDate: day,
        taskId: freeSlotId,
        title: 'Quiet rest',
        start: DateTime.utc(2026, 6, 25, 11),
        end: DateTime.utc(2026, 6, 25, 12),
      );
      final remove = await useCases.removeProtectedFreeSlot(
        context: appContext('free-remove', DateTime.utc(2026, 6, 25, 8, 10)),
        localDate: day,
        taskId: freeSlotId,
      );

      expect(create.isSuccess, isTrue);
      expect(update.isSuccess, isTrue);
      expect(taskById(store.currentTasks, freeSlotId).title, 'Quiet rest');
      expect(remove.isSuccess, isTrue);
      final removed = taskById(store.currentTasks, freeSlotId);
      expect(removed.status, TaskStatus.cancelled);
      expect(removed.scheduledStart, isNull);
      expect(store.currentOperations.map((operation) => operation.operationId),
          ['free-create', 'free-update', 'free-remove']);
    });

    test('child task commands create children and propagate completion',
        () async {
      final parent = task(
        id: 'parent',
        title: 'Parent',
        type: TaskType.flexible,
        status: TaskStatus.planned,
        createdAt: createdAt,
        start: DateTime.utc(2026, 6, 25, 9),
        end: DateTime.utc(2026, 6, 25, 10),
      );
      final child = task(
        id: 'child',
        title: 'Child',
        type: TaskType.flexible,
        status: TaskStatus.planned,
        createdAt: createdAt,
        parentTaskId: parent.id,
      );
      final sibling = task(
        id: 'sibling',
        title: 'Sibling',
        type: TaskType.flexible,
        status: TaskStatus.planned,
        createdAt: createdAt,
        parentTaskId: parent.id,
      );
      final store = storeWithSettings(tasks: [parent, child, sibling]);
      final useCases = commands(store);

      final breakUp = await useCases.breakUpTask(
        context: appContext('break-up', DateTime.utc(2026, 6, 25, 8)),
        parentTaskId: parent.id,
        children: const [
          ApplicationChildTaskDraft(title: 'New child'),
        ],
      );
      final completeParent = await useCases.completeParentTask(
        context:
            appContext('complete-parent', DateTime.utc(2026, 6, 25, 8, 30)),
        parentTaskId: parent.id,
      );

      expect(breakUp.isSuccess, isTrue);
      expect(breakUp.requireValue.childTaskIds, hasLength(1));
      expect(
        taskById(store.currentTasks, breakUp.requireValue.childTaskIds.single)
            .parentTaskId,
        parent.id,
      );
      expect(completeParent.isSuccess, isTrue);
      expect(
          taskById(store.currentTasks, parent.id).status, TaskStatus.completed);
      expect(
          taskById(store.currentTasks, child.id).status, TaskStatus.completed);
      expect(taskById(store.currentTasks, sibling.id).status,
          TaskStatus.completed);
      expect(store.currentProjectStatistics.single.completedTaskCount, 3);
    });

    test('complete child and complete parent from child commands are exposed',
        () async {
      final parent = task(
        id: 'parent',
        title: 'Parent',
        type: TaskType.flexible,
        status: TaskStatus.planned,
        createdAt: createdAt,
      );
      final child = task(
        id: 'child',
        title: 'Child',
        type: TaskType.flexible,
        status: TaskStatus.planned,
        createdAt: createdAt,
        parentTaskId: parent.id,
      );
      final store = storeWithSettings(tasks: [parent, child]);
      final useCases = commands(store);

      final completeChild = await useCases.completeChildTask(
        context: appContext('complete-child', DateTime.utc(2026, 6, 25, 8)),
        childTaskId: child.id,
      );
      final secondStore = storeWithSettings(tasks: [parent, child]);
      final completeFromChild =
          await commands(secondStore).completeParentFromChild(
        context: appContext(
            'complete-parent-from-child', DateTime.utc(2026, 6, 25, 8)),
        childTaskId: child.id,
      );

      expect(completeChild.isSuccess, isTrue);
      expect(
          taskById(store.currentTasks, parent.id).status, TaskStatus.completed);
      expect(completeFromChild.isSuccess, isTrue);
      expect(taskById(secondStore.currentTasks, parent.id).status,
          TaskStatus.completed);
      expect(taskById(secondStore.currentTasks, child.id).status,
          TaskStatus.completed);
    });
  });
}

/// Runs the `commands` top-level operation.
/// This function is part of the file-level API and gives callers a direct way to use the associated scheduler behavior.
V1ApplicationCommandUseCases commands(InMemoryApplicationUnitOfWork store) {
  return V1ApplicationCommandUseCases(
    applicationStore: store,
    timeZoneResolver: const FixedOffsetTimeZoneResolver.utc(),
  );
}

/// Runs the `storeWithSettings` top-level operation.
/// This function is part of the file-level API and gives callers a direct way to use the associated scheduler behavior.
InMemoryApplicationUnitOfWork storeWithSettings({
  List<Task> tasks = const <Task>[],
}) {
  return InMemoryApplicationUnitOfWork(
    initialTasks: tasks,
    initialOwnerSettings: [
      OwnerSettings(
        ownerId: 'owner-1',
        timeZoneId: 'UTC',
        dayStart: WallTime(hour: 9, minute: 0),
        dayEnd: WallTime(hour: 17, minute: 0),
      ),
    ],
  );
}

/// Runs the `appContext` top-level operation.
/// This function is part of the file-level API and gives callers a direct way to use the associated scheduler behavior.
ApplicationOperationContext appContext(String operationId, DateTime now) {
  return ApplicationOperationContext.start(
    operationId: operationId,
    ownerTimeZone: OwnerTimeZoneContext(ownerId: 'owner-1', timeZoneId: 'UTC'),
    clock: FixedClock(now),
    idGenerator: SequentialIdGenerator(prefix: '$operationId-task'),
  );
}

/// 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 TaskType type,
  required TaskStatus status,
  required DateTime createdAt,
  DateTime? start,
  DateTime? end,
  DateTime? actualStart,
  DateTime? actualEnd,
  DateTime? completedAt,
  int? durationMinutes,
  String projectId = 'home',
  String? parentTaskId,
}) {
  return Task(
    id: id,
    title: title,
    projectId: projectId,
    type: type,
    status: status,
    durationMinutes: durationMinutes ??
        (start != null && end != null ? end.difference(start).inMinutes : 30),
    scheduledStart: start,
    scheduledEnd: end,
    actualStart: actualStart,
    actualEnd: actualEnd,
    completedAt: completedAt,
    parentTaskId: parentTaskId,
    createdAt: createdAt,
    updatedAt: createdAt,
  );
}

/// 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 taskId) {
  return tasks.singleWhere((task) => task.id == taskId);
}
