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

/// Tests Application Management 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('V1ApplicationManagementUseCases', () {
    final now = DateTime.utc(2026, 6, 25, 12);

    test('queries backlog with owner thresholds, filters, and sorting',
        () async {
      final fresh = backlogTask(
        id: 'fresh',
        createdAt: now.subtract(const Duration(days: 1)),
      );
      final aging = backlogTask(
        id: 'aging',
        createdAt: now.subtract(const Duration(days: 5)),
        pushed: true,
      );
      final stale = backlogTask(
        id: 'stale',
        createdAt: now.subtract(const Duration(days: 10)),
        pushed: true,
      );
      final planned = backlogTask(
        id: 'planned',
        createdAt: now.subtract(const Duration(days: 40)),
      ).copyWith(status: TaskStatus.planned);
      final store = InMemoryApplicationUnitOfWork(
        initialTasks: [fresh, aging, stale, planned],
        initialOwnerSettings: [
          OwnerSettings(
            ownerId: 'owner-1',
            timeZoneId: 'UTC',
            backlogStalenessSettings: const BacklogStalenessSettings(
              greenMaxAge: Duration(days: 2),
              blueMaxAge: Duration(days: 6),
            ),
          ),
        ],
      );
      final useCases = V1ApplicationManagementUseCases(
        applicationStore: store,
      );

      final result = await useCases.getBacklog(
        GetBacklogRequest(
          context: appContext(operationId: 'read-backlog', now: now),
          filter: BacklogFilter.pushed,
          sortKey: BacklogSortKey.age,
        ),
      );

      final view = result.requireValue;
      expect(view.items.map((item) => item.task.id), ['stale', 'aging']);
      expect(view.items.map((item) => item.stalenessMarker), [
        BacklogStalenessMarker.purple,
        BacklogStalenessMarker.blue,
      ]);
      expect(
        view.items.any((item) => item.task.id == planned.id),
        isFalse,
      );
    });

    test('backlog board read models snapshot collection inputs', () {
      final childPreviews = [
        const BacklogChildPreviewReadModel(
          taskId: 'child-1',
          title: 'Gather statements',
          isCompleted: false,
          durationMinutes: 15,
          statusLabel: 'Backlog',
        ),
      ];
      final itemTags = ['finance'];
      final item = BacklogBoardItemReadModel(
        taskId: 'review-budget',
        title: 'Review Q3 budget',
        subtitle: 'Check the draft before planning.',
        projectId: 'work',
        projectName: 'Work',
        projectColorToken: 'project-work',
        durationMinutes: 60,
        priority: PriorityLevel.high,
        reward: RewardLevel.medium,
        difficulty: DifficultyLevel.easy,
        bucket: BacklogBoardBucket.needTimeBlock,
        bucketReason: 'Needs a focused time block.',
        createdAt: now,
        updatedAt: now,
        childPreview: childPreviews,
        tags: itemTags,
      );

      childPreviews.clear();
      itemTags.add('late');

      expect(item.childPreview, hasLength(1));
      expect(item.tags, ['finance']);
      expect(() => item.tags.add('new'), throwsUnsupportedError);

      final items = [item];
      final column = BacklogBoardColumnReadModel(
        bucket: BacklogBoardBucket.needTimeBlock,
        title: 'Need Time Block',
        subtitle: 'Plan deliberately.',
        accentToken: 'backlog-time-block',
        items: items,
      );
      items.clear();

      expect(column.count, 1);
      expect(column.items, [item]);
      expect(() => column.items.clear(), throwsUnsupportedError);

      final entries = [
        const BacklogDistributionEntryReadModel(
          id: 'high',
          label: 'High',
          count: 2,
          percentage: 1,
          colorToken: 'priority-high',
        ),
      ];
      final priorityDistribution = BacklogDistributionReadModel(
        title: 'Priority',
        entries: entries,
      );
      entries.clear();

      expect(priorityDistribution.totalCount, 2);
      expect(priorityDistribution.entries, hasLength(1));
      expect(
        () => priorityDistribution.entries.clear(),
        throwsUnsupportedError,
      );

      final projects = [
        const ProjectOptionReadModel(
          projectId: 'work',
          name: 'Work',
          colorToken: 'project-work',
        ),
      ];
      final summary = BacklogBoardSummaryReadModel(
        totalTaskCount: 1,
        totalEstimatedMinutes: 60,
        priorityDistribution: priorityDistribution,
        projectDistribution: BacklogDistributionReadModel(
          title: 'Project',
          entries: const [
            BacklogDistributionEntryReadModel(
              id: 'work',
              label: 'Work',
              count: 1,
              percentage: 1,
              colorToken: 'project-work',
            ),
          ],
        ),
        durationDistribution: BacklogDistributionReadModel(
          title: 'Duration',
          entries: const [
            BacklogDistributionEntryReadModel(
              id: '30-60',
              label: '30-60',
              count: 1,
              percentage: 1,
            ),
          ],
        ),
        planningTip: 'Pick one focused task before adding more.',
      );
      final result = BacklogBoardQueryResult(
        ownerId: 'owner-1',
        localDate: CivilDate(2026, 6, 25),
        summary: summary,
        columns: [column],
        projectOptions: projects,
      );
      projects.clear();

      expect(result.columns, [column]);
      expect(result.projectOptions, hasLength(1));
      expect(() => result.projectOptions.clear(), throwsUnsupportedError);

      final detail = BacklogTaskDetailReadModel(
        item: item,
        addedLabel: 'Added today',
        tags: const ['finance'],
        projectOptions: result.projectOptions,
        suggestedSlots: const [
          SuggestedSlotReadModel(
            id: 'today-1',
            localDateLabel: 'Today',
            startText: '2:00 PM',
            endText: '3:00 PM',
            durationMinutes: 60,
            fitLabel: 'Great Fit',
            fitSeverityToken: 'fit-great',
            isPrimary: true,
          ),
        ],
        notes: 'Review budget categories before scheduling.',
      );

      expect(detail.tags, ['finance']);
      expect(detail.suggestedSlots.single.fitLabel, 'Great Fit');
      expect(() => detail.suggestedSlots.clear(), throwsUnsupportedError);
    });

    test('returns empty backlog board with stable columns', () async {
      final store = InMemoryApplicationUnitOfWork();
      final result = await V1ApplicationManagementUseCases(
        applicationStore: store,
      ).getBacklogBoard(
        GetBacklogBoardRequest(
          context: appContext(operationId: 'empty-board', now: now),
          localDate: CivilDate(2026, 6, 25),
        ),
      );

      final board = result.requireValue;

      expect(board.ownerId, 'owner-1');
      expect(board.columns.map((column) => column.bucket), [
        BacklogBoardBucket.doNext,
        BacklogBoardBucket.needTimeBlock,
        BacklogBoardBucket.breakUpFirst,
        BacklogBoardBucket.notNow,
      ]);
      expect(board.columns.map((column) => column.count), [0, 0, 0, 0]);
      expect(
        board.columns.every((column) => column.items.isEmpty),
        isTrue,
      );
      expect(board.summary.totalTaskCount, 0);
      expect(board.summary.totalEstimatedMinutes, 0);
      expect(board.summary.projectDistribution.entries, isEmpty);
    });

    test('groups backlog board tasks into buckets and matching summaries',
        () async {
      final tasks = [
        backlogTask(
          id: 'quick',
          title: 'Reply to 3 emails',
          projectId: 'home',
          createdAt: now.subtract(const Duration(days: 1)),
          durationMinutes: 20,
          priority: PriorityLevel.medium,
          reward: RewardLevel.high,
          difficulty: DifficultyLevel.easy,
        ),
        backlogTask(
          id: 'block',
          title: 'Review Q3 budget',
          projectId: 'work',
          createdAt: now.subtract(const Duration(days: 2)),
          durationMinutes: 60,
          priority: PriorityLevel.high,
          reward: RewardLevel.medium,
          difficulty: DifficultyLevel.medium,
        ),
        backlogTask(
          id: 'break',
          title: 'Plan next week',
          projectId: 'work',
          createdAt: now.subtract(const Duration(days: 3)),
          durationMinutes: 120,
          priority: PriorityLevel.medium,
          reward: RewardLevel.medium,
          difficulty: DifficultyLevel.medium,
        ),
        backlogTask(
          id: 'later',
          title: 'Learn watercolor',
          projectId: 'personal',
          createdAt: now.subtract(const Duration(days: 4)),
          durationMinutes: 15,
          priority: PriorityLevel.low,
          backlogTags: const {BacklogTag.wishlist},
        ),
        backlogTask(
          id: 'planned',
          title: 'Already planned',
          projectId: 'home',
          createdAt: now,
          status: TaskStatus.planned,
          scheduledStart: DateTime.utc(2026, 6, 25, 15),
          scheduledEnd: DateTime.utc(2026, 6, 25, 15, 30),
        ),
      ];
      final store = InMemoryApplicationUnitOfWork(
        initialProjects: [
          ProjectProfile(id: 'home', name: 'Home', colorKey: 'home-blue'),
          ProjectProfile(id: 'personal', name: 'Personal', colorKey: 'pink'),
          ProjectProfile(id: 'work', name: 'Work', colorKey: 'work-green'),
        ],
        initialTasks: tasks,
      );

      final board = (await V1ApplicationManagementUseCases(
        applicationStore: store,
      ).getBacklogBoard(
        GetBacklogBoardRequest(
          context: appContext(operationId: 'bucket-board', now: now),
          localDate: CivilDate(2026, 6, 25),
          sortKey: BacklogSortKey.age,
        ),
      ))
          .requireValue;
      BacklogBoardColumnReadModel column(BacklogBoardBucket bucket) {
        return board.columns.singleWhere((column) => column.bucket == bucket);
      }

      expect(column(BacklogBoardBucket.doNext).items.single.taskId, 'quick');
      expect(
        column(BacklogBoardBucket.needTimeBlock).items.single.taskId,
        'block',
      );
      expect(
        column(BacklogBoardBucket.breakUpFirst).items.single.taskId,
        'break',
      );
      expect(column(BacklogBoardBucket.notNow).items.single.taskId, 'later');
      expect(
        board.columns
            .expand((column) => column.items)
            .map((item) => item.taskId),
        isNot(contains('planned')),
      );
      expect(board.summary.totalTaskCount, 4);
      expect(board.summary.totalEstimatedMinutes, 215);
      expect(
        board.summary.projectDistribution.entries.map((entry) => entry.label),
        ['Home', 'Personal', 'Work'],
      );
      expect(
        board.summary.projectDistribution.entries.map((entry) => entry.count),
        [1, 1, 2],
      );
    });

    test('applies backlog board search, filters, and project grouping',
        () async {
      final store = InMemoryApplicationUnitOfWork(
        initialProjects: [
          ProjectProfile(id: 'home', name: 'Home', colorKey: 'home-blue'),
          ProjectProfile(id: 'work', name: 'Work', colorKey: 'work-green'),
        ],
        initialTasks: [
          backlogTask(
            id: 'home-wish',
            title: 'Organize hallway closet',
            projectId: 'home',
            createdAt: now.subtract(const Duration(days: 1)),
            backlogTags: const {BacklogTag.wishlist},
          ),
          backlogTask(
            id: 'work-wish',
            title: 'Read optional design notes',
            projectId: 'work',
            createdAt: now.subtract(const Duration(days: 2)),
            backlogTags: const {BacklogTag.wishlist},
          ),
          backlogTask(
            id: 'not-filtered',
            title: 'Someday wording but normal backlog',
            projectId: 'work',
            createdAt: now.subtract(const Duration(days: 3)),
          ),
        ],
      );

      final board = (await V1ApplicationManagementUseCases(
        applicationStore: store,
      ).getBacklogBoard(
        GetBacklogBoardRequest(
          context: appContext(operationId: 'search-board', now: now),
          localDate: CivilDate(2026, 6, 25),
          searchText: 'someday',
          filters: const [BacklogFilter.wishlist],
          groupMode: BacklogBoardGroupMode.project,
        ),
      ))
          .requireValue;
      final notNow = board.columns.singleWhere(
        (column) => column.bucket == BacklogBoardBucket.notNow,
      );

      expect(board.summary.totalTaskCount, 2);
      expect(notNow.items.map((item) => item.taskId), [
        'home-wish',
        'work-wish',
      ]);
      expect(
          notNow.items.every((item) => item.tags.contains('someday')), isTrue);
      expect(
        board.columns
            .expand((column) => column.items)
            .map((item) => item.taskId),
        isNot(contains('not-filtered')),
      );
    });

    test('loads backlog task detail with child previews', () async {
      final parent = backlogTask(
        id: 'parent',
        title: 'Plan next week',
        createdAt: now.subtract(const Duration(days: 5)),
        durationMinutes: 30,
      );
      final childOne = backlogTask(
        id: 'child-1',
        title: 'Define weekly goals',
        createdAt: now.subtract(const Duration(days: 4)),
        durationMinutes: 15,
        parentTaskId: parent.id,
      );
      final childTwo = backlogTask(
        id: 'child-2',
        title: 'Time block schedule',
        createdAt: now.subtract(const Duration(days: 3)),
        durationMinutes: 20,
        parentTaskId: parent.id,
        status: TaskStatus.completed,
      );
      final store = InMemoryApplicationUnitOfWork(
        initialTasks: [parent, childTwo, childOne],
      );

      final detail = (await V1ApplicationManagementUseCases(
        applicationStore: store,
      ).getBacklogTaskDetail(
        GetBacklogTaskDetailRequest(
          context: appContext(operationId: 'detail-children', now: now),
          localDate: CivilDate(2026, 6, 25),
          taskId: parent.id,
        ),
      ))
          .requireValue;

      expect(detail.item.bucket, BacklogBoardBucket.breakUpFirst);
      expect(detail.item.hasChildren, isTrue);
      expect(detail.item.childPreview.map((child) => child.title), [
        'Define weekly goals',
        'Time block schedule',
      ]);
      expect(detail.item.childPreview.map((child) => child.statusLabel), [
        'Backlog',
        'Completed',
      ]);
      expect(detail.notes, isNull);
    });

    test('returns not found for missing or non-backlog task details', () async {
      final store = InMemoryApplicationUnitOfWork(
        initialTasks: [
          backlogTask(
            id: 'planned',
            createdAt: now,
            status: TaskStatus.planned,
            scheduledStart: DateTime.utc(2026, 6, 25, 14),
            scheduledEnd: DateTime.utc(2026, 6, 25, 14, 30),
          ),
        ],
      );
      final useCases = V1ApplicationManagementUseCases(
        applicationStore: store,
      );

      final missing = await useCases.getBacklogTaskDetail(
        GetBacklogTaskDetailRequest(
          context: appContext(operationId: 'detail-missing', now: now),
          localDate: CivilDate(2026, 6, 25),
          taskId: 'missing',
        ),
      );
      final nonBacklog = await useCases.getBacklogTaskDetail(
        GetBacklogTaskDetailRequest(
          context: appContext(operationId: 'detail-planned', now: now),
          localDate: CivilDate(2026, 6, 25),
          taskId: 'planned',
        ),
      );

      expect(missing.failure!.code, ApplicationFailureCode.notFound);
      expect(nonBacklog.failure!.code, ApplicationFailureCode.notFound);
    });

    test('previews detail suggested slots without mutating state', () async {
      final day = CivilDate(2026, 6, 25);
      final candidate = backlogTask(
        id: 'candidate',
        title: 'Update project status',
        createdAt: now,
        durationMinutes: 30,
      );
      final existingBlock = backlogTask(
        id: 'existing-block',
        title: 'Existing meeting',
        createdAt: now,
        type: TaskType.inflexible,
        status: TaskStatus.planned,
        scheduledStart: DateTime.utc(2026, 6, 25, 9),
        scheduledEnd: DateTime.utc(2026, 6, 25, 10),
      );
      final store = InMemoryApplicationUnitOfWork(
        initialOwnerSettings: [
          OwnerSettings(
            ownerId: 'owner-1',
            timeZoneId: 'UTC',
            dayStart: WallTime(hour: 9, minute: 0),
            dayEnd: WallTime(hour: 12, minute: 0),
          ),
        ],
        initialTasks: [candidate, existingBlock],
      );
      final beforeTasks = store.currentTasks;

      final detail = (await V1ApplicationManagementUseCases(
        applicationStore: store,
        timeZoneResolver: const FixedOffsetTimeZoneResolver.utc(),
      ).getBacklogTaskDetail(
        GetBacklogTaskDetailRequest(
          context: appContext(
            operationId: 'detail-suggestions',
            now: DateTime.utc(2026, 6, 25, 8),
          ),
          localDate: day,
          taskId: candidate.id,
        ),
      ))
          .requireValue;

      expect(detail.suggestedSlots.first.startText, '10:00 AM');
      expect(detail.suggestedSlots.first.endText, '10:30 AM');
      expect(detail.suggestedSlots.first.fitLabel, 'Great Fit');
      expect(detail.suggestedSlotUnavailableReason, isNull);
      expect(store.currentTasks, beforeTasks);
      expect(store.currentSchedulingSnapshots, isEmpty);
      expect(store.currentOperations, isEmpty);
    });

    test('returns missing-duration suggestion reason without crashing',
        () async {
      final task = backlogTask(
        id: 'missing-duration',
        title: 'Clarify estimate',
        createdAt: now,
        durationMinutes: null,
      );
      final store = InMemoryApplicationUnitOfWork(initialTasks: [task]);

      final detail = (await V1ApplicationManagementUseCases(
        applicationStore: store,
      ).getBacklogTaskDetail(
        GetBacklogTaskDetailRequest(
          context: appContext(operationId: 'detail-no-duration', now: now),
          localDate: CivilDate(2026, 6, 25),
          taskId: task.id,
        ),
      ))
          .requireValue;

      expect(detail.suggestedSlots, isEmpty);
      expect(
        detail.suggestedSlotUnavailableReason,
        'Add an estimate to see suggested slots.',
      );
    });

    test('keeps configured project defaults separate from learned suggestions',
        () async {
      final project = ProjectProfile(
        id: 'home',
        name: 'Home',
        colorKey: 'home-blue',
        defaultReward: RewardLevel.low,
        defaultDifficulty: DifficultyLevel.easy,
        defaultReminderProfile: ReminderProfile.gentle,
        defaultDurationMinutes: 20,
      );
      final statistics = ProjectStatistics(
        projectId: 'home',
        completedTaskCount: 3,
        durationMinuteCounts: const {45: 3},
        rewardCounts: const {RewardLevel.high: 3},
        difficultyCounts: const {DifficultyLevel.hard: 3},
        reminderProfileCounts: const {ReminderProfile.persistent: 3},
      );
      final store = InMemoryApplicationUnitOfWork(
        initialProjects: [project],
        initialProjectStatistics: [statistics],
      );

      final result = await V1ApplicationManagementUseCases(
        applicationStore: store,
      ).getProjectDefaults(
        GetProjectDefaultsRequest(
          context: appContext(operationId: 'read-projects', now: now),
        ),
      );

      final resolution = result.requireValue.projects.single;
      expect(resolution.configuredDurationMinutes, 20);
      expect(resolution.configuredReward, RewardLevel.low);
      expect(resolution.configuredDifficulty, DifficultyLevel.easy);
      expect(resolution.configuredReminderProfile, ReminderProfile.gentle);
      expect(resolution.suggestions.durationMinutes!.value, 45);
      expect(resolution.suggestions.reward!.value, RewardLevel.high);
      expect(resolution.suggestions.difficulty!.value, DifficultyLevel.hard);
      expect(
        resolution.suggestions.reminderProfile!.value,
        ReminderProfile.persistent,
      );
    });

    test('archives projects without deleting task history', () async {
      final task = backlogTask(id: 'task-1', projectId: 'home', createdAt: now);
      final store = InMemoryApplicationUnitOfWork(
        initialProjects: [
          ProjectProfile(id: 'home', name: 'Home', colorKey: 'home-blue'),
        ],
        initialTasks: [task],
      );
      final useCases = V1ApplicationManagementUseCases(
        applicationStore: store,
      );

      final archive = await useCases.archiveProject(
        context: appContext(operationId: 'archive-project', now: now),
        projectId: 'home',
      );
      final visibleProjects = await useCases.getProjectDefaults(
        GetProjectDefaultsRequest(
          context: appContext(operationId: 'read-active-projects', now: now),
        ),
      );
      final allProjects = await useCases.getProjectDefaults(
        GetProjectDefaultsRequest(
          context: appContext(operationId: 'read-all-projects', now: now),
          includeArchived: true,
        ),
      );

      expect(archive.requireValue.isArchived, isTrue);
      expect(visibleProjects.requireValue.projects, isEmpty);
      expect(allProjects.requireValue.projects.single.project.id, 'home');
      expect(store.currentTasks.single.id, task.id);
      expect(store.currentTasks.single.projectId, 'home');
    });

    test('creates date-scoped locked overrides and archives blocks safely',
        () async {
      final day = CivilDate(2026, 6, 25);
      final block = LockedBlock(
        id: 'work',
        name: 'Work',
        startTime: WallTime(hour: 9, minute: 0),
        endTime: WallTime(hour: 17, minute: 0),
        recurrence: LockedBlockRecurrence.weekly(
          weekdays: {LockedWeekday.thursday},
        ),
        createdAt: now,
        updatedAt: now,
      );
      final store = InMemoryApplicationUnitOfWork(
        initialLockedBlocks: [block],
      );
      final useCases = V1ApplicationManagementUseCases(
        applicationStore: store,
      );

      final replacement = await useCases.replaceOneDayLockedOccurrence(
        context: appContext(operationId: 'replace-work', now: now),
        lockedBlockId: 'work',
        date: day,
        startTime: WallTime(hour: 10, minute: 0),
        endTime: WallTime(hour: 12, minute: 0),
        name: 'Short work',
      );
      await useCases.addOneDayLockedOverride(
        context: appContext(operationId: 'add-appointment', now: now),
        date: day,
        name: 'Appointment',
        startTime: WallTime(hour: 14, minute: 0),
        endTime: WallTime(hour: 15, minute: 0),
      );

      var expansion = expandLockedBlocksForDay(
        blocks: store.currentLockedBlocks,
        overrides: store.currentLockedOverrides,
        date: day,
        timeZoneId: 'UTC',
        timeZoneResolver: const FixedOffsetTimeZoneResolver.utc(),
      );
      expect(replacement.requireValue.type, LockedBlockOverrideType.replace);
      expect(expansion.occurrences.map((occurrence) => occurrence.name), [
        'Short work',
        'Appointment',
      ]);

      await useCases.archiveLockedBlock(
        context: appContext(operationId: 'archive-work', now: now),
        lockedBlockId: 'work',
      );
      expansion = expandLockedBlocksForDay(
        blocks: store.currentLockedBlocks,
        overrides: store.currentLockedOverrides,
        date: day,
        timeZoneId: 'UTC',
        timeZoneResolver: const FixedOffsetTimeZoneResolver.utc(),
      );

      expect(store.currentLockedBlocks.single.isArchived, isTrue);
      expect(store.currentLockedOverrides, hasLength(2));
      expect(expansion.occurrences.map((occurrence) => occurrence.name), [
        'Appointment',
      ]);
    });

    test('updates owner settings with typed reinterpretation warnings',
        () async {
      final store = InMemoryApplicationUnitOfWork(
        initialOwnerSettings: [
          OwnerSettings(
            ownerId: 'owner-1',
            timeZoneId: 'UTC',
            dayStart: WallTime(hour: 8, minute: 0),
            dayEnd: WallTime(hour: 20, minute: 0),
          ),
        ],
      );

      final result = await V1ApplicationManagementUseCases(
        applicationStore: store,
      ).updateOwnerSettings(
        context: appContext(operationId: 'settings', now: now),
        update: OwnerSettingsUpdate(
          timeZoneId: 'America/Los_Angeles',
          dayStart: WallTime(hour: 7, minute: 0),
          compactModeEnabled: true,
          backlogStalenessSettings: const BacklogStalenessSettings(
            greenMaxAge: Duration(days: 3),
            blueMaxAge: Duration(days: 8),
          ),
        ),
      );

      final update = result.requireValue;
      expect(update.settings.timeZoneId, 'America/Los_Angeles');
      expect(update.settings.dayStart, WallTime(hour: 7, minute: 0));
      expect(update.settings.compactModeEnabled, isTrue);
      expect(update.settings.backlogStalenessSettings.blueMaxAge.inDays, 8);
      expect(update.warnings, [
        OwnerSettingsWarningCode.timeZoneChanged,
        OwnerSettingsWarningCode.planningWindowChanged,
      ]);
    });

    test('acknowledges one-time notices and hides them from Today state',
        () async {
      final day = CivilDate(2026, 6, 25);
      final snapshot = SchedulingStateSnapshot(
        id: 'rollover',
        capturedAt: now,
        window: SchedulingWindow(
          start: DateTime.utc(2026, 6, 25),
          end: DateTime.utc(2026, 6, 25, 23, 59),
        ),
        tasks: const [],
        notices: [
          SchedulingNotice(
            'Moved 2 unfinished tasks to today.',
            type: SchedulingNoticeType.moved,
            movementCode:
                SchedulingMovementCode.unfinishedFlexibleTasksRolledOver,
            parameters: const {'count': 2},
          ),
        ],
      );
      final store = InMemoryApplicationUnitOfWork(
        initialSchedulingSnapshots: [snapshot],
      );
      final todayQuery = GetTodayStateQuery(
        applicationStore: store,
        timeZoneResolver: const FixedOffsetTimeZoneResolver.utc(),
      );
      final useCases = V1ApplicationManagementUseCases(
        applicationStore: store,
      );

      final before = (await todayQuery.execute(
        GetTodayStateRequest(
          context: appContext(operationId: 'today-before', now: now),
          date: day,
        ),
      ))
          .requireValue;
      final noticeId = before.pendingNotices.single.noticeId;

      final acknowledged = await useCases.acknowledgeNotice(
        context: appContext(operationId: 'ack-notice', now: now),
        noticeId: noticeId,
      );
      final after = (await todayQuery.execute(
        GetTodayStateRequest(
          context: appContext(operationId: 'today-after', now: now),
          date: day,
        ),
      ))
          .requireValue;

      expect(
          noticeId, schedulingNoticeId(snapshotId: 'rollover', noticeIndex: 0));
      expect(acknowledged.requireValue.alreadyAcknowledged, isFalse);
      expect(store.currentNoticeAcknowledgements.single.noticeId, noticeId);
      expect(after.pendingNotices, isEmpty);
    });
  });
}

