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<int, Pokemon> store;
  FakeLocal([Map<int, Pokemon>? init]) : store = init ?? {};
  @override
  Future<List<Pokemon>> getAll() async => store.values.toList();
  @override
  Future<Pokemon?> getById(int id) async => store[id];
  @override
  Future<void> saveAll(List<Pokemon> pokemons) async {
    for (final p in pokemons) { store[p.id] = p; }
  }
  @override
  Future<void> update(Pokemon pokemon) async => store[pokemon.id] = pokemon;
  @override
  Future<int> caughtCount() async =>
      store.values.where((p) => p.isCaught).length;
  @override
  Future<int> seenCount() async => store.values.where((p) => p.isSeen).length;
}

class FakeRemote implements PokemonRemoteDataSource {
  final List<Pokemon> all;
  int getAllCalls = 0;
  int getByIdCalls = 0;
  FakeRemote(this.all);
  @override
  Future<List<Pokemon>> getAll() async {
    getAllCalls++;
    return all;
  }
  @override
  Future<Pokemon> getById(int id) async {
    getByIdCalls++;
    return all.firstWhere((p) => p.id == id);
  }
}

class ThrowingRemote implements PokemonRemoteDataSource {
  @override
  Future<List<Pokemon>> getAll() async => throw Exception('network');
  @override
  Future<Pokemon> 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
    });
  });
}
