import 'package:flutter/material.dart'; import 'package:flutter/services.dart'; import 'package:flutter_test/flutter_test.dart'; import 'package:hidalyx_drluiscastro/core/widgets/medical_modal.dart'; import 'package:hidalyx_drluiscastro/features/patient/data/models/patient_model.dart'; import 'package:hidalyx_drluiscastro/features/patient/data/services/patient_service.dart'; import 'package:hidalyx_drluiscastro/features/patient/presentation/store/patient_store.dart'; import 'package:hidalyx_drluiscastro/features/patient/presentation/views/patient_modal_view.dart'; import 'package:hidalyx_drluiscastro/features/patient/presentation/widgets/patient_clinical_tabs.dart'; import 'package:hidalyx_drluiscastro/features/patient/presentation/widgets/patient_demographics_form.dart'; import 'package:flutter_form_builder/flutter_form_builder.dart'; import 'package:toggle_switch/toggle_switch.dart'; class MockPatientService extends PatientService { PatientModel? storedPatient; @override Future getPatientById(int id) async { return const PatientModel( id: 99, nombre: 'MARIA', apellido: 'SANTOS', cedula: '402-0000000-1', telefono: '809-555-9999', direccion: 'Santiago', ocupacion: 'Docente', edad: '35', sexo: 'Femenino', seguro: 'Sí', estCivil: 'Casado', ); } @override Future createPatient(PatientModel model) async { storedPatient = model; return PatientModel.fromJson({ ...model.toJson(), 'id': 101, }); } @override Future updatePatient(int id, Map updates) async { return PatientModel.fromJson({ ...updates, 'id': id, }); } } void main() { group('PatientModel serialization', () { test('serializes and deserializes properly from Directus schema', () { final json = { 'id': 50, 'nombre': 'CARLOS', 'apellido': 'ARIAS', 'telefono': '809-555-1234', 'direccion': 'Calle 1 #2', 'cedula': '001-0000000-0', 'ocupacion': 'Ingeniero', 'edad': '40', 'sexo': 'Masculino', 'seguro': 'Sí', 'est_civil': 'Casado', 'mot_cons': 'Dolor de cabeza', }; final model = PatientModel.fromJson(json); expect(model.id, equals(50)); expect(model.fullName, equals('CARLOS ARIAS')); expect(model.sexo, equals('Masculino')); expect(model.seguro, equals('Sí')); final outJson = model.toJson(includeId: true); expect(outJson['id'], equals(50)); expect(outJson['nombre'], equals('CARLOS')); expect(outJson['mot_cons'], equals('Dolor de cabeza')); }); }); group('PatientStore business logic', () { test('validation requires nombre, apellido, sexo and seguro', () { final store = PatientStore(patientService: MockPatientService()); store.initForCreate(); expect(store.isValid, isFalse); store.nombre = 'Ana'; expect(store.isValid, isFalse); store.apellido = 'Gomez'; expect(store.isValid, isTrue); // sexo and seguro default to 'Masculino'/'Sí' }); test('calculates remaining days for proxima cita correctly', () { final store = PatientStore(patientService: MockPatientService()); store.initForCreate(); expect(store.diasParaProximaCita, isNull); final now = DateTime.now(); final futureDate = now.add(const Duration(days: 10)); final day = futureDate.day.toString().padLeft(2, '0'); final month = futureDate.month.toString().padLeft(2, '0'); final year = futureDate.year.toString(); store.fecProxcita = '$day/$month/$year'; expect(store.diasParaProximaCita, equals(10)); }); test('creates new patient successfully', () async { final mock = MockPatientService(); final store = PatientStore(patientService: mock); store.initForCreate(); store.nombre = 'Pedro'; store.apellido = 'Martinez'; final success = await store.savePatient(); expect(success, isTrue); expect(store.id, equals(101)); expect(store.successMessage, contains('guardado correctamente')); }); test('updates existing patient successfully', () async { final mock = MockPatientService(); final store = PatientStore(patientService: mock); await store.loadPatient(99); expect(store.isEditing, isTrue); expect(store.nombre, equals('MARIA')); expect(store.apellido, equals('SANTOS')); store.ocupacion = 'Directora'; final success = await store.savePatient(); expect(success, isTrue); expect(store.successMessage, contains('actualizado correctamente')); }); }); group('PatientModalView strict closure tests', () { testWidgets('modal adheres to strict ESC and (X) close rules', (tester) async { tester.view.physicalSize = const Size(1280, 800); tester.view.devicePixelRatio = 1.0; addTearDown(tester.view.resetPhysicalSize); await tester.pumpWidget( MaterialApp( home: Scaffold( body: Builder( builder: (context) => ElevatedButton( onPressed: () { showMedicalModal( context: context, builder: (_) => const PatientModalView(), ); }, child: const Text('Registrar'), ), ), ), ), ); await tester.tap(find.text('Registrar')); await tester.pumpAndSettle(); expect(find.text('Registro De Paciente'), findsOneWidget); // Outside tap does NOT close await tester.tapAt(const Offset(5, 5)); await tester.pumpAndSettle(); expect(find.text('Registro De Paciente'), findsOneWidget); // ESC key closes the modal await tester.sendKeyEvent(LogicalKeyboardKey.escape); await tester.pumpAndSettle(); expect(find.text('Registro De Paciente'), findsNothing); }); testWidgets('date button inserts current date at cursor in Consultas', (tester) async { tester.view.physicalSize = const Size(1280, 800); tester.view.devicePixelRatio = 1.0; addTearDown(tester.view.resetPhysicalSize); await tester.pumpWidget( MaterialApp( home: Scaffold( body: Builder( builder: (context) => ElevatedButton( onPressed: () { showMedicalModal( context: context, builder: (_) => const PatientModalView(), ); }, child: const Text('Registrar'), ), ), ), ), ); await tester.tap(find.text('Registrar')); await tester.pumpAndSettle(); // Find the Fecha actual button final insertBtn = find.widgetWithText(ElevatedButton, 'Fecha actual'); expect(insertBtn, findsOneWidget); await tester.tap(insertBtn); await tester.pumpAndSettle(); // Verify that current year is present in the field final currentYear = DateTime.now().year.toString(); expect(find.textContaining(currentYear), findsWidgets); }); testWidgets('renders and switches ToggleSwitch for Sexo, Seguro, and Est. Civil', (tester) async { await tester.binding.setSurfaceSize(const Size(1200, 900)); await tester.pumpWidget( MaterialApp( home: Scaffold( body: Builder( builder: (ctx) => ElevatedButton( onPressed: () { showMedicalModal( context: ctx, builder: (_) => const PatientModalView(), ); }, child: const Text('Registrar'), ), ), ), ), ); await tester.tap(find.text('Registrar')); await tester.pumpAndSettle(); // Verify ToggleSwitch instances exist for Sexo, Seguro, and Est. Civil expect(find.byType(ToggleSwitch), findsNWidgets(3)); // Tap Femenino await tester.tap(find.text('Femenino')); await tester.pumpAndSettle(); // Tap Sin Seguro await tester.tap(find.text('Sin Seguro')); await tester.pumpAndSettle(); // Tap Casado await tester.tap(find.text('Casado')); await tester.pumpAndSettle(); // Verify the 8 demographic text fields have unifiedRowHeight matching toggle containers and save button final textFields = find.byType(FormBuilderTextField); // Verify all 8 fields, 3 toggle selector boxes, and the Save button have unifiedRowHeight for (int i = 0; i < 8; i++) { final decFinder = find.descendant(of: textFields.at(i), matching: find.byType(InputDecorator)); final customPaintFinder = find.descendant(of: decFinder, matching: find.byType(CustomPaint)); final rb = customPaintFinder.evaluate().first.renderObject as RenderBox; final translation = rb.localToGlobal(Offset.zero); final rect = translation & rb.size; expect(rect.height, equals(PatientDemographicsForm.unifiedRowHeight)); } // Verify the 3 toggle selector visible boxes have unifiedRowHeight final selectorBoxes = find.byWidgetPredicate((w) => w is Container && w.decoration is BoxDecoration && (w.decoration as BoxDecoration).color == const Color(0xFFF8FAFC)); for (int i = 0; i < 3; i++) { final boxRect = tester.getRect(selectorBoxes.at(i)); expect(boxRect.height, equals(PatientDemographicsForm.unifiedRowHeight)); } // Verify the Save button has unifiedRowHeight final saveBtn = find.widgetWithText(ElevatedButton, 'Guardar Paciente'); expect(tester.getRect(saveBtn).height, equals(PatientDemographicsForm.unifiedRowHeight)); // Row-by-row pixel-exact alignment check for all 4 rows for (int row = 0; row < 4; row++) { // Col 1 field final f1Dec = find.descendant(of: textFields.at(row), matching: find.byType(InputDecorator)); final f1Cp = find.descendant(of: f1Dec, matching: find.byType(CustomPaint)); final f1Rb = f1Cp.evaluate().first.renderObject as RenderBox; final f1Rect = f1Rb.localToGlobal(Offset.zero) & f1Rb.size; // Col 2 field final f2Dec = find.descendant(of: textFields.at(row + 4), matching: find.byType(InputDecorator)); final f2Cp = find.descendant(of: f2Dec, matching: find.byType(CustomPaint)); final f2Rb = f2Cp.evaluate().first.renderObject as RenderBox; final f2Rect = f2Rb.localToGlobal(Offset.zero) & f2Rb.size; // Col 3 item (selector box for rows 0..2, saveBtn for row 3) final Rect f3Rect = row < 3 ? tester.getRect(selectorBoxes.at(row)) : tester.getRect(saveBtn); // Verify tops match expect(f1Rect.top, equals(f2Rect.top), reason: 'Row $row tops Col1 vs Col2'); expect(f1Rect.top, equals(f3Rect.top), reason: 'Row $row tops Col1 vs Col3'); // Verify bottoms match expect(f1Rect.bottom, equals(f2Rect.bottom), reason: 'Row $row bottoms Col1 vs Col2'); expect(f1Rect.bottom, equals(f3Rect.bottom), reason: 'Row $row bottoms Col1 vs Col3'); // Verify heights match exactly expect(f1Rect.height, equals(f3Rect.height), reason: 'Row $row heights Col1 vs Col3'); } }); testWidgets('OutlineInputBorder and Toggle Container alignment check', (tester) async { await tester.pumpWidget( MaterialApp( home: Scaffold( body: Center( child: SizedBox( width: 800, child: Row( crossAxisAlignment: CrossAxisAlignment.start, children: [ Expanded( child: SizedBox( height: 51, child: FormBuilder( child: FormBuilderTextField( name: 'test', initialValue: 'Texto', decoration: InputDecoration( labelText: 'Nombre *', isDense: true, contentPadding: const EdgeInsets.symmetric(horizontal: 11, vertical: 12), border: OutlineInputBorder(borderRadius: BorderRadius.circular(8)), ), ), ), ), ), const SizedBox(width: 12), Expanded( child: Container( margin: const EdgeInsets.only(top: 7), height: 44, decoration: BoxDecoration( color: const Color(0xFFF8FAFC), borderRadius: BorderRadius.circular(8), border: Border.all(color: const Color(0xFFE2E8F0)), ), child: const Text('Toggle Container'), ), ), ], ), ), ), ), ), ); await tester.pumpAndSettle(); expect(find.byType(FormBuilderTextField), findsOneWidget); expect(find.text('Toggle Container'), findsOneWidget); }); }); group('PatientStore consultation workflow', () { test('savePatient transitions estadoEspera from en_consulta to atendido', () async { final mockService = MockPatientService(); final store = PatientStore(patientService: mockService); store.id = 10; store.nombre = 'JUAN'; store.apellido = 'PEREZ'; store.sexo = 'Masculino'; store.seguro = 'Sí'; store.estadoEspera = 'en_consulta'; final success = await store.savePatient(); expect(success, isTrue); expect(store.estadoEspera, 'atendido'); }); test('savePatient transitions estadoEspera from en_espera to atendido', () async { final mockService = MockPatientService(); final store = PatientStore(patientService: mockService); store.id = 11; store.nombre = 'PEDRO'; store.apellido = 'GOMEZ'; store.sexo = 'Masculino'; store.seguro = 'Sí'; store.estadoEspera = 'en_espera'; final success = await store.savePatient(); expect(success, isTrue); expect(store.estadoEspera, 'atendido'); }); testWidgets('Antec. patologicos fields are editable and allow custom text or clear', (tester) async { await tester.binding.setSurfaceSize(const Size(1200, 900)); final store = PatientStore(patientService: MockPatientService()); await tester.pumpWidget( MaterialApp( home: Scaffold( body: PatientClinicalTabs(store: store), ), ), ); await tester.pumpAndSettle(); // Tap on the 'Antec. patológicos' tab await tester.tap(find.text('Antec. patológicos')); await tester.pumpAndSettle(); // Verify that section and labels exist expect(find.text('Antecedentes Personales Patológicos'), findsOneWidget); expect(find.text('Alergias'), findsOneWidget); expect(find.text('Infancia'), findsOneWidget); // Find the text field for 'Alergias' and enter custom text final alergiasField = find.ancestor( of: find.text('Alergias'), matching: find.byType(TextField), ); expect(alergiasField, findsOneWidget); await tester.enterText(alergiasField, 'Ibuprofeno y mariscos'); await tester.pumpAndSettle(); expect(store.datAlerg, 'Ibuprofeno y mariscos'); // Test clear button specifically for Alergias final alergiasClear = find.descendant( of: alergiasField, matching: find.byTooltip('Limpiar'), ); expect(alergiasClear, findsOneWidget); await tester.tap(alergiasClear); await tester.pumpAndSettle(); expect(store.datAlerg, ''); // Test opening dropdown menu and selecting an item final dropdownArrow = find.descendant( of: alergiasField, matching: find.byIcon(Icons.arrow_drop_down_rounded), ); expect(dropdownArrow, findsOneWidget); await tester.tap(dropdownArrow); await tester.pumpAndSettle(); // Tap 'Penicilina' from the menu await tester.tap(find.text('Penicilina').last); await tester.pumpAndSettle(); expect(store.datAlerg, 'Penicilina'); // Test appending text to the selected option await tester.enterText(alergiasField, 'Penicilina (shock anafiláctico)'); await tester.pumpAndSettle(); expect(store.datAlerg, 'Penicilina (shock anafiláctico)'); }); }); }