import 'package:bolao_mobile/core/api/api_client.dart';
import 'package:bolao_mobile/core/sessao/sessao_store.dart';
import 'package:bolao_mobile/features/boloes/data/boloes_repository.dart';
import 'package:bolao_mobile/features/boloes/data/rascunho_store.dart';
import 'package:bolao_mobile/features/boloes/presentation/rodada_page.dart';
import 'package:flutter/material.dart';
import 'package:flutter_riverpod/flutter_riverpod.dart';
import 'package:flutter_test/flutter_test.dart';
import 'package:shared_preferences/shared_preferences.dart';

/// Store em memória: o teste é sobre o comportamento do rascunho, não sobre o
/// plugin de disco.
class _StoreFalso extends RascunhoStore {
  _StoreFalso([List<PalpiteRascunho>? inicial]) {
    if (inicial != null) _dados['p1.20'] = inicial;
  }

  final Map<String, List<PalpiteRascunho>> _dados = {};

  @override
  Future<List<PalpiteRascunho>> ler(String poolId, int matchday) async =>
      _dados['$poolId.$matchday'] ?? const [];

  @override
  Future<void> salvar(
    String poolId,
    int matchday,
    List<PalpiteRascunho> palpites,
  ) async {
    _dados['$poolId.$matchday'] = palpites;
  }

  @override
  Future<void> limpar(String poolId, int matchday) async {
    _dados.remove('$poolId.$matchday');
  }

  List<PalpiteRascunho> get salvos => _dados['p1.20'] ?? const [];
}

class _RepoFalso extends BoloesRepository {
  _RepoFalso({
    this.falharEnvio = false,
    this.semConexao = false,
    this.sessaoExpirada = false,
  }) : super(ApiClient(SessaoStore()));

  final bool falharEnvio;
  final bool semConexao;
  final bool sessaoExpirada;
  final List<List<({int casa, int fora, String matchId})>> envios = [];

  @override
  Future<Rodada> rodada(String poolId, int matchday) async => Rodada(
    serverTime: DateTime.utc(2026, 8, 6, 12),
    jogos: [
      JogoDaRodada(
        matchId: 'm1',
        homeTeam: 'Flamengo',
        awayTeam: 'Vasco',
        kickoffAt: DateTime.utc(2026, 9, 6, 12),
        deadlineOpen: true,
      ),
      JogoDaRodada(
        matchId: 'm2',
        homeTeam: 'Palmeiras',
        awayTeam: 'Santos',
        kickoffAt: DateTime.utc(2026, 9, 7, 12),
        deadlineOpen: true,
      ),
    ],
  );

  @override
  Future<ResultadoEnvio> palpitar(
    String poolId,
    List<({int casa, int fora, String matchId})> palpites,
  ) async {
    envios.add(palpites);
    if (sessaoExpirada) throw SessaoExpirada();
    if (falharEnvio) {
      throw ApiError(
        semConexao ? 'sem conexão com o servidor' : 'falha no servidor',
        semConexao: semConexao,
      );
    }
    return ResultadoEnvio(
      salvos: palpites.length,
      recusados: 0,
      motivos: const {},
    );
  }
}

Widget _app(_RepoFalso repo, _StoreFalso store) => ProviderScope(
  overrides: [
    boloesRepositoryProvider.overrideWithValue(repo),
    rascunhoStoreProvider.overrideWithValue(store),
  ],
  child: const MaterialApp(home: RodadaPage(poolId: 'p1', matchday: 20)),
);

