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/boloes_repository_http.dart';
import 'package:bolao_mobile/features/boloes/presentation/criar_bolao_page.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';

/// Repositório falso da criação. Guarda o que foi enviado, porque metade dos
/// casos é sobre **o que o app manda** (regras omitidas, intervalo escolhido).
class _RepoFalso extends BoloesRepositoryHttp {
  _RepoFalso({this.erroNasCompeticoes, this.criado, this.erroAoCriar})
    : super(ApiClient(SessaoStore()));

  /// Rodada atual 22: é o que faz a tela sugerir 23–38.
  static const _competicoes = [
    Competicao(code: 'BSA', nome: 'Brasileirão Série A', rodadaAtual: 22),
  ];

  final ApiError? erroNasCompeticoes;
  final Bolao? criado;
  final ApiError? erroAoCriar;

  Map<String, dynamic>? enviado;

  @override
  Future<List<Competicao>> competicoes() async {
    if (erroNasCompeticoes != null) throw erroNasCompeticoes!;
    return _competicoes;
  }

  @override
  Future<Bolao> criar({
    required String nome,
    required String competitionCode,
    required int rodadaInicial,
    required int rodadaFinal,
    int? pontosExato,
    int? pontosVencedorSaldo,
    int? pontosVencedor,
    List<String>? ordemDesempate,
  }) async {
    enviado = {
      'nome': nome,
      'competitionCode': competitionCode,
      'de': rodadaInicial,
      'ate': rodadaFinal,
      'exato': pontosExato,
      'vencedorSaldo': pontosVencedorSaldo,
      'vencedor': pontosVencedor,
      'ordem': ordemDesempate,
    };
    if (erroAoCriar != null) throw erroAoCriar!;
    return criado ??
        const Bolao(
          id: 'p1',
          name: 'Bolão do setor',
          competitionCode: 'BSA',
          matchdayFrom: 23,
          matchdayTo: 38,
          totalMatches: 160,
          openMatches: 160,
        );
  }
}

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

/// Toca em "Criar bolão" e deixa o app reagir **sem** `pumpAndSettle`.
///
/// O botão mostra um `CircularProgressIndicator` enquanto envia, e ele continua
/// animando enquanto o diálogo de aviso está aberto — `pumpAndSettle` esperaria
/// para sempre por uma árvore que nunca fica parada.
Future<void> _criar(WidgetTester t) async {
  await t.tap(find.widgetWithText(FilledButton, 'Criar bolão'));
  for (var i = 0; i < 6; i++) {
    await t.pump(const Duration(milliseconds: 100));
  }
}

