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

/// Tests Persistence Edge Cases behavior for the Scheduler Core package.
library;

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

/// Runs the `values` 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('Persistence edge-case regressions', () {
    final createdAt = DateTime.utc(2026, 6, 22, 8);

    test('task ids remain stable across copy and update operations', () {
      final original = scheduledTask(
        id: 'task-1',
        createdAt: createdAt,
      );

      final updated = original.copyWith(
        title: 'Updated title',
        status: TaskStatus.active,
        updatedAt: createdAt.add(const Duration(minutes: 5)),
      );

      expect(updated.id, original.id);
      expect(updated.title, 'Updated title');
      expect(updated.status, TaskStatus.active);
    });

    test('project ids remain stable across copy and update operations', () {
      final original = ProjectProfile(
        id: 'project-1',
        name: 'Project',
        colorKey: 'project-blue',
      );

      final updated = original.copyWith(
        name: 'Renamed project',
        colorKey: 'project-green',
      );

      expect(updated.id, original.id);
      expect(updated.name, 'Renamed project');
      expect(updated.colorKey, 'project-green');
    });

    test('locked block ids remain stable across copy and update operations',
        () {
      final original = LockedBlock(
        id: 'locked-1',
        name: 'Work',
        startTime: ClockTime(hour: 9, minute: 0),
        endTime: ClockTime(hour: 17, minute: 0),
        recurrence: LockedBlockRecurrence.weekly(
          weekdays: {LockedWeekday.monday},
        ),
        createdAt: createdAt,
        updatedAt: createdAt,
      );

      final updated = original.copyWith(
        name: 'Adjusted work',
        hiddenByDefault: false,
        updatedAt: createdAt.add(const Duration(minutes: 10)),
      );

      expect(updated.id, original.id);
      expect(updated.name, 'Adjusted work');
      expect(updated.hiddenByDefault, isFalse);
    });

    test('DateTime persistence uses UTC ISO-8601 strings and UTC comparison',
        () {
      final pacificTime = DateTime.parse('2026-06-22T09:00:00-07:00');
      final sameInstantUtc = DateTime.utc(2026, 6, 22, 16);

      final stored = PersistenceDateTimeConvention.toStoredString(pacificTime);
      final restored = PersistenceDateTimeConvention.fromStoredString(stored);

      expect(PersistenceDateTimeConvention.description, contains('UTC'));
      expect(stored, '2026-06-22T16:00:00.000Z');
      expect(restored, sameInstantUtc);
      expect(
        PersistenceDateTimeConvention.compare(pacificTime, sameInstantUtc),
        0,
      );
    });

    test('civil dates and wall times persist without timezone conversion', () {
      final date = CivilDate(2026, 3, 8);
      final wallTime = WallTime(hour: 2, minute: 30);

      final storedDate = PersistenceCivilDateConvention.toStoredString(date);
      final storedWallTime =
          PersistenceWallTimeConvention.toStoredString(wallTime);

      expect(
          PersistenceCivilDateConvention.description, contains('YYYY-MM-DD'));
      expect(PersistenceWallTimeConvention.description, contains('HH:MM'));
      expect(storedDate, '2026-03-08');
      expect(storedWallTime, '02:30');
      expect(
        PersistenceCivilDateConvention.fromStoredString(storedDate),
        date,
      );
      expect(
        PersistenceWallTimeConvention.fromStoredString(storedWallTime),
        wallTime,
      );
    });

    test('nullable task fields are preserved unless explicitly cleared', () {
      final original = scheduledTask(
        id: 'nullable-task',
        createdAt: createdAt,
      ).copyWith(
        priority: PriorityLevel.high,
        parentTaskId: 'parent',
      );

      final preserved = original.copyWith(title: 'Updated');
      final clearedSchedule = original.copyWith(clearSchedule: true);

      expect(preserved.priority, PriorityLevel.high);
      expect(preserved.durationMinutes, original.durationMinutes);
      expect(preserved.scheduledStart, original.scheduledStart);
      expect(preserved.scheduledEnd, original.scheduledEnd);
      expect(preserved.parentTaskId, 'parent');

      expect(clearedSchedule.scheduledStart, isNull);
      expect(clearedSchedule.scheduledEnd, isNull);
      expect(clearedSchedule.id, original.id);
    });

    test('enum persistence names stay stable and distinguish not set values',
        () {
      expect(TaskType.flexible.persistenceName, 'flexible');
      expect(TaskStatus.backlog.persistenceName, 'backlog');
      expect(PriorityLevel.veryHigh.persistenceName, 'veryHigh');
      expect(RewardLevel.notSet.persistenceName, 'notSet');
      expect(RewardLevel.veryLow.persistenceName, 'veryLow');
      expect(DifficultyLevel.veryHard.persistenceName, 'veryHard');
      expect(ReminderProfile.gentle.persistenceName, 'gentle');
      expect(BacklogTag.wishlist.persistenceName, 'wishlist');
      expect(LockedWeekday.monday.persistenceName, 'monday');
      expect(LockedBlockOverrideType.replace.persistenceName, 'replace');

      expect(
        RewardLevel.notSet.persistenceName,
        isNot(RewardLevel.veryLow.persistenceName),
      );
    });

    test('backlog age behavior uses task creation timestamp', () {
      final oldButRecentlyUpdated = Task.quickCapture(
        id: 'old',
        title: 'Old backlog idea',
        createdAt: DateTime.utc(2026, 5, 1),
        updatedAt: DateTime.utc(2026, 6, 21),
      );
      final view = BacklogView(
        tasks: [oldButRecentlyUpdated],
        now: DateTime.utc(2026, 6, 22),
      );

      expect(view.filter(BacklogFilter.stale), [oldButRecentlyUpdated]);
      expect(
        view.stalenessMarkerFor(oldButRecentlyUpdated),
        BacklogStalenessMarker.purple,
      );
    });

    test('in-memory repositories round-trip scheduling fields', () async {
      final task = scheduledTask(
        id: 'scheduled',
        createdAt: createdAt,
      );
      final repository = InMemoryTaskRepository();

      await repository.save(task);
      final saved = await repository.findById(task.id);

      expect(saved, isNotNull);
      expect(saved!.id, task.id);
      expect(saved.scheduledStart, task.scheduledStart);
      expect(saved.scheduledEnd, task.scheduledEnd);
      expect(saved.durationMinutes, task.durationMinutes);
      expect(saved.projectId, task.projectId);
      expect(saved.reward, task.reward);
      expect(saved.difficulty, task.difficulty);
    });

    test('repository read operations do not expose mutable collections',
        () async {
      final original = scheduledTask(
        id: 'immutable',
        createdAt: createdAt,
      );
      final repository = InMemoryTaskRepository([original]);

      final savedList = await repository.findAll();

      expect(() => savedList.add(original), throwsUnsupportedError);
      expect((await repository.findAll()).map((task) => task.id), [
        'immutable',
      ]);
      expect(original.scheduledStart, DateTime.utc(2026, 6, 22, 9));
    });

    test('document field sets preserve all task and statistics fields', () {
      expect(TaskDocumentFields.all, {
        'schemaVersion',
        '_id',
        'ownerId',
        'revision',
        'createdAt',
        'updatedAt',
        'title',
        'projectId',
        'type',
        'status',
        'priority',
        'reward',
        'difficulty',
        'durationMinutes',
        'scheduledStart',
        'scheduledEnd',
        'actualStart',
        'actualEnd',
        'completedAt',
        'parentTaskId',
        'backlogTags',
        'reminderOverride',
        'stats',
        'backlogEnteredAt',
        'backlogEnteredAtProvenance',
      });
      expect(TaskActivityDocumentFields.all, {
        'schemaVersion',
        '_id',
        'ownerId',
        'revision',
        'createdAt',
        'updatedAt',
        'operationId',
        'code',
        'taskId',
        'projectId',
        'occurredAt',
        'metadata',
      });
      expect(TaskStatisticsDocumentFields.all, {
        'skippedDuringBurnoutCount',
        'manuallyPushedCount',
        'autoPushedCount',
        'movedToBacklogCount',
        'restoredFromBacklogCount',
        'missedCount',
        'cancelledCount',
        'completedLateCount',
        'completedDuringLockedHoursCount',
        'completedDuringLockedHoursMinutes',
        'completedAfterShieldCount',
        'completedAfterPushCount',
        'totalPushesBeforeCompletion',
      });
    });

    test('document field sets cover project locked and snapshot documents', () {
      expect(ProjectDocumentFields.all, {
        'schemaVersion',
        '_id',
        'ownerId',
        'revision',
        'createdAt',
        'updatedAt',
        'name',
        'colorKey',
        'defaultPriority',
        'defaultReward',
        'defaultDifficulty',
        'defaultReminderProfile',
        'defaultDurationMinutes',
        'archivedAt',
      });
      expect(ProjectStatisticsDocumentFields.all, {
        'schemaVersion',
        '_id',
        'ownerId',
        'revision',
        'createdAt',
        'updatedAt',
        'projectId',
        'completedTaskCount',
        'durationMinuteCounts',
        'completionTimeBucketCounts',
        'totalPushesBeforeCompletion',
        'completedAfterPushCount',
        'rewardCounts',
        'difficultyCounts',
        'reminderProfileCounts',
        'appliedActivityIds',
      });
      expect(
        LockedBlockDocumentFields.all,
        containsAll([
          'schemaVersion',
          '_id',
          'ownerId',
          'revision',
          'startTime',
          'recurrence',
          'hiddenByDefault',
          'archivedAt',
        ]),
      );
      expect(
        LockedBlockOverrideDocumentFields.all,
        containsAll(
            ['schemaVersion', '_id', 'ownerId', 'lockedBlockId', 'type']),
      );
      expect(
        OwnerSettingsDocumentFields.all,
        containsAll(['_id', 'ownerId', 'timeZoneId', 'backlogStaleness']),
      );
      expect(
        NoticeAcknowledgementDocumentFields.all,
        containsAll(['_id', 'ownerId', 'noticeId', 'acknowledgedAt']),
      );
      expect(
        ApplicationOperationDocumentFields.all,
        containsAll(['_id', 'ownerId', 'operationId', 'committedAt']),
      );
      expect(
        SchedulingSnapshotDocumentFields.all,
        containsAll([
          'schemaVersion',
          '_id',
          'ownerId',
          'revision',
          'capturedAt',
          'sourceDate',
          'targetDate',
          'window',
          'tasks',
          'lockedIntervals',
          'requiredVisibleIntervals',
          'notices',
          'changes',
          'overlaps',
          'operationName',
          'retentionExpiresAt',
          'truncated',
        ]),
      );
    });

    test('document field names are stable and migration-friendly', () {
      for (final fieldSet in PersistenceDocumentFieldSets.all) {
        expect(fieldSet, isNotEmpty);
        for (final fieldName in fieldSet) {
          expect(fieldName, isNotEmpty);
          expect(fieldName.trim(), fieldName);
          expect(fieldName.contains('.'), isFalse, reason: fieldName);
          expect(fieldName.startsWith(r'$'), isFalse, reason: fieldName);
        }
      }

      expect(DocumentFields.id, '_id');
      expect(TaskDocumentFields.projectId, 'projectId');
      expect(TaskDocumentFields.scheduledStart, 'scheduledStart');
      expect(TaskDocumentFields.scheduledEnd, 'scheduledEnd');
    });
  });
}

/// 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 createdAt,
}) {
  return Task(
    id: id,
    title: 'Scheduled task',
    projectId: 'project-1',
    type: TaskType.flexible,
    status: TaskStatus.planned,
    priority: PriorityLevel.medium,
    reward: RewardLevel.high,
    difficulty: DifficultyLevel.medium,
    durationMinutes: 30,
    scheduledStart: DateTime.utc(2026, 6, 22, 9),
    scheduledEnd: DateTime.utc(2026, 6, 22, 9, 30),
    createdAt: createdAt,
    updatedAt: createdAt,
  );
}
