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

part of '../../../application_layer.dart';

/// In-memory unit-of-work implementation for deterministic application tests.
class InMemoryApplicationUnitOfWork implements ApplicationUnitOfWork {
  /// Creates a `InMemoryApplicationUnitOfWork` 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.
  InMemoryApplicationUnitOfWork({
    Iterable<Task> initialTasks = const <Task>[],
    Iterable<ProjectProfile> initialProjects = const <ProjectProfile>[],
    Iterable<LockedBlock> initialLockedBlocks = const <LockedBlock>[],
    Iterable<LockedBlockOverride> initialLockedOverrides =
        const <LockedBlockOverride>[],
    Iterable<SchedulingStateSnapshot> initialSchedulingSnapshots =
        const <SchedulingStateSnapshot>[],
    Iterable<TaskActivity> initialTaskActivities = const <TaskActivity>[],
    Iterable<ProjectStatistics> initialProjectStatistics =
        const <ProjectStatistics>[],
    Iterable<OwnerSettings> initialOwnerSettings = const <OwnerSettings>[],
    Iterable<NoticeAcknowledgementRecord> initialNoticeAcknowledgements =
        const <NoticeAcknowledgementRecord>[],
    Iterable<ApplicationOperationRecord> initialOperations =
        const <ApplicationOperationRecord>[],
  }) : _state = _InMemoryApplicationState(
          tasksById: {
            for (final task in initialTasks) task.id: task,
          },
          taskOwnersById: {
            for (final task in initialTasks) task.id: 'owner-1',
          },
          taskRevisionsById: {
            for (final task in initialTasks) task.id: 1,
          },
          projectsById: {
            for (final project in initialProjects) project.id: project,
          },
          projectOwnersById: {
            for (final project in initialProjects) project.id: 'owner-1',
          },
          projectRevisionsById: {
            for (final project in initialProjects) project.id: 1,
          },
          lockedBlocksById: {
            for (final block in initialLockedBlocks) block.id: block,
          },
          lockedBlockOwnersById: {
            for (final block in initialLockedBlocks) block.id: 'owner-1',
          },
          lockedBlockRevisionsById: {
            for (final block in initialLockedBlocks) block.id: 1,
          },
          lockedOverridesById: {
            for (final override in initialLockedOverrides)
              override.id: override,
          },
          lockedOverrideOwnersById: {
            for (final override in initialLockedOverrides)
              override.id: 'owner-1',
          },
          lockedOverrideRevisionsById: {
            for (final override in initialLockedOverrides) override.id: 1,
          },
          schedulingSnapshotsById: {
            for (final snapshot in initialSchedulingSnapshots)
              snapshot.id: snapshot,
          },
          taskActivitiesById: {
            for (final activity in initialTaskActivities) activity.id: activity,
          },
          taskActivityOwnersById: {
            for (final activity in initialTaskActivities)
              activity.id: 'owner-1',
          },
          projectStatisticsByProjectId: {
            for (final statistics in initialProjectStatistics)
              statistics.projectId: statistics,
          },
          projectStatisticsOwnersByProjectId: {
            for (final statistics in initialProjectStatistics)
              statistics.projectId: 'owner-1',
          },
          projectStatisticsRevisionsByProjectId: {
            for (final statistics in initialProjectStatistics)
              statistics.projectId: 1,
          },
          ownerSettingsByOwnerId: {
            for (final settings in initialOwnerSettings)
              settings.ownerId: settings,
          },
          ownerSettingsRevisionsByOwnerId: {
            for (final settings in initialOwnerSettings) settings.ownerId: 1,
          },
          noticeAcknowledgementsByScopedId: {
            for (final acknowledgement in initialNoticeAcknowledgements)
              _noticeAcknowledgementKey(
                ownerId: acknowledgement.ownerId,
                noticeId: acknowledgement.noticeId,
              ): acknowledgement,
          },
          noticeAcknowledgementRevisionsByScopedId: {
            for (final acknowledgement in initialNoticeAcknowledgements)
              _noticeAcknowledgementKey(
                ownerId: acknowledgement.ownerId,
                noticeId: acknowledgement.noticeId,
              ): 1,
          },
          operationsById: {
            for (final operation in initialOperations)
              operation.operationId: operation,
          },
        );

  /// Private state stored as `_state` for this implementation.
  /// The field is intentionally scoped to this library so only the owning type can change the related scheduler or UI behavior.
  _InMemoryApplicationState _state;

  /// Private state stored as `_failNextCommit` for this implementation.
  /// The field is intentionally scoped to this library so only the owning type can change the related scheduler or UI behavior.
  bool _failNextCommit = false;

