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

/**
 * `MG-3 BE-4` — a Function que libera o sigilo, exercitada de verdade.
 *
 * Este arquivo **não** é teste de rule: é integração contra os emuladores de
 * Firestore e Functions. Existe porque o desenho de `MG-3` moveu o risco do sigilo
 * de lugar: no NestJS era um filtro na consulta, e valia sempre que o código
 * estivesse certo; aqui é um campo que **alguém precisa virar**.
 *
 * Então o que importa testar é a transição, não o estado final:
 * - o campo vira quando o jogo termina **com placar**;
 * - não vira no apito, nem em jogo em andamento;
 * - e — o caso que dá nome à story — **se a Function não rodar, o palpite fica
 *   secreto a mais, nunca exposto antes.**
 */

// O emulador aceita credencial falsa; o que importa é o projeto ser `demo-`.
process.env.FIRESTORE_EMULATOR_HOST ??= '127.0.0.1:8080';

// Gatilho de Function é assíncrono, e o **caso negativo** ("não liberou") só
// conclui depois de esperar. Com o limite padrão de 5 s do Jest, a espera de 5 s
// estourava o teste antes de ele afirmar nada — falha de orçamento de tempo, não
// de regra.
jest.setTimeout(30_000);

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

const POOL = 'p-sigilo';
const JOGO = 'm-sigilo';
const ANA = 'ana';
const BRUNO = 'bruno';

const palpite = (uid: string) => ({
  userId: uid,
  matchId: JOGO,
  homeScore: 2,
  awayScore: 1,
  createdAt: new Date(),
  updatedAt: new Date(),
  matchFinished: false,
  points: null,
  band: null,
  scoredAt: null,
});

/** O gatilho é assíncrono: espera a condição, com teto. */
async function esperar(
  condicao: () => Promise<boolean>,
  limiteMs = 15000,
): Promise<boolean> {
  const fim = Date.now() + limiteMs;
  while (Date.now() < fim) {
    if (await condicao()) return true;
    await new Promise((r) => setTimeout(r, 400));
  }
  return false;
}

const sigiloLiberado = async () => {
  const docs = await db
    .collection(`boloes/${POOL}/palpites`)
    .where('matchFinished', '==', true)
    .get();
  return docs.size === 2;
};