void main() {
  // A navegação pós-criação abre a tela do bolão, que toca disco.
  setUp(() => SharedPreferences.setMockInitialValues({}));

  group('CC-1 fluxo de criação', () {
    testWidgets('CC-1-06 sugere o intervalo daqui até o fim do campeonato', (
      t,
    ) async {
      await t.pumpWidget(_app(_RepoFalso()));
      await t.pumpAndSettle();

      // Rodada atual 22 → sugere 23–38. Digitar dois números a menos é o tipo
      // de atrito que decide se o admin cria ou desiste.
      expect(find.text('23'), findsOneWidget);
      expect(find.text('38'), findsOneWidget);
    });

    testWidgets('CC-1-07 nome ausente é recusado no campo, sem chamar a API', (
      t,
    ) async {
      final repo = _RepoFalso();
      await t.pumpWidget(_app(repo));
      await t.pumpAndSettle();

      await t.tap(find.widgetWithText(FilledButton, 'Criar bolão'));
      await t.pumpAndSettle();

      expect(find.text('dê um nome ao bolão'), findsOneWidget);
      expect(repo.enviado, isNull);
    });

    testWidgets('CC-1-07b rodada fora de 1–38 é recusada no campo', (t) async {
      final repo = _RepoFalso();
      await t.pumpWidget(_app(repo));
      await t.pumpAndSettle();

      await t.enterText(find.byType(TextFormField).at(0), 'Bolão do setor');
      await t.enterText(find.byType(TextFormField).at(2), '39');
      await t.tap(find.widgetWithText(FilledButton, 'Criar bolão'));
      await t.pumpAndSettle();

      expect(find.text('entre 1 e 38'), findsOneWidget);
      expect(repo.enviado, isNull);
    });

    testWidgets('CC-1-04 fonte indisponível mostra o motivo com retry', (
      t,
    ) async {
      final repo = _RepoFalso(
        erroNasCompeticoes: ApiError(
          'não foi possível carregar os jogos do campeonato agora',
        ),
      );
      await t.pumpWidget(_app(repo));
      await t.pumpAndSettle();

      // Formulário vazio sem explicação faria o admin achar que o app quebrou.
      expect(
        find.text('não foi possível carregar os jogos do campeonato agora'),
        findsOneWidget,
      );
      expect(find.text('Tentar de novo'), findsOneWidget);
      expect(find.widgetWithText(FilledButton, 'Criar bolão'), findsNothing);
    });

    testWidgets('CC-1-05 falha na criação aparece com o motivo do servidor', (
      t,
    ) async {
      final repo = _RepoFalso(
        erroAoCriar: ApiError(
          'não há jogos nessas rodadas para este campeonato',
        ),
      );
      await t.pumpWidget(_app(repo));
      await t.pumpAndSettle();

      await t.enterText(find.byType(TextFormField).at(0), 'Bolão do setor');
      await t.tap(find.widgetWithText(FilledButton, 'Criar bolão'));
      await t.pumpAndSettle();

      expect(
        find.text('não há jogos nessas rodadas para este campeonato'),
        findsOneWidget,
      );
    });

    testWidgets('CC-1-08 bolão sem prazo aberto avisa antes de seguir', (
      t,
    ) async {
      final repo = _RepoFalso(
        criado: const Bolao(
          id: 'p1',
          name: 'Bolão de rodada velha',
          competitionCode: 'BSA',
          matchdayFrom: 1,
          matchdayTo: 3,
          totalMatches: 30,
          openMatches: 0,
        ),
      );
      await t.pumpWidget(_app(repo));
      await t.pumpAndSettle();

      await t.enterText(
        find.byType(TextFormField).at(0),
        'Bolão de rodada velha',
      );
      await _criar(t);

      // Bolão inteiro em rodadas que já começaram é criado com sucesso e é
      // inútil: ninguém palpita e o convite é recusado. O admin tem que
      // descobrir aqui, não pelo grupo reclamando.
      expect(find.text('Nenhum jogo com prazo aberto'), findsOneWidget);
      expect(find.textContaining('já começaram'), findsOneWidget);
      expect(
        find.textContaining('entrada fecha no primeiro jogo'),
        findsOneWidget,
      );
    });

    testWidgets(
      'CC-1-09 bolão com prazo aberto diz quantos jogos dão palpite',
      (t) async {
        await t.pumpWidget(_app(_RepoFalso()));
        await t.pumpAndSettle();

        await t.enterText(find.byType(TextFormField).at(0), 'Bolão do setor');
        await _criar(t);

        expect(find.textContaining('160 com prazo aberto'), findsOneWidget);
        expect(find.text('Nenhum jogo com prazo aberto'), findsNothing);
      },
    );
  });

  group('CC-2 regras na criação', () {
    testWidgets('CC-2-01 a etapa de regras vem recolhida, com o padrão dito', (
      t,
    ) async {
      await t.pumpWidget(_app(_RepoFalso()));
      await t.pumpAndSettle();

      // Obrigar o admin casual a configurar pontos é o que faz ele desistir.
      expect(find.text('Mudar a pontuação e o desempate'), findsOneWidget);
      expect(find.textContaining('A pontuação padrão é 10'), findsOneWidget);
      expect(find.text('Placar exato'), findsNothing);
    });

    testWidgets(
      'CC-2-02 o aviso de travamento não mostra asterisco de markdown',
      (t) async {
        await t.pumpWidget(_app(_RepoFalso()));
        await t.pumpAndSettle();

        // `Text` não interpreta markdown: `**assim**` aparecia literal na tela,
        // justamente no aviso que precisa ser lido.
        expect(find.textContaining('**'), findsNothing);
        expect(
          find.textContaining('As regras travam no primeiro palpite'),
          findsOneWidget,
        );
      },
    );

    testWidgets('CC-2-03 criar sem mexer nas regras manda o padrão do PRD', (
      t,
    ) async {
      final repo = _RepoFalso();
      await t.pumpWidget(_app(repo));
      await t.pumpAndSettle();

      await t.enterText(find.byType(TextFormField).at(0), 'Bolão do setor');
      await _criar(t);

      // O app manda 10/5/3 **explicitamente**, mesmo sem o admin abrir a
      // etapa de regras. O comentário do repositório fala em omitir o que não
      // foi mexido; na prática os valores vão sempre. Sem efeito visível — são
      // idênticos ao default do servidor — mas fica registrado, porque um dia o
      // default do servidor pode mudar e o bolão novo não acompanharia.
      expect(repo.enviado!['exato'], 10);
      expect(repo.enviado!['de'], 23);
      expect(repo.enviado!['ate'], 38);
    });

    testWidgets('CC-2-04 tabela inconsistente é barrada antes da API', (
      t,
    ) async {
      final repo = _RepoFalso();
      await t.pumpWidget(_app(repo));
      await t.pumpAndSettle();

      await t.enterText(find.byType(TextFormField).at(0), 'Bolão do setor');
      await t.tap(find.text('Mudar a pontuação e o desempate'));
      await t.pumpAndSettle();

      // Exato valendo menos que vencedor-e-saldo: bolão em que acertar mais
      // vale menos. O servidor aceita (0 a 100 por campo), o app não pode.
      final campoExato = find.widgetWithText(TextFormField, '10');
      await t.enterText(campoExato, '2');
      await t.pumpAndSettle();
      await t.tap(find.widgetWithText(FilledButton, 'Criar bolão'));
      await t.pumpAndSettle();

      expect(repo.enviado, isNull);
      expect(find.textContaining('placar exato'), findsWidgets);
    });
  });
}
