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

/// Tests Widget behavior in the FocusFlow Flutter app.
library;

import 'dart:async';

import 'package:flutter/gestures.dart';
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/demo_scheduler_composition.dart';
import 'package:focus_flow_flutter/app/focus_flow_app.dart';
import 'package:focus_flow_flutter/models/today_screen_models.dart';
import 'package:focus_flow_flutter/theme/focus_flow_theme.dart';
import 'package:focus_flow_flutter/widgets/required_banner.dart';
import 'package:focus_flow_flutter/widgets/task_selection_modal.dart';
import 'package:focus_flow_flutter/widgets/timeline/difficulty_bars.dart';
import 'package:focus_flow_flutter/widgets/timeline/reward_icon.dart';
import 'package:focus_flow_flutter/widgets/timeline/task_timeline_card.dart';
import 'package:focus_flow_flutter/widgets/timeline/timeline_axis.dart';
import 'package:focus_flow_flutter/widgets/timeline/timeline_geometry.dart';
import 'package:focus_flow_flutter/widgets/timeline/timeline_view.dart';
import 'package:focus_flow_flutter/widgets/top_bar.dart';

/// Runs FocusFlow widget and visual adapter tests.
void main() {
  testWidgets('app shell renders compact Today frame', (tester) async {
    await _pumpPlanApp(tester);

    expect(find.text('FocusFlow'), findsOneWidget);
    expect(find.text('Today'), findsWidgets);
    expect(find.text('Backlog'), findsOneWidget);
    expect(find.text('Projects'), findsOneWidget);
    expect(find.text('Reports'), findsOneWidget);
    expect(find.text('Settings'), findsWidgets);
    expect(find.text('Compact'), findsOneWidget);
    expect(find.text('Normal'), findsOneWidget);
    expect(find.byKey(const ValueKey('nav-today-active')), findsOneWidget);
  });

  testWidgets('sidebar switches to Backlog shell', (tester) async {
    await _pumpPlanApp(tester);

    await tester.tap(find.text('Backlog').first);
    await tester.pumpAndSettle();

    expect(find.byKey(const ValueKey('nav-backlog-active')), findsOneWidget);
    expect(find.byKey(const ValueKey('nav-today-active')), findsNothing);
    expect(
      find.text('Choose work that fits your current energy.'),
      findsOneWidget,
    );
    expect(find.byKey(const ValueKey('backlog-search-field')), findsOneWidget);
    expect(find.text('Search backlog...'), findsOneWidget);
    expect(find.text('Filters'), findsOneWidget);
    expect(find.text('Sort: Custom'), findsOneWidget);
    expect(find.text('Group: None'), findsOneWidget);
    expect(find.text('Board'), findsOneWidget);
    expect(find.text('New Task'), findsOneWidget);
    expect(find.text('Backlog summary'), findsOneWidget);
    expect(find.text('24 tasks'), findsOneWidget);
    expect(find.text('Do Next'), findsOneWidget);
    expect(find.text('Need Time Block'), findsOneWidget);
    expect(find.text('Break Up First'), findsOneWidget);
    expect(find.text('Not Now'), findsOneWidget);
    expect(find.text('Review Q3 budget'), findsOneWidget);
  });

  testWidgets('seeded Today renders backend-backed compact mockup data', (
    tester,
  ) async {
    await _pumpPlanApp(tester);

    expect(find.text('May 20, 2025'), findsOneWidget);
    expect(
      find.textContaining('Next required task:', findRichText: true),
      findsOneWidget,
    );
    expect(find.text('Clean coffee maker'), findsOneWidget);
    expect(find.text('Cleaned kitchen counter'), findsOneWidget);
    expect(find.text('Completed at: 8:05 AM'), findsOneWidget);
    expect(find.textContaining('Surprise task'), findsNothing);
    expect(find.text('Sort mail'), findsOneWidget);
    expect(find.text('Start laundry'), findsOneWidget);
    expect(find.text('Water plants'), findsOneWidget);
    expect(find.text('Pay bill'), findsOneWidget);
    expect(find.text('Doctor appointment'), findsOneWidget);
    expect(find.text('Free Slot'), findsOneWidget);
    expect(find.text('Intentional rest'), findsOneWidget);
  });

  test(
    'visual token adapter maps seeded items to expected visual kinds',
    () async {
      final state = await _readSeededToday();
      final data = TodayScreenData.fromTodayState(
        state,
        now: DateTime.utc(2025, 5, 20, 15, 20),
      );

      expect(_kindFor(data, 'clean-coffee-maker'), TaskVisualKind.flexible);
      expect(_kindFor(data, 'sort-mail'), TaskVisualKind.flexible);
      expect(_kindFor(data, 'start-laundry'), TaskVisualKind.flexible);
      expect(_kindFor(data, 'water-plants'), TaskVisualKind.flexible);
      expect(_kindFor(data, 'pay-bill'), TaskVisualKind.required);
      expect(_kindFor(data, 'doctor-appointment'), TaskVisualKind.appointment);
      expect(_kindFor(data, 'free-slot'), TaskVisualKind.freeSlot);
      expect(
        _kindFor(data, 'cleaned-kitchen-counter'),
        TaskVisualKind.completedSurprise,
      );
    },
  );

  test('difficulty bars map every difficulty level to five-bar fill count', () {
    expect(DifficultyBars.fillCountForLevel(DifficultyLevel.notSet), 0);
    expect(DifficultyBars.fillCountForLevel(DifficultyLevel.veryEasy), 1);
    expect(DifficultyBars.fillCountForLevel(DifficultyLevel.easy), 2);
    expect(DifficultyBars.fillCountForLevel(DifficultyLevel.medium), 3);
    expect(DifficultyBars.fillCountForLevel(DifficultyLevel.hard), 4);
    expect(DifficultyBars.fillCountForLevel(DifficultyLevel.veryHard), 5);
  });

  testWidgets('task modal opens closes and keeps compact actions inert', (
    tester,
  ) async {
    await _pumpPlanApp(tester);

    expect(find.byKey(const ValueKey('task-selection-modal')), findsNothing);

    await _scrollIntoView(tester, const ValueKey('pay-bill'));
    await tester.tap(find.byKey(const ValueKey('pay-bill')));
    await tester.pumpAndSettle();

    expect(find.byKey(const ValueKey('task-selection-modal')), findsOneWidget);
    expect(find.text('Pay bill'), findsNWidgets(2));
    expect(find.textContaining('Required'), findsWidgets);
    expect(find.byTooltip('Reward level 2'), findsOneWidget);
    expect(find.byTooltip('Medium effort'), findsOneWidget);
    expect(find.byTooltip('Mark complete'), findsNothing);
    expect(find.text('Reward level 2'), findsNothing);
    expect(find.text('Medium effort'), findsNothing);
    expect(find.text('Done'), findsNothing);
    expect(find.text('Push'), findsWidgets);
    expect(find.text('Move to Backlog'), findsNothing);
    expect(find.text('Backlog'), findsWidgets);
    expect(find.text('Break up'), findsOneWidget);

    await tester.tap(find.text('Break up'));
    await tester.pumpAndSettle();

    expect(find.byKey(const ValueKey('task-selection-modal')), findsOneWidget);
    expect(find.text('Pay bill'), findsNWidgets(2));

    await tester.tap(find.byKey(const ValueKey('close-task-modal')));
    await tester.pumpAndSettle();

    expect(find.byKey(const ValueKey('task-selection-modal')), findsNothing);

    await _scrollIntoView(tester, const ValueKey('pay-bill'));
    await tester.tap(find.byKey(const ValueKey('pay-bill')));
    await tester.pumpAndSettle();
    await tester.tapAt(const Offset(1450, 140));
    await tester.pumpAndSettle();

    expect(find.byKey(const ValueKey('task-selection-modal')), findsNothing);
    expect(find.text('Pay bill'), findsOneWidget);
  });

  testWidgets('task modal Push exposes destination labels and callbacks', (
    tester,
  ) async {
    var nextCount = 0;
    var tomorrowCount = 0;
    var backlogCount = 0;

    Future<void> openMenu() async {
      await tester.tap(
        find.byKey(const ValueKey('modal-push-modal-push-button')),
      );
      await tester.pumpAndSettle();
    }

    await _pumpConstrainedWidget(
      tester,
      size: const Size(640, 380),
      child: TaskSelectionModal(
        card: _timelineCard(
          id: 'modal-push',
          title: 'Modal push task',
          rewardIconToken: TimelineRewardIconToken.notSet,
          difficultyIconToken: TimelineDifficultyIconToken.notSet,
        ),
        onClose: () {},
        onPushToNext: (_) async {
          nextCount += 1;
        },
        onPushToTomorrow: (_) async {
          tomorrowCount += 1;
        },
        onPushToBacklog: (_) async {
          backlogCount += 1;
        },
      ),
    );

    expect(find.byTooltip('Not Set'), findsNWidgets(2));
    expect(find.byTooltip('Push task'), findsNothing);
    expect(find.text('Remove'), findsOneWidget);

    await openMenu();
    expect(find.text('Push to next'), findsOneWidget);
    expect(find.text('Push to tomorrow'), findsOneWidget);
    expect(find.text('Push to backlog'), findsOneWidget);

    await tester.tap(find.text('Push to next'));
    await tester.pumpAndSettle();
    expect(nextCount, 1);

    await openMenu();
    await tester.tap(find.text('Push to tomorrow'));
    await tester.pumpAndSettle();
    expect(tomorrowCount, 1);

    await openMenu();
    await tester.tap(find.text('Push to backlog'));
    await tester.pumpAndSettle();
    expect(backlogCount, 1);
  });

  testWidgets('task modal Remove deletes task and closes modal', (
    tester,
  ) async {
    final composition = _composition();
    await _pumpPlanApp(tester, composition: composition);

    await _scrollIntoView(tester, const ValueKey('water-plants'));
    await tester.tap(find.byKey(const ValueKey('water-plants')));
    await tester.pumpAndSettle();

    expect(find.byKey(const ValueKey('task-selection-modal')), findsOneWidget);
    expect(find.text('Water plants'), findsWidgets);

    await tester.tap(find.text('Remove'));
    await tester.pumpAndSettle();

    expect(find.byKey(const ValueKey('task-selection-modal')), findsNothing);
    expect(find.text('Water plants'), findsNothing);
    expect(
      composition.store.currentTasks.any((task) => task.id == 'water-plants'),
      isFalse,
    );
  });

  test('Today screen timeline range covers the full day', () {
    expect(TodayScreenData.defaultRange.startMinutes, 0);
    expect(TodayScreenData.defaultRange.endMinutes, 24 * 60);
  });

  testWidgets('timeline task bubbles match duration height exactly', (
    tester,
  ) async {
    await _pumpConstrainedWidget(
      tester,
      size: const Size(680, 360),
      child: TimelineView(
        cards: [
          _timelineCard(
            id: 'five-minute-task',
            title: 'Five minute task',
            subtitle: '5 min',
            startMinutes: 8 * 60,
            endMinutes: 8 * 60 + 5,
          ),
          _timelineCard(
            id: 'fifteen-minute-task',
            title: 'Fifteen minute task',
            subtitle: '15 min',
            startMinutes: 8 * 60 + 5,
            endMinutes: 8 * 60 + 20,
          ),
        ],
        range: TodayScreenData.defaultRange,
        now: () => DateTime(2026, 6, 30, 8),
        onCardSelected: (_) {},
      ),
    );

    expect(tester.takeException(), isNull);

    final fiveMinute = tester.getRect(
      find.byKey(const ValueKey('five-minute-task')),
    );
    final fifteenMinute = tester.getRect(
      find.byKey(const ValueKey('fifteen-minute-task')),
    );

    expect(fiveMinute.height, closeTo(5 * 2.45, 0.5));
    expect(fifteenMinute.height, closeTo(15 * 2.45, 0.5));
    expect(fiveMinute.left, closeTo(fifteenMinute.left, 1));
    expect(fiveMinute.bottom, closeTo(fifteenMinute.top, 1));
  });

  testWidgets('required banner jumps timeline to linked task', (tester) async {
    await _pumpPlanApp(tester);

    final scrollable = tester.state<ScrollableState>(find.byType(Scrollable));
    await tester.tap(find.text('Show upcoming'));
    await tester.pumpAndSettle();

    expect(scrollable.position.pixels, closeTo(2634, 1));
    expect(find.byKey(const ValueKey('pay-bill')), findsOneWidget);
  });

  testWidgets(
    'timeline defaults with current time one quarter down the viewport',
    (tester) async {
      await _pumpConstrainedWidget(
        tester,
        size: const Size(680, 400),
        child: TimelineView(
          cards: const [],
          range: TodayScreenData.defaultRange,
          now: () => DateTime(2026, 6, 30, 12),
          onCardSelected: (_) {},
        ),
      );

      expect(tester.takeException(), isNull);
      expect(find.text('11:00 PM'), findsOneWidget);
      expect(find.text('12:00 AM'), findsWidgets);

      final scrollable = tester.state<ScrollableState>(find.byType(Scrollable));
      final position = scrollable.position;
      expect(position.maxScrollExtent, greaterThan(3000));
      expect(position.pixels, closeTo(1676, 1));
    },
  );

  testWidgets('timeline scroll-to-now request reuses initial scroll math', (
    tester,
  ) async {
    var request = 0;
    var currentTime = DateTime(2026, 6, 30, 12);
    late StateSetter updateTimeline;

    await _pumpConstrainedWidget(
      tester,
      size: const Size(680, 400),
      child: StatefulBuilder(
        builder: (context, setState) {
          updateTimeline = setState;
          return TimelineView(
            cards: const [],
            range: TodayScreenData.defaultRange,
            now: () => currentTime,
            scrollToNowRequest: request,
            onCardSelected: (_) {},
          );
        },
      ),
    );

    final scrollable = tester.state<ScrollableState>(find.byType(Scrollable));
    expect(scrollable.position.pixels, closeTo(1676, 1));

    scrollable.position.jumpTo(0);
    await tester.pump();
    updateTimeline(() {
      currentTime = DateTime(2026, 6, 30, 18);
      request += 1;
    });
    await tester.pumpAndSettle();

    expect(scrollable.position.pixels, closeTo(2558, 1));
  });

  testWidgets('timeline hour labels stay on one line', (tester) async {
    await _pumpConstrainedWidget(
      tester,
      size: const Size(140, 220),
      child: const TimelineAxis(
        geometry: TimelineGeometry(
          startMinutes: 0,
          endMinutes: 60,
          pixelsPerMinute: 2.45,
        ),
      ),
    );

    expect(tester.takeException(), isNull);

    final midnight = tester.widget<Text>(find.text('12:00 AM'));
    final oneAm = tester.widget<Text>(find.text('1:00 AM'));
    expect(midnight.maxLines, 1);
    expect(oneAm.maxLines, 1);
    expect(midnight.softWrap, isFalse);
    expect(oneAm.softWrap, isFalse);
    expect(midnight.overflow, TextOverflow.visible);
    expect(oneAm.overflow, TextOverflow.visible);
    expect(
      tester.getSize(find.text('12:00 AM')).height,
      closeTo(tester.getSize(find.text('1:00 AM')).height, 0.1),
    );
    expect(
      tester.getRect(find.text('12:00 AM')).top,
      greaterThanOrEqualTo(tester.getRect(find.byType(TimelineAxis)).top),
    );
  });

  testWidgets('timeline scrolls with a mouse drag', (tester) async {
    await _pumpConstrainedWidget(
      tester,
      size: const Size(680, 400),
      child: TimelineView(
        cards: const [],
        range: TodayScreenData.defaultRange,
        now: () => DateTime(2026, 6, 30, 12),
        onCardSelected: (_) {},
      ),
    );

    expect(tester.takeException(), isNull);

    final scrollable = tester.state<ScrollableState>(find.byType(Scrollable));
    final beforeDrag = scrollable.position.pixels;
    await tester.drag(
      find.byType(SingleChildScrollView),
      const Offset(0, -180),
      kind: PointerDeviceKind.mouse,
    );
    await tester.pumpAndSettle();

    expect(scrollable.position.pixels, greaterThan(beforeDrag + 80));
  });

  testWidgets('timeline context menu adds a generic flexible task', (
    tester,
  ) async {
    final composition = _composition();
    await _pumpPlanApp(tester, composition: composition);

    await tester.tapAt(
      tester.getCenter(find.byType(TimelineView)),
      buttons: kSecondaryMouseButton,
    );
    await tester.pumpAndSettle();

    expect(find.text('Add new task'), findsOneWidget);

    await tester.tap(find.text('Add new task'));
    await tester.pumpAndSettle();

    final addedTask = composition.store.currentTasks.singleWhere(
      (task) => task.title == 'New flexible task',
    );
    expect(addedTask.type, TaskType.flexible);
    expect(addedTask.status, TaskStatus.planned);
    expect(addedTask.durationMinutes, 30);
    expect(find.text('New flexible task'), findsOneWidget);
  });

  testWidgets('timeline splits partial overlaps and reuses empty lanes', (
    tester,
  ) async {
    await _pumpConstrainedWidget(
      tester,
      size: const Size(680, 520),
      child: TimelineView(
        cards: [
          _timelineCard(
            id: 'left-first',
            title: 'Left first',
            startMinutes: 9 * 60,
            endMinutes: 10 * 60,
          ),
          _timelineCard(
            id: 'right-middle',
            title: 'Right middle',
            startMinutes: 9 * 60 + 30,
            endMinutes: 10 * 60 + 30,
          ),
          _timelineCard(
            id: 'left-reused',
            title: 'Left reused',
            startMinutes: 10 * 60,
            endMinutes: 11 * 60,
          ),
        ],
        range: TodayScreenData.defaultRange,
        now: () => DateTime(2026, 6, 30, 9, 30),
        onCardSelected: (_) {},
      ),
    );

    expect(tester.takeException(), isNull);

    final leftFirst = tester.getRect(find.byKey(const ValueKey('left-first')));
    final rightMiddle = tester.getRect(
      find.byKey(const ValueKey('right-middle')),
    );
    final leftReused = tester.getRect(
      find.byKey(const ValueKey('left-reused')),
    );

    expect(rightMiddle.left, greaterThan(leftFirst.left));
    expect(leftReused.left, closeTo(leftFirst.left, 1));
    expect(leftFirst.width, closeTo(rightMiddle.width, 1));
    expect(leftReused.width, closeTo(leftFirst.width, 1));
    expect(leftFirst.right, lessThan(rightMiddle.left));
    expect(leftReused.right, lessThan(rightMiddle.left));
  });

  testWidgets('seeded demo partial overlaps pack into two lanes', (
    tester,
  ) async {
    final state = await _readSeededToday();
    final data = TodayScreenData.fromTodayState(
      state,
      now: DateTime.utc(2025, 5, 20, 15, 20),
    );

    await _pumpConstrainedWidget(
      tester,
      size: const Size(900, 620),
      child: TimelineView(
        cards: data.cards,
        range: data.timelineRange,
        now: () => DateTime(2025, 5, 20, 15, 20),
        onCardSelected: (_) {},
      ),
    );

    expect(tester.takeException(), isNull);

    final sortMail = tester.getRect(find.byKey(const ValueKey('sort-mail')));
    final startLaundry = tester.getRect(
      find.byKey(const ValueKey('start-laundry')),
    );
    final waterPlants = tester.getRect(
      find.byKey(const ValueKey('water-plants')),
    );
    final cleanCoffee = tester.getRect(
      find.byKey(const ValueKey('clean-coffee-maker')),
    );
    final cleanedKitchenCounter = tester.getRect(
      find.byKey(const ValueKey('cleaned-kitchen-counter')),
    );

    expect(startLaundry.left, greaterThan(sortMail.left));
    expect(waterPlants.left, closeTo(sortMail.left, 1));
    expect(sortMail.width, closeTo(startLaundry.width, 1));
    expect(waterPlants.width, closeTo(sortMail.width, 1));
    expect(sortMail.right, lessThan(startLaundry.left));
    expect(waterPlants.right, lessThan(startLaundry.left));
    expect(cleanCoffee.height, closeTo(15 * 2.45, 0.5));
    expect(cleanedKitchenCounter.height, closeTo(15 * 2.45, 0.5));
    expect(
      data.cards
          .singleWhere((card) => card.id == 'cleaned-kitchen-counter')
          .startMinutes,
      8 * 60 + 15,
    );
  });

  testWidgets('top bar controls scale inside a compact header width', (
    tester,
  ) async {
    await _pumpConstrainedWidget(
      tester,
      size: const Size(520, 72),
      child: const TopBar(dateLabel: 'June 30, 2026'),
    );

    expect(tester.takeException(), isNull);
    expect(find.text('Compact'), findsOneWidget);
    expect(find.text('Normal'), findsOneWidget);
    expect(find.text('Settings'), findsOneWidget);
    expect(tester.getSize(find.byType(OutlinedButton)).width, lessThan(136));
  });

  testWidgets('top bar date controls invoke callbacks', (tester) async {
    var previousCount = 0;
    var nextCount = 0;
    var datePressedCount = 0;
    var dateLabelPressedCount = 0;

    await _pumpConstrainedWidget(
      tester,
      size: const Size(760, 72),
      child: TopBar(
        dateLabel: 'June 30, 2026',
        onPreviousDate: () {
          previousCount += 1;
        },
        onNextDate: () {
          nextCount += 1;
        },
        onDatePressed: () {
          datePressedCount += 1;
        },
        onDateLabelPressed: () {
          dateLabelPressedCount += 1;
        },
      ),
    );

    await tester.tap(find.byTooltip('Previous day'));
    await tester.tap(find.byTooltip('Next day'));
    await tester.tap(find.byTooltip('Choose date'));
    await tester.tap(find.byKey(const ValueKey('top-bar-date-button')));
    await tester.pumpAndSettle();

    expect(previousCount, 1);
    expect(nextCount, 1);
    expect(datePressedCount, 1);
    expect(dateLabelPressedCount, 1);
    expect(find.byTooltip('Choose date'), findsOneWidget);
    expect(find.byTooltip('Scroll to current time'), findsOneWidget);
  });

  testWidgets('required banner scales inside a compact header width', (
    tester,
  ) async {
    await _pumpConstrainedWidget(
      tester,
      size: const Size(440, 72),
      child: const RequiredBanner(
        model: RequiredBannerModel(
          timelineCardId: 'pay-bill',
          title: 'Pay bill',
          timeText: '6:00 PM',
        ),
      ),
    );

    expect(tester.takeException(), isNull);
    expect(
      find.textContaining('Next required task:', findRichText: true),
      findsOneWidget,
    );
    expect(find.text('Show upcoming'), findsOneWidget);
    expect(tester.getSize(find.byType(OutlinedButton)).width, lessThan(176));
  });

  testWidgets('empty required banner does not reserve vertical space', (
    tester,
  ) async {
    await tester.binding.setSurfaceSize(const Size(480, 160));
    addTearDown(() => tester.binding.setSurfaceSize(null));
    await tester.pumpWidget(
      MaterialApp(
        theme: FocusFlowTheme.dark(),
        home: const Scaffold(
          body: Align(alignment: Alignment.topLeft, child: RequiredBanner()),
        ),
      ),
    );
    await tester.pumpAndSettle();

    expect(tester.getSize(find.byType(RequiredBanner)).height, 0);
  });

  testWidgets('task card controls scale inside a narrow task bubble', (
    tester,
  ) async {
    await _pumpTimelineCard(
      tester,
      card: _timelineCard(
        id: 'narrow-actions',
        title: 'A task title that would otherwise crowd the controls',
      ),
      size: const Size(320, 88),
    );

    expect(tester.takeException(), isNull);
    expect(
      tester
          .widget<AnimatedOpacity>(
            find.byKey(const ValueKey('narrow-actions-quick-actions-opacity')),
          )
          .opacity,
      0,
    );

    final gesture = await tester.createGesture(kind: PointerDeviceKind.mouse);
    await gesture.addPointer(
      location: tester.getCenter(find.byKey(const ValueKey('narrow-actions'))),
    );
    await tester.pumpAndSettle();

    expect(
      tester
          .widget<AnimatedOpacity>(
            find.byKey(const ValueKey('narrow-actions-quick-actions-opacity')),
          )
          .opacity,
      1,
    );
    final actionBackground = tester.widget<AnimatedContainer>(
      find.byKey(const ValueKey('narrow-actions-quick-actions-background')),
    );
    final decoration = actionBackground.decoration;
    expect(decoration, isA<BoxDecoration>());
    final boxDecoration = decoration! as BoxDecoration;
    expect(boxDecoration.color, const Color(0xFF021A0C));
    expect(boxDecoration.border, isNull);

    final icon = tester.widget<Icon>(find.byIcon(Icons.arrow_forward));
    expect(icon.size, lessThan(18));
    expect(
      tester.getSize(find.byIcon(Icons.arrow_forward)).width,
      lessThan(32),
    );

    await gesture.removePointer();
  });

  testWidgets('past incomplete flexible card renders Push', (tester) async {
    await _pumpTimelineCard(
      tester,
      card: _timelineCard(
        id: 'past-flexible',
        title: 'Past flexible task',
        showPastTaskPushButton: true,
      ),
      size: const Size(360, 88),
      onPushToNext: (_) async {},
    );

    expect(find.text('Push'), findsOneWidget);
    expect(
      find.byKey(const ValueKey('past-flexible-push-button')),
      findsOneWidget,
    );
  });

  testWidgets('completed and future cards do not render Push', (tester) async {
    await _pumpTimelineCard(
      tester,
      card: _timelineCard(
        id: 'completed-flexible',
        title: 'Completed flexible task',
        isCompleted: true,
        showsQuickActions: false,
      ),
      size: const Size(360, 88),
    );

    expect(find.text('Push'), findsNothing);

    await _pumpTimelineCard(
      tester,
      card: _timelineCard(id: 'future-flexible', title: 'Future flexible task'),
      size: const Size(360, 88),
    );

    expect(find.text('Push'), findsNothing);
  });

  testWidgets('past incomplete flexible card suppresses hover quick actions', (
    tester,
  ) async {
    await _pumpTimelineCard(
      tester,
      card: _timelineCard(
        id: 'past-no-hover',
        title: 'Past task',
        showPastTaskPushButton: true,
      ),
      size: const Size(360, 88),
      onPushToNext: (_) async {},
    );

    final gesture = await tester.createGesture(kind: PointerDeviceKind.mouse);
    await gesture.addPointer(
      location: tester.getCenter(find.byKey(const ValueKey('past-no-hover'))),
    );
    await tester.pumpAndSettle();

    expect(find.byType(AnimatedOpacity), findsNothing);
    expect(find.byIcon(Icons.arrow_forward), findsNothing);

    await gesture.removePointer();
  });

  testWidgets('Push menu exposes destination labels and callbacks', (
    tester,
  ) async {
    var nextCount = 0;
    var tomorrowCount = 0;
    var backlogCount = 0;

    Future<void> openMenu() async {
      await tester.tap(find.byKey(const ValueKey('push-menu-push-button')));
      await tester.pumpAndSettle();
    }

    await _pumpTimelineCard(
      tester,
      card: _timelineCard(
        id: 'push-menu',
        title: 'Past task',
        showPastTaskPushButton: true,
      ),
      size: const Size(420, 88),
      onPushToNext: (_) async {
        nextCount += 1;
      },
      onPushToTomorrow: (_) async {
        tomorrowCount += 1;
      },
      onPushToBacklog: (_) async {
        backlogCount += 1;
      },
    );

    await openMenu();

    expect(find.text('Push to next'), findsOneWidget);
    expect(find.text('Push to tomorrow'), findsOneWidget);
    expect(find.text('Push to backlog'), findsOneWidget);
    expect(find.textContaining('Push to '), findsNWidgets(3));

    await tester.tap(find.text('Push to next'));
    await tester.pumpAndSettle();
    expect(nextCount, 1);

    await openMenu();
    await tester.tap(find.text('Push to tomorrow'));
    await tester.pumpAndSettle();
    expect(tomorrowCount, 1);

    await openMenu();
    await tester.tap(find.text('Push to backlog'));
    await tester.pumpAndSettle();
    expect(backlogCount, 1);
  });

  testWidgets('hover arrow exposes the same Push destination menu', (
    tester,
  ) async {
    var nextCount = 0;
    var tomorrowCount = 0;
    var backlogCount = 0;

    Future<void> openMenu() async {
      await tester.tap(
        find.byKey(const ValueKey('quick-push-menu-quick-push-button')),
      );
      await tester.pumpAndSettle();
    }

    await _pumpTimelineCard(
      tester,
      card: _timelineCard(id: 'quick-push-menu', title: 'Current task'),
      size: const Size(420, 88),
      onPushToNext: (_) async {
        nextCount += 1;
      },
      onPushToTomorrow: (_) async {
        tomorrowCount += 1;
      },
      onPushToBacklog: (_) async {
        backlogCount += 1;
      },
    );

    final gesture = await tester.createGesture(kind: PointerDeviceKind.mouse);
    await gesture.addPointer(
      location: tester.getCenter(find.byKey(const ValueKey('quick-push-menu'))),
    );
    await tester.pumpAndSettle();

    expect(find.byIcon(Icons.check), findsNothing);
    await openMenu();
    expect(find.text('Push to next'), findsOneWidget);
    expect(find.text('Push to tomorrow'), findsOneWidget);
    expect(find.text('Push to backlog'), findsOneWidget);

    await tester.tap(find.text('Push to next'));
    await tester.pumpAndSettle();
    expect(nextCount, 1);

    await openMenu();
    await tester.tap(find.text('Push to tomorrow'));
    await tester.pumpAndSettle();
    expect(tomorrowCount, 1);

    await openMenu();
    await tester.tap(find.text('Push to backlog'));
    await tester.pumpAndSettle();
    expect(backlogCount, 1);

    await gesture.removePointer();
  });

  testWidgets('Push menu dismisses on outside click without callback', (
    tester,
  ) async {
    var count = 0;
    await _pumpTimelineCard(
      tester,
      card: _timelineCard(
        id: 'push-dismiss',
        title: 'Past task',
        showPastTaskPushButton: true,
      ),
      size: const Size(420, 88),
      onPushToNext: (_) async {
        count += 1;
      },
    );

    await tester.tap(find.byKey(const ValueKey('push-dismiss-push-button')));
    await tester.pumpAndSettle();
    await tester.tapAt(Offset.zero);
    await tester.pumpAndSettle();

    expect(find.text('Push to next'), findsNothing);
    expect(count, 0);
  });

  testWidgets('Push selection is one shot while command is running', (
    tester,
  ) async {
    final completer = Completer<void>();
    var count = 0;
    await _pumpTimelineCard(
      tester,
      card: _timelineCard(
        id: 'push-running',
        title: 'Past task',
        showPastTaskPushButton: true,
      ),
      size: const Size(420, 88),
      onPushToNext: (_) async {
        count += 1;
        await completer.future;
      },
    );

    await tester.tap(find.byKey(const ValueKey('push-running-push-button')));
    await tester.pumpAndSettle();
    await tester.tap(find.text('Push to next'));
    await tester.pump();
    await tester.tap(find.byKey(const ValueKey('push-running-push-button')));
    await tester.pump();

    expect(count, 1);

    completer.complete();
    await tester.pumpAndSettle();
  });

  testWidgets('short task card keeps time inline with title', (tester) async {
    await _pumpTimelineCard(
      tester,
      card: _timelineCard(
        id: 'short-inline-time',
        title: 'Five minute reset',
        subtitle: '5 min',
        timeText: '6:00 PM - 6:05 PM',
        startMinutes: 18 * 60,
        endMinutes: 18 * 60 + 5,
      ),
      size: const Size(360, 24),
    );

    expect(tester.takeException(), isNull);

    final title = tester.getRect(find.text('Five minute reset'));
    final time = tester.getRect(find.text('5 min'));
    final statusCircle = tester.getRect(
      find.byKey(const ValueKey('short-inline-time-status')),
    );

    expect(statusCircle.height, lessThan(20));
    expect(statusCircle.right, lessThan(title.left));
    expect(title.right, lessThan(time.left));
    expect((title.center.dy - time.center.dy).abs(), lessThan(2));
    expect(find.byType(RewardIcon), findsOneWidget);
    expect(find.byType(DifficultyBars), findsOneWidget);

    final gesture = await tester.createGesture(kind: PointerDeviceKind.mouse);
    await gesture.addPointer(
      location: tester.getCenter(
        find.byKey(const ValueKey('short-inline-time')),
      ),
    );
    await tester.pumpAndSettle();

    expect(
      tester.widget<AnimatedOpacity>(find.byType(AnimatedOpacity)).opacity,
      1,
    );
    expect(find.text('5 min'), findsOneWidget);

    await gesture.removePointer();
  });

  testWidgets('status circle toggles task completion without opening modal', (
    tester,
  ) async {
    final composition = _composition();
    await _pumpPlanApp(tester, composition: composition);

    await _scrollIntoView(tester, const ValueKey('clean-coffee-maker'));
    var statusDecoration =
        tester
                .widget<AnimatedContainer>(
                  find.byKey(const ValueKey('clean-coffee-maker-status')),
                )
                .decoration
            as BoxDecoration;
    expect(statusDecoration.boxShadow, isEmpty);

    final hover = await tester.createGesture(kind: PointerDeviceKind.mouse);
    await hover.addPointer(
      location: tester.getCenter(
        find.byKey(const ValueKey('clean-coffee-maker-status')),
      ),
    );
    await tester.pumpAndSettle();
    statusDecoration =
        tester
                .widget<AnimatedContainer>(
                  find.byKey(const ValueKey('clean-coffee-maker-status')),
                )
                .decoration
            as BoxDecoration;
    expect(statusDecoration.boxShadow, isNotEmpty);
    await hover.removePointer();
    await tester.pumpAndSettle();

    final beforeClick = DateTime.now().toUtc();
    await tester.tap(find.byKey(const ValueKey('clean-coffee-maker-status')));
    await tester.pumpAndSettle();
    final afterClick = DateTime.now().toUtc();

    final completedTask = composition.store.currentTasks.singleWhere(
      (task) => task.id == 'clean-coffee-maker',
    );
    final scheduledStart = completedTask.scheduledStart;
    final scheduledEnd = completedTask.scheduledEnd;
    expect(find.byKey(const ValueKey('task-selection-modal')), findsNothing);
    expect(completedTask.status, TaskStatus.completed);
    expect(completedTask.completedAt, isNotNull);
    expect(completedTask.completedAt!.isBefore(beforeClick), isFalse);
    expect(completedTask.completedAt!.isAfter(afterClick), isFalse);
    expect(find.textContaining('Completed at:'), findsWidgets);

    final completedHover = await tester.createGesture(
      kind: PointerDeviceKind.mouse,
    );
    await completedHover.addPointer(
      location: tester.getCenter(
        find.byKey(const ValueKey('clean-coffee-maker-status')),
      ),
    );
    await tester.pumpAndSettle();
    statusDecoration =
        tester
                .widget<AnimatedContainer>(
                  find.byKey(const ValueKey('clean-coffee-maker-status')),
                )
                .decoration
            as BoxDecoration;
    expect(statusDecoration.boxShadow, isNotEmpty);
    await completedHover.removePointer();
    await tester.pumpAndSettle();

    await tester.tap(find.byKey(const ValueKey('clean-coffee-maker-status')));
    await tester.pumpAndSettle();

    final reopenedTask = composition.store.currentTasks.singleWhere(
      (task) => task.id == 'clean-coffee-maker',
    );
    expect(find.byKey(const ValueKey('task-selection-modal')), findsNothing);
    expect(reopenedTask.status, TaskStatus.planned);
    expect(reopenedTask.scheduledStart, scheduledStart);
    expect(reopenedTask.scheduledEnd, scheduledEnd);
    expect(reopenedTask.actualStart, isNull);
    expect(reopenedTask.actualEnd, isNull);
    expect(reopenedTask.completedAt, isNull);
  });

  testWidgets('task modal status circle toggles and keeps details in sync', (
    tester,
  ) async {
    final composition = _composition();
    await _pumpPlanApp(tester, composition: composition);

    await _scrollIntoView(tester, const ValueKey('water-plants'));
    await tester.tap(find.byKey(const ValueKey('water-plants')));
    await tester.pumpAndSettle();

    expect(find.byKey(const ValueKey('task-selection-modal')), findsOneWidget);
    expect(find.text('Flexible - 3:20 PM - 4:00 PM'), findsOneWidget);
    expect(find.textContaining('Completed -'), findsNothing);
    var modalStatusDecoration =
        tester
                .widget<AnimatedContainer>(
                  find.byKey(const ValueKey('task-modal-status')),
                )
                .decoration
            as BoxDecoration;
    expect(modalStatusDecoration.boxShadow, isEmpty);

    final hover = await tester.createGesture(kind: PointerDeviceKind.mouse);
    await hover.addPointer(
      location: tester.getCenter(
        find.byKey(const ValueKey('task-modal-status')),
      ),
    );
    await tester.pumpAndSettle();
    modalStatusDecoration =
        tester
                .widget<AnimatedContainer>(
                  find.byKey(const ValueKey('task-modal-status')),
                )
                .decoration
            as BoxDecoration;
    expect(modalStatusDecoration.boxShadow, isNotEmpty);
    await hover.removePointer();
    await tester.pumpAndSettle();

    await tester.tap(find.byKey(const ValueKey('task-modal-status')));
    await tester.pumpAndSettle();

    final completedTask = composition.store.currentTasks.singleWhere(
      (task) => task.id == 'water-plants',
    );
    expect(completedTask.status, TaskStatus.completed);
    expect(find.byKey(const ValueKey('task-selection-modal')), findsOneWidget);
    expect(find.text('Water plants'), findsWidgets);
    expect(find.textContaining('Completed -'), findsOneWidget);
    expect(find.text('Planned: 3:20 PM - 4:00 PM'), findsOneWidget);
    expect(find.byIcon(Icons.check), findsWidgets);

    final completedHover = await tester.createGesture(
      kind: PointerDeviceKind.mouse,
    );
    await completedHover.addPointer(
      location: tester.getCenter(
        find.byKey(const ValueKey('task-modal-status')),
      ),
    );
    await tester.pumpAndSettle();
    modalStatusDecoration =
        tester
                .widget<AnimatedContainer>(
                  find.byKey(const ValueKey('task-modal-status')),
                )
                .decoration
            as BoxDecoration;
    expect(modalStatusDecoration.boxShadow, isNotEmpty);
    await completedHover.removePointer();
    await tester.pumpAndSettle();

    await tester.tap(find.byKey(const ValueKey('task-modal-status')));
    await tester.pumpAndSettle();

    final reopenedTask = composition.store.currentTasks.singleWhere(
      (task) => task.id == 'water-plants',
    );
    expect(reopenedTask.status, TaskStatus.planned);
    expect(find.byKey(const ValueKey('task-selection-modal')), findsOneWidget);
    expect(find.text('Flexible - 3:20 PM - 4:00 PM'), findsOneWidget);
    expect(find.textContaining('Completed -'), findsNothing);
  });

  testWidgets('free slot text scales inside a short task bubble', (
    tester,
  ) async {
    await _pumpTimelineCard(
      tester,
      card: _timelineCard(
        id: 'short-free-slot',
        title: 'Free Slot',
        subtitle: 'Intentional rest',
        timeText: '6:15 PM - 6:30 PM',
        visualKind: TaskVisualKind.freeSlot,
        showsQuickActions: false,
      ),
      size: const Size(360, 88),
    );

    expect(tester.takeException(), isNull);
    expect(find.byKey(const ValueKey('short-free-slot-status')), findsNothing);
    expect(find.text('Intentional rest'), findsOneWidget);
    expect(find.text('6:15 PM - 6:30 PM'), findsOneWidget);
  });
}

