process.env.THROTTLE_LOGIN_LIMIT = '1000';

import { INestApplication, ValidationPipe } from '@nestjs/common';
import { Test } from '@nestjs/testing';
import request from 'supertest';
import { App } from 'supertest/types';
import { AppModule } from '../src/app.module';
import { PrismaService } from '../src/prisma/prisma.service';

type BolaoResumo = {
  id: string;
  name: string;
  totalMembers: number;
  isAdmin: boolean;
  closed: boolean;
  archived: boolean;
};

/**
 * Rodada exclusiva desta suíte: `Match` é global entre bolões, então duas
 * suítes na mesma rodada mexem no estado uma da outra.
 */
const RODADA = 31;

describe('Meus bolões (e2e)', () => {
  let app: INestApplication<App>;
  let prisma: PrismaService;

  const senha = 'senhaSegura123';
  const emailAdmin = 'meus-admin@e2e.teste';
  const emailConvidado = 'meus-convidado@e2e.teste';
  const emailDeFora = 'meus-fora@e2e.teste';

  let tokenAdmin: string;
  let tokenConvidado: string;
  let tokenDeFora: string;
  let poolId: string;

  const criarConta = async (email: string, nome: string) => {
    await request(app.getHttpServer())
      .post('/auth/register')
      .send({ email, password: senha, displayName: nome })
      .expect(201);
    const login = await request(app.getHttpServer())
      .post('/auth/login')
      .send({ email, password: senha })
      .expect(200);
    return (login.body as { accessToken: string }).accessToken;
  };

  const meus = (token: string) =>
    request(app.getHttpServer())
      .get('/pools')
      .set('Authorization', `Bearer ${token}`);

  beforeAll(async () => {
    const mod = await Test.createTestingModule({
      imports: [AppModule],
    }).compile();
    app = mod.createNestApplication();
    app.useGlobalPipes(
      new ValidationPipe({
        whitelist: true,
        forbidNonWhitelisted: true,
        transform: true,
      }),
    );
    await app.init();
    // Servidor escutando **uma vez** por suíte: sem isto o supertest faz
    // listen/close por requisição, e o ciclo repetido produz "socket hang up"
    // aleatório na suíte completa — gate que falha sozinho não serve de gate.
    await app.listen(0);
    prisma = app.get(PrismaService);

    tokenAdmin = await criarConta(emailAdmin, 'Dono');
    tokenConvidado = await criarConta(emailConvidado, 'Convidado');
    tokenDeFora = await criarConta(emailDeFora, 'Alheio');

    const pool = await request(app.getHttpServer())
      .post('/pools')
      .set('Authorization', `Bearer ${tokenAdmin}`)
      .send({
        name: 'Bolão da lista',
        competitionCode: 'BSA',
        matchdayFrom: RODADA,
        matchdayTo: RODADA,
      })
      .expect(201);
    poolId = (pool.body as { id: string }).id;

    const convite = await request(app.getHttpServer())
      .post(`/pools/${poolId}/invite`)
      .set('Authorization', `Bearer ${tokenAdmin}`)
      .expect(201);

    await request(app.getHttpServer())
      .post('/pools/join')
      .set('Authorization', `Bearer ${tokenConvidado}`)
      .send({ token: (convite.body as { token: string }).token })
      .expect(200);
  });

  afterAll(async () => {
    await prisma.pool.deleteMany({ where: { name: 'Bolão da lista' } });
    await prisma.user.deleteMany({
      where: { email: { in: [emailAdmin, emailConvidado, emailDeFora] } },
    });
    await app.close();
  });

  it('exige autenticação', async () => {
    await request(app.getHttpServer()).get('/pools').expect(401);
  });

  it('lista o bolão para quem criou, marcando que é admin', async () => {
    const r = await meus(tokenAdmin).expect(200);
    const lista = r.body as BolaoResumo[];
    const bolao = lista.find((b) => b.id === poolId);

    expect(bolao).toBeDefined();
    expect(bolao!.name).toBe('Bolão da lista');
    expect(bolao!.isAdmin).toBe(true);
    expect(bolao!.closed).toBe(false);
    // Admin + convidado: a contagem é o que o participante usa para
    // reconhecer o bolão certo na lista.
    expect(bolao!.totalMembers).toBe(2);
  });

  it('lista para quem entrou pelo convite, sem marcar admin', async () => {
    const r = await meus(tokenConvidado).expect(200);
    const bolao = (r.body as BolaoResumo[]).find((b) => b.id === poolId);

    expect(bolao).toBeDefined();
    expect(bolao!.isAdmin).toBe(false);
  });

  it('não lista bolão de quem não participa', async () => {
    const r = await meus(tokenDeFora).expect(200);
    expect((r.body as BolaoResumo[]).map((b) => b.id)).not.toContain(poolId);
  });

  it('continua listando depois de encerrado, sinalizando o encerramento', async () => {
    await prisma.pool.update({
      where: { id: poolId },
      data: { closedAt: new Date() },
    });

    const r = await meus(tokenAdmin).expect(200);
    const bolao = (r.body as BolaoResumo[]).find((b) => b.id === poolId);

    // Encerrado não desaparece: o campeão e o extrato continuam consultáveis.
    expect(bolao).toBeDefined();
    expect(bolao!.closed).toBe(true);

    await prisma.pool.update({
      where: { id: poolId },
      data: { closedAt: null },
    });
  });

  it('a contagem de participantes ignora quem saiu', async () => {
    const membro = await prisma.poolMember.findFirstOrThrow({
      where: { poolId, user: { email: emailConvidado } },
    });
    await prisma.poolMember.update({
      where: { id: membro.id },
      data: { leftAt: new Date() },
    });

    const r = await meus(tokenAdmin).expect(200);
    const bolao = (r.body as BolaoResumo[]).find((b) => b.id === poolId);

    // Contar quem saiu fazia a home prometer gente que não está mais jogando.
    expect(bolao!.totalMembers).toBe(1);

    await prisma.poolMember.update({
      where: { id: membro.id },
      data: { leftAt: null },
    });
  });

  it('deixa de listar para quem saiu do bolão', async () => {
    const membro = await prisma.poolMember.findFirstOrThrow({
      where: { poolId, user: { email: emailConvidado } },
    });
    await prisma.poolMember.update({
      where: { id: membro.id },
      data: { leftAt: new Date() },
    });

    const r = await meus(tokenConvidado).expect(200);
    expect((r.body as BolaoResumo[]).map((b) => b.id)).not.toContain(poolId);

    await prisma.poolMember.update({
      where: { id: membro.id },
      data: { leftAt: null },
    });
  });

  it('CV-6 FE-1 bolão arquivado continua na lista, marcado como arquivado', async () => {
    await prisma.pool.update({
      where: { id: poolId },
      data: { archivedAt: new Date() },
    });

    const r = await meus(tokenAdmin).expect(200);
    const bolao = (r.body as BolaoResumo[]).find((b) => b.id === poolId);

    // Omitir o arquivado fazia o bolão **desaparecer** do app sem nada dizer
    // que ele foi arquivado — e o participante achava que perdeu o histórico.
    expect(bolao).toBeDefined();
    expect(bolao!.archived).toBe(true);

    await prisma.pool.update({
      where: { id: poolId },
      data: { archivedAt: null },
    });
    const depois = await meus(tokenAdmin).expect(200);
    expect(
      (depois.body as BolaoResumo[]).find((b) => b.id === poolId)!.archived,
    ).toBe(false);
  });
});
