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

/// Tests Occupancy Policy 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('OccupancyPolicy', () {
    const policy = OccupancyPolicy();
    final scheduledStart = DateTime(2026, 6, 19, 9);
    final scheduledEnd = DateTime(2026, 6, 19, 9, 30);
    final actualStart = DateTime(2026, 6, 19, 9, 5);
    final actualEnd = DateTime(2026, 6, 19, 9, 25);

    test('classifies every task type and status combination', () {
      for (final type in TaskType.values) {
        for (final status in TaskStatus.values) {
          final task = matrixTask(
            type: type,
            status: status,
            scheduledStart: scheduledStart,
            scheduledEnd: scheduledEnd,
            actualStart: status == TaskStatus.completed ? actualStart : null,
            actualEnd: status == TaskStatus.completed ? actualEnd : null,
          );
          final entry = policy.classifyTask(task);
          final expected = expectationFor(type: type, status: status);
          final reason = '${type.name}/${status.name}';

          expect(entry.category, expected.category, reason: reason);
          expect(entry.isImmovable, expected.isImmovable, reason: reason);
          expect(
            entry.blocksAutomaticFlexiblePlacement,
            expected.blocksAutomaticFlexiblePlacement,
            reason: reason,
          );
          expect(
            entry.mayBeExplicitlyOverlappedByRequiredCommitment,
            expected.mayBeExplicitlyOverlappedByRequiredCommitment,
            reason: reason,
          );
          expect(entry.reportsConflict, expected.reportsConflict,
              reason: reason);
          expect(entry.hiddenByDefault, expected.hiddenByDefault,
              reason: reason);

          if (expected.category == OccupancyCategory.nonOccupyingRecord) {
            expect(entry.interval, isNull, reason: reason);
          } else {
            expect(entry.interval, isNotNull, reason: reason);
          }

          if (expected.category == OccupancyCategory.completedActualOccupancy) {
            expect(entry.interval!.start, actualStart, reason: reason);
            expect(entry.interval!.end, actualEnd, reason: reason);
          }
        }
      }
    });

    test('classifies external locked and required intervals', () {
      final locked = TimeInterval(
        start: DateTime(2026, 6, 19, 10),
        end: DateTime(2026, 6, 19, 11),
        label: 'locked',
      );
      final required = TimeInterval(
        start: DateTime(2026, 6, 19, 12),
        end: DateTime(2026, 6, 19, 13),
        label: 'required',
      );

      final entries = policy.classify(
        tasks: const [],
        lockedIntervals: [locked],
        requiredVisibleIntervals: [required],
      );

      expect(entries.map((entry) => entry.category), [
        OccupancyCategory.hiddenLockedConstraint,
        OccupancyCategory.requiredVisibleCommitment,
      ]);
      expect(entries.first.source, OccupancySource.lockedInterval);
      expect(entries.first.hiddenByDefault, isTrue);
      expect(entries.first.blocksAutomaticFlexiblePlacement, isTrue);
      expect(entries.last.source, OccupancySource.requiredVisibleInterval);
      expect(entries.last.hiddenByDefault, isFalse);
      expect(entries.last.blocksAutomaticFlexiblePlacement, isTrue);
    });

    test('treats baseline completed surprise schedule as actual occupancy', () {
      final surprise = matrixTask(
        type: TaskType.surprise,
        status: TaskStatus.completed,
        scheduledStart: scheduledStart,
        scheduledEnd: scheduledEnd,
      );

      final entry = policy.classifyTask(surprise);

      expect(entry.category, OccupancyCategory.completedActualOccupancy);
      expect(entry.interval!.start, scheduledStart);
      expect(entry.interval!.end, scheduledEnd);
      expect(entry.blocksAutomaticFlexiblePlacement, isTrue);
    });
  });

  group('Scheduling occupancy integration', () {
    final now = DateTime(2026, 6, 19, 8);
    const engine = SchedulingEngine();

    test('automatic insertion uses policy blockers without moving them', () {
      final backlog = Task.quickCapture(
        id: 'backlog',
        title: 'Make call',
        createdAt: now,
      ).copyWith(durationMinutes: 15);
      final completedActual = Task(
        id: 'completed-actual',
        title: 'Finished appointment',
        projectId: 'health',
        type: TaskType.inflexible,
        status: TaskStatus.completed,
        priority: PriorityLevel.high,
        durationMinutes: 30,
        actualStart: DateTime(2026, 6, 19, 9),
        actualEnd: DateTime(2026, 6, 19, 9, 30),
        createdAt: now,
        updatedAt: now,
      );
      final freeSlot = Task(
        id: 'rest',
        title: 'Rest',
        projectId: 'health',
        type: TaskType.freeSlot,
        status: TaskStatus.planned,
        priority: PriorityLevel.medium,
        durationMinutes: 30,
        scheduledStart: DateTime(2026, 6, 19, 9, 30),
        scheduledEnd: DateTime(2026, 6, 19, 10),
        createdAt: now,
        updatedAt: now,
      );

      final result = engine.insertBacklogTaskIntoNextAvailableSlot(
        input: SchedulingInput(
          tasks: [backlog, completedActual, freeSlot],
          window: SchedulingWindow(
            start: DateTime(2026, 6, 19, 9),
            end: DateTime(2026, 6, 19, 11),
          ),
        ),
        taskId: 'backlog',
        updatedAt: now,
      );

      expect(taskById(result.tasks, 'backlog').scheduledStart,
          DateTime(2026, 6, 19, 10));
      expect(taskById(result.tasks, 'backlog').scheduledEnd,
          DateTime(2026, 6, 19, 10, 15));
      expect(taskById(result.tasks, 'completed-actual').actualStart,
          completedActual.actualStart);
      expect(taskById(result.tasks, 'rest').scheduledStart,
          freeSlot.scheduledStart);
      expect(result.changes.map((change) => change.taskId), ['backlog']);
    });
  });
}

