import 'package:flutter_test/flutter_test.dart'; import 'package:gel_rule_app/backend/models/content_provider_config.dart'; import 'package:gel_rule_app/backend/models/post.dart'; import 'package:gel_rule_app/backend/models/provider_diagnostics.dart'; import 'package:gel_rule_app/backend/models/provider_health.dart'; import 'package:gel_rule_app/backend/models/top_period_filter.dart'; import 'package:gel_rule_app/backend/providers/content_provider.dart'; import 'package:gel_rule_app/backend/providers/provider_factory.dart'; import 'package:gel_rule_app/backend/providers/provider_manager.dart'; import 'package:gel_rule_app/backend/repositories/provider_repository.dart'; import 'package:gel_rule_app/core/utils/result.dart'; class FakeProviderRepository implements ProviderRepository { final configs = {}; final health = {}; final diagnostics = {}; @override Future> ensureSeedProviders() async => const Success(null); @override Future>> getProviders({ bool enabledOnly = false, }) async { final values = configs.values .where((config) => !enabledOnly || config.enabled) .toList() ..sort((a, b) => a.priority.compareTo(b.priority)); return Success(values); } @override Future> getProvider(String id) async { return Success(configs[id]); } @override Future> saveProvider(ContentProviderConfig config) async { configs[config.id] = config; return const Success(null); } @override Future> deleteProvider(String id) async { configs.remove(id); return const Success(null); } @override Future> saveHealth(ProviderHealth value) async { health[value.providerId] = value; return const Success(null); } @override Future> getHealth(String providerId) async { return Success(health[providerId]); } @override Future> saveDiagnostics(ProviderDiagnostics value) async { diagnostics[value.providerId] = value; return const Success(null); } @override dynamic noSuchMethod(Invocation invocation) => super.noSuchMethod(invocation); } class FakeProviderFactory extends ProviderFactory { FakeProviderFactory(this.providers); final Map providers; @override ContentProvider create(ContentProviderConfig config) => providers[config.id]!; } class FakeProvider implements ContentProvider { FakeProvider(this.id, this.name, this.posts, {this.failSearch = false}); @override final String id; @override final String name; @override String get baseUrl => 'https://example.test'; final List posts; final bool failSearch; @override Future checkHealth() async => ProviderHealth( providerId: id, status: ProviderStatus.online, pingMs: 1, lastCheckedAt: DateTime.now(), ); @override Future getPost(String id) async => posts.where((post) => post.id == id).firstOrNull; @override Future> searchPosts({ required List tags, required int page, int limit = 50, String? rating, TopPeriodFilter topPeriod = TopPeriodFilter.none, }) async { if (failSearch) throw Exception('fail'); return posts; } } ContentProviderConfig config(String id, int priority, {bool enabled = true}) { final now = DateTime.now(); return ContentProviderConfig( id: id, name: id, baseUrl: 'https://example.test', apiType: 'fake', enabled: enabled, priority: priority, timeoutSeconds: 10, customHeaders: const {}, createdAt: now, updatedAt: now, ); } Post post(String providerId, String id) => Post( id: id, providerId: providerId, providerName: providerId, previewUrl: '', sampleUrl: '', fileUrl: '', tags: const [], rating: 'safe', width: 0, height: 0, createdAt: DateTime.now(), fileType: 'unknown', score: 0, ); void main() { test('active providers are sorted by priority', () async { final repository = FakeProviderRepository() ..configs['b'] = config('b', 2) ..configs['a'] = config('a', 1); final manager = ProviderManager( repository, FakeProviderFactory({ 'a': FakeProvider('a', 'a', []), 'b': FakeProvider('b', 'b', []), }), ); final result = await manager.activeProviders() as Success>; expect(result.data.map((provider) => provider.id), ['a', 'b']); }); test('saved offline health does not block enabled provider retry', () async { final repository = FakeProviderRepository() ..configs['a'] = config('a', 0) ..configs['b'] = config('b', 1) ..configs['c'] = config('c', 2) ..health['b'] = ProviderHealth( providerId: 'b', status: ProviderStatus.offline, pingMs: 0, lastCheckedAt: DateTime.now(), ); final manager = ProviderManager( repository, FakeProviderFactory({ 'a': FakeProvider('a', 'a', [post('a', '1')]), 'b': FakeProvider('b', 'b', [post('b', '1')]), 'c': FakeProvider('c', 'c', [], failSearch: true), }), ); final result = await manager.searchAcrossProviders(tags: [], page: 0) as Success>; expect(result.data.map((item) => item.providerId), ['a', 'b']); expect(repository.health['c']?.status, ProviderStatus.offline); expect(repository.diagnostics['a']?.lastResultCount, 1); expect(repository.diagnostics['c']?.lastErrorMessage, 'Search failed'); }); test('all providers results are interleaved by provider priority', () async { final repository = FakeProviderRepository() ..configs['a'] = config('a', 0) ..configs['b'] = config('b', 1) ..configs['c'] = config('c', 2); final manager = ProviderManager( repository, FakeProviderFactory({ 'a': FakeProvider('a', 'a', [post('a', '1'), post('a', '2')]), 'b': FakeProvider('b', 'b', [post('b', '1'), post('b', '2')]), 'c': FakeProvider('c', 'c', [post('c', '1')]), }), ); final result = await manager.searchAcrossProviders(tags: [], page: 0) as Success>; expect( result.data.map((item) => item.cacheKey), ['a:1', 'b:1', 'c:1', 'a:2', 'b:2'], ); }); test('enable disable provider persists config', () async { final repository = FakeProviderRepository()..configs['a'] = config('a', 0); final manager = ProviderManager( repository, FakeProviderFactory({'a': FakeProvider('a', 'a', [])}), ); await manager.enableProvider('a', false); expect(repository.configs['a']!.enabled, isFalse); }); }