  /// Current committed tasks, useful for contract tests.
  List<Task> get currentTasks =>
      List<Task>.unmodifiable(_state.tasksById.values);

  /// Current committed projects, useful for contract tests.
  List<ProjectProfile> get currentProjects {
    return List<ProjectProfile>.unmodifiable(_state.projectsById.values);
  }

  /// Current committed locked blocks, useful for contract tests.
  List<LockedBlock> get currentLockedBlocks {
    return List<LockedBlock>.unmodifiable(_state.lockedBlocksById.values);
  }

  /// Current committed locked overrides, useful for contract tests.
  List<LockedBlockOverride> get currentLockedOverrides {
    return List<LockedBlockOverride>.unmodifiable(
      _state.lockedOverridesById.values,
    );
  }

  /// Current committed scheduling snapshots, useful for contract tests.
  List<SchedulingStateSnapshot> get currentSchedulingSnapshots {
    return List<SchedulingStateSnapshot>.unmodifiable(
      _state.schedulingSnapshotsById.values,
    );
  }

  /// Current committed activities, useful for contract tests.
  List<TaskActivity> get currentTaskActivities {
    return List<TaskActivity>.unmodifiable(_state.taskActivitiesById.values);
  }

  /// Current committed project statistics, useful for contract tests.
  List<ProjectStatistics> get currentProjectStatistics {
    return List<ProjectStatistics>.unmodifiable(
      _state.projectStatisticsByProjectId.values,
    );
  }

  /// Current committed owner settings, useful for contract tests.
  List<OwnerSettings> get currentOwnerSettings {
    return List<OwnerSettings>.unmodifiable(
        _state.ownerSettingsByOwnerId.values);
  }

  /// Current committed notice acknowledgements, useful for contract tests.
  List<NoticeAcknowledgementRecord> get currentNoticeAcknowledgements {
    return List<NoticeAcknowledgementRecord>.unmodifiable(
      _state.noticeAcknowledgementsByScopedId.values,
    );
  }

  /// Current committed operation records, useful for contract tests.
  List<ApplicationOperationRecord> get currentOperations {
    return List<ApplicationOperationRecord>.unmodifiable(
      _state.operationsById.values,
    );
  }

  /// Cause the next successful transaction to fail at commit time.
  void failNextCommitForTesting() {
    _failNextCommit = true;
  }

  /// Runs the `run<T>` operation and completes after its asynchronous work finishes.
  /// Callers should await the returned future so repository writes, UI refreshes, and failure handling happen in a predictable order.
  @override
  Future<ApplicationResult<T>> run<T>({
    required ApplicationOperationContext context,
    required String operationName,
    required Future<ApplicationResult<T>> Function(
      ApplicationUnitOfWorkRepositories repositories,
    ) action,
  }) async {
    logger.debug(() => 'In-memory unit of work run started. '
        'operationId=${context.operationId} operationName=$operationName '
        'ownerId=${context.ownerTimeZone.ownerId}');
    if (_state.operationsById.containsKey(context.operationId)) {
      logger.warn(() =>
          'Duplicate operation ignored by in-memory unit of work. '
          'operationId=${context.operationId} operationName=$operationName');
      return ApplicationResult.failure(
        ApplicationFailure(
          code: ApplicationFailureCode.duplicateOperation,
          entityId: context.operationId,
        ),
      );
    }

    final staged = _state.copy();
    final repositories = _InMemoryApplicationRepositories(staged);

    try {
      final result = await action(repositories);
      if (result.isFailure) {
        logger.warn(() => 'In-memory unit of work action returned failure. '
            'operationId=${context.operationId} operationName=$operationName '
            'code=${result.failure!.code.name} detailCode=${result.failure!.detailCode}');
        return result;
      }

      staged.operationsById[context.operationId] = ApplicationOperationRecord(
        operationId: context.operationId,
        ownerId: context.ownerTimeZone.ownerId,
        operationName: operationName,
        committedAt: context.now,
      );

      if (_failNextCommit) {
        _failNextCommit = false;
        logger.error(() => 'In-memory unit of work commit failed by test hook. '
            'operationId=${context.operationId} operationName=$operationName');
        throw const ApplicationPersistenceException();
      }

      _state = staged;
      logger.debug(() => 'In-memory unit of work run committed. '
          'operationId=${context.operationId} operationName=$operationName '
          'taskCount=${_state.tasksById.length} projectCount=${_state.projectsById.length} '
          'lockedBlockCount=${_state.lockedBlocksById.length} '
          'snapshotCount=${_state.schedulingSnapshotsById.length} '
          'operationCount=${_state.operationsById.length}');
      return result;
    } on ApplicationFailureException catch (error) {
      logger.warn(() => 'In-memory unit of work caught application failure. '
          'operationId=${context.operationId} operationName=$operationName '
          'code=${error.failure.code.name} detailCode=${error.failure.detailCode}');
      return ApplicationResult.failure(error.failure);
    } on DomainValidationException catch (error) {
      logger.warn(
          () => 'In-memory unit of work caught domain validation failure. '
              'operationId=${context.operationId} operationName=$operationName '
              'code=${error.code.name}');
      return ApplicationResult.failure(
        ApplicationFailure(
          code: ApplicationFailureCode.validation,
          detailCode: error.code.name,
        ),
      );
    } on ArgumentError catch (error) {
      logger.warn(
          () => 'In-memory unit of work caught argument validation failure. '
              'operationId=${context.operationId} operationName=$operationName '
              'argumentName=${error.name}');
      return ApplicationResult.failure(
        ApplicationFailure(
          code: ApplicationFailureCode.validation,
          detailCode: error.name,
        ),
      );
    } on ApplicationPersistenceException catch (error, stackTrace) {
      logger.error(
        () => 'In-memory unit of work persistence failure. '
            'operationId=${context.operationId} operationName=$operationName',
        error: error,
        stackTrace: stackTrace,
      );
      return ApplicationResult.failure(
        const ApplicationFailure(
          code: ApplicationFailureCode.persistenceFailure,
        ),
      );
    } catch (error, stackTrace) {
      logger.error(
        () => 'In-memory unit of work unexpected failure. '
            'operationId=${context.operationId} operationName=$operationName',
        error: error,
        stackTrace: stackTrace,
      );
      return ApplicationResult.failure(
        const ApplicationFailure(code: ApplicationFailureCode.unexpected),
      );
    }
  }