/// Runs the `matrixTask` 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 matrixTask({
  required TaskType type,
  required TaskStatus status,
  DateTime? scheduledStart,
  DateTime? scheduledEnd,
  DateTime? actualStart,
  DateTime? actualEnd,
}) {
  return Task(
    id: '${type.name}-${status.name}',
    title: '${type.name} ${status.name}',
    projectId: 'matrix',
    type: type,
    status: status,
    priority: PriorityLevel.medium,
    durationMinutes: 30,
    scheduledStart: scheduledStart,
    scheduledEnd: scheduledEnd,
    actualStart: actualStart,
    actualEnd: actualEnd,
    createdAt: DateTime(2026, 6, 19, 8),
    updatedAt: DateTime(2026, 6, 19, 8),
  );
}

/// Runs the `expectationFor` top-level operation.
/// This function is part of the file-level API and gives callers a direct way to use the associated scheduler behavior.
OccupancyExpectation expectationFor({
  required TaskType type,
  required TaskStatus status,
}) {
  if (status == TaskStatus.backlog ||
      status == TaskStatus.cancelled ||
      status == TaskStatus.noLongerRelevant) {
    return const OccupancyExpectation.nonOccupying();
  }

  if (status == TaskStatus.completed) {
    return const OccupancyExpectation.blocking(
      category: OccupancyCategory.completedActualOccupancy,
    );
  }

  if (status == TaskStatus.missed) {
    return const OccupancyExpectation.blocking(
      category: OccupancyCategory.retainedMissedHistoricalInterval,
    );
  }

  if (type == TaskType.locked) {
    return const OccupancyExpectation.blocking(
      category: OccupancyCategory.hiddenLockedConstraint,
      hiddenByDefault: true,
    );
  }

  if (type == TaskType.freeSlot) {
    return const OccupancyExpectation.blocking(
      category: OccupancyCategory.protectedFreeSlot,
      mayBeExplicitlyOverlappedByRequiredCommitment: true,
    );
  }

  if (status == TaskStatus.active) {
    return const OccupancyExpectation.blocking(
      category: OccupancyCategory.activeWork,
    );
  }

  if (type == TaskType.critical || type == TaskType.inflexible) {
    return const OccupancyExpectation.blocking(
      category: OccupancyCategory.requiredVisibleCommitment,
    );
  }

  if (type == TaskType.flexible && status == TaskStatus.planned) {
    return const OccupancyExpectation.movableFlexible();
  }

  return const OccupancyExpectation.nonOccupying();
}

