import { initializeApp } from 'firebase-admin/app';
import { getFirestore } from 'firebase-admin/firestore';

/**
 * `MG-5` — apuração e classificação materializada, contra os emuladores.
 *
 * O que importa verificar aqui não é a aritmética (isso são os 33 casos puros
 * portados de `PR-1`/`PR-5`), e sim a **integração**: o gatilho certo dispara, a
 * tabela lida é a do bolão, a classificação sai derivada, e repetir não estraga.
 */
process.env.FIRESTORE_EMULATOR_HOST ??= '127.0.0.1:8080';
jest.setTimeout(60_000);

const app = initializeApp({ projectId: 'demo-bolao' }, `apuracao-${Date.now()}`);
const db = getFirestore(app);

const JOGO = 'm-apuracao';
const PADRAO = 'p-padrao';
const CUSTOM = 'p-custom';

const membros = [
  { uid: 'ana', nome: 'Ana', palpite: [2, 1] },      // exato
  { uid: 'bruno', nome: 'Bruno', palpite: [3, 2] },  // vencedor e saldo
  { uid: 'carla', nome: 'Carla', palpite: [4, 0] },  // só vencedor
  { uid: 'davi', nome: 'Davi', palpite: [0, 2] },    // errou
];

async function esperar(cond: () => Promise<boolean>, limiteMs = 25000) {
  const fim = Date.now() + limiteMs;
  while (Date.now() < fim) {
    if (await cond()) return true;
    await new Promise((r) => setTimeout(r, 500));
  }
  return false;
}

const classificacao = async (poolId: string) => {
  const docs = await db.collection(`boloes/${poolId}/classificacao`).get();
  return docs.docs.map((d) => d.data());
};

const pontosDe = async (poolId: string, uid: string) =>
  (await db.doc(`boloes/${poolId}/classificacao/${uid}`).get()).data()?.points;

async function montar(poolId: string, tabela: Record<string, number>) {
  await db.doc(`boloes/${poolId}`).set({
    name: poolId,
    createdBy: 'ana',
    tiebreakOrder: ['EXACT_SCORES', 'WINNERS'],
    closedAt: null,
    archivedAt: null,
    rulesLockedAt: null,
    activeMembers: membros.length,
    ...tabela,
  });
  for (const m of membros) {
    await db.doc(`boloes/${poolId}/membros/${m.uid}`).set({
      userId: m.uid,
      displayName: m.nome,
      role: m.uid === 'ana' ? 'ADMIN' : 'MEMBER',
      joinedAt: new Date(),
      leftAt: null,
    });
    await db.doc(`boloes/${poolId}/palpites/${JOGO}_${m.uid}`).set({
      userId: m.uid,
      matchId: JOGO,
      homeScore: m.palpite[0],
      awayScore: m.palpite[1],
      createdAt: new Date(),
      updatedAt: new Date(),
      matchFinished: false,
      points: null,
      band: null,
      scoredAt: null,
    });
  }
}

