import { initializeApp } from 'firebase-admin/app';
import { getFirestore } from 'firebase-admin/firestore';
import { encerrarAntes, historico } from './ciclo-vida';

/**
 * `MG-8` — paridade do **histórico de bolões** (`CV-5`), com os IDs de origem.
 *
 * Era a story em 1/10 no mapa de cobertura, e é uma das que mais dependem de
 * derivação: o histórico não guarda "campeão" em lugar nenhum. Ele compõe o
 * índice do usuário com a classificação de cada bolão encerrado, na leitura — o
 * que significa que **correção de placar depois do encerramento muda o
 * histórico**, e é assim que tem de ser.
 *
 * O que se verifica aqui é a composição, que é onde o "sem `join`" do Firestore
 * mais dói e onde é mais fácil errar em silêncio.
 */

process.env.FIRESTORE_EMULATOR_HOST ??= '127.0.0.1:8080';
jest.setTimeout(60_000);

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

const marca = Date.now();
const ANA = `ana-${marca}`;
const BRUNO = `bruno-${marca}`;
const CARLA = `carla-${marca}`;

const ENCERRADO = `p-encerrado-${marca}`;
const EM_ANDAMENTO = `p-andamento-${marca}`;
const ANTECIPADO = `p-antecipado-${marca}`;
const EMPATE = `p-empate-${marca}`;

type Linha = {
  poolId: string;
  name: string;
  outcome: string;
  position: number | null;
  points: number | null;
  closedEarly: boolean;
  isAdmin: boolean;
  archived: boolean;
};

const doHistorico = async (uid: string): Promise<Linha[]> =>
  (await historico(uid, db)) as Linha[];

async function montarBolao(
  poolId: string,
  opcoes: {
    membros: { uid: string; role: string; leftAt?: Date }[];
    classificacao?: { uid: string; points: number; position: number; shared?: boolean }[];
    encerrar?: 'normal' | 'antecipado';
  },
): Promise<void> {
  await db.doc(`boloes/${poolId}`).set({
    name: poolId,
    competitionCode: 'HST',
    matchdayFrom: 5,
    matchdayTo: 5,
    createdBy: ANA,
    createdAt: new Date(),
    pointsExact: 10,
    pointsWinnerDiff: 5,
    pointsWinner: 3,
    tiebreakOrder: ['EXACT_SCORES', 'WINNERS'],
    rulesLockedAt: null,
    closedAt: null,
    closedEarly: false,
    archivedAt: null,
    activeMembers: opcoes.membros.filter((m) => !m.leftAt).length,
  });

  for (const m of opcoes.membros) {
    await db.doc(`boloes/${poolId}/membros/${m.uid}`).set({
      userId: m.uid,
      displayName: m.uid,
      role: m.role,
      joinedAt: new Date(),
      leftAt: m.leftAt ?? null,
    });
    // O índice é escrito por gatilho, mas o teste não pode depender do tempo
    // dele para montar cenário: aqui ele é o **dado de entrada**, não o efeito
    // sob verificação.
    if (!m.leftAt) {
      await db.doc(`usuarios/${m.uid}/boloes/${poolId}`).set({
        poolId,
        name: poolId,
        matchdayFrom: 5,
        matchdayTo: 5,
        activeMembers: opcoes.membros.length,
        isAdmin: m.role === 'ADMIN',
        closed: opcoes.encerrar !== undefined,
        archived: false,
        joinedAt: new Date(),
      });
    }
  }

  // **Ordem importa, e o motivo é o defeito `CV-4-01`.** A entrada de
  // participante agora dispara recálculo da classificação — que é o certo. Se a
  // linha for plantada antes, o gatilho a sobrescreve com zero. Então espera-se
  // o recálculo assentar e só depois se escreve o resultado que o caso precisa;
  // como aqui o que está sob verificação é a **leitura** do histórico, plantar o
  // agregado é legítimo.
  if ((opcoes.classificacao ?? []).length > 0) {
    const fim = Date.now() + 20000;
    while (Date.now() < fim) {
      const atual = await db.collection(`boloes/${poolId}/classificacao`).get();
      if (atual.size >= opcoes.membros.filter((m) => !m.leftAt).length) break;
      await new Promise((r) => setTimeout(r, 300));
    }
  }

  for (const c of opcoes.classificacao ?? []) {
    await db.doc(`boloes/${poolId}/classificacao/${c.uid}`).set({
      userId: c.uid,
      displayName: c.uid,
      points: c.points,
      exactScores: 0,
      winners: 0,
      position: c.position,
      sharedPosition: c.shared ?? false,
      decidedBy: c.shared ? 'EMPATE_PERSISTE' : 'PONTOS',
      updatedAt: new Date(),
    });
  }

  if (opcoes.encerrar === 'antecipado') {
    await encerrarAntes(ANA, poolId, db);
  } else if (opcoes.encerrar === 'normal') {
    await db.doc(`boloes/${poolId}`).update({ closedAt: new Date(), closedEarly: false });
  }
}

