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

/// Tests Document Mapping 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 document codecs', () {
    final createdAt = DateTime.utc(2026, 6, 23, 8);
    final updatedAt = DateTime.utc(2026, 6, 23, 8, 5);

    test('task documents round-trip full and nullable V1 fields', () {
      final task = scheduledTask(
        id: 'task-1',
        createdAt: createdAt,
        reward: RewardLevel.high,
        difficulty: DifficultyLevel.hard,
      ).copyWith(stats: const TaskStatistics().incrementManualPush());

      final document = task.toDocument(
        ownerId: 'owner-1',
        revision: 4,
        backlogEnteredAt: createdAt,
        backlogEnteredAtProvenance: 'recorded',
      )..['forwardCompatibleExtra'] = 'ignored';
      final restored = TaskDocumentMapping.fromDocument(document);

      expect(document[DocumentFields.schemaVersion], v1SchemaVersion);
      expect(document[TaskDocumentFields.ownerId], 'owner-1');
      expect(document[TaskDocumentFields.revision], 4);
      expect(document[TaskDocumentFields.type], 'flexible');
      expect(document[TaskDocumentFields.status], 'planned');
      expect(document[TaskDocumentFields.reward], 'high');
      expect(document[TaskDocumentFields.difficulty], 'hard');
      expect(document[TaskDocumentFields.scheduledStart],
          '2026-06-23T09:00:00.000Z');
      expect(restored.id, task.id);
      expect(restored.type, task.type);
      expect(restored.status, task.status);
      expect(restored.reward, task.reward);
      expect(restored.difficulty, task.difficulty);
      expect(restored.scheduledStart, task.scheduledStart);
      expect(restored.actualStart, task.actualStart);
      expect(restored.completedAt, task.completedAt);
      expect(restored.backlogEnteredAt, createdAt);
      expect(restored.backlogEnteredAtProvenance, 'recorded');
      expect(restored.backlogTags, task.backlogTags);
      expect(restored.stats.manuallyPushedCount, 1);

      final nullableTask = Task(
        id: 'task-nullable',
        title: 'Nullable task',
        projectId: 'inbox',
        type: TaskType.flexible,
        status: TaskStatus.backlog,
        createdAt: createdAt,
        updatedAt: updatedAt,
      );
      final nullableDocument = nullableTask.toDocument(ownerId: 'owner-1');
      final nullableRestored = TaskDocumentMapping.fromDocument(
        nullableDocument,
      );

      expect(nullableDocument[TaskDocumentFields.priority], isNull);
      expect(nullableDocument[TaskDocumentFields.durationMinutes], isNull);
      expect(nullableDocument[TaskDocumentFields.scheduledStart], isNull);
      expect(nullableDocument[TaskDocumentFields.backlogEnteredAt], isNull);
      expect(nullableRestored.priority, isNull);
      expect(nullableRestored.durationMinutes, isNull);
      expect(nullableRestored.scheduledStart, isNull);
    });

    test('task documents serialize backlog entry fields from the task', () {
      final task = Task.quickCapture(
        id: 'backlog-entry',
        title: 'Backlog entry',
        createdAt: createdAt,
      ).copyWith(
        backlogEnteredAt: updatedAt,
        backlogEnteredAtProvenance:
            Task.backlogEnteredAtProvenanceMovedToBacklog,
      );

      final document = task.toDocument(ownerId: 'owner-1');
      final restored = TaskDocumentMapping.fromDocument(document);

      expect(
        document[TaskDocumentFields.backlogEnteredAt],
        '2026-06-23T08:05:00.000Z',
      );
      expect(
        document[TaskDocumentFields.backlogEnteredAtProvenance],
        Task.backlogEnteredAtProvenanceMovedToBacklog,
      );
      expect(restored.backlogEnteredAt, updatedAt);
      expect(
        restored.backlogEnteredAtProvenance,
        Task.backlogEnteredAtProvenanceMovedToBacklog,
      );
    });

    test('task documents preserve explicit schedule clearing', () {
      final task = scheduledTask(id: 'scheduled', createdAt: createdAt);

      final document = task.toDocument(
        ownerId: 'owner-1',
        clearSchedule: true,
      );
      final restored = TaskDocumentMapping.fromDocument(document);

      expect(document[TaskDocumentFields.scheduledStart], isNull);
      expect(document[TaskDocumentFields.scheduledEnd], isNull);
      expect(restored.scheduledStart, isNull);
      expect(restored.scheduledEnd, isNull);
      expect(restored.actualStart, task.actualStart);
    });

    test('task statistics document mapping preserves every counter', () {
      const stats = TaskStatistics(
        skippedDuringBurnoutCount: 1,
        manuallyPushedCount: 2,
        autoPushedCount: 3,
        movedToBacklogCount: 4,
        restoredFromBacklogCount: 5,
        missedCount: 6,
        cancelledCount: 7,
        completedLateCount: 8,
        completedDuringLockedHoursCount: 9,
        completedDuringLockedHoursMinutes: 10,
        completedAfterShieldCount: 11,
        completedAfterPushCount: 12,
        totalPushesBeforeCompletion: 13,
      );

      final document = stats.toDocument();
      final restored = TaskStatisticsDocumentMapping.fromDocument(document);

      expect(document.keys.toSet(), TaskStatisticsDocumentFields.all);
      expect(restored.skippedDuringBurnoutCount, 1);
      expect(restored.manuallyPushedCount, 2);
      expect(restored.autoPushedCount, 3);
      expect(restored.movedToBacklogCount, 4);
      expect(restored.restoredFromBacklogCount, 5);
      expect(restored.missedCount, 6);
      expect(restored.cancelledCount, 7);
      expect(restored.completedLateCount, 8);
      expect(restored.completedDuringLockedHoursCount, 9);
      expect(restored.completedDuringLockedHoursMinutes, 10);
      expect(restored.completedAfterShieldCount, 11);
      expect(restored.completedAfterPushCount, 12);
      expect(restored.totalPushesBeforeCompletion, 13);
    });

    test('project profile documents round-trip configured defaults', () {
      final project = ProjectProfile(
        id: 'home',
        name: 'Home',
        colorKey: 'home-blue',
        defaultPriority: PriorityLevel.high,
        defaultReward: RewardLevel.low,
        defaultDifficulty: DifficultyLevel.easy,
        defaultReminderProfile: ReminderProfile.persistent,
        defaultDurationMinutes: 45,
        archivedAt: DateTime.utc(2026, 6, 24, 9),
      );

      final document = ProjectDocumentMapping.toDocument(
        project,
        ownerId: 'owner-1',
        createdAt: createdAt,
        updatedAt: updatedAt,
        revision: 2,
      );
      final restored = ProjectDocumentMapping.fromDocument(document);

      expect(document.keys.toSet(), ProjectDocumentFields.all);
      expect(document[ProjectDocumentFields.defaultPriority], 'high');
      expect(
          document[ProjectDocumentFields.defaultReminderProfile], 'persistent');
      expect(restored.id, project.id);
      expect(restored.defaultDurationMinutes, 45);
      expect(restored.archivedAt, project.archivedAt);
    });

    test('project statistics documents preserve aggregates and applied ids',
        () {
      final statistics = ProjectStatistics(
        projectId: 'home',
        completedTaskCount: 8,
        durationMinuteCounts: const {20: 3, 45: 5},
        completionTimeBucketCounts: const {
          ProjectCompletionTimeBucket.morning: 6,
          ProjectCompletionTimeBucket.evening: 2,
        },
        totalPushesBeforeCompletion: 11,
        completedAfterPushCount: 4,
        rewardCounts: const {
          RewardLevel.high: 5,
          RewardLevel.low: 3,
        },
        difficultyCounts: const {
          DifficultyLevel.easy: 2,
          DifficultyLevel.hard: 6,
        },
        reminderProfileCounts: const {
          ReminderProfile.gentle: 3,
          ReminderProfile.persistent: 5,
        },
      );

      final document = statistics.toDocument(
        ownerId: 'owner-1',
        createdAt: createdAt,
        updatedAt: updatedAt,
        appliedActivityIds: const ['activity-1'],
      );
      final restored = ProjectStatisticsDocumentMapping.fromDocument(document);

      expect(document.keys.toSet(), ProjectStatisticsDocumentFields.all);
      expect(document[ProjectStatisticsDocumentFields.id], 'home');
      expect(document[ProjectStatisticsDocumentFields.projectId], 'home');
      expect(document[ProjectStatisticsDocumentFields.durationMinuteCounts], {
        '20': 3,
        '45': 5,
      });
      expect(document[ProjectStatisticsDocumentFields.rewardCounts], {
        'high': 5,
        'low': 3,
      });
      expect(document[ProjectStatisticsDocumentFields.appliedActivityIds], [
        'activity-1',
      ]);
      expect(restored.projectId, statistics.projectId);
      expect(restored.completedTaskCount, 8);
      expect(restored.durationMinuteCounts, statistics.durationMinuteCounts);
      expect(restored.completionTimeBucketCounts,
          statistics.completionTimeBucketCounts);
      expect(restored.reminderProfileCounts, statistics.reminderProfileCounts);
    });

    test(
        'locked block and override documents preserve civil dates and wall time',
        () {
      final recurrence = LockedBlockRecurrence.weekly(
        weekdays: {LockedWeekday.monday, LockedWeekday.thursday},
      );
      final recurringBlock = LockedBlock(
        id: 'work',
        name: 'Work',
        startTime: WallTime(hour: 9, minute: 0),
        endTime: WallTime(hour: 17, minute: 0),
        recurrence: recurrence,
        hiddenByDefault: true,
        createdAt: createdAt,
        updatedAt: updatedAt,
        archivedAt: DateTime.utc(2026, 6, 30),
      );
      final replaceOverride = LockedBlockOverride.replace(
        id: 'replace-work',
        lockedBlockId: 'work',
        date: CivilDate(2026, 6, 25),
        startTime: WallTime(hour: 10, minute: 0),
        endTime: WallTime(hour: 15, minute: 0),
        createdAt: createdAt,
        name: 'Short work',
        updatedAt: updatedAt,
      );
      final addOverride = LockedBlockOverride.add(
        id: 'appointment',
        date: CivilDate(2026, 6, 25),
        name: 'Appointment',
        startTime: WallTime(hour: 18, minute: 0),
        endTime: WallTime(hour: 19, minute: 0),
        createdAt: createdAt,
      );
      final removeOverride = LockedBlockOverride.remove(
        id: 'remove-work',
        lockedBlockId: 'work',
        date: CivilDate(2026, 6, 26),
        createdAt: createdAt,
      );

      final blockDocument = LockedBlockDocumentMapping.toDocument(
        recurringBlock,
        ownerId: 'owner-1',
      );
      final restoredBlock = LockedBlockDocumentMapping.fromDocument(
        blockDocument,
      );
      final replaceDocument = LockedBlockOverrideDocumentMapping.toDocument(
        replaceOverride,
        ownerId: 'owner-1',
      );
      final addDocument = LockedBlockOverrideDocumentMapping.toDocument(
        addOverride,
        ownerId: 'owner-1',
      );
      final removeDocument = LockedBlockOverrideDocumentMapping.toDocument(
        removeOverride,
        ownerId: 'owner-1',
      );

      expect(blockDocument[LockedBlockDocumentFields.startTime], '09:00');
      expect(
        (blockDocument[LockedBlockDocumentFields.recurrence] as Map<String,
            Object?>)[LockedBlockRecurrenceDocumentFields.weekdays],
        ['monday', 'thursday'],
      );
      expect(restoredBlock.recurrence!.weekdays, recurrence.weekdays);
      expect(restoredBlock.archivedAt, recurringBlock.archivedAt);
      expect(replaceDocument[LockedBlockOverrideDocumentFields.date],
          '2026-06-25');
      expect(
          replaceDocument[LockedBlockOverrideDocumentFields.type], 'replace');
      expect(addDocument[LockedBlockOverrideDocumentFields.type], 'add');
      expect(removeDocument[LockedBlockOverrideDocumentFields.type], 'remove');
      expect(
        LockedBlockOverrideDocumentMapping.fromDocument(replaceDocument).date,
        CivilDate(2026, 6, 25),
      );
      expect(
        LockedBlockOverrideDocumentMapping.fromDocument(removeDocument)
            .startTime,
        isNull,
      );
    });

    test('settings, notices, operations, and activities round-trip', () {
      final settings = OwnerSettings(
        ownerId: 'owner-1',
        timeZoneId: 'America/Los_Angeles',
        dayStart: WallTime(hour: 7, minute: 30),
        dayEnd: WallTime(hour: 21, minute: 0),
        compactModeEnabled: true,
        backlogStalenessSettings: const BacklogStalenessSettings(
          greenMaxAge: Duration(days: 3),
          blueMaxAge: Duration(days: 10),
        ),
      );
      final activity = TaskActivity(
        id: 'activity-1',
        operationId: 'op-1',
        code: TaskActivityCode.completed,
        taskId: 'task-1',
        projectId: 'home',
        occurredAt: createdAt,
        metadata: {'completedAt': createdAt},
      );
      final acknowledgement = NoticeAcknowledgementRecord(
        noticeId: 'snapshot:0',
        ownerId: 'owner-1',
        acknowledgedAt: updatedAt,
      );
      final operation = ApplicationOperationRecord(
        operationId: 'op-1',
        ownerId: 'owner-1',
        operationName: 'completeTask',
        committedAt: updatedAt,
      );

      final settingsDocument = OwnerSettingsDocumentMapping.toDocument(
        settings,
        createdAt: createdAt,
        updatedAt: updatedAt,
      );
      final activityDocument = TaskActivityDocumentMapping.toDocument(
        activity,
        ownerId: 'owner-1',
      );
      final acknowledgementDocument =
          NoticeAcknowledgementDocumentMapping.toDocument(acknowledgement);
      final operationDocument = ApplicationOperationDocumentMapping.toDocument(
        operation,
      );

      expect(settingsDocument[OwnerSettingsDocumentFields.dayStart], '07:30');
      expect(
        (settingsDocument[OwnerSettingsDocumentFields.backlogStaleness] as Map<
            String, Object?>)[BacklogStalenessDocumentFields.blueMaxAgeDays],
        10,
      );
      expect(activityDocument[TaskActivityDocumentFields.code], 'completed');
      expect(
        (activityDocument[TaskActivityDocumentFields.metadata]
            as Map<String, Object?>)['completedAt'],
        '2026-06-23T08:00:00.000Z',
      );
      expect(acknowledgementDocument[NoticeAcknowledgementDocumentFields.id],
          'snapshot:0');
      expect(operationDocument[ApplicationOperationDocumentFields.operationId],
          'op-1');

      expect(OwnerSettingsDocumentMapping.fromDocument(settingsDocument).dayEnd,
          settings.dayEnd);
      expect(TaskActivityDocumentMapping.fromDocument(activityDocument).code,
          TaskActivityCode.completed);
      expect(
        NoticeAcknowledgementDocumentMapping.fromDocument(
          acknowledgementDocument,
        ).noticeId,
        'snapshot:0',
      );
      expect(
        ApplicationOperationDocumentMapping.fromDocument(operationDocument)
            .operationName,
        'completeTask',
      );
    });

    test('scheduling snapshot structures round-trip nested data', () {
      final task = scheduledTask(id: 'task-1', createdAt: createdAt);
      final snapshot = SchedulingStateSnapshot(
        id: 'rollover:owner-1:2026-06-25',
        capturedAt: updatedAt,
        window: SchedulingWindow(
          start: DateTime.utc(2026, 6, 24, 9),
          end: DateTime.utc(2026, 6, 24, 17),
        ),
        tasks: [task],
        lockedIntervals: [
          TimeInterval(
            start: DateTime.utc(2026, 6, 24, 12),
            end: DateTime.utc(2026, 6, 24, 13),
            label: 'Hidden locked',
          ),
        ],
        notices: [
          SchedulingNotice(
            '1 unfinished flexible task was moved to today.',
            type: SchedulingNoticeType.moved,
            movementCode:
                SchedulingMovementCode.unfinishedFlexibleTasksRolledOver,
            parameters: {'count': 1},
          ),
        ],
        changes: [
          SchedulingChange(
            taskId: task.id,
            previousStart: task.scheduledStart,
            previousEnd: task.scheduledEnd,
            nextStart: DateTime.utc(2026, 6, 24, 9),
            nextEnd: DateTime.utc(2026, 6, 24, 9, 30),
          ),
        ],
        overlaps: [
          SchedulingOverlap(
            taskId: task.id,
            taskInterval: TimeInterval(
              start: DateTime.utc(2026, 6, 23, 9),
              end: DateTime.utc(2026, 6, 23, 9, 30),
            ),
            blockedInterval: TimeInterval(
              start: DateTime.utc(2026, 6, 23, 9, 15),
              end: DateTime.utc(2026, 6, 23, 9, 45),
            ),
          ),
        ],
        operationName: 'appOpenRecovery',
      );

      final document = SchedulingSnapshotDocumentMapping.toDocument(
        snapshot,
        ownerId: 'owner-1',
        sourceDate: CivilDate(2026, 6, 23),
        targetDate: CivilDate(2026, 6, 24),
        retentionExpiresAt: DateTime.utc(2026, 7, 24),
      );
      final restored = SchedulingSnapshotDocumentMapping.fromDocument(document);

      expect(document.keys.toSet(), SchedulingSnapshotDocumentFields.all);
      expect(
          document[SchedulingSnapshotDocumentFields.sourceDate], '2026-06-23');
      expect(restored.id, snapshot.id);
      expect(restored.window.start, snapshot.window.start);
      expect(restored.tasks.single.id, task.id);
      expect(restored.lockedIntervals.single.label, 'Hidden locked');
      expect(restored.notices.single.movementCode,
          SchedulingMovementCode.unfinishedFlexibleTasksRolledOver);
      expect(restored.notices.single.parameters['count'], 1);
      expect(restored.changes.single.taskId, task.id);
      expect(restored.overlaps.single.taskId, task.id);
    });

    test('stable enum code tables do not depend on Dart enum names', () {
      expect(
          PersistenceEnumMapping.encodeReward(RewardLevel.notSet), 'not_set');
      expect(
          PersistenceEnumMapping.encodeTaskStatus(
            TaskStatus.noLongerRelevant,
          ),
          'no_longer_relevant');
      expect(PersistenceEnumMapping.encodeTaskType(TaskType.freeSlot),
          'free_slot');
      expect(
        PersistenceEnumMapping.decodeTaskStatus('no_longer_relevant'),
        TaskStatus.noLongerRelevant,
      );
      expect(
        PersistenceEnumMapping.decodeReward('not_set'),
        RewardLevel.notSet,
      );
    });

    test('malformed documents produce typed mapping failures', () {
      final valid = scheduledTask(id: 'task-1', createdAt: createdAt)
          .toDocument(ownerId: 'owner-1');

      expect(
        () => TaskDocumentMapping.fromDocument(
          {...valid}..remove(TaskDocumentFields.title),
        ),
        throwsMapping(
            DocumentMappingFailureCode.missingField, TaskDocumentFields.title),
      );
      expect(
        () => TaskDocumentMapping.fromDocument({
          ...valid,
          TaskDocumentFields.reward: 'notSet',
        }),
        throwsMapping(DocumentMappingFailureCode.unknownCode, 'RewardLevel'),
      );
      expect(
        () => TaskDocumentMapping.fromDocument({
          ...valid,
          TaskDocumentFields.durationMinutes: '30',
        }),
        throwsMapping(DocumentMappingFailureCode.wrongType,
            TaskDocumentFields.durationMinutes),
      );
      expect(
        () => TaskDocumentMapping.fromDocument({
          ...valid,
          DocumentFields.revision: 0,
        }),
        throwsMapping(DocumentMappingFailureCode.invalidRevision,
            DocumentFields.revision),
      );
      expect(
        () => TaskDocumentMapping.fromDocument({
          ...valid,
          DocumentFields.schemaVersion: 99,
        }),
        throwsMapping(DocumentMappingFailureCode.invalidSchemaVersion,
            DocumentFields.schemaVersion),
      );
      expect(
        () => SchedulingWindowDocumentMapping.fromDocument({
          SchedulingWindowDocumentFields.start: '2026-06-23T10:00:00.000Z',
          SchedulingWindowDocumentFields.end: '2026-06-23T09:00:00.000Z',
        }),
        throwsMapping(DocumentMappingFailureCode.invalidValue),
      );
    });
  });
}

