#!/usr/bin/env python3
# -*- coding: utf-8 -*-
"""
Demanda 32 — O PERFIL DO MEDICO. Monta os cards e ENTREGA.
A chave le-se NO INSTANTE DA ENTREGA (regra medida na D39/D40: outros
enquadradores consomem numeros em paralelo).
"""
import json, os, sqlite3, subprocess, sys

BD = "/srv/fab/estado.db"

def proxima_livre():
    c = sqlite3.connect(BD)
    n = c.execute(
        "select max(cast(substr(chave,4) as integer)) from ("
        " select chave from card where chave like 'CS-%'"
        " union all select chave from demanda where chave like 'CS-%')"
    ).fetchone()[0]
    c.close()
    return int(n) + 1

N = proxima_livre()
EPIC = f"CS-{N}"
K = [f"CS-{N + 1 + i}" for i in range(11)]
B1, B2, B3, B4, B5, B6, F1, F2, F3, F4, F5 = K

def jest(f, nome=None):
    base = f"cd backend && npx jest test/{f} --runInBand --ci"
    return base if nome is None else f"{base} -t '{nome}'"

def pw(f, nome=None):
    base = f"cd frontend && npx playwright test e2e/{f} --reporter=line"
    return base if nome is None else f"{base} -g '{nome}'"