describe('MG-3 BE-4 — liberação do sigilo pela Function', () => {
  beforeEach(async () => {
    for (const uid of [ANA, BRUNO]) {
      await db.doc(`boloes/${POOL}/palpites/${JOGO}_${uid}`).set(palpite(uid));
    }
    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 () => {
    await db.recursiveDelete(db.collection(`boloes/${POOL}/palpites`));
    await db.doc(`jogos/${JOGO}`).delete();
  });

  it('MG-3-27 o apito não libera nada', async () => {
    // Libera no **fim**, não no início. Quem vê o palpite alheio no intervalo sabe
    // o que precisa acontecer no segundo tempo.
    await db.doc(`jogos/${JOGO}`).update({ status: 'IN_PLAY' });

    const liberou = await esperar(sigiloLiberado, 4000);
    expect(liberou).toBe(false);
  });

  it('MG-3-28 terminar sem placar também não libera', async () => {
    // A fonte oficial marca `FINISHED` antes de publicar o placar. Liberar aí
    // revelaria o palpite de todos sem ninguém ter pontuado — e sem o que conferir.
    await db.doc(`jogos/${JOGO}`).update({ status: 'FINISHED' });

    const liberou = await esperar(sigiloLiberado, 4000);
    expect(liberou).toBe(false);
  });

  it('MG-3-29 terminar com placar libera todos os palpites do jogo', async () => {
    await db
      .doc(`jogos/${JOGO}`)
      .update({ status: 'FINISHED', homeScore: 2, awayScore: 1 });

    const liberou = await esperar(sigiloLiberado);
    expect(liberou).toBe(true);
  });

  it('MG-3-30 sem a Function, o palpite fica secreto a mais — nunca exposto antes', async () => {
    // O caso que justifica o desenho. Simula a Function não rodando: o jogo é
    // marcado como terminado **e** o flag do palpite é forçado de volta, como se o
    // gatilho tivesse falhado.
    await db
      .doc(`jogos/${JOGO}`)
      .update({ status: 'FINISHED', homeScore: 2, awayScore: 1 });
    await esperar(sigiloLiberado);

    for (const uid of [ANA, BRUNO]) {
      await db
        .doc(`boloes/${POOL}/palpites/${JOGO}_${uid}`)
        .update({ matchFinished: false });
    }

    // Estado resultante: jogo terminado, palpites ainda fechados. É falha **segura**
    // — o participante vê "aguardando" em vez de ver o palpite dos outros. O
    // inverso (flag ligado com jogo em andamento) é que seria grave, e não tem como
    // acontecer: só a Function escreve o campo, e ela só dispara no fim com placar.
    const fechados = await db
      .collection(`boloes/${POOL}/palpites`)
      .where('matchFinished', '==', false)
      .get();
    expect(fechados.size).toBe(2);

    const jogo = await db.doc(`jogos/${JOGO}`).get();
    expect(jogo.data()?.status).toBe('FINISHED');
  });

  describe('MG-3 BE-5 — revisão do palpite', () => {
    const revisoes = () =>
      db.collection(`boloes/${POOL}/palpites/${JOGO}_${BRUNO}/revisoes`).get();

    it('MG-3-31 mudar o valor guarda a versão anterior', async () => {
      await db
        .doc(`boloes/${POOL}/palpites/${JOGO}_${BRUNO}`)
        .update({ homeScore: 3, awayScore: 0 });

      const guardou = await esperar(async () => (await revisoes()).size === 1);
      expect(guardou).toBe(true);

      const [primeira] = (await revisoes()).docs;
      // Guarda o que **valia antes**: é a versão que a pessoa alega ter mudado.
      expect(primeira?.data().homeScore).toBe(2);
      expect(primeira?.data().awayScore).toBe(1);
    });

    it('MG-3-32 reenviar o mesmo valor não cria revisão', async () => {
      // A fila offline de PP-4 reenvia o mesmo lote quando a rede volta. Registrar
      // revisão em toda escrita encheria o histórico de linhas idênticas e ele
      // deixaria de servir para resolver disputa — foi o defeito corrigido no
      // backend antigo em 06/08 (PP-2-04).
      await db
        .doc(`boloes/${POOL}/palpites/${JOGO}_${BRUNO}`)
        .update({ homeScore: 2, awayScore: 1, updatedAt: new Date() });

      await new Promise((r) => setTimeout(r, 3000));
      expect((await revisoes()).size).toBe(0);
    });

    it('MG-3-33 a apuração escrevendo pontos não cria revisão', async () => {
      // Apurar mexe no mesmo documento (`points`, `band`, `scoredAt`) sem mudar o
      // palpite. Se isso gerasse revisão, todo jogo apurado poluiria o histórico.
      await db
        .doc(`boloes/${POOL}/palpites/${JOGO}_${BRUNO}`)
        .update({ points: 10, band: 'PLACAR_EXATO', scoredAt: new Date() });

      await new Promise((r) => setTimeout(r, 3000));
      expect((await revisoes()).size).toBe(0);
    });

    it('MG-3-34 duas mudanças deixam duas revisões, na ordem', async () => {
      const ref = db.doc(`boloes/${POOL}/palpites/${JOGO}_${BRUNO}`);
      await ref.update({ homeScore: 3, awayScore: 0 });
      await esperar(async () => (await revisoes()).size === 1);
      await ref.update({ homeScore: 1, awayScore: 1 });

      const duas = await esperar(async () => (await revisoes()).size === 2);
      expect(duas).toBe(true);

      const valores = (await revisoes()).docs
        .map((d) => `${d.data().homeScore}x${d.data().awayScore}`)
        .sort();
      // 2x1 (original) e 3x0 (intermediário). O valor atual, 1x1, não é revisão.
      expect(valores).toEqual(['2x1', '3x0']);
    });
  });
});
