import 'package:flutter_test/flutter_test.dart'; import 'package:shared_preferences/shared_preferences.dart'; import 'package:tm_app/core/network/network_manager.dart'; import 'package:tm_app/core/utils/result.dart'; import 'package:tm_app/data/repositories/company_ai_repository.dart'; import 'package:tm_app/data/repositories/home_repository.dart'; import 'package:tm_app/data/services/company_ai_service.dart'; import 'package:tm_app/data/services/home_service.dart'; import 'package:tm_app/data/services/liked_tenders_service.dart'; import 'package:tm_app/data/services/model/recommended_tenders_data/recommended_tenders_data.dart'; import 'package:tm_app/data/services/model/recommended_tenders_response/recommended_tenders_response.dart'; import 'package:tm_app/data/services/model/tender_data/tender_data.dart'; import 'package:tm_app/data/services/your_tenders_service.dart'; import 'package:tm_app/view_models/ai_recommendations_view_model.dart'; void main() { group('AiRecommendationsViewModel', () { test('loads recommended tenders from GET /api/v1/tenders/recommend', () async { final prefs = await _prefs(); final homeRepository = _FakeHomeRepository( prefs, result: Result.ok( RecommendedTendersResponse( succes: true, message: 'ok', error: null, data: RecommendedTendersData( tenders: [_sampleTender(id: 'tender-1', title: 'Bridge repair')], metadata: null, ), ), ), ); final viewModel = AiRecommendationsViewModel( companyAiRepository: _FakeCompanyAiRepository(prefs), homeRepository: homeRepository, ); await viewModel.loadRecommendations(); expect(viewModel.hasLoaded, isTrue); expect(viewModel.errorMessage, isNull); expect(homeRepository.recommendedTendersCalls, 1); expect(viewModel.recommendedTenders.single.id, 'tender-1'); }); test('keeps failed transient loads retryable for loadIfNeeded', () async { final prefs = await _prefs(); final homeRepository = _FakeHomeRepository( prefs, results: [ Result.error(Exception('network failed')), Result.ok( RecommendedTendersResponse( succes: true, message: 'ok', error: null, data: RecommendedTendersData( tenders: [_sampleTender(id: 'tender-1', title: 'Bridge repair')], metadata: null, ), ), ), ], ); final viewModel = AiRecommendationsViewModel( companyAiRepository: _FakeCompanyAiRepository(prefs), homeRepository: homeRepository, ); await viewModel.loadRecommendations(); await viewModel.loadIfNeeded(); expect(homeRepository.recommendedTendersCalls, 2); expect(viewModel.hasLoaded, isTrue); expect(viewModel.errorMessage, isNull); expect(viewModel.recommendedTenders.single.id, 'tender-1'); }); test( 'marks empty results as still learning after recent onboarding', () async { final prefs = await _prefs(); final viewModel = AiRecommendationsViewModel( companyAiRepository: _FakeCompanyAiRepository( prefs, lastOnboardingStartedAt: DateTime.now().millisecondsSinceEpoch, ), homeRepository: _FakeHomeRepository(prefs), ); await viewModel.loadRecommendations(); expect(viewModel.hasLoaded, isTrue); expect(viewModel.stillLearning, isTrue); expect(viewModel.isEmpty, isTrue); }, ); }); } Future _prefs() async { SharedPreferences.setMockInitialValues({}); return SharedPreferences.getInstance(); } class _FakeCompanyAiRepository extends CompanyAiRepository { _FakeCompanyAiRepository( SharedPreferences prefs, { int? lastOnboardingStartedAt, }) : _lastOnboardingStartedAt = lastOnboardingStartedAt, super(companyAiService: _UnusedCompanyAiService(prefs), prefs: prefs); final int? _lastOnboardingStartedAt; @override int? lastOnboardingStartedAt() => _lastOnboardingStartedAt; } class _FakeHomeRepository extends HomeRepository { _FakeHomeRepository( SharedPreferences prefs, { List>? results, Result? result, }) : _results = results ?? [ result ?? const Result.ok( RecommendedTendersResponse( succes: true, message: 'ok', error: null, data: RecommendedTendersData(tenders: [], metadata: null), ), ), ], super( homeService: HomeService(networkManager: NetworkManager(prefs)), yourTendersService: YourTendersService( networkManager: NetworkManager(prefs), ), likedTendersService: LikedTendersService( networkManager: NetworkManager(prefs), ), ); final List> _results; int recommendedTendersCalls = 0; @override Future> getHomeRecommendedTenders({ required int limit, required int offset, }) async { final index = recommendedTendersCalls; recommendedTendersCalls += 1; return _results[index]; } } class _UnusedCompanyAiService extends CompanyAiService { _UnusedCompanyAiService(SharedPreferences prefs) : super(networkManager: NetworkManager(prefs)); } TenderData _sampleTender({required String id, required String title}) { return TenderData( success: null, message: null, id: id, tenderId: id, noticePublicationId: null, noticeTypeCode: null, formType: null, noticeLanguageCode: null, issueDate: null, issueTime: null, publicationDate: null, title: title, description: null, procurementTypeCode: null, procedureCode: null, estimatedValue: null, currency: null, duration: null, tenderDeadline: null, submissionDeadline: null, applicationDeadline: null, submissionUrl: null, countryCode: null, durationUnit: null, buyerOrganization: null, lots: null, officialLanguages: null, status: null, ); }