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

/// Tests Repositories 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('Repository interfaces', () {
    final createdAt = DateTime(2026, 6, 21, 8);
    final window = SchedulingWindow(
      start: DateTime(2026, 6, 21, 9),
      end: DateTime(2026, 6, 21, 17),
    );

    test('task repository supports in-memory fakes for domain services',
        () async {
      final backlogTask = task(
        id: 'backlog',
        title: 'Backlog task',
        status: TaskStatus.backlog,
        createdAt: createdAt,
      );
      final scheduledTask = task(
        id: 'scheduled',
        title: 'Scheduled task',
        status: TaskStatus.planned,
        createdAt: createdAt,
        scheduledStart: DateTime(2026, 6, 21, 10),
        scheduledEnd: DateTime(2026, 6, 21, 10, 30),
      );
      final repository = InMemoryTaskRepository([backlogTask]);

      await repository.save(scheduledTask);

      expect(await repository.findById('backlog'), same(backlogTask));
      expect(await repository.findByStatus(TaskStatus.backlog), [backlogTask]);
      expect(
        (await repository.findScheduledInWindow(window)).map((task) => task.id),
        ['scheduled'],
      );
    });

    test('project repository stores project profiles by stable id', () async {
      final project = ProjectProfile(
        id: 'home',
        name: 'Home',
        colorKey: 'project-home',
      );
      final repository = InMemoryProjectRepository();

      await repository.save(project);

      expect(await repository.findById('home'), same(project));
      expect(await repository.findAll(), [project]);
    });

    test('locked block repository separates blocks from overrides', () async {
      final block = LockedBlock(
        id: 'work',
        name: 'Work',
        startTime: ClockTime(hour: 9, minute: 0),
        endTime: ClockTime(hour: 17, minute: 0),
        recurrence: LockedBlockRecurrence.weekly(
          weekdays: {LockedWeekday.monday},
        ),
        createdAt: createdAt,
        updatedAt: createdAt,
      );
      final override = LockedBlockOverride.remove(
        id: 'holiday',
        lockedBlockId: block.id,
        date: CivilDate(2026, 6, 22),
        createdAt: createdAt,
      );
      final repository = InMemoryLockedBlockRepository();

      await repository.saveBlock(block);
      await repository.saveOverride(override);

      expect(await repository.findBlockById('work'), same(block));
      expect(await repository.findOverrideById('holiday'), same(override));
      expect(await repository.findAllBlocks(), [block]);
      expect(await repository.findAllOverrides(), [override]);
    });

    test('scheduling snapshot repository preserves scheduler input shape',
        () async {
      final scheduledTask = task(
        id: 'scheduled',
        title: 'Scheduled task',
        status: TaskStatus.planned,
        createdAt: createdAt,
        scheduledStart: DateTime(2026, 6, 21, 10),
        scheduledEnd: DateTime(2026, 6, 21, 10, 30),
      );
      final lockedInterval = TimeInterval(
        start: DateTime(2026, 6, 21, 12),
        end: DateTime(2026, 6, 21, 13),
        label: 'Lunch',
      );
      final snapshot = SchedulingStateSnapshot(
        id: 'snapshot-1',
        capturedAt: DateTime(2026, 6, 21, 8, 30),
        window: window,
        tasks: [scheduledTask],
        lockedIntervals: [lockedInterval],
        notices: [
          SchedulingNotice('Saved schedule'),
        ],
        operationName: 'load-day',
      );
      final repository = InMemorySchedulingSnapshotRepository();

      await repository.save(snapshot);

      final saved = await repository.findById('snapshot-1');
      expect(saved, same(snapshot));
      expect(saved!.input.tasks, [scheduledTask]);
      expect(saved.input.lockedIntervals, [lockedInterval]);
      expect(saved.result.tasks, [scheduledTask]);
      expect(saved.result.notices.single.message, 'Saved schedule');
      expect(
        (await repository.findInWindow(window)).map((snapshot) => snapshot.id),
        ['snapshot-1'],
      );
    });
  });

  runCoreRepositoryConformance(
    createTaskRepository: () => InMemoryTaskRepository(
      [
        task(
          id: 'backlog-a',
          title: 'Backlog A',
          status: TaskStatus.backlog,
          createdAt: DateTime.utc(2026, 6, 20),
        ),
        task(
          id: 'backlog-b',
          title: 'Backlog B',
          status: TaskStatus.backlog,
          createdAt: DateTime.utc(2026, 6, 21),
        ).copyWith(projectId: 'home', parentTaskId: 'parent'),
        task(
          id: 'scheduled',
          title: 'Scheduled',
          status: TaskStatus.planned,
          createdAt: DateTime.utc(2026, 6, 21),
          scheduledStart: DateTime.utc(2026, 6, 22, 10),
          scheduledEnd: DateTime.utc(2026, 6, 22, 10, 30),
        ).copyWith(projectId: 'home'),
      ],
    ),
    createProjectRepository: () => InMemoryProjectRepository([
      ProjectProfile(id: 'home', name: 'Home', colorKey: 'home-blue'),
      ProjectProfile(id: 'work', name: 'Work', colorKey: 'work-green'),
    ]),
    createLockedBlockRepository: () => InMemoryLockedBlockRepository(
      initialBlocks: [
        LockedBlock(
          id: 'work',
          name: 'Work',
          startTime: ClockTime(hour: 9, minute: 0),
          endTime: ClockTime(hour: 17, minute: 0),
          recurrence: LockedBlockRecurrence.weekly(
            weekdays: {LockedWeekday.monday},
          ),
          createdAt: DateTime.utc(2026, 6, 20),
          updatedAt: DateTime.utc(2026, 6, 20),
        ),
      ],
      initialOverrides: [
        LockedBlockOverride.remove(
          id: 'holiday',
          lockedBlockId: 'work',
          date: CivilDate(2026, 6, 22),
          createdAt: DateTime.utc(2026, 6, 20),
        ),
      ],
    ),
  );
}