describe('MG-5 — apuração e classificação', () => {
  beforeEach(async () => {
    await montar(PADRAO, { pointsExact: 10, pointsWinnerDiff: 5, pointsWinner: 3 });
    await montar(CUSTOM, { pointsExact: 20, pointsWinnerDiff: 10, pointsWinner: 5 });
    await db.doc(`jogos/${JOGO}`).set({
      externalId: JOGO, competitionCode: 'BSA', matchday: 23,
      homeTeam: 'Fluminense', awayTeam: 'Palmeiras',
      kickoffAt: new Date(Date.now() - 3 * 60 * 60 * 1000),
      status: 'TIMED', homeScore: null, awayScore: null, sourceUpdatedAt: null,
    });
  });

  afterEach(async () => {
    for (const p of [PADRAO, CUSTOM]) await db.recursiveDelete(db.collection('boloes').doc(p));
    await db.doc(`jogos/${JOGO}`).delete();
  });

  const apurar = () =>
    db.doc(`jogos/${JOGO}`).update({ status: 'FINISHED', homeScore: 2, awayScore: 1 });

  it('MG-5-01 o gatilho do jogo apura e materializa a classificação', async () => {
    await apurar();
    // Espera pelo **estado de interesse**, não por "existem 4 linhas": a primeira
    // versão esperava a contagem, que já é satisfeita pela passada anterior à
    // apuração (classificação de zeros). O teste lia zero e acusava o produto.
    const pronto = await esperar(async () => (await pontosDe(PADRAO, 'ana')) === 10);
    expect(pronto).toBe(true);
    expect((await classificacao(PADRAO)).length).toBe(4);

    // 10 / 5 / 3 / 0 — as quatro faixas, pela tabela padrão.
    expect(await pontosDe(PADRAO, 'ana')).toBe(10);
    expect(await pontosDe(PADRAO, 'bruno')).toBe(5);
    expect(await pontosDe(PADRAO, 'carla')).toBe(3);
    expect(await pontosDe(PADRAO, 'davi')).toBe(0);
  });

  it('MG-5-02 a tabela lida é a do bolão, não uma constante', async () => {
    // Mesmo jogo, mesmos palpites, bolão configurado 20/10/5. Se a apuração usasse
    // constante, os dois bolões sairiam iguais — e a configuração seria decorativa,
    // que foi exatamente o defeito de `CC-2 BE-3`.
    await apurar();
    await esperar(async () => (await pontosDe(CUSTOM, 'ana')) === 20);

    expect(await pontosDe(CUSTOM, 'ana')).toBe(20);
    expect(await pontosDe(CUSTOM, 'bruno')).toBe(10);
    expect(await pontosDe(CUSTOM, 'carla')).toBe(5);
    expect(await pontosDe(PADRAO, 'ana')).toBe(10);
  });

  it('MG-5-03 a classificação sai ordenada, com posição e critério', async () => {
    await apurar();
    await esperar(async () => (await pontosDe(PADRAO, 'ana')) === 10);

    const linhas = (await classificacao(PADRAO)).sort((a, b) => a.position - b.position);
    expect(linhas.map((l) => l.displayName)).toEqual(['Ana', 'Bruno', 'Carla', 'Davi']);
    expect(linhas.map((l) => l.position)).toEqual([1, 2, 3, 4]);
    // Sem empate em pontos, o critério é PONTOS — é o contrato de `PR-5`.
    expect(linhas.every((l) => l.decidedBy === 'PONTOS')).toBe(true);
    expect(linhas.every((l) => l.sharedPosition === false)).toBe(true);
  });

  it('MG-5-04 apurar duas vezes não dobra ponto', async () => {
    await apurar();
    await esperar(async () => (await pontosDe(PADRAO, 'ana')) === 10);

    // O Firestore garante *at-least-once*: o gatilho pode rodar de novo. Duas
    // proteções, e o teste cobre as duas de fora: escrita sem mudança de resultado
    // é ignorada, e a apuração recalcula do zero em vez de somar — é o caso
    // `PR-1-13` do backend antigo.
    await db.doc(`jogos/${JOGO}`).update({ sourceUpdatedAt: new Date() });
    await db
      .doc(`jogos/${JOGO}`)
      .update({ status: 'FINISHED', homeScore: 2, awayScore: 1 });
    await new Promise((r) => setTimeout(r, 4000));

    expect(await pontosDe(PADRAO, 'ana')).toBe(10);
  });

  it('MG-5-05 correção de placar recalcula e troca a ordem', async () => {
    await apurar();
    await esperar(async () => (await pontosDe(PADRAO, 'ana')) === 10);

    // Placar corrigido para 4x0: Carla passa a ter o exato, Ana erra o saldo.
    await db.doc(`jogos/${JOGO}`).update({ homeScore: 4, awayScore: 0 });
    const trocou = await esperar(async () => (await pontosDe(PADRAO, 'carla')) === 10);
    expect(trocou).toBe(true);

    const linhas = (await classificacao(PADRAO)).sort((a, b) => a.position - b.position);
    expect(linhas[0]?.displayName).toBe('Carla');
  });

  it('MG-5-06 placar retirado desfaz a apuração', async () => {
    // Jogo adiado depois de apurado (`PP-6-07`): manter o ponto favoreceria quem já
    // sabia o resultado antigo, porque o palpite reabre.
    await apurar();
    await esperar(async () => (await pontosDe(PADRAO, 'ana')) === 10);

    await db.doc(`jogos/${JOGO}`).update({
      status: 'POSTPONED', homeScore: null, awayScore: null,
    });

    const desfez = await esperar(async () => (await pontosDe(PADRAO, 'ana')) === 0);
    expect(desfez).toBe(true);

    const palpite = (await db.doc(`boloes/${PADRAO}/palpites/${JOGO}_ana`).get()).data();
    expect(palpite?.points).toBeNull();
    expect(palpite?.band).toBeNull();
  });

  it('MG-5-07 quem saiu fica fora do ranking, e o palpite dele permanece', async () => {
    await db.doc(`boloes/${PADRAO}/membros/davi`).update({ leftAt: new Date() });
    await apurar();
    await esperar(async () => (await pontosDe(PADRAO, 'ana')) === 10);

    const nomes = (await classificacao(PADRAO)).map((l) => l.displayName);
    expect(nomes).not.toContain('Davi');
    // O vínculo se encerra; o passado não (CV-4).
    const dele = await db.doc(`boloes/${PADRAO}/palpites/${JOGO}_davi`).get();
    expect(dele.exists).toBe(true);
  });
});
