import 'package:bolao_mobile/core/api/api_client.dart';
import 'package:bolao_mobile/core/sessao/sessao_store.dart';
import 'package:bolao_mobile/features/auth/data/auth_repository.dart';
import 'package:bolao_mobile/features/auth/presentation/sessao_controller.dart';
import 'package:bolao_mobile/features/boloes/data/ciclo_vida_repository.dart';
import 'package:bolao_mobile/features/boloes/presentation/encerramento_dialog.dart';
import 'package:bolao_mobile/features/home/presentation/historico_page.dart';
import 'package:flutter/material.dart';
import 'package:flutter_riverpod/flutter_riverpod.dart';
import 'package:flutter_test/flutter_test.dart';

class _RepoFalso extends CicloVidaRepository {
  _RepoFalso({this.resultado, this.erroAoEncerrar, this.oHistorico = const []})
    : super(ApiClient(SessaoStore()));

  final Encerramento? resultado;
  final String? erroAoEncerrar;
  final List<ParticipacaoEncerrada> oHistorico;

  int normais = 0;
  int antecipados = 0;
  int leituras = 0;
  final List<String> arquivados = [];
  final List<String> desarquivados = [];

  @override
  Future<Encerramento> encerrar(String poolId) async {
    if (erroAoEncerrar != null) throw ApiError(erroAoEncerrar!, status: 409);
    normais++;
    return resultado!;
  }

  @override
  Future<Encerramento> lerEncerramento(String poolId) async {
    leituras++;
    return resultado!;
  }

  @override
  Future<Encerramento> encerrarAntecipado(String poolId) async {
    antecipados++;
    return resultado!;
  }

  @override
  Future<List<ParticipacaoEncerrada>> historico() async => oHistorico;

  @override
  Future<void> arquivar(String poolId) async => arquivados.add(poolId);

  @override
  Future<void> desarquivar(String poolId) async => desarquivados.add(poolId);
}

/// Histórico que falha por rede: "sem histórico" e "não deu para carregar" são
/// estados parecidos na tela e significam coisas opostas.
class _RepoHistoricoQuebrado extends _RepoFalso {
  _RepoHistoricoQuebrado() : super();

  @override
  Future<List<ParticipacaoEncerrada>> historico() async =>
      throw ApiError('sem conexão com o servidor');
}

class _SessaoFalsa extends SessaoNotifier {
  @override
  EstadoSessao build() => const Autenticado(
    Conta(id: 'ana', email: 'ana@teste.com', displayName: 'Ana'),
  );
}

Widget _app(_RepoFalso repo, Widget tela) => ProviderScope(
  overrides: [
    cicloVidaRepositoryProvider.overrideWithValue(repo),
    sessaoProvider.overrideWith(_SessaoFalsa.new),
  ],
  child: MaterialApp(home: Scaffold(body: tela)),
);

ParticipacaoEncerrada _p({
  required String nome,
  required String resultado,
  int? posicao,
  int? pontos,
  bool antecipado = false,
  bool arquivado = false,
  bool souAdmin = true,
  String id = 'p1',
}) => ParticipacaoEncerrada(
  poolId: id,
  nome: nome,
  encerradoEm: DateTime.utc(2026, 7, 15),
  antecipado: antecipado,
  arquivado: arquivado,
  resultado: resultado,
  posicao: posicao,
  pontos: pontos,
  souAdmin: souAdmin,
);

