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

/// Tests Project Statistics 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('Project statistics aggregation', () {
    final createdAt = DateTime.utc(2026, 6, 25, 8);
    const transitionService = TaskTransitionService();
    const aggregationService = ProjectStatisticsAggregationService();

    test('applies completed task activities exactly once', () {
      final task = scheduledTask(
        id: 'task-1',
        projectId: 'home',
        createdAt: createdAt,
        start: DateTime.utc(2026, 6, 25, 9),
        reward: RewardLevel.high,
        difficulty: DifficultyLevel.hard,
        reminderOverride: ReminderProfile.strict,
      ).copyWith(
        stats: const TaskStatistics(
          manuallyPushedCount: 1,
          autoPushedCount: 2,
        ),
      );
      final transition = transitionService.apply(
        task: task,
        transitionCode: TaskTransitionCode.complete,
        operationId: 'complete-task-1',
        activityId: 'activity-1',
        occurredAt: DateTime.utc(2026, 6, 25, 10),
        actualStart: DateTime.utc(2026, 6, 25, 9, 5),
        actualEnd: DateTime.utc(2026, 6, 25, 9, 25),
      );

      final first = aggregationService.apply(
        statistics: ProjectStatistics(projectId: 'home'),
        activities: transition.activities,
        tasks: [transition.task],
      );
      final retry = aggregationService.apply(
        statistics: first.statistics,
        activities: transition.activities,
        tasks: [transition.task],
        alreadyAppliedActivityIds: first.appliedActivityIds,
      );

      expect(first.appliedActivityIds, ['activity-1']);
      expect(first.statistics.completedTaskCount, 1);
      expect(first.statistics.durationMinuteCounts, {20: 1});
      expect(first.statistics.completionTimeBucketCounts, {
        ProjectCompletionTimeBucket.morning: 1,
      });
      expect(first.statistics.totalPushesBeforeCompletion, 3);
      expect(first.statistics.completedAfterPushCount, 1);
      expect(first.statistics.rewardCounts, {RewardLevel.high: 1});
      expect(first.statistics.difficultyCounts, {DifficultyLevel.hard: 1});
      expect(first.statistics.reminderProfileCounts, {
        ReminderProfile.strict: 1,
      });
      expect(first.statistics.averagePushesBeforeCompletionNumerator, 3);
      expect(first.statistics.averagePushesBeforeCompletionDenominator, 1);
      expect(first.statistics.averagePushesBeforeCompletion, 3);

      expect(retry.appliedActivityIds, isEmpty);
      expect(retry.statistics.completedTaskCount, 1);
      expect(retry.statistics.totalPushesBeforeCompletion, 3);
    });

    test('skips non-completion and other-project activities', () {
      final completed = TaskActivity(
        id: 'completed',
        operationId: 'complete',
        code: TaskActivityCode.completed,
        taskId: 'task-1',
        projectId: 'home',
        occurredAt: DateTime.utc(2026, 6, 25, 9),
        metadata: const {'durationMinutes': 25},
      );
      final pushed = TaskActivity(
        id: 'pushed',
        operationId: 'push',
        code: TaskActivityCode.manuallyPushed,
        taskId: 'task-1',
        projectId: 'home',
        occurredAt: DateTime.utc(2026, 6, 25, 8),
      );
      final otherProject = TaskActivity(
        id: 'other-project',
        operationId: 'complete-other',
        code: TaskActivityCode.completed,
        taskId: 'task-2',
        projectId: 'work',
        occurredAt: DateTime.utc(2026, 6, 25, 10),
      );

      final result = aggregationService.apply(
        statistics: ProjectStatistics(projectId: 'home'),
        activities: [completed, pushed, otherProject, completed],
      );

      expect(result.appliedActivityIds, ['completed']);
      expect(result.statistics.completedTaskCount, 1);
      expect(result.statistics.durationMinuteCounts, {25: 1});
    });
  });

  group('Project learned suggestions', () {
    const service = ProjectSuggestionService();

    test('uses deterministic samples and resists duration outliers', () {
      final statistics = ProjectStatistics(
        projectId: 'home',
        completedTaskCount: 4,
        durationMinuteCounts: const {25: 2, 30: 1, 240: 1},
        completionTimeBucketCounts: const {
          ProjectCompletionTimeBucket.morning: 3,
          ProjectCompletionTimeBucket.evening: 1,
        },
        rewardCounts: const {
          RewardLevel.high: 2,
          RewardLevel.low: 1,
          RewardLevel.notSet: 10,
        },
        difficultyCounts: const {
          DifficultyLevel.medium: 3,
          DifficultyLevel.hard: 1,
        },
        reminderProfileCounts: const {
          ReminderProfile.persistent: 3,
          ReminderProfile.gentle: 1,
        },
      );

      final suggestions = service.suggestionsFor(statistics);

      expect(suggestions.durationMinutes!.value, 25);
      expect(suggestions.durationMinutes!.sampleSize, 4);
      expect(suggestions.durationMinutes!.supportingSampleCount, 2);
      expect(
        suggestions.completionTimeBucket!.value,
        ProjectCompletionTimeBucket.morning,
      );
      expect(suggestions.completionTimeBucket!.confidence,
          ProjectSuggestionConfidence.high);
      expect(suggestions.reward!.value, RewardLevel.high);
      expect(suggestions.reward!.sampleSize, 3);
      expect(suggestions.difficulty!.value, DifficultyLevel.medium);
      expect(suggestions.reminderProfile!.value, ReminderProfile.persistent);
    });

    test('does not suggest when samples are insufficient', () {
      final statistics = ProjectStatistics(
        projectId: 'home',
        completedTaskCount: 2,
        durationMinuteCounts: const {25: 2},
        completionTimeBucketCounts: const {
          ProjectCompletionTimeBucket.morning: 2,
        },
        rewardCounts: const {RewardLevel.high: 2},
        difficultyCounts: const {DifficultyLevel.medium: 2},
        reminderProfileCounts: const {ReminderProfile.gentle: 2},
      );

      final suggestions = service.suggestionsFor(statistics);

      expect(suggestions.durationMinutes, isNull);
      expect(suggestions.completionTimeBucket, isNull);
      expect(suggestions.reward, isNull);
      expect(suggestions.difficulty, isNull);
      expect(suggestions.reminderProfile, isNull);
    });

    test('does not choose categorical suggestions on ties', () {
      final statistics = ProjectStatistics(
        projectId: 'home',
        completedTaskCount: 4,
        completionTimeBucketCounts: const {
          ProjectCompletionTimeBucket.morning: 2,
          ProjectCompletionTimeBucket.evening: 2,
        },
        rewardCounts: const {
          RewardLevel.high: 2,
          RewardLevel.low: 2,
        },
        difficultyCounts: const {
          DifficultyLevel.easy: 2,
          DifficultyLevel.hard: 2,
        },
        reminderProfileCounts: const {
          ReminderProfile.gentle: 2,
          ReminderProfile.persistent: 2,
        },
      );

      final suggestions = service.suggestionsFor(statistics);

      expect(suggestions.completionTimeBucket, isNull);
      expect(suggestions.reward, isNull);
      expect(suggestions.difficulty, isNull);
      expect(suggestions.reminderProfile, isNull);
    });

    test('stable recomputation is independent of observation order', () {
      final first = ProjectStatistics(projectId: 'home')
          .recordCompletion(
            completedAt: DateTime.utc(2026, 6, 25, 9),
            durationMinutes: 20,
            reward: RewardLevel.high,
          )
          .recordCompletion(
            completedAt: DateTime.utc(2026, 6, 25, 18),
            durationMinutes: 40,
            reward: RewardLevel.low,
          )
          .recordCompletion(
            completedAt: DateTime.utc(2026, 6, 25, 10),
            durationMinutes: 20,
            reward: RewardLevel.high,
          );
      final second = ProjectStatistics(projectId: 'home')
          .recordCompletion(
            completedAt: DateTime.utc(2026, 6, 25, 18),
            durationMinutes: 40,
            reward: RewardLevel.low,
          )
          .recordCompletion(
            completedAt: DateTime.utc(2026, 6, 25, 10),
            durationMinutes: 20,
            reward: RewardLevel.high,
          )
          .recordCompletion(
            completedAt: DateTime.utc(2026, 6, 25, 9),
            durationMinutes: 20,
            reward: RewardLevel.high,
          );

      final firstSuggestions = service.suggestionsFor(first);
      final secondSuggestions = service.suggestionsFor(second);

      expect(firstSuggestions.durationMinutes!.value, 20);
      expect(secondSuggestions.durationMinutes!.value, 20);
      expect(firstSuggestions.reward!.value, RewardLevel.high);
      expect(secondSuggestions.reward!.value, RewardLevel.high);
    });

    test('configured defaults remain authoritative beside suggestions', () {
      final project = ProjectProfile(
        id: 'home',
        name: 'Home',
        colorKey: 'green',
        defaultReward: RewardLevel.high,
        defaultDifficulty: DifficultyLevel.hard,
        defaultReminderProfile: ReminderProfile.strict,
        defaultDurationMinutes: 45,
      );
      final statistics = ProjectStatistics(
        projectId: 'home',
        completedTaskCount: 3,
        durationMinuteCounts: const {20: 3},
        rewardCounts: const {RewardLevel.low: 3},
        difficultyCounts: const {DifficultyLevel.easy: 3},
        reminderProfileCounts: const {ReminderProfile.gentle: 3},
      );

      final resolution = service.resolveDefaults(
        project: project,
        statistics: statistics,
      );

      expect(resolution.configuredDurationMinutes, 45);
      expect(resolution.configuredReward, RewardLevel.high);
      expect(resolution.configuredDifficulty, DifficultyLevel.hard);
      expect(resolution.configuredReminderProfile, ReminderProfile.strict);
      expect(resolution.suggestions.durationMinutes!.value, 20);
      expect(resolution.suggestions.reward!.value, RewardLevel.low);
      expect(resolution.suggestions.difficulty!.value, DifficultyLevel.easy);
      expect(
        resolution.suggestions.reminderProfile!.value,
        ReminderProfile.gentle,
      );
      expect(project.defaultReward, RewardLevel.high);
    });
  });
}

/// 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 String projectId,
  required DateTime createdAt,
  required DateTime start,
  RewardLevel reward = RewardLevel.notSet,
  DifficultyLevel difficulty = DifficultyLevel.notSet,
  ReminderProfile? reminderOverride,
}) {
  return Task(
    id: id,
    title: 'Task',
    projectId: projectId,
    type: TaskType.flexible,
    status: TaskStatus.planned,
    reward: reward,
    difficulty: difficulty,
    durationMinutes: 30,
    scheduledStart: start,
    scheduledEnd: start.add(const Duration(minutes: 30)),
    reminderOverride: reminderOverride,
    createdAt: createdAt,
    updatedAt: createdAt,
  );
}
