import 'package:flutter_riverpod/flutter_riverpod.dart'; import 'package:flutter_test/flutter_test.dart'; import 'package:mocktail/mocktail.dart'; import 'package:timetrack/core/database/app_database.dart' as appdb; import 'package:timetrack/core/database/daos/projects_dao.dart'; import 'package:timetrack/features/entries/data/drift_entries_repository.dart'; import 'package:timetrack/features/entries/data/entries_repository.dart'; import 'package:timetrack/features/entries/domain/time_entry.dart'; import 'package:timetrack/features/projects/domain/project.dart'; import 'package:timetrack/features/timer/domain/timer_notifier.dart'; import 'package:timetrack/features/timer/domain/timer_state.dart'; // Mocks class MockTimeEntriesRepository extends Mock implements TimeEntriesRepository {} class MockAppDatabase extends Mock implements appdb.AppDatabase {} class MockProjectsDao extends Mock implements ProjectsDao {} class FakeTimeEntry extends Fake implements TimeEntry {} void main() { setUpAll(() { registerFallbackValue(FakeTimeEntry()); }); late MockTimeEntriesRepository mockRepo; late MockAppDatabase mockDb; late MockProjectsDao mockProjectsDao; final testProject = Project( id: 1, name: 'Test', colorValue: 0xFF2563EB, createdAt: DateTime(2026), ); final activeEntry = TimeEntry( id: 42, projectId: 1, startTime: DateTime(2026, 7, 1, 9), tags: const [], createdAt: DateTime(2026, 7, 1, 9), ); setUp(() { mockRepo = MockTimeEntriesRepository(); mockDb = MockAppDatabase(); mockProjectsDao = MockProjectsDao(); when(() => mockDb.projectsDao).thenReturn(mockProjectsDao); // Default: no project found (safe fallback for restore) when(() => mockProjectsDao.getById(any())) .thenAnswer((_) async => null); }); ProviderContainer buildContainer({TimeEntry? activeEntryOverride}) { when(() => mockRepo.getActiveEntry()) .thenAnswer((_) async => activeEntryOverride); return ProviderContainer( overrides: [ timeEntriesRepositoryProvider.overrideWithValue(mockRepo), appdb.appDatabaseProvider.overrideWithValue(mockDb), ], ); } group('TimerNotifier', () { test('build starts idle when no active entry', () async { final container = buildContainer(activeEntryOverride: null); addTearDown(container.dispose); await Future.delayed(const Duration(milliseconds: 50)); final state = container.read(timerNotifierProvider); expect(state, isA()); }); test('build restores running state from DB active entry', () async { // Override the default null-returning stub for id=1 when(() => mockProjectsDao.getById(1)) .thenAnswer((_) async => appdb.Project( id: 1, name: 'Test', colorValue: 0xFF2563EB, createdAt: DateTime(2026), )); final container = buildContainer(activeEntryOverride: activeEntry); addTearDown(container.dispose); // Trigger provider creation and keep it alive with a listener container.listen(timerNotifierProvider, (prev, next) {}); await Future.delayed(const Duration(milliseconds: 500)); final state = container.read(timerNotifierProvider); expect(state, isA()); expect((state as TimerRunning).entryId, equals(42)); }); test('start transitions to running state', () async { final container = buildContainer(activeEntryOverride: null); addTearDown(container.dispose); await Future.delayed(const Duration(milliseconds: 50)); when(() => mockRepo.create(any())).thenAnswer((_) async => 99); await container .read(timerNotifierProvider.notifier) .start(testProject); final state = container.read(timerNotifierProvider); expect(state, isA()); expect((state as TimerRunning).project.name, equals('Test')); }); test('stop transitions to idle and saves entry', () async { final container = buildContainer(activeEntryOverride: null); addTearDown(container.dispose); await Future.delayed(const Duration(milliseconds: 50)); when(() => mockRepo.create(any())).thenAnswer((_) async => 99); // After start(), getActiveEntry should return the running entry var callCount = 0; when(() => mockRepo.getActiveEntry()).thenAnswer((_) async { callCount++; if (callCount == 1) return null; // initial restore check return activeEntry; }); when(() => mockRepo.update(any())).thenAnswer((_) async {}); await container .read(timerNotifierProvider.notifier) .start(testProject); await container.read(timerNotifierProvider.notifier).stop(); final state = container.read(timerNotifierProvider); expect(state, isA()); }); test('discard transitions to idle and deletes entry', () async { final container = buildContainer(activeEntryOverride: null); addTearDown(container.dispose); await Future.delayed(const Duration(milliseconds: 50)); when(() => mockRepo.create(any())).thenAnswer((_) async => 99); when(() => mockRepo.delete(any())).thenAnswer((_) async {}); await container .read(timerNotifierProvider.notifier) .start(testProject); await container.read(timerNotifierProvider.notifier).discard(); final state = container.read(timerNotifierProvider); expect(state, isA()); verify(() => mockRepo.delete(99)).called(1); }); }); }