import 'package:flutter_test/flutter_test.dart'; import 'package:pokeguess/domain/entities/pokemon.dart'; import 'package:pokeguess/data/datasources/pokemon_local_datasource.dart'; import 'package:pokeguess/data/datasources/pokemon_remote_datasource.dart'; import 'package:pokeguess/data/repositories/pokemon_repository_impl.dart'; Pokemon _mk(int id, {bool caught = false}) => Pokemon( name: 'p$id', id: id, type1: PokemonType.normal, hp: 1, atk: 1, def: 1, spd: 1, isCaught: caught, ); class FakeLocal implements PokemonLocalDataSource { final Map store; FakeLocal([Map? init]) : store = init ?? {}; @override Future> getAll() async => store.values.toList(); @override Future getById(int id) async => store[id]; @override Future saveAll(List pokemons) async { for (final p in pokemons) store[p.id] = p; } @override Future update(Pokemon pokemon) async => store[pokemon.id] = pokemon; @override Future caughtCount() async => store.values.where((p) => p.isCaught).length; @override Future seenCount() async => store.values.where((p) => p.isSeen).length; } class FakeRemote implements PokemonRemoteDataSource { final List all; int getAllCalls = 0; int getByIdCalls = 0; FakeRemote(this.all); @override Future> getAll() async { getAllCalls++; return all; } @override Future getById(int id) async { getByIdCalls++; return all.firstWhere((p) => p.id == id); } } class ThrowingRemote implements PokemonRemoteDataSource { @override Future> getAll() async => throw Exception('network'); @override Future getById(int id) async => throw Exception('network'); } void main() { group('getById', () { test('retourne le cache local sans appeler l\'API', () async { final local = FakeLocal({5: _mk(5)}); final remote = FakeRemote([_mk(5)]); final repo = PokemonRepositoryImpl(remote: remote, local: local); final p = await repo.getById(5); expect(p?.id, 5); expect(remote.getByIdCalls, 0); }); test('va chercher sur l\'API et met en cache si absent en local', () async { final local = FakeLocal(); final remote = FakeRemote([_mk(7)]); final repo = PokemonRepositoryImpl(remote: remote, local: local); final p = await repo.getById(7); expect(p?.id, 7); expect(remote.getByIdCalls, 1); expect(await local.getById(7), isNotNull); // mis en cache }); test('sans local (web), passe directement par l\'API', () async { final remote = FakeRemote([_mk(9)]); final repo = PokemonRepositoryImpl(remote: remote, local: null); final p = await repo.getById(9); expect(p?.id, 9); expect(remote.getByIdCalls, 1); }); }); group('getAll', () { test('si le cache est complet, ne rappelle pas l\'API', () async { final store = {for (var i = 1; i <= 1025; i++) i: _mk(i)}; final local = FakeLocal(store); final remote = FakeRemote([]); final repo = PokemonRepositoryImpl(remote: remote, local: local); final list = await repo.getAll(); expect(list.length, 1025); expect(remote.getAllCalls, 0); }); test('si le cache est incomplet, synchronise depuis l\'API', () async { final local = FakeLocal({1: _mk(1)}); final remote = FakeRemote([_mk(1), _mk(2), _mk(3)]); final repo = PokemonRepositoryImpl(remote: remote, local: local); final list = await repo.getAll(); expect(remote.getAllCalls, 1); expect(list.length, 3); }); }); group('repli sur erreur', () { test('getById retourne null si l\'API échoue et le cache est vide', () async { final repo = PokemonRepositoryImpl(remote: ThrowingRemote(), local: FakeLocal()); expect(await repo.getById(42), isNull); }); test('getAll retourne le cache existant si la synchro API échoue', () async { final local = FakeLocal({1: _mk(1)}); // cache incomplet -> tente l'API, qui échoue final repo = PokemonRepositoryImpl(remote: ThrowingRemote(), local: local); final list = await repo.getAll(); expect(list.length, 1); // repli sur le cache }); }); }