beforeAll(async () => {
  // Encerrado normalmente: Ana campeã, Bruno segundo, Carla saiu no meio.
  await montarBolao(ENCERRADO, {
    membros: [
      { uid: ANA, role: 'ADMIN' },
      { uid: BRUNO, role: 'MEMBER' },
      { uid: CARLA, role: 'MEMBER', leftAt: new Date() },
    ],
    classificacao: [
      { uid: ANA, points: 40, position: 1 },
      { uid: BRUNO, points: 25, position: 2 },
    ],
    encerrar: 'normal',
  });

  // Em andamento: não pode aparecer no histórico de ninguém.
  await montarBolao(EM_ANDAMENTO, {
    membros: [
      { uid: ANA, role: 'ADMIN' },
      { uid: BRUNO, role: 'MEMBER' },
    ],
    classificacao: [{ uid: BRUNO, points: 12, position: 1 }],
  });

  // Encerrado antecipadamente.
  await montarBolao(ANTECIPADO, {
    membros: [
      { uid: ANA, role: 'ADMIN' },
      { uid: BRUNO, role: 'MEMBER' },
    ],
    classificacao: [{ uid: BRUNO, points: 8, position: 1 }],
    encerrar: 'antecipado',
  });

  // Empate que resistiu a tudo: co-campeões.
  await montarBolao(EMPATE, {
    membros: [
      { uid: ANA, role: 'ADMIN' },
      { uid: BRUNO, role: 'MEMBER' },
    ],
    classificacao: [
      { uid: ANA, points: 30, position: 1, shared: true },
      { uid: BRUNO, points: 30, position: 1, shared: true },
    ],
    encerrar: 'normal',
  });
});

describe('CV-5 — histórico de bolões encerrados', () => {
  it('CV-5-01 lista os encerrados com a minha posição final', async () => {
    const linhas = await doHistorico(BRUNO);
    const linha = linhas.find((l) => l.poolId === ENCERRADO)!;
    expect(linha).toMatchObject({ position: 2, points: 25, outcome: 'PARTICIPOU' });
  });

  it('CV-5-02 o campeão fica marcado como campeão', async () => {
    const linhas = await doHistorico(ANA);
    expect(linhas.find((l) => l.poolId === ENCERRADO)!.outcome).toBe('CAMPEAO');
  });

  it('CV-5-03 co-campeão aparece como título compartilhado', async () => {
    const daAna = await doHistorico(ANA);
    const doBruno = await doHistorico(BRUNO);
    // Os dois recebem `CO_CAMPEAO`, e não "campeão" para um e "2º" para o outro:
    // o empate que resistiu a todos os critérios divide o título.
    expect(daAna.find((l) => l.poolId === EMPATE)!.outcome).toBe('CO_CAMPEAO');
    expect(doBruno.find((l) => l.poolId === EMPATE)!.outcome).toBe('CO_CAMPEAO');
  });

  it('CV-5-04 quem abandonou não recebe posição final', async () => {
    const linhas = await doHistorico(CARLA);
    // O bolão sai da lista de quem saiu — junto com a posição, que ela não
    // disputou até o fim. Mostrar "3º lugar" para quem abandonou seria dar a ela
    // um resultado que ela não tem.
    expect(linhas.find((l) => l.poolId === ENCERRADO)).toBeUndefined();
  });

  it('CV-5-06 bolão em andamento não entra no histórico', async () => {
    const linhas = await doHistorico(BRUNO);
    expect(linhas.map((l) => l.poolId)).not.toContain(EM_ANDAMENTO);
  });

  it('CV-5-07 encerramento antecipado é distinguido do normal', async () => {
    const linhas = await doHistorico(BRUNO);
    // Título de bolão encerrado na rodada 5 não vale o mesmo, e a tela precisa
    // poder dizer isso.
    expect(linhas.find((l) => l.poolId === ANTECIPADO)!.closedEarly).toBe(true);
    expect(linhas.find((l) => l.poolId === ENCERRADO)!.closedEarly).toBe(false);
  });

  it('CV-5-08 o histórico diz quem é admin', async () => {
    // Arquivar é ação de admin: sem este campo a tela oferece o botão a todos e o
    // participante comum descobre pela recusa.
    const daAna = await doHistorico(ANA);
    const doBruno = await doHistorico(BRUNO);
    expect(daAna.find((l) => l.poolId === ENCERRADO)!.isAdmin).toBe(true);
    expect(doBruno.find((l) => l.poolId === ENCERRADO)!.isAdmin).toBe(false);
  });

  it('CV-5-10 dá para contar títulos a partir do histórico', async () => {
    const linhas = await doHistorico(ANA);
    const titulos = linhas.filter(
      (l) => l.outcome === 'CAMPEAO' || l.outcome === 'CO_CAMPEAO',
    );
    expect(titulos.map((l) => l.poolId).sort()).toEqual([ENCERRADO, EMPATE].sort());
  });

  it('CV-5-11 o campeão do histórico é derivado, não congelado', async () => {
    // Não é caso do plano original — é a consequência de `CV-1-07` no histórico, e
    // apareceu ao portar. Correção de placar depois do encerramento muda quem
    // aparece como campeão aqui, porque nada disso é gravado.
    await db.doc(`boloes/${EMPATE}/classificacao/${BRUNO}`).update({
      points: 45,
      position: 1,
      sharedPosition: false,
    });
    await db.doc(`boloes/${EMPATE}/classificacao/${ANA}`).update({
      points: 30,
      position: 2,
      sharedPosition: false,
    });

    const daAna = await doHistorico(ANA);
    const doBruno = await doHistorico(BRUNO);
    expect(doBruno.find((l) => l.poolId === EMPATE)!.outcome).toBe('CAMPEAO');
    expect(daAna.find((l) => l.poolId === EMPATE)!.outcome).toBe('PARTICIPOU');
  });
});
