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/boloes_repository.dart';
import 'package:bolao_mobile/features/boloes/data/boloes_repository_http.dart';
import 'package:bolao_mobile/features/boloes/presentation/rodada_page.dart';
import 'package:bolao_mobile/features/home/presentation/home_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: a home não pode depender de rede para ser testada, e
/// `meus()` é o único método que ela chama ao abrir.
class _RepoFalso extends BoloesRepositoryHttp {
  _RepoFalso(this.resposta, {this.erro}) : super(ApiClient(SessaoStore()));

  final List<BolaoResumo> resposta;
  final String? erro;

  /// Quantas vezes a lista foi buscada. Importa porque voltar de um bolão tem
  /// que recarregar: pode ter entrado gente ou o bolão pode ter encerrado.
  int chamadas = 0;

  @override
  Future<List<BolaoResumo>> meus() async {
    chamadas++;
    if (erro != null) throw ApiError(erro!, semConexao: true);
    return resposta;
  }
}

/// Registra o token que chegou ao repositório: o que o servidor recebe é o que
/// decide se o convidado entra ou lê "convite inválido".
class _RepoConvite extends BoloesRepositoryHttp {
  _RepoConvite() : super(ApiClient(SessaoStore()));

  String? recebido;

  @override
  Future<List<BolaoResumo>> meus() async => [];

  @override
  Future<({String poolId, String poolName, bool jaParticipava})>
  entrarPorConvite(String token) async {
    recebido = token;
    return (poolId: 'p9', poolName: 'Bolão do link', jaParticipava: false);
  }
}

/// Sessão fixa: sem isto o `build()` real tentaria ler o secure storage, que
/// não existe no ambiente de teste.
class _SessaoFalsa extends SessaoNotifier {
  @override
  EstadoSessao build() => const Autenticado(
    Conta(id: 'u1', email: 'ana@teste.com', displayName: 'Ana'),
  );
}

BolaoResumo _bolao({
  String id = 'p1',
  String name = 'Bolão do setor',
  int membros = 4,
  bool admin = false,
  bool encerrado = false,
}) => BolaoResumo(
  id: id,
  name: name,
  matchdayFrom: 20,
  matchdayTo: 21,
  totalMembers: membros,
  isAdmin: admin,
  encerrado: encerrado,
);

Widget _app(BoloesRepository repo) => ProviderScope(
  overrides: [
    boloesRepositoryProvider.overrideWithValue(repo),
    sessaoProvider.overrideWith(_SessaoFalsa.new),
  ],
  child: const MaterialApp(home: HomePage()),
);

