import 'package:bolao_mobile/core/firebase/firebase_config.dart';
import 'package:bolao_mobile/core/firebase/relogio.dart';
import 'package:bolao_mobile/features/auth/data/auth_repository.dart';
import 'package:bolao_mobile/features/auth/data/auth_repository_firebase.dart';
import 'package:bolao_mobile/features/boloes/data/boloes_repository.dart'
    show Competicao;
import 'package:bolao_mobile/features/boloes/data/boloes_repository_firebase.dart';
import 'package:bolao_mobile/features/boloes/data/participantes_repository_firebase.dart';
import 'package:bolao_mobile/features/boloes/data/ranking_repository_firebase.dart';
import 'package:bolao_mobile/core/sessao/sessao_store.dart';
import 'package:cloud_firestore/cloud_firestore.dart';
import 'package:firebase_auth/firebase_auth.dart';
import 'package:flutter_test/flutter_test.dart';
import 'package:integration_test/integration_test.dart';

/// `MG-7` frente 3 — o app contra o Firebase de verdade, em device.
///
/// Widget test com repositório falso prova que a tela reage; **este** prova que
/// a camada de dados fala com o backend novo. É onde a promessa da story se
/// verifica: mesma regra, mesmo resultado, backend diferente.
///
/// Roda contra os emuladores, nunca contra o projeto real:
///
/// ```
/// flutter test integration_test/fluxo_firebase_test.dart \
///   --dart-define=FIREBASE_EMULADOR=true
/// ```
void main() {
  IntegrationTestWidgetsFlutterBinding.ensureInitialized();

  late AuthRepository auth;
  late BoloesRepositoryFirebase boloes;
  late ParticipantesRepositoryFirebase participantes;
  late RankingRepositoryFirebase ranking;
  late FirebaseFirestore db;

  /// Campeonatos, lidos **uma vez por execução**: cada leitura gasta uma
  /// requisição da cota de 10 por minuto da fonte oficial, e este teste fala com
  /// ela de verdade. Rodar o arquivo várias vezes no mesmo minuto faz o
  /// adaptador recusar antes de gastar — que é ele funcionando, não falhando.
  List<Competicao>? cacheDeCompeticoes;
  Future<List<Competicao>> competicoesUmaVez() async =>
      cacheDeCompeticoes ??= await boloes.competicoes();

  final sufixo = DateTime.now().millisecondsSinceEpoch;
  final emailA = 'ana$sufixo@teste.com';
  final emailB = 'bruno$sufixo@teste.com';
  const senha = 'senhaforte123';

  setUpAll(() async {
    await iniciarFirebase();
    expect(
      FirebaseConfig.usarEmulador,
      isTrue,
      reason:
          'rode com --dart-define=FIREBASE_EMULADOR=true; '
          'este teste **não** pode tocar o projeto real',
    );

    db = FirebaseFirestore.instance;
    final sessao = SessaoStore();
    auth = AuthRepositoryFirebase(sessao);
    boloes = BoloesRepositoryFirebase(RelogioDoServidor());
    participantes = ParticipantesRepositoryFirebase();
    ranking = RankingRepositoryFirebase();

    // Começa deslogado, sempre. O Firebase persiste a sessão no aparelho, então
    // sem isto uma execução herdava o login da anterior — e o teste passava por
    // um motivo que não era o dele.
    await FirebaseAuth.instance.signOut();
  });

  testWidgets('MG-7-79 cadastro, bolão, convite, palpite e ranking', (t) async {
    // --- cadastro (`AC-1`) -------------------------------------------------
    final ana = await auth.cadastrar(
      email: emailA,
      senha: senha,
      nomeExibicao: 'Ana',
    );
    expect(ana.id, isNotEmpty);

    // O perfil é criado por gatilho, não pelo app: o cliente não escreve o
    // próprio nome na lista de participantes.
    var perfil = await db.doc('usuarios/${ana.id}').get();
    for (var i = 0; i < 40 && !perfil.exists; i++) {
      await Future<void>.delayed(const Duration(milliseconds: 500));
      perfil = await db.doc('usuarios/${ana.id}').get();
    }
    expect(
      perfil.exists,
      isTrue,
      reason: 'o gatilho de cadastro não criou o perfil',
    );

    // --- criar bolão (`CC-1`) ---------------------------------------------
    final competicoes = await competicoesUmaVez();
    expect(competicoes, isNotEmpty);

    final rodada = (competicoes.first.rodadaAtual ?? 1) + 1;
    final bolao = await boloes.criar(
      nome: 'Bolão do device $sufixo',
      competitionCode: competicoes.first.code,
      rodadaInicial: rodada,
      rodadaFinal: rodada,
    );
    expect(bolao.totalMatches, greaterThan(0));

    // --- a tela inicial enxerga o bolão (`CC-10`) --------------------------
    var meus = await boloes.meus();
    for (var i = 0; i < 40 && meus.isEmpty; i++) {
      await Future<void>.delayed(const Duration(milliseconds: 500));
      meus = await boloes.meus();
    }
    expect(meus.map((b) => b.id), contains(bolao.id));
    expect(meus.firstWhere((b) => b.id == bolao.id).isAdmin, isTrue);

    // --- convite (`CC-4`/`CC-5`) ------------------------------------------
    final convite = await boloes.gerarConvite(bolao.id);
    expect(convite.expiraEm.isAfter(DateTime.now()), isTrue);

    await auth.sair();
    await auth.cadastrar(email: emailB, senha: senha, nomeExibicao: 'Bruno');
    final entrada = await boloes.entrarPorConvite(convite.token);
    expect(entrada.poolId, bolao.id);
    expect(entrada.jaParticipava, isFalse);

    // --- palpite (`PP-1`) --------------------------------------------------
    final r = await boloes.rodada(bolao.id, rodada);
    final abertos = r.jogos.where((j) => j.deadlineOpen).toList();
    expect(abertos, isNotEmpty, reason: 'rodada sem jogo com prazo aberto');

    final envio = await boloes.palpitar(bolao.id, [
      (matchId: abertos.first.matchId, casa: 2, fora: 1),
    ]);
    expect(envio.salvos, 1);
    expect(envio.motivos, isEmpty);

    // O palpite volta na leitura da rodada — é o que a tela mostra ao reabrir.
    final depois = await boloes.rodada(bolao.id, rodada);
    final jogo = depois.jogos.firstWhere(
      (j) => j.matchId == abertos.first.matchId,
    );
    expect(jogo.temPalpite, isTrue);
    expect(jogo.palpiteCasa, 2);

    // --- sigilo (`PP-7`) ---------------------------------------------------
    final palpites = await boloes.palpitesDoJogo(
      bolao.id,
      abertos.first.matchId,
    );
    // Jogo não terminou: ninguém vê palpite de ninguém, nem o próprio dono na
    // lista pública. "Revelado" falso é a resposta certa, não erro de permissão.
    expect(palpites.revelado, isFalse);
    expect(palpites.palpites, isEmpty);

    // --- ranking (`PR-3`) --------------------------------------------------
    final classificacao = await ranking.geral(bolao.id);
    expect(classificacao.total, 2);

    // --- regras (`CC-6`) e participantes (`CC-7`) --------------------------
    final regras = await participantes.regras(bolao.id);
    expect(regras.pontosExato, 10);
    expect(
      regras.travadas,
      isTrue,
      reason: 'o primeiro palpite trava as regras',
    );

    final lista = await participantes.listar(bolao.id);
    expect(lista.total, 2);
    expect(lista.linhas.where((p) => p.isCreator).length, 1);
  });

  testWidgets('MG-7-81 / PP-3-11 relógio do aparelho não abre prazo fechado', (t) async {
    // O device pode estar com a hora errada — e o prazo continua sendo do
    // servidor. Aqui isso é verificado pelo desvio medido, e a recusa de fato
    // fica com a rule, exercitada em `MG-7-80`.
    final relogio = RelogioDoServidor();
    await relogio.sincronizar();
    expect(relogio.sincronizado, isTrue);
    expect(relogio.desvio.inMinutes.abs(), lessThan(5));
  });

  testWidgets('MG-7-80 palpite depois do apito é recusado com motivo', (
    t,
  ) async {
    // Bolão sobre uma rodada **que já aconteceu**: é o cenário do apito, e ele
    // precisa ser montado de propósito. Antes este caso dependia de sobrar um
    // jogo passado na rodada corrente e se auto-pulava quando não havia — caso
    // que pula sozinho é caso que não protege nada.
    final competicoes = await competicoesUmaVez();
    final passada = (competicoes.first.rodadaAtual ?? 2) - 1;

    final bolao = await boloes.criar(
      nome: 'Bolão do apito $sufixo',
      competitionCode: competicoes.first.code,
      rodadaInicial: passada,
      rodadaFinal: passada,
    );
    // O próprio servidor avisa que ninguém vai palpitar aqui (`CC-1`).
    expect(bolao.openMatches, 0);

    final r = await boloes.rodada(bolao.id, passada);
    expect(r.jogos, isNotEmpty);
    expect(r.jogos.every((j) => !j.deadlineOpen), isTrue);

    final envio = await boloes.palpitar(bolao.id, [
      (matchId: r.jogos.first.matchId, casa: 1, fora: 1),
    ]);

    // Recusado **pelo banco**, e com motivo legível: a tela precisa dizer por
    // que não deu, não "erro".
    expect(envio.salvos, 0);
    expect(envio.recusados, 1);
    expect(envio.motivos[r.jogos.first.matchId], contains('prazo'));
  });

  testWidgets('MG-7-82 sessão sobrevive ao cold start', (t) async {
    // O Firebase persiste a sessão sozinho; o que o app guarda é o retrato da
    // conta, para o cold start **sem rede** não cair no login (`AC-3`).
    final conta = await auth.minhaConta();
    expect(conta.email, emailB);

    final guardado = await SessaoStore().conta;
    expect(guardado?['displayName'], 'Bruno');
  });
}