cards = [

# ───────────────────────────── BACKEND ─────────────────────────────
{
 "chave": B1, "territorio": "backend",
 "titulo": ("O PERFIL PASSA A TER ONDE SE GUARDAR — UMA migracao e ZERO rotas: `Especialidade` no molde "
            "EXACTO do `Nucleo` (nome unico + `arquivadoEm` nulo=vivo), `especialidadeId` e `fotoId` "
            "NULAVEIS no `Utilizador`, e `AlteracaoDePerfil`. A lista nasce VAZIA: NAO herda os dez nucleos"),
 "escopo_paths": [
   "backend/prisma/schema.prisma",
   "backend/prisma/migrations/",
   "backend/test/perfil-base.e2e-spec.ts",
 ],
 "aceites": [
   jest("perfil-base.e2e-spec.ts"),
   jest("perfil-base.e2e-spec.ts", "a especialidade grava-se e o segundo com o mesmo nome bate no unico do banco"),
   jest("perfil-base.e2e-spec.ts", "arquivar poe data em arquivadoEm, a linha continua na base, e desarquivar volta a nulo"),
   jest("perfil-base.e2e-spec.ts", "o utilizador aponta a uma especialidade e o ponteiro e nulavel: sem ela grava-se igual"),
   jest("perfil-base.e2e-spec.ts", "a migracao deixa ZERO especialidades e os dez nucleos continuam dez e intactos"),
   jest("perfil-base.e2e-spec.ts", "a alteracao de perfil grava quem mudou, o campo, o valor antigo, o valor novo e quando"),
   jest("perfil-base.e2e-spec.ts", "a segunda alteracao do mesmo campo nao apaga a primeira: ficam duas linhas"),
   jest("perfil-base.e2e-spec.ts", "o utilizador ganha fotoId nulavel e o campo especialidade da aula nao foi tocado"),
 ],
 "depende_de": [],
},
{
 "chave": B2, "territorio": "backend",
 "titulo": ("A SECRETARIA MANDA NA LISTA DE ESPECIALIDADES, SEM DEPLOY — criar, renomear, ARQUIVAR e "
            "DESARQUIVAR no molde do `nucleo.controlador.ts`: ler e de qualquer sessao activa, MUDAR e "
            "SECRETARIA-so, arquivar TEM VOLTA e NADA se apaga"),
 "escopo_paths": [
   "backend/src/modules/especialidade/",
   "backend/src/app.module.ts",
   "backend/test/especialidade.e2e-spec.ts",
 ],
 "aceites": [
   jest("especialidade.e2e-spec.ts"),
   jest("especialidade.e2e-spec.ts", "a secretaria cria Medicina de familia e ela sai na lista com 201"),
   jest("especialidade.e2e-spec.ts", "nome repetido sem caixa, sem acentos e sem espaco a mais da 409 e nao cria segunda linha"),
   jest("especialidade.e2e-spec.ts", "coordenador, medico e especialista leem a lista com 200 e levam 403 em criar, alterar e arquivar"),
   jest("especialidade.e2e-spec.ts", "arquivar responde 200, e idempotente, e a linha continua na base contada"),
   jest("especialidade.e2e-spec.ts", "desarquivar poe arquivadoEm a nulo e a especialidade volta a lista"),
   jest("especialidade.e2e-spec.ts", "a arquivada sai por id com 200 e fica FORA da lista de quem nao e secretaria"),
   jest("especialidade.e2e-spec.ts", "a lista nao traz um unico dos dez nucleos e a tabela de nucleos nao foi lida"),
 ],
 "depende_de": [B1],
},
{
 "chave": B3, "territorio": "backend",
 "titulo": ("`GET /perfil` — os quatro dados e as UPAs dele com a situacao POR EXTENSO (PENDENTE/ACEITO/"
            "REMOVIDO, com quem decidiu e quando), e o PENDENTE ALCANCA-O (`@PermitePendente()` + "
            "`@Papeis(MEDICO)`). MEDIDO: `GET /identidade/eu` NAO SE TOCA"),
 "escopo_paths": [
   "backend/src/modules/perfil/",
   "backend/src/app.module.ts",
   "backend/test/perfil.e2e-spec.ts",
 ],
 "aceites": [
   jest("perfil.e2e-spec.ts"),
   jest("perfil.e2e-spec.ts", "o medico ve nome, email, papel, estado, crm, especialidade e fotoId numa leitura"),
   jest("perfil.e2e-spec.ts", "as tres upas dele saem com a situacao PENDENTE, ACEITO e REMOVIDO por extenso"),
   jest("perfil.e2e-spec.ts", "a upa aceita traz o nome de quem o aceitou e a data, e a pendente traz os dois a nulo"),
   jest("perfil.e2e-spec.ts", "um medico PENDENTE com zero upas aceitas recebe 200 e nao 403"),
   jest("perfil.e2e-spec.ts", "secretaria, coordenador e especialista levam 403 e sem sessao leva 401"),
   jest("perfil.e2e-spec.ts", "a resposta nao traz senhaHash e nao traz dado de nenhum outro medico"),
   jest("perfil.e2e-spec.ts", "o corpo de GET identidade eu continua com os mesmos cinco campos e nem um mais"),
 ],
 "depende_de": [B1],
},
{
 "chave": B4, "territorio": "backend",
 "titulo": ("O MEDICO CORRIGE O CRM E A ESPECIALIDADE E VALE NA HORA (regra 131) — cada alteracao deixa "
            "RASTO na MESMA transaccao, lido por `GET /perfil/alteracoes`. NAO chama "
            "`recalcularEstadoDoMedico`: o estado NAO volta a PENDENTE"),
 "escopo_paths": [
   "backend/src/modules/perfil/",
   "backend/test/perfil-alterar.e2e-spec.ts",
 ],
 "aceites": [
   jest("perfil-alterar.e2e-spec.ts"),
   jest("perfil-alterar.e2e-spec.ts", "corrigir o crm vale na hora: a leitura seguinte traz o valor novo"),
   jest("perfil-alterar.e2e-spec.ts", "um medico ATIVO que corrige o crm continua ATIVO e nenhum vinculo mudou de situacao"),
   jest("perfil-alterar.e2e-spec.ts", "a alteracao grava quem mudou, o campo, o valor antigo, o valor novo e quando"),
   jest("perfil-alterar.e2e-spec.ts", "escolher a especialidade guarda o ponteiro e limpa-la poe-no a nulo, e as duas deixam rasto"),
   jest("perfil-alterar.e2e-spec.ts", "escolher uma especialidade ARQUIVADA da 422 e nada e escrito nas duas tabelas"),
   jest("perfil-alterar.e2e-spec.ts", "o extracto do proprio sai por ordem inversa e nao traz uma linha de outro medico"),
   jest("perfil-alterar.e2e-spec.ts", "um medico PENDENTE corrige o crm e continua PENDENTE, e nao ha rota de aprovar"),
   jest("perfil-alterar.e2e-spec.ts", "crm vazio ou so espacos da 400 com frase que diz o que fazer, e nada e gravado"),
 ],
 "depende_de": [B3],
},
{
 "chave": B5, "territorio": "backend",
 "titulo": ("O MEDICO PEDE UMA UPA E ELA NASCE PENDENTE NA LINHA QUE JA EXISTE, caindo na lista do "
            "COORDENADOR DAQUELA UPA — e NENHUM caminho dele escreve ACEITO. MEDIDO: `GET /upas` NAO "
            "SE TOCA (um teste de FRONTEND fixa-lhe os papeis)"),
 "escopo_paths": [
   "backend/src/modules/vinculo/",
   "backend/test/vinculo-pedir.e2e-spec.ts",
 ],
 "aceites": [
   jest("vinculo-pedir.e2e-spec.ts"),
   jest("vinculo-pedir.e2e-spec.ts", "o medico pede uma upa e nasce UMA linha em PENDENTE, contada na base"),
   jest("vinculo-pedir.e2e-spec.ts", "o pedido aparece na lista do coordenador daquela upa e nao na de outro coordenador"),
   jest("vinculo-pedir.e2e-spec.ts", "pedir a mesma upa duas vezes nao cria segunda linha: o unico medicoId upaId trava"),
   jest("vinculo-pedir.e2e-spec.ts", "quem foi REMOVIDO e pede outra vez volta a PENDENTE na MESMA linha"),
   jest("vinculo-pedir.e2e-spec.ts", "ao voltar a PENDENTE quem o tirou e a data continuam legiveis e nao foram apagados"),
   jest("vinculo-pedir.e2e-spec.ts", "nenhuma rota deste card escreve ACEITO: o medico continua PENDENTE depois de pedir"),
   jest("vinculo-pedir.e2e-spec.ts", "coordenador, secretaria e especialista levam 403 ao pedir, e sem sessao leva 401"),
   jest("vinculo-pedir.e2e-spec.ts", "a lista do que ele pode pedir esconde as upas que ja tem e as upas inactivas"),
   jest("vinculo-pedir.e2e-spec.ts", "GET upas continua a responder so a SECRETARIA e ao COORDENADOR, contado no controlador"),
 ],
 "depende_de": [],
},
{
 "chave": B6, "territorio": "backend",
 "titulo": ("A FOTO: o PROPRIO MEDICO carrega a sua por `POST /perfil/foto`, reusando o carregamento JA "
            "PROVADO (PNG/WEBP/SVG, 512 KB, formato lido do CONTEUDO). MEDIDO: `GET /marcas/:id` NAO "
            "tem `@PermitePendente()` — o PENDENTE levava 403 na sua propria foto"),
 "escopo_paths": [
   "backend/src/modules/perfil/",
   "backend/src/modules/marca/marca.controlador.ts",
   "backend/test/perfil-foto.e2e-spec.ts",
 ],
 "aceites": [
   jest("perfil-foto.e2e-spec.ts"),
   jest("perfil-foto.e2e-spec.ts", "o medico carrega png, webp e svg e os tres ficam guardados e apontados no fotoId"),
   jest("perfil-foto.e2e-spec.ts", "um ficheiro que minta na extensao e recusado com frase que diz o que fazer"),
   jest("perfil-foto.e2e-spec.ts", "acima de 512 KB e recusado com o numero medido e nada vai para o armazem"),
   jest("perfil-foto.e2e-spec.ts", "trocar a foto deixa rasto no extracto com valor antigo e valor novo"),
   jest("perfil-foto.e2e-spec.ts", "um medico PENDENTE carrega a sua foto e consegue ir buscar os bytes com 200"),
   jest("perfil-foto.e2e-spec.ts", "POST marcas continua a recusar o medico com 403: a marca do nucleo fica SECRETARIA-so"),
   jest("perfil-foto.e2e-spec.ts", "o medico nao consegue escrever o fotoId de outro medico nem ler a ficha de outro"),
   jest("perfil-foto.e2e-spec.ts", "os seis cabecalhos que impedem a foto de executar continuam os mesmos"),
 ],
 "depende_de": [B4],
},

# ───────────────────────────── FRONTEND ─────────────────────────────
{
 "chave": F1, "territorio": "frontend",
 "titulo": ("O ECRA `/perfil` NASCE E E PORTA DO MEDICO NO MENU: os quatro dados, as UPAs por extenso e os "
            "SEIS estados. Sem foto ve-se o MONOGRAMA derivado em codigo. O vermelho `error` so em erro "
            "de sistema, NUNCA a marcar o dado do medico"),
 "escopo_paths": [
   "frontend/src/app/perfil/",
   "frontend/src/lib/navegacao.ts",
   "frontend/e2e/navegacao.spec.ts",
   "frontend/e2e/alcance.spec.ts",
   "frontend/e2e/perfil.spec.ts",
 ],
 "aceites": [
   pw("perfil.spec.ts"),
   pw("perfil.spec.ts", "o medico ve a porta Perfil no menu e chega la por clique, sem escrever a morada"),
   pw("perfil.spec.ts", "o ecra mostra os quatro dados e as tres upas com PENDENTE, ACEITO e REMOVIDO escritos"),
   pw("perfil.spec.ts", "os seis estados do ecra de perfil: vazio, a carregar, erro, sem permissao, sucesso e parcial"),
   pw("perfil.spec.ts", "sem permissao nao deixa ver o dado por baixo nem desfocado e diz que papel ve aquilo"),
   pw("perfil.spec.ts", "um medico sem foto ve o monograma das iniciais e o nome dele esta a uma linha do monograma"),
   pw("perfil.spec.ts", "nenhuma peca do perfil usa a cor error para marcar o dado do medico"),
   pw("perfil.spec.ts", "o crm e as datas vao em tabular e alinhados a direita"),
   pw("perfil.spec.ts", "cada campo tem foco de teclado visivel e alvo de toque de 48dp"),
   pw("perfil.spec.ts", "a pagina perfil esta declarada como porta e nao como orfa, e a conta do disco fecha"),
   pw("perfil.spec.ts", "com prefers-reduced-motion reduce as duracoes sao zero e o estado final e o mesmo pixel"),
 ],
 "depende_de": [B3],
},
{
 "chave": F2, "territorio": "frontend",
 "titulo": ("CORRIGE O CRM E ESCOLHE A ESPECIALIDADE DE UMA LISTA (NUNCA texto livre), E VE VALER NA HORA "
            "— E O EXTRACTO LE-SE NO MESMO ECRA (quem, antigo ⟶ novo, quando). Os dois no MESMO card: "
            "o registo e a UNICA rede da regra 131 e NAO se entrega sozinho"),
 "escopo_paths": [
   "frontend/src/app/perfil/",
   "frontend/e2e/perfil-corrigir.spec.ts",
 ],
 "aceites": [
   pw("perfil-corrigir.spec.ts"),
   pw("perfil-corrigir.spec.ts", "corrigir o crm mostra o valor novo na propria peca que mudou, sem recarregar"),
   pw("perfil-corrigir.spec.ts", "nenhuma frase do ecra poe o crm a espera de coordenador nem diz pendente"),
   pw("perfil-corrigir.spec.ts", "a especialidade escolhe-se de um selector e nao ha campo de texto livre no ecra"),
   pw("perfil-corrigir.spec.ts", "o selector traz as especialidades do servidor e nenhum dos dez nucleos"),
   pw("perfil-corrigir.spec.ts", "o extracto le-se no ecra com quem mudou, valor antigo, valor novo e quando"),
   pw("perfil-corrigir.spec.ts", "o extracto vazio diz o que ali vai estar e nao parece avaria"),
   pw("perfil-corrigir.spec.ts", "o erro do servidor volta ao campo que o causou e diz o que fazer"),
   pw("perfil-corrigir.spec.ts", "o botao a trabalhar mantem o rotulo e a largura e fica inactivo"),
   pw("perfil-corrigir.spec.ts", "cada campo e cada acto tem foco de teclado visivel e alvo de 48dp"),
 ],
 "depende_de": [F1, B4, B2],
},
{
 "chave": F3, "territorio": "frontend",
 "titulo": ("PEDE UMA UPA A PARTIR DO PERFIL e ela aparece PENDENTE — e NENHUM caminho do ecra escreve "
            "ACEITO. O ecra DIZ POR PALAVRAS que o CRM vale na hora e a UPA PEDE-SE: ficam diferentes "
            "DE PROPOSITO e NAO se uniformizam"),
 "escopo_paths": [
   "frontend/src/app/perfil/",
   "frontend/e2e/perfil-pedir-upa.spec.ts",
 ],
 "aceites": [
   pw("perfil-pedir-upa.spec.ts"),
   pw("perfil-pedir-upa.spec.ts", "pedir uma upa poe-na na lista dele em PENDENTE com a palavra escrita"),
   pw("perfil-pedir-upa.spec.ts", "o ecra diz que quem aceita e o coordenador daquela upa e nao oferece aceitar"),
   pw("perfil-pedir-upa.spec.ts", "nenhum botao nem caminho do ecra escreve ACEITO nem ATIVO"),
   pw("perfil-pedir-upa.spec.ts", "pedir uma upa que ele ja tem nao poe uma segunda linha no ecra"),
   pw("perfil-pedir-upa.spec.ts", "o ecra diz por palavras que o crm vale na hora e que a upa fica a espera"),
   pw("perfil-pedir-upa.spec.ts", "o selector de upas esconde as que ele ja tem e o vazio diz o que fazer"),
   pw("perfil-pedir-upa.spec.ts", "o erro de pedir fica so no painel afectado e o resto do perfil continua legivel"),
   pw("perfil-pedir-upa.spec.ts", "o acto de pedir e alcancavel por teclado, com foco visivel e alvo de 48dp"),
 ],
 "depende_de": [F2, B5],
},
{
 "chave": F4, "territorio": "frontend",
 "titulo": ("A FOTO NO PERFIL: o medico CARREGA a sua nos tres formatos provados, e o ficheiro que MINTA "
            "NA EXTENSAO e RECUSADO com frase que diz o que fazer. A foto NAO entra em ecra nenhum alem "
            "do perfil neste epic (regra 134: os outros ecras sao demanda propria)"),
 "escopo_paths": [
   "frontend/src/app/perfil/",
   "frontend/e2e/perfil-foto.spec.ts",
 ],
 "aceites": [
   pw("perfil-foto.spec.ts"),
   pw("perfil-foto.spec.ts", "o medico carrega a sua foto e ela DESENHA no perfil, substituindo o monograma"),
   pw("perfil-foto.spec.ts", "os tres formatos png webp e svg sao aceitos e aparecem no ecra"),
   pw("perfil-foto.spec.ts", "ficheiro que minta na extensao e recusado com frase que diz o que fazer"),
   pw("perfil-foto.spec.ts", "ficheiro acima de meio megabyte e recusado com o tamanho medido escrito"),
   pw("perfil-foto.spec.ts", "a foto carregada nao executa nada no browser"),
   pw("perfil-foto.spec.ts", "os seis estados do painel da foto: vazio, a carregar, erro, sem permissao, sucesso e parcial"),
   pw("perfil-foto.spec.ts", "a foto nao aparece em nenhum outro ecra do produto neste epic, contado no disco"),
   pw("perfil-foto.spec.ts", "a imagem leva alt que descreve o que la esta e nao carrega informacao que nao esteja escrita"),
   pw("perfil-foto.spec.ts", "o acto de carregar e alcancavel por teclado e o foco e visivel"),
 ],
 "depende_de": [F3, B6],
},
{
 "chave": F5, "territorio": "frontend",
 "titulo": ("A SECRETARIA CRIA E ARQUIVA ESPECIALIDADES SEM DEPLOY, em `/secretaria/especialidades` com "
            "porta no menu dela: arquivar TEM VOLTA e pede confirmacao, e NENHUM dos dez nucleos aparece "
            "na lista. Vem depois do ecra do medico: as duas portas partilham a tabela FECHADA"),
 "escopo_paths": [
   "frontend/src/app/secretaria/especialidades/",
   "frontend/src/lib/navegacao.ts",
   "frontend/e2e/navegacao.spec.ts",
   "frontend/e2e/alcance.spec.ts",
   "frontend/e2e/especialidades.spec.ts",
 ],
 "aceites": [
   pw("especialidades.spec.ts"),
   pw("especialidades.spec.ts", "a secretaria ve a porta Especialidades no menu e chega la por clique"),
   pw("especialidades.spec.ts", "a secretaria cria Medicina de familia e ela aparece na lista sem recarregar"),
   pw("especialidades.spec.ts", "arquivar pede confirmacao, nao e botao vermelho, e a linha sai da lista viva"),
   pw("especialidades.spec.ts", "desarquivar traz a especialidade de volta e nada foi apagado"),
   pw("especialidades.spec.ts", "nenhum dos dez nucleos aparece nesta lista e o ecra nao fala de nucleos"),
   pw("especialidades.spec.ts", "os seis estados do ecra de especialidades: vazio, a carregar, erro, sem permissao, sucesso e parcial"),
   pw("especialidades.spec.ts", "medico, coordenador e especialista veem SEM PERMISSAO e nao o dado por baixo"),
   pw("especialidades.spec.ts", "nome repetido e recusado e a recusa vai ao campo que a causou"),
   pw("especialidades.spec.ts", "cada acto tem foco de teclado visivel e alvo de toque de 48dp"),
   pw("especialidades.spec.ts", "a pagina esta declarada como porta e nao como orfa, e a conta do disco fecha"),
 ],
 "depende_de": [B2, F1],
},
]

