import { initializeApp } from 'firebase-admin/app';
import { getFirestore } from 'firebase-admin/firestore';
import { lembrar, MINUTOS_ANTES } from './lembretes';

/**
 * `PP-8` / `MG-6 BE-3` — lembrete de prazo, com os IDs de origem.
 *
 * Era a única story em **zero** no mapa de cobertura, e estava em zero por um
 * motivo que não era o dela: faltava o job. O que a travava de verdade era o
 * push, e push é `BE-4` — **entregar** é outra coisa de **decidir quem
 * lembrar**. Separando os dois, a regra fica verificada antes de existir
 * credencial de FCM.
 *
 * A janela é de 2 h antes do apito, e o job roda de meia em meia hora. Os casos
 * abaixo controlam o tempo pelo parâmetro `agora`, em vez de esperar: teste que
 * espera duas horas não é teste.
 */

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

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

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

let contador = 0;
const proximaCompeticao = () => `L${'ABCDEFGHIJKLMNOPQRSTUVWXYZ'[contador++]}`;

interface Cenario {
  poolId: string;
  jogoId: string;
  competicao: string;
}

async function montar(opcoes: {
  minutosAteOApito: number;
  encerrado?: boolean;
  arquivado?: boolean;
  saiu?: string[];
}): Promise<Cenario> {
  const competicao = proximaCompeticao();
  const poolId = `p-${competicao}-${marca}`;
  const jogoId = `j-${competicao}-${marca}`;

  await db.doc(`boloes/${poolId}`).set({
    name: poolId,
    competitionCode: competicao,
    matchdayFrom: 1,
    matchdayTo: 1,
    createdBy: ANA,
    createdAt: new Date(),
    pointsExact: 10,
    pointsWinnerDiff: 5,
    pointsWinner: 3,
    tiebreakOrder: ['EXACT_SCORES', 'WINNERS'],
    rulesLockedAt: null,
    closedAt: opcoes.encerrado ? new Date() : null,
    closedEarly: false,
    archivedAt: opcoes.arquivado ? new Date() : null,
    activeMembers: 3,
  });

  for (const uid of [ANA, BRUNO, CARLA]) {
    await db.doc(`boloes/${poolId}/membros/${uid}`).set({
      userId: uid,
      displayName: uid,
      role: uid === ANA ? 'ADMIN' : 'MEMBER',
      joinedAt: new Date(),
      leftAt: (opcoes.saiu ?? []).includes(uid) ? new Date() : null,
    });
  }

  await db.doc(`jogos/${jogoId}`).set({
    externalId: jogoId,
    competitionCode: competicao,
    matchday: 1,
    homeTeam: 'Bahia',
    awayTeam: 'Vitória',
    kickoffAt: new Date(Date.now() + opcoes.minutosAteOApito * MIN),
    status: 'SCHEDULED',
    homeScore: null,
    awayScore: null,
    sourceUpdatedAt: null,
  });

  return { poolId, jogoId, competicao };
}

const palpitar = (c: Cenario, uid: string) =>
  db.doc(`boloes/${c.poolId}/palpites/${c.jogoId}_${uid}`).set({
    userId: uid,
    matchId: c.jogoId,
    homeScore: 1,
    awayScore: 0,
    createdAt: new Date(),
    updatedAt: new Date(),
    matchFinished: false,
    points: null,
    band: null,
    scoredAt: null,
  });

/**
 * Lembretes gerados **neste** bolão.
 *
 * O retorno do job conta o mundo inteiro, e o mundo inteiro inclui os bolões que
 * os outros arquivos de teste deixaram com jogo prestes a começar. Contar o
 * global fazia este caso acusar o produto de lembrar gente demais — quando o que
 * havia era vizinhança.
 */
const marcasDe = async (poolId: string) =>
  (await db.collection(`boloes/${poolId}/lembretes`).get()).size;

const avisosDe = async (uid: string, poolId: string) =>
  (
    await db
      .collection(`usuarios/${uid}/avisos`)
      .where('poolId', '==', poolId)
      .get()
  ).docs.map((d) => d.data());