/// Runs the `throwsMapping` top-level operation.
/// This function is part of the file-level API and gives callers a direct way to use the associated scheduler behavior.
Matcher throwsMapping(
  DocumentMappingFailureCode code, [
  String? fieldName,
]) {
  var matcher = isA<DocumentMappingException>().having(
    (error) => error.code,
    'code',
    code,
  );
  if (fieldName != null) {
    matcher = matcher.having(
      (error) => error.fieldName,
      'fieldName',
      fieldName,
    );
  }
  return throwsA(matcher);
}

/// 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,
  RewardLevel reward = RewardLevel.notSet,
  DifficultyLevel difficulty = DifficultyLevel.medium,
}) {
  return Task(
    id: id,
    title: 'Scheduled task',
    projectId: 'project-1',
    type: TaskType.flexible,
    status: TaskStatus.planned,
    priority: PriorityLevel.medium,
    reward: reward,
    difficulty: difficulty,
    durationMinutes: 30,
    scheduledStart: DateTime.utc(2026, 6, 23, 9),
    scheduledEnd: DateTime.utc(2026, 6, 23, 9, 30),
    actualStart: DateTime.utc(2026, 6, 23, 9, 5),
    actualEnd: DateTime.utc(2026, 6, 23, 9, 25),
    completedAt: DateTime.utc(2026, 6, 23, 9, 30),
    parentTaskId: 'parent-1',
    backlogTags: const {BacklogTag.wishlist},
    reminderOverride: ReminderProfile.persistent,
    createdAt: createdAt,
    updatedAt: createdAt.add(const Duration(minutes: 5)),
  );
}