/// Top-level helper that performs the `_pumpPlanApp` operation for this file.
/// Keeping the helper private makes the surrounding flow easier to read while preventing unrelated libraries from depending on implementation detail.
Future<void> _pumpPlanApp(
  WidgetTester tester, {
  DemoSchedulerComposition? composition,
}) async {
  await tester.binding.setSurfaceSize(const Size(1586, 992));
  addTearDown(() => tester.binding.setSurfaceSize(null));
  await tester.pumpWidget(
    FocusFlowApp(composition: composition ?? _composition()),
  );
  await tester.pumpAndSettle();
}

/// Top-level helper that performs the `_composition` operation for this file.
/// Keeping the helper private makes the surrounding flow easier to read while preventing unrelated libraries from depending on implementation detail.
DemoSchedulerComposition _composition() {
  return DemoSchedulerComposition.seeded(selectedDate: CivilDate(2025, 5, 20));
}

/// Top-level helper that performs the `_readSeededToday` operation for this file.
/// Keeping the helper private makes the surrounding flow easier to read while preventing unrelated libraries from depending on implementation detail.
Future<TodayState> _readSeededToday() async {
  final composition = _composition();
  final result = await composition.todayQuery.execute(
    GetTodayStateRequest(
      context: composition.context('test-read-today'),
      date: composition.date,
    ),
  );
  return result.requireValue;
}

