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/participantes_repository.dart';
import 'package:bolao_mobile/features/boloes/presentation/participantes_page.dart';
import 'package:flutter/material.dart';
import 'package:flutter_riverpod/flutter_riverpod.dart';
import 'package:flutter_test/flutter_test.dart';

class _RepoFalso extends ParticipantesRepository {
  _RepoFalso({required this.lista, this.status, this.erroNoStatus = false})
    : super(ApiClient(SessaoStore()));

  final ListaParticipantes lista;

  /// Falha ao carregar a **lista** (não o status): é o caso em que a tela não
  /// tem nada para mostrar e precisa dizer por quê.
  ApiError? erroNaLista;
  final StatusDaRodada? status;
  final bool erroNoStatus;

  /// Recusa do servidor a simular no rebaixamento (último admin, criador).
  ApiError? erroAoRebaixar;

  final List<String> promovidos = [];
  final List<String> rebaixados = [];
  final List<String> removidos = [];
  int saidas = 0;

  @override
  Future<ListaParticipantes> listar(String poolId) async {
    if (erroNaLista != null) throw erroNaLista!;
    return lista;
  }

  @override
  Future<StatusDaRodada> statusDaRodada(String poolId, int matchday) async {
    if (erroNoStatus || status == null) {
      throw ApiError('apenas o administrador do bolão pode fazer isso');
    }
    return status!;
  }

  @override
  Future<void> promover(String poolId, String userId) async =>
      promovidos.add(userId);

  @override
  Future<void> rebaixar(String poolId, String userId) async {
    if (erroAoRebaixar != null) throw erroAoRebaixar!;
    rebaixados.add(userId);
  }

  @override
  Future<void> remover(String poolId, String userId) async =>
      removidos.add(userId);

  @override
  Future<void> sair(String poolId) async => saidas++;
}

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

Participante _p({
  required String id,
  required String nome,
  bool admin = false,
  bool criador = false,
  bool ativo = true,
}) => Participante(
  userId: id,
  displayName: nome,
  isAdmin: admin || criador,
  isCreator: criador,
  ativo: ativo,
);

Widget _app(_RepoFalso repo, {required bool souAdmin}) => ProviderScope(
  overrides: [
    participantesRepositoryProvider.overrideWithValue(repo),
    sessaoProvider.overrideWith(_SessaoFalsa.new),
  ],
  child: MaterialApp(
    home: Scaffold(
      body: ParticipantesPage(poolId: 'p1', matchday: 20, souAdmin: souAdmin),
    ),
  ),
);

