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/ranking_repository.dart';
import 'package:bolao_mobile/features/boloes/presentation/ranking_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 RankingRepository {
  _RepoFalso(this.ranking, {this.rodada}) : super(ApiClient(SessaoStore()));

  final Ranking ranking;
  final RankingRodada? rodada;

  final List<int> offsetsPedidos = [];

  @override
  Future<Ranking> geral(String poolId, {int limit = 50, int offset = 0}) async {
    offsetsPedidos.add(offset);
    if (offset == 0) return ranking;
    // Página seguinte: devolve o resto, mantendo o mesmo total.
    return Ranking(total: ranking.total, linhas: paginaSeguinte ?? const []);
  }

  /// Linhas devolvidas quando a tela pede a página seguinte.
  List<LinhaRanking>? paginaSeguinte;

  @override
  Future<RankingRodada> daRodada(String poolId, int matchday) async {
    if (rodada == null) throw ApiError('sem ranking de rodada no teste');
    return rodada!;
  }
}

/// Ranking que falha por rede. Serve para ler o que ficou guardado — o uso real
/// do bolão arquivado é rever meses depois, muitas vezes para provar algo no
/// grupo, e isso não pode depender de conexão.
class _RepoSemRede extends RankingRepository {
  _RepoSemRede() : super(ApiClient(SessaoStore()));

  @override
  Future<Ranking> geral(
    String poolId, {
    int limit = 50,
    int offset = 0,
  }) async => throw ApiError('sem conexão com o servidor', semConexao: true);

  @override
  Future<RankingRodada> daRodada(String poolId, int matchday) async =>
      throw ApiError('sem conexão com o servidor', semConexao: true);
}

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

LinhaRanking _linha({
  required String id,
  required String nome,
  required int posicao,
  required int pontos,
  required int exatos,
  String criterio = 'PONTOS',
  bool dividida = false,
}) => LinhaRanking(
  userId: id,
  displayName: nome,
  position: posicao,
  points: pontos,
  exactScores: exatos,
  decidedBy: criterio,
  sharedPosition: dividida,
);

Widget _appCom(RankingRepository repo) => ProviderScope(
  overrides: [
    rankingRepositoryProvider.overrideWithValue(repo),
    sessaoProvider.overrideWith(_SessaoFalsa.new),
  ],
  child: const MaterialApp(
    home: RankingPage(poolId: 'p1', matchday: 20, rodadaFinal: 22),
  ),
);