describe('PP-8 — lembrete de prazo', () => {
  it('PP-8-01 e PP-8-02 lembra só quem não palpitou', async () => {
    const c = await montar({ minutosAteOApito: 60 });
    await palpitar(c, BRUNO);

    await lembrar(db);
    expect(await marcasDe(c.poolId)).toBe(2);

    expect(await avisosDe(ANA, c.poolId)).toHaveLength(1);
    expect(await avisosDe(CARLA, c.poolId)).toHaveLength(1);
    // Quem já palpitou não recebe nada: lembrar quem não precisa é o começo do
    // caminho para a pessoa desligar a notificação e perder a que importa.
    expect(await avisosDe(BRUNO, c.poolId)).toHaveLength(0);
  });

  it('PP-8-03 palpite entre agendar e disparar cancela o lembrete', async () => {
    const c = await montar({ minutosAteOApito: 60 });

    // O job não agenda nada com antecedência: ele decide **na hora**, olhando quem
    // ainda não palpitou. É o que faz "palpitou no meio do caminho" cancelar
    // sozinho, sem precisar de cancelamento.
    await palpitar(c, ANA);
    await palpitar(c, BRUNO);
    await palpitar(c, CARLA);

    await lembrar(db);
    expect(await marcasDe(c.poolId)).toBe(0);
  });

  it('PP-8-04 não lembra duas vezes o mesmo jogo', async () => {
    const c = await montar({ minutosAteOApito: 90 });

    await lembrar(db);
    await lembrar(db);
    await lembrar(db);

    // O job roda de meia em meia hora; sem a marca, a pessoa receberia quatro
    // avisos do mesmo jogo antes do apito.
    expect(await avisosDe(ANA, c.poolId)).toHaveLength(1);
    const marcas = await db.collection(`boloes/${c.poolId}/lembretes`).get();
    expect(marcas.size).toBe(3);
  });

  it('PP-8-05 jogo fora da janela não gera lembrete', async () => {
    const longe = await montar({ minutosAteOApito: MINUTOS_ANTES + 60 });
    const passado = await montar({ minutosAteOApito: -30 });

    await lembrar(db);

    // Cedo demais é spam; depois do apito é inútil — e cruel.
    expect(await avisosDe(ANA, longe.poolId)).toHaveLength(0);
    expect(await avisosDe(ANA, passado.poolId)).toHaveLength(0);
  });

  it('PP-8-06 bolão encerrado ou arquivado não gera lembrete', async () => {
    const encerrado = await montar({ minutosAteOApito: 60, encerrado: true });
    const arquivado = await montar({ minutosAteOApito: 60, arquivado: true });

    await lembrar(db);

    expect(await avisosDe(ANA, encerrado.poolId)).toHaveLength(0);
    expect(await avisosDe(ANA, arquivado.poolId)).toHaveLength(0);
  });

  it('PP-8-07 quem saiu ou foi removido não recebe', async () => {
    const c = await montar({ minutosAteOApito: 60, saiu: [CARLA] });

    await lembrar(db);

    expect(await avisosDe(ANA, c.poolId)).toHaveLength(1);
    // Cobrar palpite de quem saiu é o pior tipo de notificação: ela lembra a
    // pessoa de um bolão que ela decidiu deixar.
    expect(await avisosDe(CARLA, c.poolId)).toHaveLength(0);
  });

  it('PP-8-08 sem push, o aviso fica legível no app', async () => {
    const c = await montar({ minutosAteOApito: 45 });
    await lembrar(db);

    const [aviso] = await avisosDe(ANA, c.poolId);
    // O canal que sempre existe é o Firestore. O push (`MG-6 BE-4`) é entrega, e
    // entrega que falha não pode custar o lembrete.
    expect(aviso).toMatchObject({
      kind: 'PRAZO_FECHANDO',
      poolId: c.poolId,
      matchId: c.jogoId,
      readAt: null,
    });
    expect(aviso!.title).toBe('Seu palpite está fechando');
    expect(aviso!.body).toContain('Bahia x Vitória');
    // E ele diz o que a pessoa perde se não agir — sem isso é só barulho.
    expect(aviso!.body).toContain('Depois do apito não dá mais');
  });

  it('o lembrete é por bolão: o mesmo jogo em dois bolões lembra nos dois', async () => {
    const primeiro = await montar({ minutosAteOApito: 60 });
    // Segundo bolão sobre o **mesmo** jogo: o calendário é global, e cada bolão
    // tem o seu prazo a cobrar.
    const segundoId = `p2-${primeiro.competicao}-${marca}`;
    await db.doc(`boloes/${segundoId}`).set({
      name: segundoId,
      competitionCode: primeiro.competicao,
      matchdayFrom: 1,
      matchdayTo: 1,
      createdBy: BRUNO,
      createdAt: new Date(),
      pointsExact: 10,
      pointsWinnerDiff: 5,
      pointsWinner: 3,
      tiebreakOrder: ['EXACT_SCORES', 'WINNERS'],
      rulesLockedAt: null,
      closedAt: null,
      closedEarly: false,
      archivedAt: null,
      activeMembers: 1,
    });
    await db.doc(`boloes/${segundoId}/membros/${BRUNO}`).set({
      userId: BRUNO,
      displayName: BRUNO,
      role: 'ADMIN',
      joinedAt: new Date(),
      leftAt: null,
    });

    await lembrar(db);

    expect(await avisosDe(ANA, primeiro.poolId)).toHaveLength(1);
    expect(await avisosDe(BRUNO, segundoId)).toHaveLength(1);
  });
});
