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

/// Tests persistent SQLite composition behavior in the Flutter app.
library;

import 'dart:io';

import 'package:flutter/material.dart';
import 'package:flutter_test/flutter_test.dart';
import 'package:scheduler_core/scheduler_core.dart';

import 'package:focus_flow_flutter/app/focus_flow_app.dart';
import 'package:focus_flow_flutter/app/persistent_scheduler_composition.dart';
import 'package:focus_flow_flutter/app/runtime/focus_flow_runtime_config.dart';
import 'package:focus_flow_flutter/controllers/scheduler_command_controller.dart';
import 'package:focus_flow_flutter/models/today_screen_models.dart';

/// Runs persistent composition smoke tests.
void main() {
  testWidgets(
    'persistent app starts without seeded demo tasks and reopens capture',
    (tester) async {
      final path = await _runAsync(tester, _tempDatabasePath);
      final composition = await _runAsync(
        tester,
        () => PersistentSchedulerComposition.open(
          sqlitePath: path,
          selectedDate: _date,
          clock: FixedClock(_instant(8)),
        ),
      );

      await tester.pumpWidget(FocusFlowApp(composition: composition));
      await tester.pump();
      await tester.pump(const Duration(milliseconds: 100));
      await tester.pump(const Duration(seconds: 1));

      expect(find.text('January 2, 2026'), findsOneWidget);
      expect(find.text('Clean coffee maker'), findsNothing);
      expect(find.text('Water plants'), findsNothing);

      final commandController = composition.createCommandController(
        refreshReads: () async {},
        selectedDate: () => composition.initialDate,
      );
      addTearDown(commandController.dispose);
      await tester.runAsync(
        () => commandController.quickCaptureToBacklog(
          'Captured from Flutter test',
        ),
      );
      expect(commandController.state, isA<SchedulerCommandSuccess>());

      await tester.pumpWidget(const SizedBox.shrink());
      await tester.pump();
      await tester.runAsync(composition.dispose);

      final reopened = await _runAsync(
        tester,
        () => PersistentSchedulerComposition.open(
          sqlitePath: path,
          selectedDate: _date,
          clock: FixedClock(_instant(8, 10)),
        ),
      );
      addTearDown(() => tester.runAsync(reopened.dispose));
      final backlog = await _runAsync(
        tester,
        () => reopened.managementUseCases.getBacklog(
          GetBacklogRequest(
            context: reopened.context('flutter-read-backlog'),
            sortKey: BacklogSortKey.age,
          ),
        ),
      );

      expect(backlog.requireValue.items, hasLength(1));
      expect(
        backlog.requireValue.items.single.task.title,
        'Captured from Flutter test',
      );
    },
  );

  testWidgets('ui timezone config controls local startup date only', (
    tester,
  ) async {
    final path = await _runAsync(tester, _tempDatabasePath);
    final composition = await _runAsync(
      tester,
      () => PersistentSchedulerComposition.open(
        sqlitePath: path,
        clock: FixedClock(DateTime.utc(2026, 1, 3, 2)),
        uiTimeZone: FocusFlowUiTimeZone.tryParse(
          'pst',
          referenceInstant: DateTime.utc(2026, 1, 3, 2),
        ),
      ),
    );
    addTearDown(() => tester.runAsync(composition.dispose));

    expect(composition.initialDate, CivilDate(2026, 1, 2));
    expect(
      composition.context('timezone-context').ownerTimeZone.timeZoneId,
      'PST',
    );

    final storedSettings = await _runAsync(
      tester,
      () => composition.runtime.applicationStore.read<OwnerSettings?>(
        action: (repositories) async {
          return ApplicationResult.success(
            await repositories.ownerSettings.findByOwnerId('owner-1'),
          );
        },
      ),
    );

    expect(storedSettings.requireValue?.timeZoneId, 'UTC');
  });

  testWidgets(
    'selected future-day schedule completion and uncomplete persist across reopen',
    (tester) async {
      final path = await _runAsync(tester, _tempDatabasePath);
      final tomorrow = _date.addDays(1);
      const title = 'Schedule tomorrow from selected date';

      final selectedDate = tomorrow;
      var composition = await _openComposition(
        tester,
        path: path,
        selectedDate: selectedDate,
        clock: FixedClock(_instant(8)),
      );
      var commandController = _commandController(
        composition,
        selectedDate: selectedDate,
        operationIdPrefix: 'future-schedule',
        now: () => _instant(8),
      );
      await tester.runAsync(
        () => commandController.quickCaptureToBacklog(title),
      );
      expect(commandController.state, isA<SchedulerCommandSuccess>());

      final backlogBeforeSchedule = await _readBacklog(tester, composition);
      final captured = backlogBeforeSchedule.items.single.task;
      await tester.runAsync(
        () => commandController.scheduleBacklogItem(
          taskId: captured.id,
          durationMinutes: 30,
          expectedUpdatedAt: captured.updatedAt,
        ),
      );
      expect(commandController.state, isA<SchedulerCommandSuccess>());
      expect((await _readBacklog(tester, composition)).items, isEmpty);

      commandController.dispose();
      await _closeComposition(tester, composition);

      composition = await _openComposition(
        tester,
        path: path,
        selectedDate: selectedDate,
        clock: FixedClock(_instant(8, 10)),
      );
      final scheduled = await _readTodayItem(
        tester,
        composition,
        tomorrow,
        title,
      );
      expect(scheduled.taskStatus, TaskStatus.planned);
      expect(CivilDate.fromDateTime(scheduled.start!.toUtc()), tomorrow);

      commandController = _commandController(
        composition,
        selectedDate: selectedDate,
        operationIdPrefix: 'future-complete',
        now: () => DateTime.utc(2026, 1, 4, 8, 5),
      );
      await tester.runAsync(
        () => commandController.completeTask(
          taskId: scheduled.id,
          taskType: scheduled.taskType,
        ),
      );
      expect(commandController.state, isA<SchedulerCommandSuccess>());
      commandController.dispose();
      await _closeComposition(tester, composition);

      composition = await _openComposition(
        tester,
        path: path,
        selectedDate: selectedDate,
        clock: FixedClock(_instant(8, 20)),
      );
      final completed = await _readTodayItem(
        tester,
        composition,
        tomorrow,
        title,
      );
      expect(completed.taskStatus, TaskStatus.completed);
      expect(completed.item.completedAt, isNotNull);
      final completedData = TodayScreenData.fromTodayState(
        await _readToday(tester, composition, tomorrow),
        now: DateTime.utc(2026, 1, 4, 8, 20),
      );
      final completedCard = completedData.cards.singleWhere(
        (card) => card.title == title,
      );
      expect(completedCard.completionDateContextRequired, isTrue);
      expect(completedCard.completedTimeText, 'Jan 4, 8:05 AM');
      expect(completedCard.subtitle, 'Completed at: Jan 4, 8:05 AM');

      commandController = _commandController(
        composition,
        selectedDate: selectedDate,
        operationIdPrefix: 'future-uncomplete',
        now: () => DateTime.utc(2026, 1, 4, 8, 10),
      );
      await tester.runAsync(
        () => commandController.uncompleteTask(taskId: completed.id),
      );
      expect(commandController.state, isA<SchedulerCommandSuccess>());
      commandController.dispose();
      await _closeComposition(tester, composition);

      composition = await _openComposition(
        tester,
        path: path,
        selectedDate: selectedDate,
        clock: FixedClock(_instant(8, 30)),
      );
      addTearDown(() => tester.runAsync(composition.dispose));
      final reopened = await _readTodayItem(
        tester,
        composition,
        tomorrow,
        title,
      );
      expect(reopened.taskStatus, TaskStatus.planned);
      expect(reopened.item.completedAt, isNull);
      expect((await _readBacklog(tester, composition)).items, isEmpty);
    },
  );

  testWidgets('past task push destinations persist across reopen', (
    tester,
  ) async {
    final path = await _runAsync(tester, _tempDatabasePath);
    const pushNextTitle = 'Push next persistence';
    const pushTomorrowTitle = 'Push tomorrow persistence';
    const pushBacklogTitle = 'Push backlog persistence';
    var commandNow = _instant(8);

    var composition = await _openComposition(
      tester,
      path: path,
      selectedDate: _date,
      clock: FixedClock(commandNow),
    );
    final commandController = _commandController(
      composition,
      selectedDate: _date,
      operationIdPrefix: 'past-push',
      now: () => commandNow,
    );

    await _captureAndSchedule(
      tester,
      composition,
      commandController,
      pushNextTitle,
    );
    await _captureAndSchedule(
      tester,
      composition,
      commandController,
      pushTomorrowTitle,
    );
    await _captureAndSchedule(
      tester,
      composition,
      commandController,
      pushBacklogTitle,
    );

    commandNow = _instant(12);
    final pastCards =
        TodayScreenData.fromTodayState(
          await _readToday(tester, composition, _date),
          now: commandNow,
        ).cards.where(
          (card) => {
            pushNextTitle,
            pushTomorrowTitle,
            pushBacklogTitle,
          }.contains(card.title),
        );
    expect(pastCards, hasLength(3));
    expect(
      pastCards.map((card) => card.showPastTaskPushButton),
      everyElement(isTrue),
    );

    final pushNextItem = await _readTodayItem(
      tester,
      composition,
      _date,
      pushNextTitle,
    );
    await tester.runAsync(
      () => commandController.pushTaskToNextAvailableSlot(
        taskId: pushNextItem.id,
        expectedUpdatedAt: pushNextItem.item.updatedAt,
      ),
    );
    expect(commandController.state, isA<SchedulerCommandSuccess>());

    final pushTomorrowItem = await _readTodayItem(
      tester,
      composition,
      _date,
      pushTomorrowTitle,
    );
    await tester.runAsync(
      () => commandController.pushTaskToTomorrow(
        taskId: pushTomorrowItem.id,
        expectedUpdatedAt: pushTomorrowItem.item.updatedAt,
      ),
    );
    expect(commandController.state, isA<SchedulerCommandSuccess>());

    final pushBacklogItem = await _readTodayItem(
      tester,
      composition,
      _date,
      pushBacklogTitle,
    );
    await tester.runAsync(
      () => commandController.pushTaskToBacklog(
        taskId: pushBacklogItem.id,
        expectedUpdatedAt: pushBacklogItem.item.updatedAt,
      ),
    );
    expect(commandController.state, isA<SchedulerCommandSuccess>());

    final pushedNext = await _readTodayItem(
      tester,
      composition,
      _date,
      pushNextTitle,
    );
    expect(pushedNext.start, _instant(12));
    expect(pushedNext.end, _instant(12, 30));

    commandNow = _instant(12, 45);
    await tester.runAsync(
      () => commandController.completeTask(
        taskId: pushedNext.id,
        taskType: pushedNext.taskType,
        expectedUpdatedAt: pushedNext.item.updatedAt,
      ),
    );
    expect(commandController.state, isA<SchedulerCommandSuccess>());
    commandController.dispose();
    await _closeComposition(tester, composition);

    composition = await _openComposition(
      tester,
      path: path,
      selectedDate: _date,
      clock: FixedClock(_instant(13)),
    );
    addTearDown(() => tester.runAsync(composition.dispose));

    final reopenedNext = await _readTodayItem(
      tester,
      composition,
      _date,
      pushNextTitle,
    );
    expect(reopenedNext.taskStatus, TaskStatus.completed);
    expect(reopenedNext.start, _instant(12));
    expect(reopenedNext.item.completedAt, _instant(12, 45));
    final reopenedNextCard = TodayScreenData.fromTodayState(
      await _readToday(tester, composition, _date),
      now: _instant(13),
    ).cards.singleWhere((card) => card.title == pushNextTitle);
    expect(reopenedNextCard.showPastTaskPushButton, isFalse);

    final reopenedTomorrow = await _readTodayItem(
      tester,
      composition,
      _date.addDays(1),
      pushTomorrowTitle,
    );
    expect(reopenedTomorrow.taskStatus, TaskStatus.planned);
    expect(reopenedTomorrow.start, DateTime.utc(2026, 1, 3));
    expect(reopenedTomorrow.end, DateTime.utc(2026, 1, 3, 0, 30));

    final reopenedBacklog = await _readBacklog(tester, composition);
    final backlogTask = reopenedBacklog.items.single.task;
    expect(backlogTask.title, pushBacklogTitle);
    expect(backlogTask.scheduledStart, isNull);
    expect(backlogTask.backlogEnteredAt, _instant(12));
    expect(
      backlogTask.backlogEnteredAtProvenance,
      Task.backlogEnteredAtProvenanceMovedToBacklog,
    );
    expect(
      (await _readToday(
        tester,
        composition,
        _date,
      )).timelineItems.map((item) => item.item.displayTitle),
      isNot(contains(pushBacklogTitle)),
    );
  });

  testWidgets('push to tomorrow uses ui-local current date at UTC rollover', (
    tester,
  ) async {
    final path = await _runAsync(tester, _tempDatabasePath);
    final currentLocalDate = CivilDate(2026, 7, 4);
    final tomorrow = currentLocalDate.addDays(1);
    final dayAfterTomorrow = currentLocalDate.addDays(2);
    final commandNow = DateTime.utc(2026, 7, 5, 2);
    const title = 'Push tomorrow from evening PST';

    final composition = await _openComposition(
      tester,
      path: path,
      selectedDate: currentLocalDate,
      clock: FixedClock(commandNow),
      uiTimeZone: FocusFlowUiTimeZone.tryParse(
        'pst',
        referenceInstant: commandNow,
      ),
    );
    addTearDown(() => tester.runAsync(composition.dispose));
    final commandController = _commandController(
      composition,
      selectedDate: currentLocalDate,
      operationIdPrefix: 'local-tomorrow-rollover',
      now: () => commandNow,
    );
    addTearDown(commandController.dispose);

    await _captureAndSchedule(tester, composition, commandController, title);
    final scheduled = await _readTodayItem(
      tester,
      composition,
      currentLocalDate,
      title,
    );

    await tester.runAsync(
      () => commandController.pushTaskToTomorrow(
        taskId: scheduled.id,
        expectedUpdatedAt: scheduled.item.updatedAt,
      ),
    );

    expect(commandController.state, isA<SchedulerCommandSuccess>());
    final pushed = await _readTodayItem(tester, composition, tomorrow, title);
    expect(pushed.taskStatus, TaskStatus.planned);
    expect(pushed.start, DateTime.utc(2026, 7, 5, 7));
    expect(pushed.end, DateTime.utc(2026, 7, 5, 7, 30));
    expect(
      (await _readToday(
        tester,
        composition,
        dayAfterTomorrow,
      )).timelineItems.map((item) => item.item.displayTitle),
      isNot(contains(title)),
    );
  });
}

