import 'package:bolao_mobile/core/erros/erro_do_app.dart';
import 'package:bolao_mobile/core/firebase/chamadas.dart';
import 'package:bolao_mobile/core/firebase/firebase_config.dart';
import 'package:bolao_mobile/core/firebase/relogio.dart';
import 'package:cloud_functions/cloud_functions.dart';
import 'package:firebase_auth/firebase_auth.dart';
import 'package:flutter_test/flutter_test.dart';

/// `MG-7` frente 2 — a camada de dados do app.
///
/// O que dá para verificar sem device é o que **não** depende do Firebase estar
/// de pé: a tradução da recusa, o desvio do relógio e a configuração. O resto da
/// frente 2 e a frente 3 inteira são em device, porque é lá que a promessa da
/// story se verifica — que nada mudou para quem usa.

/// Callables falsa: devolve o que mandarem, na hora que mandarem.
class _CallablesFalso implements Callables {
  _CallablesFalso(this.resposta, {this.erro});

  final Map<String, dynamic> resposta;
  final Object? erro;
  int chamadas = 0;

  @override
  Future<T> chamar<T>(String nome, [Map<String, dynamic>? dados]) async {
    chamadas++;
    if (erro != null) throw erro!;
    return resposta as T;
  }
}

void main() {
  group('MG-7-76 a contagem de prazo segue o horário do servidor', () {
    test('PP-3-01 o app busca o horário do servidor, não o do aparelho', () async {
      // Servidor 2 h à frente do aparelho: é o caso do relógio adiantado, que é
      // exatamente o que não pode abrir prazo fechado.
      final servidor = DateTime.now().add(const Duration(hours: 2));
      final relogio = RelogioDoServidor(
        _CallablesFalso({'agora': servidor.toIso8601String()}),
      );

      expect(relogio.sincronizado, isFalse);
      await relogio.sincronizar();

      expect(relogio.sincronizado, isTrue);
      expect(relogio.desvio.inMinutes, closeTo(120, 1));
      expect(
        relogio.agora.difference(DateTime.now()).inMinutes,
        closeTo(120, 1),
      );
    });

    test('MG-7-76 o relógio é medido uma vez por sessão', () async {
      final falso = _CallablesFalso({
        'agora': DateTime.now().toIso8601String(),
      });
      final relogio = RelogioDoServidor(falso);
      await relogio.sincronizar();

      expect(relogio.desvio.inSeconds.abs(), lessThan(2));
      expect(relogio.agora.difference(DateTime.now()).inSeconds.abs(), lessThan(2));

      // E mede **uma vez por sessão**: uma callable por tela seria cold start no
      // momento em que todo mundo abre a rodada junto.
      await relogio.sincronizar();
      await relogio.sincronizar();
      expect(falso.chamadas, 1);
    });

    test('sem rede a contagem cai no aparelho, e isso fica sabido', () async {
      final relogio = RelogioDoServidor(
        _CallablesFalso(const {}, erro: ApiError('sem conexão')),
      );
      await relogio.sincronizar();

      // Falhar não pode derrubar a tela nem travar o palpite: quem recusa de
      // verdade é a rule, que compara com o relógio do Google.
      expect(relogio.sincronizado, isFalse);
      expect(relogio.agora.difference(DateTime.now()).inSeconds, lessThan(2));
    });
  });

  group('MG-7-78 recusa vira mensagem específica, não "erro"', () {
    test('a mensagem escrita para o participante é preservada', () {
      final erro = traduzir(
        FirebaseFunctionsException(
          code: 'failed-precondition',
          message: 'este bolão já começou e não aceita mais participantes',
          details: {'motivo': 'JA_COMECOU'},
        ),
      );

      expect(erro.mensagem, contains('já começou'));
      // O motivo em código existe para a tela **não precisar ler o texto** para
      // decidir o que oferecer.
      expect(erro.motivo, 'JA_COMECOU');
      expect(erro.status, 409);
    });

    test('sessão morta é sessão morta, não erro genérico', () {
      final erro = traduzir(
        FirebaseFunctionsException(code: 'unauthenticated', message: ''),
      );
      expect(erro, isA<SessaoExpirada>());
    });

    test('login errado não vira verificador de quem tem conta', () {
      // Mesmo texto para senha errada e e-mail inexistente: dizer qual dos dois
      // falhou transforma a tela de login num oráculo de cadastros (`AC-2-02`).
      final semConta = traduzirAuth(
        FirebaseAuthException(code: 'user-not-found'),
      );
      final senhaErrada = traduzirAuth(
        FirebaseAuthException(code: 'wrong-password'),
      );
      expect(semConta.mensagem, senhaErrada.mensagem);
    });
  });

  group('MG-7-72 apontar para o emulador é configuração', () {
    test('o padrão é o ambiente de verdade, não o emulador', () {
      // Esquecer o `--dart-define` num build de release tem de cair no servidor
      // real — e não num emulador que não existe no aparelho de ninguém.
      expect(FirebaseConfig.usarEmulador, isFalse);
    });

    test(
      'o projeto padrão é o de demonstração, que não encosta em serviço pago',
      () {
        expect(FirebaseConfig.projeto, startsWith('demo-'));
      },
    );

    test('Functions e Firestore na mesma região', () {
      // Região diferente do banco é latência que teste local nenhum mostra: no
      // emulador tudo é localhost.
      expect(FirebaseConfig.regiao, 'southamerica-east1');
    });
  });
}
