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 { createHash } from 'node:crypto';
import { AppModule } from '../src/app.module';
import { PrismaService } from '../src/prisma/prisma.service';

type Convite = { token: string; expiresAt: string };
type Entrada = { poolId: string; poolName: string; jaParticipava: boolean };

describe('Convite e entrada no bolão (e2e)', () => {
  let app: INestApplication<App>;
  let prisma: PrismaService;
  let admin: { id: string; token: string };
  let convidado: { id: string; token: string };

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

  /** Bolão novo com rodadas futuras — entrada aberta. */
  const criarBolao = async (nome: string, de = 30, ate = 31) => {
    const res = await request(app.getHttpServer())
      .post('/pools')
      .set('Authorization', `Bearer ${admin.token}`)
      .send({
        name: nome,
        competitionCode: 'BSA',
        matchdayFrom: de,
        matchdayTo: ate,
      })
      .expect(201);
    return (res.body as { id: string }).id;
  };

  const gerarConvite = (poolId: string, token = admin.token) =>
    request(app.getHttpServer())
      .post(`/pools/${poolId}/invite`)
      .set('Authorization', `Bearer ${token}`);

  const entrar = (inviteToken: string, token = convidado.token) =>
    request(app.getHttpServer())
      .post('/pools/join')
      .set('Authorization', `Bearer ${token}`)
      .send({ token: inviteToken });

  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);

    admin = await criarConta('convite-admin@e2e.teste', 'Admin');
    convidado = await criarConta('convite-convidado@e2e.teste', 'Convidado');
  });

  afterAll(async () => {
    await prisma.pool.deleteMany({ where: { createdById: admin.id } });
    await prisma.user.deleteMany({
      where: { email: { contains: 'convite-' } },
    });
    await app.close();
  });

  it('admin gera convite com validade de 1 dia', async () => {
    const poolId = await criarBolao('Bolão convite');
    const res = await gerarConvite(poolId).expect(201);
    const c = res.body as Convite;

    expect(c.token.length).toBeGreaterThanOrEqual(40);
    const horas = (new Date(c.expiresAt).getTime() - Date.now()) / 3_600_000;
    expect(horas).toBeGreaterThan(23);
    expect(horas).toBeLessThanOrEqual(24);
  });

  it('o token não é guardado em texto no banco', async () => {
    const poolId = await criarBolao('Bolão hash');
    const { token } = (await gerarConvite(poolId).expect(201)).body as Convite;

    const cru = await prisma.invite.findFirst({
      where: { poolId, tokenHash: token },
      select: { id: true },
    });
    expect(cru).toBeNull();
  });

  it('participante comum não gera nem invalida convite', async () => {
    const poolId = await criarBolao('Bolão autorização');
    const { token } = (await gerarConvite(poolId).expect(201)).body as Convite;
    await entrar(token).expect(200);

    await gerarConvite(poolId, convidado.token).expect(403);
    await request(app.getHttpServer())
      .delete(`/pools/${poolId}/invite`)
      .set('Authorization', `Bearer ${convidado.token}`)
      .expect(403);
  });

  it('convidado entra pelo link e cai no bolão certo', async () => {
    const poolId = await criarBolao('Bolão do setor');
    const { token } = (await gerarConvite(poolId).expect(201)).body as Convite;

    const res = await entrar(token).expect(200);
    const e = res.body as Entrada;

    expect(e.poolId).toBe(poolId);
    expect(e.poolName).toBe('Bolão do setor');
    expect(e.jaParticipava).toBe(false);
  });

  it('reabrir o link não duplica participação', async () => {
    const poolId = await criarBolao('Bolão idempotente');
    const { token } = (await gerarConvite(poolId).expect(201)).body as Convite;

    await entrar(token).expect(200);
    const segunda = await entrar(token).expect(200);

    expect((segunda.body as Entrada).jaParticipava).toBe(true);
    expect(
      await prisma.poolMember.count({
        where: { poolId, userId: convidado.id },
      }),
    ).toBe(1);
  });

  it('gerar novo convite invalida o anterior na hora', async () => {
    const poolId = await criarBolao('Bolão rotação');
    const antigo = (await gerarConvite(poolId).expect(201)).body as Convite;
    const novo = (await gerarConvite(poolId).expect(201)).body as Convite;

    expect(novo.token).not.toBe(antigo.token);
    await entrar(antigo.token).expect(404);
    await entrar(novo.token).expect(200);
  });

  it('convite invalidado deixa de dar acesso', async () => {
    const poolId = await criarBolao('Bolão invalidado');
    const { token } = (await gerarConvite(poolId).expect(201)).body as Convite;

    await request(app.getHttpServer())
      .delete(`/pools/${poolId}/invite`)
      .set('Authorization', `Bearer ${admin.token}`)
      .expect(204);

    await entrar(token).expect(404);
  });

  it('convite expirado responde 410, distinto de inválido', async () => {
    const poolId = await criarBolao('Bolão expirado');
    const { token } = (await gerarConvite(poolId).expect(201)).body as Convite;

    await prisma.invite.updateMany({
      where: { poolId, revokedAt: null },
      data: { expiresAt: new Date(Date.now() - 1000) },
    });

    const res = await entrar(token).expect(410);
    expect(JSON.stringify(res.body)).toMatch(/expirou/i);
  });

  it('bolão já começado recusa gerar convite — não se convida para o que fechou', async () => {
    // Rodadas passadas: o primeiro jogo já aconteceu.
    const poolId = await criarBolao('Bolão já começado gerar', 1, 2);

    // Sem isto o admin recebia um código que nunca funcionaria, mandava no
    // grupo, e quem descobria o erro era o convidado.
    const res = await gerarConvite(poolId).expect(409);
    expect(JSON.stringify(res.body)).toMatch(/já começou/i);
  });

  it('bolão já começado recusa entrada — não se entra atrasado', async () => {
    // Rodadas passadas: o primeiro jogo já aconteceu.
    const poolId = await criarBolao('Bolão já começado', 1, 2);

    // Convite gravado direto no banco: gerar pela API já é recusado, e a
    // guarda da **entrada** precisa continuar coberta — é ela que protege de
    // um link antigo, gerado antes do bolão começar.
    const token = 'token-de-antes-do-inicio';
    await prisma.invite.create({
      data: {
        poolId,
        tokenHash: createHash('sha256').update(token).digest('hex'),
        expiresAt: new Date(Date.now() + 60 * 60 * 1000),
      },
    });

    const res = await entrar(token).expect(409);
    expect(JSON.stringify(res.body)).toMatch(/já começou/i);
    expect(
      await prisma.poolMember.count({
        where: { poolId, userId: convidado.id },
      }),
    ).toBe(0);
  });

  it('bolão encerrado recusa entrada e recusa gerar convite', async () => {
    const poolId = await criarBolao('Bolão encerrado');
    const { token } = (await gerarConvite(poolId).expect(201)).body as Convite;

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

    const res = await entrar(token).expect(410);
    expect(JSON.stringify(res.body)).toMatch(/encerrado/i);
    await gerarConvite(poolId).expect(410);
  });

  it('quem saiu do bolão não volta — a saída é definitiva', async () => {
    const poolId = await criarBolao('Bolão saída');
    const { token } = (await gerarConvite(poolId).expect(201)).body as Convite;
    await entrar(token).expect(200);

    // Simula a saída (quem implementa é CV-4).
    await prisma.poolMember.update({
      where: { poolId_userId: { poolId, userId: convidado.id } },
      data: { leftAt: new Date() },
    });

    const res = await entrar(token).expect(409);
    expect(JSON.stringify(res.body)).toMatch(/definitiva/i);
  });

  it('token inexistente responde 404', async () => {
    await entrar('token-que-nunca-existiu-mas-tem-tamanho-ok').expect(404);
  });

  it('exige autenticação para entrar', async () => {
    await request(app.getHttpServer())
      .post('/pools/join')
      .send({ token: 'qualquer-token-com-tamanho-suficiente' })
      .expect(401);
  });

  it('CC-4-04 cada bolão tem token próprio e a entrada respeita o token', async () => {
    // Plano docs/testes/CC-4-plano.md. Token que servisse para o bolão errado
    // colocaria gente no bolão de outro.
    const a = await criarBolao('Bolão A do token');
    const b = await criarBolao('Bolão B do token');

    const tokenA = ((await gerarConvite(a).expect(201)).body as Convite).token;
    const tokenB = ((await gerarConvite(b).expect(201)).body as Convite).token;
    expect(tokenA).not.toBe(tokenB);

    const entrou = await entrar(tokenB).expect(200);
    expect((entrou.body as { poolId: string }).poolId).toBe(b);

    // E o vínculo ficou **só** no bolão do token usado.
    expect(
      await prisma.poolMember.count({ where: { poolId: a, userId: convidado.id } }),
    ).toBe(0);
  });

  it('CC-4-10 invalidar sem convite ativo é idempotente', async () => {
    // Plano docs/testes/CC-4-plano.md. Admin que toca duas vezes em
    // "invalidar" não pode ver falha.
    const poolId = await criarBolao('Bolão sem convite');

    await request(app.getHttpServer())
      .delete(`/pools/${poolId}/invite`)
      .set('Authorization', `Bearer ${admin.token}`)
      .expect(204);

    await gerarConvite(poolId).expect(201);
    await request(app.getHttpServer())
      .delete(`/pools/${poolId}/invite`)
      .set('Authorization', `Bearer ${admin.token}`)
      .expect(204);
    await request(app.getHttpServer())
      .delete(`/pools/${poolId}/invite`)
      .set('Authorization', `Bearer ${admin.token}`)
      .expect(204);
  });
});