void main() {
  // A home grava e lê o cache da lista; sem o plugin falso o `getInstance`
  // nunca resolve e o `pumpAndSettle` estoura.
  setUp(() => SharedPreferences.setMockInitialValues({}));

  group('HomePage', () {
    testWidgets('saúda pelo nome do ranking', (t) async {
      await t.pumpWidget(_app(_RepoFalso([_bolao()])));
      await t.pumpAndSettle();

      expect(find.text('Olá, Ana'), findsOneWidget);
    });

    testWidgets('CC-10-09 a linha traz nome, rodadas e participantes', (t) async {
      await t.pumpWidget(_app(_RepoFalso([_bolao()])));
      await t.pumpAndSettle();

      expect(find.text('Bolão do setor'), findsOneWidget);
      // O participante em dois bolões reconhece o dele por estes dados.
      expect(find.text('Rodadas 20–21 · 4 participantes'), findsOneWidget);
    });

    testWidgets('concorda em português no singular', (t) async {
      await t.pumpWidget(_app(_RepoFalso([_bolao(membros: 1)])));
      await t.pumpAndSettle();

      expect(find.text('Rodadas 20–21 · 1 participante'), findsOneWidget);
    });

    testWidgets('marca o bolão que a pessoa administra', (t) async {
      await t.pumpWidget(
        _app(
          _RepoFalso([_bolao(admin: true), _bolao(id: 'p2', name: 'Outro')]),
        ),
      );
      await t.pumpAndSettle();

      // Admin é permanente e muda o que dá para fazer lá dentro.
      expect(find.text('admin'), findsOneWidget);
    });

    testWidgets('CV-1-10 o estado de encerrado aparece na lista', (t) async {
      await t.pumpWidget(_app(_RepoFalso([_bolao(encerrado: true)])));
      await t.pumpAndSettle();

      // Encerrado continua consultável: é onde ficam campeão e extrato.
      expect(find.textContaining('encerrado'), findsOneWidget);
    });

    testWidgets('CC-10-10 lista vazia oferece criar e entrar por convite', (t) async {
      await t.pumpWidget(_app(_RepoFalso([])));
      await t.pumpAndSettle();

      // Metade de quem chega foi convidado e não quer criar bolão nenhum.
      expect(find.text('Você ainda não está em nenhum bolão'), findsOneWidget);
      expect(find.text('Entrar por convite'), findsOneWidget);
      expect(find.text('Criar bolão'), findsOneWidget);
    });

    testWidgets('CC-10-11 erro de rede é distinto de lista vazia', (
      t,
    ) async {
      await t.pumpWidget(
        _app(_RepoFalso([], erro: 'não foi possível conectar')),
      );
      await t.pumpAndSettle();

      expect(find.text('não foi possível conectar'), findsOneWidget);
      expect(find.text('Tentar de novo'), findsOneWidget);
      // Erro de rede não pode virar "você não tem bolão": o participante
      // acharia que perdeu o bolão e o palpite dele.
      expect(find.text('Você ainda não está em nenhum bolão'), findsNothing);
    });

    testWidgets('pede o código ao entrar por convite', (t) async {
      await t.pumpWidget(_app(_RepoFalso([])));
      await t.pumpAndSettle();

      await t.tap(find.text('Entrar por convite'));
      await t.pumpAndSettle();

      expect(
        find.text('Cole o código que o organizador mandou:'),
        findsOneWidget,
      );
    });
  });

  group('Entrada por código colado', () {
    Future<void> colar(WidgetTester t, _RepoConvite repo, String texto) async {
      await t.pumpWidget(_app(repo));
      await t.pumpAndSettle();
      await t.tap(find.text('Entrar por convite'));
      await t.pumpAndSettle();
      await t.enterText(find.byType(TextField), texto);
      await t.tap(find.text('Entrar'));
      await t.pumpAndSettle();
    }

    testWidgets('CC-4-12 colar o link inteiro entra no bolão', (t) async {
      // O que circula no grupo é o link, não o código solto. Antes da correção
      // o servidor não achava o hash e respondia "convite inválido ou já
      // substituído" — diagnóstico errado sobre um convite válido.
      final repo = _RepoConvite();
      await colar(t, repo, 'bolao://bolao.app/convite/tok-123');

      expect(repo.recebido, 'tok-123');
      expect(
        find.textContaining('Você entrou em Bolão do link'),
        findsOneWidget,
      );
    });

    testWidgets('CC-4-13 colar a mensagem inteira também entra', (t) async {
      final repo = _RepoConvite();
      await colar(t, repo, 'entra aí: bolao://bolao.app/convite/tok-123 vlw');

      expect(repo.recebido, 'tok-123');
    });

    testWidgets('código solto continua funcionando', (t) async {
      final repo = _RepoConvite();
      await colar(t, repo, 'tok-123');

      expect(repo.recebido, 'tok-123');
    });
  });

  group('CC-10 FE-5 abrir o bolão e voltar', () {
    testWidgets('CC-10-12 voltar do bolão recarrega a lista', (
      t,
    ) async {
      final repo = _RepoFalso([_bolao()]);
      await t.pumpWidget(_app(repo));
      await t.pumpAndSettle();
      expect(repo.chamadas, 1);

      await t.tap(find.text('Bolão do setor'));
      // A tela do bolão busca rodada/ranking, que este repositório não stuba:
      // fica carregando, e é o suficiente para provar que navegou.
      await t.pump();
      await t.pump(const Duration(milliseconds: 300));
      expect(find.text('Bolão do setor'), findsWidgets);

      // Volta para a home.
      final nav = tester(t);
      nav.pop();
      await t.pump();
      await t.pump(const Duration(milliseconds: 300));

      // Recarregou: sem isso a lista mostraria contagem e estado velhos.
      expect(repo.chamadas, 2);
    });
  });
}

/// `NavigatorState` da árvore corrente, para simular o botão de voltar.
NavigatorState tester(WidgetTester t) =>
    t.state<NavigatorState>(find.byType(Navigator).first);
