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

/// Tests Domain Invariants 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('Domain invariants', () {
    final now = DateTime(2026, 6, 24, 9);

    test('task rejects blank identifiers titles and project ids with codes',
        () {
      expectValidationCode(
        () => _task(now: now, id: '  '),
        DomainValidationCode.blankStableId,
      );
      expectValidationCode(
        () => _task(now: now, title: '  '),
        DomainValidationCode.blankTitle,
      );
      expectValidationCode(
        () => _task(now: now, projectId: '  '),
        DomainValidationCode.blankProjectId,
      );
    });

    test('task rejects invalid duration and schedule intervals', () {
      expectValidationCode(
        () => _task(now: now, durationMinutes: 0),
        DomainValidationCode.nonPositiveDuration,
      );
      expectValidationCode(
        () => _task(
          now: now,
          scheduledStart: DateTime(2026, 6, 24, 9),
        ),
        DomainValidationCode.partialScheduledInterval,
      );
      expectValidationCode(
        () => _task(
          now: now,
          scheduledStart: DateTime(2026, 6, 24, 10),
          scheduledEnd: DateTime(2026, 6, 24, 9),
        ),
        DomainValidationCode.invalidScheduledInterval,
      );
    });

    test('task rejects invalid actual intervals and self parenting', () {
      expectValidationCode(
        () => _task(
          now: now,
          actualStart: DateTime(2026, 6, 24, 9),
        ),
        DomainValidationCode.partialActualInterval,
      );
      expectValidationCode(
        () => _task(
          now: now,
          actualStart: DateTime(2026, 6, 24, 10),
          actualEnd: DateTime(2026, 6, 24, 9),
        ),
        DomainValidationCode.invalidActualInterval,
      );
      expectValidationCode(
        () => _task(now: now, id: 'task-1', parentTaskId: 'task-1'),
        DomainValidationCode.selfParent,
      );
    });

    test('copyWith has explicit clear paths for nullable task fields', () {
      final task = _task(
        now: now,
        priority: PriorityLevel.high,
        durationMinutes: 25,
        scheduledStart: DateTime(2026, 6, 24, 9),
        scheduledEnd: DateTime(2026, 6, 24, 9, 25),
        actualStart: DateTime(2026, 6, 24, 9, 5),
        actualEnd: DateTime(2026, 6, 24, 9, 20),
        completedAt: DateTime(2026, 6, 24, 9, 25),
        parentTaskId: 'parent-1',
        reminderOverride: ReminderProfile.strict,
      );

      final preserved = task.copyWith(title: 'Renamed');
      final cleared = task.copyWith(
        clearPriority: true,
        clearDuration: true,
        clearSchedule: true,
        clearActualInterval: true,
        clearCompletion: true,
        clearParentTask: true,
        clearReminderOverride: true,
      );

      expect(preserved.priority, PriorityLevel.high);
      expect(preserved.durationMinutes, 25);
      expect(preserved.scheduledStart, DateTime(2026, 6, 24, 9));
      expect(preserved.actualStart, DateTime(2026, 6, 24, 9, 5));
      expect(preserved.completedAt, DateTime(2026, 6, 24, 9, 25));
      expect(preserved.parentTaskId, 'parent-1');
      expect(preserved.reminderOverride, ReminderProfile.strict);

      expect(cleared.priority, isNull);
      expect(cleared.durationMinutes, isNull);
      expect(cleared.scheduledStart, isNull);
      expect(cleared.scheduledEnd, isNull);
      expect(cleared.actualStart, isNull);
      expect(cleared.actualEnd, isNull);
      expect(cleared.completedAt, isNull);
      expect(cleared.parentTaskId, isNull);
      expect(cleared.reminderOverride, isNull);
    });

    test('mutable task collections do not leak into domain objects', () {
      final tags = {BacklogTag.wishlist};
      final task = _task(now: now, backlogTags: tags);

      tags.clear();

      expect(task.backlogTags, {BacklogTag.wishlist});
      expect(() => task.backlogTags.clear(), throwsUnsupportedError);
    });

    test('quick capture reports invalid durations without throwing', () {
      const service = QuickCaptureService();
      final result = service.capture(
        QuickCaptureRequest(
          id: 'capture-1',
          title: 'Buy stamps',
          createdAt: now,
          durationMinutes: -5,
        ),
      );

      expect(result.status, QuickCaptureStatus.validationError);
      expect(result.task.durationMinutes, isNull);
      expect(result.messages.single, contains('positive'));
    });

    test('project profile validates identifiers and default duration', () {
      expectValidationCode(
        () => ProjectProfile(id: ' ', name: 'Home', colorKey: 'blue'),
        DomainValidationCode.blankStableId,
      );
      expectValidationCode(
        () => ProjectProfile(id: 'home', name: ' ', colorKey: 'blue'),
        DomainValidationCode.blankName,
      );
      expectValidationCode(
        () => ProjectProfile(id: 'home', name: 'Home', colorKey: ' '),
        DomainValidationCode.blankColorKey,
      );
      expectValidationCode(
        () => ProjectProfile(
          id: 'home',
          name: 'Home',
          colorKey: 'blue',
          defaultDurationMinutes: -1,
        ),
        DomainValidationCode.nonPositiveDuration,
      );

      final project = ProjectProfile(
        id: 'home',
        name: 'Home',
        colorKey: 'blue',
        defaultDurationMinutes: 20,
      );
      expect(
          project.copyWith(clearDefaultDuration: true).defaultDurationMinutes,
          isNull);
    });

    test('time interval and scheduling window reject non-positive ranges', () {
      expectValidationCode(
        () => TimeInterval(
          start: DateTime(2026, 6, 24, 9),
          end: DateTime(2026, 6, 24, 9),
        ),
        DomainValidationCode.invalidTimeInterval,
      );
      expectValidationCode(
        () => SchedulingWindow(
          start: DateTime(2026, 6, 24, 10),
          end: DateTime(2026, 6, 24, 9),
        ),
        DomainValidationCode.invalidSchedulingWindow,
      );
    });
  });

  group('Locked-time invariants', () {
    final now = DateTime(2026, 6, 24, 9);

    test('clock time and recurrence validate real values', () {
      expectValidationCode(
        () => ClockTime(hour: 24, minute: 0),
        DomainValidationCode.invalidClockTime,
      );
      expectValidationCode(
        () => LockedBlockRecurrence.weekly(weekdays: const {}),
        DomainValidationCode.emptyRecurrence,
      );
    });

    test('locked blocks require a source date or recurrence and positive time',
        () {
      expectValidationCode(
        () => LockedBlock(
          id: 'locked-1',
          name: 'Work',
          startTime: ClockTime(hour: 9, minute: 0),
          endTime: ClockTime(hour: 10, minute: 0),
          createdAt: now,
          updatedAt: now,
        ),
        DomainValidationCode.invalidLockedBlock,
      );
      expectValidationCode(
        () => LockedBlock(
          id: 'locked-1',
          name: 'Work',
          startTime: ClockTime(hour: 10, minute: 0),
          endTime: ClockTime(hour: 9, minute: 0),
          date: CivilDate.fromDateTime(now),
          createdAt: now,
          updatedAt: now,
        ),
        DomainValidationCode.invalidLockedBlock,
      );
    });

    test('locked block overrides enforce valid shapes', () {
      expectValidationCode(
        () => LockedBlockOverride.add(
          id: 'override-1',
          date: CivilDate.fromDateTime(now),
          name: 'Extra work',
          startTime: ClockTime(hour: 12, minute: 0),
          endTime: ClockTime(hour: 11, minute: 0),
          createdAt: now,
        ),
        DomainValidationCode.invalidLockedOverride,
      );
      expectValidationCode(
        () => LockedBlockOverride(
          id: 'override-2',
          date: CivilDate.fromDateTime(now),
          type: LockedBlockOverrideType.remove,
          name: 'Invalid remove',
          createdAt: now,
          updatedAt: now,
        ),
        DomainValidationCode.invalidLockedOverride,
      );
    });
  });

  group('Collection immutability', () {
    final now = DateTime(2026, 6, 24, 9);

    test('scheduling input and result copy mutable lists', () {
      final task = _task(now: now);
      final sourceTasks = [task];
      final input = SchedulingInput(
        tasks: sourceTasks,
        window: SchedulingWindow(
          start: DateTime(2026, 6, 24, 9),
          end: DateTime(2026, 6, 24, 10),
        ),
      );
      final result = SchedulingResult(tasks: sourceTasks);

      sourceTasks.clear();

      expect(input.tasks, [task]);
      expect(result.tasks, [task]);
      expect(() => input.tasks.add(task), throwsUnsupportedError);
      expect(() => result.tasks.add(task), throwsUnsupportedError);
    });

    test('view and action result collections are immutable snapshots', () {
      final task = _task(now: now);
      final tasks = [task];
      final backlogView = BacklogView(tasks: tasks, now: now);
      final childView = ChildTaskView(tasks: tasks);
      final quickResult = QuickCaptureResult(
        task: task,
        status: QuickCaptureStatus.validationError,
        messages: ['Need duration'],
      );
      final completionResult = ChildTaskCompletionResult(
        tasks: tasks,
        changedTaskIds: ['task-1'],
      );

      tasks.clear();

      expect(backlogView.tasks, [task]);
      expect(childView.tasks, [task]);
      expect(quickResult.messages, ['Need duration']);
      expect(completionResult.tasks, [task]);
      expect(() => backlogView.tasks.add(task), throwsUnsupportedError);
      expect(() => childView.tasks.add(task), throwsUnsupportedError);
      expect(() => quickResult.messages.add('Other'), throwsUnsupportedError);
      expect(() => completionResult.changedTaskIds.add('task-2'),
          throwsUnsupportedError);
    });
  });
}

