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

/// Tests Today State 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('GetTodayStateQuery', () {
    final day = CivilDate(2026, 6, 25);
    final createdAt = DateTime.utc(2026, 6, 20, 8);
    final project = ProjectProfile(
      id: 'home',
      name: 'Home',
      colorKey: 'project-home',
    );

    test('returns an empty side-effect-free day state', () async {
      final store = InMemoryApplicationUnitOfWork(
        initialProjects: [project],
      );
      final query = todayQuery(store);

      final result = await query.execute(
        GetTodayStateRequest(
          context: appContext(
            operationId: 'read-empty',
            now: DateTime.utc(2026, 6, 25, 8),
          ),
          date: day,
        ),
      );

      final state = result.requireValue;
      expect(state.date, day);
      expect(state.ownerId, 'owner-1');
      expect(state.timeZoneId, 'UTC');
      expect(state.timelineItems, isEmpty);
      expect(state.currentItem, isNull);
      expect(state.nextRequiredItem, isNull);
      expect(state.nextFlexibleItem, isNull);
      expect(state.pendingNotices, isEmpty);
      expect(store.currentOperations, isEmpty);
      expect(store.currentTaskActivities, isEmpty);
      expect(store.currentProjectStatistics, isEmpty);
    });

    test('uses context timezone without rewriting persisted owner settings',
        () async {
      final localDay = CivilDate(2026, 1, 3);
      final store = InMemoryApplicationUnitOfWork(
        initialOwnerSettings: [
          OwnerSettings(ownerId: 'owner-1', timeZoneId: 'UTC'),
        ],
      );
      final query = GetTodayStateQuery(
        applicationStore: store,
        timeZoneResolver: const FixedOffsetTimeZoneResolver(
          offset: Duration(hours: -8),
        ),
      );

      final state = (await query.execute(
        GetTodayStateRequest(
          context: appContext(
            operationId: 'read-context-timezone',
            now: DateTime.utc(2026, 1, 3, 12),
            timeZoneId: 'PST',
          ),
          date: localDay,
        ),
      ))
          .requireValue;

      expect(state.timeZoneId, 'PST');
      expect(state.ownerSettings.timeZoneId, 'PST');
      expect(state.dayStart, DateTime.utc(2026, 1, 3, 8));
      expect(state.dayEnd, DateTime.utc(2026, 1, 4, 7, 59));
      expect(store.currentOwnerSettings.single.timeZoneId, 'UTC');
    });

    test(
        'builds one sorted model for tasks, Free Slots, locked overlays, and notices',
        () async {
      final dayStart = DateTime.utc(2026, 6, 25);
      final dayEnd = DateTime.utc(2026, 6, 25, 23, 59);
      final flexible = task(
        id: 'flexible',
        title: 'Flexible work',
        type: TaskType.flexible,
        status: TaskStatus.planned,
        start: DateTime.utc(2026, 6, 25, 9),
        end: DateTime.utc(2026, 6, 25, 9, 30),
        createdAt: createdAt,
      );
      final freeSlot = task(
        id: 'rest',
        title: 'Rest',
        type: TaskType.freeSlot,
        status: TaskStatus.planned,
        start: DateTime.utc(2026, 6, 25, 10),
        end: DateTime.utc(2026, 6, 25, 10, 30),
        createdAt: createdAt,
      );
      final required = task(
        id: 'appointment',
        title: 'Appointment',
        type: TaskType.inflexible,
        status: TaskStatus.planned,
        start: DateTime.utc(2026, 6, 25, 11),
        end: DateTime.utc(2026, 6, 25, 11, 30),
        createdAt: createdAt,
      );
      final surprise = task(
        id: 'surprise',
        title: 'Surprise done',
        type: TaskType.surprise,
        status: TaskStatus.completed,
        start: DateTime.utc(2026, 6, 25, 12),
        end: DateTime.utc(2026, 6, 25, 12, 20),
        createdAt: createdAt,
        completedAt: DateTime.utc(2026, 6, 25, 12, 20),
      );
      final visibleLocked = LockedBlock(
        id: 'visible-lock',
        name: 'Visible locked',
        startTime: WallTime(hour: 13, minute: 0),
        endTime: WallTime(hour: 14, minute: 0),
        date: day,
        hiddenByDefault: false,
        projectId: project.id,
        createdAt: createdAt,
        updatedAt: createdAt,
      );
      final snapshot = SchedulingStateSnapshot(
        id: 'rollover-notice',
        capturedAt: DateTime.utc(2026, 6, 25, 7),
        window: SchedulingWindow(start: dayStart, end: dayEnd),
        tasks: const [],
        notices: [
          SchedulingNotice(
            'Moved 2 unfinished tasks to today.',
            type: SchedulingNoticeType.moved,
            movementCode:
                SchedulingMovementCode.unfinishedFlexibleTasksRolledOver,
            parameters: const {'count': 2},
          ),
        ],
      );
      final store = InMemoryApplicationUnitOfWork(
        initialProjects: [project],
        initialTasks: [surprise, required, freeSlot, flexible],
        initialLockedBlocks: [visibleLocked],
        initialSchedulingSnapshots: [snapshot],
      );

      final state = (await todayQuery(store).execute(
        GetTodayStateRequest(
          context: appContext(
            operationId: 'read-mixed',
            now: DateTime.utc(2026, 6, 25, 8),
          ),
          date: day,
        ),
      ))
          .requireValue;

      expect(state.timelineItems.map((item) => item.id), [
        'flexible',
        'rest',
        'appointment',
        'surprise',
        'locked-block:visible-lock:2026-06-25',
      ]);
      expect(state.timelineItems.first.taskStatus, TaskStatus.planned);
      expect(state.timelineItems.first.quickActions, [
        TimelineQuickAction.done,
        TimelineQuickAction.push,
        TimelineQuickAction.backlog,
        TimelineQuickAction.breakUp,
      ]);
      expect(
        state.timelineItems.singleWhere((item) => item.id == 'rest').taskType,
        TaskType.freeSlot,
      );
      expect(
        state.timelineItems
            .singleWhere((item) => item.id == 'rest')
            .quickActions,
        isEmpty,
      );
      expect(
        state.timelineItems
            .singleWhere((item) => item.id == 'surprise')
            .taskStatus,
        TaskStatus.completed,
      );
      final locked = state.timelineItems.last;
      expect(locked.source, TodayTimelineItemSource.lockedOccurrence);
      expect(locked.taskType, TaskType.locked);
      expect(locked.projectColorToken, 'project-home');
      expect(state.pendingNotices.single.movementCode,
          SchedulingMovementCode.unfinishedFlexibleTasksRolledOver);
      expect(state.pendingNotices.single.parameters['count'], 2);
    });

    test('uses start-inclusive end-exclusive boundaries for current selection',
        () async {
      final ended = task(
        id: 'ended',
        title: 'Ended',
        type: TaskType.flexible,
        status: TaskStatus.planned,
        start: DateTime.utc(2026, 6, 25, 9),
        end: DateTime.utc(2026, 6, 25, 10),
        createdAt: createdAt,
      );
      final starting = task(
        id: 'starting',
        title: 'Starting',
        type: TaskType.inflexible,
        status: TaskStatus.planned,
        start: DateTime.utc(2026, 6, 25, 10),
        end: DateTime.utc(2026, 6, 25, 10, 30),
        createdAt: createdAt,
      );
      final store = InMemoryApplicationUnitOfWork(
        initialProjects: [project],
        initialTasks: [ended, starting],
      );

      final state = (await todayQuery(store).execute(
        GetTodayStateRequest(
          context: appContext(
            operationId: 'read-boundary',
            now: DateTime.utc(2026, 6, 25, 10),
          ),
          date: day,
        ),
      ))
          .requireValue;

      expect(state.currentItem!.id, 'starting');
      expect(state.currentItem!.id, isNot('ended'));
      expect(state.nextRequiredItem!.id, 'starting');
    });

    test(
        'builds compact projection without duplicating the current flexible task',
        () async {
      final current = task(
        id: 'current-flex',
        title: 'Current flexible',
        type: TaskType.flexible,
        status: TaskStatus.planned,
        start: DateTime.utc(2026, 6, 25, 9),
        end: DateTime.utc(2026, 6, 25, 9, 30),
        createdAt: createdAt,
      );
      final nextRequired = task(
        id: 'next-required',
        title: 'Next required',
        type: TaskType.critical,
        status: TaskStatus.planned,
        start: DateTime.utc(2026, 6, 25, 10),
        end: DateTime.utc(2026, 6, 25, 10, 30),
        createdAt: createdAt,
      );
      final laterFlexible = task(
        id: 'later-flex',
        title: 'Later flexible',
        type: TaskType.flexible,
        status: TaskStatus.planned,
        start: DateTime.utc(2026, 6, 25, 11),
        end: DateTime.utc(2026, 6, 25, 11, 30),
        createdAt: createdAt,
      );
      final store = InMemoryApplicationUnitOfWork(
        initialProjects: [project],
        initialTasks: [laterFlexible, nextRequired, current],
        initialOwnerSettings: [
          OwnerSettings(
            ownerId: 'owner-1',
            timeZoneId: 'UTC',
            compactModeEnabled: true,
          ),
        ],
      );

      final state = (await todayQuery(store).execute(
        GetTodayStateRequest(
          context: appContext(
            operationId: 'read-compact',
            now: DateTime.utc(2026, 6, 25, 9),
          ),
          date: day,
        ),
      ))
          .requireValue;

      expect(state.currentItem!.id, 'current-flex');
      expect(state.nextRequiredItem!.id, 'next-required');
      expect(state.nextFlexibleItem!.id, 'later-flex');
      expect(state.compactState.fullTimelineVisible, isFalse);
      expect(state.compactState.compactItems.map((item) => item.id), [
        'current-flex',
        'next-required',
        'later-flex',
      ]);
    });

    test('keeps hidden locked details absent unless reveal mode is requested',
        () async {
      final hidden = LockedBlock(
        id: 'work-hours',
        name: 'Work hours',
        startTime: WallTime(hour: 9, minute: 0),
        endTime: WallTime(hour: 17, minute: 0),
        recurrence: LockedBlockRecurrence.weekly(
          weekdays: {LockedWeekday.thursday},
        ),
        hiddenByDefault: true,
        createdAt: createdAt,
        updatedAt: createdAt,
      );
      final store = InMemoryApplicationUnitOfWork(
        initialLockedBlocks: [hidden],
      );

      final defaultState = (await todayQuery(store).execute(
        GetTodayStateRequest(
          context: appContext(
            operationId: 'read-hidden-default',
            now: DateTime.utc(2026, 6, 25, 8),
          ),
          date: day,
        ),
      ))
          .requireValue;
      final revealedState = (await todayQuery(store).execute(
        GetTodayStateRequest(
          context: appContext(
            operationId: 'read-hidden-revealed',
            now: DateTime.utc(2026, 6, 25, 8),
          ),
          date: day,
          revealHiddenLockedBlocks: true,
        ),
      ))
          .requireValue;

      expect(defaultState.timelineItems, isEmpty);
      expect(revealedState.timelineItems.single.id,
          'locked-block:work-hours:2026-06-25');
      expect(revealedState.timelineItems.single.hiddenByDefault, isTrue);
    });

    test('uses date-stable locked occurrence ids across days', () async {
      final block = LockedBlock(
        id: 'daily-lock',
        name: 'Daily lock',
        startTime: WallTime(hour: 9, minute: 0),
        endTime: WallTime(hour: 10, minute: 0),
        recurrence: LockedBlockRecurrence.weekly(
          weekdays: {LockedWeekday.thursday, LockedWeekday.friday},
        ),
        hiddenByDefault: false,
        createdAt: createdAt,
        updatedAt: createdAt,
      );
      final store = InMemoryApplicationUnitOfWork(
        initialLockedBlocks: [block],
      );
      final query = todayQuery(store);

      final thursday = (await query.execute(
        GetTodayStateRequest(
          context: appContext(
            operationId: 'read-thursday',
            now: DateTime.utc(2026, 6, 25, 8),
          ),
          date: CivilDate(2026, 6, 25),
        ),
      ))
          .requireValue;
      final friday = (await query.execute(
        GetTodayStateRequest(
          context: appContext(
            operationId: 'read-friday',
            now: DateTime.utc(2026, 6, 26, 8),
          ),
          date: CivilDate(2026, 6, 26),
        ),
      ))
          .requireValue;

      expect(thursday.timelineItems.single.id,
          'locked-block:daily-lock:2026-06-25');
      expect(
          friday.timelineItems.single.id, 'locked-block:daily-lock:2026-06-26');
      expect(thursday.timelineItems.single.id,
          isNot(friday.timelineItems.single.id));
    });

    test('expands locked occurrences through explicit DST resolver context',
        () async {
      final dstDay = CivilDate(2026, 3, 8);
      final block = LockedBlock(
        id: 'dst-lock',
        name: 'DST lock',
        startTime: WallTime(hour: 1, minute: 30),
        endTime: WallTime(hour: 3, minute: 30),
        date: dstDay,
        hiddenByDefault: false,
        createdAt: createdAt,
        updatedAt: createdAt,
      );
      final store = InMemoryApplicationUnitOfWork(
        initialLockedBlocks: [block],
        initialOwnerSettings: [
          OwnerSettings(
            ownerId: 'owner-1',
            timeZoneId: 'America/Los_Angeles',
          ),
        ],
      );
      final query = GetTodayStateQuery(
        applicationStore: store,
        timeZoneResolver: _LosAngelesDstFixtureResolver(),
      );

      final state = (await query.execute(
        GetTodayStateRequest(
          context: appContext(
            operationId: 'read-dst',
            now: DateTime.utc(2026, 3, 8, 8),
            timeZoneId: 'America/Los_Angeles',
          ),
          date: dstDay,
        ),
      ))
          .requireValue;

      final item = state.timelineItems.single;
      expect(state.dayStart, DateTime.utc(2026, 3, 8, 8));
      expect(state.dayEnd, DateTime.utc(2026, 3, 9, 6, 59));
      expect(item.id, 'locked-block:dst-lock:2026-03-08');
      expect(item.start, DateTime.utc(2026, 3, 8, 9, 30));
      expect(item.end, DateTime.utc(2026, 3, 8, 10, 30));
    });

    test('uses deterministic ordering for equal-start items', () async {
      final bTask = task(
        id: 'b-task',
        title: 'B task',
        type: TaskType.flexible,
        status: TaskStatus.planned,
        start: DateTime.utc(2026, 6, 25, 9),
        end: DateTime.utc(2026, 6, 25, 9, 30),
        createdAt: createdAt,
      );
      final aTask = task(
        id: 'a-task',
        title: 'A task',
        type: TaskType.flexible,
        status: TaskStatus.planned,
        start: DateTime.utc(2026, 6, 25, 9),
        end: DateTime.utc(2026, 6, 25, 9, 30),
        createdAt: createdAt,
      );
      final lock = LockedBlock(
        id: 'same-start',
        name: 'Same start',
        startTime: WallTime(hour: 9, minute: 0),
        endTime: WallTime(hour: 9, minute: 30),
        date: day,
        hiddenByDefault: false,
        createdAt: createdAt,
        updatedAt: createdAt,
      );
      final store = InMemoryApplicationUnitOfWork(
        initialTasks: [bTask, aTask],
        initialLockedBlocks: [lock],
      );

      final state = (await todayQuery(store).execute(
        GetTodayStateRequest(
          context: appContext(
            operationId: 'read-ordering',
            now: DateTime.utc(2026, 6, 25, 8),
          ),
          date: day,
        ),
      ))
          .requireValue;

      expect(state.timelineItems.map((item) => item.id), [
        'a-task',
        'b-task',
        'locked-block:same-start:2026-06-25',
      ]);
    });
  });
}