void main() {
  setUp(() => SharedPreferences.setMockInitialValues({}));
  _extras();

  group('Rascunho de palpite', () {
    testWidgets('o que foi digitado é gravado no aparelho', (t) async {
      final store = _StoreFalso();
      await t.pumpWidget(_app(_RepoFalso(), store));
      await t.pumpAndSettle();

      await t.enterText(find.byType(TextFormField).at(0), '2');
      await t.enterText(find.byType(TextFormField).at(1), '1');
      await t.pumpAndSettle();

      // Fechar o app no meio da digitação não pode custar o palpite.
      expect(store.salvos.length, 1);
      expect(store.salvos.first.matchId, 'm1');
      expect(store.salvos.first.casa, 2);
      expect(store.salvos.first.enviado, isFalse);
    });

    testWidgets('digitado e não confirmado aparece como "não enviado"', (
      t,
    ) async {
      await t.pumpWidget(_app(_RepoFalso(), _StoreFalso()));
      await t.pumpAndSettle();

      await t.enterText(find.byType(TextFormField).at(0), '2');
      await t.enterText(find.byType(TextFormField).at(1), '1');
      await t.pumpAndSettle();

      // Saber que digitou não é saber que salvou.
      expect(find.text('não enviado'), findsOneWidget);
      expect(
        find.textContaining('1 palpite(s) ainda não enviado'),
        findsOneWidget,
      );
    });

    testWidgets('envio bem-sucedido limpa a marca de pendente', (t) async {
      final store = _StoreFalso();
      await t.pumpWidget(_app(_RepoFalso(), store));
      await t.pumpAndSettle();

      await t.enterText(find.byType(TextFormField).at(0), '2');
      await t.enterText(find.byType(TextFormField).at(1), '1');
      await t.pumpAndSettle();
      await t.tap(find.text('Enviar palpites'));
      await t.pumpAndSettle();

      expect(find.text('não enviado'), findsNothing);
      expect(store.salvos.first.enviado, isTrue);
    });

    testWidgets('falha sem conexão preserva o digitado e explica o que fazer', (
      t,
    ) async {
      final repo = _RepoFalso(falharEnvio: true, semConexao: true);
      final store = _StoreFalso();
      await t.pumpWidget(_app(repo, store));
      await t.pumpAndSettle();

      await t.enterText(find.byType(TextFormField).at(0), '3');
      await t.enterText(find.byType(TextFormField).at(1), '0');
      await t.pumpAndSettle();
      await t.tap(find.text('Enviar palpites'));
      await t.pumpAndSettle();

      // O valor digitado continua na tela e no disco, e o texto diz que a fila
      // vai tentar sozinha — em vez de um "erro" que sugere digitar de novo.
      // A faixa fixa **e** o aviso momentâneo falam de conexão, não de "erro".
      expect(find.textContaining('sem conexão'), findsWidgets);
      expect(
        find.textContaining('vão sozinhos quando a rede voltar'),
        findsOneWidget,
      );
      expect(store.salvos.first.enviado, isFalse);
      expect(store.salvos.first.casa, 3);
    });

    testWidgets('AC-3-12 sessão expirada no envio não custa o digitado', (
      t,
    ) async {
      final store = _StoreFalso();
      await t.pumpWidget(_app(_RepoFalso(sessaoExpirada: true), store));
      await t.pumpAndSettle();

      await t.enterText(find.byType(TextFormField).at(0), '4');
      await t.enterText(find.byType(TextFormField).at(1), '1');
      await t.pumpAndSettle();
      await t.tap(find.text('Enviar palpites'));
      await t.pumpAndSettle();

      // Perder palpite por causa de sessão é somar dois problemas que o
      // participante nem relaciona — ele culpa o app por ter "apagado" o que
      // digitou.
      expect(store.salvos.first.casa, 4);
      expect(store.salvos.first.enviado, isFalse);
      expect(find.text('não enviado'), findsOneWidget);
    });

    testWidgets('rascunho pendente de sessão anterior é enviado ao abrir', (
      t,
    ) async {
      final repo = _RepoFalso();
      final store = _StoreFalso(const [
        PalpiteRascunho(matchId: 'm1', casa: 1, fora: 0, enviado: false),
        PalpiteRascunho(matchId: 'm2', casa: 2, fora: 2, enviado: true),
      ]);

      await t.pumpWidget(_app(repo, store));
      await t.pumpAndSettle();

      // A fila envia só o pendente: reenviar o que já foi salvo desperdiçaria
      // rede e poluiria o histórico de revisões do palpite.
      expect(repo.envios.length, 1);
      expect(repo.envios.first.map((e) => e.matchId), ['m1']);
      expect(find.text('não enviado'), findsNothing);
    });

    testWidgets('rascunho restaurado aparece nos campos, mesmo antes da rede', (
      t,
    ) async {
      final store = _StoreFalso(const [
        PalpiteRascunho(matchId: 'm1', casa: 4, fora: 2, enviado: true),
      ]);
      await t.pumpWidget(_app(_RepoFalso(), store));
      await t.pumpAndSettle();

      final campo = t.widget<TextFormField>(find.byType(TextFormField).at(0));
      expect(campo.initialValue, '4');
    });
  });
}

/// Casos do plano `docs/testes/PP-4-plano.md` que faltavam cobertura.
/// Store real sobre `SharedPreferences` em memória — o ponto destes casos é a
/// serialização e a chave, não o comportamento da tela.
void _extras() {
  group('PP-4 — chave e resiliência do rascunho', () {
    test('PP-4-10 rascunho é por bolão e por rodada', () async {
      SharedPreferences.setMockInitialValues({});
      final store = RascunhoStore(await SharedPreferences.getInstance());

      await store.salvar('A', 20, const [
        PalpiteRascunho(matchId: 'm1', casa: 2, fora: 1, enviado: false),
      ]);
      await store.salvar('A', 21, const [
        PalpiteRascunho(matchId: 'm9', casa: 0, fora: 0, enviado: true),
      ]);
      await store.salvar('B', 20, const [
        PalpiteRascunho(matchId: 'm1', casa: 5, fora: 5, enviado: false),
      ]);

      // Chave errada aqui faria o participante enviar palpite de um bolão em
      // outro — e descobrir só no extrato.
      expect((await store.ler('A', 20)).single.casa, 2);
      expect((await store.ler('A', 21)).single.matchId, 'm9');
      expect((await store.ler('B', 20)).single.casa, 5);
      expect(await store.ler('A', 22), isEmpty);
    });

    test('PP-4-09 rascunho corrompido é descartado, não trava o app', () async {
      SharedPreferences.setMockInitialValues({
        'bolao.rascunho.A.20': 'isto não é json',
      });
      final store = RascunhoStore(await SharedPreferences.getInstance());

      // Rascunho ilegível não pode impedir o participante de palpitar de novo.
      expect(await store.ler('A', 20), isEmpty);
      // E a chave suja é limpa, para não tentar decodificar em cada abertura.
      expect(await store.ler('A', 20), isEmpty);

      await store.salvar('A', 20, const [
        PalpiteRascunho(matchId: 'm1', casa: 1, fora: 1, enviado: false),
      ]);
      expect((await store.ler('A', 20)).single.casa, 1);
    });

    test('PP-4-11 salvar lista vazia limpa a chave', () async {
      SharedPreferences.setMockInitialValues({});
      final prefs = await SharedPreferences.getInstance();
      final store = RascunhoStore(prefs);

      await store.salvar('A', 20, const [
        PalpiteRascunho(matchId: 'm1', casa: 1, fora: 0, enviado: true),
      ]);
      expect(prefs.getString('bolao.rascunho.A.20'), isNotNull);

      await store.salvar('A', 20, const []);
      // Deixar `[]` no disco acumularia chave morta por rodada, para sempre.
      expect(prefs.getString('bolao.rascunho.A.20'), isNull);
    });
  });
}