Widget _app(Ranking ranking, {RankingRodada? rodada}) => ProviderScope(
  overrides: [
    rankingRepositoryProvider.overrideWithValue(
      _RepoFalso(ranking, rodada: rodada),
    ),
    sessaoProvider.overrideWith(_SessaoFalsa.new),
  ],
  child: const MaterialApp(
    home: RankingPage(poolId: 'p1', matchday: 20, rodadaFinal: 22),
  ),
);

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('RankingPage', () {
    testWidgets('marca a linha de quem está olhando', (t) async {
      await t.pumpWidget(
        _app(
          Ranking(
            total: 2,
            linhas: [
              _linha(
                id: 'bruno',
                nome: 'Bruno',
                posicao: 1,
                pontos: 30,
                exatos: 2,
              ),
              _linha(id: 'ana', nome: 'Ana', posicao: 2, pontos: 22, exatos: 1),
            ],
          ),
        ),
      );
      await t.pumpAndSettle();

      // Sem isto o participante precisa procurar o próprio nome na lista.
      expect(find.text('Ana (você)'), findsOneWidget);
      expect(find.text('Bruno'), findsOneWidget);
    });

    testWidgets('mostra o critério do desempate também para quem lidera', (
      t,
    ) async {
      await t.pumpWidget(
        _app(
          Ranking(
            total: 2,
            linhas: [
              _linha(
                id: 'ana',
                nome: 'Ana',
                posicao: 1,
                pontos: 22,
                exatos: 1,
                criterio: 'EXACT_SCORES',
              ),
              _linha(
                id: 'bruno',
                nome: 'Bruno',
                posicao: 2,
                pontos: 22,
                exatos: 0,
                criterio: 'EXACT_SCORES',
              ),
            ],
          ),
        ),
      );
      await t.pumpAndSettle();

      // O AC de PR-5 vale para os dois lados: quem está na frente por
      // desempate é justamente quem tem que explicar por quê.
      expect(find.text('desempate: mais placares exatos'), findsNWidgets(2));
    });

    testWidgets('não fala de desempate quando a diferença foi de pontos', (
      t,
    ) async {
      await t.pumpWidget(
        _app(
          Ranking(
            total: 2,
            linhas: [
              _linha(
                id: 'bruno',
                nome: 'Bruno',
                posicao: 1,
                pontos: 30,
                exatos: 2,
              ),
              _linha(id: 'ana', nome: 'Ana', posicao: 2, pontos: 22, exatos: 1),
            ],
          ),
        ),
      );
      await t.pumpAndSettle();

      // Sugerir desempate onde não houve empate inventa uma disputa.
      expect(find.textContaining('desempate'), findsNothing);
      expect(find.text('por pontos'), findsNothing);
    });

    testWidgets('sinaliza posição dividida entre co-campeões', (t) async {
      await t.pumpWidget(
        _app(
          Ranking(
            total: 2,
            linhas: [
              _linha(
                id: 'ana',
                nome: 'Ana',
                posicao: 1,
                pontos: 22,
                exatos: 1,
                criterio: 'EMPATE_PERSISTE',
                dividida: true,
              ),
              _linha(
                id: 'bruno',
                nome: 'Bruno',
                posicao: 1,
                pontos: 22,
                exatos: 1,
                criterio: 'EMPATE_PERSISTE',
                dividida: true,
              ),
            ],
          ),
        ),
      );
      await t.pumpAndSettle();

      expect(find.text('1º'), findsNWidgets(2));
      expect(find.text('empate mantido — dividem a posição'), findsNWidgets(2));
      expect(find.byIcon(Icons.people_outline), findsNWidgets(2));
    });

    testWidgets('posição de dois dígitos com empate não estoura o layout', (
      t,
    ) async {
      await t.pumpWidget(
        _app(
          Ranking(
            total: 2,
            linhas: [
              _linha(
                id: 'ana',
                nome: 'Ana',
                posicao: 10,
                pontos: 22,
                exatos: 1,
                criterio: 'EMPATE_PERSISTE',
                dividida: true,
              ),
              _linha(
                id: 'bruno',
                nome: 'Bruno',
                posicao: 10,
                pontos: 22,
                exatos: 1,
                criterio: 'EMPATE_PERSISTE',
                dividida: true,
              ),
            ],
          ),
        ),
      );
      await t.pumpAndSettle();

      // Overflow aqui aparece como listra amarela no fim do campeonato, que é
      // quando a tela mais é olhada.
      expect(t.takeException(), isNull);
      expect(find.text('10º'), findsNWidgets(2));
    });

    testWidgets('acessibilidade: fonte grande não estoura a linha', (t) async {
      // Quem aumenta a fonte do sistema é exatamente quem não vai conseguir
      // ler uma linha cortada.
      t.platformDispatcher.textScaleFactorTestValue = 2.0;
      addTearDown(t.platformDispatcher.clearTextScaleFactorTestValue);

      await t.pumpWidget(
        _app(
          Ranking(
            total: 2,
            linhas: [
              _linha(
                id: 'ana',
                nome: 'Ana',
                posicao: 10,
                pontos: 22,
                exatos: 1,
                criterio: 'EMPATE_PERSISTE',
                dividida: true,
              ),
              _linha(
                id: 'bruno',
                nome: 'Bruno',
                posicao: 12,
                pontos: 20,
                exatos: 0,
              ),
            ],
          ),
        ),
      );
      await t.pumpAndSettle();

      expect(t.takeException(), isNull);
    });

    testWidgets('alterna entre geral e rodada, mantendo os dois separados', (
      t,
    ) async {
      await t.pumpWidget(
        _app(
          Ranking(
            total: 1,
            linhas: [
              _linha(
                id: 'ana',
                nome: 'Ana',
                posicao: 1,
                pontos: 100,
                exatos: 9,
              ),
            ],
          ),
          rodada: RankingRodada(
            matchday: 20,
            totalJogos: 10,
            apurados: 10,
            parcial: false,
            linhas: [
              _linha(
                id: 'bruno',
                nome: 'Bruno',
                posicao: 1,
                pontos: 13,
                exatos: 1,
              ),
            ],
          ),
        ),
      );
      await t.pumpAndSettle();

      expect(find.text('100 pts'), findsOneWidget);

      await t.tap(find.text('Rodada'));
      await t.pumpAndSettle();

      // Quem foi bem na rodada e mal no campeonato precisa ver as duas coisas.
      expect(find.text('13 pts'), findsOneWidget);
      expect(find.text('100 pts'), findsNothing);
    });

    testWidgets('rodada parcial avisa quantos jogos foram apurados', (t) async {
      await t.pumpWidget(
        _app(
          Ranking(
            total: 1,
            linhas: [
              _linha(id: 'ana', nome: 'Ana', posicao: 1, pontos: 10, exatos: 1),
            ],
          ),
          rodada: RankingRodada(
            matchday: 20,
            totalJogos: 10,
            apurados: 3,
            parcial: true,
            linhas: [
              _linha(id: 'ana', nome: 'Ana', posicao: 1, pontos: 10, exatos: 1),
            ],
          ),
        ),
      );
      await t.pumpAndSettle();

      await t.tap(find.text('Rodada'));
      await t.pumpAndSettle();

      // Ranking parcial parecendo final produz comemoração e briga depois.
      expect(
        find.textContaining('Rodada parcial: 3 de 10 jogos apurados'),
        findsOneWidget,
      );
    });

    testWidgets('o seletor abre qualquer rodada do intervalo do bolão', (
      t,
    ) async {
      await t.pumpWidget(
        _app(
          Ranking(
            total: 1,
            linhas: [
              _linha(id: 'ana', nome: 'Ana', posicao: 1, pontos: 30, exatos: 3),
            ],
          ),
          rodada: RankingRodada(
            matchday: 20,
            totalJogos: 10,
            apurados: 10,
            parcial: false,
            linhas: [
              _linha(id: 'ana', nome: 'Ana', posicao: 1, pontos: 10, exatos: 1),
            ],
          ),
        ),
      );
      await t.pumpAndSettle();

      await t.tap(find.text('Rodada'));
      await t.pumpAndSettle();

      // Quem quer ver a rodada 22 não deveria depender de qual rodada o bolão
      // começou: o endpoint aceita qualquer uma.
      await t.tap(find.byType(DropdownButton<int>));
      await t.pumpAndSettle();
      expect(find.text('22').hitTestable(), findsOneWidget);
    });

    testWidgets('PR-3 FE-3 rola para buscar a página seguinte', (t) async {
      // 60 no total, 50 na primeira página: bolão de empresa passa de 50, e sem
      // paginação quem está da metade para baixo não aparece.
      final primeira = [
        for (var i = 0; i < 50; i++)
          _linha(
            id: 'u$i',
            nome: 'P$i',
            posicao: i + 1,
            pontos: 100 - i,
            exatos: 1,
          ),
      ];
      final repo = _RepoFalso(Ranking(total: 60, linhas: primeira));
      repo.paginaSeguinte = [
        for (var i = 50; i < 60; i++)
          _linha(
            id: 'u$i',
            nome: 'P$i',
            posicao: i + 1,
            pontos: 40 - i,
            exatos: 0,
          ),
      ];

      await t.pumpWidget(
        ProviderScope(
          overrides: [
            rankingRepositoryProvider.overrideWithValue(repo),
            sessaoProvider.overrideWith(_SessaoFalsa.new),
          ],
          child: const MaterialApp(
            home: RankingPage(poolId: 'p1', matchday: 20, rodadaFinal: 22),
          ),
        ),
      );
      await t.pumpAndSettle();

      expect(repo.offsetsPedidos, [0]);
      expect(find.text('P59'), findsNothing);

      // Rola até o fim: a próxima página entra sem apagar o que já estava.
      await t.drag(find.byType(ListView), const Offset(0, -6000));
      await t.pumpAndSettle();

      expect(repo.offsetsPedidos, [0, 50]);
      await t.drag(find.byType(ListView), const Offset(0, -3000));
      await t.pumpAndSettle();
      expect(find.text('P59'), findsOneWidget);
    });

    testWidgets('não pede página nenhuma quando a lista já está completa', (
      t,
    ) async {
      final repo = _RepoFalso(
        Ranking(
          total: 2,
          linhas: [
            _linha(id: 'ana', nome: 'Ana', posicao: 1, pontos: 30, exatos: 3),
            _linha(
              id: 'bruno',
              nome: 'Bruno',
              posicao: 2,
              pontos: 10,
              exatos: 0,
            ),
          ],
        ),
      );

      await t.pumpWidget(
        ProviderScope(
          overrides: [
            rankingRepositoryProvider.overrideWithValue(repo),
            sessaoProvider.overrideWith(_SessaoFalsa.new),
          ],
          child: const MaterialApp(
            home: RankingPage(poolId: 'p1', matchday: 20, rodadaFinal: 22),
          ),
        ),
      );
      await t.pumpAndSettle();
      await t.drag(find.byType(ListView), const Offset(0, -2000));
      await t.pumpAndSettle();

      // Pedir página vazia para sempre é o erro clássico de scroll infinito.
      expect(repo.offsetsPedidos, [0]);
    });
  });

  group('PR-3/PR-4 estados sem apuração', () {
    testWidgets('PR-3-10 ninguém pontuou: todos com zero e o motivo dito', (
      t,
    ) async {
      await t.pumpWidget(
        _app(
          Ranking(
            total: 2,
            linhas: [
              _linha(id: 'ana', nome: 'Ana', posicao: 1, pontos: 0, exatos: 0),
              _linha(
                id: 'bruno',
                nome: 'Bruno',
                posicao: 1,
                pontos: 0,
                exatos: 0,
              ),
            ],
          ),
        ),
      );
      await t.pumpAndSettle();

      // Lista de zeros sem explicação parece defeito de apuração.
      expect(
        find.text('Nenhuma rodada apurada ainda — todos com zero.'),
        findsOneWidget,
      );
      // Quem está olhando continua marcado, mesmo com todo mundo em zero.
      expect(find.text('Ana (você)'), findsOneWidget);
    });

    testWidgets('PR-3-10b ranking sem nenhuma linha explica quando aparece', (
      t,
    ) async {
      await t.pumpWidget(_app(const Ranking(total: 0, linhas: [])));
      await t.pumpAndSettle();

      expect(find.text('Ranking ainda vazio'), findsOneWidget);
      expect(
        find.textContaining('primeira rodada for apurada'),
        findsOneWidget,
      );
    });

    testWidgets('PR-4-06 rodada sem jogo apurado tem mensagem própria', (
      t,
    ) async {
      await t.pumpWidget(
        _app(
          Ranking(
            total: 1,
            linhas: [
              _linha(id: 'ana', nome: 'Ana', posicao: 1, pontos: 7, exatos: 1),
            ],
          ),
          rodada: const RankingRodada(
            matchday: 20,
            totalJogos: 10,
            apurados: 0,
            parcial: true,
            linhas: [],
          ),
        ),
      );
      await t.pumpAndSettle();

      await t.tap(find.text('Rodada'));
      await t.pumpAndSettle();

      // Texto próprio da rodada: "ranking vazio" no geral significa outra coisa.
      expect(find.text('Rodada 20 sem classificação'), findsOneWidget);
      expect(
        find.textContaining('Nenhum jogo desta rodada foi apurado'),
        findsOneWidget,
      );
    });
  });

  group('CV-6-09 ranking final do arquivado, sem conexão', () {
    testWidgets('mostra o que ficou guardado, rotulado com a idade', (t) async {
      // Primeira abertura, com rede: grava o ranking final.
      await t.pumpWidget(
        _app(
          Ranking(
            total: 2,
            linhas: [
              _linha(
                id: 'bruno',
                nome: 'Bruno',
                posicao: 1,
                pontos: 41,
                exatos: 3,
              ),
              _linha(id: 'ana', nome: 'Ana', posicao: 2, pontos: 22, exatos: 1),
            ],
          ),
        ),
      );
      await t.pumpAndSettle();
      expect(find.text('41 pts'), findsOneWidget);

      // Desmonta e reabre sem rede: é o cenário de rever o bolão arquivado.
      await t.pumpWidget(const MaterialApp(home: SizedBox.shrink()));
      await t.pumpAndSettle();
      await t.pumpWidget(_appCom(_RepoSemRede()));
      await t.pumpAndSettle();

      expect(find.text('41 pts'), findsOneWidget);
      // Rotulado: sem isso um ranking velho é lido como o de agora.
      expect(find.textContaining('guardada no aparelho'), findsOneWidget);
    });

    testWidgets('sem nada guardado, explica em vez de mostrar lista vazia', (
      t,
    ) async {
      await t.pumpWidget(_appCom(_RepoSemRede()));
      await t.pumpAndSettle();

      expect(find.text('sem conexão com o servidor'), findsOneWidget);
      expect(find.text('Ranking ainda vazio'), findsNothing);
    });
  });
}