/// Runs the `expectValidationCode` 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 expectValidationCode(
  Object? Function() callback,
  DomainValidationCode code,
) {
  expect(
    callback,
    throwsA(
      isA<DomainValidationException>().having(
        (error) => error.code,
        'code',
        code,
      ),
    ),
  );
}

/// Top-level helper that performs the `_task` operation for this file.
/// Keeping the helper private makes the surrounding flow easier to read while preventing unrelated libraries from depending on implementation detail.
Task _task({
  required DateTime now,
  String id = 'task-1',
  String title = 'Task',
  String projectId = 'project-1',
  TaskType type = TaskType.flexible,
  TaskStatus status = TaskStatus.planned,
  PriorityLevel? priority,
  int? durationMinutes,
  DateTime? scheduledStart,
  DateTime? scheduledEnd,
  DateTime? actualStart,
  DateTime? actualEnd,
  DateTime? completedAt,
  String? parentTaskId,
  Set<BacklogTag> backlogTags = const {},
  ReminderProfile? reminderOverride,
}) {
  return Task(
    id: id,
    title: title,
    projectId: projectId,
    type: type,
    status: status,
    priority: priority,
    durationMinutes: durationMinutes,
    scheduledStart: scheduledStart,
    scheduledEnd: scheduledEnd,
    actualStart: actualStart,
    actualEnd: actualEnd,
    completedAt: completedAt,
    parentTaskId: parentTaskId,
    backlogTags: backlogTags,
    reminderOverride: reminderOverride,
    createdAt: now,
    updatedAt: now,
  );
}
