import 'dart:io'; import 'package:flutter_test/flutter_test.dart'; import 'package:flutter_riverpod/flutter_riverpod.dart'; import 'package:christian_period_tracker/models/scripture.dart'; import 'package:christian_period_tracker/models/cycle_entry.dart'; import 'package:christian_period_tracker/models/user_profile.dart'; import 'package:christian_period_tracker/providers/scripture_provider.dart'; import 'package:christian_period_tracker/providers/user_provider.dart'; import 'package:hive_flutter/hive_flutter.dart'; // Fake ScriptureDatabase implementation for testing class FakeScriptureDatabase implements ScriptureDatabase { final int Function(String phase) getScriptureCountForPhaseFn; final Scripture? Function(String phase, int index) getScriptureForPhaseByIndexFn; final Scripture? Function(String phase)? getRandomScriptureForPhaseFn; FakeScriptureDatabase({ required this.getScriptureCountForPhaseFn, required this.getScriptureForPhaseByIndexFn, this.getRandomScriptureForPhaseFn, }); @override int getScriptureCountForPhase(String phase) => getScriptureCountForPhaseFn(phase); @override Scripture? getScriptureForPhaseByIndex(String phase, int index) => getScriptureForPhaseByIndexFn(phase, index); @override Scripture? getRandomScriptureForPhase(String phase) => getRandomScriptureForPhaseFn != null ? getRandomScriptureForPhaseFn!(phase) : null; // Unimplemented methods (not used by ScriptureNotifier) @override List getAllScriptures() => throw UnimplementedError(); @override Scripture getHusbandScripture() => throw UnimplementedError(); @override Future loadScriptures() => Future.value(); // Can be mocked to do nothing @override Scripture? getRecommendedScripture(CycleEntry entry) => throw UnimplementedError(); @override Scripture getScriptureForPhase(String phase) => throw UnimplementedError(); } void main() { group('ScriptureNotifier', () { late ProviderContainer container; late String testPath; setUpAll(() async { testPath = '${Directory.current.path}/test_hive_temp_scripture_provider'; final Directory tempDir = Directory(testPath); if (!await tempDir.exists()) { await tempDir.create(recursive: true); } Hive.init(testPath); Hive.registerAdapter(UserProfileAdapter()); Hive.registerAdapter(RelationshipStatusAdapter()); Hive.registerAdapter(FertilityGoalAdapter()); Hive.registerAdapter(BibleTranslationAdapter()); Hive.registerAdapter(UserRoleAdapter()); await Hive.openBox('user_profile'); }); tearDownAll(() async { await Hive.close(); await Directory(testPath).delete(recursive: true); }); final testScripture1 = Scripture( verses: {BibleTranslation.esv: "Verse 1"}, reference: "Ref 1", applicablePhases: ['menstrual'], ); final testScripture2 = Scripture( verses: {BibleTranslation.esv: "Verse 2"}, reference: "Ref 2", applicablePhases: ['menstrual'], ); final testScripture3 = Scripture( verses: {BibleTranslation.esv: "Verse 3"}, reference: "Ref 3", applicablePhases: ['menstrual'], ); tearDown(() { container.dispose(); }); test('initializes with correct scripture for phase', () async { final fakeDb = FakeScriptureDatabase( getScriptureCountForPhaseFn: (phase) => 3, getScriptureForPhaseByIndexFn: (phase, index) => testScripture1, ); container = ProviderContainer( overrides: [ scriptureDatabaseProvider.overrideWithValue(fakeDb), userProfileProvider.overrideWith( (ref) => UserProfileNotifier(), ), ], ); final notifier = container.read(scriptureProvider.notifier); notifier.initializeScripture(CyclePhase.menstrual); final state = container.read(scriptureProvider); expect(state.currentScripture, testScripture1); expect(state.currentPhase, CyclePhase.menstrual); expect(state.maxIndex, 3); // currentIndex will depend on dayOfYear % 3, which is hard to predict // So we'll just check it's within bounds. expect(state.currentIndex, isNonNegative); expect(state.currentIndex, lessThan(3)); }); test('getNextScripture cycles correctly', () async { final scriptures = [testScripture1, testScripture2, testScripture3]; final fakeDb = FakeScriptureDatabase( getScriptureCountForPhaseFn: (phase) => scriptures.length, getScriptureForPhaseByIndexFn: (phase, index) => scriptures[index], ); container = ProviderContainer( overrides: [ scriptureDatabaseProvider.overrideWithValue(fakeDb), userProfileProvider.overrideWith( (ref) => UserProfileNotifier(), ), ], ); final notifier = container.read(scriptureProvider.notifier); // Force initial state to 0 for predictable cycling notifier.initializeScripture(CyclePhase.menstrual); // Override currentIndex to 0 for predictable test container.read(scriptureProvider.notifier).state = container.read(scriptureProvider).copyWith(currentIndex: 0); // First next notifier.getNextScripture(); expect( container.read(scriptureProvider).currentScripture, testScripture2); expect(container.read(scriptureProvider).currentIndex, 1); // Second next notifier.getNextScripture(); expect( container.read(scriptureProvider).currentScripture, testScripture3); expect(container.read(scriptureProvider).currentIndex, 2); // Wrap around notifier.getNextScripture(); expect( container.read(scriptureProvider).currentScripture, testScripture1); expect(container.read(scriptureProvider).currentIndex, 0); }); test('getPreviousScripture cycles correctly', () async { final scriptures = [testScripture1, testScripture2, testScripture3]; final fakeDb = FakeScriptureDatabase( getScriptureCountForPhaseFn: (phase) => scriptures.length, getScriptureForPhaseByIndexFn: (phase, index) => scriptures[index], ); container = ProviderContainer( overrides: [ scriptureDatabaseProvider.overrideWithValue(fakeDb), userProfileProvider.overrideWith( (ref) => UserProfileNotifier(), ), ], ); final notifier = container.read(scriptureProvider.notifier); // Force initial state to 0 for predictable cycling notifier.initializeScripture(CyclePhase.menstrual); // Override currentIndex to 0 for predictable test container.read(scriptureProvider.notifier).state = container.read(scriptureProvider).copyWith(currentIndex: 0); // First previous (wraps around) notifier.getPreviousScripture(); expect( container.read(scriptureProvider).currentScripture, testScripture3); expect(container.read(scriptureProvider).currentIndex, 2); // Second previous notifier.getPreviousScripture(); expect( container.read(scriptureProvider).currentScripture, testScripture2); expect(container.read(scriptureProvider).currentIndex, 1); // Third previous notifier.getPreviousScripture(); expect( container.read(scriptureProvider).currentScripture, testScripture1); expect(container.read(scriptureProvider).currentIndex, 0); }); test('getRandomScripture updates to a valid scripture', () async { final scriptures = [testScripture1, testScripture2, testScripture3]; final fakeDb = FakeScriptureDatabase( getScriptureCountForPhaseFn: (phase) => scriptures.length, getScriptureForPhaseByIndexFn: (phase, index) => scriptures[ index % scriptures.length], // Ensure it always returns a valid one ); container = ProviderContainer( overrides: [ scriptureDatabaseProvider.overrideWithValue(fakeDb), userProfileProvider.overrideWith( (ref) => UserProfileNotifier(), ), ], ); final notifier = container.read(scriptureProvider.notifier); notifier.initializeScripture(CyclePhase.menstrual); // Perform a random selection notifier.getRandomScripture(); final state = container.read(scriptureProvider); expect(state.currentScripture, isNotNull); expect(state.currentScripture, isIn(scriptures)); // Ensure it's one of the valid scriptures expect(state.currentIndex, isNonNegative); expect(state.currentIndex, lessThan(scriptures.length)); }); test('does not change state if maxIndex is 0', () async { final fakeDb = FakeScriptureDatabase( getScriptureCountForPhaseFn: (phase) => 0, getScriptureForPhaseByIndexFn: (phase, index) => null, ); container = ProviderContainer( overrides: [ scriptureDatabaseProvider.overrideWithValue(fakeDb), userProfileProvider.overrideWith( (ref) => UserProfileNotifier(), ), ], ); final notifier = container.read(scriptureProvider.notifier); notifier.initializeScripture(CyclePhase.menstrual); final initialState = container.read(scriptureProvider); expect(initialState.currentScripture, isNull); expect(initialState.maxIndex, 0); notifier.getNextScripture(); notifier.getPreviousScripture(); notifier.getRandomScripture(); // State should remain unchanged expect(container.read(scriptureProvider), initialState); }); }); }