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

/// Tests Repo Conformance behavior for the Scheduler Persistence package.
library;

import 'package:scheduler_core/scheduler_core.dart' as core;
import 'package:scheduler_persistence/persistence.dart';
import 'package:test/test.dart';

/// Factory set used by adapter packages to run the shared repository contract.
final class RepositoryComplianceFactories {
  /// Creates a `RepositoryComplianceFactories` 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 RepositoryComplianceFactories({
    required this.taskRepository,
    required this.projectRepository,
    required this.lockedBlockRepository,
    required this.settingsRepository,
    required this.scheduleSnapshotRepository,
  });

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

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

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

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

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

/// Runs the cross-adapter behavioral checks expected of repository packages.
void runRepositoryComplianceTests(RepositoryComplianceFactories factories) {
  group('task repository compliance', () {
    test('supports CRUD, owner isolation, and JSON-cloned returns', () async {
      final repo = factories.taskRepository();
      final owner = core.OwnerId('owner-a');
      final otherOwner = core.OwnerId('owner-b');
      final task = _task('task-a');

      final inserted = await repo.insert(ownerId: owner, task: task);
      expect(inserted.right, core.Revision.initial);

      final found =
          (await repo.findById(ownerId: owner, taskId: task.id)).right;
      expect(found, isNotNull);
      expect(found, isNot(same(task)));
      expect(found!.id, task.id);

      final hiddenFromOtherOwner =
          (await repo.findById(ownerId: otherOwner, taskId: task.id)).right;
      expect(hiddenFromOtherOwner, isNull);

      final updated = task.copyWith(
        title: 'Updated task',
        updatedAt: _instant(2),
      );
      final saved = await repo.save(
        ownerId: owner,
        task: updated,
        expectedRevision: inserted.right,
      );
      expect(saved.right, const core.Revision(2));

      final afterSave =
          (await repo.findById(ownerId: owner, taskId: task.id)).right;
      expect(afterSave!.title, 'Updated task');

      final deleted = await repo.delete(
        ownerId: owner,
        taskId: task.id,
        expectedRevision: saved.right,
      );
      expect(deleted.right, const core.Revision(3));
      expect(
          (await repo.findById(ownerId: owner, taskId: task.id)).right, isNull);
    });

    test('enforces duplicate ids and optimistic revision conflicts', () async {
      final repo = factories.taskRepository();
      final owner = core.OwnerId('owner-a');
      final otherOwner = core.OwnerId('owner-b');
      final task = _task('task-a');

      final inserted = await repo.insert(ownerId: owner, task: task);
      expect(
        (await repo.insert(ownerId: owner, task: task)).left,
        isA<RepositoryDuplicateId>(),
      );
      expect(
        (await repo.save(
          ownerId: owner,
          task: task.copyWith(title: 'Invalid'),
          expectedRevision: const core.Revision(0),
        ))
            .left,
        isA<RepositoryInvalidRevision>(),
      );
      expect(
        (await repo.save(
          ownerId: otherOwner,
          task: task.copyWith(title: 'Other owner'),
          expectedRevision: inserted.right,
        ))
            .left,
        isA<RepositoryOwnerMismatch>(),
      );

      final saved = await repo.save(
        ownerId: owner,
        task: task.copyWith(title: 'Fresh'),
        expectedRevision: inserted.right,
      );
      final stale = (await repo.save(
        ownerId: owner,
        task: task.copyWith(title: 'Stale'),
        expectedRevision: inserted.right,
      ))
          .left;
      expect(stale, isA<RepositoryStaleRevision>());
      expect(stale.actualRevision, saved.right);
    });

    test('returns deterministic pages and filtered task queries', () async {
      final repo = factories.taskRepository();
      final owner = core.OwnerId('owner-a');
      await repo.insert(ownerId: owner, task: _task('task-c'));
      await repo.insert(
        ownerId: owner,
        task: _task(
          'task-a',
          projectId: 'project-filter',
          status: core.TaskStatus.completed,
        ),
      );
      await repo.insert(
        ownerId: owner,
        task: _task('task-b', parentTaskId: 'parent-1'),
      );

      final firstPage = (await repo.findByOwner(
        ownerId: owner,
        page: const core.PageRequest(limit: 2),
      ))
          .right;
      expect(firstPage.items.map((task) => task.id), ['task-a', 'task-b']);
      expect(firstPage.nextCursor, isNotNull);

      final secondPage = (await repo.findByOwner(
        ownerId: owner,
        page: core.PageRequest(cursor: firstPage.nextCursor, limit: 2),
      ))
          .right;
      expect(secondPage.items.map((task) => task.id), ['task-c']);
      expect(secondPage.nextCursor, isNull);

      expect(
        (await repo.findByProjectId(
          ownerId: owner,
          projectId: 'project-filter',
        ))
            .right
            .items
            .map((task) => task.id),
        ['task-a'],
      );
      expect(
        (await repo.findByParentTaskId(
          ownerId: owner,
          parentTaskId: 'parent-1',
        ))
            .right
            .items
            .map((task) => task.id),
        ['task-b'],
      );
      expect(
        (await repo.findByStatus(
          ownerId: owner,
          status: core.TaskStatus.completed,
        ))
            .right
            .items
            .map((task) => task.id),
        ['task-a'],
      );
    });
  });

  group('project repository compliance', () {
    test('supports save, archive filters, and stale conflicts', () async {
      final repo = factories.projectRepository();
      final owner = core.OwnerId('owner-a');
      final project = _project('project-a');

      final inserted = await repo.insert(ownerId: owner, project: project);
      expect(inserted.right, core.Revision.initial);
      expect(
        (await repo.insert(ownerId: owner, project: project)).left,
        isA<RepositoryDuplicateId>(),
      );

      final found =
          (await repo.findById(ownerId: owner, projectId: project.id)).right;
      expect(found, isNot(same(project)));
      expect(found!.name, project.name);

      final saved = await repo.save(
        ownerId: owner,
        project: project.copyWith(name: 'Updated project'),
        expectedRevision: inserted.right,
      );
      expect(saved.right, const core.Revision(2));
      expect(
        (await repo.save(
          ownerId: owner,
          project: project.copyWith(name: 'Stale project'),
          expectedRevision: inserted.right,
        ))
            .left,
        isA<RepositoryStaleRevision>(),
      );

      final archived = await repo.archive(
        ownerId: owner,
        projectId: project.id,
        expectedRevision: saved.right,
        archivedAtUtc: _instant(3),
      );
      expect(archived.right, const core.Revision(3));
      expect((await repo.findByOwner(ownerId: owner)).right.items, isEmpty);
      expect(
        (await repo.findByOwner(ownerId: owner, includeArchived: true))
            .right
            .items
            .single
            .isArchived,
        isTrue,
      );
    });
  });

  group('locked block repository compliance', () {
    test('supports block archive and override date lookup', () async {
      final repo = factories.lockedBlockRepository();
      final owner = core.OwnerId('owner-a');
      final block = _lockedBlock('block-a');
      final override = _override('override-a');

      final insertedBlock =
          await repo.insertBlock(ownerId: owner, block: block);
      expect(insertedBlock.right, core.Revision.initial);
      expect(
        (await repo.insertBlock(ownerId: owner, block: block)).left,
        isA<RepositoryDuplicateId>(),
      );

      final foundBlock =
          (await repo.findBlockById(ownerId: owner, blockId: block.id)).right;
      expect(foundBlock, isNot(same(block)));
      expect(foundBlock!.name, block.name);

      final savedBlock = await repo.saveBlock(
        ownerId: owner,
        block: block.copyWith(name: 'Updated block', updatedAt: _instant(2)),
        expectedRevision: insertedBlock.right,
      );
      expect(savedBlock.right, const core.Revision(2));
      expect(
        (await repo.saveBlock(
          ownerId: owner,
          block: block,
          expectedRevision: insertedBlock.right,
        ))
            .left,
        isA<RepositoryStaleRevision>(),
      );

      final archived = await repo.archiveBlock(
        ownerId: owner,
        blockId: block.id,
        expectedRevision: savedBlock.right,
        archivedAtUtc: _instant(3),
      );
      expect(archived.right, const core.Revision(3));
      expect(
          (await repo.findBlocksByOwner(ownerId: owner)).right.items, isEmpty);

      final insertedOverride =
          await repo.insertOverride(ownerId: owner, override: override);
      expect(insertedOverride.right, core.Revision.initial);
      expect(
        (await repo.findOverridesForDate(ownerId: owner, date: override.date))
            .right
            .items
            .map((item) => item.id),
        [override.id],
      );

      final savedOverride = await repo.saveOverride(
        ownerId: owner,
        override: _override('override-a', name: 'Replacement block'),
        expectedRevision: insertedOverride.right,
      );
      expect(savedOverride.right, const core.Revision(2));
    });
  });

  group('settings repository compliance', () {
    test('supports owner singleton save and stale conflict', () async {
      final repo = factories.settingsRepository();
      final owner = core.OwnerId('owner-a');
      final settings = _settings(owner.value);

      final inserted = await repo.insert(ownerId: owner, settings: settings);
      expect(inserted.right, core.Revision.initial);
      expect(
        (await repo.insert(ownerId: owner, settings: settings)).left,
        isA<RepositoryDuplicateId>(),
      );

      final found = (await repo.findByOwner(ownerId: owner)).right;
      expect(found, isNot(same(settings)));
      expect(found!.timeZoneId, 'America/Los_Angeles');

      final saved = await repo.save(
        ownerId: owner,
        settings: settings.copyWith(compactModeEnabled: true),
        expectedRevision: inserted.right,
      );
      expect(saved.right, const core.Revision(2));
      expect((await repo.findByOwner(ownerId: owner)).right!.compactModeEnabled,
          isTrue);
      expect(
        (await repo.save(
          ownerId: owner,
          settings: settings,
          expectedRevision: inserted.right,
        ))
            .left,
        isA<RepositoryStaleRevision>(),
      );
    });
  });

  group('schedule snapshot repository compliance', () {
    test('supports window lookup, owner isolation, duplicates, and expiry',
        () async {
      final repo = factories.scheduleSnapshotRepository();
      final owner = core.OwnerId('owner-a');
      final otherOwner = core.OwnerId('owner-b');
      final snapshot = _snapshot('snapshot-a');

      final inserted = await repo.insert(ownerId: owner, snapshot: snapshot);
      expect(inserted.right, core.Revision.initial);
      expect(
        (await repo.insert(ownerId: owner, snapshot: snapshot)).left,
        isA<RepositoryDuplicateId>(),
      );

      final found =
          (await repo.findById(ownerId: owner, snapshotId: snapshot.id)).right;
      expect(found, isNot(same(snapshot)));
      expect(found!.tasks.single.id, 'task-a');
      expect(
        (await repo.findById(ownerId: otherOwner, snapshotId: snapshot.id))
            .right,
        isNull,
      );

      final overlapping = (await repo.findInWindow(
        ownerId: owner,
        window: core.SchedulingWindow(
          start: _instant(0, hour: 12),
          end: _instant(0, hour: 13),
        ),
      ))
          .right;
      expect(overlapping.items.map((item) => item.id), [snapshot.id]);

      final deleted = await repo.deleteExpired(
        ownerId: owner,
        nowUtc: snapshot.capturedAt.add(const Duration(days: 31)),
      );
      expect(deleted.right, 1);
      expect(
        (await repo.findById(ownerId: owner, snapshotId: snapshot.id)).right,
        isNull,
      );
    });
  });
}

