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

/// Tests SQLite application unit-of-work behavior.
library;

import 'dart:io';

import 'package:scheduler_core/scheduler_core.dart';
import 'package:scheduler_persistence_sqlite/sqlite.dart';
import 'package:test/test.dart';

/// Runs SQLite application unit-of-work tests.
void main() {
  test('bootstrap is idempotent across close and reopen', () async {
    final path = await _tempDatabasePath();
    var runtime = await _openBootstrappedRuntime(path);

    var settingsRows = await runtime.db.select(runtime.db.settingsTable).get();
    var projectRows = await runtime.db.select(runtime.db.projects).get();
    expect(settingsRows, hasLength(1));
    expect(projectRows, hasLength(1));
    await runtime.close();

    runtime = await _openBootstrappedRuntime(path);
    addTearDown(runtime.close);
    settingsRows = await runtime.db.select(runtime.db.settingsTable).get();
    projectRows = await runtime.db.select(runtime.db.projects).get();
    expect(settingsRows, hasLength(1));
    expect(projectRows, hasLength(1));
  });

  test('quick capture schedule complete and uncomplete survive reopen',
      () async {
    final path = await _tempDatabasePath();
    var runtime = await _openBootstrappedRuntime(path);
    var commands = _commands(runtime);
    var management = _management(runtime);
    var todayQuery = _todayQuery(runtime);

    final capture = await commands.quickCaptureToBacklog(
      context: _context('capture-task', _instant(8)),
      title: 'Persist close reopen task',
      projectId: SqliteApplicationRuntime.defaultV1ProjectId,
    );
    expect(capture.isSuccess, isTrue);
    await runtime.close();

    runtime = await _openBootstrappedRuntime(path);
    commands = _commands(runtime);
    management = _management(runtime);
    todayQuery = _todayQuery(runtime);
    final backlog = await management.getBacklog(
      GetBacklogRequest(
        context: _context('read-backlog-after-capture', _instant(8, 5)),
        sortKey: BacklogSortKey.age,
      ),
    );
    expect(backlog.requireValue.items, hasLength(1));
    final task = backlog.requireValue.items.single.task;
    expect(task.title, 'Persist close reopen task');

    final scheduled = await commands.scheduleBacklogItemToNextAvailableSlot(
      context: _context('schedule-task', _instant(8, 10)),
      localDate: _date,
      taskId: task.id,
      durationMinutes: 30,
      expectedUpdatedAt: task.updatedAt,
    );
    expect(scheduled.isSuccess, isTrue);
    await runtime.close();

    runtime = await _openBootstrappedRuntime(path);
    commands = _commands(runtime);
    todayQuery = _todayQuery(runtime);
    var today = await todayQuery.execute(
      GetTodayStateRequest(
        context: _context('read-today-after-schedule', _instant(8, 15)),
        date: _date,
      ),
    );
    var scheduledTask = today.requireValue.timelineItems
        .singleWhere((item) => item.taskId == task.id);
    expect(scheduledTask.taskStatus, TaskStatus.planned);

    final completed = await commands.completeFlexibleTask(
      context: _context('complete-task', _instant(8, 20)),
      localDate: _date,
      taskId: task.id,
    );
    expect(completed.isSuccess, isTrue);
    await runtime.close();

    runtime = await _openBootstrappedRuntime(path);
    commands = _commands(runtime);
    todayQuery = _todayQuery(runtime);
    today = await todayQuery.execute(
      GetTodayStateRequest(
        context: _context('read-today-after-complete', _instant(8, 25)),
        date: _date,
      ),
    );
    scheduledTask = today.requireValue.timelineItems
        .singleWhere((item) => item.taskId == task.id);
    expect(scheduledTask.taskStatus, TaskStatus.completed);

    final reopened = await commands.uncompleteTask(
      context: _context('uncomplete-task', _instant(8, 30)),
      localDate: _date,
      taskId: task.id,
    );
    expect(reopened.isSuccess, isTrue);
    await runtime.close();

    runtime = await _openBootstrappedRuntime(path);
    commands = _commands(runtime);
    todayQuery = _todayQuery(runtime);
    today = await todayQuery.execute(
      GetTodayStateRequest(
        context: _context('read-today-after-uncomplete', _instant(8, 35)),
        date: _date,
      ),
    );
    scheduledTask = today.requireValue.timelineItems
        .singleWhere((item) => item.taskId == task.id);
    expect(scheduledTask.taskStatus, TaskStatus.planned);

    final removed = await commands.removeTask(
      context: _context('remove-task', _instant(8, 40)),
      taskId: task.id,
      expectedUpdatedAt: scheduledTask.item.updatedAt,
    );
    expect(removed.isSuccess, isTrue);
    await runtime.close();

    runtime = await _openBootstrappedRuntime(path);
    addTearDown(runtime.close);
    todayQuery = _todayQuery(runtime);
    today = await todayQuery.execute(
      GetTodayStateRequest(
        context: _context('read-today-after-remove', _instant(8, 45)),
        date: _date,
      ),
    );
    expect(
      today.requireValue.timelineItems.any((item) => item.taskId == task.id),
      isFalse,
    );
    final taskRows = await runtime.db.select(runtime.db.tasks).get();
    expect(taskRows, isEmpty);

    final activityRows =
        await runtime.db.select(runtime.db.taskActivities).get();
    final operationRows =
        await runtime.db.select(runtime.db.applicationOperations).get();
    expect(activityRows, isNotEmpty);
    expect(
        operationRows.map((row) => row.operationId), contains('capture-task'));
  });

  test('failed command rolls back task and operation writes', () async {
    final path = await _tempDatabasePath();
    final runtime = await _openBootstrappedRuntime(path);
    addTearDown(runtime.close);
    final commands = _commands(runtime);

    final result = await commands.quickCaptureToBacklog(
      context: _context('invalid-capture', _instant(9)),
      title: '',
      projectId: SqliteApplicationRuntime.defaultV1ProjectId,
    );

    expect(result.isFailure, isTrue);
    final taskRows = await runtime.db.select(runtime.db.tasks).get();
    final operationRows =
        await runtime.db.select(runtime.db.applicationOperations).get();
    expect(taskRows, isEmpty);
    expect(operationRows, isEmpty);
  });

  test('duplicate operation is rejected after reopen', () async {
    final path = await _tempDatabasePath();
    var runtime = await _openBootstrappedRuntime(path);
    var commands = _commands(runtime);

    final first = await commands.quickCaptureToBacklog(
      context: _context('duplicate-capture', _instant(10)),
      title: 'Only once',
      projectId: SqliteApplicationRuntime.defaultV1ProjectId,
    );
    expect(first.isSuccess, isTrue);
    await runtime.close();

    runtime = await _openBootstrappedRuntime(path);
    addTearDown(runtime.close);
    commands = _commands(runtime);
    final second = await commands.quickCaptureToBacklog(
      context: _context('duplicate-capture', _instant(10, 5)),
      title: 'Should not persist',
      projectId: SqliteApplicationRuntime.defaultV1ProjectId,
    );

    expect(second.failure?.code, ApplicationFailureCode.duplicateOperation);
    final taskRows = await runtime.db.select(runtime.db.tasks).get();
    expect(taskRows, hasLength(1));
    expect(taskRows.single.title, 'Only once');
  });

  test('application repositories persist owner scoped records and conflicts',
      () async {
    final path = await _tempDatabasePath();
    final runtime = await SqliteApplicationRuntime.open(path: path);
    addTearDown(runtime.close);
    const ownerId = SqliteApplicationRuntime.defaultV1OwnerId;
    final now = _instant(11);
    final project = _project('project-alpha', 'Alpha');
    final scheduledTask = _task(
      id: 'task-scheduled',
      title: 'Scheduled task',
      status: TaskStatus.planned,
      scheduledStart: _instant(8),
      scheduledEnd: _instant(8, 30),
    );
    final backlogTask = _task(
      id: 'task-backlog',
      title: 'Backlog task',
      status: TaskStatus.backlog,
      backlogTags: {BacklogTag.wishlist},
    );
    final childTask = _task(
      id: 'task-child',
      title: 'Child task',
      status: TaskStatus.backlog,
      parentTaskId: backlogTask.id,
    );
    final block = LockedBlock(
      id: 'block-work',
      name: 'Work block',
      date: _date,
      startTime: WallTime(hour: 9, minute: 0),
      endTime: WallTime(hour: 10, minute: 0),
      hiddenByDefault: false,
      projectId: project.id,
      createdAt: now,
      updatedAt: now,
    );
    final override = LockedBlockOverride.add(
      id: 'override-errand',
      date: _date,
      name: 'Errand',
      startTime: WallTime(hour: 13, minute: 0),
      endTime: WallTime(hour: 13, minute: 30),
      projectId: project.id,
      createdAt: now,
    );
    final snapshot = SchedulingStateSnapshot(
      id: 'snapshot-1',
      capturedAt: now,
      window: _window(8, 12),
      tasks: [scheduledTask],
      lockedIntervals: [
        TimeInterval(
          start: _instant(9),
          end: _instant(10),
          label: block.name,
        ),
      ],
      operationName: 'coverage-snapshot',
    );
    final activity = TaskActivity(
      id: 'activity-1',
      operationId: 'repository-surface',
      code: TaskActivityCode.completed,
      taskId: scheduledTask.id,
      projectId: project.id,
      occurredAt: now,
      metadata: {
        'completedAt': now,
        'reward': RewardLevel.high,
        'labels': ['done', 1],
        'nested': {'ok': true},
      },
    );
    final statistics = ProjectStatistics(
      projectId: project.id,
      completedTaskCount: 2,
      durationMinuteCounts: {30: 2},
      completionTimeBucketCounts: {
        ProjectCompletionTimeBucket.morning: 2,
      },
      totalPushesBeforeCompletion: 3,
      completedAfterPushCount: 1,
      rewardCounts: {RewardLevel.high: 2},
      difficultyCounts: {DifficultyLevel.easy: 1},
      reminderProfileCounts: {ReminderProfile.gentle: 2},
    );
    final settings = OwnerSettings(
      ownerId: ownerId,
      timeZoneId: 'UTC',
      dayStart: WallTime(hour: 7, minute: 30),
      dayEnd: WallTime(hour: 22, minute: 0),
      compactModeEnabled: true,
      backlogStalenessSettings: const BacklogStalenessSettings(
        greenMaxAge: Duration(days: 3),
        blueMaxAge: Duration(days: 10),
      ),
    );
    final notice = NoticeAcknowledgementRecord(
      ownerId: ownerId,
      noticeId: 'notice-rollover',
      acknowledgedAt: now,
    );

    final write = await runtime.applicationStore.run<Map<String, Object?>>(
      context: _context('repository-surface', now),
      operationName: 'repository-surface',
      action: (repositories) async {
        await repositories.ownerSettings.save(settings);
        final settingsUpdate = await repositories.ownerSettings.saveIfRevision(
          settings: settings.copyWith(compactModeEnabled: false),
          expectedRevision: 1,
        );
        final settingsStale = await repositories.ownerSettings.saveIfRevision(
          settings: settings,
          expectedRevision: 1,
        );

        final projectInsert = await repositories.projects.insert(
          project: project,
          ownerId: ownerId,
        );
        final projectDuplicate = await repositories.projects.insert(
          project: project,
          ownerId: ownerId,
        );
        final projectUpdate = await repositories.projects.saveIfRevision(
          project: project.copyWith(name: 'Alpha Renamed'),
          ownerId: ownerId,
          expectedRevision: projectInsert.revision!,
        );
        final projectArchive = await repositories.projects.archive(
          projectId: project.id,
          ownerId: ownerId,
          expectedRevision: projectUpdate.revision!,
          archivedAt: _instant(11, 5),
        );
        final projectMissingArchive = await repositories.projects.archive(
          projectId: 'missing-project',
          ownerId: ownerId,
          expectedRevision: 1,
          archivedAt: _instant(11, 5),
        );

        final taskInsert = await repositories.tasks.insert(
          task: scheduledTask,
          ownerId: ownerId,
        );
        final taskDuplicate = await repositories.tasks.insert(
          task: scheduledTask,
          ownerId: ownerId,
        );
        final taskUpdate = await repositories.tasks.saveIfRevision(
          task: scheduledTask.copyWith(title: 'Scheduled task updated'),
          ownerId: ownerId,
          expectedRevision: taskInsert.revision!,
        );
        await repositories.tasks.saveAll([backlogTask, childTask]);
        await repositories.tasks.save(
          backlogTask.copyWith(title: 'Backlog task updated'),
        );

        final blockInsert = await repositories.lockedBlocks.insertBlock(
          block: block,
          ownerId: ownerId,
        );
        final blockDuplicate = await repositories.lockedBlocks.insertBlock(
          block: block,
          ownerId: ownerId,
        );
        final blockUpdate = await repositories.lockedBlocks.saveBlockIfRevision(
          block: block.copyWith(name: 'Work block renamed'),
          ownerId: ownerId,
          expectedRevision: blockInsert.revision!,
        );
        final blockArchive = await repositories.lockedBlocks.archiveBlock(
          blockId: block.id,
          ownerId: ownerId,
          expectedRevision: blockUpdate.revision!,
          archivedAt: _instant(11, 10),
        );

        final overrideInsert = await repositories.lockedBlocks.insertOverride(
          override: override,
          ownerId: ownerId,
        );
        final overrideDuplicate =
            await repositories.lockedBlocks.insertOverride(
          override: override,
          ownerId: ownerId,
        );
        final overrideUpdate =
            await repositories.lockedBlocks.saveOverrideIfRevision(
          override: LockedBlockOverride.add(
            id: override.id,
            date: _date,
            name: 'Errand moved',
            startTime: WallTime(hour: 14, minute: 0),
            endTime: WallTime(hour: 14, minute: 30),
            projectId: project.id,
            createdAt: now,
            updatedAt: _instant(11, 15),
          ),
          ownerId: ownerId,
          expectedRevision: overrideInsert.revision!,
        );

        await repositories.schedulingSnapshots.save(snapshot);
        await repositories.schedulingSnapshots.save(
          SchedulingStateSnapshot(
            id: snapshot.id,
            capturedAt: _instant(11, 20),
            window: snapshot.window,
            tasks: [scheduledTask, backlogTask],
            operationName: 'coverage-snapshot-updated',
          ),
        );

        final activityAppend = await repositories.taskActivities.append(
          activity: activity,
          ownerId: ownerId,
        );
        final activityDuplicate = await repositories.taskActivities.append(
          activity: activity,
          ownerId: ownerId,
        );
        await repositories.taskActivities.save(
          TaskActivity(
            id: activity.id,
            operationId: activity.operationId,
            code: activity.code,
            taskId: activity.taskId,
            projectId: activity.projectId,
            occurredAt: _instant(11, 30),
            metadata: {'updated': true},
          ),
        );

        await repositories.projectStatistics.save(statistics);
        final statisticsUpdate =
            await repositories.projectStatistics.saveIfRevision(
          statistics: statistics.copyWith(completedTaskCount: 3),
          ownerId: ownerId,
          expectedRevision: 1,
        );
        final statisticsOwnerMismatch =
            await repositories.projectStatistics.saveIfRevision(
          statistics: statistics,
          ownerId: 'other-owner',
          expectedRevision: statisticsUpdate.revision!,
        );

        final noticeInsert =
            await repositories.noticeAcknowledgements.insert(notice);
        final noticeDuplicate =
            await repositories.noticeAcknowledgements.insert(notice);
        await repositories.noticeAcknowledgements.save(
          NoticeAcknowledgementRecord(
            ownerId: ownerId,
            noticeId: notice.noticeId,
            acknowledgedAt: _instant(11, 40),
          ),
        );

        return ApplicationResult.success({
          'settingsRevision': settingsUpdate.revision,
          'settingsStale': settingsStale.failure?.code,
          'projectRevision': projectArchive.revision,
          'projectDuplicate': projectDuplicate.failure?.code,
          'projectMissingArchive': projectMissingArchive.failure?.code,
          'taskRevision': taskUpdate.revision,
          'taskDuplicate': taskDuplicate.failure?.code,
          'blockRevision': blockArchive.revision,
          'blockDuplicate': blockDuplicate.failure?.code,
          'overrideRevision': overrideUpdate.revision,
          'overrideDuplicate': overrideDuplicate.failure?.code,
          'activityRevision': activityAppend.revision,
          'activityDuplicate': activityDuplicate.failure?.code,
          'statisticsRevision': statisticsUpdate.revision,
          'statisticsOwnerMismatch': statisticsOwnerMismatch.failure?.code,
          'noticeRevision': noticeInsert.revision,
          'noticeDuplicate': noticeDuplicate.failure?.code,
        });
      },
    );

    final writeSummary = write.requireValue;
    expect(writeSummary['settingsRevision'], 2);
    expect(writeSummary['settingsStale'], RepositoryFailureCode.staleRevision);
    expect(writeSummary['projectRevision'], 3);
    expect(
      writeSummary['projectDuplicate'],
      RepositoryFailureCode.duplicateId,
    );
    expect(
      writeSummary['projectMissingArchive'],
      RepositoryFailureCode.notFound,
    );
    expect(writeSummary['taskRevision'], 2);
    expect(writeSummary['taskDuplicate'], RepositoryFailureCode.duplicateId);
    expect(writeSummary['blockRevision'], 3);
    expect(writeSummary['blockDuplicate'], RepositoryFailureCode.duplicateId);
    expect(writeSummary['overrideRevision'], 2);
    expect(
      writeSummary['overrideDuplicate'],
      RepositoryFailureCode.duplicateId,
    );
    expect(writeSummary['activityRevision'], 1);
    expect(
      writeSummary['activityDuplicate'],
      RepositoryFailureCode.duplicateId,
    );
    expect(writeSummary['statisticsRevision'], 2);
    expect(
      writeSummary['statisticsOwnerMismatch'],
      RepositoryFailureCode.ownerMismatch,
    );
    expect(writeSummary['noticeRevision'], 1);
    expect(writeSummary['noticeDuplicate'], RepositoryFailureCode.duplicateId);

    final read = await runtime.applicationStore.read<Map<String, Object?>>(
      action: (repositories) async {
        final taskPage = await repositories.tasks.findByOwner(
          ownerId: ownerId,
          page: const RepositoryPageRequest(limit: 2),
        );
        final taskPage2 = await repositories.tasks.findByOwner(
          ownerId: ownerId,
          page: RepositoryPageRequest(cursor: taskPage.nextCursor, limit: 2),
        );
        final backlogCandidates =
            await repositories.tasks.findBacklogCandidates(
          const BacklogCandidateQuery(
            ownerId: ownerId,
            projectId: 'project-alpha',
            tags: {BacklogTag.wishlist},
          ),
        );
        final projectActive = await repositories.projects.findByOwner(
          ownerId: ownerId,
        );
        final projectArchived = await repositories.projects.findByOwner(
          ownerId: ownerId,
          includeArchived: true,
        );
        final blocksActive = await repositories.lockedBlocks.findBlocksByOwner(
          ownerId: ownerId,
        );
        final blocksArchived =
            await repositories.lockedBlocks.findBlocksByOwner(
          ownerId: ownerId,
          includeArchived: true,
        );
        final overridesForDate =
            await repositories.lockedBlocks.findOverridesForDate(
          ownerId: ownerId,
          date: _date,
        );
        final scheduled = await repositories.tasks.findScheduledInWindow(
          _window(7, 9),
        );
        final scheduledForOwner =
            await repositories.tasks.findScheduledInWindowForOwner(
          ownerId: ownerId,
          window: _window(7, 9),
          page: const RepositoryPageRequest(cursor: 'bad-cursor', limit: 1),
        );
        final snapshotsInWindow =
            await repositories.schedulingSnapshots.findInWindow(_window(7, 9));
        final snapshotsOutside = await repositories.schedulingSnapshots
            .findInWindow(_window(16, 17));
        final activitiesByOwner = await repositories.taskActivities.findByOwner(
          ownerId: ownerId,
        );
        final noticePage =
            await repositories.noticeAcknowledgements.findByOwner(
          ownerId: ownerId,
          page: const RepositoryPageRequest(limit: 1),
        );
        final operation = await repositories.operations.findById(
          'repository-surface',
        );
        final idempotency = await repositories.operations.findByIdempotencyKey(
          ownerId: ownerId,
          operationId: 'repository-surface',
        );

        return ApplicationResult.success({
          'taskPageCount': taskPage.items.length,
          'taskPage2Count': taskPage2.items.length,
          'taskNextCursor': taskPage.nextCursor,
          'taskByIdTitle':
              (await repositories.tasks.findById(scheduledTask.id))?.title,
          'taskRecordRevision': (await repositories.tasks.findRecordById(
            scheduledTask.id,
          ))
              ?.revision,
          'plannedCount':
              (await repositories.tasks.findByStatus(TaskStatus.planned))
                  .length,
          'projectTaskCount': (await repositories.tasks.findByProjectId(
            ownerId: ownerId,
            projectId: project.id,
          ))
              .items
              .length,
          'childTaskCount': (await repositories.tasks.findByParentTaskId(
            ownerId: ownerId,
            parentTaskId: backlogTask.id,
          ))
              .items
              .length,
          'backlogCandidateTitle': backlogCandidates.items.single.title,
          'scheduledCount': scheduled.length,
          'scheduledForOwnerCount': scheduledForOwner.items.length,
          'activeProjectCount': projectActive.items.length,
          'archivedProjectCount': projectArchived.items.length,
          'projectRecordRevision': (await repositories.projects.findRecordById(
            project.id,
          ))
              ?.revision,
          'allProjectCount': (await repositories.projects.findAll()).length,
          'activeBlockCount': blocksActive.items.length,
          'archivedBlockCount': blocksArchived.items.length,
          'blockRecordRevision':
              (await repositories.lockedBlocks.findBlockRecordById(block.id))
                  ?.revision,
          'allBlockCount':
              (await repositories.lockedBlocks.findAllBlocks()).length,
          'overrideRecordRevision': (await repositories.lockedBlocks
                  .findOverrideRecordById(override.id))
              ?.revision,
          'overrideCount': overridesForDate.items.length,
          'allOverrideCount':
              (await repositories.lockedBlocks.findAllOverrides()).length,
          'snapshotTaskCount':
              (await repositories.schedulingSnapshots.findById(snapshot.id))
                  ?.tasks
                  .length,
          'snapshotInWindowCount': snapshotsInWindow.length,
          'snapshotOutsideCount': snapshotsOutside.length,
          'activityMetadataUpdated':
              (await repositories.taskActivities.findById(activity.id))
                  ?.metadata['updated'],
          'activityByOperationCount': (await repositories.taskActivities
                  .findByOperationId(activity.operationId))
              .length,
          'activityByTaskCount':
              (await repositories.taskActivities.findByTaskId(activity.taskId))
                  .length,
          'activityByProjectCount': (await repositories.taskActivities
                  .findByProjectId(activity.projectId))
              .length,
          'activityByOwnerCount': activitiesByOwner.items.length,
          'activityAllCount':
              (await repositories.taskActivities.findAll()).length,
          'statisticsCompleted':
              (await repositories.projectStatistics.findByProjectId(project.id))
                  ?.completedTaskCount,
          'statisticsRecordRevision': (await repositories.projectStatistics
                  .findRecordByProjectId(project.id))
              ?.revision,
          'statisticsAllCount':
              (await repositories.projectStatistics.findAll()).length,
          'settingsCompact':
              (await repositories.ownerSettings.findByOwnerId(ownerId))
                  ?.compactModeEnabled,
          'settingsRevision':
              (await repositories.ownerSettings.findRecordByOwnerId(ownerId))
                  ?.revision,
          'noticeAcknowledgedAt':
              (await repositories.noticeAcknowledgements.findById(
            ownerId: ownerId,
            noticeId: notice.noticeId,
          ))
                  ?.acknowledgedAt,
          'noticeOwnerCount':
              (await repositories.noticeAcknowledgements.findByOwnerId(ownerId))
                  .length,
          'noticePageCount': noticePage.items.length,
          'operationName': operation?.operationName,
          'idempotencyName': idempotency?.operationName,
        });
      },
    );

    final readSummary = read.requireValue;
    expect(readSummary['taskPageCount'], 2);
    expect(readSummary['taskPage2Count'], 1);
    expect(readSummary['taskNextCursor'], '2');
    expect(readSummary['taskByIdTitle'], 'Scheduled task updated');
    expect(readSummary['taskRecordRevision'], 2);
    expect(readSummary['plannedCount'], 1);
    expect(readSummary['projectTaskCount'], 3);
    expect(readSummary['childTaskCount'], 1);
    expect(readSummary['backlogCandidateTitle'], 'Backlog task updated');
    expect(readSummary['scheduledCount'], 1);
    expect(readSummary['scheduledForOwnerCount'], 1);
    expect(readSummary['activeProjectCount'], 0);
    expect(readSummary['archivedProjectCount'], 1);
    expect(readSummary['projectRecordRevision'], 3);
    expect(readSummary['allProjectCount'], 1);
    expect(readSummary['activeBlockCount'], 0);
    expect(readSummary['archivedBlockCount'], 1);
    expect(readSummary['blockRecordRevision'], 3);
    expect(readSummary['allBlockCount'], 1);
    expect(readSummary['overrideRecordRevision'], 2);
    expect(readSummary['overrideCount'], 1);
    expect(readSummary['allOverrideCount'], 1);
    expect(readSummary['snapshotTaskCount'], 2);
    expect(readSummary['snapshotInWindowCount'], 1);
    expect(readSummary['snapshotOutsideCount'], 0);
    expect(readSummary['activityMetadataUpdated'], isTrue);
    expect(readSummary['activityByOperationCount'], 1);
    expect(readSummary['activityByTaskCount'], 1);
    expect(readSummary['activityByProjectCount'], 1);
    expect(readSummary['activityByOwnerCount'], 1);
    expect(readSummary['activityAllCount'], 1);
    expect(readSummary['statisticsCompleted'], 3);
    expect(readSummary['statisticsRecordRevision'], 2);
    expect(readSummary['statisticsAllCount'], 1);
    expect(readSummary['settingsCompact'], isFalse);
    expect(readSummary['settingsRevision'], 2);
    expect(readSummary['noticeAcknowledgedAt'], _instant(11, 40));
    expect(readSummary['noticeOwnerCount'], 1);
    expect(readSummary['noticePageCount'], 1);
    expect(readSummary['operationName'], 'repository-surface');
    expect(readSummary['idempotencyName'], 'repository-surface');
  });
}