/// Represents `OccupancyExpectation` within the pure scheduler domain package.
/// The type groups related data and behavior behind a named API so readers can understand this part of the scheduler without relying on tribal knowledge about the surrounding files.
class OccupancyExpectation {
  /// Creates a `OccupancyExpectation` instance with the values required by its invariants.
  /// Callers should pass already-normalized scheduler data because validation and defaulting belong at the model or service boundary, not in arbitrary UI code.
  const OccupancyExpectation({
    required this.category,
    required this.isImmovable,
    required this.blocksAutomaticFlexiblePlacement,
    required this.mayBeExplicitlyOverlappedByRequiredCommitment,
    required this.reportsConflict,
    required this.hiddenByDefault,
  });

  /// Creates a `OccupancyExpectation.nonOccupying` instance with the values required by its invariants.
  /// Callers should pass already-normalized scheduler data because validation and defaulting belong at the model or service boundary, not in arbitrary UI code.
  const OccupancyExpectation.nonOccupying()
      : this(
          category: OccupancyCategory.nonOccupyingRecord,
          isImmovable: false,
          blocksAutomaticFlexiblePlacement: false,
          mayBeExplicitlyOverlappedByRequiredCommitment: false,
          reportsConflict: false,
          hiddenByDefault: false,
        );

  /// Creates a `OccupancyExpectation.movableFlexible` instance with the values required by its invariants.
  /// Callers should pass already-normalized scheduler data because validation and defaulting belong at the model or service boundary, not in arbitrary UI code.
  const OccupancyExpectation.movableFlexible()
      : this(
          category: OccupancyCategory.movablePlannedFlexible,
          isImmovable: false,
          blocksAutomaticFlexiblePlacement: false,
          mayBeExplicitlyOverlappedByRequiredCommitment: false,
          reportsConflict: false,
          hiddenByDefault: false,
        );

  /// Creates a `OccupancyExpectation.blocking` instance with the values required by its invariants.
  /// Callers should pass already-normalized scheduler data because validation and defaulting belong at the model or service boundary, not in arbitrary UI code.
  const OccupancyExpectation.blocking({
    required OccupancyCategory category,
    bool mayBeExplicitlyOverlappedByRequiredCommitment = false,
    bool hiddenByDefault = false,
  }) : this(
          category: category,
          isImmovable: true,
          blocksAutomaticFlexiblePlacement: true,
          mayBeExplicitlyOverlappedByRequiredCommitment:
              mayBeExplicitlyOverlappedByRequiredCommitment,
          reportsConflict: true,
          hiddenByDefault: hiddenByDefault,
        );

  /// Stores the `category` value for this object.
  /// This field is part of the explicit state that downstream scheduling, persistence, or UI code reads when composing behavior.
  final OccupancyCategory category;

  /// Stores the `isImmovable` value for this object.
  /// This field is part of the explicit state that downstream scheduling, persistence, or UI code reads when composing behavior.
  final bool isImmovable;

  /// Stores the `blocksAutomaticFlexiblePlacement` value for this object.
  /// This field is part of the explicit state that downstream scheduling, persistence, or UI code reads when composing behavior.
  final bool blocksAutomaticFlexiblePlacement;

  /// Stores the `mayBeExplicitlyOverlappedByRequiredCommitment` value for this object.
  /// This field is part of the explicit state that downstream scheduling, persistence, or UI code reads when composing behavior.
  final bool mayBeExplicitlyOverlappedByRequiredCommitment;

  /// Stores the `reportsConflict` value for this object.
  /// This field is part of the explicit state that downstream scheduling, persistence, or UI code reads when composing behavior.
  final bool reportsConflict;

  /// Stores the `hiddenByDefault` value for this object.
  /// This field is part of the explicit state that downstream scheduling, persistence, or UI code reads when composing behavior.
  final bool hiddenByDefault;
}

/// 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 id) {
  return tasks.singleWhere((task) => task.id == id);
}