/// Top-level helper that performs the `_instant` operation for this file.
/// Keeping the helper private makes the surrounding flow easier to read while preventing unrelated libraries from depending on implementation detail.
DateTime _instant(int day, {int hour = 9}) {
  return DateTime.utc(2026, 1, 1 + day, hour);
}

/// Top-level helper that performs the `_task` operation for this file.
/// Keeping the helper private makes the surrounding flow easier to read while preventing unrelated libraries from depending on implementation detail.
core.Task _task(
  String id, {
  String projectId = 'project-a',
  String? parentTaskId,
  core.TaskStatus status = core.TaskStatus.backlog,
}) {
  return core.Task(
    id: id,
    title: 'Task $id',
    projectId: projectId,
    type: core.TaskType.flexible,
    status: status,
    durationMinutes: 30,
    parentTaskId: parentTaskId,
    createdAt: _instant(0),
    updatedAt: _instant(0),
  );
}

/// Top-level helper that performs the `_project` operation for this file.
/// Keeping the helper private makes the surrounding flow easier to read while preventing unrelated libraries from depending on implementation detail.
core.ProjectProfile _project(String id) {
  return core.ProjectProfile(
    id: id,
    name: 'Project $id',
    colorKey: 'blue',
    defaultDurationMinutes: 30,
  );
}

