"""Roda o JavaScript do copiloto da Dra. Suellen num DOM falso e exercita telas e ações."""
import json
import re
import sys

import quickjs

html = open(sys.argv[1], encoding='utf-8').read()
script = re.findall(r'<script>(.*?)</script>', html, re.S)[0]
fim = script.rfind('})();')
exporta = ('\nglobalThis.__t = { get estado() { return estado; }, ACOES, render, responder, htmlMsg, definirModo, '
           'statusDe, conferirPeca, legendaDe, conferirResposta, parceira, ocupacaoDe, roteiroDe, precisaVisita, textoDe, get modo() { return modo; }, '
           'paciente, grupoDe, faltasCadastro, pacientesFiltrados, conferirResposta, metaCadastro, agendaMonielle, '
           'situacaoCurta, pagamentosDe, conferirPeca, extratoDe, comissoesDoMes, TIPOS, PRECOS, ranking, '
           'proximoPasso, medidaDe, peca, visaoParceira, CAMPANHA, PRODUTOS, preco, recebidoPorTipo, ticketMedio, '
           'receitaPorProfissional, previsao, recorrencia, valorPorOrigem, livresNoDia, livresEm7, proximaAvaliacaoLivre, '
           'podeReservar, projecaoAna, semanaQueLota, receitaEmJogo, achados, cotas, nDecisoes, nEsperando, ROTEIROS, '
           'AGENDA7, LIVRES_SUELLEN, MINIMO_AVALIACAO, PROJ_ANA, META_RECEITA, FECHAMENTO, CAPT_BASE, TEXTO_MMP, '
           'TEXTO_PREPARO, PEDIDO_INSUMOS, util, diasAte, dd, MESES_LONGOS, principais, lembrancinhas, lembrancinhasDoDia };\n')
script = script[:fim] + exporta + script[fim:]

# Só leitura: os insumos no copiloto da Fer, que faz a gestão do estoque (Luiz, 07/10/2026). Enquanto o bloco não
# existir lá, a leitura devolve null, e o teste pula a comparação com um aviso.
PROTOS = r'C:\Claude\DraSuellenApp\prototipos'
def _bloco(arquivo, inicio, fim_bloco):
    t = open(PROTOS + '\\' + arquivo, encoding='utf-8').read()
    if inicio not in t:
        return None
    i = t.index(inicio)
    return t[i:t.index(fim_bloco, i) + len(fim_bloco)]
_INSUMOS = _bloco('copiloto-recepcao.html', 'const INSUMOS_BASE = [', '];')
OUTROS = ('globalThis.__INSUMOS_FER = ' + ('(function () { ' + _INSUMOS + ' return INSUMOS_BASE; })()' if _INSUMOS else 'null') + ';\n')

SHIM = r'''
var __DataReal = globalThis.Date;
var __AGORA = new __DataReal(2026, 9, 6, 9, 20, 0).getTime();
globalThis.Date = function Date(a, b, c, d, e, f, g) {
  if (!(this instanceof Date)) return new __DataReal(__AGORA).toString();
  if (arguments.length === 0) return new __DataReal(__AGORA);
  if (arguments.length === 1) return new __DataReal(a);
  return new __DataReal(a, b, c === undefined ? 1 : c, d || 0, e || 0, f || 0, g || 0);
};
globalThis.Date.now = function () { return __AGORA; };
globalThis.Date.UTC = __DataReal.UTC;
globalThis.Date.parse = __DataReal.parse;
globalThis.Date.prototype = __DataReal.prototype;
var __handlers = {};
var __timers = [];
function setTimeout(fn) { __timers.push(fn); return __timers.length; }
function clearTimeout() {}
function __flush() { var n = 0; while (__timers.length && n < 200) { __timers.shift()(); n++; } }
function ListaClasses() { this.s = new Set(); }
ListaClasses.prototype.add = function (c) { this.s.add(c); };
ListaClasses.prototype.remove = function (c) { this.s.delete(c); };
ListaClasses.prototype.toggle = function (c, f) { if (f === undefined) f = !this.s.has(c); if (f) this.s.add(c); else this.s.delete(c); return f; };
ListaClasses.prototype.contains = function (c) { return this.s.has(c); };
function El(id) { this.id = id; this.classList = new ListaClasses(); this.innerHTML = ''; this.textContent = ''; this.hidden = false; this.dataset = {}; this.style = {}; this.attrs = {}; this.scrollTop = 0; this.scrollHeight = 0; this.value = ''; }
Object.defineProperty(El.prototype, 'className', { get: function () { return Array.from(this.classList.s).join(' '); }, set: function (v) { this.classList.s = new Set(String(v).split(/\s+/).filter(Boolean)); } });
Object.defineProperty(El.prototype, 'lastElementChild', { get: function () { return new El('ultimo'); } });
El.prototype.setAttribute = function (k, v) { this.attrs[k] = String(v); };
El.prototype.getAttribute = function (k) { return k in this.attrs ? this.attrs[k] : null; };
El.prototype.removeAttribute = function (k) { delete this.attrs[k]; };
El.prototype.hasAttribute = function (k) { return k in this.attrs; };
El.prototype.querySelector = function () { return null; };
El.prototype.querySelectorAll = function () { return []; };
El.prototype.closest = function () { return null; };
El.prototype.focus = function () {};
El.prototype.scrollIntoView = function () {};
El.prototype.insertAdjacentHTML = function (pos, h) { this.innerHTML += h; };
El.prototype.getBoundingClientRect = function () { return { top: 0, left: 0, width: 0, height: 0 }; };
El.prototype.appendChild = function (c) { return c; };
El.prototype.remove = function () {};
El.prototype.setSelectionRange = function () {};
var __els = {};
function getEl(id) { if (!__els[id]) __els[id] = new El(id); return __els[id]; }
var CONHECIDOS = ['tela', 'horaTopo', 'horaAparelho', 'painel', 'veu', 'toast', 'botaoTema', 'telaApp', 'botaoGirar', 'textoPrevia'];
['painel', 'veu', 'toast'].forEach(function (id) { getEl(id).hidden = true; });
var document = {
  documentElement: new El('html'),
  body: new El('body'),
  activeElement: null,
  querySelector: function (s) {
    var m = /^#([\w-]+)$/.exec(s);
    if (!m) return null;
    var id = m[1];
    if (CONHECIDOS.indexOf(id) >= 0) return getEl(id);
    var p = __els.painel;
    if (id === 'chatDigitando') {
      var cm = __els.chatMensagens;
      if (!cm || cm.innerHTML.indexOf('id="chatDigitando"') < 0) return null;
      var d = new El('chatDigitando');
      d.remove = function () { cm.innerHTML = cm.innerHTML.replace(/<div class="msg msg--ia" id="chatDigitando">[\s\S]*?<\/div><\/div>/, ''); };
      return d;
    }
    if (!p.hidden && p.innerHTML.indexOf('id="' + id + '"') >= 0) return getEl(id);
    if (__els.tela && __els.tela.innerHTML.indexOf('id="' + id + '"') >= 0) return getEl(id);
    return null;
  },
  querySelectorAll: function () { return []; },
  addEventListener: function (t, fn) { (__handlers[t] = __handlers[t] || []).push(fn); },
  contains: function () { return true; },
  createElement: function () { return new El('novo'); },
};
var window = globalThis;
window.innerWidth = 1440;
window.innerHeight = 900;
window.matchMedia = function (q) { return { matches: /min-width: (1024|760)px|pointer: fine/.test(q) }; };
window.scrollTo = function () {};
window.addEventListener = function (t, fn) { (__handlers[t] = __handlers[t] || []).push(fn); };
var location = { _h: '', get hash() { return this._h; }, set hash(v) { v = String(v); this._h = v ? (v[0] === '#' ? v : '#' + v) : ''; (__handlers.hashchange || []).forEach(function (fn) { fn(); }); } };
var localStorage = { getItem: function () { return null; }, setItem: function () {} };
var navigator = { clipboard: { writeText: function () { return Promise.resolve(); } } };
'''

