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

/// Tests Application Recovery 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('V1AppOpenRecoveryUseCases', () {
    final sourceDay = CivilDate(2026, 6, 25);
    final targetDay = CivilDate(2026, 6, 26);
    final createdAt = DateTime.utc(2026, 6, 20, 8);

    test('rolls source-day flexible work once and exposes notice lifecycle',
        () async {
      final targetExisting = task(
        id: 'target-existing',
        title: 'Target 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: [targetExisting]);
      final commandUseCases = commands(store);
      final recoveryUseCases = recovery(store);
      final managementUseCases =
          V1ApplicationManagementUseCases(applicationStore: store);
      final todayQuery = today(store);

      final capture = await commandUseCases.quickCaptureToNextAvailableSlot(
        context: appContext('capture-source', DateTime.utc(2026, 6, 25, 8)),
        localDate: sourceDay,
        title: 'Captured source task',
        durationMinutes: 30,
      );
      expect(capture.isSuccess, isTrue);
      final capturedTaskId = capture.requireValue.changedTasks.single.id;

      final firstOpen = await recoveryUseCases.runAppOpenRecovery(
        RunAppOpenRecoveryRequest(
          context: appContext('open-next-day', DateTime.utc(2026, 6, 26, 8)),
          sourceDate: sourceDay,
          targetDate: targetDay,
        ),
      );

      expect(firstOpen.requireValue.outcome, AppOpenRecoveryOutcome.rolledOver);
      expect(firstOpen.requireValue.changedTasks.map((task) => task.id), [
        capturedTaskId,
        'target-existing',
      ]);
      expect(taskById(store.currentTasks, capturedTaskId).status,
          TaskStatus.planned);
      expect(taskById(store.currentTasks, capturedTaskId).scheduledStart,
          DateTime.utc(2026, 6, 26, 9));
      expect(taskById(store.currentTasks, 'target-existing').scheduledStart,
          DateTime.utc(2026, 6, 26, 9, 30));
      expect(
          store.currentSchedulingSnapshots.single.notices.single.movementCode,
          SchedulingMovementCode.unfinishedFlexibleTasksRolledOver);
      expect(
          store.currentSchedulingSnapshots.single.notices.single
              .parameters['count'],
          1);

      final todayBeforeAck = (await todayQuery.execute(
        GetTodayStateRequest(
          context: appContext('today-before-ack', DateTime.utc(2026, 6, 26, 8)),
          date: targetDay,
        ),
      ))
          .requireValue;
      expect(todayBeforeAck.pendingNotices.single.parameters['count'], 1);

      await managementUseCases.acknowledgeNotice(
        context: appContext('ack-rollover', DateTime.utc(2026, 6, 26, 8, 5)),
        noticeId: todayBeforeAck.pendingNotices.single.noticeId,
      );
      final todayAfterAck = (await todayQuery.execute(
        GetTodayStateRequest(
          context:
              appContext('today-after-ack', DateTime.utc(2026, 6, 26, 8, 6)),
          date: targetDay,
        ),
      ))
          .requireValue;
      final secondOpen = await recoveryUseCases.runAppOpenRecovery(
        RunAppOpenRecoveryRequest(
          context:
              appContext('open-next-day-again', DateTime.utc(2026, 6, 26, 9)),
          sourceDate: sourceDay,
          targetDate: targetDay,
        ),
      );

      expect(todayAfterAck.pendingNotices, isEmpty);
      expect(
        secondOpen.requireValue.outcome,
        AppOpenRecoveryOutcome.alreadyProcessed,
      );
      expect(secondOpen.requireValue.changedTasks, isEmpty);
      expect(store.currentSchedulingSnapshots, hasLength(1));
      expect(
        store.currentOperations
            .map((operation) => operation.operationId)
            .toSet(),
        containsAll({
          'capture-source',
          'open-next-day',
          'ack-rollover',
          'open-next-day-again',
        }),
      );
    });

    test('records deterministic no-op without creating a notice', () async {
      final completed = task(
        id: 'done',
        title: 'Done',
        type: TaskType.flexible,
        status: TaskStatus.completed,
        createdAt: createdAt,
        start: DateTime.utc(2026, 6, 25, 10),
        end: DateTime.utc(2026, 6, 25, 10, 30),
      );
      final store = storeWithSettings(tasks: [completed]);

      final result = await recovery(store).runAppOpenRecovery(
        RunAppOpenRecoveryRequest(
          context: appContext('open-noop', DateTime.utc(2026, 6, 26, 8)),
          sourceDate: sourceDay,
          targetDate: targetDay,
        ),
      );
      final duplicate = await recovery(store).runAppOpenRecovery(
        RunAppOpenRecoveryRequest(
          context: appContext('open-noop-again', DateTime.utc(2026, 6, 26, 9)),
          sourceDate: sourceDay,
          targetDate: targetDay,
        ),
      );

      expect(
        result.requireValue.outcome,
        AppOpenRecoveryOutcome.noUnfinishedFlexibleTasks,
      );
      expect(result.requireValue.notices, isEmpty);
      expect(store.currentSchedulingSnapshots.single.notices, isEmpty);
      expect(duplicate.requireValue.outcome,
          AppOpenRecoveryOutcome.alreadyProcessed);
      expect(store.currentSchedulingSnapshots, hasLength(1));
    });

    test('does not pull future-day tasks into source-day rollover', () async {
      final sourceTask = task(
        id: 'source',
        title: 'Source',
        type: TaskType.flexible,
        status: TaskStatus.planned,
        createdAt: createdAt,
        start: DateTime.utc(2026, 6, 25, 10),
        end: DateTime.utc(2026, 6, 25, 10, 30),
      );
      final futureTask = task(
        id: 'future',
        title: 'Future',
        type: TaskType.flexible,
        status: TaskStatus.planned,
        createdAt: createdAt,
        start: DateTime.utc(2026, 6, 27, 9),
        end: DateTime.utc(2026, 6, 27, 9, 30),
      );
      final store = storeWithSettings(tasks: [sourceTask, futureTask]);

      final result = await recovery(store).runAppOpenRecovery(
        RunAppOpenRecoveryRequest(
          context: appContext('open-future-safe', DateTime.utc(2026, 6, 26, 8)),
          sourceDate: sourceDay,
          targetDate: targetDay,
        ),
      );

      expect(result.isSuccess, isTrue);
      expect(taskById(store.currentTasks, sourceTask.id).scheduledStart,
          DateTime.utc(2026, 6, 26, 9));
      expect(taskById(store.currentTasks, futureTask.id).scheduledStart,
          DateTime.utc(2026, 6, 27, 9));
      expect(
        result.requireValue.changedTasks.map((task) => task.id),
        isNot(contains('future')),
      );
    });
  });
}

/// 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 `recovery` top-level operation.
/// This function is part of the file-level API and gives callers a direct way to use the associated scheduler behavior.
V1AppOpenRecoveryUseCases recovery(InMemoryApplicationUnitOfWork store) {
  return V1AppOpenRecoveryUseCases(
    applicationStore: store,
    timeZoneResolver: const FixedOffsetTimeZoneResolver.utc(),
  );
}

/// Runs the `today` top-level operation.
/// This function is part of the file-level API and gives callers a direct way to use the associated scheduler behavior.
GetTodayStateQuery today(InMemoryApplicationUnitOfWork store) {
  return GetTodayStateQuery(
    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-id'),
  );
}

/// 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,
}) {
  return Task(
    id: id,
    title: title,
    projectId: 'home',
    type: type,
    status: status,
    durationMinutes:
        start != null && end != null ? end.difference(start).inMinutes : 30,
    scheduledStart: start,
    scheduledEnd: end,
    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);
}
