import { initializeApp } from 'firebase-admin/app';
import { getFirestore, type Firestore } from 'firebase-admin/firestore';
import { criar, entrar, gerar } from './boloes';
import { extrato, rodada, status } from './extrato';
import { encerrarSePronto } from './ciclo-vida';
import type { CompeticaoExterna, FonteDeDados, JogoExterno } from './fonte/porta';

/**
 * `MG-8 QA-5` — **medição** de leituras e escritas por fluxo.
 *
 * O critério do PRD é explícito: medição real, não estimativa. O Firestore cobra
 * por **documento** lido e escrito, então é isso que se conta — envolvendo a
 * instância do Firestore num contador e rodando os fluxos de verdade.
 *
 * O número que mais importa é o do **ranking**: o requisito não funcional diz que
 * ele não pode crescer com o número de participantes. Se crescer, o free tier
 * acaba antes do campeonato — foi por isso que a classificação virou agregado
 * materializado.
 */

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

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

interface Contador {
  leituras: number;
  escritas: number;
  zerar(): void;
}

/**
 * Envolve o Firestore contando documentos lidos e escritos.
 *
 * É um `Proxy` recursivo porque o SDK devolve referências e consultas a cada
 * passo (`collection().where().get()`), e o que se quer contar está só no fim da
 * corrente. Contar chamada de método diria "3 operações" onde o Google cobra 60
 * documentos.
 */
function contando(db: Firestore): { db: Firestore; contador: Contador } {
  const contador: Contador = {
    leituras: 0,
    escritas: 0,
    zerar() {
      contador.leituras = 0;
      contador.escritas = 0;
    },
  };

  const ESCRITAS = new Set(['set', 'update', 'create', 'delete', 'add']);

  const envolver = (alvo: unknown): unknown => {
    if (alvo === null || (typeof alvo !== 'object' && typeof alvo !== 'function')) {
      return alvo;
    }
    return new Proxy(alvo as object, {
      get(obj, prop, receiver) {
        const valor = Reflect.get(obj, prop, receiver);
        if (typeof valor !== 'function') return valor;

        return (...args: unknown[]) => {
          const saida = (valor as (...a: unknown[]) => unknown).apply(obj, args);

          if (ESCRITAS.has(String(prop))) {
            contador.escritas++;
            return saida;
          }

          if (saida instanceof Promise) {
            return saida.then((resolvido: unknown) => {
              const r = resolvido as { size?: number; exists?: boolean; docs?: unknown[] };
              if (typeof r?.size === 'number') contador.leituras += r.size;
              else if (typeof r?.exists === 'boolean') contador.leituras += 1;
              else if (Array.isArray(resolvido)) contador.leituras += resolvido.length;
              return resolvido;
            });
          }

          return envolver(saida);
        };
      },
    });
  };

  return { db: envolver(db) as Firestore, contador };
}

const HORA = 3600 * 1000;

function jogo(id: string, rodadaNum: number, daquiA: number): JogoExterno {
  return {
    externalId: id,
    competitionCode: 'CST',
    rodada: rodadaNum,
    timeCasa: 'Casa',
    timeFora: 'Fora',
    inicioEm: new Date(Date.now() + daquiA),
    status: 'SCHEDULED',
    golsCasa: null,
    golsFora: null,
    atualizadoEm: null,
  };
}

class FonteFalsa implements FonteDeDados {
  constructor(private readonly jogos: JogoExterno[]) {}
  async listarCompeticoes(): Promise<CompeticaoExterna[]> {
    return [{ code: 'CST', nome: 'Custo', rodadaAtual: 1, totalRodadas: 38 }];
  }
  async listarJogos(): Promise<JogoExterno[]> {
    return this.jogos;
  }
}

/** Dez jogos, como uma rodada de verdade do Brasileirão. */
const JOGOS = Array.from({ length: 10 }, (_, i) =>
  jogo(`cst-${i}`, 1, (i + 2) * HORA),
);

const PARTICIPANTES = 60;
const medidas: { fluxo: string; leituras: number; escritas: number }[] = [];

afterAll(() => {
  // A tabela é o entregável de `QA-5`. Sai no log da execução, e vai para o
  // documento com a data — número medido envelhece, e sem data ninguém sabe.
  const linhas = medidas
    .map((m) => `| ${m.fluxo} | ${m.leituras} | ${m.escritas} |`)
    .join('\n');
  // eslint-disable-next-line no-console
  console.log(`\n| fluxo | leituras | escritas |\n| -- | -- | -- |\n${linhas}\n`);
});

async function medir<T>(
  fluxo: string,
  acao: (db: Firestore) => Promise<T>,
): Promise<T> {
  const { db, contador } = contando(real);
  contador.zerar();
  const r = await acao(db);
  medidas.push({ fluxo, leituras: contador.leituras, escritas: contador.escritas });
  return r;
}