/// Fixed date used by lifecycle tests.
final _date = CivilDate(2026, 1, 2);

/// Returns an instant on the fixed lifecycle date.
DateTime _instant(int hour, [int minute = 0]) {
  return DateTime.utc(_date.year, _date.month, _date.day, hour, minute);
}

/// Opens and bootstraps a runtime at [path].
Future<SqliteApplicationRuntime> _openBootstrappedRuntime(String path) async {
  final runtime = await SqliteApplicationRuntime.open(path: path);
  final bootstrap = await runtime.bootstrap();
  expect(bootstrap.isSuccess, isTrue);
  return runtime;
}

/// Creates command use cases for [runtime].
V1ApplicationCommandUseCases _commands(SqliteApplicationRuntime runtime) {
  return V1ApplicationCommandUseCases(
    applicationStore: runtime.applicationStore,
    timeZoneResolver: const FixedOffsetTimeZoneResolver.utc(),
  );
}

/// Creates management use cases for [runtime].
V1ApplicationManagementUseCases _management(SqliteApplicationRuntime runtime) {
  return V1ApplicationManagementUseCases(
    applicationStore: runtime.applicationStore,
  );
}

/// Creates the Today read query for [runtime].
GetTodayStateQuery _todayQuery(SqliteApplicationRuntime runtime) {
  return GetTodayStateQuery(
    applicationStore: runtime.applicationStore,
    timeZoneResolver: const FixedOffsetTimeZoneResolver.utc(),
  );
}

