import 'package:flutter_riverpod/flutter_riverpod.dart'; import 'package:lunchpick/domain/entities/recommendation_record.dart'; import 'package:lunchpick/domain/entities/restaurant.dart'; import 'package:lunchpick/domain/entities/visit_record.dart'; import 'package:lunchpick/presentation/providers/di_providers.dart'; class DebugTestDataState { final bool isEnabled; final bool isProcessing; final String? errorMessage; const DebugTestDataState({ this.isEnabled = false, this.isProcessing = false, this.errorMessage, }); DebugTestDataState copyWith({ bool? isEnabled, bool? isProcessing, String? errorMessage, }) { return DebugTestDataState( isEnabled: isEnabled ?? this.isEnabled, isProcessing: isProcessing ?? this.isProcessing, errorMessage: errorMessage, ); } } class DebugTestDataNotifier extends StateNotifier { DebugTestDataNotifier(this._ref) : super(const DebugTestDataState()); final Ref _ref; static const String _idPrefix = 'debug-preview-'; Future initialize() async { if (state.isProcessing) return; state = state.copyWith(isProcessing: true, errorMessage: null); try { final hasDebugData = await _hasExistingDebugData(); state = state.copyWith(isEnabled: hasDebugData, isProcessing: false); } catch (e) { state = state.copyWith(isProcessing: false, errorMessage: e.toString()); } } Future enableTestData() async { if (state.isProcessing) return; state = state.copyWith(isProcessing: true, errorMessage: null); try { await _clearDebugData(); await _seedDebugData(); state = state.copyWith(isEnabled: true, isProcessing: false); } catch (e) { state = state.copyWith(isProcessing: false, errorMessage: e.toString()); } } Future disableTestData() async { if (state.isProcessing) return; state = state.copyWith(isProcessing: true, errorMessage: null); try { await _clearDebugData(); state = state.copyWith(isEnabled: false, isProcessing: false); } catch (e) { state = state.copyWith(isProcessing: false, errorMessage: e.toString()); } } Future _seedDebugData() async { final restaurantRepo = _ref.read(restaurantRepositoryProvider); final visitRepo = _ref.read(visitRepositoryProvider); final recommendationRepo = _ref.read(recommendationRepositoryProvider); final samples = _buildDebugSamples(); for (final sample in samples) { await restaurantRepo.addRestaurant(sample.restaurant); } for (final sample in samples) { for (final visit in sample.visits) { await visitRepo.addVisitRecord(visit); } for (final reco in sample.recommendations) { await recommendationRepo.addRecommendationRecord(reco); } } } Future _clearDebugData() async { final visitRepo = _ref.read(visitRepositoryProvider); final recommendationRepo = _ref.read(recommendationRepositoryProvider); final restaurantRepo = _ref.read(restaurantRepositoryProvider); final visits = await visitRepo.getAllVisitRecords(); for (final visit in visits.where((v) => _isDebugId(v.id))) { await visitRepo.deleteVisitRecord(visit.id); } final recos = await recommendationRepo.getAllRecommendationRecords(); for (final reco in recos.where((r) => _isDebugId(r.id))) { await recommendationRepo.deleteRecommendationRecord(reco.id); } final restaurants = await restaurantRepo.getAllRestaurants(); for (final restaurant in restaurants.where((r) => _isDebugId(r.id))) { await restaurantRepo.deleteRestaurant(restaurant.id); } } Future _hasExistingDebugData() async { final visitRepo = _ref.read(visitRepositoryProvider); final recommendationRepo = _ref.read(recommendationRepositoryProvider); final restaurantRepo = _ref.read(restaurantRepositoryProvider); final visits = await visitRepo.getAllVisitRecords(); final recos = await recommendationRepo.getAllRecommendationRecords(); final restaurants = await restaurantRepo.getAllRestaurants(); return visits.any((v) => _isDebugId(v.id)) || recos.any((r) => _isDebugId(r.id)) || restaurants.any((r) => _isDebugId(r.id)); } List<_DebugSample> _buildDebugSamples() { final today = DateTime.now(); final baseDay = DateTime(today.year, today.month, today.day); DateTime atDayOffset(int daysAgo, {int hour = 12, int minute = 0}) { return baseDay .subtract(Duration(days: daysAgo)) .add(Duration(hours: hour, minutes: minute)); } VisitRecord buildVisit({ required String id, required String restaurantId, required DateTime visitDate, required bool isConfirmed, }) { return VisitRecord( id: id, restaurantId: restaurantId, visitDate: visitDate, isConfirmed: isConfirmed, createdAt: visitDate, ); } RecommendationRecord buildRecommendation({ required String id, required String restaurantId, required DateTime recommendationDate, }) { return RecommendationRecord( id: id, restaurantId: restaurantId, recommendationDate: recommendationDate, visited: false, createdAt: recommendationDate, ); } Restaurant buildRestaurant({ required String id, required String name, required String category, required String subCategory, required String roadAddress, required String jibunAddress, required double latitude, required double longitude, required String description, required String phoneNumber, required List visits, }) { final latestVisit = visits .map((v) => v.visitDate) .reduce((a, b) => a.isAfter(b) ? a : b); return Restaurant( id: id, name: name, category: category, subCategory: subCategory, description: description, phoneNumber: phoneNumber, roadAddress: roadAddress, jibunAddress: jibunAddress, latitude: latitude, longitude: longitude, lastVisitDate: latestVisit, source: DataSource.PRESET, createdAt: baseDay, updatedAt: baseDay, naverPlaceId: null, naverUrl: null, businessHours: null, lastVisited: latestVisit, visitCount: visits.length, needsAddressVerification: false, ); } final bistroId = _withPrefix('bistro'); final sushiId = _withPrefix('sushi'); final coffeeId = _withPrefix('coffee'); final bistroVisits = [ buildVisit( id: _withPrefix('visit-bistro-0'), restaurantId: bistroId, visitDate: atDayOffset(0, hour: 12, minute: 10), isConfirmed: false, ), buildVisit( id: _withPrefix('visit-bistro-1'), restaurantId: bistroId, visitDate: atDayOffset(2, hour: 19, minute: 0), isConfirmed: true, ), buildVisit( id: _withPrefix('visit-bistro-2'), restaurantId: bistroId, visitDate: atDayOffset(5, hour: 13, minute: 15), isConfirmed: true, ), ]; final sushiVisits = [ buildVisit( id: _withPrefix('visit-sushi-0'), restaurantId: sushiId, visitDate: atDayOffset(1, hour: 12, minute: 40), isConfirmed: true, ), buildVisit( id: _withPrefix('visit-sushi-1'), restaurantId: sushiId, visitDate: atDayOffset(3, hour: 18, minute: 30), isConfirmed: false, ), buildVisit( id: _withPrefix('visit-sushi-2'), restaurantId: sushiId, visitDate: atDayOffset(6, hour: 20, minute: 10), isConfirmed: true, ), ]; final coffeeVisits = [ buildVisit( id: _withPrefix('visit-coffee-0'), restaurantId: coffeeId, visitDate: atDayOffset(2, hour: 9, minute: 30), isConfirmed: true, ), buildVisit( id: _withPrefix('visit-coffee-1'), restaurantId: coffeeId, visitDate: atDayOffset(4, hour: 15, minute: 15), isConfirmed: true, ), buildVisit( id: _withPrefix('visit-coffee-2'), restaurantId: coffeeId, visitDate: atDayOffset(7, hour: 11, minute: 50), isConfirmed: true, ), ]; final samples = <_DebugSample>[ _DebugSample( restaurant: buildRestaurant( id: bistroId, name: 'Debug Bistro', category: 'Fusion', subCategory: 'Brunch', description: 'Sample data to preview the record and statistics experience.', phoneNumber: '02-100-0001', roadAddress: '서울 테스트로 12', jibunAddress: '서울 테스트동 12-1', latitude: 37.5665, longitude: 126.9780, visits: bistroVisits, ), visits: bistroVisits, recommendations: [ buildRecommendation( id: _withPrefix('reco-bistro-0'), restaurantId: bistroId, recommendationDate: atDayOffset(1, hour: 11, minute: 20), ), buildRecommendation( id: _withPrefix('reco-bistro-1'), restaurantId: bistroId, recommendationDate: atDayOffset(4, hour: 18, minute: 40), ), ], ), _DebugSample( restaurant: buildRestaurant( id: sushiId, name: 'Sample Sushi Bar', category: 'Japanese', subCategory: 'Sushi', description: 'Rotating omakase picks to mimic real visit timelines.', phoneNumber: '02-200-0002', roadAddress: '서울 샘플로 21', jibunAddress: '서울 샘플동 21-3', latitude: 37.5559, longitude: 126.9363, visits: sushiVisits, ), visits: sushiVisits, recommendations: [ buildRecommendation( id: _withPrefix('reco-sushi-0'), restaurantId: sushiId, recommendationDate: atDayOffset(3, hour: 12, minute: 0), ), buildRecommendation( id: _withPrefix('reco-sushi-1'), restaurantId: sushiId, recommendationDate: atDayOffset(7, hour: 19, minute: 10), ), ], ), _DebugSample( restaurant: buildRestaurant( id: coffeeId, name: 'Test Coffee Lab', category: 'Cafe', subCategory: 'Dessert', description: 'Morning cafe stops added so charts render immediately.', phoneNumber: '02-300-0003', roadAddress: '서울 예제길 5', jibunAddress: '서울 예제동 5-2', latitude: 37.5412, longitude: 126.986, visits: coffeeVisits, ), visits: coffeeVisits, recommendations: [ buildRecommendation( id: _withPrefix('reco-coffee-0'), restaurantId: coffeeId, recommendationDate: atDayOffset(0, hour: 8, minute: 50), ), buildRecommendation( id: _withPrefix('reco-coffee-1'), restaurantId: coffeeId, recommendationDate: atDayOffset(5, hour: 16, minute: 30), ), ], ), ]; return samples; } bool _isDebugId(String id) => id.startsWith(_idPrefix); String _withPrefix(String rawId) => '$_idPrefix$rawId'; } final debugTestDataNotifierProvider = StateNotifierProvider((ref) { return DebugTestDataNotifier(ref); }); class _DebugSample { final Restaurant restaurant; final List visits; final List recommendations; _DebugSample({ required this.restaurant, required this.visits, required this.recommendations, }); }