void main() {
  final trio = ListaParticipantes(
    total: 3,
    linhas: [
      _p(id: 'ana', nome: 'Ana', criador: true),
      _p(id: 'bruno', nome: 'Bruno'),
      _p(id: 'carla', nome: 'Carla', ativo: false),
    ],
  );

  group('ParticipantesPage', () {
    testWidgets('lista todos e marca criador, você e quem saiu', (t) async {
      await t.pumpWidget(_app(_RepoFalso(lista: trio), souAdmin: false));
      await t.pumpAndSettle();

      expect(find.text('Ana (você)'), findsOneWidget);
      expect(find.text('criador'), findsOneWidget);
      // Quem saiu continua listado: o histórico dele continua existindo.
      expect(find.text('Carla'), findsOneWidget);
      expect(find.text('saiu do bolão'), findsOneWidget);
    });

    testWidgets('participante comum não recebe ações de admin', (t) async {
      await t.pumpWidget(_app(_RepoFalso(lista: trio), souAdmin: false));
      await t.pumpAndSettle();

      // Oferecer o que vai dar 403 é pior que não oferecer.
      expect(find.byType(PopupMenuButton<String>), findsNothing);
    });

    testWidgets('CC-9 FE-3 a linha do criador diz por que não tem ação', (
      t,
    ) async {
      await t.pumpWidget(_app(_RepoFalso(lista: trio), souAdmin: true));
      await t.pumpAndSettle();

      // Decisão do dono em 07/08: em vez de oferecer a remoção e recusá-la, a
      // a ação fica indisponível **com o motivo à vista**. Sem o texto, o admin
      // procura o menu, não acha, e conclui que é defeito do app.
      expect(
        find.textContaining('não pode ser rebaixado nem removido'),
        findsOneWidget,
      );
    });

    testWidgets('CC-9 FE-3 participante comum não vê essa explicação', (
      t,
    ) async {
      await t.pumpWidget(_app(_RepoFalso(lista: trio), souAdmin: false));
      await t.pumpAndSettle();

      // Quem não administra não tem ação nenhuma para procurar: o texto seria
      // ruído numa lista que ele só lê.
      expect(
        find.textContaining('não pode ser rebaixado nem removido'),
        findsNothing,
      );
      expect(find.text('Ana (você)'), findsWidgets);
    });

    testWidgets('admin não recebe ação sobre o criador nem sobre quem saiu', (
      t,
    ) async {
      await t.pumpWidget(_app(_RepoFalso(lista: trio), souAdmin: true));
      await t.pumpAndSettle();

      // Criador é admin permanente; quem saiu não volta. Só Bruno tem menu.
      expect(find.byType(PopupMenuButton<String>), findsOneWidget);
    });

    testWidgets('promover pede confirmação e só age se confirmada', (t) async {
      final repo = _RepoFalso(lista: trio);
      await t.pumpWidget(_app(repo, souAdmin: true));
      await t.pumpAndSettle();

      await t.tap(find.byType(PopupMenuButton<String>));
      await t.pumpAndSettle();
      await t.tap(find.text('Tornar administrador'));
      await t.pumpAndSettle();

      expect(find.textContaining('convidar, corrigir placar'), findsOneWidget);
      await t.tap(find.text('Cancelar'));
      await t.pumpAndSettle();
      expect(repo.promovidos, isEmpty);

      await t.tap(find.byType(PopupMenuButton<String>));
      await t.pumpAndSettle();
      await t.tap(find.text('Tornar administrador'));
      await t.pumpAndSettle();
      await t.tap(find.widgetWithText(FilledButton, 'Promover'));
      await t.pumpAndSettle();
      expect(repo.promovidos, ['bruno']);
    });

    testWidgets('remover avisa que não há volta pelo convite', (t) async {
      final repo = _RepoFalso(lista: trio);
      await t.pumpWidget(_app(repo, souAdmin: true));
      await t.pumpAndSettle();

      await t.tap(find.byType(PopupMenuButton<String>));
      await t.pumpAndSettle();
      await t.tap(find.text('Remover do bolão'));
      await t.pumpAndSettle();

      expect(find.textContaining('não volta pelo convite'), findsOneWidget);
      await t.tap(find.widgetWithText(FilledButton, 'Remover'));
      await t.pumpAndSettle();
      expect(repo.removidos, ['bruno']);
    });

    testWidgets('criador não recebe a opção de sair, e sabe por quê', (
      t,
    ) async {
      // Ana é a criadora do trio: o servidor recusaria com 409.
      await t.pumpWidget(_app(_RepoFalso(lista: trio), souAdmin: true));
      await t.pumpAndSettle();

      expect(find.text('Sair do bolão'), findsNothing);
      expect(find.textContaining('administrador permanente'), findsOneWidget);
    });

    testWidgets('sair avisa que a saída é definitiva', (t) async {
      // Aqui Ana é participante comum, não criadora.
      final repo = _RepoFalso(
        lista: ListaParticipantes(
          total: 2,
          linhas: [
            _p(id: 'bruno', nome: 'Bruno', criador: true),
            _p(id: 'ana', nome: 'Ana'),
          ],
        ),
      );
      await t.pumpWidget(_app(repo, souAdmin: false));
      await t.pumpAndSettle();

      await t.tap(find.text('Sair do bolão'));
      await t.pumpAndSettle();

      // Saída sem aviso é perda de histórico sem consentimento informado.
      expect(find.textContaining('definitiva'), findsOneWidget);
      await t.tap(find.widgetWithText(FilledButton, 'Sair do bolão'));
      await t.pumpAndSettle();
      expect(repo.saidas, 1);
    });

    testWidgets('admin vê quem falta palpitar, sem nenhum placar', (t) async {
      final repo = _RepoFalso(
        lista: trio,
        status: const StatusDaRodada(
          matchday: 20,
          totalJogos: 10,
          linhas: [
            StatusPalpite(
              userId: 'ana',
              displayName: 'Ana',
              feitos: 10,
              estado: 'COMPLETO',
            ),
            StatusPalpite(
              userId: 'bruno',
              displayName: 'Bruno',
              feitos: 4,
              estado: 'PARCIAL',
            ),
          ],
        ),
      );
      await t.pumpWidget(_app(repo, souAdmin: true));
      await t.pumpAndSettle();

      // A linha da criadora carrega o estado do palpite **e** o motivo de não
      // ter ação (CC-9 FE-3), então aqui a asserção é por conteúdo.
      expect(find.textContaining('palpitou em todos os jogos'), findsOneWidget);
      expect(find.text('palpitou em 4 de 10 jogos'), findsOneWidget);
      // O sigilo de PP-7 vale inclusive para o admin: só o estado, nunca placar.
      // A busca é por **padrão de placar** (dígito x dígito), não pela letra
      // "x": procurar a letra reprovava qualquer texto legítimo que a contenha,
      // como "não pode ser rebaixado".
      expect(find.textContaining(RegExp(r'\d\s*[xX]\s*\d')), findsNothing);
    });

    testWidgets('falha no status de palpite não derruba a lista', (t) async {
      final repo = _RepoFalso(lista: trio, erroNoStatus: true);
      await t.pumpWidget(_app(repo, souAdmin: true));
      await t.pumpAndSettle();

      // Ver os participantes é direito de todos; o status é só do admin.
      expect(find.text('Bruno'), findsOneWidget);
      expect(find.text('Tentar de novo'), findsNothing);
    });

    testWidgets('CC-7-06 falha ao carregar a lista mostra o motivo com retry', (
      t,
    ) async {
      final repo = _RepoFalso(lista: trio)
        ..erroNaLista = ApiError('sem conexão com o servidor');
      await t.pumpWidget(_app(repo, souAdmin: true));
      await t.pumpAndSettle();

      // Lista vazia por erro de rede seria lida como "todo mundo saiu".
      expect(find.text('sem conexão com o servidor'), findsOneWidget);
      expect(find.text('Tentar de novo'), findsOneWidget);
      expect(find.text('Bruno'), findsNothing);
    });

    testWidgets('bolão de uma pessoa diz o que fazer em vez de lista seca', (
      t,
    ) async {
      final repo = _RepoFalso(
        lista: ListaParticipantes(
          total: 1,
          linhas: [_p(id: 'ana', nome: 'Ana', criador: true)],
        ),
      );
      await t.pumpWidget(_app(repo, souAdmin: true));
      await t.pumpAndSettle();

      expect(find.textContaining('Só você por aqui'), findsOneWidget);
    });
  });

  group('CC-8 rebaixamento na UI', () {
    /// Dupla de admins: só assim a opção de rebaixar existe na lista.
    final doisAdmins = ListaParticipantes(
      total: 2,
      linhas: [
        _p(id: 'ana', nome: 'Ana', criador: true),
        _p(id: 'bruno', nome: 'Bruno', admin: true),
      ],
    );

    Future<void> abrirRebaixar(WidgetTester t, _RepoFalso repo) async {
      await t.pumpWidget(_app(repo, souAdmin: true));
      await t.pumpAndSettle();
      await t.tap(find.byType(PopupMenuButton<String>));
      await t.pumpAndSettle();
      await t.tap(find.text('Remover administração'));
      await t.pumpAndSettle();
    }

    testWidgets(
      'CC-8-11 rebaixar pede confirmação e diz que ele continua no bolão',
      (t) async {
        final repo = _RepoFalso(lista: doisAdmins);
        await abrirRebaixar(t, repo);

        // Rebaixar não é remover — a confirmação precisa dizer isso, senão o
        // admin evita a ação achando que vai expulsar alguém.
        expect(find.textContaining('continua participando'), findsOneWidget);

        await t.tap(find.text('Cancelar'));
        await t.pumpAndSettle();
        expect(repo.rebaixados, isEmpty);

        await abrirRebaixar(t, repo);
        await t.tap(find.widgetWithText(FilledButton, 'Rebaixar'));
        await t.pumpAndSettle();
        expect(repo.rebaixados, ['bruno']);
      },
    );

    testWidgets(
      'CC-8-12 recusa do servidor aparece com o motivo, não como erro',
      (t) async {
        final repo = _RepoFalso(lista: doisAdmins)
          ..erroAoRebaixar = ApiError(
            'quem criou o bolão é administrador permanente',
          );
        await abrirRebaixar(t, repo);
        await t.tap(find.widgetWithText(FilledButton, 'Rebaixar'));
        await t.pumpAndSettle();

        // "Erro" genérico não ensina nada: o admin tentaria de novo para sempre.
        expect(
          find.text('quem criou o bolão é administrador permanente'),
          findsOneWidget,
        );
        expect(repo.rebaixados, isEmpty);
      },
    );
  });
}