/// Creates an operation context for [operationId].
ApplicationOperationContext _context(String operationId, DateTime now) {
  return ApplicationOperationContext.start(
    operationId: operationId,
    ownerTimeZone: OwnerTimeZoneContext(
      ownerId: SqliteApplicationRuntime.defaultV1OwnerId,
      timeZoneId: SqliteApplicationRuntime.defaultV1TimeZoneId,
    ),
    clock: FixedClock(now),
    idGenerator: SequentialIdGenerator(prefix: operationId),
  );
}

/// Returns a fresh temporary SQLite path.
Future<String> _tempDatabasePath() async {
  final directory = await Directory.systemTemp.createTemp(
    'focus-flow-sqlite-app-',
  );
  addTearDown(() => directory.delete(recursive: true));
  return '${directory.path}/scheduler.sqlite';
}

/// Creates a project profile fixture.
ProjectProfile _project(String id, String name) {
  return ProjectProfile(
    id: id,
    name: name,
    colorKey: 'project-$id',
    defaultPriority: PriorityLevel.high,
    defaultReward: RewardLevel.high,
    defaultDifficulty: DifficultyLevel.easy,
    defaultReminderProfile: ReminderProfile.gentle,
    defaultDurationMinutes: 30,
  );
}

/// Creates a task fixture.
Task _task({
  required String id,
  required String title,
  required TaskStatus status,
  DateTime? scheduledStart,
  DateTime? scheduledEnd,
  String? parentTaskId,
  Set<BacklogTag> backlogTags = const <BacklogTag>{},
}) {
  return Task(
    id: id,
    title: title,
    projectId: 'project-alpha',
    type: TaskType.flexible,
    status: status,
    priority: PriorityLevel.medium,
    reward: RewardLevel.high,
    difficulty: DifficultyLevel.easy,
    durationMinutes: 30,
    scheduledStart: scheduledStart,
    scheduledEnd: scheduledEnd,
    parentTaskId: parentTaskId,
    backlogTags: backlogTags,
    reminderOverride: ReminderProfile.gentle,
    createdAt: _instant(7),
    updatedAt: _instant(7),
  );
}

/// Creates a scheduling window on the fixed test date.
SchedulingWindow _window(int startHour, int endHour) {
  return SchedulingWindow(
    start: _instant(startHour),
    end: _instant(endHour),
  );
}