void main() {
  group('descreverResultado', () {
    test('nomeia campeão, co-campeão, posição e abandono', () {
      expect(descreverResultado('CAMPEAO', 1), 'campeão');
      expect(descreverResultado('CO_CAMPEAO', 1), 'co-campeão');
      // A ordinal precisa sair colada no número, sem chaves sobrando.
      expect(descreverResultado('PARTICIPOU', 4), '4º lugar');
      expect(descreverResultado('PARTICIPOU', null), 'participou');
      expect(
        descreverResultado('ABANDONADO', null),
        'você saiu antes do fim — sem posição final',
      );
    });
  });

  group('EncerramentoDialog', () {
    testWidgets('explica a diferença entre encerrar e encerrar agora', (
      t,
    ) async {
      await t.pumpWidget(
        _app(_RepoFalso(), const EncerramentoDialog(poolId: 'p1')),
      );
      await t.pumpAndSettle();

      expect(find.textContaining('tiverem resultado'), findsOneWidget);
      expect(find.textContaining('adiado sem data'), findsOneWidget);
    });

    testWidgets('jogo pendente recusa o encerramento normal com o motivo', (
      t,
    ) async {
      final repo = _RepoFalso(erroAoEncerrar: 'ainda há jogo sem resultado');
      await t.pumpWidget(_app(repo, const EncerramentoDialog(poolId: 'p1')));
      await t.pumpAndSettle();

      await t.tap(find.widgetWithText(FilledButton, 'Encerrar'));
      await t.pumpAndSettle();

      // É essa informação que faz o admin escolher entre esperar e antecipar.
      expect(find.text('ainda há jogo sem resultado'), findsOneWidget);
      expect(repo.normais, 0);
    });

    testWidgets('encerramento antecipado exige confirmação avisando o peso', (
      t,
    ) async {
      final repo = _RepoFalso(
        resultado: Encerramento(
          encerradoEm: DateTime.utc(2026, 8, 6),
          antecipado: true,
          campeoes: const [
            Campeao(userId: 'ana', displayName: 'Ana', points: 40),
          ],
        ),
      );
      await t.pumpWidget(_app(repo, const EncerramentoDialog(poolId: 'p1')));
      await t.pumpAndSettle();

      await t.tap(find.widgetWithText(TextButton, 'Encerrar agora'));
      await t.pumpAndSettle();
      expect(find.textContaining('não vale o mesmo'), findsOneWidget);
      // Sem a palavra "irreversível" o admin acha que dá para desfazer.
      expect(find.textContaining('irreversível'), findsOneWidget);

      await t.tap(find.widgetWithText(FilledButton, 'Encerrar agora'));
      await t.pumpAndSettle();

      expect(repo.antecipados, 1);
      expect(find.text('Campeão: Ana'), findsOneWidget);
      expect(find.textContaining('Encerrado antecipadamente'), findsOneWidget);
    });

    testWidgets('mostra a classificação final, não só o campeão', (t) async {
      final repo = _RepoFalso(
        resultado: Encerramento(
          encerradoEm: DateTime.utc(2026, 8, 6),
          antecipado: false,
          campeoes: const [
            Campeao(userId: 'ana', displayName: 'Ana', points: 40),
          ],
          rankingFinal: const [
            LinhaFinal(posicao: 1, displayName: 'Ana', points: 40),
            LinhaFinal(posicao: 2, displayName: 'Bruno', points: 22),
            LinhaFinal(posicao: 3, displayName: 'Carla', points: 9),
          ],
        ),
      );
      await t.pumpWidget(_app(repo, const EncerramentoDialog(poolId: 'p1')));
      await t.pumpAndSettle();

      await t.tap(find.widgetWithText(FilledButton, 'Encerrar'));
      await t.pumpAndSettle();

      // Quem ficou em 2º também quer ver onde parou.
      expect(find.text('Classificação final'), findsOneWidget);
      expect(find.text('2º  Bruno — 22 pts'), findsOneWidget);
      expect(find.text('3º  Carla — 9 pts'), findsOneWidget);
    });

    testWidgets('co-campeões aparecem juntos, sem inventar um vencedor', (
      t,
    ) async {
      final repo = _RepoFalso(
        resultado: Encerramento(
          encerradoEm: DateTime.utc(2026, 8, 6),
          antecipado: false,
          campeoes: const [
            Campeao(userId: 'ana', displayName: 'Ana', points: 40),
            Campeao(userId: 'bruno', displayName: 'Bruno', points: 40),
          ],
        ),
      );
      await t.pumpWidget(_app(repo, const EncerramentoDialog(poolId: 'p1')));
      await t.pumpAndSettle();

      await t.tap(find.widgetWithText(FilledButton, 'Encerrar'));
      await t.pumpAndSettle();

      expect(find.text('Co-campeões: Ana, Bruno'), findsOneWidget);
      expect(find.text('Você dividiu o título.'), findsOneWidget);
    });

    testWidgets('diz que nada fica congelado depois do encerramento', (
      t,
    ) async {
      final repo = _RepoFalso(
        resultado: Encerramento(
          encerradoEm: DateTime.utc(2026, 8, 6),
          antecipado: false,
          campeoes: const [
            Campeao(userId: 'bruno', displayName: 'Bruno', points: 50),
          ],
        ),
      );
      await t.pumpWidget(_app(repo, const EncerramentoDialog(poolId: 'p1')));
      await t.pumpAndSettle();

      await t.tap(find.widgetWithText(FilledButton, 'Encerrar'));
      await t.pumpAndSettle();

      // Correção de placar troca o campeão mesmo depois de encerrar.
      expect(find.textContaining('nada fica congelado'), findsOneWidget);
      // Ana não é campeã aqui: nada de parabenizar quem não ganhou.
      expect(find.text('Você é o campeão.'), findsNothing);
    });
  });

  group('HistoricoPage', () {
    testWidgets('conta os títulos e mostra a posição de cada bolão', (t) async {
      final repo = _RepoFalso(
        oHistorico: [
          _p(
            nome: 'Bolão do setor',
            resultado: 'CAMPEAO',
            posicao: 1,
            pontos: 90,
          ),
          _p(
            nome: 'Bolão da família',
            resultado: 'PARTICIPOU',
            posicao: 4,
            pontos: 40,
            id: 'p2',
          ),
        ],
      );
      await t.pumpWidget(_app(repo, const HistoricoPage()));
      await t.pumpAndSettle();

      expect(
        find.textContaining('1 título(s) em 2 bolão(ões)'),
        findsOneWidget,
      );
      expect(find.textContaining('campeão · 90 pts'), findsOneWidget);
      expect(find.textContaining('4º lugar · 40 pts'), findsOneWidget);
    });

    testWidgets('quem abandonou não recebe posição final', (t) async {
      final repo = _RepoFalso(
        oHistorico: [_p(nome: 'Bolão abandonado', resultado: 'ABANDONADO')],
      );
      await t.pumpWidget(_app(repo, const HistoricoPage()));
      await t.pumpAndSettle();

      expect(find.textContaining('sem posição final'), findsOneWidget);
      expect(find.textContaining('nenhum título ainda'), findsOneWidget);
    });

    testWidgets('encerramento antecipado é distinguido no histórico', (
      t,
    ) async {
      final repo = _RepoFalso(
        oHistorico: [
          _p(
            nome: 'Bolão travado',
            resultado: 'CAMPEAO',
            posicao: 1,
            pontos: 12,
            antecipado: true,
          ),
        ],
      );
      await t.pumpWidget(_app(repo, const HistoricoPage()));
      await t.pumpAndSettle();

      // Título de bolão encerrado na rodada 5 não vale o mesmo.
      expect(find.textContaining('(antecipadamente)'), findsOneWidget);
    });

    testWidgets('arquivar e desarquivar são reversíveis na mesma tela', (
      t,
    ) async {
      final repo = _RepoFalso(
        oHistorico: [
          _p(nome: 'Bolão velho', resultado: 'PARTICIPOU', posicao: 3),
        ],
      );
      await t.pumpWidget(_app(repo, const HistoricoPage()));
      await t.pumpAndSettle();

      // As ações de admin agora vivem num menu, ao lado de "recriar temporada".
      await t.tap(find.byType(PopupMenuButton<String>));
      await t.pumpAndSettle();
      await t.tap(find.text('Arquivar'));
      await t.pumpAndSettle();
      expect(repo.arquivados, ['p1']);
    });

    testWidgets('participante comum não recebe a ação de arquivar', (t) async {
      final repo = _RepoFalso(
        oHistorico: [
          _p(
            nome: 'Bolão de outro',
            resultado: 'PARTICIPOU',
            posicao: 5,
            souAdmin: false,
          ),
        ],
      );
      await t.pumpWidget(_app(repo, const HistoricoPage()));
      await t.pumpAndSettle();

      // O servidor recusa com 403: oferecer aqui era prometer o que não dá.
      expect(find.byType(PopupMenuButton<String>), findsNothing);
    });

    testWidgets('histórico vazio explica quando ele se preenche', (t) async {
      await t.pumpWidget(_app(_RepoFalso(), const HistoricoPage()));
      await t.pumpAndSettle();

      expect(find.text('Nenhum bolão encerrado ainda'), findsOneWidget);
    });
  });

  group('CV-1 leitura do encerramento (bolão já encerrado)', () {
    final resultado = Encerramento(
      encerradoEm: DateTime.utc(2026, 8, 6),
      antecipado: false,
      campeoes: const [
        Campeao(userId: 'bruno', displayName: 'Bruno', points: 40),
      ],
      rankingFinal: const [
        LinhaFinal(posicao: 1, displayName: 'Bruno', points: 40),
        LinhaFinal(posicao: 2, displayName: 'Ana', points: 22),
      ],
    );

    testWidgets('abre direto no resultado, sem oferecer encerrar', (t) async {
      final repo = _RepoFalso(resultado: resultado);
      await t.pumpWidget(
        _app(repo, const EncerramentoDialog(poolId: 'p1', jaEncerrado: true)),
      );
      await t.pumpAndSettle();

      // Leitura, não ação: o campeão precisa continuar alcançável depois do
      // encerramento, inclusive para quem não é admin.
      expect(repo.leituras, 1);
      expect(repo.normais, 0);
      expect(repo.antecipados, 0);
      expect(find.text('Campeão: Bruno'), findsOneWidget);
      expect(find.textContaining('1º  Bruno'), findsOneWidget);
      expect(find.widgetWithText(TextButton, 'Encerrar agora'), findsNothing);
      expect(find.widgetWithText(FilledButton, 'Encerrar'), findsNothing);
      expect(find.widgetWithText(TextButton, 'Fechar'), findsOneWidget);
    });

    testWidgets('não promete resultado congelado', (t) async {
      await t.pumpWidget(
        _app(
          _RepoFalso(resultado: resultado),
          const EncerramentoDialog(poolId: 'p1', jaEncerrado: true),
        ),
      );
      await t.pumpAndSettle();

      // Correção posterior troca o campeão (decisão de 03/08) — a tela não pode
      // dizer "definitivo".
      expect(find.textContaining('nada fica congelado'), findsOneWidget);
    });

    testWidgets('falha na leitura mostra o motivo, não tela vazia', (t) async {
      final repo = _RepoFalsoSemResultado();
      await t.pumpWidget(
        _app(repo, const EncerramentoDialog(poolId: 'p1', jaEncerrado: true)),
      );
      await t.pumpAndSettle();

      expect(find.text('sem conexão com o servidor'), findsOneWidget);
      expect(find.textContaining('Não foi possível carregar'), findsOneWidget);
      // Sem retry, "fecha e abre de novo" seria a única saída.
      expect(find.widgetWithText(TextButton, 'Tentar de novo'), findsOneWidget);
      expect(find.byType(CircularProgressIndicator), findsNothing);
    });
  });

  group('CV-5-09 histórico com falha de rede', () {
    testWidgets('mostra o motivo e permite tentar de novo', (t) async {
      await t.pumpWidget(_app(_RepoHistoricoQuebrado(), const HistoricoPage()));
      await t.pumpAndSettle();

      expect(find.text('sem conexão com o servidor'), findsOneWidget);
      expect(find.text('Tentar de novo'), findsOneWidget);
      // Confundir com "sem histórico" faria a pessoa achar que perdeu os
      // títulos dela.
      expect(find.textContaining('quando um bolão encerrar'), findsNothing);
    });
  });
}

/// Leitura que falha: o diálogo não pode ficar num spinner eterno nem mostrar
/// "sem campeão" por causa de rede.
class _RepoFalsoSemResultado extends _RepoFalso {
  @override
  Future<Encerramento> lerEncerramento(String poolId) async =>
      throw ApiError('sem conexão com o servidor');
}