/// Runs the `runCoreRepositoryConformance` top-level operation.
/// This function is part of the file-level API and gives callers a direct way to use the associated scheduler behavior.
void runCoreRepositoryConformance({
  required TaskRepository Function() createTaskRepository,
  required ProjectRepository Function() createProjectRepository,
  required LockedBlockRepository Function() createLockedBlockRepository,
}) {
  group('Core repository conformance', () {
    const ownerId = 'owner-1';

    test('task queries are owner-scoped, ordered, paged, and revisioned',
        () async {
      final repository = createTaskRepository();
      final firstPage = await repository.findByOwner(
        ownerId: ownerId,
        page: const RepositoryPageRequest(limit: 2),
      );
      final secondPage = await repository.findByOwner(
        ownerId: ownerId,
        page: RepositoryPageRequest(cursor: firstPage.nextCursor, limit: 2),
      );
      final homeTasks = await repository.findByProjectId(
        ownerId: ownerId,
        projectId: 'home',
      );
      final children = await repository.findByParentTaskId(
        ownerId: ownerId,
        parentTaskId: 'parent',
      );
      final backlog = await repository.findBacklogCandidates(
        const BacklogCandidateQuery(ownerId: ownerId, projectId: 'home'),
      );
      final localDay = await repository.findForLocalDay(
        ownerId: ownerId,
        localDate: CivilDate(2026, 6, 22),
        window: SchedulingWindow(
          start: DateTime.utc(2026, 6, 22),
          end: DateTime.utc(2026, 6, 23),
        ),
      );
      final record = await repository.findRecordById('scheduled');
      final updated = record!.value.copyWith(
        title: 'Updated scheduled',
        updatedAt: DateTime.utc(2026, 6, 22, 12),
      );

      expect(firstPage.items.map((task) => task.id), [
        'backlog-a',
        'backlog-b',
      ]);
      expect(firstPage.hasMore, isTrue);
      expect(secondPage.items.map((task) => task.id), ['scheduled']);
      expect(homeTasks.items.map((task) => task.id), [
        'backlog-b',
        'scheduled',
      ]);
      expect(children.items.map((task) => task.id), ['backlog-b']);
      expect(backlog.items.map((task) => task.id), ['backlog-b']);
      expect(localDay.items.map((task) => task.id), ['scheduled']);
      expect(record.ownerId, ownerId);
      expect(record.revision, 1);

      final stale = await repository.saveIfRevision(
        task: updated,
        ownerId: ownerId,
        expectedRevision: 99,
      );
      final invalidRevision = await repository.saveIfRevision(
        task: updated,
        ownerId: ownerId,
        expectedRevision: 0,
      );
      final wrongOwner = await repository.saveIfRevision(
        task: updated,
        ownerId: 'other-owner',
        expectedRevision: 1,
      );
      expect(stale.failure!.code, RepositoryFailureCode.staleRevision);
      expect(stale.failure!.actualRevision, 1);
      expect(
        invalidRevision.failure!.code,
        RepositoryFailureCode.invalidRevision,
      );
      expect(wrongOwner.failure!.code, RepositoryFailureCode.ownerMismatch);

      final saved = await repository.saveIfRevision(
        task: updated,
        ownerId: ownerId,
        expectedRevision: 1,
      );
      expect(saved.isSuccess, isTrue);
      expect(saved.revision, 2);
      expect((await repository.findRecordById('scheduled'))!.revision, 2);

      expect(
        () => firstPage.items.add(record.value),
        throwsUnsupportedError,
      );
    });

    test('project queries hide archived records by default and honor revisions',
        () async {
      final repository = createProjectRepository();
      final record = await repository.findRecordById('home');

      final archived = await repository.archive(
        projectId: 'home',
        ownerId: ownerId,
        expectedRevision: record!.revision,
        archivedAt: DateTime.utc(2026, 6, 25),
      );
      final visible = await repository.findByOwner(ownerId: ownerId);
      final includingArchived = await repository.findByOwner(
        ownerId: ownerId,
        includeArchived: true,
      );

      expect(archived.isSuccess, isTrue);
      expect(visible.items.map((project) => project.id), ['work']);
      expect(includingArchived.items.map((project) => project.id), [
        'home',
        'work',
      ]);
      expect(
        (await repository.findRecordById('home'))!.archivedAt,
        DateTime.utc(2026, 6, 25),
      );
      expect(
        (await repository.insert(
          project: ProjectProfile(id: 'home', name: 'Home', colorKey: 'again'),
          ownerId: ownerId,
        ))
            .failure!
            .code,
        RepositoryFailureCode.duplicateId,
      );
    });

    test('locked queries find date-scoped overrides and archive by revision',
        () async {
      final repository = createLockedBlockRepository();
      final blockRecord = await repository.findBlockRecordById('work');
      final overrides = await repository.findOverridesForDate(
        ownerId: ownerId,
        date: CivilDate(2026, 6, 22),
      );

      final archived = await repository.archiveBlock(
        blockId: 'work',
        ownerId: ownerId,
        expectedRevision: blockRecord!.revision,
        archivedAt: DateTime.utc(2026, 6, 25),
      );
      final visible = await repository.findBlocksByOwner(ownerId: ownerId);
      final includingArchived = await repository.findBlocksByOwner(
        ownerId: ownerId,
        includeArchived: true,
      );

      expect(overrides.items.map((override) => override.id), ['holiday']);
      expect(archived.isSuccess, isTrue);
      expect(visible.items, isEmpty);
      expect(includingArchived.items.map((block) => block.id), ['work']);
      expect(
        (await repository.findBlockRecordById('work'))!.archivedAt,
        DateTime.utc(2026, 6, 25),
      );
    });
  });
}

/// Runs the `task` 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 task({
  required String id,
  required String title,
  required TaskStatus status,
  required DateTime createdAt,
  DateTime? scheduledStart,
  DateTime? scheduledEnd,
}) {
  return Task(
    id: id,
    title: title,
    projectId: 'inbox',
    type: TaskType.flexible,
    status: status,
    durationMinutes: 30,
    scheduledStart: scheduledStart,
    scheduledEnd: scheduledEnd,
    createdAt: createdAt,
    updatedAt: createdAt,
  );
}
