import 'package:flutter_riverpod/flutter_riverpod.dart'; import 'package:flutter_test/flutter_test.dart'; import 'package:glp1_companion/core/platform/health_gateway.dart'; import 'package:glp1_companion/features/health_sync/presentation/controllers/biometric_controller.dart'; import 'package:glp1_companion/features/settings/presentation/controllers/integrations_controller.dart'; /// Phase 3B: connection-status orchestration for the Settings integrations /// section (connect / disconnect round-trips, dynamic status indicators, /// graceful degradation on unsupported platforms). /// Scriptable [IHealthGateway] standing in for HealthKit / Health Connect. /// /// Extends the interface so the Phase 3B defaults stay available; the /// scripted members mirror what the mobile driver does on a real device. class _ScriptedHealthGateway extends IHealthGateway { _ScriptedHealthGateway({this.nativeSupported = true}); bool nativeSupported; bool grantOnRequest = true; HealthConnectionStatus nativeStatus = HealthConnectionStatus.notConfigured; final requestedProviders = []; final disconnectedProviders = []; @override bool get supportsNativeHealth => nativeSupported; @override NativeHealthPlatform? get nativePlatform => nativeSupported ? NativeHealthPlatform.healthConnect : null; @override Future checkStatus( HealthProviderType provider, ) async { if (provider == HealthProviderType.manualFallback) { return HealthConnectionStatus.connected; } if (provider == HealthProviderType.enterpriseFhir) { return HealthConnectionStatus.notConfigured; } if (!nativeSupported) return HealthConnectionStatus.unsupportedPlatform; if (provider == HealthProviderType.healthConnect) return nativeStatus; return HealthConnectionStatus.unsupportedPlatform; } @override Future requestAuthorization(HealthProviderType provider) async { requestedProviders.add(provider); if (provider == activeProvider && grantOnRequest) { nativeStatus = HealthConnectionStatus.connected; } return grantOnRequest; } @override Future disconnect(HealthProviderType provider) async { disconnectedProviders.add(provider); if (provider == activeProvider) { nativeStatus = HealthConnectionStatus.notConfigured; } } @override Future requestPermissions() async => grantOnRequest; @override Future> fetchWeightHistory({ required DateTime start, required DateTime end, }) async => const []; @override Future> fetchActiveEnergyBurned({ required DateTime start, required DateTime end, }) async => const []; @override Future writeWeight({ required double weightLbs, required DateTime timestamp, }) async => false; } void main() { late _ScriptedHealthGateway gateway; late ProviderContainer container; setUp(() { gateway = _ScriptedHealthGateway(); container = ProviderContainer( overrides: [healthGatewayProvider.overrideWithValue(gateway)], ); addTearDown(container.dispose); }); IntegrationsState state() => container.read(integrationsControllerProvider); IntegrationsController controller() => container.read(integrationsControllerProvider.notifier); test( 'unsupported platforms disable native providers and surface the ' 'fallback notice', () async { gateway = _ScriptedHealthGateway(nativeSupported: false); container = ProviderContainer( overrides: [healthGatewayProvider.overrideWithValue(gateway)], ); addTearDown(container.dispose); await controller().refreshStatuses(); final s = state(); expect(s.appleHealthStatus, HealthConnectionStatus.unsupportedPlatform); expect(s.healthConnectStatus, HealthConnectionStatus.unsupportedPlatform); expect(s.fhirStatus, HealthConnectionStatus.notConfigured); expect(s.nativeHealthUnsupported, isTrue); expect(s.errorMessage, contains('manual-entry mode')); expect(s.isRequestingPermissions, isFalse); }); test('initial probe reports notConfigured native providers on mobile', () async { await controller().refreshStatuses(); final s = state(); expect(s.appleHealthStatus, HealthConnectionStatus.unsupportedPlatform); expect(s.healthConnectStatus, HealthConnectionStatus.notConfigured); expect(s.fhirStatus, HealthConnectionStatus.notConfigured); expect(s.errorMessage, isNull); }); test('granted authorization flips only the addressed row to connected', () async { await controller().refreshStatuses(); await controller().connectHealthConnect(); final s = state(); expect(gateway.requestedProviders, [HealthProviderType.healthConnect]); expect(s.healthConnectStatus, HealthConnectionStatus.connected); expect(s.healthConnectConnected, isTrue); // The Apple Health row is untouched on an Android-backed gateway. expect(s.appleHealthStatus, HealthConnectionStatus.unsupportedPlatform); expect(s.errorMessage, isNull); expect(s.isRequestingPermissions, isFalse); }); test('denied authorization records permissionDenied and an error message', () async { gateway.grantOnRequest = false; await controller().connectHealthConnect(); final s = state(); expect(s.healthConnectStatus, HealthConnectionStatus.permissionDenied); expect(s.healthConnectConnected, isFalse); expect(s.errorMessage, contains('denied')); expect(s.isRequestingPermissions, isFalse); }); test('disconnect tears down the binding and re-probes the status', () async { await controller().connectHealthConnect(); expect(state().healthConnectStatus, HealthConnectionStatus.connected); await controller().disconnectProvider(HealthProviderType.healthConnect); expect(gateway.disconnectedProviders, [HealthProviderType.healthConnect]); expect(state().healthConnectStatus, HealthConnectionStatus.notConfigured); }); test('clearError resets the transient banner', () async { gateway.grantOnRequest = false; await controller().connectHealthConnect(); expect(state().errorMessage, isNotNull); controller().clearError(); expect(state().errorMessage, isNull); }); }