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/participantes_repository.dart';
import 'package:bolao_mobile/features/boloes/data/participantes_repository_http.dart';
import 'package:bolao_mobile/features/boloes/presentation/avisos_page.dart';
import 'package:bolao_mobile/features/boloes/presentation/participantes_page.dart';
import 'package:bolao_mobile/features/boloes/presentation/regras_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';

class _RepoFalso extends ParticipantesRepositoryHttp {
  _RepoFalso({this.asRegras, this.osAvisos = const []})
    : super(ApiClient(SessaoStore()));

  /// Regras a devolver. Mutável para simular o admin alterando a tabela entre
  /// duas aberturas da tela (CC-6-10).
  Regras? asRegras;
  final List<Aviso> osAvisos;
  final List<String> lidos = [];

  @override
  Future<Regras> regras(String poolId) async {
    if (asRegras == null) throw ApiError('não foi possível conectar');
    return asRegras!;
  }

  @override
  Future<List<Aviso>> avisos(String poolId) async => osAvisos;

  @override
  Future<void> marcarLido(String poolId, String avisoId) async =>
      lidos.add(avisoId);
}

Widget _app(_RepoFalso repo, Widget tela) => ProviderScope(
  overrides: [participantesRepositoryProvider.overrideWithValue(repo)],
  child: MaterialApp(home: tela),
);

/// Tira a tela da árvore entre duas aberturas. Sem isto o Flutter reusa o
/// `State` (mesmo tipo de widget, mesma posição) e `initState` não roda de novo:
/// o teste mediria a primeira carga, não a segunda.
Future<void> _desmontar(WidgetTester t) async {
  await t.pumpWidget(const MaterialApp(home: SizedBox.shrink()));
  await t.pumpAndSettle();
}