/// Top-level helper that performs the `_kindFor` operation for this file.
/// Keeping the helper private makes the surrounding flow easier to read while preventing unrelated libraries from depending on implementation detail.
TaskVisualKind _kindFor(TodayScreenData data, String id) {
  return data.cards.singleWhere((card) => card.id == id).visualKind;
}

/// Top-level helper that performs the `_scrollIntoView` operation for this file.
/// Keeping the helper private makes the surrounding flow easier to read while preventing unrelated libraries from depending on implementation detail.
Future<void> _scrollIntoView(WidgetTester tester, ValueKey<String> key) async {
  await tester.ensureVisible(find.byKey(key));
  await tester.pumpAndSettle();
}

/// Top-level helper that performs the `_pumpTimelineCard` operation for this file.
/// Keeping the helper private makes the surrounding flow easier to read while preventing unrelated libraries from depending on implementation detail.
Future<void> _pumpTimelineCard(
  WidgetTester tester, {
  required TimelineCardModel card,
  required Size size,
  Future<void> Function(TimelineCardModel card)? onPushToNext,
  Future<void> Function(TimelineCardModel card)? onPushToTomorrow,
  Future<void> Function(TimelineCardModel card)? onPushToBacklog,
}) async {
  await tester.binding.setSurfaceSize(
    Size(size.width + 120, size.height + 220),
  );
  addTearDown(() => tester.binding.setSurfaceSize(null));
  await tester.pumpWidget(
    MaterialApp(
      home: Scaffold(
        body: Center(
          child: SizedBox(
            width: size.width,
            height: size.height,
            child: TaskTimelineCard(
              card: card,
              onTap: () {},
              onStatusPressed: () {},
              onPushToNext: onPushToNext,
              onPushToTomorrow: onPushToTomorrow,
              onPushToBacklog: onPushToBacklog,
            ),
          ),
        ),
      ),
    ),
  );
  await tester.pumpAndSettle();
}

