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 Regras = {
  pointsExact: number;
  pointsWinnerDiff: number;
  pointsWinner: number;
  tiebreakOrder: string[];
  rulesLockedAt: string | null;
  nextDeadlineAt: string | null;
};

describe('Regras do bolão (e2e)', () => {
  let app: INestApplication<App>;
  let prisma: PrismaService;
  let tokenAdmin: string;
  let tokenOutro: string;
  let adminId: string;
  let outroId: string;
  let poolId: 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,
    };
  };

  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 — gate que falha sozinho não serve como gate.
    await app.listen(0);
    prisma = app.get(PrismaService);

    const admin = await criarConta('regras-admin@e2e.teste', 'Admin');
    const outro = await criarConta('regras-outro@e2e.teste', 'Outro');
    adminId = admin.id;
    tokenAdmin = admin.token;
    outroId = outro.id;
    tokenOutro = outro.token;

    const pool = await request(app.getHttpServer())
      .post('/pools')
      .set('Authorization', `Bearer ${tokenAdmin}`)
      // Rodadas futuras: é o que garante prazo aberto para nextDeadlineAt.
      .send({
        name: 'Bolão regras',
        competitionCode: 'BSA',
        matchdayFrom: 30,
        matchdayTo: 31,
      })
      .expect(201);
    poolId = (pool.body as { id: string }).id;
  });

  afterAll(async () => {
    await prisma.pool.deleteMany({
      where: { createdById: { in: [adminId, outroId] } },
    });
    await prisma.user.deleteMany({ where: { email: { contains: 'regras-' } } });
    await app.close();
  });

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

  it('bolão novo já vem com a tabela padrão do PRD', async () => {
    const res = await lerRegras(tokenAdmin).expect(200);
    const r = res.body as Regras;

    expect(r.pointsExact).toBe(10);
    expect(r.pointsWinnerDiff).toBe(5);
    expect(r.pointsWinner).toBe(3);
    expect(r.tiebreakOrder).toEqual(['EXACT_SCORES', 'WINNERS']);
    expect(r.rulesLockedAt).toBeNull();
  });

  it('mostra o prazo do próximo jogo junto com as regras', async () => {
    const r = (await lerRegras(tokenAdmin).expect(200)).body as Regras;
    expect(r.nextDeadlineAt).not.toBeNull();
  });

  it('admin altera as regras enquanto não há palpite', async () => {
    const res = await request(app.getHttpServer())
      .patch(`/pools/${poolId}/rules`)
      .set('Authorization', `Bearer ${tokenAdmin}`)
      .send({ pointsExact: 25, pointsWinnerDiff: 18, pointsWinner: 12 })
      .expect(200);

    const r = res.body as Regras;
    expect(r.pointsExact).toBe(25);
    expect(r.pointsWinner).toBe(12);
  });

  it('participante comum não altera as regras', async () => {
    await prisma.poolMember.create({
      data: { poolId, userId: outroId, role: 'MEMBER' },
    });

    await request(app.getHttpServer())
      .patch(`/pools/${poolId}/rules`)
      .set('Authorization', `Bearer ${tokenOutro}`)
      .send({ pointsExact: 99 })
      .expect(403);

    // Mas pode ler: o competitivo precisa da regra antes de palpitar.
    await lerRegras(tokenOutro).expect(200);
  });

  it('quem não participa não lê as regras', async () => {
    const estranho = await criarConta('regras-estranho@e2e.teste', 'Estranho');
    await request(app.getHttpServer())
      .get(`/pools/${poolId}/rules`)
      .set('Authorization', `Bearer ${estranho.token}`)
      .expect(403);
  });

  it('recusa valores inválidos e critério de desempate desconhecido', async () => {
    const patch = (body: Record<string, unknown>) =>
      request(app.getHttpServer())
        .patch(`/pools/${poolId}/rules`)
        .set('Authorization', `Bearer ${tokenAdmin}`)
        .send(body);

    await patch({ pointsExact: -1 }).expect(400);
    await patch({ pointsExact: 1000 }).expect(400);
    await patch({ tiebreakOrder: ['SORTEIO'] }).expect(400);
    await patch({ tiebreakOrder: [] }).expect(400);
  });

  it('regras travadas recusam alteração, apontando o caminho de encerrar e abrir outro', async () => {
    // Simula o efeito do primeiro palpite (quem trava é CC-3).
    await prisma.pool.update({
      where: { id: poolId },
      data: { rulesLockedAt: new Date() },
    });

    const res = await request(app.getHttpServer())
      .patch(`/pools/${poolId}/rules`)
      .set('Authorization', `Bearer ${tokenAdmin}`)
      .send({ pointsExact: 7 })
      .expect(409);

    expect(JSON.stringify(res.body)).toMatch(
      /encerre este bolão e abra outro/i,
    );

    // E o valor não mudou.
    const r = (await lerRegras(tokenAdmin).expect(200)).body as Regras;
    expect(r.pointsExact).toBe(25);
    expect(r.rulesLockedAt).not.toBeNull();
  });

  it('criação aceita regras diferentes do padrão', async () => {
    const res = await request(app.getHttpServer())
      .post('/pools')
      .set('Authorization', `Bearer ${tokenAdmin}`)
      .send({
        name: 'Bolão simples',
        competitionCode: 'BSA',
        matchdayFrom: 30,
        matchdayTo: 30,
        rules: { pointsExact: 3, pointsWinnerDiff: 1, pointsWinner: 1 },
      })
      .expect(201);

    const novo = (res.body as { id: string }).id;
    const r = (
      await request(app.getHttpServer())
        .get(`/pools/${novo}/rules`)
        .set('Authorization', `Bearer ${tokenAdmin}`)
        .expect(200)
    ).body as Regras;

    expect(r.pointsExact).toBe(3);
    expect(r.pointsWinner).toBe(1);
  });
});