void main() {
  // As telas gravam e leem cache local; sem o plugin falso o `getInstance`
  // nunca resolve e o `pumpAndSettle` estoura.
  setUp(() => SharedPreferences.setMockInitialValues({}));

  group('RegrasPage', () {
    testWidgets('CC-6-01 tabela de pontos e ordem de desempate na tela', (t) async {
      final repo = _RepoFalso(
        asRegras: const Regras(
          pontosExato: 10,
          pontosVencedorSaldo: 5,
          pontosVencedor: 3,
          ordemDesempate: ['EXACT_SCORES', 'WINNERS'],
          travadas: true,
          proximoPrazo: null,
        ),
      );
      await t.pumpWidget(_app(repo, const RegrasPage(poolId: 'p1')));
      await t.pumpAndSettle();

      // É a resposta a "por que ele levou 5 e eu levei 3".
      expect(find.text('10 pts'), findsOneWidget);
      expect(find.text('5 pts'), findsOneWidget);
      expect(find.text('3 pts'), findsOneWidget);
      expect(find.text('0 pts'), findsOneWidget);
      expect(
        find.textContaining(
          'mais placares exatos, depois mais acertos de vencedor',
        ),
        findsOneWidget,
      );
    });

    testWidgets('CC-6-02 empate não tem faixa de saldo, e a tela diz isso', (t) async {
      final repo = _RepoFalso(
        asRegras: const Regras(
          pontosExato: 10,
          pontosVencedorSaldo: 5,
          pontosVencedor: 3,
          ordemDesempate: ['EXACT_SCORES'],
          travadas: true,
          proximoPrazo: null,
        ),
      );
      await t.pumpWidget(_app(repo, const RegrasPage(poolId: 'p1')));
      await t.pumpAndSettle();

      // Quem palpitou 1x1 num jogo que deu 4x4 leva 3, não 5: todo empate tem
      // saldo 0, então acertar o saldo não diz nada além de acertar o empate.
      expect(find.textContaining('não existe faixa de saldo'), findsOneWidget);
    });

    testWidgets('CC-6-03 e CC-3-08 regras travadas dizem que travaram, e quando', (t) async {
      final repo = _RepoFalso(
        asRegras: const Regras(
          pontosExato: 10,
          pontosVencedorSaldo: 5,
          pontosVencedor: 3,
          ordemDesempate: ['EXACT_SCORES'],
          travadas: true,
          proximoPrazo: null,
        ),
      );
      await t.pumpWidget(_app(repo, const RegrasPage(poolId: 'p1')));
      await t.pumpAndSettle();

      // Rolar até o fim também prova que o aviso é alcançável na tela.
      await t.drag(find.byType(ListView), const Offset(0, -600));
      await t.pumpAndSettle();

      // Saber que ninguém pode mais mudar é o que encerra a discussão.
      expect(find.textContaining('estão travadas'), findsOneWidget);
      expect(find.byIcon(Icons.lock_outline), findsOneWidget);
    });

    testWidgets('CC-6-04 regras abertas avisam que travam no primeiro palpite', (
      t,
    ) async {
      final repo = _RepoFalso(
        asRegras: const Regras(
          pontosExato: 12,
          pontosVencedorSaldo: 6,
          pontosVencedor: 2,
          ordemDesempate: ['WINNERS'],
          travadas: false,
          proximoPrazo: null,
        ),
      );
      await t.pumpWidget(_app(repo, const RegrasPage(poolId: 'p1')));
      await t.pumpAndSettle();

      // A tabela é lida do bolão, não fixa no app: pontuação customizada
      // precisa aparecer como está — e ela fica acima do fold.
      expect(find.text('12 pts'), findsOneWidget);

      await t.drag(find.byType(ListView), const Offset(0, -600));
      await t.pumpAndSettle();

      expect(find.textContaining('travam no primeiro palpite'), findsOneWidget);
    });

    testWidgets('CC-6-08 offline sem cache: motivo e retry', (t) async {
      await t.pumpWidget(_app(_RepoFalso(), const RegrasPage(poolId: 'p1')));
      await t.pumpAndSettle();

      expect(find.text('não foi possível conectar'), findsOneWidget);
      expect(find.text('Tentar de novo'), findsOneWidget);
    });
  });

  group('AvisosPage', () {
    Aviso aviso({
      String id = 'a1',
      String kind = 'PLACAR_CORRIGIDO',
      String titulo = 'Sua pontuação mudou',
      bool lido = false,
    }) => Aviso(
      id: id,
      kind: kind,
      titulo: titulo,
      corpo: 'Flamengo x Vasco: o placar mudou de 2x0 para 2x1.',
      criadoEm: DateTime.utc(2026, 8, 6, 12),
      lido: lido,
    );

    testWidgets('PR-7-09 e PP-8-09 sem push, o aviso continua visível e é marcável no app', (t) async {
      final repo = _RepoFalso(osAvisos: [aviso()]);
      await t.pumpWidget(
        _app(repo, const AvisosPage(poolId: 'p1', matchday: 20)),
      );
      await t.pumpAndSettle();

      expect(find.text('Sua pontuação mudou'), findsOneWidget);
      await t.tap(find.text('Marcar lido'));
      await t.pumpAndSettle();
      expect(repo.lidos, ['a1']);
    });

    testWidgets('aviso já lido não oferece marcar de novo', (t) async {
      final repo = _RepoFalso(osAvisos: [aviso(lido: true)]);
      await t.pumpWidget(
        _app(repo, const AvisosPage(poolId: 'p1', matchday: 20)),
      );
      await t.pumpAndSettle();

      expect(find.text('Marcar lido'), findsNothing);
    });

    testWidgets('PR-2-09 e PR-7-06 correção manual fica identificada como ação humana', (
      t,
    ) async {
      final repo = _RepoFalso(
        osAvisos: [aviso(kind: 'PLACAR_CORRIGIDO_MANUAL')],
      );
      await t.pumpWidget(
        _app(repo, const AvisosPage(poolId: 'p1', matchday: 20)),
      );
      await t.pumpAndSettle();

      // Ranking mudando para trás sem explicação é indistinguível de fraude.
      expect(
        find.textContaining('alterado pelo administrador'),
        findsOneWidget,
      );
    });

    testWidgets('PR-7-03 tocar no aviso de pontuação leva ao extrato', (
      t,
    ) async {
      // Sem isto o participante lê "sua pontuação mudou" e não tem para onde ir:
      // a conta do ponto está no extrato, e é ela que encerra a dúvida.
      final repo = _RepoFalso(osAvisos: [aviso()]);
      await t.pumpWidget(
        _app(repo, const AvisosPage(poolId: 'p1', matchday: 20)),
      );
      await t.pumpAndSettle();

      await t.tap(find.text('Sua pontuação mudou'));
      // `pump` em passos, não `pumpAndSettle`: o extrato aberto busca dados do
      // repositório de ranking, que este teste não stuba — a árvore fica
      // carregando para sempre, e é justamente o que se quer ver (navegou).
      await t.pump();
      await t.pump(const Duration(milliseconds: 300));

      expect(find.text('Extrato da rodada'), findsOneWidget);
      // E o aviso tocado já conta como lido.
      expect(repo.lidos, ['a1']);
    });

    testWidgets('PR-7-03b aviso que não é de pontuação não navega', (t) async {
      // Lembrete de prazo não tem extrato para abrir; toque que não faz nada é
      // melhor que toque que abre a tela errada.
      final repo = _RepoFalso(
        osAvisos: [
          aviso(kind: 'PRAZO_FECHANDO', titulo: 'O prazo está fechando'),
        ],
      );
      await t.pumpWidget(
        _app(repo, const AvisosPage(poolId: 'p1', matchday: 20)),
      );
      await t.pumpAndSettle();

      await t.tap(find.text('O prazo está fechando'));
      await t.pump();
      await t.pump(const Duration(milliseconds: 300));

      expect(find.text('Extrato da rodada'), findsNothing);
    });

    testWidgets('sem avisos, explica quando eles aparecem', (t) async {
      await t.pumpWidget(
        _app(_RepoFalso(), const AvisosPage(poolId: 'p1', matchday: 20)),
      );
      await t.pumpAndSettle();

      expect(find.text('Nenhum aviso neste bolão'), findsOneWidget);
      expect(find.textContaining('placar é corrigido'), findsOneWidget);
    });

    testWidgets('cada tipo de aviso tem ícone próprio', (t) async {
      final repo = _RepoFalso(
        osAvisos: [
          aviso(id: 'a1', kind: 'JOGO_REABERTO', titulo: 'Jogo adiado'),
          aviso(id: 'a2', kind: 'PONTO_REVERTIDO', titulo: 'Ponto revertido'),
          aviso(id: 'a3', kind: 'BOLAO_RECRIADO', titulo: 'Temporada nova'),
          aviso(id: 'a4', kind: 'PRAZO_FECHANDO', titulo: 'Prazo fechando'),
        ],
      );
      await t.pumpWidget(
        _app(repo, const AvisosPage(poolId: 'p1', matchday: 20)),
      );
      await t.pumpAndSettle();

      // Ícone genérico em tudo faz a lista parecer uma parede de texto, e o
      // aviso que importa se perde no meio.
      expect(find.byIcon(Icons.event_repeat), findsOneWidget);
      expect(find.byIcon(Icons.undo), findsOneWidget);
      expect(find.byIcon(Icons.autorenew), findsOneWidget);
      expect(find.byIcon(Icons.timer_outlined), findsOneWidget);
    });
  });

  group('CC-6-12 próximo prazo junto das regras', () {
    testWidgets('mostra data e hora no fuso do aparelho', (t) async {
      // Prazo em UTC na tela é o erro que faz a pessoa perder o jogo por três
      // horas — o palpite fecha no apito, não "por volta de".
      final prazo = DateTime.now().toUtc().add(const Duration(days: 2));
      await t.pumpWidget(
        _app(
          _RepoFalso(
            asRegras: Regras(
              pontosExato: 10,
              pontosVencedorSaldo: 5,
              pontosVencedor: 3,
              ordemDesempate: const ['EXACT_SCORES', 'WINNERS'],
              travadas: false,
              proximoPrazo: prazo,
            ),
          ),
          const RegrasPage(poolId: 'p1'),
        ),
      );
      await t.pumpAndSettle();

      // O prazo fica no fim da lista; rolar também prova que é alcançável.
      await t.drag(find.byType(ListView), const Offset(0, -600));
      await t.pumpAndSettle();

      final l = prazo.toLocal();
      final esperado =
          '${l.day.toString().padLeft(2, '0')}/${l.month.toString().padLeft(2, '0')}';
      expect(find.textContaining('Próximo prazo: $esperado'), findsOneWidget);
      // E o quanto falta, que é o que a pessoa realmente usa. `1d 23h` porque
      // faltam dois dias menos os milissegundos gastos até aqui — arredondar
      // para cima ("2d") faria a tela prometer prazo que já passou.
      expect(find.textContaining('fecha em 1d 23h'), findsOneWidget);
    });
  });

  group('CC-6 regras offline', () {
    Regras regras({int exato = 10}) => Regras(
      pontosExato: exato,
      pontosVencedorSaldo: 5,
      pontosVencedor: 3,
      ordemDesempate: const ['EXACT_SCORES', 'WINNERS'],
      travadas: false,
      proximoPrazo: null,
    );

    testWidgets('CC-6-07 sem rede, mostra a regra guardada com a idade', (
      t,
    ) async {
      // Primeira abertura com rede: grava.
      await t.pumpWidget(
        _app(_RepoFalso(asRegras: regras()), const RegrasPage(poolId: 'p1')),
      );
      await t.pumpAndSettle();
      expect(find.text('10 pts'), findsOneWidget);

      await _desmontar(t);

      // Segunda abertura sem rede: a regra é o que se consulta no meio da
      // discussão, e discussão não espera rede.
      await t.pumpWidget(_app(_RepoFalso(), const RegrasPage(poolId: 'p1')));
      await t.pumpAndSettle();

      expect(find.text('10 pts'), findsOneWidget);
      // Rotulada: regra destravada pode ter mudado desde então.
      expect(find.textContaining('guardadas no aparelho'), findsOneWidget);
      expect(find.textContaining('agora mesmo'), findsOneWidget);
    });

    testWidgets('CC-6-09 cache é por bolão — B não mostra a regra de A', (
      t,
    ) async {
      await t.pumpWidget(
        _app(
          _RepoFalso(asRegras: regras(exato: 20)),
          const RegrasPage(poolId: 'A'),
        ),
      );
      await t.pumpAndSettle();
      expect(find.text('20 pts'), findsOneWidget);

      await _desmontar(t);

      // Mostrar a regra de outro bolão é pior que não mostrar nada: a pessoa
      // decide o palpite pela tabela errada.
      await t.pumpWidget(_app(_RepoFalso(), const RegrasPage(poolId: 'B')));
      await t.pumpAndSettle();

      expect(find.text('20 pts'), findsNothing);
      expect(find.text('não foi possível conectar'), findsOneWidget);
    });

    testWidgets('CC-6-10 com rede, a regra nova substitui a guardada', (
      t,
    ) async {
      final repo = _RepoFalso(asRegras: regras());
      await t.pumpWidget(_app(repo, const RegrasPage(poolId: 'p1')));
      await t.pumpAndSettle();
      expect(find.text('10 pts'), findsOneWidget);

      await _desmontar(t);

      // Admin alterou a tabela antes do travamento.
      repo.asRegras = regras(exato: 12);
      await t.pumpWidget(_app(repo, const RegrasPage(poolId: 'p1')));
      await t.pumpAndSettle();

      expect(find.text('12 pts'), findsOneWidget);
      // Veio da rede: não pode aparecer rótulo de dado guardado.
      expect(find.textContaining('guardadas no aparelho'), findsNothing);

      await _desmontar(t);

      // E o cache foi atualizado: a próxima abertura offline mostra a nova.
      await t.pumpWidget(_app(_RepoFalso(), const RegrasPage(poolId: 'p1')));
      await t.pumpAndSettle();
      expect(find.text('12 pts'), findsOneWidget);
    });
  });
}