TESTES = r'''
var __out = [];
var T = globalThis.__t;
function run(nome, fn) { try { fn(); __flush(); __out.push('ok    ' + nome); } catch (err) { __out.push('FALHA ' + nome + ': ' + err + ' | ' + String((err && err.stack) || '').split('\n').slice(0, 3).join(' / ')); } }
// Confere no HTML e no texto visível (sem as tags): os nomes de pacientes viraram botões que abrem a ficha.
function semTags(h) { return String(h).replace(/<[^>]+>/g, ''); }
// O espaço fixo entre "R$" e o número (para não quebrar de linha) conta como espaço comum na conferência.
function nb(s) { return String(s).replace(/ /g, ' '); }
function tem(trecho, onde) { var h = nb(onde === undefined ? __els.tela.innerHTML : onde); trecho = nb(trecho); if (h.indexOf(trecho) < 0 && semTags(h).indexOf(trecho) < 0) throw new Error('não achei: ' + trecho); }
function naoTem(trecho, onde) { var h = nb(onde === undefined ? __els.tela.innerHTML : onde); trecho = nb(trecho); if (h.indexOf(trecho) >= 0 || semTags(h).indexOf(trecho) >= 0) throw new Error('não devia aparecer: ' + trecho); }
function painel() { return __els.painel.innerHTML; }
function ultimaMsg() { var c = T.estado.chat; return T.htmlMsg(c[c.length - 1]); }
function igual(a, b, msg) { if (a !== b) throw new Error((msg || 'valor') + ': esperava ' + b + ', veio ' + a); }
function ctxCom(mapa, dados) { var c = new El('ctx'); c.dataset = dados || {}; c.querySelector = function (s) { return mapa[s] || null; }; c.querySelectorAll = function (s) { return mapa['TODOS ' + s] || []; }; return c; }
function campo(v) { var e = new El('campo'); e.value = v; return e; }
function opc(v) { var e = new El('opcao'); e.dataset = { valor: v }; return e; }
function botao(acao, dados, ctx) {
  var e = new El('botao');
  e.dataset = Object.assign({ acao: acao }, dados || {});
  e.closest = function (s) { if (s === '[data-contexto]') return ctx || null; return null; };
  return e;
}
function ir(h) { location.hash = h; }
// O Hoje em grupos (09/10/2026): abre um grupo; com '', volta à página do Hoje.
function grupo(g) { T.ACOES['abrir-grupo'](botao('abrir-grupo', { gid: g })); }
function pc(id) { return T.estado.pecas.filter(function (x) { return x.id === id; })[0]; }

run('abre em Hoje', function () {
  tem('Bom dia, doutora.'); tem('Esperando você'); tem('Crescimento da semana'); tem('Sua agenda hoje'); tem('Para você saber');
  // André Lacerda (a Lina Costa mandou o contato dele, há 1 dia) entrou nos contatos novos: 8 em 7 dias, cinco pelas parceiras.
  tem('8 pessoas novas procuraram'); tem('cinco delas pelas parceiras');
  // As decisões: as 2 perguntas da Fer, o kit da Vintage, a lembrancinha da Navalha de Ouro, os 2 textos fixos e o pedido
  // de insumos da Fer; os agentes deixam 2 oportunidades. A programação de duas semanas tem 5 peças esperando a doutora.
  tem('7 decisões, 2 oportunidades e 5 posts'); tem('Antes e agora pronto');
  tem('A Dom Bigode e a Barbearia do Zé pararam de indicar');
  tem('Camila Duarte'); tem('Veio pela Navalha de Ouro');
  // Desde 09/10/2026, o Hoje mostra uma linha por grupo, e o que cada grupo tem abre na tela dele.
  ['fer', 'marketing', 'parcerias', 'crescimento', 'receita', 'textos'].forEach(function (g) { tem('data-gid="' + g + '"'); });
  grupo('marketing'); tem('5 posts esperam a sua aprovação'); tem('Hoje é dia de post'); tem('Voltar ao Hoje');
  grupo('parcerias'); tem('4 visitas na '); tem('Parceira do mês: Navalha de Ouro'); tem('Salões: um caminho novo');
  grupo('crescimento'); tem('Juliana Prado indicou 3 pessoas');
  grupo('receita'); tem('Receita em jogo'); tem('3 pacientes terminaram o ciclo sem reavaliação marcada'); naoTem('Otávio Ramos fechou há 3 dias'); naoTem('1 recebimento a lançar');
  grupo(''); if (T.estado.grupo !== null) throw new Error('não voltou ao Hoje');
});
run('regras das parceiras', function () {
  igual(T.statusDe(T.parceira('navalha')), 'forte', 'navalha');
  igual(T.statusDe(T.parceira('dombigode')), 'esfriando', 'dom bigode');
  igual(T.statusDe(T.parceira('ze')), 'parada', 'zé');
  igual(T.statusDe(T.parceira('vintage')), 'nova', 'vintage');
  igual(T.statusDe(T.parceira('porto')), 'conhecer', 'porto');
  igual(T.statusDe(T.parceira('barberclub')), 'ativa', 'barber club');
  if (!T.precisaVisita(T.parceira('vintage'))) throw new Error('vintage sem kit precisa de visita');
  if (T.precisaVisita(T.parceira('navalha'))) throw new Error('navalha visitada há 18 dias não precisa');
});
run('decisões: responder some da lista e avisa a Fer', function () {
  T.ACOES.decidir(botao('decidir', { id: 'pix', valor: 'sim' }));
  igual(T.estado.respostas.pix, 'Pode oferecer', 'resposta');
  tem('Respondida'); tem('6 decisões, 2 oportunidades e 5 posts');
  T.ACOES.decidir(botao('decidir', { id: 'kit', valor: 'fer' }));
  if (!T.parceira('vintage').kitPedido) throw new Error('kit não pedido');
  T.ACOES.decidir(botao('decidir', { id: 'kit', valor: 'eu' }));
  igual(T.estado.respostas.kit, 'Pedir para a Fer separar', 'não pode responder duas vezes');
});
run('reservar a visita muda o resumo e a agenda', function () {
  var antes = T.ocupacaoDe('suellen').pct;
  T.ACOES.reservar(botao('reservar', { id: 'r1' }));
  if (!T.estado.reservados.r1) throw new Error('não reservou');
  tem('já está na sua agenda'); grupo('parcerias'); tem('Na sua agenda'); grupo('');
  if (T.ocupacaoDe('suellen').pct <= antes) throw new Error('ocupação não subiu');
  T.ACOES['cancelar-reserva'](botao('cancelar-reserva', { id: 'r1' }));
  if (T.estado.reservados.r1) throw new Error('não tirou');
  T.ACOES.reservar(botao('reservar', { id: 'r1' }));
});
run('consulta: resumo antes e resultado depois', function () {
  T.ACOES['abrir-consulta'](botao('abrir-consulta', { id: 'camila' }));
  tem('Antes de entrar', painel()); tem('Navalha de Ouro', painel()); tem('A Navalha de Ouro mandou o contato dela', painel()); naoTem('Termo de uso de imagem', painel()); tem('prontuário continua no Clinicorp', painel());
  var f = T.estado.capt.d30.fecharam;
  T.ACOES.resultado(botao('resultado', { id: 'camila', valor: 'fechou' }));
  igual(T.estado.capt.d30.fecharam, f + 1, 'fecharam');
  igual(T.parceira('navalha').fech90, 5, 'navalha fecharam');
  igual(T.estado.aLancar.length, 2, 'a lançar');
  tem('A Fer lança o pacote e os valores, R$ 2.600, e marca a série: as 6 sessões', painel());
  T.ACOES.resultado(botao('resultado', { id: 'camila', valor: 'pensar' }));
  igual(T.estado.consultas.camila, 'fechou', 'não muda depois de registrado');
  T.ACOES['abrir-consulta'](botao('abrir-consulta', { id: 'jorge' }));
  tem('#comparar/jorge', painel()); tem('Abrir o antes e agora na estação', painel()); // 13/10: a estação de fotos
  tem('Novo ciclo', painel()); tem('Alta', painel());
  T.ACOES.resultado(botao('resultado', { id: 'jorge', valor: 'alta' }));
  tem('60 e aos 90 dias', painel());
  T.ACOES.fechar();
});
run('marketing e captação', function () {
  ir('captacao');
  tem('Marketing da clínica.');
  ir('marketing');
  // A programação das próximas duas semanas (Luiz, 07/10/2026): 8 posts; a linha Parcerias fica em espera.
  tem('A Fer mandou a programação das próximas duas semanas'); tem('8 posts'); tem('Esperando você'); tem('Programação das próximas duas semanas');
  tem('Esta semana'); tem('Próxima semana'); naoTem('Daqui a 3 semanas'); tem('Em espera: a linha Parcerias'); tem('Falta a autorização do paciente');
  tem('Ideias para o seu vídeo'); tem('Como funciona'); tem('No dia</strong>');
  tem('De onde vêm os pacientes'); tem('Do contato ao tratamento'); tem('Por origem'); tem('Meta: 15 tratamentos novos');
  tem('Bruno Teixeira'); tem('Henrique Lopes'); tem('Sem resposta'); tem('Pacientes que mais indicam');
  T.ACOES['filtrar-posts'](botao('filtrar-posts', { filtro: 'resultado' }));
  igual(T.estado.filtroPosts, 'resultado', 'filtro');
  tem('Antes e agora: entradas'); tem('Antes e agora: coroa, protocolo completo'); naoTem('Depoimento: o que mudou na rotina');
  T.ACOES['filtrar-posts'](botao('filtrar-posts', { filtro: 'todos' }));
  T.ACOES['agradecer-paciente'](botao('agradecer-paciente', { nome: 'Aline Castilho' }));
  tem('Na lista da Fer');
});
run('marketing: aprovar, ajustar, gravar e publicar', function () {
  ir('marketing');
  T.ACOES['aprovar-peca'](botao('aprovar-peca', { id: 'p1' }));
  igual(pc('p1').estado, 'aprovado', 'p1 aprovada');
  T.ACOES['abrir-peca'](botao('abrir-peca', { id: 'p1' }));
  tem('Na programação, para', painel()); tem('Copiar a legenda', painel()); tem('Já publiquei', painel());
  T.ACOES['publicada-peca'](botao('publicada-peca', { id: 'p1' }));
  igual(pc('p1').estado, 'publicado', 'p1 publicada');
  T.ACOES['abrir-peca'](botao('abrir-peca', { id: 'p6' }));
  tem('A conferência fixa não apontou nada', painel()); tem('Conheça a Ana Carolina', painel()); tem('Volta para a Fer', painel()); tem('Falta foto', painel()); tem('Outra versão', painel()); tem('Não vai', painel()); // Luiz, 07/10/2026: vai, não vai, falta foto ou outra versão
  // A doutora pode ajustar ou sugerir a legenda, e a conferência roda de novo sobre o texto dela.
  tem('Legenda: ajuste ou sugira a sua', painel()); tem('a conferência roda de novo sobre o seu texto', painel());
  T.ACOES['ajuste-peca'](botao('ajuste-peca', { id: 'p3', pedido: 'Trocar a foto' }));
  igual(pc('p3').estado, 'ajuste', 'p3 em ajuste');
  tem('Você pediu: trocar a foto');
  T.ACOES['gravei-peca'](botao('gravei-peca', { id: 'p4' }));
  igual(pc('p4').estado, 'aprovar', 'p4 gravado');
  var ruim = ctxCom({ '#leg-p4': campo('Resultado garantido: cabelo de volta em 3 meses!') });
  T.ACOES['aprovar-peca'](botao('aprovar-peca', { id: 'p4' }, ruim));
  igual(pc('p4').estado, 'aprovar', 'promessa não passa');
  var caixa = new El('caixa');
  var ctxIn = ctxCom({ '[data-conferido]': caixa });
  __handlers.input[0]({ target: { dataset: { legenda: 'p4' }, value: 'Quem toma minoxidil pode fazer?', closest: function () { return ctxIn; } } });
  tem('Fala de medicamento', caixa.innerHTML);
  var bom = ctxCom({ '#leg-p4': campo('A tricoscopia dói? A Dra. Suellen explica como é o exame.') });
  T.ACOES['aprovar-peca'](botao('aprovar-peca', { id: 'p4' }, bom));
  igual(pc('p4').estado, 'aprovado', 'p4 aprovada');
  T.ACOES.fechar();
});
run('carrossel: passar as fotos', function () {
  ir('marketing');
  // O carrossel que espera a doutora é o do que levar na primeira consulta, com 3 cartões.
  tem('1/3'); tem('Ver as 3 fotos');
  T.ACOES['abrir-peca'](botao('abrir-peca', { id: 'p1' }));
  tem('1 de 5', painel()); tem('Próxima foto', painel()); tem('Ir para a foto 5', painel());
  T.ACOES.slide(botao('slide', { id: 'p1', passo: '1' }));
  igual(T.estado.slides.p1, 1, 'avançou');
  tem('Etapa 1 de 4', __els['carrossel-p1'].innerHTML);
  T.ACOES.slide(botao('slide', { id: 'p1', ir: '4' }));
  igual(T.estado.slides.p1, 4, 'pontinho');
  tem('A explicação', __els['carrossel-p1'].innerHTML);
  T.ACOES.slide(botao('slide', { id: 'p1', passo: '1' }));
  igual(T.estado.slides.p1, 4, 'não passa do fim');
  __handlers.keydown[0]({ key: 'ArrowLeft', target: { closest: function () { return null; } }, preventDefault: function () {} });
  igual(T.estado.slides.p1, 3, 'seta do teclado');
  T.ACOES.fechar();
  // A peça em espera, da linha Parcerias, também abre como vai ficar.
  T.ACOES['abrir-peca'](botao('abrir-peca', { id: 'p2' }));
  tem('Parceira do mês', painel()); tem('1 de 4', painel()); tem('em espera', painel());
  T.ACOES.slide(botao('slide', { id: 'p2', passo: '1' }));
  tem('Foto da fachada', __els['carrossel-p2'].innerHTML);
  T.ACOES.fechar();
  __handlers.keydown[0]({ key: 'ArrowRight', target: { closest: function () { return null; } }, preventDefault: function () {} });
  igual(T.estado.slides.p2, 1, 'sem painel, a seta não mexe');
});
run('Luiz, 07/10 · antes e agora: a doutora mostra na consulta, e a Fer manda no mesmo dia, com o link do Google', function () {
  T.ACOES.reiniciar(botao('reiniciar'));
  ir('hoje');
  naoTem('Mandar a comparação para Jorge Almeida');
  if (T.ACOES['retorno-enviado']) throw new Error('quem manda é a Fer');
  T.ACOES['abrir-consulta'](botao('abrir-consulta', { id: 'jorge' }));
  tem('Evolução: antes e agora', painel()); tem('Fotos ilustrativas', painel()); tem('a Fer recebe a mensagem pronta e manda a comparação no mesmo dia', painel());
  T.ACOES.resultado(botao('resultado', { id: 'jorge', valor: 'alta' }));
  tem('Oi, Jorge! Aqui é a Fer', painel()); tem('Na lista da Fer', painel()); tem('junto com o link das avaliações do Google', painel()); tem('como imagem pelo WhatsApp da clínica', painel()); naoTem('link privado', painel());
  naoTem('Marcar como enviado', painel()); naoTem('Quem manda é você', painel());
  var msg = /id="retorno-jorge">([^<]*)</.exec(painel());
  if (!msg || /melhor|resultado/.test(msg[1])) throw new Error('a mensagem não pode avaliar o resultado');
  T.ACOES.fechar();
  tem('Antes e agora de Jorge Almeida'); tem('a Fer manda a comparação hoje, antes de ele ir embora, junto com o link das avaliações do Google');
  T.ACOES['abrir-ficha'](botao('abrir-ficha', { id: 'jorge' }));
  tem('Mandar o antes e agora hoje, antes de o paciente ir embora, com o link das avaliações do Google.', painel());
  T.ACOES.fechar();
  T.ACOES['abrir-consulta'](botao('abrir-consulta', { id: 'leticia' }));
  naoTem('Evolução: antes e agora', painel());
  T.ACOES.fechar();
  T.ACOES.reiniciar(botao('reiniciar'));
});
run('Luiz, 07/10 · programação das próximas duas semanas: sem lote, autorização, post do dia e resultado da reavaliação', function () {
  T.ACOES.reiniciar(botao('reiniciar'));
  ir('marketing');
  naoTem('Aprovar os'); if (T.ACOES['aprovar-lote']) throw new Error('sem aprovar em lote: a doutora decide cada peça');
  tem('Programação das próximas duas semanas'); tem('Esta semana'); tem('Próxima semana'); naoTem('Daqui a 3 semanas');
  T.estado.pecas.filter(function (x) { return x.estado !== 'espera'; }).forEach(function (x) { if (x.em > 13) throw new Error('fora das duas semanas: ' + x.id); });
  // Um dia de publicação a cada dois ou três dias; o Stories sai de manhã, no dia de um post.
  var dias = []; T.estado.pecas.forEach(function (x) { if (x.estado !== 'espera' && dias.indexOf(x.em) < 0) dias.push(x.em); });
  dias.sort(function (a, b) { return a - b; }); igual(dias.join(','), '0,2,4,7,9,11,13', 'dias de publicação');
  tem('com um dia de publicação a cada dois ou três dias');
  // No mesmo dia, o Stories da manhã vem antes do post da noite.
  var cal = calendario(); if (cal.indexOf('Conheça a sala de fototerapia') > cal.indexOf('Antes e agora: coroa, protocolo completo')) throw new Error('ordem do dia');
  igual(pc('p7').estado, 'termo', 'resultado sem autorização');
  T.ACOES['abrir-peca'](botao('abrir-peca', { id: 'p7' }));
  tem('Falta a autorização do paciente', painel());
  T.ACOES['aprovar-peca'](botao('aprovar-peca', { id: 'p7' }));
  igual(pc('p7').estado, 'termo', 'sem autorização não aprova');
  T.ACOES['autorizacao-ok'](botao('autorizacao-ok', { id: 'p7' }));
  igual(pc('p7').estado, 'aprovar', 'com autorização vai para a doutora');
  T.ACOES.fechar();
  ir('hoje'); grupo('marketing');
  tem('Hoje é dia de post'); tem('Bastidores: a recepção da clínica');
  T.ACOES['publicada-peca'](botao('publicada-peca', { id: 'p0' }));
  igual(pc('p0').estado, 'publicado', 'post do dia');
  naoTem('Hoje é dia de post'); grupo('');
  T.ACOES['abrir-consulta'](botao('abrir-consulta', { id: 'jorge' }));
  T.ACOES.resultado(botao('resultado', { id: 'jorge', valor: 'alta' }));
  tem('Também pode virar post de resultado', painel());
  T.ACOES['virar-post'](botao('virar-post', { id: 'jorge' }));
  var r = pc('r-jorge');
  if (!r || r.estado !== 'termo' || r.autorizacao) throw new Error('post de resultado da reavaliação');
  tem('Autorização pedida', painel()); tem('o post entra na programação', painel());
  T.ACOES.fechar();
  ir('marketing');
  tem('Para a próxima programação', calendario());
  T.ACOES.reiniciar(botao('reiniciar'));
});
run('parceiras: ranking, divulgação e filtros', function () {
  ir('parceiras');
  tem('Quem indica a clínica.'); tem('Indicações nos últimos 90 dias'); tem('Divulgar as parceiras'); tem('Material para salões');
  // Com as visagistas Lina Costa (4 e 3) e Ateliê Bruno Rocha (0 e 0), o total passou de 21 contra 24 para 25 contra 27.
  tem('No total, foram 25, contra 27 nos 90 dias anteriores.');
  T.ACOES.filtrar(botao('filtrar', { filtro: 'esfriando' }));
  tem('Dom Bigode Barbearia'); tem('Barbearia do Zé'); naoTem('Barber Club Bigorrilho');
  T.ACOES.filtrar(botao('filtrar', { filtro: 'conhecer' }));
  tem('Barbearia do Porto'); tem('Navalha &amp; Café'); naoTem('Dom Bigode Barbearia');
  T.ACOES.filtrar(botao('filtrar', { filtro: 'todas' }));
});
run('parceira: painel com kit e textos prontos', function () {
  T.ACOES['abrir-parceira'](botao('abrir-parceira', { id: 'navalha' }));
  tem('Kit da parceira', painel()); tem('Kit das barbearias: cartões da clínica com o WhatsApp, o jeito de indicar e o treinamento de 15 minutos.', painel()); naoTem('NAVALHA', painel()); naoTem('viewBox="0 0 25 25"', painel()); tem('manda o contato dele para o WhatsApp da clínica', painel()); tem('só números gerais', painel());
  naoTem('data-tipo="post"', painel());
  T.ACOES.texto(botao('texto', { tipo: 'barbeiro', id: 'navalha' }));
  tem('Mensagem pronta para o barbeiro', painel()); tem('Segue o contato de um cliente', painel()); tem('Ele autorizou passar o contato.', painel()); naoTem('código', painel()); tem('data-acao="copiar-msg"', painel()); naoTem('Copiar o texto', painel());
  T.ACOES.texto(botao('texto', { tipo: 'agradecimento', id: 'ze' }));
  tem('parceria de sempre', painel());
  T.ACOES.texto(botao('texto', { tipo: 'barbeiro', id: 'bella' }));
  tem('Mensagem pronta para a equipe', painel()); tem('Aqui é Cláudia, do Espaço Bella', painel()); naoTem('veio pelo Espaço Bella', painel());
  T.ACOES.copiar(botao('copiar', { alvo: 'texto-3' }));
  T.ACOES['abrir-parceira'](botao('abrir-parceira', { id: 'porto' }));
  tem('Como apresentar a parceria', painel());
  T.ACOES.fechar();
});
run('visitas: roteiros e registro', function () {
  ir('visitas');
  tem('Roteiro da semana.'); tem('Água Verde, Rebouças e Portão'); tem('Centro, Mercês e Juvevê'); tem('Roteiro da conversa'); tem('Nunca falar de paciente pelo nome');
  tem('Fora dos roteiros'); tem('Visitas feitas');
  T.ACOES['registrar-visita'](botao('registrar-visita', { parceira: 'dombigode', roteiro: 'r1' }));
  tem('Registrar visita', painel()); tem('Márcio', painel());
  var n = T.estado.visitas.length;
  var vazio = ctxCom({ '#vis-parceira': campo('dombigode') }, { roteiro: 'r1' });
  T.ACOES['salvar-visita'](botao('salvar-visita', {}, vazio));
  igual(T.estado.visitas.length, n, 'sem clima não salva');
  var ctx = ctxCom({ '#vis-parceira': campo('dombigode'), '#vis-quem': campo('Márcio'), '#vis-nota': campo('Pediu cartões novos.'), 'TODOS .opcao[data-grupo="clima"][aria-pressed="true"]': [opc('boa')], 'TODOS .opcao[data-grupo="levou"][aria-pressed="true"]': [opc('Cartões')] }, { roteiro: 'r1' });
  T.ACOES['salvar-visita'](botao('salvar-visita', {}, ctx));
  igual(T.estado.visitas.length, n + 1, 'visita');
  if (!T.estado.paradasFeitas['r1-dombigode']) throw new Error('parada não marcada');
  tem('Visita registrada');
  var ctx2 = ctxCom({ '#vis-parceira': campo('porto'), '#vis-quem': campo('Renato'), 'TODOS .opcao[data-grupo="clima"][aria-pressed="true"]': [opc('boa')], 'TODOS .opcao[data-grupo="levou"][aria-pressed="true"]': [opc('Kit completo')], 'TODOS .opcao[data-grupo="virou"][aria-pressed="true"]': [opc('sim')] }, {});
  T.ACOES['salvar-visita'](botao('salvar-visita', {}, ctx2));
  var porto = T.parceira('porto');
  if (porto.prospect) throw new Error('porto não virou parceira');
  igual(T.statusDe(porto), 'nova', 'status do porto');
  if (!porto.kit) throw new Error('porto sem kit');
  if (!T.estado.paradasFeitas['r1-porto']) throw new Error('parada do porto não marcada pelo roteiro');
});
run('nova parceira', function () {
  T.ACOES['nova-parceira'](botao('nova-parceira'));
  tem('Nova parceira', painel());
  var n = T.estado.parceiras.length;
  T.ACOES['salvar-parceira'](botao('salvar-parceira', {}, ctxCom({ '#np-nome': campo('') })));
  igual(T.estado.parceiras.length, n, 'sem nome não salva');
  T.ACOES['salvar-parceira'](botao('salvar-parceira', {}, ctxCom({ '#np-nome': campo('Studio Fios'), '#np-bairro': campo('Bigorrilho'), 'TODOS .opcao[data-grupo="tipo"][aria-pressed="true"]': [opc('Salão')] })));
  igual(T.estado.parceiras.length, n + 1, 'nova');
  ir('parceiras');
  tem('Studio Fios'); tem('Cadastrada por você hoje.');
});
run('Luiz, 07/10 · gestão: quem lança é a Fer; a doutora vê o que falta lançar', function () {
  T.ACOES.reiniciar(botao('reiniciar'));
  ir('gestao');
  tem('A clínica em números.'); tem('Recebimentos por semana'); tem('O que a clínica recebeu'); tem('Agenda dos próximos 7 dias'); tem('Rotina da Fer'); tem('Pauta da conversa de segunda');
  naoTem('Lançar R$ 2.600'); tem('Falta a Fer lançar'); tem('Simular: a Fer lançou'); tem('cartão 70%, PIX 28% e outras formas 2%');
  if (T.ACOES.lancar) throw new Error('a doutora não lança: quem lança é a Fer');
  tem('1 lançamento pendente com a Fer, de Otávio Ramos', trecho(__els.tela.innerHTML, 'id="h-pauta"', '</section>'));
  var antes = T.estado.receb.d30;
  T.ACOES['fer-lancou'](botao('fer-lancou', { nome: 'Otávio Ramos' }));
  igual(T.estado.receb.d30, antes + 2600, 'recebido');
  tem('A Fer lançou', __els.toast.innerHTML);
  tem('Tudo lançado');
  T.ACOES.reiniciar(botao('reiniciar'));
});
run('conversa: sugestões e guardiões', function () {
  T.ACOES.chat(botao('chat'));
  tem('Conversar com o copiloto', painel()); tem('Oi, doutora!', painel());
  T.ACOES['chat-sugestao'](botao('chat-sugestao', { pergunta: 'Quais parceiras estão esfriando?' })); __flush();
  tem('Dom Bigode', ultimaMsg());
  T.ACOES['chat-sugestao'](botao('chat-sugestao', { pergunta: 'Monta meu roteiro de visitas desta semana.' })); __flush();
  tem('dois roteiros', ultimaMsg());
  T.ACOES['chat-sugestao'](botao('chat-sugestao', { pergunta: 'De onde vieram os pacientes novos nos últimos 30 dias?' })); __flush();
  tem('Indicação de pacientes', ultimaMsg());
  T.ACOES['chat-sugestao'](botao('chat-sugestao', { pergunta: 'Quanto a clínica recebeu nos últimos 30 dias?' })); __flush();
  tem('a clínica recebeu', ultimaMsg());
  T.ACOES['chat-sugestao'](botao('chat-sugestao', { pergunta: 'Escreve um agradecimento para o Rafael, da Navalha de Ouro.' })); __flush();
  tem('Oi, Rafael!', ultimaMsg());
  T.ACOES['chat-sugestao'](botao('chat-sugestao', { pergunta: 'Jorge Almeida pode voltar a usar o Capilare Fort?' })); __flush();
  tem('não opino', ultimaMsg());
  T.ACOES['chat-sugestao'](botao('chat-sugestao', { pergunta: 'Posso contar para o Rafael quem ele indicou?' })); __flush();
  tem('números gerais', ultimaMsg());
  T.ACOES['chat-sugestao'](botao('chat-sugestao', { pergunta: 'Como está a Navalha & Café?' })); __flush();
  tem('Ainda não é parceira', ultimaMsg());
  T.ACOES['chat-sugestao'](botao('chat-sugestao', { pergunta: 'Como estão os posts da semana?' })); __flush();
  tem('A Fer mandou a programação das próximas duas semanas', ultimaMsg()); naoTem('Parceira do mês', ultimaMsg()); naoTem('Em espera', ultimaMsg());
  T.ACOES['chat-sugestao'](botao('chat-sugestao', { pergunta: 'Qual a previsão do tempo?' })); __flush();
  tem('No protótipo', ultimaMsg());
  T.ACOES.fechar();
});
run('prévias de iPhone e iPad, e tema', function () {
  T.definirModo('iphone');
  if (!document.documentElement.classList.contains('modo-iphone')) throw new Error('sem iPhone');
  T.ACOES.modo(botao('modo', { modo: 'ipad' }));
  if (!document.documentElement.classList.contains('modo-ipad')) throw new Error('sem iPad');
  T.ACOES.girar(botao('girar'));
  if (!document.documentElement.classList.contains('em-pe')) throw new Error('não girou');
  T.definirModo('tela');
  if (document.documentElement.classList.contains('preview')) throw new Error('ficou em prévia');
  ir('ipad');
  igual(T.modo, 'ipad', 'hash de prévia');
  T.definirModo('tela');
  // Desde 09/10/2026, abre no claro, como a Fer, e o botão liga o modo noturno.
  if (!document.documentElement.classList.contains('dia')) throw new Error('não abriu no claro');
  T.ACOES.tema(botao('tema'));
  if (!document.documentElement.classList.contains('noite')) throw new Error('não foi para a noite');
  T.ACOES.tema(botao('tema'));
  if (!document.documentElement.classList.contains('dia')) throw new Error('não voltou para o dia');
});
run('atalhos de teclado', function () {
  ir('hoje');
  __handlers.keydown[0]({ key: '/', target: { closest: function () { return null; } }, preventDefault: function () {}, ctrlKey: false, metaKey: false, altKey: false });
  tem('Conversar com o copiloto', painel());
  __handlers.keydown[0]({ key: 'Escape', target: { closest: function () { return null; } }, preventDefault: function () {} });
  if (!__els.painel.hidden) throw new Error('não fechou');
});
// ---------- Etapa 1 e começo da etapa 2: pacientes, ficha, alerta, meta, vai pensar, indicação, pesquisa e Google ----------
function nomes(xs) { return xs.map(function (p) { return p.nome; }).join(', '); }
function gg(id) { return T.estado.google.filter(function (g) { return g.id === id; })[0]; }
function sintetico(ag, extra) {
  return Object.assign({ id: 'teste-' + Math.random(), nome: 'Paciente Teste', cod: '9999', tel: '(41) 90000-9999', whats: true, horario: 'manhã', etapa: 'protocolo', ini: 60, origem: { tipo: 'canal', nome: 'Google' }, prot: { feitas: 2, total: 6 }, hc: [], termos: { trat: 60, img: 60 }, ag: ag }, extra || {});
}
run('pacientes: tela, busca e filtros', function () {
  T.ACOES.reiniciar(botao('reiniciar'));
  ir('pacientes');
  // André Lacerda (contato novo que a Lina Costa mandou) entrou na amostra: 40 pacientes.
  tem('Todos os pacientes.'); tem('Cadastro completo: meta de 100%'); tem('Quem vê o quê na ficha'); tem('40 pacientes'); tem('em ordem alfabética');
  tem('Nome, final do telefone ou código'); tem('Profissional: todas'); tem('Origem: todas'); tem('Pendência: todas');
  igual(T.estado.pacientes.length, 40, 'amostra');
  // Busca: ignora acento, acha pelo final do telefone e pelo código do cadastro. Saiu o código pessoal (Luiz, 07/10/2026).
  T.estado.buscaPac = 'leticia'; igual(nomes(T.pacientesFiltrados()), 'Letícia Moura', 'sem acento');
  T.estado.buscaPac = 'SÉRGIO'; igual(nomes(T.pacientesFiltrados()), 'Paulo Sérgio Nunes, Sérgio Pacheco', 'maiúsculas e acento');
  T.estado.buscaPac = 'sergio pach'; igual(nomes(T.pacientesFiltrados()), 'Sérgio Pacheco', 'duas palavras');
  T.estado.buscaPac = '0461'; igual(nomes(T.pacientesFiltrados()), 'Rodrigo Mattos', 'final do telefone');
  T.estado.buscaPac = '0533'; igual(nomes(T.pacientesFiltrados()), 'Juliana Prado', 'telefone da Juliana');
  T.estado.buscaPac = '1041'; igual(nomes(T.pacientesFiltrados()), 'Jorge Almeida', 'código do cadastro');
  T.estado.buscaPac = 'jorge41'; igual(T.pacientesFiltrados().length, 0, 'sem o código pessoal');
  T.estado.buscaPac = 'helena'; igual(T.pacientesFiltrados().length, 2, 'duas Helenas');
  T.estado.buscaPac = 'zzz'; igual(T.pacientesFiltrados().length, 0, 'ninguém');
  T.estado.buscaPac = '';
  // Digitar atualiza só a lista, sem tirar o foco da busca.
  __handlers.input[0]({ target: { dataset: { busca: '' }, value: 'otavio', closest: function () { return null; } } });
  igual(T.estado.buscaPac, 'otavio', 'busca digitada');
  tem('Otávio Ramos', __els.listaPacientes.innerHTML); tem('Otávio Reis', __els.listaPacientes.innerHTML); tem('2 pacientes', __els.listaPacientes.innerHTML);
  tem('Limpar a busca e os filtros', __els.listaPacientes.innerHTML);
  T.ACOES['limpar-pac'](botao('limpar-pac'));
  igual(T.estado.buscaPac, '', 'limpou');
  // Filtros por grupo, profissional, origem e pendência.
  function filtro(campo, valor) { T.ACOES['filtro-pac'](botao('filtro-pac', { campo: campo, valor: valor })); }
  filtro('grupo', 'risco'); igual(nomes(T.pacientesFiltrados()), 'Camila Ribas, Juliana Prado, Leandro Schmitt, Marcos Vieira', 'ativos em risco');
  filtro('grupo', 'pensar'); igual(nomes(T.pacientesFiltrados()), 'Carla Mendes, Diego Andrade, Eduardo Viana', 'vão pensar');
  filtro('grupo', 'reativacao'); igual(nomes(T.pacientesFiltrados()), 'André Wolff, Vanessa Kowalski', 'reativação');
  filtro('grupo', 'todos');
  // A dúvida sobre o Capilare Fort é do Paulo Mendes, como no copiloto da Dra. Monielle: a Aline não foi encaminhada.
  // A Dra. Monielle à parte (Luiz, 07/10/2026): na agenda dela, só quem tem consulta marcada.
  filtro('prof', 'moni'); igual(nomes(T.pacientesFiltrados()), 'Lia Fagundes, Odete Prado', 'Dra. Monielle');
  __handlers.change[0]({ target: { dataset: { filtroPac: 'prof' }, value: 'todas' } });
  igual(T.estado.filtrosPac.prof, 'todas', 'select de profissional');
  filtro('origem', 'paciente'); igual(T.pacientesFiltrados().length, 9, 'indicação de pacientes');
  filtro('origem', 'sem'); igual(nomes(T.pacientesFiltrados()), 'Diego Andrade, Vanessa Kowalski', 'sem origem');
  filtro('origem', 'todas');
  // Nenhum relato fica registrado (Luiz, 07/10/2026): sai a pendência de relato. Os termos ficam para depois.
  naoTem('Relato de hoje'); tem('Falta a Fer lançar');
  filtro('pend', 'cadastro'); igual(nomes(T.pacientesFiltrados()), 'Camila Duarte, Diego Andrade, Lucas Fontana, Marcos Vieira, Otávio Ramos, Sérgio Pacheco, Vanessa Kowalski', 'cadastro incompleto');
  filtro('pend', 'lancar'); igual(nomes(T.pacientesFiltrados()), 'Otávio Ramos', 'falta a Fer lançar');
  filtro('pend', 'homecare'); igual(nomes(T.pacientesFiltrados()), 'Aline Castilho, Leandro Schmitt, Lia Fagundes', 'home care acabando (Luiz, 07/10/2026: kits de 120 e 75 dias)');
  T.ACOES['limpar-pac'](botao('limpar-pac'));
  igual(T.pacientesFiltrados().length, 40, 'tudo de volta');
});
run('grupos dos pacientes (decisão 2) e cadastro completo (decisão 5)', function () {
  igual(T.grupoDe(sintetico([[-10, '09:00', 'ana', 'Sessão 2 de 6', 'feito']])), 'tratamento', '10 dias sem sessão');
  // Fez 2 de 6: a 3ª está prevista para 30 dias depois da 2ª (Luiz, 07/10/2026).
  igual(T.grupoDe(sintetico([[-15, '09:00', 'ana', 'Sessão 2 de 6', 'feito']])), 'tratamento', '15 dias: ainda no intervalo');
  igual(T.grupoDe(sintetico([[-30, '09:00', 'ana', 'Sessão 2 de 6', 'feito']])), 'tratamento', '30 dias: no limite');
  igual(T.grupoDe(sintetico([[-31, '09:00', 'ana', 'Sessão 2 de 6', 'feito']])), 'risco', '31 dias: passou da data prevista');
  igual(T.grupoDe(sintetico([[-60, '09:00', 'ana', 'Sessão 2 de 6', 'feito']])), 'risco', '60 dias: antes ficava sem grupo');
  igual(T.grupoDe(sintetico([[-90, '09:00', 'ana', 'Sessão 2 de 6', 'feito']])), 'risco', '90 dias');
  igual(T.grupoDe(sintetico([[-91, '09:00', 'ana', 'Sessão 2 de 6', 'feito']])), 'reativacao', '91 dias vai para a reativação');
  igual(T.grupoDe(sintetico([[-40, '09:00', 'ana', 'Sessão 2 de 6', 'feito'], [3, '09:00', 'ana', 'Sessão 3 de 6', 'marcado']])), 'tratamento', 'com sessão marcada');
  igual(T.grupoDe(sintetico([[-100, '09:00', 'ana', 'Manutenção', 'feito']], { etapa: 'manutencao' })), 'reativacao', 'manutenção parada há 100 dias');
  igual(T.grupoDe(sintetico([], { etapa: 'alta', altaDias: 200 })), 'reativacao', 'alta há mais de 6 meses');
  igual(T.grupoDe(T.paciente('juliana')), 'risco', 'Juliana, 36 dias, a 3ª prevista para 30');
  igual(T.grupoDe(T.paciente('vanessa')), 'reativacao', 'Vanessa, 104 dias');
  igual(T.grupoDe(T.paciente('thiago')), 'tratamento', 'Thiago, 13 dias');
  igual(T.grupoDe(T.paciente('helenab')), 'alta', 'Helena Brandão, alta há 95 dias');
  igual(T.faltasCadastro(T.paciente('lucas')).join(', '), 'WhatsApp confirmado', 'Lucas: os termos ficam para depois');
  igual(T.faltasCadastro(T.paciente('sergio')).join(', '), 'protocolo', 'Sérgio');
  igual(T.faltasCadastro(T.paciente('otavio')).join(', '), 'home care', 'Otávio Ramos');
  igual(T.faltasCadastro(T.paciente('diego')).join(', '), 'origem', 'Diego');
  igual(T.faltasCadastro(T.paciente('jorge')).length, 0, 'Jorge completo');
  igual(T.faltasCadastro(T.paciente('carla')).length, 0, 'quem vai pensar não precisa de protocolo nem de home care');
  var m = T.metaCadastro();
  igual(m.faltam, 109, 'faltam'); igual(m.semanas, 3, 'semanas no ritmo de 8 por dia'); igual(m.pct, 71, 'porcentagem');
  ir('gestao');
  tem('A meta é 100%'); tem('no ritmo de 8 por dia, faltam 109 e termina em 3 semanas'); tem('375 pacientes que já vieram à clínica'); naoTem('meta de 80%'); naoTem('combinar a meta');
  ir('pacientes');
  tem('No ritmo de 8 por dia, termina em 3 semanas'); tem('266 de 375 que já vieram à clínica');
});
run('ficha: abre de qualquer nome e mostra tudo para a doutora', function () {
  ir('hoje');
  tem('href="#pacientes"');
  grupo('fer'); tem('data-acao="abrir-ficha" data-id="diego"'); tem('data-acao="abrir-ficha" data-id="helena"');
  grupo('crescimento'); tem('data-acao="abrir-ficha" data-id="juliana"');
  grupo(''); T.ACOES['aba-lado'](botao('aba-lado', { aba: 'passadinha' })); tem('data-acao="abrir-ficha" data-id="roberto"'); tem('data-acao="abrir-ficha" data-id="caio"');
  T.ACOES['aba-lado'](botao('aba-lado', { aba: 'saber' }));
  // A Tatiane teve alta na reavaliação das 08:00: a doutora não encaminha mais à Dra. Monielle (Luiz, 07/10/2026).
  tem('Tatiane Moura'); naoTem('Para a Dra. Monielle');
  T.ACOES['abrir-consulta'](botao('abrir-consulta', { id: 'tatiane' }));
  tem('A Fer programa os contatos de manutenção aos 60 e aos 90 dias.', painel()); tem('Acompanhada pela Dra. Monielle desde que fechou o protocolo.', painel());
  T.ACOES.fechar();
  T.ACOES['abrir-consulta'](botao('abrir-consulta', { id: 'jorge' }));
  tem('Abrir a ficha completa', painel()); tem('data-acao="abrir-ficha" data-id="rodrigo"', painel());
  T.ACOES['abrir-ficha'](botao('abrir-ficha', { id: 'jorge' }));
  tem('Ficha · cód. 1041', painel()); tem('Ver como', painel()); tem('Só no protótipo, para conferir quem vê o quê', painel());
  ['Contato e preferências', 'Origem e indicação', 'Sessões', 'Agenda', 'Home care', 'Autorização de publicação', 'Evolução: antes e agora', 'Pagamentos', 'Contatos e tarefas', 'Registro de acesso'].forEach(function (s) { tem('<h3>' + s + '</h3>', painel()); });
  naoTem('<h3>Alertas</h3>', painel()); naoTem('<h3>Termos e autorizações</h3>', painel());
  tem('O prontuário continua no Clinicorp', painel()); tem('Toda abertura e toda alteração desta ficha ficam registradas', painel());
  tem('Reavaliação · Hoje, 10:00 · Dra. Suellen', painel()); tem('(41) 90000-0441 · confirmado', painel()); naoTem('Abrir no WhatsApp', painel()); naoTem('whats-pac', painel());
  tem('Indicação de Rodrigo Mattos', painel()); naoTem('JORGE41', painel()); tem('Capilare Nutri', painel()); tem('Acaba em 72 dias', painel()); tem('Entregue pela Ana Carolina', painel());
  tem('R$ 2.600', painel()); tem('R$ 350', painel()); tem('Valores fictícios e configuráveis', painel());
  tem('Hoje, 09:20', painel()); tem('Abriu a ficha.', painel()); tem('Atualizou a ficha com a leitura do Clinicorp', painel());
  igual(T.estado.acessos.jorge.length, 1, 'abertura registrada');
  T.ACOES.fechar();
  T.ACOES['abrir-ficha'](botao('abrir-ficha', { id: 'juliana' }));
  tem('Ativo em risco', painel()); tem('Indicou 3 pessoas', painel()); tem('data-acao="abrir-ficha" data-id="carla"', painel()); naoTem('JULIANA12', painel()); tem('Indica a clínica', painel());
  tem('Antes: tarde. Agora: manhã.', painel());
  T.ACOES['abrir-ficha'](botao('abrir-ficha', { id: 'carla' }));
  tem('Carla Mendes', painel()); tem('Motivo: <strong>decidir em casa</strong>', painel()); tem('Retorno de 2 dias hoje, na lista da Fer', painel());
  T.ACOES.fechar();
  T.ACOES['abrir-ficha'](botao('abrir-ficha', { id: 'aline' }));
  tem('Capilare Fort', painel()); tem('só com a prescrição da Dra. Monielle', painel()); tem('Acaba em 8 dias', painel());
  naoTem('<h3>Alertas</h3>', painel());
  T.ACOES.fechar();
  // Nenhum relato fica registrado (Luiz, 07/10/2026): a dúvida do Paulo Mendes foi direto para a Dra. Monielle.
  T.ACOES['abrir-ficha'](botao('abrir-ficha', { id: 'paulomendes' }));
  if (T.paciente('paulomendes').alertas) throw new Error('relato registrado'); naoTem('Capilare Fort junto com as sessões', painel()); tem('Capilare Tonic', painel());
  T.ACOES.fechar();
  T.ACOES['abrir-ficha'](botao('abrir-ficha', { id: 'otavio' }));
  tem('Falta lançar', painel()); tem('Pago há 3 dias, falta a Fer lançar', painel()); tem('Home care não registrado', painel());
  tem('Lançar protocolo completo, R$ 2.600, combinado na avaliação há 3 dias.', painel());
  if (T.ACOES['whats-pac']) throw new Error('o Abrir no WhatsApp devia ter saído');
  T.ACOES.fechar();
  ir('marketing'); tem('data-acao="abrir-ficha" data-id="bruno"'); tem('data-acao="abrir-ficha" data-id="juliana"'); tem('data-acao="abrir-ficha" data-id="eduardo"');
  ir('gestao'); tem('data-acao="abrir-ficha" data-id="otavio"'); tem('data-acao="abrir-ficha" data-id="henrique"');
  T.ACOES['ver-log'](botao('ver-log'));
  T.ACOES.fechar();
  T.ACOES['chat-sugestao'](botao('chat-sugestao', { pergunta: 'Como está a Juliana Prado?' })); __flush();
  tem('Ativo em risco', ultimaMsg()); tem('Abrir a ficha', ultimaMsg());
  T.ACOES.fechar();
});
run('Luiz, 07/10 · ficha: a Fer, a Ana Carolina e a doutora veem a ficha inteira, com as fotos; a Dra. Monielle, a parte dela', function () {
  T.ACOES.reiniciar(botao('reiniciar'));
  T.ACOES['abrir-ficha'](botao('abrir-ficha', { id: 'ricardo' }));
  tem('<h3>Pagamentos</h3>', painel()); tem('Evolução: antes e agora', painel()); tem('<h3>Origem e indicação</h3>', painel()); naoTem('<h3>Alertas</h3>', painel()); naoTem('ardência', painel()); naoTem('<h3>Termos e autorizações</h3>', painel());
  ['fer', 'ana'].forEach(function (q) {
    T.ACOES['ver-como'](botao('ver-como', { valor: q }));
    igual(T.estado.verComo, q, 'vendo como ' + q);
    ['<h3>Contato e preferências</h3>', '<h3>Origem e indicação</h3>', '<h3>Pagamentos</h3>', '<h3>Home care</h3>', '<h3>Agenda</h3>', '<h3>Contatos e tarefas</h3>', '<h3>Autorização de publicação</h3>', 'Evolução: antes e agora', '(41) 90000-0419'].forEach(function (t) { tem(t, painel()); }); naoTem('Abrir no WhatsApp', painel());
    naoTem('<h3>Registro de acesso</h3>', painel()); naoTem('Só para consulta', painel()); naoTem('Só o nome', painel());
    tem('Não aparecem: registro de acesso.', painel());
  });
  T.ACOES['ver-como'](botao('ver-como', { valor: 'moni' }));
  tem('(41) 90000-0419', painel()); naoTem('<h3>Origem e indicação</h3>', painel()); naoTem('<h3>Pagamentos</h3>', painel()); naoTem('<h3>Autorização de publicação</h3>', painel());
  tem('Evolução: antes e agora', painel()); tem('<h3>Home care</h3>', painel()); naoTem('<h3>Alertas</h3>', painel());
  T.ACOES['ver-como'](botao('ver-como', { valor: 'suellen' }));
  tem('<h3>Registro de acesso</h3>', painel()); naoTem('Vendo como', painel());
  igual(T.estado.acessos.ricardo.length, 1, 'trocar a visão não conta como abertura');
  T.ACOES.fechar();
  T.ACOES['abrir-ficha'](botao('abrir-ficha', { id: 'lucas' }));
  igual(T.estado.verComo, 'suellen', 'a ficha abre na visão da doutora');
  naoTem('Sem termo de uso de imagem', painel()); tem('Cadastro incompleto', painel());
  T.ACOES.fechar();
  ir('pacientes');
  naoTem('só o nome'); naoTem('só a situação'); naoTem('só consulta'); tem('Continua no Clinicorp'); tem('A Fer, a Ana Carolina e você veem a ficha inteira');
});
run('Luiz, 07/10 · relato: nada fica registrado; quem ouve passa direto para a Dra. Monielle', function () {
  T.ACOES.reiniciar(botao('reiniciar'));
  ir('hoje');
  naoTem('Alerta clínico'); naoTem('Passado à Dra. Monielle'); naoTem('Simular a resposta da Dra. Monielle'); naoTem('cobrar'); naoTem('Ligar para a Dra. Monielle');
  if (T.ACOES['simular-alerta'] || T.ACOES['google-alerta']) throw new Error('nada de relato no sistema');
  T.estado.pacientes.forEach(function (p) { if (p.alertas) throw new Error('relato registrado: ' + p.id); });
  T.ACOES['abrir-ficha'](botao('abrir-ficha', { id: 'ricardo' }));
  naoTem('<h3>Alertas</h3>', painel()); naoTem('relato', painel());
  T.ACOES.fechar();
  ir('pacientes');
  naoTem('Relato de hoje'); naoTem('relato passado');
  naoTem('alerta clínico', T.cotas().map(function (c) { return c.join(' '); }).join(' | '));
});
run('vai pensar: motivo em um toque e resumo do plano', function () {
  T.ACOES['abrir-consulta'](botao('abrir-consulta', { id: 'leticia' }));
  var aval = T.estado.capt.d30.avaliacoes;
  T.ACOES.resultado(botao('resultado', { id: 'leticia', valor: 'pensar' }));
  igual(T.estado.consultas.leticia, undefined, 'só registra com o motivo');
  tem('Por que vai pensar?', painel()); tem('Valor', painel()); tem('Tempo', painel()); tem('Decidir em casa', painel()); tem('Outro', painel());
  T.ACOES['pensar-voltar'](botao('pensar-voltar', { id: 'leticia' }));
  naoTem('Por que vai pensar?', painel()); tem('Fechou o protocolo', painel());
  T.ACOES.resultado(botao('resultado', { id: 'leticia', valor: 'pensar' }));
  T.ACOES['motivo-pensar'](botao('motivo-pensar', { id: 'leticia', motivo: 'valor' }));
  igual(T.estado.consultas.leticia, 'pensar', 'registrado'); igual(T.estado.motivos.leticia, 'valor', 'motivo');
  igual(T.estado.capt.d30.avaliacoes, aval + 1, 'conta a avaliação');
  tem('retornos de 2, 7, 30 e 90 dias, com o motivo: valor', painel()); tem('Resumo do plano para levar', painel());
  var m = /id="plano-leticia">([^<]*)</.exec(painel());
  if (!m) throw new Error('sem resumo do plano');
  if (/garant|resultado|melhor|cura|promet|recuper/i.test(m[1])) throw new Error('o resumo não pode prometer resultado: ' + m[1]);
  tem('Oi, Letícia!', m[1]); tem('6 sessões com a Ana Carolina', m[1]); tem('com 20, 30, 40, 45 e 45 dias entre uma e outra', m[1]); tem('No fim, a reavaliação com a Dra. Suellen', m[1]); tem('R$ 2.600', m[1]); tem('cartão de crédito ou PIX', m[1]); tem('formas de pagamento, a Fer explica', m[1]);
  tem('Valores fictícios e configuráveis', painel());
  T.ACOES['motivo-pensar'](botao('motivo-pensar', { id: 'leticia', motivo: 'tempo' }));
  igual(T.estado.motivos.leticia, 'valor', 'não muda depois de registrado');
  T.ACOES.fechar();
  igual(T.grupoDe(T.paciente('leticia')), 'pensar', 'grupo');
  T.ACOES['abrir-ficha'](botao('abrir-ficha', { id: 'leticia' }));
  tem('Avaliação: vai pensar. Motivo: valor.', painel()); tem('A Fer manda o resumo do plano', painel());
  T.ACOES.fechar();
  ir('marketing');
  tem('Avaliaram e vão pensar'); tem('Motivo: decidir em casa'); tem('Motivo: valor'); tem('retorno de 90 dias na lista da Fer'); tem('próximo retorno em 2 dias');
  ir('hoje');
});
run('Luiz, 07/10 · sem a indicação na hora certa e sem a pesquisa de zero a dez, por enquanto', function () {
  T.ACOES.reiniciar(botao('reiniciar'));
  ir('marketing');
  naoTem('código JULIANA12'); naoTem('Vim pela indicação de'); tem('Pacientes que mais indicam'); tem('Juliana Prado');
  T.ACOES['abrir-consulta'](botao('abrir-consulta', { id: 'jorge' }));
  naoTem('Pedir uma indicação', painel()); naoTem('Bom momento para indicar', painel());
  T.ACOES.fechar();
  if (T.ACOES.indicar) throw new Error('sem a indicação na hora certa');
  if (T.codigoPessoal || T.linkIndicacao) throw new Error('sem o código pessoal');
  ir('hoje');
  naoTem('Pesquisa de satisfação'); naoTem('Pesquisa do fim do ciclo'); naoTem('nota 6');
  T.ACOES['abrir-ficha'](botao('abrir-ficha', { id: 'roberto' })); naoTem('ROBERTO85', painel()); naoTem('Pesquisa de uma pergunta', painel()); T.ACOES.fechar();
  T.ACOES['abrir-ficha'](botao('abrir-ficha', { id: 'juliana' })); tem('Indica a clínica', painel()); naoTem('JULIANA12', painel()); T.ACOES.fechar();
});
run('avaliações no Google', function () {
  ir('marketing');
  tem('Avaliações no Google'); tem('4,9'); tem('nota média, com 86 avaliações'); tem('avaliações novas no mês'); tem('pedidos feitos no mês, na reavaliação');
  tem('O pedido vai no dia da reavaliação'); tem('a Fer mostra o QR Code ou manda o link das avaliações do Google, e ele avalia direto no Google');
  tem('sem filtrar por satisfação e sem brinde'); tem('nunca confirma que a pessoa é paciente');
  T.estado.google.forEach(function (g) { var av = T.conferirResposta(g.resposta); if (av.length) throw new Error('rascunho com aviso: ' + g.id + ' ' + av); });
  if (!T.conferirResposta('Que bom que o seu tratamento está indo bem!').length) throw new Error('confirmou que é paciente');
  if (!T.conferirResposta('Até a sua próxima consulta!').length) throw new Error('consulta');
  if (!T.conferirResposta('Ganhe um brinde por avaliar!').length) throw new Error('brinde');
  if (!T.conferirResposta('Que pena que a coceira voltou.').length) throw new Error('comentou o relato');
  // Relato clínico numa avaliação: só o lembrete; nada fica registrado (Luiz, 07/10/2026).
  tem('Relato clínico: passe direto para a Dra. Monielle. A resposta pública não comenta o que a pessoa contou, e nada fica registrado aqui.');
  naoTem('Passei à Dra. Monielle'); naoTem('data-acao="google-alerta"'); naoTem('Alerta enviado');
  ir('hoje'); naoTem('Alerta de uma avaliação no Google');
  ir('marketing');
  // A conferência barra promessa e confirmação de paciente; o rascunho passa.
  var original = gg('g1').resposta;
  var ruim = ctxCom({ '#resp-g1': campo('Obrigada! O seu tratamento vai dar resultado garantido.') });
  T.ACOES['google-aprovar'](botao('google-aprovar', { id: 'g1' }, ruim));
  igual(gg('g1').estado, 'rascunho', 'não aprova');
  igual(gg('g1').resposta, 'Obrigada! O seu tratamento vai dar resultado garantido.', 'a edição não se perde');
  T.ACOES['google-aprovar'](botao('google-aprovar', { id: 'g1' }, ctxCom({ '#resp-g1': campo(original) })));
  igual(gg('g1').estado, 'aprovada', 'aprovada');
  tem('Já publiquei');
  T.ACOES['google-publicada'](botao('google-publicada', { id: 'g1' }));
  igual(gg('g1').estado, 'publicada', 'publicada');
  var caixa = new El('caixa'); var ctxIn = ctxCom({ '[data-conferido]': caixa });
  __handlers.input[0]({ target: { dataset: { resposta: 'g3' }, value: 'Obrigada, e até a sua próxima consulta!', closest: function () { return ctxIn; } } });
  tem('confirma que a pessoa é paciente', caixa.innerHTML);
  __handlers.input[0]({ target: { dataset: { resposta: 'g3' }, value: 'Obrigada pelo retorno!', closest: function () { return ctxIn; } } });
  tem('não apontou paciente, promessa, remédio nem brinde', caixa.innerHTML);
  ir('hoje');
});
// ---------- Etapa 2, segunda parte: linha Parcerias, tipos de parceiro, comissão, campanha e captação por tipo ----------
function pp(id) { return T.parceira(id); }
function trecho(h, de, ate) { var a = h.indexOf(de); if (a < 0) throw new Error('não achei o começo: ' + de); var b = ate ? h.indexOf(ate, a) : -1; return h.slice(a, b < 0 ? h.length : b); }
function calendario() { return trecho(__els.tela.innerHTML, 'id="h-calendario"', 'id="h-videos"'); }
function textoAviso(a) { return typeof a === 'string' ? a : a.titulo + ' ' + a.texto; }
function avisosPeca(t) { return T.conferirPeca(t, { linha: 'como' }).map(textoAviso).join(' | '); }
run('Luiz, 07/10 · linha Parcerias em espera: as peças ficam guardadas, fora da programação', function () {
  T.ACOES.reiniciar(botao('reiniciar'));
  ir('marketing');
  ['p2', 'p10', 'p12', 'p13', 'p14'].forEach(function (id) { igual(pc(id).linha, 'parcerias', id); igual(pc(id).estado, 'espera', 'em espera: ' + id); });
  tem('Em espera: a linha Parcerias'); tem('Não entra na programação por enquanto');
  var cal = calendario();
  naoTem('Parceira do mês: Navalha de Ouro', cal); naoTem('Seja parceiro da clínica', cal); naoTem('linha--parcerias', cal); tem('Como é a avaliação capilar', cal);
  naoTem('Parcerias<span class="n">'); tem('Como funciona<span class="n">6</span>'); tem('Resultados<span class="n">2</span>');
  tem('8 posts'); tem('6 de como funciona e 2 de resultado'); tem('A linha Parcerias está em espera');
  // Sem código nem QR: cada peça seria medida pelos contatos que as parceiras mandam.
  T.estado.pecas.filter(function (x) { return x.linha === 'parcerias'; }).forEach(function (x) { if (/código|QR/.test(T.medidaDe(x) + ' ' + (x.legenda || ''))) throw new Error('peça de parceria com código: ' + x.titulo); });
  ['p12', 'p13', 'p14'].forEach(function (id) { igual(T.conferirPeca(pc(id).legenda, pc(id)).length, 0, 'conferência de ' + id); });
  T.ACOES['abrir-peca'](botao('abrir-peca', { id: 'p2' }));
  tem('Em espera, com a linha Parcerias', painel()); tem('Medida pelos contatos que a Navalha de Ouro manda', painel()); naoTem('Vim pela', painel()); naoTem('data-acao="aprovar-peca"', painel());
  T.ACOES.fechar();
  igual(T.estado.pecas.filter(function (x) { return x.estado === 'aprovar'; }).length, 5, 'esperando a doutora');
  ir('parceiras');
  naoTem('Convite para novos parceiros'); tem('Agradecer a Navalha de Ouro, a parceira do mês');
});
run('tipos de parceiro: régua própria, kit e conversa', function () {
  T.ACOES.reiniciar(botao('reiniciar'));
  igual(T.TIPOS.barbearia.forte, 6, 'barbearia'); igual(T.TIPOS.visagista.forte, 4, 'visagista'); igual(T.TIPOS.salao.forte, 3, 'salão'); igual(T.TIPOS.saude.forte, 2, 'saúde');
  ['escola', 'distribuidora', 'micro', 'protese'].forEach(function (k) { igual(T.TIPOS[k].forte, 2, k); });
  igual(T.TIPOS.saude.comissao, false, 'saúde sem comissão');
  igual(T.statusDe(pp('bella')), 'forte', 'Espaço Bella, 3 em 90 dias, pela régua dos salões');
  igual(T.statusDe(pp('barberclub')), 'ativa', 'Barber Club, 4 em 90 dias, abaixo da régua das barbearias');
  igual(T.statusDe(pp('linacosta')), 'forte', 'Lina Costa, 4 em 90 dias');
  igual(T.statusDe(pp('brunorocha')), 'nova', 'Ateliê Bruno Rocha, há 25 dias');
  ['henriquesalles', 'equilibrio', 'barberpro', 'fioforte', 'microhair'].forEach(function (id) { igual(T.statusDe(pp(id)), 'conhecer', id); });
  function sint(tipo, ind) { return { id: 'x', tipo: tipo, diasParceira: 200, ind: ind, ultInd: 3, kit: true }; }
  igual(T.statusDe(sint('visagista', [0, 0, 0, 1, 1, 1])), 'ativa', 'visagista com 3');
  igual(T.statusDe(sint('visagista', [0, 0, 0, 2, 1, 1])), 'forte', 'visagista com 4');
  igual(T.statusDe(sint('saude', [0, 0, 0, 0, 1, 1])), 'forte', 'saúde com 2');
  igual(T.statusDe(sint('barbearia', [0, 0, 0, 2, 2, 1])), 'ativa', 'barbearia com 5');
  igual(T.statusDe(sint('escola', [0, 0, 0, 0, 1, 1])), 'forte', 'escola com 2');
  igual(T.statusDe(sint('salao', [0, 0, 0, 0, 1, 1])), 'ativa', 'salão com 2');
  if (/Parceira do mês/.test(T.proximoPasso(pp('linacosta')))) throw new Error('só a Navalha é a parceira do mês');
  igual(T.proximoPasso(pp('henriquesalles')), 'Conhecer: troca de indicação, sem comissão', 'próximo passo da saúde');
  tem('Forte pela régua das visagistas', T.visaoParceira(pp('linacosta')));
  tem('ainda não indicou ninguém', T.visaoParceira(pp('brunorocha')));
  ir('parceiras');
  tem('Lina Costa Visagismo'); tem('Ateliê Bruno Rocha'); tem('Dr. Henrique Salles'); tem('Clínica Equilíbrio Nutrição'); tem('Barber Pro Escola'); tem('Fio Forte Distribuidora'); tem('Studio Micro Hair');
  tem('Profissional de saúde · Endocrinologista'); tem('Escola de barbeiros'); tem('Distribuidora'); tem('Micropigmentação');
  tem('Tipos de parceiro'); tem('Forte com 4 ou mais indicações em 90 dias'); tem('Sem comissão: troca de indicação e relatório de volta com a autorização do paciente');
  tem('Prótese capilar'); tem('Pela régua de cada tipo, o Espaço Bella e a Lina Costa também são fortes.');
  // Ranking com filtro por tipo.
  tem('Visagistas<span class="n">2</span>'); tem('Barbearias<span class="n">7</span>'); tem('Salões<span class="n">1</span>');
  T.ACOES['filtrar-tipo'](botao('filtrar-tipo', { filtro: 'visagista' }));
  igual(T.estado.filtroTipo, 'visagista', 'filtro');
  var r = T.ranking();
  tem('Lina Costa', r); tem('Ateliê Bruno Rocha', r); naoTem('Navalha de Ouro', r); tem('barra-marca', r); tem('régua das visagistas: forte com 4 ou mais', r);
  T.ACOES['filtrar-tipo'](botao('filtrar-tipo', { filtro: 'todos' }));
  r = T.ranking(); tem('Navalha de Ouro', r); tem('Lina Costa', r); naoTem('barra-marca', r); tem('barbearia 6, visagista 4 e salão 3', r);
  // Profissional de saúde: sem comissão, troca de indicação, relatório de volta e conversa de colega para colega.
  T.ACOES['abrir-parceira'](botao('abrir-parceira', { id: 'henriquesalles' }));
  tem('Endocrinologista', painel()); tem('Sem comissão', painel()); tem('troca de indicação', painel()); tem('relatório de volta com a autorização do paciente', painel());
  tem('De colega para colega', painel()); tem('Ainda não é parceiro', painel()); tem('Folheto para a sala de espera', painel()); naoTem('DRHENRIQUE', painel());
  naoTem('Extrato da comissão', painel()); naoTem('R$ 150 por tratamento', painel());
  T.ACOES.fechar();
  // Kit e conversa da visagista.
  T.ACOES['abrir-parceira'](botao('abrir-parceira', { id: 'linacosta' }));
  naoTem('LINACOSTA', painel()); tem('guia de uma página', painel()); tem('Mensagem para a visagista', painel()); tem('Forte pela régua das visagistas', painel()); tem('Régua de forte', painel());
  T.ACOES.texto(botao('texto', { tipo: 'barbeiro', id: 'linacosta' }));
  tem('Mensagem pronta para a visagista', painel()); tem('Aqui é Lina, da Lina Costa', painel()); naoTem('código', painel());
  T.ACOES.fechar();
  ir('visitas');
  T.ACOES['abrir-parceira'](botao('abrir-parceira', { id: 'barberpro' })); tem('aula de 20 minutos', painel()); tem('R$ 150 por tratamento iniciado', painel()); T.ACOES.fechar();
  T.ACOES['abrir-parceira'](botao('abrir-parceira', { id: 'fioforte' })); tem('Wesley', painel()); tem('representante', painel()); T.ACOES.fechar();
  T.ACOES['abrir-parceira'](botao('abrir-parceira', { id: 'microhair' })); tem('um serviço completa o outro', painel()); T.ACOES.fechar();
  // Nova parceira de um tipo novo usa a régua e a conversa dele.
  T.ACOES['nova-parceira'](botao('nova-parceira'));
  tem('Profissional de saúde', painel()); tem('Visagista', painel()); tem('Prótese capilar', painel());
  T.ACOES['salvar-parceira'](botao('salvar-parceira', {}, ctxCom({ '#np-nome': campo('Consultório Dra. Paula'), '#np-bairro': campo('Batel'), 'TODOS .opcao[data-grupo="tipo"][aria-pressed="true"]': [opc('saude')] })));
  var nova = T.estado.parceiras[T.estado.parceiras.length - 1];
  igual(nova.tipo, 'saude', 'tipo da nova'); igual(T.proximoPasso(nova), 'Conhecer: troca de indicação, sem comissão', 'nova da saúde');
  T.ACOES['chat-sugestao'](botao('chat-sugestao', { pergunta: 'Como está a Lina Costa?' })); __flush();
  tem('Forte', ultimaMsg()); tem('régua das visagistas', ultimaMsg());
  T.ACOES.fechar();
});
run('extrato de comissão por parceira, sem nome de paciente', function () {
  T.ACOES.reiniciar(botao('reiniciar'));
  T.estado.parceiras.filter(function (p) { return !p.prospect; }).forEach(function (p) { igual(p.fechMes.reduce(function (a, b) { return a + b; }, 0), p.fech90, 'fechMes de ' + p.id); });
  igual(T.PRECOS.comissao, 150, 'comissão');
  var e = T.extratoDe(pp('navalha'));
  igual(e.n, 4, 'tratamentos'); igual(e.pago, 450, 'pago'); igual(e.aPagar, 150, 'a pagar');
  igual(T.extratoDe(pp('henriquesalles')), null, 'saúde sem extrato'); igual(T.extratoDe(pp('porto')), null, 'para conhecer, sem extrato');
  T.ACOES['abrir-parceira'](botao('abrir-parceira', { id: 'navalha' }));
  tem('Extrato da comissão', painel()); tem('R$ 150 cada', painel()); tem('<strong>R$ 450</strong> pagos', painel()); tem('<strong>R$ 150</strong> a pagar no fechamento do mês', painel());
  tem('A pagar', painel()); tem('Pago', painel()); tem('nunca mostra nome de paciente', painel());
  var ext = trecho(painel(), 'Extrato da comissão', 'Kit da parceira');
  T.estado.pacientes.forEach(function (p) { naoTem(p.nome, ext); });
  T.ACOES.texto(botao('texto', { tipo: 'extrato', id: 'navalha' }));
  tem('Oi, Rafael!', painel()); tem('4 tratamentos começaram por indicação de vocês', painel()); tem('R$ 450 já foram pagos', painel()); tem('R$ 150 ficam para o fechamento do mês', painel());
  T.estado.pacientes.forEach(function (p) { naoTem(p.nome, painel()); });
  T.ACOES.fechar();
  ir('gestao');
  var g = trecho(__els.tela.innerHTML, 'id="h-comissoes"', '</section>');
  tem('Comissões das parceiras'); tem('R$ 750', g); tem('a pagar no fechamento do mês', g); tem('R$ 1.500', g); tem('5 tratamentos iniciados nos últimos 30 dias', g); tem('5 parceiras', g);
  T.estado.pacientes.forEach(function (p) { naoTem(p.nome, g); });
  var c = T.comissoesDoMes(); igual(c.aPagar, 750, 'mês'); igual(c.pago, 1500, 'pago');
  // Quando a avaliação de quem veio pela Navalha fecha, a comissão do mês sobe.
  T.ACOES['abrir-consulta'](botao('abrir-consulta', { id: 'camila' }));
  T.ACOES.resultado(botao('resultado', { id: 'camila', valor: 'fechou' }));
  T.ACOES.fechar();
  igual(T.extratoDe(pp('navalha')).aPagar, 300, 'navalha a pagar'); igual(T.comissoesDoMes().aPagar, 900, 'total do mês');
  ir('gestao'); tem('R$ 900', trecho(__els.tela.innerHTML, 'id="h-comissoes"', '</section>'));
  T.ACOES['abrir-parceira'](botao('abrir-parceira', { id: 'equilibrio' }));
  tem('Sem comissão', painel()); naoTem('Extrato da comissão', painel());
  T.ACOES.fechar();
});
run('campanha do emagrecimento: proposta, sem ativar', function () {
  T.ACOES.reiniciar(botao('reiniciar'));
  ir('marketing');
  var prop = trecho(__els.tela.innerHTML, 'class="cartao proposta"', 'id="h-resultados"');
  tem('Proposta do agente de marketing', prop); tem('Perca os quilos, cuide dos fios', prop); tem('Aguardando decisão: o papel da Dra. Monielle', prop);
  // Foco no público final (Luiz, 07/10/2026): quem toma o remédio para emagrecer e está perdendo cabelo; saem médicos e barbeiros.
  tem('O foco agora é o público final', prop); tem('O conteúdo', prop); tem('O anúncio', prop); tem('A busca', prop);
  naoTem('Começou o remédio', prop); naoTem('Médicos, sem comissão', prop); naoTem('Barbeiros, com comissão', prop); naoTem('Folheto para a sala de espera', prop); naoTem('Mounjaro', prop);
  // Sem código nem frase pronta (Luiz, 07/10/2026): a Fer registra a origem no contato.
  naoTem('Vim pelo emagrecimento', prop); tem('a Fer registra a origem no contato', prop); tem('Ponto zero', prop);
  tem('Emagrecer rápido e a queda de cabelo: o que se sabe', prop); tem('Emagrecendo? Cuide dos fios também.', prop); tem('Nome de remédio não entra em anúncio', prop);
  ['e1', 'e2', 'e3'].forEach(function (id) { var x = T.peca(id); igual(T.conferirPeca(x.legenda, x).filter(function (a) { return typeof a !== 'string' || a.indexOf('Fala de medicamento') < 0; }).length, 0, 'conferência de ' + id); });
  if (!/<button[^>]*\bdisabled\b[^>]*>(<svg[\s\S]*?<\/svg>)?Ativar a campanha<\/button>/.test(prop)) throw new Error('o botão de ativar tem de ficar desligado');
  if (T.ACOES['ativar-campanha'] || /data-acao="ativar/.test(prop)) throw new Error('não pode haver ação de ativar');
  // As peças de exemplo ficam fora da programação e das contagens.
  igual(T.estado.pecas.filter(function (x) { return /emagrec|quilos/i.test(x.titulo); }).length, 0, 'fora do banco de posts');
  naoTem('Perca os quilos', calendario()); naoTem('Emagrecer rápido', calendario());
  tem('Fala de medicamento (canetas para emagrecer)', prop);
  // Conferência fixa: canetas para emagrecer e nomes de remédio.
  tem('Fala de medicamento', avisosPeca('Começou as canetas para emagrecer e o cabelo caiu?'));
  tem('Fala de medicamento', avisosPeca('Usa caneta pra emagrecer?'));
  ['Mounjaro', 'Ozempic', 'Wegovy', 'tirzepatida', 'semaglutida', 'liraglutida'].forEach(function (n) {
    var a = avisosPeca('Quem usa ' + n + ' pode ter queda.');
    tem('Nome de remédio não entra em anúncio', a); tem('Fala de medicamento', a);
  });
  igual(T.conferirPeca('Ozempic', { linha: 'como' })[0].forte, true, 'o aviso do nome de remédio é forte');
  igual(avisosPeca('Perca os quilos, cuide dos fios. Emagrecimento rápido pede cuidado com o cabelo.'), '', 'a frase da campanha passa');
  if (!T.conferirResposta('Quem usa Mounjaro pode vir?').length) throw new Error('resposta do Google com nome de remédio');
  // Peça de exemplo: abre, mostra o aviso e confere ao digitar, mas não se aprova.
  T.ACOES['abrir-peca'](botao('abrir-peca', { id: 'e1' }));
  tem('Proposta, sem ativar', painel()); tem('fora da programação', painel()); tem('Fala de medicamento (canetas para emagrecer)', painel()); tem('1 de 4', painel());
  tem('Aguardando decisão: o papel da Dra. Monielle', painel()); naoTem('data-acao="aprovar-peca"', painel()); naoTem('Volta para a Fer', painel());
  var caixa = new El('caixa'); var ctxIn = ctxCom({ '[data-conferido]': caixa });
  __handlers.input[0]({ target: { dataset: { legenda: 'e2' }, value: 'Perca os quilos com Ozempic e cuide dos fios', closest: function () { return ctxIn; } } });
  tem('Nome de remédio não entra em anúncio', caixa.innerHTML); tem('conferido-forte', caixa.innerHTML); tem('Troque “Ozempic”', caixa.innerHTML);
  T.ACOES['aprovar-peca'](botao('aprovar-peca', { id: 'e1' }));
  igual(T.peca('e1').estado, 'proposta', 'exemplo não se aprova');
  T.ACOES.fechar();
  T.ACOES['abrir-peca'](botao('abrir-peca', { id: 'e2' }));
  tem('A conferência fixa não apontou nada', painel());
  T.ACOES.fechar();
  T.estado.pecas.forEach(function (x) { if (/emagrec/i.test(x.titulo)) throw new Error('exemplo entrou no banco'); });
  // Conversa: a IA explica que a campanha é proposta, e pergunta sobre remédio vai para a regra clínica.
  T.ACOES['chat-sugestao'](botao('chat-sugestao', { pergunta: 'Como está a campanha do emagrecimento?' })); __flush();
  tem('Aguardando decisão: o papel da Dra. Monielle', ultimaMsg()); tem('data-alvo="h-proposta"', ultimaMsg());
  T.ACOES['chat-sugestao'](botao('chat-sugestao', { pergunta: 'O Mounjaro faz cair cabelo?' })); __flush();
  tem('não opino', ultimaMsg());
  T.ACOES.ir(botao('ir', { tela: 'marketing', alvo: 'h-proposta' }));
  igual(T.estado.rolarPara, null, 'rolou até a proposta');
  tem('Proposta do agente de marketing');
});
run('captação: as parceiras se abrem por tipo, mantendo os totais', function () {
  T.ACOES.reiniciar(botao('reiniciar'));
  var o = T.estado.capt.origens.filter(function (x) { return x.id === 'parceira'; })[0];
  function somaT(k) { return o.tipos.reduce(function (a, t) { return a + t[k]; }, 0); }
  ['contatos', 'avaliacoes', 'fecharam'].forEach(function (k) { igual(somaT(k), o[k], k); });
  igual(o.tipos.map(function (t) { return t.tipo; }).join(','), 'barbearia,visagista,salao', 'tipos');
  // Os contatos do mês das parceiras batem com as indicações do último mês de cada uma.
  var ultimoMes = T.estado.parceiras.filter(function (p) { return !p.prospect; }).reduce(function (a, p) { return a + p.ind[5]; }, 0);
  igual(ultimoMes, o.contatos, 'indicações do último mês');
  igual(T.estado.capt.origens.reduce(function (a, x) { return a + x.contatos; }, 0), T.estado.capt.d30.contatos, 'origens somam os contatos do mês');
  ir('marketing');
  var origem = trecho(__els.tela.innerHTML, 'id="h-origem"', 'id="h-meta"');
  tem('Barbearias', origem); tem('Visagistas', origem); tem('Salões', origem); tem('barra--sub', origem); tem('As parceiras se abrem por tipo', origem);
  tem('as parceiras trouxeram 10 (8 pelas barbearias, 1 pelas visagistas e 1 pelos salões)');
  T.ACOES['abrir-consulta'](botao('abrir-consulta', { id: 'camila' }));
  T.ACOES.resultado(botao('resultado', { id: 'camila', valor: 'fechou' }));
  T.ACOES.fechar();
  ['contatos', 'avaliacoes', 'fecharam'].forEach(function (k) { igual(somaT(k), o[k], 'depois de fechar: ' + k); });
  igual(o.tipos[0].fecharam, 4, 'barbearias');
  T.ACOES['chat-sugestao'](botao('chat-sugestao', { pergunta: 'De onde vieram os pacientes novos nos últimos 30 dias?' })); __flush();
  tem('pelas barbearias', ultimaMsg()); tem('pelas visagistas', ultimaMsg());
  T.ACOES.fechar();
});
run('Luiz, 07/10 · contato novo: a Lina Costa mandou o contato do André, e a Fer agradeceu e convidou', function () {
  T.ACOES.reiniciar(botao('reiniciar'));
  ir('marketing');
  var cont = trecho(__els.tela.innerHTML, 'id="h-contatos"', '</section>');
  tem('André Lacerda', cont); tem('Veio pela Lina Costa', cont); tem('a Lina Costa mandou o contato dele', cont); naoTem('LINACOSTA', cont); tem('Respondeu, sem avaliação marcada', cont);
  T.ACOES['abrir-ficha'](botao('abrir-ficha', { id: 'andrelacerda' }));
  tem('Contato novo', painel()); tem('Veio pela Lina Costa', painel()); tem('data-acao="abrir-parceira" data-id="linacosta"', painel()); tem('A Lina Costa mandou o contato dele', painel()); tem('primeiros horários livres', painel());
  T.ACOES.fechar();
  // Paulo Mendes: a dúvida sobre o Capilare Fort foi direto para a Dra. Monielle, e nada ficou registrado (Luiz, 07/10/2026).
  var paulo = T.paciente('paulomendes');
  igual(paulo.alertas, undefined, 'sem relato registrado'); if (paulo.encaminhado) throw new Error('o que a Dra. Monielle decide fica com ela');
  igual(paulo.tarefas.length, 0, 'sem tarefa'); igual(paulo.prot.feitas, 2, 'sessões'); igual(T.grupoDe(paulo), 'tratamento', 'grupo');
  T.ACOES['abrir-ficha'](botao('abrir-ficha', { id: 'paulomendes' }));
  naoTem('Capilare Fort junto com as sessões', painel()); tem('Sessão 3 de 6', painel()); tem('17:00', painel()); tem('Capilare Tonic', painel());
  T.ACOES.fechar();
});
// ---------- Etapas 3 e 4: preços, previsão, valor, oportunidades, receita em jogo, capacidade, textos fixos, exame e insumos ----------
function ach(id) { return T.achados().filter(function (a) { return a.id === id; })[0]; }
function somaV(xs) { return xs.reduce(function (a, b) { return a + b; }, 0); }
run('preços por produto: tabela fictícia, e o lançamento usa a tabela', function () {
  T.ACOES.reiniciar(botao('reiniciar'));
  igual(T.preco('avaliacao'), 350, 'avaliação'); igual(T.preco('protocolo'), 2600, 'protocolo'); igual(T.preco('ciclo'), 2200, 'novo ciclo');
  igual(T.preco('manutencao'), 390, 'manutenção');
  // A consulta da Dra. Monielle não entra na receita da clínica (Luiz, 07/10/2026): só se teve ou não.
  igual(T.PRODUTOS.consulta, undefined, 'a consulta da Dra. Monielle não tem preço aqui');
  // O home care vem incluído no pacote, e o paciente não paga nada à parte (Luiz, 07/10/2026): não tem preço.
  ['nutri', 'tonic', 'fort'].forEach(function (k) { igual(T.PRODUTOS[k], undefined, 'sem preço: ' + k); });
  ir('gestao');
  var precos = trecho(__els.tela.innerHTML, 'id="h-precos"', '</section>');
  tem('Preços por produto', precos); tem('Fictícios e configuráveis', precos); tem('R$ 2.200', precos); tem('Por sessão.', precos); tem('Capilare Nutri, 120 dias; Capilare Tonic, 75; Capilare Fort, 120.', precos); tem('pelo padrão do ciclo: 20, 30, 40, 45 e 45 dias', precos); naoTem('15 dias', precos);
  tem('No pacote', precos); tem('O paciente não paga nada à parte', precos); tem('com o home care incluído', precos); naoTem('Quanto dura cada kit', precos);
  tem('R$ 150', precos); tem('A avaliação é cobrada à parte?', precos); tem('A manutenção é por sessão ou por pacote?', precos);
  // Os recebimentos por tipo são a quantidade vezes o preço e somam o total dos 30 dias; as semanas batem.
  var R = T.estado.receb;
  igual(somaV(T.recebidoPorTipo().map(function (x) { return x.valor; })), R.d30, 'os tipos somam os 30 dias');
  igual(R.d30, 40220, '30 dias'); igual(R.d30ant, 33580, '30 dias anteriores');
  igual(somaV(R.semanas.slice(4)), R.d30, 'as 4 últimas semanas'); igual(somaV(R.semanas.slice(0, 4)), R.d30ant, 'as 4 primeiras semanas');
  var tipos = trecho(__els.tela.innerHTML, 'id="h-tipos"', '</section>');
  tem('11 × R$ 2.600', tipos); tem('18 × R$ 350', tipos); naoTem('Home care', tipos); naoTem('kits', tipos); naoTem('Lançar R$ 2.600', tipos); tem('Falta a Fer lançar', tipos);
  // Na ficha, o home care aparece na parte dele, e não nos pagamentos.
  T.ACOES['abrir-ficha'](botao('abrir-ficha', { id: 'aline' }));
  tem('<h3>Home care</h3>', painel()); naoTem('Capilare', trecho(painel(), '<h3>Pagamentos</h3>', '</section>'));
  T.ACOES.fechar();
  // Em tratamento, a ficha mostra o padrão do ciclo, e não mais um intervalo fixo.
  T.ACOES['abrir-ficha'](botao('abrir-ficha', { id: 'leandro' }));
  tem('Padrão do ciclo: 20, 30, 40, 45 e 45 dias entre as sessões', painel()); naoTem('Uma sessão a cada', painel());
  T.ACOES.fechar();
  // Quem lança é a Fer (Luiz, 07/10/2026): novo ciclo, R$ 2.200, em Novos ciclos; manutenção, R$ 390; fechar o protocolo, R$ 2.600.
  T.ACOES['abrir-consulta'](botao('abrir-consulta', { id: 'jorge' }));
  T.ACOES.resultado(botao('resultado', { id: 'jorge', valor: 'ciclo' }));
  tem('lança o pacote e os valores, R$ 2.200', painel()); naoTem('ficou na sua lista para lançar', painel());
  T.ACOES.fechar();
  var it = T.estado.aLancar.filter(function (x) { return x.nome === 'Jorge Almeida'; })[0];
  igual(it.valor, 2200, 'novo ciclo'); igual(it.tipo, 'ciclo', 'tipo');
  var antes = R.d30;
  T.ACOES['fer-lancou'](botao('fer-lancou', { nome: 'Jorge Almeida' }));
  igual(R.d30, antes + 2200, '30 dias com o novo ciclo'); igual(R.itens.ciclo, 2, 'novos ciclos');
  igual(somaV(T.recebidoPorTipo().map(function (x) { return x.valor; })), R.d30, 'os tipos seguem somando o total');
  ir('gestao'); tem('2 × R$ 2.200', trecho(__els.tela.innerHTML, 'id="h-tipos"', '</section>'));
  T.ACOES.reiniciar(botao('reiniciar'));
  T.ACOES['abrir-consulta'](botao('abrir-consulta', { id: 'jorge' }));
  T.ACOES.resultado(botao('resultado', { id: 'jorge', valor: 'manutencao' }));
  tem('lança a sessão, de R$ 390', painel());
  T.ACOES.fechar();
  T.ACOES['abrir-consulta'](botao('abrir-consulta', { id: 'camila' }));
  T.ACOES.resultado(botao('resultado', { id: 'camila', valor: 'fechou' }));
  T.ACOES.fechar();
  igual(T.estado.aLancar.filter(function (x) { return x.nome === 'Jorge Almeida'; })[0].valor, 390, 'manutenção');
  igual(T.estado.aLancar.filter(function (x) { return x.nome === 'Camila Duarte'; })[0].valor, 2600, 'protocolo');
  // Nenhum lançamento usa mais um valor fixo para tudo: cada um sai da tabela.
  T.estado.aLancar.forEach(function (x) { igual(x.valor, T.preco(x.tipo), 'valor da tabela para ' + x.nome); });
  T.ACOES['fer-lancou'](botao('fer-lancou', { nome: 'Jorge Almeida' }));
  igual(T.estado.receb.itens.manutencao, 9, 'a manutenção entra em Manutenções');
});
run('previsão de 30, 60 e 90 dias em faixa, com como calculei', function () {
  T.ACOES.reiniciar(botao('reiniciar'));
  var P = T.previsao();
  igual(P.acumulado.map(function (x) { return x.dias; }).join(','), '30,60,90', 'janelas');
  igual(P.acumulado[0].min, 34410, '30 dias, mínimo'); igual(P.acumulado[0].max, 46790, '30 dias, máximo');
  igual(P.acumulado[1].min, 68380, '60 dias, mínimo'); igual(P.acumulado[2].max, 140370, '90 dias, máximo');
  // A janela de 30 dias, refeita à mão: avaliações marcadas × fechamento × ticket e manutenções.
  var j = P.janelas[0];
  function parte(id) { return j.partes.filter(function (x) { return x.id === id; })[0]; }
  igual(parte('avaliacoes').min, 15 * 350, 'avaliações'); igual(parte('tratamentos').min, Math.round(15 * 0.6 * 2600), 'tratamentos, mínimo');
  igual(parte('tratamentos').max, Math.round(18 * (12 / 18) * 2600), 'tratamentos, máximo');
  igual(parte('homecare'), undefined, 'o home care vem no pacote e não entra');
  igual(parte('manutencao').min, 8 * 390, 'manutenções'); igual(parte('consultas'), undefined, 'a consulta da Dra. Monielle não entra');
  if (P.acumulado[0].min >= P.acumulado[0].max) throw new Error('faixa invertida');
  if (P.acumulado[2].max - P.acumulado[2].min <= P.acumulado[0].max - P.acumulado[0].min) throw new Error('a faixa de 90 dias tem de ser mais larga');
  // As sessões da Ana Carolina não entram: o tratamento é pago na avaliação.
  igual(j.partes.filter(function (x) { return /sess/i.test(x.nome); }).length, 0, 'sem sessões');
  ir('gestao');
  var prev = trecho(__els.tela.innerHTML, 'id="h-previsao"', '</section>');
  tem('R$ 34,4 mil a R$ 46,8 mil', prev); tem('R$ 68,4 mil a R$ 93,6 mil', prev); tem('R$ 102,4 mil a R$ 140,4 mil', prev);
  tem('Como calculei os próximos 30 dias', prev); tem('14 já marcadas', prev); tem('60% a 67% de fechamento', prev); tem('Home care: vem incluído no pacote', prev);
  // Sem o home care na receita (Luiz, 07/10/2026), nem o cenário alto chega à meta fictícia.
  tem('As sessões da Ana Carolina não entram', prev); tem('Nem o cenário alto chega à meta de R$ 50 mil por mês', prev); tem('faixa-meta', prev);
  tem('previstos para os próximos 30 dias, em faixa'); tem('A clínica em números.');
  // Quando uma avaliação fecha, a taxa de fechamento muda e a previsão acompanha.
  T.ACOES['abrir-consulta'](botao('abrir-consulta', { id: 'camila' }));
  T.ACOES.resultado(botao('resultado', { id: 'camila', valor: 'fechou' }));
  T.ACOES.fechar();
  if (T.previsao().acumulado[0].max <= 48390) throw new Error('a previsão não acompanhou o fechamento');
  T.ACOES['chat-sugestao'](botao('chat-sugestao', { pergunta: 'Quanto deve entrar nos próximos 30 dias?' })); __flush();
  tem('Próximos 90 dias', ultimaMsg()); tem('já foram pagas na avaliação', ultimaMsg()); tem('data-alvo="h-previsao"', ultimaMsg());
  T.ACOES['chat-sugestao'](botao('chat-sugestao', { pergunta: 'Qual a previsão do tempo?' })); __flush();
  tem('No protótipo', ultimaMsg());
  T.ACOES.fechar();
});
run('valor: ticket, quem vende e quem executa, recorrência, origem, fechamento do mês e meta', function () {
  T.ACOES.reiniciar(botao('reiniciar'));
  igual(T.ticketMedio(), 350 + 2600, 'ticket médio: avaliação e protocolo, com o home care no pacote');
  var x = T.receitaPorProfissional();
  igual(somaV(x.vende.map(function (v) { return v.valor; })) + x.executa.valorManut, T.estado.receb.d30, 'quem vende e as manutenções somam o que entrou');
  igual(x.vende[0].valor, 18 * 350 + 11 * 2600 + 2200, 'a doutora vende avaliação, protocolo e novo ciclo');
  igual(x.vende.length, 1, 'a consulta da Dra. Monielle não entra: ela trabalha com o sistema dela');
  igual(x.executa.sessoes, 176, 'sessões já pagas'); igual(x.executa.valor, Math.round((158 * 2600 + 18 * 2200) / 6), 'valor executado');
  igual(T.recorrencia().total, 8 * 390 + 2200, 'recorrência: manutenções e novos ciclos');
  var o = {}; T.valorPorOrigem().forEach(function (v) { o[v.id] = v; });
  igual(o.visagista.porPaciente, Math.round((10 * 350 + 8 * 2600 + 2 * 2200 + 9 * 390) / 10), 'visagista');
  igual(o.barbearia.porPaciente, 2519, 'barbearia'); igual(o.Instagram.porPaciente, 1636, 'Instagram');
  igual(o.barbearia.comissaoPct, 3.8, 'comissão da barbearia, em % do tratamento iniciado'); igual(o.paciente.comissaoPct, null, 'indicação de paciente não tem comissão');
  igual(T.valorPorOrigem().slice().sort(function (a, b) { return b.porPaciente - a.porPaciente; })[0].id, 'visagista', 'a origem que mais vale');
  ir('gestao');
  tem('ticket médio do tratamento novo'); tem('R$ </small>2.950'); tem('com o home care incluído');
  var profs = trecho(__els.tela.innerHTML, 'id="h-profs"', '</section>');
  tem('Quem vende', profs); tem('Quem executa: Ana Carolina', profs); tem('176 · R$ 75,1 mil', profs); tem('8 · R$ 3.120', profs); tem('R$ 37,1 mil', profs); naoTem('Home care', profs);
  var rec = trecho(__els.tela.innerHTML, 'id="h-recorrencia"', '</section>');
  tem('R$ 5.320', rec); tem('13% do que entrou', rec); tem('manutenções e novos ciclos', rec); tem('R$ 5.760 a R$ 9.290', rec);
  tem('O home care vem incluído no pacote', rec); naoTem('Reposições de home care', rec);
  var vo = trecho(__els.tela.innerHTML, 'id="h-valor-origem"', '</section>');
  tem('Visagistas', vo); tem('R$ 3.221', vo); tem('3,7% desse valor', vo); tem('O home care vem no pacote', vo); tem('quem decide é a clínica', vo); tem('Profissional de saúde não recebe comissão', vo);
  T.estado.pacientes.forEach(function (p) { naoTem(p.nome, vo); });
  var fech = trecho(__els.tela.innerHTML, 'id="h-fechamento"', '</section>');
  var h = T.util(0);
  var mes = new Date(h.getFullYear(), h.getMonth() - 1, 1);
  var nome = T.MESES_LONGOS[mes.getMonth()];
  nome = nome.charAt(0).toUpperCase() + nome.slice(1);
  tem(nome + ' de ' + mes.getFullYear(), fech); tem(nome + ' de ' + (mes.getFullYear() - 1), fech); tem('52% acima do mesmo mês do ano passado', fech);
  tem('Meta de receita: R$ 50 mil por mês', fech); tem('R$ 40.220 · 80%', fech); tem('A linha marca a meta.', fech);
  tem('80% da meta de R$ 50 mil por mês');
  T.ACOES['chat-sugestao'](botao('chat-sugestao', { pergunta: 'Quanto vale um paciente de cada origem?' })); __flush();
  tem('Visagistas', ultimaMsg()); tem('comissão', ultimaMsg());
  T.ACOES.fechar();
});
run('painel Oportunidades: seis agentes, cotas e regras à vista', function () {
  T.ACOES.reiniciar(botao('reiniciar'));
  ir('gestao');
  var op = trecho(__els.tela.innerHTML, 'id="h-oportunidades"', 'id="h-receita"');
  ['Cadastro', 'Retenção', 'Agenda e capacidade', 'Vigia da receita', 'Crescimento', 'Qualidade e reputação'].forEach(function (n) { tem(n, op); });
  tem('2 de 2', op); tem('para você hoje, no Esperando você', op); tem('apostas de crescimento nesta semana', op); tem('achados da vigia da receita hoje', op);
  tem('itens novos para a Fer hoje', op); naoTem('alerta clínico', op);
  tem('O que é muito recusado perde peso', op); tem('com 4 recusas', op); tem('Mudar uma regra é sempre proposta para você aprovar', op);
  tem('Nenhum agente fala com paciente nem lê prontuário', op); tem('Um editor, em código, junta o que é do mesmo paciente', op);
  ['Evidência', 'Impacto', 'Ação', 'Dono', 'Validade'].forEach(function (c) { tem('<dt>' + c + '</dt>', op); });
  // Cotas: a doutora recebe 2 por dia e 3 apostas por semana, a vigia deixa até 3 e a Fer recebe até 3 novos por dia.
  var A = T.achados();
  function conta(l) { return A.filter(function (a) { return a.lugar === l; }).length; }
  igual(conta('hoje'), 2, 'doutora hoje'); igual(conta('apostas'), 3, 'apostas'); igual(conta('receita'), 1, 'vigia'); igual(conta('fer'), 3, 'Fer');
  igual(conta('fila'), 3, 'fora da cota');
  A.forEach(function (a) { ['evidencia', 'impacto', 'acao', 'dono', 'validade'].forEach(function (k) { if (!a[k]) throw new Error('achado sem ' + k + ': ' + a.id); }); });
  A.forEach(function (a) { if (/garant|promet/i.test(a.titulo + a.evidencia + a.acao)) throw new Error('promessa em ' + a.id); });
  // O editor juntou os dois achados do Leandro Schmitt numa ligação só, e quem decide é a Fer.
  // A sessão que falta e o home care que acabou: dois achados da retenção. O kit vem no pacote e não é receita.
  igual(ach('leandro').agentes.join(','), 'retencao', 'agentes juntados'); tem('que já vem no pacote', op); tem('Juntado pelo editor', op); tem('uma ligação só', op);
  naoTem('data-acao="achado" data-id="leandro"', op); tem('A Fer decide na lista dela', op);
  tem('Proposta de regra', op); tem('Fora da cota', op); tem('Amanhã, na lista da Fer', op); tem('Semana que vem', op);
  tem('Aceito ontem. Entra no próximo roteiro de visitas.', op); naoTem('data-acao="jogo" data-id="lancar-otavio"', op); tem('Otávio Ramos fechou há 3 dias, e o pacote ainda não foi lançado', op);
  // Filtro por agente.
  T.ACOES['filtrar-agente'](botao('filtrar-agente', { filtro: 'crescimento' }));
  op = trecho(__els.tela.innerHTML, 'id="h-oportunidades"', 'id="h-receita"');
  tem('A Barbearia Kings', op); naoTem('Leandro Schmitt: uma ligação', op);
  T.ACOES['filtrar-agente'](botao('filtrar-agente', { filtro: 'qualquer' }));
  igual(T.estado.filtroAgente, 'crescimento', 'filtro inválido não muda');
  T.ACOES['filtrar-agente'](botao('filtrar-agente', { filtro: 'todos' }));
  T.ACOES['chat-sugestao'](botao('chat-sugestao', { pergunta: 'O que os agentes da noite acharam?' })); __flush();
  tem('Os seis agentes da noite deixaram 12 achados', ultimaMsg()); tem('data-alvo="h-oportunidades"', ultimaMsg());
  T.ACOES.fechar();
});
run('oportunidades: aceitar, adiar e recusar com motivo, em um toque', function () {
  T.ACOES.reiniciar(botao('reiniciar'));
  ir('hoje'); tem('lota na semana 7'); grupo('receita');
  tem('Quem para o protocolo, para depois da sessão 3'); tem('1 dos 4 ativos em risco está nesse ponto: Camila Ribas'); tem('Todas as oportunidades');
  naoTem('Proposta de regra: o contato de manutenção'); // passou da cota de 2 por dia: fica para amanhã
  // Aceitar: a campanha vai para a Fer, e a conversa de segunda ganha o assunto.
  T.ACOES.achado(botao('achado', { id: 'sessao3', valor: 'aceitar' }));
  igual(T.estado.achados.sessao3.st, 'aceito', 'aceito'); tem('Campanha aprovada'); grupo(''); tem('7 decisões, 1 oportunidade e 5 posts'); grupo('receita');
  T.ACOES.achado(botao('achado', { id: 'sessao3', valor: 'recusar' }));
  igual(T.estado.achados.sessao3.st, 'aceito', 'não decide duas vezes');
  // Recusar pede o motivo, em um toque.
  T.ACOES.achado(botao('achado', { id: 'lotaAna', valor: 'recusar' }));
  tem('Por que recusar?'); tem('A evidência não convence');
  T.ACOES['achado-voltar'](botao('achado-voltar', { id: 'lotaAna' }));
  naoTem('Por que recusar?');
  T.ACOES.achado(botao('achado', { id: 'lotaAna', valor: 'recusar' }));
  T.ACOES['achado-motivo'](botao('achado-motivo', { id: 'lotaAna', motivo: 'momento' }));
  igual(T.estado.achados.lotaAna.st, 'recusado', 'recusado'); igual(T.estado.achados.lotaAna.motivo, 'momento', 'motivo'); igual(T.estado.recusas.capacidade, 1, 'recusa contada');
  tem('Recusado: não é o momento'); grupo(''); tem('7 decisões e 5 posts');
  ir('gestao'); tem('Campanha da sessão 3, aprovada hoje', trecho(__els.tela.innerHTML, 'id="h-pauta"', '</section>'));
  // Adiar: volta em 7 dias. Aceitar a aposta da Kings cadastra a barbearia em Para conhecer.
  T.ACOES.achado(botao('achado', { id: 'regra45', valor: 'adiar' }));
  igual(T.estado.achados.regra45.st, 'adiado', 'adiado'); tem('Adiado: volta em 7 dias');
  T.ACOES.achado(botao('achado', { id: 'kings', valor: 'aceitar' }));
  var k = T.parceira('kings');
  if (!k || !k.prospect) throw new Error('a Kings não entrou em Para conhecer');
  igual(T.statusDe(k), 'conhecer', 'status da Kings');
  ir('parceiras'); tem('Barbearia Kings');
  // O item da Fer e o da vigia não se decidem por aqui com aceitar ou recusar.
  T.ACOES.achado(botao('achado', { id: 'leandro', valor: 'aceitar' })); igual(T.estado.achados.leandro, undefined, 'item da Fer');
  T.ACOES.achado(botao('achado', { id: 'reavaliacoes', valor: 'aceitar' })); igual(T.estado.achados.reavaliacoes, undefined, 'item da vigia');
  // Três recusas do mesmo tipo: o tipo perde peso.
  T.ACOES.reiniciar(botao('reiniciar'));
  ['kings', 'indicaFim', 'instagram'].forEach(function (id) {
    T.ACOES.achado(botao('achado', { id: id, valor: 'recusar' }));
    T.ACOES['achado-motivo'](botao('achado-motivo', { id: id, motivo: 'momento' }));
  });
  igual(T.estado.recusas.aposta, 3, 'três recusas');
  tem('perdeu peso', __els.toast.innerHTML);
  ir('gestao'); tem('“aposta de crescimento”, com 3 recusas');
  ir('hoje'); grupo('crescimento'); tem('Apostas da semana: todas decididas'); grupo('');
  T.ACOES.ir(botao('ir', { tela: 'gestao', alvo: 'h-oportunidades', agente: 'crescimento' }));
  igual(T.estado.filtroAgente, 'crescimento', 'o atalho abre o painel nas apostas');
});
run('receita em jogo: o que a doutora decide, em reais; o que fechou e não foi lançado vai para a Fer', function () {
  T.ACOES.reiniciar(botao('reiniciar'));
  ir('hoje'); grupo('receita');
  var rj = trecho(__els.tela.innerHTML, 'id="h-receita-jogo"', '</section>');
  tem('Receita em jogo', rj); tem('1 achado da vigia da receita, de até 3 por dia', rj);
  naoTem('Otávio Ramos', rj); naoTem('Foi cortesia', rj); naoTem('Capilare Nutri', rj); naoTem('em reposição', rj);
  tem('3 pacientes terminaram o ciclo sem reavaliação marcada', rj); tem('R$ 1.170 a R$ 6.600', rj); tem('Reforçar reavaliações', rj); tem('Abrir horário extra', rj);
  if (T.receitaEmJogo().length > 3) throw new Error('mais de 3 achados');
  // O que fechou e não foi lançado está na lista da Fer, que lança o pacote e os valores (Luiz, 07/10/2026).
  var a = ach('lancar-otavio');
  igual(a.dono, 'fer', 'dono'); igual(a.lugar, 'fer', 'lugar'); tem('A Fer lança o pacote e os valores', a.acao);
  // Reavaliações: o horário extra muda a próxima avaliação livre.
  var prox = T.proximaAvaliacaoLivre();
  T.ACOES.jogo(botao('jogo', { id: 'reavaliacoes', valor: 'extra' }));
  igual(T.proximaAvaliacaoLivre().util, 1, 'horário extra no próximo dia útil'); if (prox.util === 1) throw new Error('já era no próximo dia');
  igual(T.ocupacaoDe('suellen').total, 26, 'um horário a mais na agenda');
  ir('hoje'); tem('Horário extra aberto');
  T.ACOES.jogo(botao('jogo', { id: 'reavaliacoes', valor: 'reforcar' }));
  igual(T.estado.extras.length, 1, 'decide uma vez');
  // O que fecha hoje, a Fer lança na saída: a doutora não tem recebimento a lançar.
  T.ACOES['abrir-consulta'](botao('abrir-consulta', { id: 'camila' }));
  T.ACOES.resultado(botao('resultado', { id: 'camila', valor: 'fechou' }));
  T.ACOES.fechar();
  naoTem('recebimento a lançar'); igual(T.receitaEmJogo().filter(function (x) { return x.nome === 'Camila Duarte'; }).length, 0, 'a vigia roda de madrugada');
  T.ACOES.reiniciar(botao('reiniciar'));
});
run('lembrancinhas: o calendário do ano e o aviso 10 dias antes', function () {
  T.ACOES.reiniciar(botao('reiniciar'));
  // Quem recebe: as parceiras fortes pela régua do tipo (Luiz, 07/10/2026).
  igual(T.principais().map(function (p) { return p.id; }).join(','), 'navalha,bella,linacosta', 'as parceiras principais');
  var L = T.lembrancinhas();
  igual(L.length, 7, '4 datas do ano e 3 aniversários de parceria');
  // Relógio em 06/10/2026: os 2 anos da Navalha de Ouro em 8 dias; o São Martinho em 28, com o aviso em 24/10.
  igual(L[0].id, 'aniv-navalha', 'a mais perto'); igual(L[0].dias, 8, 'em 8 dias'); igual(L[0].nome, '2 anos de parceria da Navalha de Ouro', 'o nome');
  var por = {}; L.forEach(function (x) { por[x.id] = x; });
  igual(por.martinho.dias, 28, 'São Martinho'); igual(T.dd(por.martinho.aviso), '24/10', 'o aviso, 10 dias antes');
  igual(T.dd(por.natal.data), '25/12', 'Natal'); igual(T.dd(por.barbeiro.data), '19/01', '19 de janeiro'); igual(por.barbeiro.data.getFullYear(), 2027, 'o de 2027');
  // A semana do Dia dos Pais vai da segunda ao segundo domingo de agosto: em 2027, de 02/08 a 08/08.
  igual(T.dd(por.pais.data) + ' a ' + T.dd(por.pais.fim), '02/08 a 08/08', 'semana do Dia dos Pais'); igual(por.pais.fim.getDay(), 0, 'termina no domingo');
  igual(por['aniv-bella'].para.length, 1, 'o aniversário é só da parceira'); igual(por.martinho.para.length, 3, 'as datas do ano são das três');
  // No Hoje, só o que chega em até 10 dias, no Esperando você.
  igual(T.lembrancinhasDoDia().length, 1, 'um aviso hoje');
  ir('hoje'); grupo('parcerias');
  tem('O copiloto lembra'); tem('2 anos de parceria da Navalha de Ouro, quarta, 14/10'); tem('Faltam 8 dias. Parceira desde out/2024. Sugestão: cartão escrito à mão pela doutora.');
  tem('Pedir para a Fer preparar'); tem('Eu mesma entrego'); tem('Este ano não'); tem('Sem remédio nem suplemento de presente'); naoTem('São Martinho');
  var n = T.nDecisoes();
  T.ACOES.decidir(botao('decidir', { id: 'lemb-aniv-navalha', valor: 'fer' }));
  igual(T.nDecisoes(), n - 1, 'saiu do Esperando você'); igual(T.estado.respostas['lemb-aniv-navalha'], 'Pedir para a Fer preparar', 'a resposta');
  tem('Na lista da Fer: preparar a lembrancinha', __els.toast.innerHTML); tem('Respondida');
  T.ACOES.decidir(botao('decidir', { id: 'lemb-aniv-navalha', valor: 'pular' }));
  igual(T.estado.respostas['lemb-aniv-navalha'], 'Pedir para a Fer preparar', 'decide uma vez');
  // Na tela Parceiras: o calendário, quem recebe, a situação de cada data e os cuidados, sem nome de paciente.
  ir('parceiras');
  var c = trecho(__els.tela.innerHTML, 'id="h-lembrancinhas"', '</section>');
  tem('Lembrancinhas do ano', c); tem('Para as parceiras mais fortes: Navalha de Ouro, Espaço Bella e Lina Costa', c); tem('avisa 10 dias antes', c);
  tem('Com a Fer', c); tem('Aviso em 24/10', c); tem('Dia de São Martinho de Porres', c); tem('02/08 a 08/08', c); tem('2 anos de parceria do Espaço Bella', c);
  tem('sem nome de paciente no cartão', c);
  T.estado.pacientes.forEach(function (p) { naoTem(p.nome, c); });
});
run('capacidade: próxima avaliação livre, mínimo protegido e a Ana Carolina em 8 semanas', function () {
  T.ACOES.reiniciar(botao('reiniciar'));
  igual(T.livresEm7(), T.AGENDA7.suellen.total - T.AGENDA7.suellen.ocupados, 'os livres batem com a agenda da doutora');
  var prox = T.proximaAvaliacaoLivre();
  igual(prox.util, 2, 'próxima avaliação livre'); igual(prox.dias, T.diasAte(T.util(2)), 'em dias');
  // Reservar a visita ocupa a tarde livre: a próxima avaliação livre passa para o dia seguinte.
  T.ACOES.reservar(botao('reservar', { id: 'r1' }));
  igual(T.proximaAvaliacaoLivre().util, 3, 'depois da reserva'); igual(T.livresEm7(), 6, 'livres depois da reserva');
  // O mínimo protegido: a segunda reserva deixaria 3 horários, abaixo do mínimo de 4.
  var pode = T.podeReservar(T.ROTEIROS[1]);
  igual(pode.ok, false, 'bloqueado'); igual(pode.sobram, 3, 'sobrariam 3');
  T.ACOES.reservar(botao('reservar', { id: 'r2' }));
  if (T.estado.reservados.r2) throw new Error('a visita ocupou o mínimo protegido');
  tem('o mínimo protegido é 4', __els.toast.innerHTML);
  ir('visitas');
  tem('Ocuparia horários protegidos'); tem('abaixo do mínimo protegido de 4'); tem('As visitas não ocupam os 4 horários de avaliação protegidos');
  T.ACOES['cancelar-reserva'](botao('cancelar-reserva', { id: 'r1' }));
  igual(T.podeReservar(T.ROTEIROS[1]).ok, true, 'liberado');
  // Ana Carolina: com a meta de 15 tratamentos novos, lota na semana 7; no ritmo atual, de 12, não lota.
  var L = T.projecaoAna(15);
  igual(L.length, 8, '8 semanas'); igual(T.semanaQueLota().semana, 7, 'lota na semana 7');
  // As sessões novas seguem o padrão do ciclo (20, 30, 40, 45 e 45 dias): na semana 7, a 1ª e a 2ª de cada tratamento.
  igual(L[6].novas, 7, 'sessões novas na semana 7'); igual(L[6].total, 50, 'total da semana 7'); igual(L[0].previstas, T.AGENDA7.ana.ocupados, 'a semana 1 bate com a agenda');
  igual(T.semanaQueLota(12), null, 'no ritmo atual não lota');
  ir('gestao');
  var capac = trecho(__els.tela.innerHTML, 'id="h-capacidade"', 'id="h-equipe"');
  tem('Sua próxima avaliação livre', capac); tem('Mínimo protegido: 4', capac); tem('A Ana Carolina em 8 semanas', capac);
  tem('Lota na semana 7', capac); tem('50 sessões para 50 horários', capac); tem('fica em até 98%', capac); tem('pelo padrão do ciclo (20, 30, 40, 45 e 45 dias entre elas)', capac); tem('barra-marca', capac);
  ir('hoje'); tem('Sua próxima avaliação livre é'); tem('lota na semana 7');
  T.ACOES['chat-sugestao'](botao('chat-sugestao', { pergunta: 'Como estão as agendas?' })); __flush();
  tem('Sua próxima avaliação livre é', ultimaMsg());
  T.ACOES.fechar();
});
run('Luiz, 07/10 · textos fixos: só a doutora aprova, e os cuidados depois da sessão são os que a clínica ditou', function () {
  T.ACOES.reiniciar(botao('reiniciar'));
  ir('hoje'); grupo('textos');
  tem('2 textos esperam o seu OK'); tem('Preparo do cabelo para a avaliação'); tem('Cuidados depois da sessão'); naoTem('Dra. Monielle aprovou');
  tem('A IA anexa o texto como está e nunca reescreve conteúdo clínico');
  ['Não lave o cabelo nas próximas 4 horas.', 'Nas próximas 4 horas, evite atividade física que faça suar muito.', 'Química nos fios, como tintura, alisamento ou selagem, só 48 horas depois da sessão.', 'Hoje, dia da aplicação no consultório, não precisa usar a loção do seu home care.', 'Qualquer dúvida, é só chamar por aqui.'].forEach(function (t) { tem(t, T.TEXTO_MMP); });
  naoTem('24 horas', T.TEXTO_MMP);
  T.estado.textos.forEach(function (t) { igual(t.precisa.join(','), 'Dra. Suellen', 'só a doutora aprova: ' + t.id); });
  T.ACOES['abrir-texto'](botao('abrir-texto', { id: 'mmp' }));
  tem('os cuidados que a clínica ditou', painel()); tem('sem ligação e sem perguntar como a pessoa está', painel()); tem('Só você aprova.', painel()); naoTem('Dra. Monielle', painel());
  tem('Com a sua aprovação, o texto entra na biblioteca de textos da Fer.', painel()); tem('Não lave o cabelo nas próximas 4 horas.', painel());
  tem('nunca reescreve conteúdo clínico', painel());
  T.ACOES['aprovar-texto'](botao('aprovar-texto', { id: 'mmp' }));
  igual(T.estado.textos[1].estado, 'aprovado', 'aprovado'); tem('Na biblioteca de textos da Fer', painel());
  T.ACOES['aprovar-texto'](botao('aprovar-texto', { id: 'mmp' }));
  T.ACOES.fechar();
  tem('1 texto espera o seu OK'); tem('na biblioteca de textos da Fer');
  // Pedir ajuste exige a observação, e o texto volta para quem escreveu.
  T.ACOES['abrir-texto'](botao('abrir-texto', { id: 'preparo' }));
  tem('Rascunho da Fer', painel()); tem('Lave o cabelo na véspera', painel()); tem('Se tiver exames de sangue feitos nos últimos 90 dias, traga.', painel());
  T.ACOES['ajuste-texto'](botao('ajuste-texto', { id: 'preparo' }, ctxCom({ '#obs-texto-preparo': campo('') })));
  igual(T.estado.textos[0].estado, 'aprovar', 'sem observação não pede ajuste');
  T.ACOES['ajuste-texto'](botao('ajuste-texto', { id: 'preparo' }, ctxCom({ '#obs-texto-preparo': campo('Dizer quanto tempo antes lavar o cabelo.') })));
  igual(T.estado.textos[0].estado, 'ajuste', 'ajuste'); tem('Voltou para a Fer ajustar', painel()); tem('Dizer quanto tempo antes lavar o cabelo.', painel());
  T.ACOES.fechar();
  naoTem('esperam o seu OK'); naoTem('espera o seu OK'); tem('Ajuste pedido');
  T.ACOES['ver-log'](botao('ver-log'));
  tem('entrou na biblioteca de textos da Fer', painel()); naoTem('que a Dra. Monielle já tinha aprovado', painel());
  T.ACOES.fechar();
});
run('Luiz, 07/10 · exame: todo exame vai para a Dra. Monielle, que cuida de tudo; a doutora não marca normal ou alterado', function () {
  T.ACOES.reiniciar(botao('reiniciar'));
  ir('hoje');
  naoTem('Exame para ver'); naoTem('Normal: a Fer dá o retorno'); naoTem('Alterado: encaminhar à Dra. Monielle');
  if (T.ACOES.exame) throw new Error('a doutora não marca o exame: quem cuida é a Dra. Monielle');
  T.ACOES['abrir-ficha'](botao('abrir-ficha', { id: 'natalia' }));
  tem('Exame de sangue', painel()); tem('Passado à Dra. Monielle', painel()); tem('Na hora, pela Fer', painel()); tem('A Fer passa todo exame direto para a Dra. Monielle, no mesmo dia, e ela cuida de tudo com o paciente', painel());
  tem('O arquivo fica no Clinicorp', painel()); naoTem('Visto pela doutora', painel()); tem('com a Dra. Monielle, que cuida de tudo e fala com ela direto', painel());
  T.ACOES.fechar();
});
run('Luiz, 07/10 · pedido de insumos da Fer, que faz a gestão do estoque: quem compra ainda está em aberto', function () {
  T.ACOES.reiniciar(botao('reiniciar'));
  ir('hoje'); grupo('textos');
  tem('A Fer pede'); naoTem('A Ana Carolina pede'); tem('Cartuchos de microagulhas do MMP e toucas descartáveis estão acabando');
  tem('6 em estoque, mínimo 10'); tem('8 em estoque, mínimo 15'); tem('Quem compra os insumos ainda está em aberto'); tem('Vi o pedido'); tem('A Fer faz a gestão do estoque');
  // Os números batem com o estoque no copiloto da Fer. Enquanto o bloco não existir lá, a comparação fica de fora, com aviso.
  var fer = globalThis.__INSUMOS_FER;
  if (fer) {
    var f = {}; fer.forEach(function (x) { f[x.id] = x; });
    igual(T.PEDIDO_INSUMOS.itens[0].qtd, f.cartucho.qtd, 'cartuchos'); igual(T.PEDIDO_INSUMOS.itens[0].minimo, f.cartucho.minimo, 'mínimo dos cartuchos');
    igual(T.PEDIDO_INSUMOS.itens[1].qtd, f.touca.qtd, 'toucas'); igual(T.PEDIDO_INSUMOS.itens[1].minimo, f.touca.minimo, 'mínimo das toucas');
  } else __out.push('aviso: o copiloto da Fer ainda não tem o INSUMOS_BASE; a comparação dos insumos ficou de fora');
  var n = T.nDecisoes();
  T.ACOES['insumos-visto'](botao('insumos-visto'));
  igual(T.nDecisoes(), n - 1, 'saiu do Esperando você');
  naoTem('A Fer pede'); tem('Pedido de insumos da Fer'); tem('Quem compra ainda está em aberto');
  tem('A Fer fica sabendo que você viu.', __els.toast.innerHTML);
  // Sem decisão inventada: o cartão não diz quem compra.
  naoTem('Eu compro'); naoTem('a Fer compra');
});
run('Natália Freitas vem pela Lina Costa, como no copiloto da Fer', function () {
  T.ACOES.reiniciar(botao('reiniciar'));
  var n = T.paciente('natalia');
  igual(n.origem.tipo, 'parceira', 'parceira'); igual(n.origem.id, 'linacosta', 'Lina Costa');
  T.ACOES['abrir-ficha'](botao('abrir-ficha', { id: 'natalia' }));
  tem('Veio pela Lina Costa', painel()); tem('data-acao="abrir-parceira" data-id="linacosta"', painel()); naoTem('Espaço Bella', painel());
  tem('A Lina Costa mandou o contato dela', painel()); naoTem('LINACOSTA', painel());
  T.ACOES.fechar();
});
run('reiniciar', function () {
  T.ACOES.reiniciar(botao('reiniciar'));
  tem('Bom dia, doutora.'); tem('7 decisões, 2 oportunidades e 5 posts');
  igual(T.estado.textos.filter(function (t) { return t.estado === 'aprovar'; }).length, 2, 'textos voltaram');
  igual(T.estado.insumosVisto, null, 'insumos'); igual(T.estado.extras.length, 0, 'horário extra'); igual(Object.keys(T.estado.receitaFeita).length, 0, 'receita em jogo');
  igual(Object.keys(T.estado.achados).join(','), 'visagistas', 'só a aposta de ontem'); igual(T.estado.recusas.aposta, undefined, 'recusas');
  igual(T.estado.receb.d30, 40220, 'recebimentos'); igual(T.estado.receb.itens.protocolo, 11, 'protocolos'); if (T.parceira('kings')) throw new Error('a Kings não saiu');
  igual(T.estado.filtroAgente, 'todos', 'filtro por agente'); igual(T.paciente('natalia').encaminhado, undefined, 'encaminhamento do exame');
  igual(T.estado.chat.length, 1, 'chat');
  igual(pc('p1').estado, 'aprovar', 'peças voltaram'); if (pc('r-jorge')) throw new Error('post da reavaliação não saiu');
  if (!T.parceira('porto').prospect) throw new Error('porto não voltou');
  igual(T.estado.aLancar.length, 1, 'a lançar');
  igual(T.estado.pacientes.length, 40, 'pacientes'); igual(T.paciente('ricardo').alertas, undefined, 'sem relato registrado'); if (T.estado.google.some(function (g) { return g.alerta; })) throw new Error('o relato do Google voltou a passar');
  igual(T.estado.filtroTipo, 'todos', 'filtro por tipo'); igual(T.extratoDe(T.parceira('navalha')).aPagar, 150, 'comissão da Navalha voltou');
  if (T.parceira('nova-18')) throw new Error('a parceira cadastrada não saiu');
  igual(T.estado.consultas.tatiane, 'alta', 'reavaliação das 08:00'); igual(T.estado.indicar, undefined, 'sem a indicação na hora certa');
  igual(T.estado.google.filter(function (g) { return g.estado === 'publicada'; }).length, 1, 'Google'); igual(T.estado.verComo, 'suellen', 'ver como');
});
run('conferência única: peça inteira, remédio, suplemento e resposta pública', function () {
  var p = { id: 'teste', titulo: 'Como é a avaliação', legenda: 'Passo a passo.', slides: [{ frase: 'A conversa' }, { titulo: 'Resultado garantido', texto: 'em 3 meses' }] };
  var a = T.conferirPeca(p.legenda, p);
  if (!a.some(function (x) { return typeof x === 'string' && x.indexOf('promessa') >= 0 && x.indexOf('no cartão 2') >= 0; })) throw new Error(JSON.stringify(a));
  if (!T.conferirPeca('Dutasterida para a queda', null).some(function (x) { return typeof x === 'string' && x.indexOf('Dra. Monielle') >= 0; })) throw new Error('dutasterida');
  if (!T.conferirPeca('Biotina e vitaminas para o cabelo', null).some(function (x) { return typeof x === 'string' && x.indexOf('suplemento') >= 0; })) throw new Error('suplemento');
  if (!T.conferirPeca('90% dos pacientes melhoram', null).length) throw new Error('percentual');
  ['Foi um prazer te atender, volte sempre!', 'Que bom que melhorou muito! Continue assim.', 'Continue com o tratamento, vai dar certo.'].forEach(function (f) {
    if (!T.conferirResposta(f).length) throw new Error('resposta pública: ' + f);
  });
  if (T.conferirResposta('Obrigada pela sua avaliação, Marcos! A equipe da clínica agradece.').length) throw new Error('barrou resposta limpa');
});
run('conferência única: conversa não responde pergunta de remédio nem relato', function () {
  ['Ela usa finasterida, tem problema?', 'A Odete reclamou que ficou com inchaço'].forEach(function (f) {
    var r = T.responder(f);
    var html = (r && (r.html || r.texto)) || JSON.stringify(r);
    if (html.indexOf('eu não opino') < 0) throw new Error(f + ' -> ' + html.slice(0, 120));
  });
});
run('Luiz, 07/10 · texto fixo: nenhum passa pela Dra. Monielle; com a aprovação da doutora, entra na biblioteca', function () {
  T.ACOES.reiniciar(botao('reiniciar'));
  ['mmp', 'preparo'].forEach(function (id) {
    var t = T.estado.textos.filter(function (x) { return x.id === id; })[0];
    T.ACOES['aprovar-texto'](botao('aprovar-texto', { id: id }));
    if (t.estado !== 'aprovado') throw new Error(id + ' ficou ' + t.estado);
    naoTem('Falta Dra. Monielle', painel());
  });
  T.ACOES.fechar();
  T.ACOES.reiniciar(botao('reiniciar'));
});
run('Luiz, 07/10 · consulta: na avaliação, fechou, vai pensar ou não fechou; quem fecha tem a primeira consulta com a Dra. Monielle na série, e a reavaliação não encaminha', function () {
  T.ACOES.reiniciar(botao('reiniciar'));
  T.ACOES['abrir-consulta'](botao('abrir-consulta', { id: 'leticia' }));
  tem('data-valor="fechou"', painel()); tem('data-valor="pensar"', painel()); naoTem('data-valor="medica"', painel()); naoTem('Encaminhar à Dra. Monielle', painel());
  T.ACOES.resultado(botao('resultado', { id: 'leticia', valor: 'fechou' }));
  tem('A Fer lança o pacote e os valores, R$ 2.600, e marca a série: as 6 sessões com a Ana Carolina, a primeira consulta com a Dra. Monielle e a reavaliação com você no fim. Dali em diante, a Dra. Monielle cuida de tudo direto com o paciente.', painel());
  T.ACOES['abrir-consulta'](botao('abrir-consulta', { id: 'jorge' }));
  tem('data-valor="alta"', painel()); naoTem('Passar à Dra. Monielle', painel()); naoTem('data-valor="medica"', painel());
  T.ACOES.fechar();
  T.ACOES.reiniciar(botao('reiniciar'));
});
run('Luiz, 07/10 · marketing: a doutora decide cada peça (vai, não vai, falta foto, outra versão); o que volta, a Fer termina e manda de novo', function () {
  T.ACOES.reiniciar(botao('reiniciar'));
  var a = T.estado.pecas.filter(function (x) { return x.estado === 'aprovar'; });
  if (a.length < 2) throw new Error('peças para decidir: ' + a.length);
  T.ACOES['abrir-peca'](botao('abrir-peca', { id: a[0].id }));
  tem('Vai</button>', painel()); tem('Não vai</button>', painel()); tem('Falta foto</button>', painel()); tem('Outra versão', painel()); tem('A devolutiva vai para a Fer', painel());
  T.ACOES['ajuste-peca'](botao('ajuste-peca', { id: a[0].id, pedido: 'Falta foto' }));
  igual(a[0].estado, 'ajuste', 'voltou para a Fer'); igual(a[0].pedido, 'Falta foto', 'falta foto');
  T.ACOES['abrir-peca'](botao('abrir-peca', { id: a[0].id }));
  tem('Voltou para a Fer: falta foto', painel()); tem('Simular: a Fer terminou e mandou de novo', painel());
  T.ACOES['fer-mandou-peca'](botao('fer-mandou-peca', { id: a[0].id }));
  igual(a[0].estado, 'aprovar', 'voltou para a doutora decidir de novo');
  T.ACOES['nao-vai-peca'](botao('nao-vai-peca', { id: a[1].id }));
  igual(a[1].estado, 'fora', 'não vai');
  T.ACOES.fechar();
  T.ACOES.reiniciar(botao('reiniciar'));
});
run('Luiz, 07/10 · passadinha na sala: sessões 1, 3 e a última com a Ana Carolina; sem obrigação, com o aviso de consulta no mesmo horário', function () {
  T.ACOES.reiniciar(botao('reiniciar'));
  ir('hoje'); T.ACOES['aba-lado'](botao('aba-lado', { aba: 'passadinha' }));
  var h = __els.tela.innerHTML;
  var i = h.indexOf('id="h-passadinha"');
  if (i < 0) throw new Error('sem a passadinha');
  var sec = h.slice(i, h.indexOf('</section>', i));
  ['sem obrigação', 'Caio Ferraz', 'Sessão 3 de 6', 'Otávio Reis', 'Sessão 1 de 6', 'Roberto Albuquerque', 'Sessão 6 de 6', 'Nesse horário você está com Jorge Almeida: se der, no fim.', 'A última sessão, antes da reavaliação', 'Não é obrigação'].forEach(function (t) { tem(t, sec); });
  naoTem('Sessão 4 de 6', sec); naoTem('Sessão 2 de 6', sec); naoTem('Manutenção', sec);
  T.ACOES.passei(botao('passei', { id: 'roberto' }));
  igual(T.estado.passadinhas.roberto, '09:20', 'passou'); tem('Passou às 09:20');
  T.ACOES.reiniciar(botao('reiniciar'));
});
run('Luiz, 07/10 · horário livre: a doutora liga para quem avaliou e não fechou, com o motivo e o roteiro, e registra como foi', function () {
  T.ACOES.reiniciar(botao('reiniciar'));
  ir('hoje'); grupo('crescimento');
  tem('Livre das 15h às 18h.'); tem('Que tal ligar para quem avaliou e não fechou?'); tem('Diego Andrade'); tem('motivo: valor'); tem('Carla Mendes'); tem('motivo: decidir em casa');
  T.ACOES['ligar-pensar'](botao('ligar-pensar', { id: 'diego' }));
  tem('Roteiro curto', painel()); tem('explique as formas de pagamento, sem desconto', painel()); tem('Quer começar', painel()); tem('Sem desconto e sem pressão', painel());
  T.ACOES['resultado-pensar'](botao('resultado-pensar', { id: 'diego', valor: 'comecar' }));
  igual(T.estado.ligacoesDoutora.diego, 'comecar', 'registrou');
  if (!T.estado.log.some(function (l) { return /A doutora ligou para Diego Andrade: quer começar\. A Fer marca/.test(l[1]); })) throw new Error('registro da ligação');
  T.ACOES['ligar-pensar'](botao('ligar-pensar', { id: 'carla' }));
  tem('pergunte se quer que a Fer mande de novo o resumo do plano', painel());
  T.ACOES.fechar();
  T.ACOES.reiniciar(botao('reiniciar'));
});
run('Luiz, 07/10 · gestão: a agenda da Dra. Monielle, a semana anterior e a próxima, sem contagens para ela', function () {
  T.ACOES.reiniciar(botao('reiniciar'));
  var m = T.agendaMonielle();
  // Na semana anterior, só se teve ou não a consulta (Luiz, 07/10/2026).
  igual(m.ant.map(function (x) { return x.p.nome + ':' + x.e.st; }).join(', '), 'Odete Prado:feito', 'semana anterior'); igual(m.prox.map(function (x) { return x.p.nome; }).join(', '), 'Odete Prado, Lia Fagundes', 'próxima semana');
  ir('gestao');
  var c = trecho(__els.tela.innerHTML, 'id="h-moni"', '</section>');
  tem('Agenda da Dra. Monielle', c); tem('Semana anterior', c); tem('Teve consulta', c); tem('Próxima semana', c);
  tem('Odete Prado', c); tem('Hoje, 15:00', c); tem('Lia Fagundes', c); tem('Consulta médica', c); tem('A Dra. Monielle, que pediu o retorno direto à Fer', c);
  tem('Só para a gestão: se teve ou não a consulta.', c); tem('a consulta não entra na receita da clínica', c); naoTem('R$', c);
});
run('Luiz, 07/10 · a parceira manda o contato direto para a clínica: sem QR Code e sem "Vim pela"', function () {
  T.ACOES.reiniciar(botao('reiniciar'));
  Object.keys(T.TIPOS).forEach(function (k) {
    var t = T.TIPOS[k];
    if (/código|QR|display/.test(t.kit)) throw new Error('kit com código: ' + k);
    if (/código (da|do)|QR|display/.test(t.apresentar.join(' '))) throw new Error('conversa com código: ' + k);
  });
  ['parceiras', 'visitas', 'marketing', 'hoje', 'gestao'].forEach(function (tela) { ir(tela); naoTem('Vim pela'); naoTem('Código QR'); });
  T.ACOES['abrir-parceira'](botao('abrir-parceira', { id: 'vintage' }));
  tem('pede a autorização do cliente e manda o contato dele para o WhatsApp da clínica', painel()); naoTem('VINTAGE41', painel());
  T.ACOES.fechar();
  T.ACOES['abrir-parceira'](botao('abrir-parceira', { id: 'porto' }));
  tem('Kit para levar', painel()); naoTem('Código que vai no kit', painel()); naoTem('PORTO', painel());
  T.ACOES.fechar();
  T.ACOES.texto(botao('texto', { tipo: 'treinamento', id: 'navalha' }));
  tem('pedir a autorização do cliente e mandar o contato dele para o WhatsApp da clínica', painel());
  T.ACOES.fechar();
  T.ACOES.texto(botao('texto', { tipo: 'barbeiro', id: 'henriquesalles' }));
  tem('passar o seu contato para a clínica', painel()); naoTem('código', painel());
  T.ACOES.fechar();
  ir('visitas'); naoTem('código da parceira');
});
run('Luiz, 07/10 · sem ligação depois da sessão: nada na lista da Fer nem no resumo da consulta', function () {
  T.ACOES.reiniciar(botao('reiniciar'));
  T.estado.pacientes.forEach(function (p) { (p.tarefas || []).concat((p.hist || []).map(function (h) { return h[2]; })).forEach(function (t) { if (/24 a 48 horas|pós-MMP|igação depois/.test(t)) throw new Error('ligação depois da sessão em ' + p.id + ': ' + t); }); });
  T.ACOES['abrir-consulta'](botao('abrir-consulta', { id: 'jorge' }));
  naoTem('igação depois', painel()); tem('Recebeu os cuidados depois da sessão por mensagem', painel());
  T.ACOES.fechar();
});

run('Luiz, 08/10 · gestão: os gastos da API do Claude no mês, contra o teto', function () {
  T.ACOES.reiniciar(botao('reiniciar'));
  ir('gestao');
  tem('Gastos com o Claude (API)'); tem('US$ 1,90'); tem('de um teto de US$ 15,00 por mês'); tem('Marketing'); tem('Conversa com o copiloto');
  tem('no teto, a IA para e o sistema segue com os textos fixos');
});
run('padrão 09/10 · recomeçar volta ao Hoje; o "Esperando você" é o número do menu; a linha de Crescimento bate com a tela', function () {
  ir('gestao');
  T.ACOES.reiniciar(botao('reiniciar'));
  igual(location.hash, '#hoje', 'recomeçar volta ao Hoje'); tem('Bom dia, doutora.');
  tem('<strong class="dia-kpi-v num">' + T.nEsperando() + '</strong>'); tem('decisões e oportunidades');
  var m = /data-gid="crescimento"><span class="lg-qtd num" title="[^"]*">(\d+)</.exec(__els.tela.innerHTML);
  if (!m) throw new Error('sem a linha de crescimento');
  grupo('crescimento');
  var h = __els.tela.innerHTML;
  var ligar = (h.match(/data-acao="ligar-pensar"/g) || []).length;
  var agradecer = (h.match(/data-acao="agradecer-paciente"/g) || []).length;
  var apostas = T.achados().filter(function (a) { return a.lugar === 'apostas' && !T.estado.achados[a.id]; }).length;
  igual(Number(m[1]), ligar + agradecer + apostas, 'a linha e a tela do grupo');
  tem('<strong class="num">' + m[1] + '</strong> esperando você');
  grupo('');
});
// Sem integração com o WhatsApp (Luiz, 15/10/2026): quem manda copia o texto daqui e cola no WhatsApp. Copiar não marca como enviado.
function sinc(v) { return { then: function (a) { return sinc(a ? a(v) : v); } }; }
function negado() { return { then: function (a, b) { return sinc(b ? b() : undefined); } }; }
function alvoCopiar(h) { var m = /data-acao="copiar-msg" data-alvo="([^"]+)"/.exec(h); if (!m) throw new Error('sem o botão Copiar'); return m[1]; }
run('Luiz, 15/10 · WhatsApp sem integração: sem wa.me nem Abrir no WhatsApp, e o Copiar em cada mensagem', function () {
  naoTem('wa.me', __FONTE); naoTem('Abrir no WhatsApp', __FONTE); naoTem('abre o WhatsApp', __FONTE); naoTem('whats-pac', __FONTE);
  T.ACOES.reiniciar(botao('reiniciar'));
  [['agradecimento', 'ze'], ['agradecimento', 'navalha'], ['extrato', 'navalha'], ['barbeiro', 'navalha'], ['barbeiro', 'bella']].forEach(function (x) {
    T.ACOES.texto(botao('texto', { tipo: x[0], id: x[1] }));
    tem('data-acao="copiar-msg"', painel()); tem('<span>Copiar</span>', painel()); tem('copie o texto e cole na conversa', painel()); naoTem('Copiar o texto', painel());
    if (!T.textoDe(x[0], T.parceira(x[1])).whats) throw new Error(x.join(' ') + ': devia ser mensagem de WhatsApp');
  });
  // O que não vai pelo WhatsApp continua como estava: o treinamento é roteiro de conversa, e o post é legenda do Instagram.
  [['treinamento', 'navalha'], ['post', 'navalha']].forEach(function (x) {
    T.ACOES.texto(botao('texto', { tipo: x[0], id: x[1] }));
    tem('Copiar o texto', painel()); naoTem('copiar-msg', painel());
  });
  T.ACOES.fechar();
  // O retorno do antes e agora, na consulta da reavaliação.
  T.ACOES['abrir-consulta'](botao('abrir-consulta', { id: 'jorge' }));
  T.ACOES.resultado(botao('resultado', { id: 'jorge', valor: 'alta' }));
  igual(alvoCopiar(painel()), 'retorno-jorge', 'o Copiar do retorno');
  T.ACOES.fechar();
  // O resumo do plano de quem vai pensar.
  T.ACOES['abrir-consulta'](botao('abrir-consulta', { id: 'leticia' }));
  T.ACOES.resultado(botao('resultado', { id: 'leticia', valor: 'pensar' }));
  T.ACOES['motivo-pensar'](botao('motivo-pensar', { id: 'leticia', motivo: 'casa' }));
  igual(alvoCopiar(painel()), 'plano-leticia', 'o Copiar do resumo do plano'); naoTem('Copiar o resumo', painel());
  T.ACOES.fechar();
  // O agradecimento escrito na conversa com o copiloto.
  T.ACOES['chat-sugestao'](botao('chat-sugestao', { pergunta: 'Escreve um agradecimento para a Navalha de Ouro' })); __flush();
  tem('data-acao="copiar-msg"', ultimaMsg());
  // A legenda do Instagram não é WhatsApp: continua "Copiar a legenda".
  tem("Copiar a legenda</button>", __FONTE);
});
run('Luiz, 15/10 · Copiar: copia o texto exato, vira Copiado por 2 segundos e não marca nada como enviado', function () {
  T.ACOES.reiniciar(botao('reiniciar'));
  var original = navigator.clipboard.writeText, copiado = null;
  navigator.clipboard.writeText = function (x) { copiado = x; return sinc(); };
  try {
    T.ACOES.texto(botao('texto', { tipo: 'agradecimento', id: 'ze' }));
    var id = alvoCopiar(painel());
    var texto = T.textoDe('agradecimento', T.parceira('ze')).texto;
    getEl(id).textContent = texto;
    var antes = JSON.stringify(T.estado);
    var b = botao('copiar-msg', { alvo: id }); b.innerHTML = '<span>Copiar</span>';
    T.ACOES['copiar-msg'](b);
    igual(copiado, texto, 'copiou exatamente a mensagem');
    tem('Copiado', b.innerHTML); if (!b.classList.contains('copiado')) throw new Error('sem o estado copiado');
    tem('Copiado: cole no WhatsApp.', __els.toast.innerHTML);
    igual(JSON.stringify(T.estado), antes, 'copiar não registra envio nem muda nada');
    __flush();
    igual(b.innerHTML, '<span>Copiar</span>', 'volta a Copiar depois de 2 segundos'); if (b.classList.contains('copiado')) throw new Error('ficou copiado');
    // Plano B: a área de transferência negada seleciona o texto e avisa.
    navigator.clipboard.writeText = function () { return negado(); };
    var b2 = botao('copiar-msg', { alvo: id }); b2.innerHTML = '<span>Copiar</span>';
    T.ACOES['copiar-msg'](b2);
    tem('Não deu para copiar sozinho: o texto está selecionado, use Ctrl+C', __els.toast.innerHTML); naoTem('Copiado', b2.innerHTML);
    navigator.clipboard.writeText = function () { throw new Error('bloqueado'); };
    T.ACOES['copiar-msg'](b2);
    tem('use Ctrl+C', __els.toast.innerHTML);
  } finally { navigator.clipboard.writeText = original; T.ACOES.fechar(); }
});

'''

ctx = quickjs.Context()
ctx.eval(SHIM)
ctx.eval(script)
ctx.eval('globalThis.__FONTE = ' + json.dumps(html) + ';')
ctx.eval(OUTROS)
ctx.eval(TESTES)
while ctx.execute_pending_job():
    pass
print(ctx.eval('__out.join("\\n")'))