# ── conferencias antes de entregar ────────────────────────────────────────────
chaves = [k["chave"] for k in cards]
assert len(set(chaves)) == len(chaves), "chave repetida"
for k in cards:
    for pai in k["depende_de"]:
        assert pai in chaves, f"{k['chave']} depende de {pai}, que nao e card deste epic"
    for p in k["escopo_paths"]:
        assert p not in ("backend/src", "backend/src/", "frontend/src", "frontend/src/"), \
            f"cerca larga em {k['chave']}: {p}"
    assert k["aceites"], f"{k['chave']} sem aceite"
    assert len(k["titulo"]) <= 320, f"titulo de {k['chave']} tem {len(k['titulo'])}"

# profundidade + ciclos
prof = {}
def profundidade(ch, visto=()):
    assert ch not in visto, f"ciclo em {ch}"
    if ch in prof: return prof[ch]
    pais = next(k for k in cards if k["chave"] == ch)["depende_de"]
    prof[ch] = 1 + max((profundidade(p, visto + (ch,)) for p in pais), default=0)
    return prof[ch]
for ch in chaves: profundidade(ch)

destino = "/srv/produtos/capacitasalvador/escopo/ENQUADRAMENTO-D32.cards.json"
with open(destino, "w", encoding="utf-8") as fh:
    json.dump(cards, fh, ensure_ascii=False, indent=1)

texto_pedido = (f"{EPIC}: <titulo>\n{len(cards)} cards\n\n" +
                "\n".join(f"· {k['chave']} [{k['territorio']}] {k['titulo']}" +
                          (f"  (depois de {', '.join(k['depende_de'])})" if k["depende_de"] else "")
                          for k in cards))

print(f"epic {EPIC} · cards {chaves[0]}..{chaves[-1]} · {len(cards)} cards")
print("profundidade:", {c: prof[c] for c in chaves})
print("raizes (sem pai):", [k["chave"] for k in cards if not k["depende_de"]])
print("aviso ao dono:", len(texto_pedido), "caracteres (tecto pratico do Telegram ~4096)")
print("cards em", destino)

if "--entregar" in sys.argv:
    r = subprocess.run(["fab", "enquadrar", "32", "--chave", EPIC, "--cards", json.dumps(cards, ensure_ascii=False)],
                       capture_output=True, text=True)
    print(r.stdout); print(r.stderr, file=sys.stderr)
    sys.exit(r.returncode)
