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 Tokens = { accessToken: string };
type Pool = {
  id: string;
  name: string;
  totalMatches: number;
  matchdayFrom: number;
  matchdayTo: number;
};
type Competicao = { code: string; nome: string };

describe('Criação de bolão (e2e)', () => {
  let app: INestApplication<App>;
  let prisma: PrismaService;
  let token: string;
  let userId: string;

  const email = 'pools@e2e.teste';
  const password = 'senhaSegura123';

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

    const conta = await request(app.getHttpServer())
      .post('/auth/register')
      .send({ email, password, displayName: 'Dono' })
      .expect(201);
    userId = (conta.body as { id: string }).id;

    const login = await request(app.getHttpServer())
      .post('/auth/login')
      .send({ email, password })
      .expect(200);
    token = (login.body as Tokens).accessToken;
  });

  afterAll(async () => {
    await prisma.pool.deleteMany({ where: { createdById: userId } });
    await prisma.user.deleteMany({
      where: { email: { contains: 'pools@e2e.teste' } },
    });
    await app.close();
  });

  const criar = (body: Record<string, unknown>) =>
    request(app.getHttpServer())
      .post('/pools')
      .set('Authorization', `Bearer ${token}`)
      .send(body);

  const bolaoValido = {
    name: 'Bolão do setor',
    competitionCode: 'BSA',
    matchdayFrom: 1,
    matchdayTo: 3,
  };

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

  it('lista os campeonatos disponíveis', async () => {
    const res = await request(app.getHttpServer())
      .get('/pools/competitions')
      .set('Authorization', `Bearer ${token}`)
      .expect(200);

    const lista = res.body as Competicao[];
    expect(lista.some((c) => c.code === 'BSA')).toBe(true);
  });

  it('cria o bolão com os jogos já carregados, sem cadastrar jogo a jogo', async () => {
    const res = await criar(bolaoValido).expect(201);
    const pool = res.body as Pool;

    // 3 rodadas × 10 jogos: a premissa que impede o admin de desistir.
    expect(pool.totalMatches).toBe(30);
    expect(pool.id).toBeDefined();

    const vinculos = await prisma.poolMatch.count({
      where: { poolId: pool.id },
    });
    expect(vinculos).toBe(30);
  });

  it('define o criador como admin e participante', async () => {
    const res = await criar({ ...bolaoValido, name: 'Bolão admin' }).expect(
      201,
    );
    const pool = res.body as Pool;

    const membro = await prisma.poolMember.findFirst({
      where: { poolId: pool.id, userId },
      select: { role: true, leftAt: true },
    });

    expect(membro?.role).toBe('ADMIN');
    expect(membro?.leftAt).toBeNull();
  });

  it('recusa rodada final antes da inicial', async () => {
    await criar({ ...bolaoValido, matchdayFrom: 10, matchdayTo: 5 }).expect(
      400,
    );
  });

  it('recusa nome ausente e rodada fora do intervalo', async () => {
    await criar({ ...bolaoValido, name: '' }).expect(400);
    await criar({ ...bolaoValido, matchdayTo: 99 }).expect(400);
  });

  it('recusa campeonato sem jogos', async () => {
    await criar({ ...bolaoValido, competitionCode: 'XXX' }).expect(400);
  });

  it('reaproveita jogo já existente em vez de duplicar', async () => {
    const antes = await prisma.match.count({
      where: { competitionCode: 'BSA' },
    });
    await criar({
      ...bolaoValido,
      name: 'Segundo bolão, mesmas rodadas',
    }).expect(201);
    const depois = await prisma.match.count({
      where: { competitionCode: 'BSA' },
    });

    // Mesmo intervalo de rodadas: os jogos são os mesmos, não novos registros.
    expect(depois).toBe(antes);
  });

  it('fonte indisponível NÃO cria bolão pela metade', async () => {
    const poolsAntes = await prisma.pool.count({
      where: { createdById: userId },
    });

    process.env.FONTE_FAKE_INDISPONIVEL = 'true';
    try {
      await criar({
        ...bolaoValido,
        name: 'Bolão que não deve existir',
      }).expect(503);
    } finally {
      process.env.FONTE_FAKE_INDISPONIVEL = 'false';
    }

    // O critério real desta story: nada foi persistido.
    const poolsDepois = await prisma.pool.count({
      where: { createdById: userId },
    });
    expect(poolsDepois).toBe(poolsAntes);
    expect(
      await prisma.pool.count({
        where: { createdById: userId, name: 'Bolão que não deve existir' },
      }),
    ).toBe(0);
  });

  it('jogos vêm com horários distintos — prazo fecha por jogo, não por rodada', async () => {
    const res = await criar({ ...bolaoValido, name: 'Bolão horários' }).expect(
      201,
    );
    const pool = res.body as Pool;

    const jogos = await prisma.poolMatch.findMany({
      where: { poolId: pool.id },
      select: { match: { select: { kickoffAt: true, matchday: true } } },
    });

    const daPrimeira = jogos
      .filter((j) => j.match.matchday === 1)
      .map((j) => j.match.kickoffAt.getTime());

    expect(new Set(daPrimeira).size).toBeGreaterThan(1);
  });

  it('CC-1-08 rodadas já encerradas: bolão criado avisa que não há prazo aberto', async () => {
    // Plano docs/testes/CC-1-plano.md. Rodadas 1–3 estão no passado no
    // calendário falso. O bolão é criado (o critério pede **aviso**, não
    // recusa), mas ninguém consegue palpitar nele — e o convite também é
    // recusado, porque a entrada fecha no primeiro jogo. Sem este número o
    // admin só descobre pelo grupo reclamando.
    const res = await criar({ ...bolaoValido, name: 'Bolão de rodada velha' })
      .expect(201);
    const pool = res.body as Pool & { openMatches: number };

    expect(pool.totalMatches).toBe(30);
    expect(pool.openMatches).toBe(0);
  });

  it('CC-1-09 rodadas futuras: todos os jogos entram com prazo aberto', async () => {
    // Plano docs/testes/CC-1-plano.md. O contraste importa: `openMatches` só
    // serve de aviso se for diferente nos dois casos.
    const res = await criar({
      name: 'Bolão de rodada futura',
      competitionCode: 'BSA',
      matchdayFrom: 30,
      matchdayTo: 31,
    }).expect(201);
    const pool = res.body as Pool & { openMatches: number };

    expect(pool.totalMatches).toBe(20);
    expect(pool.openMatches).toBe(20);
  });

  it('CC-1-10 intervalo que cruza a rodada atual tem prazo aberto parcial', async () => {
    // Plano docs/testes/CC-1-plano.md. É o caso real do admin que cria "do
    // início do returno até o fim": parte já passou.
    const res = await criar({
      name: 'Bolão que cruza hoje',
      competitionCode: 'BSA',
      matchdayFrom: 20,
      matchdayTo: 24,
    }).expect(201);
    const pool = res.body as Pool & { openMatches: number };

    expect(pool.openMatches).toBeGreaterThan(0);
    expect(pool.openMatches).toBeLessThan(pool.totalMatches);
  });
});