/// Runs the `todayQuery` top-level operation.
/// This function is part of the file-level API and gives callers a direct way to use the associated scheduler behavior.
GetTodayStateQuery todayQuery(InMemoryApplicationUnitOfWork store) {
  return GetTodayStateQuery(
    applicationStore: store,
    timeZoneResolver: const FixedOffsetTimeZoneResolver.utc(),
  );
}

/// 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,
  String timeZoneId = 'UTC',
}) {
  return ApplicationOperationContext.start(
    operationId: operationId,
    ownerTimeZone: OwnerTimeZoneContext(
      ownerId: 'owner-1',
      timeZoneId: timeZoneId,
    ),
    clock: FixedClock(now),
    idGenerator: SequentialIdGenerator(prefix: 'generated'),
  );
}

/// 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 TaskType type,
  required TaskStatus status,
  required DateTime start,
  required DateTime end,
  required DateTime createdAt,
  DateTime? completedAt,
}) {
  return Task(
    id: id,
    title: title,
    projectId: 'home',
    type: type,
    status: status,
    durationMinutes: end.difference(start).inMinutes,
    scheduledStart: start,
    scheduledEnd: end,
    completedAt: completedAt,
    createdAt: createdAt,
    updatedAt: createdAt,
  );
}

/// Private implementation type for `_LosAngelesDstFixtureResolver` in this library.
/// It keeps supporting state and behavior scoped to the owning file so the public API stays focused on the scheduler concepts that callers use directly.
class _LosAngelesDstFixtureResolver implements TimeZoneResolver {
  /// Performs the `resolve` behavior for this scheduler component.
  /// The method provides a named entry point for this operation so callers do not need to know the lower-level state or persistence steps involved.
  @override
  ResolvedLocalDateTime resolve({
    required CivilDate date,
    required WallTime wallTime,
    required String timeZoneId,
    TimeZoneResolutionOptions options = const TimeZoneResolutionOptions(),
  }) {
    if (timeZoneId.trim() != 'America/Los_Angeles') {
      throw DomainValidationException(
        code: DomainValidationCode.blankTimeZoneId,
        invalidValue: timeZoneId,
        name: 'timeZoneId',
        message: 'Fixture resolver only supports America/Los_Angeles.',
      );
    }

    final offset = _offsetFor(date, wallTime);
    final localAsUtc = DateTime.utc(
      date.year,
      date.month,
      date.day,
      wallTime.hour,
      wallTime.minute,
    );

    return ResolvedLocalDateTime(
      date: date,
      wallTime: wallTime,
      timeZoneId: timeZoneId,
      instant: localAsUtc.subtract(offset),
      resolution: LocalTimeResolution.exact,
      utcOffset: offset,
    );
  }

  /// Runs the `_offsetFor` helper used inside this library.
  /// The helper centralizes a small piece of transformation or validation so higher-level scheduler flow remains easier to read.
  Duration _offsetFor(CivilDate date, WallTime wallTime) {
    if (date == CivilDate(2026, 3, 8) && wallTime.hour >= 3) {
      return const Duration(hours: -7);
    }

    return const Duration(hours: -8);
  }
}
