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

/// Tests Application Layer 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('Application unit of work', () {
    final now = DateTime.utc(2026, 6, 25, 16);
    final context = appContext(operationId: 'op-1', now: now);

    test(
        'commits task, activity, aggregate, settings, and operation atomically',
        () async {
      final original = task(
        id: 'task-1',
        status: TaskStatus.planned,
        createdAt: now,
      );
      final unitOfWork = InMemoryApplicationUnitOfWork(
        initialTasks: [original],
      );

      final result = await unitOfWork.run<String>(
        context: context,
        operationName: 'complete-task',
        action: (repositories) async {
          final loaded = await repositories.tasks.findById(original.id);
          final completed = loaded!.copyWith(
            status: TaskStatus.completed,
            completedAt: now,
            updatedAt: now,
          );
          final activity = activityFor(
            id: 'activity-1',
            operationId: context.operationId,
            task: completed,
            occurredAt: now,
          );

          await repositories.tasks.save(completed);
          await repositories.taskActivities.save(activity);
          await repositories.projectStatistics.save(
            ProjectStatistics(projectId: completed.projectId)
                .recordCompletion(completedAt: now, durationMinutes: 30),
          );
          await repositories.ownerSettings.save(
            OwnerSettings(
                ownerId: 'owner-1', timeZoneId: 'America/Los_Angeles'),
          );

          return ApplicationResult.success(completed.id);
        },
      );

      expect(result.isSuccess, isTrue);
      expect(result.requireValue, 'task-1');
      expect(unitOfWork.currentTasks.single.status, TaskStatus.completed);
      expect(unitOfWork.currentTaskActivities.single.id, 'activity-1');
      expect(
        unitOfWork.currentProjectStatistics.single.completedTaskCount,
        1,
      );
      expect(unitOfWork.currentOwnerSettings.single.ownerId, 'owner-1');
      expect(unitOfWork.currentOperations.single.operationId, 'op-1');
      expect(
          unitOfWork.currentOperations.single.operationName, 'complete-task');
    });

    test('rolls back staged changes when the action returns a typed failure',
        () async {
      final original = task(
        id: 'task-1',
        status: TaskStatus.planned,
        createdAt: now,
      );
      final unitOfWork = InMemoryApplicationUnitOfWork(
        initialTasks: [original],
      );

      final result = await unitOfWork.run<void>(
        context: context,
        operationName: 'complete-task',
        action: (repositories) async {
          await repositories.tasks.save(
            original.copyWith(status: TaskStatus.completed, updatedAt: now),
          );
          await repositories.taskActivities.save(
            activityFor(
              id: 'activity-1',
              operationId: context.operationId,
              task: original,
              occurredAt: now,
            ),
          );

          return ApplicationResult.failure(
            const ApplicationFailure(
              code: ApplicationFailureCode.conflict,
              entityId: 'task-1',
            ),
          );
        },
      );

      expect(result.failure!.code, ApplicationFailureCode.conflict);
      expect(unitOfWork.currentTasks.single.status, TaskStatus.planned);
      expect(unitOfWork.currentTaskActivities, isEmpty);
      expect(unitOfWork.currentOperations, isEmpty);
    });

    test('rolls back staged changes when commit persistence fails', () async {
      final original = task(
        id: 'task-1',
        status: TaskStatus.planned,
        createdAt: now,
      );
      final unitOfWork = InMemoryApplicationUnitOfWork(
        initialTasks: [original],
      )..failNextCommitForTesting();

      final result = await unitOfWork.run<void>(
        context: context,
        operationName: 'complete-task',
        action: (repositories) async {
          await repositories.tasks.save(
            original.copyWith(status: TaskStatus.completed, updatedAt: now),
          );
          return ApplicationResult.success(null);
        },
      );

      expect(result.failure!.code, ApplicationFailureCode.persistenceFailure);
      expect(unitOfWork.currentTasks.single.status, TaskStatus.planned);
      expect(unitOfWork.currentOperations, isEmpty);
    });

    test('rejects duplicate operation IDs without running the action',
        () async {
      final unitOfWork = InMemoryApplicationUnitOfWork();
      var runCount = 0;

      final first = await unitOfWork.run<String>(
        context: context,
        operationName: 'capture',
        action: (_) async {
          runCount += 1;
          return ApplicationResult.success('done');
        },
      );
      final duplicate = await unitOfWork.run<String>(
        context: context,
        operationName: 'capture',
        action: (_) async {
          runCount += 1;
          return ApplicationResult.success('duplicate');
        },
      );

      expect(first.requireValue, 'done');
      expect(
          duplicate.failure!.code, ApplicationFailureCode.duplicateOperation);
      expect(duplicate.failure!.entityId, 'op-1');
      expect(runCount, 1);
      expect(unitOfWork.currentOperations, hasLength(1));
    });

    test('maps domain validation exceptions to stable validation failures',
        () async {
      final unitOfWork = InMemoryApplicationUnitOfWork();

      final result = await unitOfWork.run<void>(
        context: context,
        operationName: 'bad-task',
        action: (_) async {
          Task(
            id: '',
            title: 'Bad task',
            projectId: 'inbox',
            type: TaskType.flexible,
            status: TaskStatus.backlog,
            createdAt: now,
            updatedAt: now,
          );
          return ApplicationResult.success(null);
        },
      );

      expect(result.failure!.code, ApplicationFailureCode.validation);
      expect(
          result.failure!.detailCode, DomainValidationCode.blankStableId.name);
      expect(unitOfWork.currentOperations, isEmpty);
    });

    test(
        'repository contracts expose activity settings notice and operation '
        'conflicts', () async {
      final unitOfWork = InMemoryApplicationUnitOfWork(
        initialOwnerSettings: [
          OwnerSettings(ownerId: 'owner-1', timeZoneId: 'America/Los_Angeles'),
        ],
      );

      final result = await unitOfWork.run<void>(
        context: appContext(operationId: 'repo-contracts', now: now),
        operationName: 'repo-contracts',
        action: (repositories) async {
          final activity = activityFor(
            id: 'activity-1',
            operationId: 'manual-op',
            task: task(
              id: 'task-1',
              status: TaskStatus.completed,
              createdAt: now,
            ),
            occurredAt: now,
          );
          final appended = await repositories.taskActivities.append(
            activity: activity,
            ownerId: 'owner-1',
          );
          final duplicateActivity = await repositories.taskActivities.append(
            activity: activity,
            ownerId: 'owner-1',
          );
          final activities = await repositories.taskActivities.findByOwner(
            ownerId: 'owner-1',
          );

          final settingsRecord =
              await repositories.ownerSettings.findRecordByOwnerId('owner-1');
          final staleSettings = await repositories.ownerSettings.saveIfRevision(
            settings: settingsRecord!.value.copyWith(compactModeEnabled: true),
            expectedRevision: 99,
          );
          final savedSettings = await repositories.ownerSettings.saveIfRevision(
            settings: settingsRecord.value.copyWith(compactModeEnabled: true),
            expectedRevision: settingsRecord.revision,
          );

          final acknowledgement = NoticeAcknowledgementRecord(
            noticeId: 'snapshot:0',
            ownerId: 'owner-1',
            acknowledgedAt: now,
          );
          final insertedNotice =
              await repositories.noticeAcknowledgements.insert(acknowledgement);
          final duplicateNotice =
              await repositories.noticeAcknowledgements.insert(acknowledgement);
          final notices = await repositories.noticeAcknowledgements.findByOwner(
            ownerId: 'owner-1',
          );

          final operation = ApplicationOperationRecord(
            operationId: 'manual-op',
            ownerId: 'owner-1',
            operationName: 'manual',
            committedAt: now,
          );
          final insertedOperation =
              await repositories.operations.insertIfAbsent(operation);
          final duplicateOperation =
              await repositories.operations.insertIfAbsent(operation);
          final idempotentOperation =
              await repositories.operations.findByIdempotencyKey(
            ownerId: 'owner-1',
            operationId: 'manual-op',
          );

          expect(appended.isSuccess, isTrue);
          expect(
            duplicateActivity.failure!.code,
            RepositoryFailureCode.duplicateId,
          );
          expect(activities.items.map((activity) => activity.id), [
            'activity-1',
          ]);
          expect(
            staleSettings.failure!.code,
            RepositoryFailureCode.staleRevision,
          );
          expect(savedSettings.revision, 2);
          expect(insertedNotice.isSuccess, isTrue);
          expect(
            duplicateNotice.failure!.code,
            RepositoryFailureCode.duplicateId,
          );
          expect(notices.items.map((notice) => notice.noticeId), [
            'snapshot:0',
          ]);
          expect(insertedOperation.isSuccess, isTrue);
          expect(
            duplicateOperation.failure!.code,
            RepositoryFailureCode.duplicateOperation,
          );
          expect(idempotentOperation!.operationName, 'manual');
          return ApplicationResult.success(null);
        },
      );

      expect(result.isSuccess, isTrue);
      expect(unitOfWork.currentTaskActivities.single.id, 'activity-1');
      expect(unitOfWork.currentOwnerSettings.single.compactModeEnabled, isTrue);
      expect(unitOfWork.currentNoticeAcknowledgements.single.noticeId,
          'snapshot:0');
      expect(
        unitOfWork.currentOperations.map((operation) => operation.operationId),
        containsAll(['manual-op', 'repo-contracts']),
      );
    });
  });

  group('Application scheduling loader', () {
    test('centralizes repository loading into scheduler input', () async {
      final now = DateTime.utc(2026, 6, 25, 8);
      final inWindow = task(
        id: 'in-window',
        status: TaskStatus.planned,
        createdAt: now,
        scheduledStart: DateTime.utc(2026, 6, 25, 10),
        scheduledEnd: DateTime.utc(2026, 6, 25, 10, 30),
      );
      final outsideWindow = task(
        id: 'outside-window',
        status: TaskStatus.planned,
        createdAt: now,
        scheduledStart: DateTime.utc(2026, 6, 26, 10),
        scheduledEnd: DateTime.utc(2026, 6, 26, 10, 30),
      );
      final unitOfWork = InMemoryApplicationUnitOfWork(
        initialTasks: [inWindow, outsideWindow],
      );
      late SchedulingInput input;

      final result = await unitOfWork.run<void>(
        context: appContext(operationId: 'load', now: now),
        operationName: 'load-input',
        action: (repositories) async {
          input = await ApplicationSchedulingLoader(repositories)
              .loadInputForWindow(
            window: SchedulingWindow(
              start: DateTime.utc(2026, 6, 25, 0),
              end: DateTime.utc(2026, 6, 26, 0),
            ),
            lockedIntervals: [
              TimeInterval(
                start: DateTime.utc(2026, 6, 25, 12),
                end: DateTime.utc(2026, 6, 25, 13),
              ),
            ],
          );
          return ApplicationResult.success(null);
        },
      );

      expect(result.isSuccess, isTrue);
      expect(input.tasks.map((task) => task.id), ['in-window']);
      expect(input.lockedIntervals, hasLength(1));
    });
  });
}