/// Fixed date used by persistent Flutter 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);
}

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

/// Opens a persistent composition for a test database.
Future<PersistentSchedulerComposition> _openComposition(
  WidgetTester tester, {
  required String path,
  required CivilDate selectedDate,
  required Clock clock,
  FocusFlowUiTimeZone? uiTimeZone,
}) {
  return _runAsync(
    tester,
    () => PersistentSchedulerComposition.open(
      sqlitePath: path,
      selectedDate: selectedDate,
      clock: clock,
      uiTimeZone: uiTimeZone,
    ),
  );
}

/// Closes [composition] outside the widget-test fake async zone.
Future<void> _closeComposition(
  WidgetTester tester,
  PersistentSchedulerComposition composition,
) async {
  await tester.runAsync(composition.dispose);
}

/// Reads the backlog through the management use case.
Future<BacklogQueryResult> _readBacklog(
  WidgetTester tester,
  PersistentSchedulerComposition composition,
) async {
  final result = await _runAsync(
    tester,
    () => composition.managementUseCases.getBacklog(
      GetBacklogRequest(
        context: composition.context('flutter-read-backlog'),
        sortKey: BacklogSortKey.age,
      ),
    ),
  );
  return result.requireValue;
}

/// Builds a deterministic command controller for persistence lifecycle tests.
SchedulerCommandController _commandController(
  PersistentSchedulerComposition composition, {
  required CivilDate selectedDate,
  required String operationIdPrefix,
  required DateTime Function() now,
}) {
  return SchedulerCommandController(
    commands: composition.commandUseCases,
    contextFor: composition.context,
    selectedDate: () => selectedDate,
    refreshReads: () async {},
    quickCaptureProjectId: composition.defaultProjectId,
    operationIdPrefix: operationIdPrefix,
    currentDateFor: composition.uiTimeZone.toLocalCivilDate,
    now: now,
  );
}