/// Runs the `appContext` top-level operation.
/// This function is part of the file-level API and gives callers a direct way to use the associated scheduler behavior.
ApplicationOperationContext appContext({
  required String operationId,
  required DateTime now,
}) {
  return ApplicationOperationContext.start(
    operationId: operationId,
    ownerTimeZone: OwnerTimeZoneContext(ownerId: 'owner-1', timeZoneId: 'UTC'),
    clock: FixedClock(now),
    idGenerator: SequentialIdGenerator(prefix: operationId),
  );
}

/// Runs the `backlogTask` 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 backlogTask({
  required String id,
  required DateTime createdAt,
  String? title,
  String projectId = 'inbox',
  TaskType type = TaskType.flexible,
  TaskStatus status = TaskStatus.backlog,
  PriorityLevel? priority = PriorityLevel.medium,
  RewardLevel reward = RewardLevel.notSet,
  DifficultyLevel difficulty = DifficultyLevel.notSet,
  int? durationMinutes = 30,
  DateTime? scheduledStart,
  DateTime? scheduledEnd,
  String? parentTaskId,
  Set<BacklogTag> backlogTags = const <BacklogTag>{},
  bool pushed = false,
}) {
  return Task(
    id: id,
    title: title ?? 'Task $id',
    projectId: projectId,
    type: type,
    status: status,
    priority: priority,
    reward: reward,
    difficulty: difficulty,
    durationMinutes: durationMinutes,
    scheduledStart: scheduledStart,
    scheduledEnd: scheduledEnd,
    parentTaskId: parentTaskId,
    backlogTags: backlogTags,
    createdAt: createdAt,
    updatedAt: createdAt,
    stats: pushed
        ? const TaskStatistics(manuallyPushedCount: 1)
        : const TaskStatistics(),
  );
}