/// 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({
  required String operationId,
  required DateTime now,
}) {
  return ApplicationOperationContext.start(
    operationId: operationId,
    ownerTimeZone: OwnerTimeZoneContext(
      ownerId: 'owner-1',
      timeZoneId: 'America/Los_Angeles',
    ),
    clock: FixedClock(now),
    idGenerator: SequentialIdGenerator(prefix: 'generated'),
  );
}

/// 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 TaskStatus status,
  required DateTime createdAt,
  DateTime? scheduledStart,
  DateTime? scheduledEnd,
}) {
  return Task(
    id: id,
    title: 'Task $id',
    projectId: 'inbox',
    type: TaskType.flexible,
    status: status,
    durationMinutes: 30,
    scheduledStart: scheduledStart,
    scheduledEnd: scheduledEnd,
    createdAt: createdAt,
    updatedAt: createdAt,
  );
}

/// Runs the `activityFor` top-level operation.
/// This function is part of the file-level API and gives callers a direct way to use the associated scheduler behavior.
TaskActivity activityFor({
  required String id,
  required String operationId,
  required Task task,
  required DateTime occurredAt,
}) {
  return TaskActivity(
    id: id,
    operationId: operationId,
    code: TaskActivityCode.completed,
    taskId: task.id,
    projectId: task.projectId,
    occurredAt: occurredAt,
  );
}
