import 'package:cloud_firestore/cloud_firestore.dart'; import 'package:fake_cloud_firestore/fake_cloud_firestore.dart'; import 'package:firebase_auth_mocks/firebase_auth_mocks.dart'; import 'package:flutter/material.dart'; import 'package:flutter_riverpod/flutter_riverpod.dart'; import 'package:flutter_test/flutter_test.dart'; import 'package:firebase_auth/firebase_auth.dart'; import 'package:leafittome/app.dart'; import 'package:leafittome/core/firebase/firebase_providers.dart'; import 'package:leafittome/core/settings/settings_provider.dart'; import 'package:leafittome/features/notifications/data/push_registration_service.dart'; import 'package:shared_preferences/shared_preferences.dart'; /// Tests brauchen kein echtes FCM – Push-Registrierung ist ein No-Op. class _FakePushRegistrationService implements PushRegistrationService { @override Future registerDevice(User user) async {} @override Future syncReminderTime(TimeOfDay time) async {} } /// Baut die App mit gemocktem Firebase: angemeldeter Nutzer 'u1' als Besitzer /// des Haushalts 'h1' mit einer Monstera, deren Gießen überfällig ist. /// Mit [secondHousehold] ist u1 zusätzlich Sitter im fremden Haushalt 'h2' /// (Besitzer 'u2'), in dem ein Ficus fällig ist. /// Mit [extraMember] hat 'h1' zusätzlich den Sitter 'u3'. Future buildTestApp({ String role = 'member', bool secondHousehold = false, bool extraMember = false, }) async { SharedPreferences.setMockInitialValues({}); final prefs = await SharedPreferences.getInstance(); final auth = MockFirebaseAuth( signedIn: true, mockUser: MockUser(uid: 'u1', email: 'test@example.com'), ); final firestore = FakeFirebaseFirestore(); await firestore.collection('users').doc('u1').set({ 'email': 'test@example.com', 'householdId': 'h1', }); await firestore.collection('households').doc('h1').set({ 'name': 'Mein Haushalt', 'ownerUid': 'u1', 'memberUids': ['u1', if (extraMember) 'u3'], 'members': { 'u1': {'role': role, 'email': 'test@example.com'}, if (extraMember) 'u3': {'role': 'sitter', 'email': 'sitter@example.com'}, }, }); if (secondHousehold) { await firestore.collection('households').doc('h2').set({ 'name': 'Zweiter Haushalt', 'ownerUid': 'u2', 'memberUids': ['u2', 'u1'], 'members': { 'u1': {'role': 'sitter', 'email': 'test@example.com'}, 'u2': {'role': 'member', 'email': 'other@example.com'}, }, }); await firestore .collection('households') .doc('h2') .collection('plants') .add({ 'nickname': 'Ficus', 'species': 'Ficus benjamina', 'locationId': null, 'description': '', 'careNotes': '', 'wateringIntervalDays': 7, 'fertilizingIntervalDays': 28, 'lastWatered': null, 'lastFertilized': Timestamp.fromDate( DateTime.now().subtract(const Duration(days: 3))), }); } final now = DateTime.now(); await firestore .collection('households') .doc('h1') .collection('plants') .add({ 'nickname': 'Monstera', 'species': 'Monstera deliciosa', 'locationId': null, 'description': '', 'careNotes': '', 'wateringIntervalDays': 7, 'fertilizingIntervalDays': 28, 'lastWatered': Timestamp.fromDate(now.subtract(const Duration(days: 8))), 'lastFertilized': Timestamp.fromDate(now.subtract(const Duration(days: 3))), }); return ProviderScope( overrides: [ sharedPreferencesProvider.overrideWithValue(prefs), firebaseAuthProvider.overrideWithValue(auth), firestoreProvider.overrideWithValue(firestore), pushRegistrationServiceProvider .overrideWithValue(_FakePushRegistrationService()), ], child: const LeafItToMeApp(), ); } void main() { testWidgets('Angemeldeter Nutzer sieht die Heute-Checkliste mit fälliger Aufgabe', (tester) async { await tester.pumpWidget(await buildTestApp()); await tester.pumpAndSettle(); expect(find.text('Heute'), findsOneWidget); expect(find.textContaining('offene Aufgabe'), findsOneWidget); expect(find.textContaining('Monstera gießen'), findsOneWidget); }); testWidgets('Aufgabe bestätigen schreibt nach Firestore und leert die Liste', (tester) async { await tester.pumpWidget(await buildTestApp()); await tester.pumpAndSettle(); await tester.tap(find.text('Erledigt').first); await tester.pumpAndSettle(); expect(find.text('Erledigt'), findsNothing); expect(find.text('Alles versorgt!'), findsOneWidget); }); testWidgets('Sitter kann abhaken, sieht aber keinen Anlegen-Button', (tester) async { await tester.pumpWidget(await buildTestApp(role: 'sitter')); await tester.pumpAndSettle(); // Abhaken ist erlaubt … expect(find.text('Erledigt'), findsWidgets); // … aber Pflanzen anlegen nicht. expect(find.text('Pflanze hinzufügen'), findsNothing); }); testWidgets('Mitglied mehrerer Haushalte kann den aktiven Haushalt wechseln', (tester) async { await tester.pumpWidget(await buildTestApp(secondHousehold: true)); await tester.pumpAndSettle(); // Über das Menü zur Haushalts-Ansicht. await tester.tap(find.byIcon(Icons.menu)); await tester.pumpAndSettle(); await tester.tap(find.text('Haushalt')); await tester.pumpAndSettle(); // Beide Haushalte sind gelistet, h1 ist aktiv, Besitzer sind ausgewiesen. expect(find.text('Meine Haushalte'), findsOneWidget); expect(find.text('Aktiv'), findsOneWidget); expect(find.text('Zweiter Haushalt'), findsOneWidget); expect(find.text('Dein Haushalt'), findsOneWidget); expect(find.text('Besitzer: other@example.com'), findsOneWidget); // Wechsel nach h2: Zeiger in users/u1 wird umgestellt … await tester.tap(find.text('Zweiter Haushalt')); await tester.pumpAndSettle(); // … und die Ansicht zeigt den neuen Haushalt (AppBar + Listeneintrag). expect(find.text('Zweiter Haushalt'), findsNWidgets(2)); expect(find.text('other@example.com'), findsOneWidget); // Im fremden Haushalt (nicht Besitzer): Austreten möglich, Entfernen nicht. expect(find.text('Haushalt verlassen'), findsOneWidget); expect(find.byIcon(Icons.person_remove), findsNothing); }); testWidgets('Checkliste gruppiert Aufgaben aus mehreren Haushalten', (tester) async { await tester.pumpWidget(await buildTestApp(secondHousehold: true)); await tester.pumpAndSettle(); // Aufgaben beider Haushalte mit Zwischenüberschriften. expect(find.textContaining('Monstera gießen'), findsOneWidget); expect(find.textContaining('Ficus gießen'), findsOneWidget); expect(find.text('Mein Haushalt'), findsOneWidget); expect(find.text('Zweiter Haushalt'), findsOneWidget); // Die Ficus-Aufgabe (zweite Gruppe) bestätigen → sie schreibt in den // richtigen Haushalt, die h2-Gruppe verschwindet mitsamt Überschriften. await tester.tap(find.text('Erledigt').last); await tester.pumpAndSettle(); expect(find.textContaining('Ficus gießen'), findsNothing); expect(find.textContaining('Monstera gießen'), findsOneWidget); expect(find.text('Zweiter Haushalt'), findsNothing); }); testWidgets('Besitzer sieht Besitzer-Kennzeichnung und kann entfernen, ' 'aber nicht austreten', (tester) async { await tester.pumpWidget(await buildTestApp(extraMember: true)); await tester.pumpAndSettle(); await tester.tap(find.byIcon(Icons.menu)); await tester.pumpAndSettle(); await tester.tap(find.text('Haushalt')); await tester.pumpAndSettle(); // Besitzer-Kennzeichnung am eigenen Eintrag, Entfernen-Knopf beim Sitter. expect(find.text('Mitglied · Besitzer'), findsOneWidget); expect(find.byIcon(Icons.person_remove), findsOneWidget); expect(find.text('Haushalt verlassen'), findsNothing); // Entfernen fragt nach. await tester.tap(find.byIcon(Icons.person_remove)); await tester.pumpAndSettle(); expect(find.text('Mitglied entfernen?'), findsOneWidget); }); testWidgets( 'Alt-Haushalt ohne members-Eintrag: eigene E-Mail wird nachgetragen', (tester) async { SharedPreferences.setMockInitialValues({}); final prefs = await SharedPreferences.getInstance(); final auth = MockFirebaseAuth( signedIn: true, mockUser: MockUser(uid: 'u1', email: 'test@example.com'), ); final firestore = FakeFirebaseFirestore(); await firestore.collection('users').doc('u1').set({ 'email': 'test@example.com', 'householdId': 'h1', }); // V1-Zeit: kein ownerUid, keine members-Map – nur memberUids. await firestore.collection('households').doc('h1').set({ 'name': 'Mein Haushalt', 'memberUids': ['u1'], }); await tester.pumpWidget(ProviderScope( overrides: [ sharedPreferencesProvider.overrideWithValue(prefs), firebaseAuthProvider.overrideWithValue(auth), firestoreProvider.overrideWithValue(firestore), pushRegistrationServiceProvider .overrideWithValue(_FakePushRegistrationService()), ], child: const LeafItToMeApp(), )); await tester.pumpAndSettle(); final doc = await firestore.collection('households').doc('h1').get(); final members = doc.data()?['members'] as Map?; expect((members?['u1'] as Map?)?['email'], 'test@example.com'); expect((members?['u1'] as Map?)?['role'], 'member'); }); testWidgets( 'Erster Login ohne Profil (z. B. Apple): Haushalt wird reaktiv angelegt', (tester) async { SharedPreferences.setMockInitialValues({}); final prefs = await SharedPreferences.getInstance(); // Angemeldet (wie nach signInWithProvider), aber Firestore ist leer – // genau der Zustand, in dem Pflanzen-Speichern an den Rules scheiterte. final auth = MockFirebaseAuth( signedIn: true, mockUser: MockUser(uid: 'apple1', email: 'apple@example.com'), ); final firestore = FakeFirebaseFirestore(); await tester.pumpWidget(ProviderScope( overrides: [ sharedPreferencesProvider.overrideWithValue(prefs), firebaseAuthProvider.overrideWithValue(auth), firestoreProvider.overrideWithValue(firestore), pushRegistrationServiceProvider .overrideWithValue(_FakePushRegistrationService()), ], child: const LeafItToMeApp(), )); await tester.pumpAndSettle(); // Das BootstrapGate hat Profil + Haushalt angelegt … final userDoc = await firestore.collection('users').doc('apple1').get(); expect(userDoc.exists, isTrue); final householdId = userDoc.data()?['householdId'] as String?; expect(householdId, isNotNull); final household = await firestore.collection('households').doc(householdId).get(); expect(household.data()?['ownerUid'], 'apple1'); expect(household.data()?['memberUids'], ['apple1']); expect( ((household.data()?['members'] as Map)['apple1'] as Map)['role'], 'member'); // … und die App ist normal nutzbar (Heute-Screen, keine Aufgaben). expect(find.text('Heute'), findsOneWidget); expect(find.text('Alles versorgt!'), findsOneWidget); }); testWidgets( 'users-Doc existiert ohne householdId (Push-Registrierung war schneller): ' 'Haushalt wird nachgezogen, Push-Felder bleiben erhalten', (tester) async { SharedPreferences.setMockInitialValues({}); final prefs = await SharedPreferences.getInstance(); final auth = MockFirebaseAuth( signedIn: true, mockUser: MockUser(uid: 'apple1', email: 'apple@example.com'), ); final firestore = FakeFirebaseFirestore(); // Exakt der Zustand des kaputten Apple-Accounts: Die Push-Registrierung // hat das users-Dokument angelegt, der Bootstrap kam nie durch. await firestore.collection('users').doc('apple1').set({ 'fcmTokens': ['token-1'], 'timezone': 'Europe/Berlin', }); await tester.pumpWidget(ProviderScope( overrides: [ sharedPreferencesProvider.overrideWithValue(prefs), firebaseAuthProvider.overrideWithValue(auth), firestoreProvider.overrideWithValue(firestore), pushRegistrationServiceProvider .overrideWithValue(_FakePushRegistrationService()), ], child: const LeafItToMeApp(), )); await tester.pumpAndSettle(); final userDoc = await firestore.collection('users').doc('apple1').get(); final householdId = userDoc.data()?['householdId'] as String?; expect(householdId, isNotNull); // Die Push-Felder wurden nicht überschrieben (merge statt set). expect(userDoc.data()?['fcmTokens'], ['token-1']); expect(userDoc.data()?['timezone'], 'Europe/Berlin'); final household = await firestore.collection('households').doc(householdId).get(); expect(household.data()?['memberUids'], ['apple1']); expect(find.text('Heute'), findsOneWidget); }); testWidgets( 'Untersuchungshistorie: gespeicherte Diagnosen sind im Detail sichtbar', (tester) async { SharedPreferences.setMockInitialValues({}); final prefs = await SharedPreferences.getInstance(); final auth = MockFirebaseAuth( signedIn: true, mockUser: MockUser(uid: 'u1', email: 'test@example.com'), ); final firestore = FakeFirebaseFirestore(); await firestore.collection('users').doc('u1').set({ 'email': 'test@example.com', 'householdId': 'h1', }); await firestore.collection('households').doc('h1').set({ 'name': 'Mein Haushalt', 'ownerUid': 'u1', 'memberUids': ['u1'], 'members': { 'u1': {'role': 'member', 'email': 'test@example.com'}, }, }); final plantRef = firestore.collection('households').doc('h1').collection('plants'); await plantRef.doc('p1').set({ 'nickname': 'Monstera', 'species': 'Monstera deliciosa', 'locationId': null, 'description': '', 'careNotes': '', 'wateringIntervalDays': 7, 'fertilizingIntervalDays': 28, 'lastWatered': Timestamp.fromDate(DateTime.now()), 'lastFertilized': Timestamp.fromDate(DateTime.now()), }); await plantRef.doc('p1').collection('diagnoses').add({ 'healthy': false, 'matchesSpecies': true, 'summary': 'Die Blätter zeigen braune Flecken.', 'details': 'Vermutlich zu viel direkte Sonne.', 'treatment': 'Stelle die Pflanze etwas schattiger.', 'prevention': 'Kein direktes Mittagslicht.', 'createdAt': Timestamp.fromDate(DateTime(2026, 7, 20, 14, 30)), }); await tester.pumpWidget(ProviderScope( overrides: [ sharedPreferencesProvider.overrideWithValue(prefs), firebaseAuthProvider.overrideWithValue(auth), firestoreProvider.overrideWithValue(firestore), pushRegistrationServiceProvider .overrideWithValue(_FakePushRegistrationService()), ], child: const LeafItToMeApp(), )); await tester.pumpAndSettle(); // Zur Pflanze navigieren: Menü → Meine Pflanzen → Monstera. await tester.tap(find.byIcon(Icons.menu)); await tester.pumpAndSettle(); await tester.tap(find.text('Meine Pflanzen')); await tester.pumpAndSettle(); await tester.tap(find.text('Monstera')); await tester.pumpAndSettle(); // Historie-Button ist aktiv (Untersuchungen vorhanden) → Seite öffnen. await tester.scrollUntilVisible(find.text('Frühere Untersuchungen'), 200); await tester.tap(find.text('Frühere Untersuchungen')); await tester.pumpAndSettle(); // Untersuchungs-Seite listet den Eintrag. expect(find.textContaining('Untersuchungen'), findsWidgets); expect(find.text('Die Blätter zeigen braune Flecken.'), findsOneWidget); expect(find.textContaining('20. Juli 2026'), findsOneWidget); // Eintrag öffnen → volles Ergebnis-Sheet mit Behandlungstipps. await tester.tap(find.text('Die Blätter zeigen braune Flecken.')); await tester.pumpAndSettle(); expect(find.text('Was du tun kannst'), findsOneWidget); expect(find.text('Stelle die Pflanze etwas schattiger.'), findsOneWidget); }); testWidgets( 'Ohne gespeicherte Untersuchungen ist der Historie-Button deaktiviert', (tester) async { await tester.pumpWidget(await buildTestApp()); await tester.pumpAndSettle(); await tester.tap(find.byIcon(Icons.menu)); await tester.pumpAndSettle(); await tester.tap(find.text('Meine Pflanzen')); await tester.pumpAndSettle(); await tester.tap(find.text('Monstera')); await tester.pumpAndSettle(); await tester.scrollUntilVisible(find.text('Frühere Untersuchungen'), 200); final button = tester.widget(find.ancestor( of: find.text('Frühere Untersuchungen'), matching: find.byType(OutlinedButton), )); expect(button.onPressed, isNull); }); testWidgets( 'Umtopf-Erinnerung: fällige Aufgabe erscheint in der Checkliste und ' 'Bestätigen schreibt lastRepotted', (tester) async { SharedPreferences.setMockInitialValues({}); final prefs = await SharedPreferences.getInstance(); final auth = MockFirebaseAuth( signedIn: true, mockUser: MockUser(uid: 'u1', email: 'test@example.com'), ); final firestore = FakeFirebaseFirestore(); await firestore.collection('users').doc('u1').set({ 'email': 'test@example.com', 'householdId': 'h1', }); await firestore.collection('households').doc('h1').set({ 'name': 'Mein Haushalt', 'ownerUid': 'u1', 'memberUids': ['u1'], 'members': { 'u1': {'role': 'member', 'email': 'test@example.com'}, }, }); final now = DateTime.now(); // Gießen/Düngen frisch erledigt — nur das Umtopfen (vor 13 Monaten, // Intervall 12 Monate) ist überfällig. await firestore .collection('households') .doc('h1') .collection('plants') .doc('p1') .set({ 'nickname': 'Monstera', 'species': 'Monstera deliciosa', 'locationId': null, 'description': '', 'careNotes': '', 'wateringIntervalDays': 7, 'fertilizingIntervalDays': 28, 'lastWatered': Timestamp.fromDate(now), 'lastFertilized': Timestamp.fromDate(now), 'repottingIntervalMonths': 12, 'lastRepotted': Timestamp.fromDate( DateTime(now.year - 1, now.month - 1, now.day)), }); await tester.pumpWidget(ProviderScope( overrides: [ sharedPreferencesProvider.overrideWithValue(prefs), firebaseAuthProvider.overrideWithValue(auth), firestoreProvider.overrideWithValue(firestore), pushRegistrationServiceProvider .overrideWithValue(_FakePushRegistrationService()), ], child: const LeafItToMeApp(), )); await tester.pumpAndSettle(); expect(find.textContaining('Monstera umtopfen'), findsOneWidget); await tester.tap(find.text('Erledigt')); await tester.pumpAndSettle(); expect(find.text('Alles versorgt!'), findsOneWidget); final doc = await firestore .collection('households') .doc('h1') .collection('plants') .doc('p1') .get(); final lastRepotted = (doc.data()?['lastRepotted'] as Timestamp).toDate(); expect(now.difference(lastRepotted).inDays, 0); expect(doc.data()?['lastRepottedBy'], 'test'); }); testWidgets('Mitglied kann den Haushalt umbenennen', (tester) async { await tester.pumpWidget(await buildTestApp()); await tester.pumpAndSettle(); await tester.tap(find.byIcon(Icons.menu)); await tester.pumpAndSettle(); await tester.tap(find.text('Haushalt')); await tester.pumpAndSettle(); await tester.tap(find.byIcon(Icons.edit)); await tester.pumpAndSettle(); await tester.enterText(find.byType(TextField).last, 'Wohnung Chris'); await tester.tap(find.text('Speichern')); await tester.pumpAndSettle(); // Der neue Name kommt live aus Firestore zurück (AppBar). expect(find.text('Wohnung Chris'), findsOneWidget); }); }