/// Top-level helper that performs the `_lockedBlock` operation for this file.
/// Keeping the helper private makes the surrounding flow easier to read while preventing unrelated libraries from depending on implementation detail.
core.LockedBlock _lockedBlock(String id) {
  return core.LockedBlock(
    id: id,
    name: 'Block $id',
    date: core.CivilDate(2026, 1, 1),
    startTime: core.WallTime(hour: 10, minute: 0),
    endTime: core.WallTime(hour: 11, minute: 0),
    createdAt: _instant(0),
    updatedAt: _instant(0),
  );
}

/// Top-level helper that performs the `_override` operation for this file.
/// Keeping the helper private makes the surrounding flow easier to read while preventing unrelated libraries from depending on implementation detail.
core.LockedBlockOverride _override(
  String id, {
  String name = 'Added block',
}) {
  return core.LockedBlockOverride.add(
    id: id,
    date: core.CivilDate(2026, 1, 2),
    name: name,
    startTime: core.WallTime(hour: 14, minute: 0),
    endTime: core.WallTime(hour: 15, minute: 0),
    createdAt: _instant(1),
  );
}

/// Top-level helper that performs the `_settings` operation for this file.
/// Keeping the helper private makes the surrounding flow easier to read while preventing unrelated libraries from depending on implementation detail.
core.OwnerSettings _settings(String ownerId) {
  return core.OwnerSettings(
    ownerId: ownerId,
    timeZoneId: 'America/Los_Angeles',
    dayStart: core.WallTime(hour: 8, minute: 0),
    dayEnd: core.WallTime(hour: 22, minute: 0),
  );
}

/// Top-level helper that performs the `_snapshot` operation for this file.
/// Keeping the helper private makes the surrounding flow easier to read while preventing unrelated libraries from depending on implementation detail.
core.SchedulingStateSnapshot _snapshot(String id) {
  return core.SchedulingStateSnapshot(
    id: id,
    capturedAt: _instant(0),
    window: core.SchedulingWindow(
      start: _instant(0, hour: 8),
      end: _instant(0, hour: 17),
    ),
    tasks: [_task('task-a')],
    operationName: 'conformance',
  );
}
