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

/**
 * `MG-2` — participação e papel, aplicados pelo **banco**.
 *
 * Cada caso aqui é a mesma regra que hoje vive num service do NestJS e tem caso
 * de e2e correspondente. O objetivo não é "as rules compilam": é provar que quem
 * não participa não lê, que quem saiu perde o acesso, e que papel não se
 * autopromove.
 */
describe('MG-2 — participação e papel', () => {
  let ambiente: RulesTestEnvironment;

  const POOL = 'p1';
  const ANA = 'ana'; // criadora e admin
  const BRUNO = 'bruno'; // membro comum
  const CARLA = 'carla'; // saiu do bolão
  const DE_FORA = 'defora'; // nunca participou

  const bolao = {
    name: 'Bolão do setor',
    competitionCode: 'BSA',
    matchdayFrom: 23,
    matchdayTo: 25,
    createdBy: ANA,
    pointsExact: 10,
    pointsWinnerDiff: 5,
    pointsWinner: 3,
    tiebreakOrder: ['EXACT_SCORES', 'WINNERS'],
    rulesLockedAt: null,
    closedAt: null,
    closedEarly: false,
    archivedAt: null,
    activeMembers: 2,
  };

  const membro = (uid: string, role: string, leftAt: Date | null = null) => ({
    userId: uid,
    displayName: uid,
    role,
    joinedAt: new Date('2026-08-01'),
    leftAt,
  });

  /** Índice de "meus bolões", sob o caminho do próprio usuário. */
  const meuBolao = (isAdmin: boolean) => ({
    poolId: POOL,
    name: bolao.name,
    matchdayFrom: 23,
    matchdayTo: 25,
    activeMembers: 2,
    isAdmin,
    closed: false,
    archived: false,
    joinedAt: new Date('2026-08-01'),
  });

  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}`), bolao);
      await setDoc(doc(db, `boloes/${POOL}/membros/${ANA}`), membro(ANA, 'ADMIN'));
      await setDoc(
        doc(db, `boloes/${POOL}/membros/${BRUNO}`),
        membro(BRUNO, 'MEMBER'),
      );
      await setDoc(
        doc(db, `boloes/${POOL}/membros/${CARLA}`),
        membro(CARLA, 'MEMBER', new Date('2026-08-05')),
      );
      await setDoc(doc(db, `usuarios/${ANA}/boloes/${POOL}`), meuBolao(true));
      await setDoc(doc(db, `usuarios/${BRUNO}/boloes/${POOL}`), meuBolao(false));
      await setDoc(doc(db, `boloes/${POOL}/classificacao/${ANA}`), {
        userId: ANA,
        displayName: 'Ana',
        points: 40,
        exactScores: 3,
        winners: 5,
        position: 1,
        sharedPosition: false,
        decidedBy: 'PONTOS',
        updatedAt: new Date(),
      });
    });
  });

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

  describe('leitura do bolão', () => {
    it('MG-2-01 participante ativo lê o bolão', async () => {
      await assertSucceeds(getDoc(doc(como(BRUNO), `boloes/${POOL}`)));
    });

    it('MG-2-02 quem não participa não lê', async () => {
      // O bolão é privado: a existência dele e quem está nele são informação de
      // quem participa.
      await assertFails(getDoc(doc(como(DE_FORA), `boloes/${POOL}`)));
    });

    it('MG-2-03 quem saiu perde o acesso na hora', async () => {
      // Saída é definitiva (CV-4): perde a leitura, mas o histórico dele fica.
      await assertFails(getDoc(doc(como(CARLA), `boloes/${POOL}`)));
    });

    it('MG-2-04 anônimo não lê nada', async () => {
      await assertFails(getDoc(doc(anonimo(), `boloes/${POOL}`)));
    });

    it('MG-2-05 participante lê a lista de participantes', async () => {
      await assertSucceeds(getDocs(collection(como(BRUNO), `boloes/${POOL}/membros`)));
    });

    it('MG-2-06 quem não participa não lista participantes', async () => {
      await assertFails(
        getDocs(collection(como(DE_FORA), `boloes/${POOL}/membros`)),
      );
    });
  });

  describe('papel e imutabilidade do criador', () => {
    it('MG-2-07 nem o admin altera a tabela de pontos pela escrita direta', async () => {
      // Este caso **inverteu** em 07/08, ao ligar o app (`MG-7`). Ele afirmava
      // que o admin altera as regras direto no documento — e isso era um furo do
      // `CC-3`: a rule não consegue olhar se já existe palpite no bolão, então
      // ela deixava trocar a pontuação **depois** do primeiro palpite, que é o
      // que a trava existe para impedir. Alterar regra passou a ser callable.
      await assertFails(
        updateDoc(doc(como(ANA), `boloes/${POOL}`), { pointsExact: 20 }),
      );
    });

    it('MG-2-08 participante comum não altera o bolão', async () => {
      // Sem isso, qualquer um muda a pontuação no meio do campeonato.
      await assertFails(
        updateDoc(doc(como(BRUNO), `boloes/${POOL}`), { pointsExact: 20 }),
      );
    });

    it('MG-2-09 nem o admin troca quem é o criador', async () => {
      // `createdBy` imutável é o que sustenta "criador é admin permanente"
      // (CC-8): se desse para reescrever, um segundo admin se declararia criador.
      await assertFails(
        updateDoc(doc(como(ANA), `boloes/${POOL}`), { createdBy: BRUNO }),
      );
    });

    it('MG-2-10 ninguém apaga o bolão', async () => {
      // Encerra ou arquiva — as duas preservam o histórico.
      await assertFails(deleteDoc(doc(como(ANA), `boloes/${POOL}`)));
    });

    it('MG-2-11 participante não se promove a admin', async () => {
      await assertFails(
        updateDoc(doc(como(BRUNO), `boloes/${POOL}/membros/${BRUNO}`), {
          role: 'ADMIN',
        }),
      );
    });

    it('MG-2-12 nem o admin promove pela escrita direta — isso é Function', async () => {
      // Promover mexe em regra (criador não é rebaixado, bolão não fica sem
      // admin). Regra de negócio em rule fica ilegível e sem teste decente.
      await assertFails(
        updateDoc(doc(como(ANA), `boloes/${POOL}/membros/${BRUNO}`), {
          role: 'ADMIN',
        }),
      );
    });

    it('MG-2-13 ninguém remove participante pela escrita direta', async () => {
      await assertFails(
        deleteDoc(doc(como(ANA), `boloes/${POOL}/membros/${BRUNO}`)),
      );
    });
  });

  describe('criação de bolão', () => {
    const novo = { ...bolao, createdBy: DE_FORA, name: 'Bolão novo' };

    it('MG-2-14 autenticado cria bolão sendo o criador', async () => {
      await assertSucceeds(
        setDoc(doc(como(DE_FORA), 'boloes/p2'), novo),
      );
    });

    it('MG-2-15 não dá para criar bolão no nome de outro', async () => {
      await assertFails(
        setDoc(doc(como(DE_FORA), 'boloes/p3'), { ...novo, createdBy: ANA }),
      );
    });

    it('MG-2-16 bolão não nasce encerrado nem com regras travadas', async () => {
      // Nascer encerrado deixaria um bolão que ninguém pode usar e ninguém
      // consegue reabrir; nascer travado impediria o admin de ajustar a tabela
      // antes do primeiro palpite (CC-3).
      await assertFails(
        setDoc(doc(como(DE_FORA), 'boloes/p4'), {
          ...novo,
          closedAt: new Date(),
        }),
      );
      await assertFails(
        setDoc(doc(como(DE_FORA), 'boloes/p5'), {
          ...novo,
          rulesLockedAt: new Date(),
        }),
      );
    });

    it('MG-2-17 o criador cria o próprio vínculo, como admin', async () => {
      await setDoc(doc(como(DE_FORA), 'boloes/p6'), novo);
      await assertSucceeds(
        setDoc(
          doc(como(DE_FORA), `boloes/p6/membros/${DE_FORA}`),
          membro(DE_FORA, 'ADMIN'),
        ),
      );
    });

    it('MG-2-18 e não consegue se inscrever num bolão de outro', async () => {
      await assertFails(
        setDoc(
          doc(como(DE_FORA), `boloes/${POOL}/membros/${DE_FORA}`),
          membro(DE_FORA, 'MEMBER'),
        ),
      );
    });
  });

  describe('classificação materializada', () => {
    it('MG-2-19 participante lê o ranking inteiro em uma consulta', async () => {
      // É o desenho que mantém o custo de pé: uma leitura por linha já pronta,
      // em vez de somar pontos de todos os palpites a cada abertura de tela.
      await assertSucceeds(
        getDocs(collection(como(BRUNO), `boloes/${POOL}/classificacao`)),
      );
    });

    it('MG-2-20 quem não participa não lê o ranking', async () => {
      await assertFails(
        getDocs(collection(como(DE_FORA), `boloes/${POOL}/classificacao`)),
      );
    });

    it('MG-2-21 ninguém escreve na classificação pelo cliente', async () => {
      // Pontuação calculada no cliente não é fonte de verdade. Nem o admin
      // escreve aqui — só a Function de apuração.
      await assertFails(
        updateDoc(doc(como(ANA), `boloes/${POOL}/classificacao/${ANA}`), {
          points: 999,
        }),
      );
      await assertFails(
        setDoc(doc(como(BRUNO), `boloes/${POOL}/classificacao/${BRUNO}`), {
          userId: BRUNO,
          points: 999,
        }),
      );
    });
  });

  describe('calendário e perfil', () => {
    beforeEach(async () => {
      await ambiente.withSecurityRulesDisabled(async (ctx) => {
        await setDoc(doc(ctx.firestore(), 'jogos/m1'), {
          externalId: '554964',
          competitionCode: 'BSA',
          matchday: 23,
          homeTeam: 'Fluminense',
          awayTeam: 'Palmeiras',
          kickoffAt: new Date('2026-08-15T19:30:00Z'),
          status: 'TIMED',
          homeScore: null,
          awayScore: null,
          sourceUpdatedAt: null,
        });
        await setDoc(doc(ctx.firestore(), `usuarios/${ANA}`), {
          displayName: 'Ana',
          email: 'ana@teste.com',
          createdAt: new Date(),
        });
      });
    });

    it('MG-2-22 jogo é legível por quem está autenticado', async () => {
      // Time, horário e placar não são segredo — o segredo é o palpite sobre eles.
      await assertSucceeds(getDoc(doc(como(DE_FORA), 'jogos/m1')));
    });

    it('MG-2-23 cliente não mexe no jogo — moveria o próprio prazo', async () => {
      await assertFails(
        updateDoc(doc(como(ANA), 'jogos/m1'), {
          kickoffAt: new Date('2027-01-01'),
        }),
      );
    });

    it('MG-2-24 perfil é do dono, e ninguém enumera contas', async () => {
      await assertSucceeds(getDoc(doc(como(ANA), `usuarios/${ANA}`)));
      await assertFails(getDoc(doc(como(BRUNO), `usuarios/${ANA}`)));
    });
  });

  describe('meus bolões', () => {
    it('MG-2-26 listo os meus bolões numa consulta, sem leitura extra', async () => {
      // É como a tela inicial monta a lista (CC-10): uma consulta dentro do meu
      // próprio caminho, com o resumo do bolão já no documento.
      //
      // A primeira versão disto tentava consulta de **grupo** em `membros`, e
      // falhou: no `list` de consulta de grupo o `{uid}` do caminho não é
      // vinculado, e a rule não consegue inspecionar o `where` — não há como
      // provar que a pessoa pediu só o que é dela. Daí o índice sob o usuário.
      await assertSucceeds(
        getDocs(collection(como(BRUNO), `usuarios/${BRUNO}/boloes`)),
      );
    });

    it('MG-2-27 não listo os bolões de outra pessoa', async () => {
      await assertFails(
        getDocs(collection(como(BRUNO), `usuarios/${ANA}/boloes`)),
      );
    });

    it('MG-2-28 não me coloco num bolão pela escrita no índice', async () => {
      // O índice é mantido por Function. Se o cliente pudesse escrever, alguém se
      // colocaria num bolão alheio na própria lista — e a lista é o que a tela
      // inicial mostra.
      await assertFails(
        setDoc(doc(como(DE_FORA), `usuarios/${DE_FORA}/boloes/${POOL}`), meuBolao(false)),
      );
    });
  });

  describe('palpite segue fechado até MG-3', () => {
    it('MG-2-25 leitura e escrita de palpite recusadas', async () => {
      // Sigilo (`PP-7`) e prazo (`PP-5`) entram em MG-3. Abrir antes, "para
      // destravar o app", é como um vazamento chega em produção.
      await assertFails(
        setDoc(doc(como(BRUNO), `boloes/${POOL}/palpites/m1_${BRUNO}`), {
          userId: BRUNO,
          matchId: 'm1',
          homeScore: 2,
          awayScore: 1,
        }),
      );
      await assertFails(
        getDocs(collection(como(BRUNO), `boloes/${POOL}/palpites`)),
      );
    });
  });
});
