import {
  assertFails,
  assertSucceeds,
  initializeTestEnvironment,
  RulesTestEnvironment,
} from '@firebase/rules-unit-testing';
import {
  collection,
  deleteDoc,
  doc,
  getDoc,
  getDocs,
  query,
  setDoc,
  updateDoc,
  where,
} from 'firebase/firestore';
import { readFileSync } from 'node:fs';

/**
 * `MG-3` — sigilo do palpite e prazo, aplicados pelo **banco**.
 *
 * São as duas regras que, quebradas, corrompem o bolão. No NestJS elas eram filtro
 * e comparação dentro de um service; aqui são cláusulas de rule, e a diferença
 * importa: cláusula ausente não aparece lendo o arquivo.
 */
describe('MG-3 — sigilo e prazo', () => {
  let ambiente: RulesTestEnvironment;

  const POOL = 'p1';
  const ANA = 'ana';
  const BRUNO = 'bruno';
  const DE_FORA = 'defora';

  const FUTURO = 'm_futuro'; // prazo aberto
  const PASSADO = 'm_passado'; // apito já foi, jogo não terminou
  const TERMINADO = 'm_terminado'; // terminou com placar

  const daquiA = (horas: number) =>
    new Date(Date.now() + horas * 60 * 60 * 1000);

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

  const id = (matchId: string, uid: string) => `${matchId}_${uid}`;

  beforeAll(async () => {
    ambiente = await initializeTestEnvironment({
      projectId: 'demo-bolao',
      firestore: {
        rules: readFileSync(`${__dirname}/../../firestore.rules`, 'utf8'),
        host: '127.0.0.1',
        port: 8080,
      },
    });
  });

  afterAll(() => ambiente.cleanup());

  beforeEach(async () => {
    await ambiente.clearFirestore();
    await ambiente.withSecurityRulesDisabled(async (ctx) => {
      const db = ctx.firestore();
      await setDoc(doc(db, `boloes/${POOL}`), {
        name: 'Bolão do setor',
        createdBy: ANA,
        closedAt: null,
        archivedAt: null,
        rulesLockedAt: null,
      });
      for (const uid of [ANA, BRUNO]) {
        await setDoc(doc(db, `boloes/${POOL}/membros/${uid}`), {
          userId: uid,
          displayName: uid,
          role: uid === ANA ? 'ADMIN' : 'MEMBER',
          joinedAt: new Date(),
          leftAt: null,
        });
      }
      const jogo = (matchId: string, kickoff: Date, status: string, gols: number | null) => ({
        externalId: matchId,
        competitionCode: 'BSA',
        matchday: 23,
        homeTeam: 'Fluminense',
        awayTeam: 'Palmeiras',
        kickoffAt: kickoff,
        status,
        homeScore: gols,
        awayScore: gols,
        sourceUpdatedAt: null,
      });
      await setDoc(doc(db, `jogos/${FUTURO}`), jogo(FUTURO, daquiA(24), 'TIMED', null));
      await setDoc(doc(db, `jogos/${PASSADO}`), jogo(PASSADO, daquiA(-2), 'IN_PLAY', null));
      await setDoc(doc(db, `jogos/${TERMINADO}`), jogo(TERMINADO, daquiA(-5), 'FINISHED', 2));
    });
  });

  const como = (uid: string) => ambiente.authenticatedContext(uid).firestore();

  const semearPalpite = async (uid: string, matchId: string, extra = {}) => {
    await ambiente.withSecurityRulesDisabled(async (ctx) => {
      await setDoc(
        doc(ctx.firestore(), `boloes/${POOL}/palpites/${id(matchId, uid)}`),
        palpite(uid, matchId, extra),
      );
    });
  };

  // ---- sigilo (PP-7) -------------------------------------------------------

  describe('sigilo do palpite', () => {
    it('MG-3-01 leio o meu palpite antes do jogo', async () => {
      await semearPalpite(BRUNO, FUTURO);
      await assertSucceeds(
        getDoc(doc(como(BRUNO), `boloes/${POOL}/palpites/${id(FUTURO, BRUNO)}`)),
      );
    });

    it('MG-3-02 não leio o palpite de outro em jogo que não terminou', async () => {
      await semearPalpite(BRUNO, FUTURO);
      await assertFails(
        getDoc(doc(como(ANA), `boloes/${POOL}/palpites/${id(FUTURO, BRUNO)}`)),
      );
    });

    it('MG-3-03 nem o admin — o sigilo não tem exceção de papel', async () => {
      // Ana é admin. No backend antigo isso era um `if` no service; aqui é a
      // ausência de qualquer cláusula que privilegie papel.
      await semearPalpite(BRUNO, FUTURO);
      await assertFails(
        getDoc(doc(como(ANA), `boloes/${POOL}/palpites/${id(FUTURO, BRUNO)}`)),
      );
    });

    it('MG-3-04 jogo em andamento continua secreto — libera no fim, não no apito', async () => {
      // Quem vê o palpite alheio no intervalo sabe o que precisa acontecer no
      // segundo tempo.
      await semearPalpite(BRUNO, PASSADO);
      await assertFails(
        getDoc(doc(como(ANA), `boloes/${POOL}/palpites/${id(PASSADO, BRUNO)}`)),
      );
    });

    it('MG-3-05 jogo terminado libera o palpite de todos', async () => {
      await semearPalpite(BRUNO, TERMINADO);
      await assertSucceeds(
        getDoc(doc(como(ANA), `boloes/${POOL}/palpites/${id(TERMINADO, BRUNO)}`)),
      );
    });

    it('MG-3-06 quem não participa não lê palpite nenhum', async () => {
      await semearPalpite(BRUNO, TERMINADO);
      await assertFails(
        getDoc(doc(como(DE_FORA), `boloes/${POOL}/palpites/${id(TERMINADO, BRUNO)}`)),
      );
    });

    it('MG-3-07 consulta de jogo terminado exige o filtro que prova o acesso', async () => {
      await semearPalpite(ANA, TERMINADO);
      await semearPalpite(BRUNO, TERMINADO);
      const palpitesDoJogo = collection(como(ANA), `boloes/${POOL}/palpites`);

      // **Rules não são filtros.** Numa consulta, a rule é avaliada contra a
      // própria consulta: o `where` tem de provar o acesso. Filtrar só por jogo
      // deixa `userId` e `matchFinished` indefinidos para a rule, e ela recusa —
      // mesmo que todos os documentos fossem legíveis.
      await assertFails(
        getDocs(query(palpitesDoJogo, where('matchId', '==', TERMINADO))),
      );

      // Com o filtro que a rule precisa, passa. O app tem de **pedir o que pode
      // ver**, e não pedir tudo esperando que o banco filtre.
      await assertSucceeds(
        getDocs(
          query(
            palpitesDoJogo,
            where('matchId', '==', TERMINADO),
            where('matchFinished', '==', true),
          ),
        ),
      );
    });

    it('MG-3-08 consulta de jogo não terminado falha inteira, não vaza em parte', async () => {
      // A rule é avaliada documento por documento e um só que falhe derruba a
      // consulta. Falhar é o comportamento desejado: vazamento parcial seria pior
      // que erro.
      await semearPalpite(ANA, FUTURO);
      await semearPalpite(BRUNO, FUTURO);
      await assertFails(
        getDocs(
          query(
            collection(como(ANA), `boloes/${POOL}/palpites`),
            where('matchId', '==', FUTURO),
          ),
        ),
      );
    });

    it('MG-3-09 os meus palpites da rodada, esses eu listo', async () => {
      // É o que a tela de palpite faz ao abrir: só os meus, de todos os jogos.
      await semearPalpite(BRUNO, FUTURO);
      await semearPalpite(BRUNO, PASSADO);
      await assertSucceeds(
        getDocs(
          query(
            collection(como(BRUNO), `boloes/${POOL}/palpites`),
            where('userId', '==', BRUNO),
          ),
        ),
      );
    });
  });

  // ---- prazo (PP-5) --------------------------------------------------------

  describe('prazo', () => {
    it('MG-3-10 palpito em jogo com prazo aberto', async () => {
      await assertSucceeds(
        setDoc(
          doc(como(BRUNO), `boloes/${POOL}/palpites/${id(FUTURO, BRUNO)}`),
          palpite(BRUNO, FUTURO),
        ),
      );
    });

    it('MG-3-11 não palpito depois do apito', async () => {
      // `request.time` é o relógio do Google. Adiantar o celular não move nada:
      // quem decide é o banco.
      await assertFails(
        setDoc(
          doc(como(BRUNO), `boloes/${POOL}/palpites/${id(PASSADO, BRUNO)}`),
          palpite(BRUNO, PASSADO),
        ),
      );
    });

    it('MG-3-12 altero o palpite enquanto o prazo está aberto', async () => {
      await semearPalpite(BRUNO, FUTURO);
      await assertSucceeds(
        updateDoc(
          doc(como(BRUNO), `boloes/${POOL}/palpites/${id(FUTURO, BRUNO)}`),
          { homeScore: 3, awayScore: 0, updatedAt: new Date() },
        ),
      );
    });

    it('MG-3-13 não altero depois do apito', async () => {
      await semearPalpite(BRUNO, PASSADO);
      await assertFails(
        updateDoc(
          doc(como(BRUNO), `boloes/${POOL}/palpites/${id(PASSADO, BRUNO)}`),
          { homeScore: 5, awayScore: 5 },
        ),
      );
    });

    it('MG-3-14 bolão encerrado não aceita palpite', async () => {
      await ambiente.withSecurityRulesDisabled(async (ctx) => {
        await updateDoc(doc(ctx.firestore(), `boloes/${POOL}`), {
          closedAt: new Date(),
        });
      });
      await assertFails(
        setDoc(
          doc(como(BRUNO), `boloes/${POOL}/palpites/${id(FUTURO, BRUNO)}`),
          palpite(BRUNO, FUTURO),
        ),
      );
    });

    it('MG-3-15 bolão arquivado é somente leitura', async () => {
      await ambiente.withSecurityRulesDisabled(async (ctx) => {
        await updateDoc(doc(ctx.firestore(), `boloes/${POOL}`), {
          closedAt: new Date(),
          archivedAt: new Date(),
        });
      });
      await assertFails(
        setDoc(
          doc(como(BRUNO), `boloes/${POOL}/palpites/${id(FUTURO, BRUNO)}`),
          palpite(BRUNO, FUTURO),
        ),
      );
    });
  });

  // ---- integridade do palpite ---------------------------------------------

  describe('integridade', () => {
    it('MG-3-16 não palpito no nome de outra pessoa', async () => {
      await assertFails(
        setDoc(
          doc(como(BRUNO), `boloes/${POOL}/palpites/${id(FUTURO, ANA)}`),
          palpite(ANA, FUTURO),
        ),
      );
    });

    it('MG-3-17 id fora do padrão é recusado', async () => {
      // O id determinístico é o que permite à rule saber de quem é o palpite sem
      // ler o documento — e o que faz o reenvio da fila offline sobrescrever em
      // vez de duplicar.
      await assertFails(
        setDoc(
          doc(como(BRUNO), `boloes/${POOL}/palpites/qualquer-coisa`),
          palpite(BRUNO, FUTURO),
        ),
      );
    });

    it('MG-3-18 placar inválido é recusado', async () => {
      const casos = [
        { homeScore: -1 },
        { awayScore: 21 },
        { homeScore: 1.5 },
        { homeScore: '2' },
      ];
      for (const invalido of casos) {
        await assertFails(
          setDoc(
            doc(como(BRUNO), `boloes/${POOL}/palpites/${id(FUTURO, BRUNO)}`),
            palpite(BRUNO, FUTURO, invalido),
          ),
        );
      }
    });

    it('MG-3-19 não escrevo a minha própria pontuação', async () => {
      // Pontuação vem da apuração. Se o cliente pudesse escrever, o participante
      // escolheria os próprios pontos.
      await assertFails(
        setDoc(
          doc(como(BRUNO), `boloes/${POOL}/palpites/${id(FUTURO, BRUNO)}`),
          palpite(BRUNO, FUTURO, { points: 10, band: 'PLACAR_EXATO' }),
        ),
      );
    });

    it('MG-3-20 não libero o sigilo do meu palpite por conta própria', async () => {
      // `matchFinished: true` na criação revelaria o palpite antes do jogo acabar.
      // Quem vira esse flag é a Function do fim do jogo.
      await assertFails(
        setDoc(
          doc(como(BRUNO), `boloes/${POOL}/palpites/${id(FUTURO, BRUNO)}`),
          palpite(BRUNO, FUTURO, { matchFinished: true }),
        ),
      );
    });

    it('MG-3-21 não transfiro palpite para outro dono na alteração', async () => {
      await semearPalpite(BRUNO, FUTURO);
      await assertFails(
        updateDoc(
          doc(como(BRUNO), `boloes/${POOL}/palpites/${id(FUTURO, BRUNO)}`),
          { userId: ANA },
        ),
      );
    });

    it('MG-3-22 palpite não se apaga', async () => {
      // Apagar apagaria a prova da disputa.
      await semearPalpite(BRUNO, FUTURO);
      await assertFails(
        deleteDoc(doc(como(BRUNO), `boloes/${POOL}/palpites/${id(FUTURO, BRUNO)}`)),
      );
    });

    it('MG-3-23 quem não participa não palpita', async () => {
      await assertFails(
        setDoc(
          doc(como(DE_FORA), `boloes/${POOL}/palpites/${id(FUTURO, DE_FORA)}`),
          palpite(DE_FORA, FUTURO),
        ),
      );
    });
  });

  // ---- revisões -------------------------------------------------------------

  describe('revisões', () => {
    beforeEach(async () => {
      await semearPalpite(BRUNO, FUTURO);
      await ambiente.withSecurityRulesDisabled(async (ctx) => {
        await setDoc(
          doc(
            ctx.firestore(),
            `boloes/${POOL}/palpites/${id(FUTURO, BRUNO)}/revisoes/r1`,
          ),
          { homeScore: 1, awayScore: 1, substituidoEm: new Date() },
        );
      });
    });

    it('MG-3-24 leio o histórico do meu palpite', async () => {
      await assertSucceeds(
        getDocs(
          collection(
            como(BRUNO),
            `boloes/${POOL}/palpites/${id(FUTURO, BRUNO)}/revisoes`,
          ),
        ),
      );
    });

    it('MG-3-25 não leio o histórico do palpite de outro', async () => {
      await assertFails(
        getDocs(
          collection(
            como(ANA),
            `boloes/${POOL}/palpites/${id(FUTURO, BRUNO)}/revisoes`,
          ),
        ),
      );
    });

    it('MG-3-26 ninguém escreve revisão pelo cliente', async () => {
      // Prova escrita por quem tem interesse nela não é prova.
      await assertFails(
        setDoc(
          doc(
            como(BRUNO),
            `boloes/${POOL}/palpites/${id(FUTURO, BRUNO)}/revisoes/r2`,
          ),
          { homeScore: 9, awayScore: 9, substituidoEm: new Date() },
        ),
      );
    });
  });
});