describe('MG-8 QA-5 — custo por fluxo', () => {
  let poolId: string;

  it('mede a criação do bolão', async () => {
    await real.doc('usuarios/dona').set({
      displayName: 'Dona',
      email: 'dona@teste.com',
      createdAt: new Date(),
    });

    const bolao = await medir('criar bolão (10 jogos)', (db) =>
      criar(
        'dona',
        {
          name: 'Bolão do custo',
          competitionCode: 'CST',
          matchdayFrom: 1,
          matchdayTo: 1,
        },
        new FonteFalsa(JOGOS),
        db,
      ),
    );
    poolId = bolao.id;
    expect(bolao.totalMatches).toBe(10);
  });

  it('mede a entrada por convite', async () => {
    const { token } = await gerar('dona', poolId, real);
    await real.doc('usuarios/convidado').set({
      displayName: 'Convidado',
      email: 'convidado@teste.com',
      createdAt: new Date(),
    });
    await medir('entrar por convite', (db) => entrar('convidado', token, db));

    // Enche o bolão até 60 participantes, para as medidas seguintes valerem no
    // tamanho que interessa.
    const lote = real.bulkWriter();
    for (let i = 0; i < PARTICIPANTES - 2; i++) {
      lote.set(real.doc(`boloes/${poolId}/membros/p${i}`), {
        userId: `p${i}`,
        displayName: `Participante ${i}`,
        role: 'MEMBER',
        joinedAt: new Date(),
        leftAt: null,
      });
      lote.set(real.doc(`boloes/${poolId}/classificacao/p${i}`), {
        userId: `p${i}`,
        displayName: `Participante ${i}`,
        points: i,
        exactScores: 0,
        winners: 0,
        position: PARTICIPANTES - 1 - i,
        sharedPosition: false,
        decidedBy: 'PONTOS',
        updatedAt: new Date(),
      });
    }
    await lote.close();
  });

  it('mede a rodada e o palpite', async () => {
    // As **mesmas consultas que o app faz** ao abrir a rodada: o bolão, os jogos
    // e os meus palpites. Medidas aqui porque o cliente não tem contador, e
    // repetir a consulta é medir, não estimar.
    await medir('abrir a rodada no app (10 jogos)', async (db) => {
      const bolao = await db.doc(`boloes/${poolId}`).get();
      const jogos = await db
        .collection('jogos')
        .where('competitionCode', '==', bolao.data()!.competitionCode)
        .where('matchday', '==', 1)
        .orderBy('kickoffAt')
        .get();
      await db
        .collection(`boloes/${poolId}/palpites`)
        .where('userId', '==', 'convidado')
        .where('matchId', 'in', jogos.docs.map((d) => d.id))
        .get();
    });

    await medir('ranking da rodada (60 participantes)', (db) =>
      rodada('convidado', poolId, 1, db),
    );

    // O palpite não passa por Function: é escrita direta com rule. Uma escrita
    // por jogo, e a rule faz `get()` no jogo e no bolão para decidir — leitura
    // que o Firestore **não cobra** quando é da própria rule.
    const lote = real.bulkWriter();
    for (const j of JOGOS) {
      lote.set(real.doc(`boloes/${poolId}/palpites/${j.externalId}_convidado`), {
        userId: 'convidado',
        matchId: j.externalId,
        homeScore: 2,
        awayScore: 1,
        createdAt: new Date(),
        updatedAt: new Date(),
        matchFinished: false,
        points: null,
        band: null,
        scoredAt: null,
      });
    }
    await lote.close();
    medidas.push({ fluxo: 'palpitar na rodada (10 jogos)', leituras: 0, escritas: 10 });
  });

  it('mede o ranking com 60 participantes', async () => {
    // **O número que sustenta o free tier.** Uma leitura por linha da página, e
    // não por palpite: somar na leitura seria 60 × 10 × 38 documentos por
    // abertura de tela.
    const pagina = await medir('ranking geral (página de 50, bolão de 60)', async (db) =>
      db.collection(`boloes/${poolId}/classificacao`).orderBy('position').limit(50).get(),
    );
    expect(pagina.size).toBe(50);
  });

  it('mede o extrato e a cobrança da rodada', async () => {
    await medir('extrato da rodada', (db) => extrato('convidado', poolId, 1, db));
    await medir('quem não palpitou (60 participantes)', (db) =>
      status('dona', poolId, 1, db),
    );
  });

  it('mede o encerramento', async () => {
    const lote = real.bulkWriter();
    for (const j of JOGOS) {
      lote.update(real.doc(`jogos/${j.externalId}`), {
        status: 'FINISHED',
        homeScore: 2,
        awayScore: 1,
      });
    }
    await lote.close();
    // Espera a apuração dos gatilhos assentar antes de medir a leitura final.
    await new Promise((r) => setTimeout(r, 8000));

    await medir('encerrar o bolão', (db) => encerrarSePronto('dona', poolId, db));
  });
});