/// Captures [title] into Backlog and schedules it on the selected test date.
Future<void> _captureAndSchedule(
  WidgetTester tester,
  PersistentSchedulerComposition composition,
  SchedulerCommandController commandController,
  String title,
) async {
  await tester.runAsync(() => commandController.quickCaptureToBacklog(title));
  expect(commandController.state, isA<SchedulerCommandSuccess>());

  final captured = (await _readBacklog(
    tester,
    composition,
  )).items.singleWhere((item) => item.task.title == title).task;
  await tester.runAsync(
    () => commandController.scheduleBacklogItem(
      taskId: captured.id,
      durationMinutes: 30,
      expectedUpdatedAt: captured.updatedAt,
    ),
  );
  expect(commandController.state, isA<SchedulerCommandSuccess>());
}

/// Reads one Today timeline item by [title].
Future<TodayTimelineItem> _readTodayItem(
  WidgetTester tester,
  PersistentSchedulerComposition composition,
  CivilDate date,
  String title,
) async {
  final state = await _readToday(tester, composition, date);
  return state.timelineItems.singleWhere(
    (item) => item.item.displayTitle == title,
  );
}

/// Reads Today state for [date].
Future<TodayState> _readToday(
  WidgetTester tester,
  PersistentSchedulerComposition composition,
  CivilDate date,
) async {
  final result = await _runAsync(
    tester,
    () => composition.todayQuery.execute(
      GetTodayStateRequest(
        context: composition.context(
          'flutter-read-today-${date.toIsoString()}',
        ),
        date: date,
        includeNextFlexibleItem: true,
      ),
    ),
  );
  return result.requireValue;
}

/// Runs real async work outside the widget-test fake async zone.
Future<T> _runAsync<T>(
  WidgetTester tester,
  Future<T> Function() callback,
) async {
  final result = await tester.runAsync(callback);
  if (result == null) {
    throw StateError('runAsync returned null.');
  }
  return result;
}