/// Top-level helper that performs the `_pumpConstrainedWidget` operation for this file.
/// Keeping the helper private makes the surrounding flow easier to read while preventing unrelated libraries from depending on implementation detail.
Future<void> _pumpConstrainedWidget(
  WidgetTester tester, {
  required Size size,
  required Widget child,
}) async {
  await tester.binding.setSurfaceSize(Size(size.width + 40, size.height + 40));
  addTearDown(() => tester.binding.setSurfaceSize(null));
  await tester.pumpWidget(
    MaterialApp(
      theme: FocusFlowTheme.dark(),
      home: Scaffold(
        body: Center(
          child: SizedBox(width: size.width, height: size.height, child: child),
        ),
      ),
    ),
  );
  await tester.pumpAndSettle();
}

/// Top-level helper that performs the `_timelineCard` operation for this file.
/// Keeping the helper private makes the surrounding flow easier to read while preventing unrelated libraries from depending on implementation detail.
TimelineCardModel _timelineCard({
  required String id,
  required String title,
  String subtitle = '30 min',
  String timeText = '6:00 PM - 6:30 PM',
  int startMinutes = 18 * 60,
  int endMinutes = 18 * 60 + 30,
  TaskVisualKind visualKind = TaskVisualKind.flexible,
  TaskType? taskType,
  bool showsQuickActions = true,
  bool showPastTaskPushButton = false,
  bool isCompleted = false,
  String? completedTimeText,
  TimelineRewardIconToken rewardIconToken = TimelineRewardIconToken.medium,
  TimelineDifficultyIconToken difficultyIconToken =
      TimelineDifficultyIconToken.medium,
}) {
  final resolvedTaskType =
      taskType ??
      switch (visualKind) {
        TaskVisualKind.flexible => TaskType.flexible,
        TaskVisualKind.required => TaskType.critical,
        TaskVisualKind.appointment => TaskType.inflexible,
        TaskVisualKind.freeSlot => TaskType.freeSlot,
        TaskVisualKind.completedSurprise => TaskType.flexible,
      };
  return TimelineCardModel(
    id: id,
    title: title,
    typeLabel: 'Flexible',
    subtitle: subtitle,
    timeText: timeText,
    startMinutes: startMinutes,
    endMinutes: endMinutes,
    taskType: resolvedTaskType,
    visualKind: visualKind,
    rewardIconToken: rewardIconToken,
    difficultyIconToken: difficultyIconToken,
    completedTimeText: completedTimeText,
    isCompleted: isCompleted,
    isSelectable: true,
    showsQuickActions: showPastTaskPushButton ? false : showsQuickActions,
    showPastTaskPushButton: showPastTaskPushButton,
    durationMinutes: endMinutes - startMinutes,
  );
}