  /// Runs the `read<T>` operation and completes after its asynchronous work finishes.
  /// Callers should await the returned future so repository writes, UI refreshes, and failure handling happen in a predictable order.
  @override
  Future<ApplicationResult<T>> read<T>({
    required Future<ApplicationResult<T>> Function(
      ApplicationUnitOfWorkRepositories repositories,
    ) action,
  }) async {
    logger.debug(() => 'In-memory unit of work read started. '
        'taskCount=${_state.tasksById.length} projectCount=${_state.projectsById.length} '
        'lockedBlockCount=${_state.lockedBlocksById.length} '
        'snapshotCount=${_state.schedulingSnapshotsById.length}');
    final staged = _state.copy();
    final repositories = _InMemoryApplicationRepositories(staged);

    try {
      final result = await action(repositories);
      if (result.isFailure) {
        logger.warn(() => 'In-memory unit of work read returned failure. '
            'code=${result.failure!.code.name} detailCode=${result.failure!.detailCode}');
      } else {
        logger.debug(() => 'In-memory unit of work read finished. '
            'taskCount=${staged.tasksById.length} projectCount=${staged.projectsById.length} '
            'lockedBlockCount=${staged.lockedBlocksById.length} '
            'snapshotCount=${staged.schedulingSnapshotsById.length}');
      }
      return result;
    } on ApplicationFailureException catch (error) {
      logger.warn(() =>
          'In-memory unit of work read caught application failure. '
          'code=${error.failure.code.name} detailCode=${error.failure.detailCode}');
      return ApplicationResult.failure(error.failure);
    } on DomainValidationException catch (error) {
      logger.warn(
          () => 'In-memory unit of work read caught domain validation failure. '
              'code=${error.code.name}');
      return ApplicationResult.failure(
        ApplicationFailure(
          code: ApplicationFailureCode.validation,
          detailCode: error.code.name,
        ),
      );
    } on ArgumentError catch (error) {
      logger.warn(() =>
          'In-memory unit of work read caught argument validation failure. '
          'argumentName=${error.name}');
      return ApplicationResult.failure(
        ApplicationFailure(
          code: ApplicationFailureCode.validation,
          detailCode: error.name,
        ),
      );
    } on ApplicationPersistenceException catch (error, stackTrace) {
      logger.error(
        () => 'In-memory unit of work read persistence failure.',
        error: error,
        stackTrace: stackTrace,
      );
      return ApplicationResult.failure(
        const ApplicationFailure(
          code: ApplicationFailureCode.persistenceFailure,
        ),
      );
    } catch (error, stackTrace) {
      logger.error(
        () => 'In-memory unit of work read unexpected failure.',
        error: error,
        stackTrace: stackTrace,
      );
      return ApplicationResult.failure(
        const ApplicationFailure(code: ApplicationFailureCode.unexpected),
      );
    }
  }
}
