import http from 'node:http';
import { spawn } from 'node:child_process';
import { mkdir, writeFile, readFile, writeFile as writeFileProm } from 'node:fs/promises';
import path from 'node:path';
import { randomUUID } from 'node:crypto';
import { tmpdir } from 'node:os';
import { isChromeDebugReachable, launchChromeDebug, cdpHttpGet } from '../services/chrome.js';
import { appendUsageRecord } from '../services/usage.js';
import { toSlug } from '../services/specs.js';
import { jobs, authSignals } from './job-store.js';
import { CLAUDE_BIN } from './job-runner.js';

// Tests Chrome CDP directly (no Claude subprocess) — reliable sanity check
export function startChromeDirectTest() {
  const jobId = randomUUID();
  const job = { chunks: [], done: false, code: null, listeners: new Set() };
  jobs.set(jobId, job);

  const emit = (event) => {
    job.chunks.push(event);
    for (const fn of job.listeners) fn(event);
  };

  const finish = (code) => {
    job.done = true; job.code = code;
    emit({ type: 'done', code });
    job.listeners.clear();
    setTimeout(() => jobs.delete(jobId), 5 * 60_000);
  };

  const _cdpHttpGet = (path) => new Promise((resolve) => {
    const req = http.get(`http://localhost:9222${path}`, (res) => {
      const chunks = [];
      res.on('data', c => chunks.push(c));
      res.on('end', () => { try { resolve(JSON.parse(Buffer.concat(chunks).toString())); } catch { resolve(null); } });
    });
    req.on('error', () => resolve(null));
    req.setTimeout(3000, () => { req.destroy(); resolve(null); });
  });

  (async () => {
    const sleep = (ms) => new Promise(r => setTimeout(r, ms));

    if (!(await isChromeDebugReachable())) {
      emit({ type: 'data', text: 'Iniciando Chrome com depuração remota...\n' });
      await launchChromeDebug();
      let ready = false;
      for (let i = 0; i < 30; i++) {
        await sleep(1000);
        if (await isChromeDebugReachable()) { ready = true; break; }
      }
      if (!ready) { emit({ type: 'data', text: '❌ FALHA: Chrome não ficou disponível na porta 9222\n' }); finish(1); return; }
    }

    const version = await _cdpHttpGet('/json/version');
    if (!version?.webSocketDebuggerUrl) { emit({ type: 'data', text: '❌ FALHA: Chrome não retornou WebSocket URL\n' }); finish(1); return; }

    emit({ type: 'data', text: `${version.Browser || 'Chrome'} detectado — porta 9222 OK\n` });

    const ws = new WebSocket(version.webSocketDebuggerUrl);
    let msgId = 0;
    const pending = new Map();
    const listeners = new Map();

    ws.onmessage = (e) => {
      let msg; try { msg = JSON.parse(e.data); } catch { return; }
      if (msg.id !== undefined && pending.has(msg.id)) {
        const { resolve, reject } = pending.get(msg.id);
        pending.delete(msg.id);
        msg.error ? reject(new Error(msg.error.message)) : resolve(msg.result || {});
      }
      if (msg.method) {
        const key = `${msg.sessionId || ''}:${msg.method}`;
        (listeners.get(key) || new Set()).forEach(h => h(msg.params || {}));
      }
    };

    await new Promise((resolve, reject) => {
      ws.onopen = resolve;
      ws.onerror = (e) => reject(new Error('WS erro: ' + e.message));
      setTimeout(() => reject(new Error('WS timeout')), 5000);
    });

    const cdp = (method, params = {}, sid = null) => new Promise((resolve, reject) => {
      const id = ++msgId;
      pending.set(id, { resolve, reject });
      ws.send(JSON.stringify({ id, method, params, ...(sid ? { sessionId: sid } : {}) }));
      setTimeout(() => { if (pending.has(id)) { pending.delete(id); reject(new Error(`CDP timeout: ${method}`)); } }, 12000);
    });

    const once = (method, sid) => new Promise(resolve => {
      const key = `${sid || ''}:${method}`;
      if (!listeners.has(key)) listeners.set(key, new Set());
      const h = (p) => { listeners.get(key).delete(h); resolve(p); };
      listeners.get(key).add(h);
    });

    const evalJs = (expr, sid) => cdp('Runtime.evaluate', { expression: expr, returnByValue: true, awaitPromise: false }, sid);

    const injectCursor = async (sid) => {
      await evalJs(`(function(){
        if(document.getElementById('__sdd_cur__'))return;
        var d=document.createElement('div');
        d.id='__sdd_cur__';
        d.style.cssText='position:fixed;left:400px;top:300px;pointer-events:none;z-index:2147483647;filter:drop-shadow(1px 1px 2px rgba(0,0,0,.5));';
        d.innerHTML='<svg width="22" height="26" viewBox="0 0 22 26" xmlns="http://www.w3.org/2000/svg"><path d="M2 2 L2 22 L7 16.5 L10.5 24 L13 23 L9.5 15.5 L17 15.5 Z" fill="white" stroke="#222" stroke-width="1.4" stroke-linejoin="round"/></svg>';
        document.body.appendChild(d);
        window.__sddCur=d;
      })()`, sid).catch(()=>{});
    };

    const navigate = async (url, sid) => {
      const loaded = once('Page.loadEventFired', sid);
      const { errorText } = await cdp('Page.navigate', { url }, sid);
      if (errorText && errorText !== 'net::ERR_ABORTED') {
        throw new Error(`Servidor não acessível em "${url}" — ${errorText}`);
      }
      await Promise.race([loaded, sleep(8000)]);
      await sleep(300);
      await injectCursor(sid);
    };

    let mouseX = 400, mouseY = 300;
    const moveMouse = async (toX, toY, sid, steps = 22, durationMs = 300) => {
      const ease = t => t < 0.5 ? 2*t*t : -1+(4-2*t)*t;
      const fromX = mouseX, fromY = mouseY;
      const interval = durationMs / steps;
      for (let i = 1; i <= steps; i++) {
        const t = ease(i / steps);
        mouseX = Math.round(fromX + (toX - fromX) * t);
        mouseY = Math.round(fromY + (toY - fromY) * t);
        await Promise.all([
          cdp('Input.dispatchMouseEvent', { type: 'mouseMoved', x: mouseX, y: mouseY }, sid),
          evalJs(`window.__sddCur&&(window.__sddCur.style.left='${mouseX}px',window.__sddCur.style.top='${mouseY}px')`, sid).catch(()=>{}),
        ]);
        await sleep(interval);
      }
    };

    const click = async (x, y, sid) => {
      await moveMouse(x, y, sid);
      await evalJs(`window.__sddCur&&(window.__sddCur.style.transform='scale(0.75)')`, sid).catch(()=>{});
      await cdp('Input.dispatchMouseEvent', { type: 'mousePressed', x, y, button: 'left', clickCount: 1 }, sid);
      await sleep(80);
      await cdp('Input.dispatchMouseEvent', { type: 'mouseReleased', x, y, button: 'left', clickCount: 1 }, sid);
      await evalJs(`window.__sddCur&&(window.__sddCur.style.transform='')`, sid).catch(()=>{});
      await sleep(100);
    };

    const typeText = async (text, sid, delayMs = 45) => {
      for (const ch of text) {
        await cdp('Input.dispatchKeyEvent', { type: 'char', text: ch }, sid);
        await sleep(delayMs + Math.random() * 20);
      }
    };

    const getRect = async (selector, sid) => {
      const { result } = await evalJs(
        `(function(){var el=document.querySelector(${JSON.stringify(selector)});if(!el)return null;var r=el.getBoundingClientRect();return{x:r.x,y:r.y,w:r.width,h:r.height};})()`,
        sid
      );
      return result?.value || null;
    };

    const { targetInfos } = await cdp('Target.getTargets', {}).catch(() => ({ targetInfos: [] }));
    const blankTarget = (targetInfos || []).find(t =>
      t.type === 'page' && !t.attached &&
      (t.url === 'about:blank' || t.url === 'chrome://newtab/' || t.url === '')
    );
    let targetId;
    if (blankTarget) {
      targetId = blankTarget.targetId;
      emit({ type: 'data', text: 'Usando aba existente...\n' });
    } else {
      emit({ type: 'data', text: 'Abrindo nova aba...\n' });
      ({ targetId } = await cdp('Target.createTarget', { url: 'about:blank' }));
    }
    const { sessionId: sid } = await cdp('Target.attachToTarget', { targetId, flatten: true });
    await cdp('Page.enable', {}, sid);
    await cdp('Runtime.enable', {}, sid);

    emit({ type: 'data', text: 'Navegando para https://www.google.com...\n' });
    await navigate('https://www.google.com', sid);
    await sleep(500);

    emit({ type: 'data', text: 'Clicando no campo de busca...\n' });
    const searchRect = await getRect('textarea[name="q"], input[name="q"]', sid);
    if (searchRect) {
      await click(Math.round(searchRect.x + searchRect.w / 2), Math.round(searchRect.y + searchRect.h / 2), sid);
    } else {
      await click(640, 360, sid);
    }
    await sleep(200);

    emit({ type: 'data', text: 'Digitando "lorem ipsum"...\n' });
    await typeText('lorem ipsum', sid);
    await sleep(300);

    const searchDone = once('Page.loadEventFired', sid);
    await cdp('Input.dispatchKeyEvent', { type: 'keyDown', key: 'Return', code: 'Enter', windowsVirtualKeyCode: 13 }, sid);
    await cdp('Input.dispatchKeyEvent', { type: 'keyUp', key: 'Return', code: 'Enter', windowsVirtualKeyCode: 13 }, sid);

    emit({ type: 'data', text: 'Aguardando resultados...\n' });
    await Promise.race([searchDone, sleep(8000)]);
    await sleep(600);
    await injectCursor(sid);

    const { result: titleR } = await evalJs('document.title', sid);
    emit({ type: 'data', text: `Resultados: ${titleR?.value || '(carregado)'}\n` });

    emit({ type: 'data', text: 'Procurando link para lipsum.com...\n' });
    const { result: linkInfo } = await evalJs(
      `(function(){
        var ls = document.querySelectorAll('a[href]');
        for (var l of ls) {
          if (l.href && l.href.includes('lipsum.com')) {
            var r = l.getBoundingClientRect();
            return { url: l.href, x: r.x, y: r.y, w: r.width, h: r.height };
          }
        }
        return null;
      })()`,
      sid
    );

    if (!linkInfo?.value) {
      emit({ type: 'data', text: '❌ FALHA: Link para lipsum.com não encontrado nos resultados\n' });
      await cdp('Target.closeTarget', { targetId }); ws.close(); finish(1); return;
    }

    const { url: lipsumUrl, x: lx, y: ly, w: lw, h: lh } = linkInfo.value;
    emit({ type: 'data', text: `Link encontrado: ${lipsumUrl}\n` });

    await evalJs(`document.querySelector('a[href*="lipsum.com"]').scrollIntoView({behavior:'smooth',block:'center'})`, sid);
    await sleep(500);

    const { result: linkRect2 } = await evalJs(
      `(function(){var l=document.querySelector('a[href*="lipsum.com"]');var r=l.getBoundingClientRect();return{x:r.x,y:r.y,w:r.width,h:r.height};})()`,
      sid
    );
    const lr = linkRect2?.value;
    const clickX = lr ? Math.round(lr.x + lr.w / 2) : Math.round(lx + lw / 2);
    const clickY = lr ? Math.round(lr.y + lr.h / 2) : Math.round(ly + lh / 2);

    emit({ type: 'data', text: 'Clicando no link...\n' });
    await click(clickX, clickY, sid);

    emit({ type: 'data', text: 'Aguardando lipsum.com carregar...\n' });
    await Promise.race([once('Page.loadEventFired', sid), sleep(8000)]);
    await sleep(500);

    const { result: urlR } = await evalJs('window.location.href', sid);
    const finalUrl = urlR?.value || '';
    emit({ type: 'data', text: `Página atual: ${finalUrl}\n` });

    await sleep(800);
    await cdp('Target.closeTarget', { targetId });
    ws.close();
    emit({ type: 'data', text: 'Aba de teste fechada.\n\n' });

    if (finalUrl.includes('lipsum.com')) {
      emit({ type: 'data', text: '✅ CONFIGURAÇÃO OK\n' });
      finish(0);
    } else {
      emit({ type: 'data', text: `❌ FALHA: URL final não contém lipsum.com (${finalUrl})\n` });
      finish(1);
    }
  })().catch(err => {
    emit({ type: 'data', text: `Erro inesperado: ${err.message}\n` });
    finish(1);
  });

  return jobId;
}

export function startJobWithChrome(command) {
  const jobId = randomUUID();
  const job = { chunks: [], done: false, code: null, listeners: new Set() };
  jobs.set(jobId, job);

  const emit = (event) => {
    job.chunks.push(event);
    for (const fn of job.listeners) fn(event);
  };

  (async () => {
    if (!(await isChromeDebugReachable())) {
      emit({ type: 'data', text: 'Preparando Chrome para testes (fechando e reabrindo com depuração remota)...\n' });
      await launchChromeDebug();
      let ready = false;
      for (let i = 0; i < 30; i++) {
        await new Promise(r => setTimeout(r, 1000));
        if (await isChromeDebugReachable()) { ready = true; break; }
        if (i === 9) emit({ type: 'data', text: 'Aguardando Chrome ficar pronto...\n' });
      }
      if (ready) {
        emit({ type: 'data', text: 'Chrome pronto. Iniciando teste...\n\n' });
      } else {
        emit({ type: 'data', text: 'Aviso: Chrome pode não estar totalmente pronto. Tentando mesmo assim...\n\n' });
      }
    }

    const mcpCfg = { mcpServers: { 'chrome-devtools': { command: 'npx', args: ['-y', 'chrome-devtools-mcp', '--browserUrl', 'http://127.0.0.1:9222'] } } };
    const mcpCfgFile = path.join(tmpdir(), 'sdd-mcp-config.json');
    await writeFileProm(mcpCfgFile, JSON.stringify(mcpCfg));

    const msg = 'IMPORTANTE: Responda SEMPRE em português do Brasil. Nunca gere conteúdo em inglês.\n\n' + command;
    const proc = spawn(CLAUDE_BIN, [
      '--print', '--dangerously-skip-permissions',
      '--mcp-config', mcpCfgFile,
    ], {
      cwd: process.cwd(),
      stdio: ['pipe', 'pipe', 'pipe'],
    });
    proc.stdin.write(msg);
    proc.stdin.end();

    let _qaOut = '';
    proc.stdout.on('data', d => { const t = d.toString(); _qaOut += t; emit({ type: 'data', text: t }); });
    proc.stderr.on('data', d => emit({ type: 'data', text: d.toString() }));
    proc.on('close', code => {
      appendUsageRecord('qa-robot', msg, _qaOut, process.cwd()).catch(() => {});
      job.done = true; job.code = code;
      emit({ type: 'done', code });
      job.listeners.clear();
      setTimeout(() => jobs.delete(jobId), 5 * 60_000);
    });
    proc.on('error', err => {
      emit({ type: 'data', text: `Erro ao iniciar claude: ${err.message}\nBinário: ${CLAUDE_BIN}\n` });
      emit({ type: 'error', text: err.message });
      job.done = true;
      job.listeners.clear();
    });
  })();

  return jobId;
}

export async function generateScenariosFromDiff(filePaths, cwd) {
  const { execSync: execS, execFileSync } = await import('node:child_process');
  const getBase = () => {
    try { return execS('git merge-base HEAD origin/main', { cwd, encoding: 'utf8', stdio: ['ignore','pipe','ignore'] }).trim(); } catch {}
    try { return execS('git merge-base HEAD main', { cwd, encoding: 'utf8', stdio: ['ignore','pipe','ignore'] }).trim(); } catch {}
    try { return execS('git rev-parse HEAD~1', { cwd, encoding: 'utf8', stdio: ['ignore','pipe','ignore'] }).trim(); } catch {}
    return null;
  };
  const base = getBase();
  const fileContexts = [];
  for (const fp of filePaths) {
    if (typeof fp !== 'string' || /[`$|;&<>{}!()\n\r'"\\]/.test(fp)) continue;
    let content = '';
    if (base) {
      try { content = execFileSync('git', ['diff', base, '--', fp], { cwd, encoding: 'utf8', stdio: ['ignore','pipe','ignore'] }); } catch {}
    }
    if (!content.trim()) {
      try { content = await readFile(path.join(cwd, fp), 'utf8'); } catch {}
    }
    if (content.trim()) {
      const truncated = content.length > 3500 ? content.slice(0, 3500) + '\n...[truncado]' : content;
      fileContexts.push(`### ${fp}\n\`\`\`\n${truncated}\n\`\`\``);
    }
  }
  if (!fileContexts.length) return [{ title: 'Teste geral', criteria: 'Verificar funcionamento da aplicação após as alterações' }];

  return new Promise(resolve => {
    const prompt = `Você é um engenheiro de QA sênior. Analise as seguintes mudanças de código e gere cenários de teste.

## Arquivos alterados

${fileContexts.join('\n\n')}

## Instruções
Com base nas mudanças acima, identifique comportamentos que precisam ser testados:
- Novos fluxos introduzidos
- Validações adicionadas ou removidas
- Casos de borda criados pelas alterações
- Regressões potenciais em fluxos existentes

Gere entre 2 e 8 cenários de teste. Responda APENAS com JSON válido, sem explicações:
[{"title":"<nome do cenário>","criteria":"<critério de aceite em 1-3 linhas>"}]

Foque no comportamento do usuário final, não nos detalhes técnicos do código.`;

    const proc = spawn(CLAUDE_BIN, ['--print', '--dangerously-skip-permissions', '--model', 'claude-sonnet-4-6', prompt], {
      cwd, stdio: ['ignore', 'pipe', 'pipe'],
    });
    let out = '';
    proc.stdout.on('data', d => { out += d.toString(); });
    proc.on('close', () => {
      appendUsageRecord('qa-diff', prompt, out, cwd, 'sonnet').catch(() => {});
      try {
        const match = out.match(/\[[\s\S]*\]/);
        if (match) { resolve(JSON.parse(match[0])); return; }
      } catch {}
      resolve([{ title: 'Teste geral', criteria: 'Verificar funcionamento após as alterações' }]);
    });
    proc.on('error', () => resolve([{ title: 'Teste geral', criteria: 'Verificar funcionamento' }]));
    setTimeout(() => { try { proc.kill(); } catch {} }, 90000);
  });
}

const _projectCtxCache = new Map();
export async function gatherProjectContext(cwd) {
  const now = Date.now();
  const cached = _projectCtxCache.get(cwd);
  if (cached && cached.expiresAt > now) return cached.data;
  const { readFile: rf, readdir } = await import('node:fs/promises');
  const MAX_TOTAL = 6000;
  const MAX_FILE  = 2000;

  let pkgInfo = '';
  try {
    const pkg = JSON.parse(await rf(path.join(cwd, 'package.json'), 'utf8'));
    const deps = { ...pkg.dependencies, ...pkg.devDependencies };
    const libs = [];
    if (deps.next)                                        libs.push('Next.js');
    else if (deps.react)                                  libs.push('React');
    if (deps.vue || deps.nuxt)                            libs.push(deps.nuxt ? 'Nuxt.js' : 'Vue.js');
    if (deps['@angular/core'])                            libs.push('Angular');
    if (deps.svelte)                                      libs.push('Svelte');
    if (deps.tailwindcss)                                 libs.push('Tailwind CSS');
    if (deps['@mui/material'] || deps['@material-ui/core']) libs.push('Material UI');
    if (deps['@chakra-ui/react'])                         libs.push('Chakra UI');
    if (deps['@radix-ui/react-dialog'] || deps['@radix-ui/themes']) libs.push('Radix UI');
    pkgInfo = libs.length ? 'Stack: ' + libs.join(', ') : '';
  } catch {}

  const files = [];
  let totalLen = 0;

  const tryRead = async (relPath, maxLen) => {
    if (totalLen >= MAX_TOTAL) return;
    try {
      const content = await rf(path.join(cwd, relPath), 'utf8');
      if (content.trim().length < 50) return;
      const trunc = content.length > maxLen ? content.slice(0, maxLen) + '\n...[truncado]' : content;
      files.push({ path: relPath, content: trunc });
      totalLen += trunc.length;
    } catch {}
  };

  const cssCandidates = [
    'src/styles/globals.css', 'src/styles/global.css', 'src/styles/variables.css',
    'src/styles/theme.css',   'src/styles/index.css',  'src/index.css',
    'src/App.css',            'styles/globals.css',    'styles/global.css',
    'app/globals.css',        'src/global.css',        'src/main.css',
  ];
  for (const f of cssCandidates) { await tryRead(f, MAX_FILE); if (files.length && totalLen > 1000) break; }

  for (const f of ['tailwind.config.js', 'tailwind.config.ts', 'tailwind.config.cjs']) {
    await tryRead(f, 1500); if (files.some(x => x.path === f)) break;
  }

  const layoutCandidates = [
    'src/app/layout.tsx', 'src/app/layout.jsx', 'app/layout.tsx', 'app/layout.jsx',
    'src/layouts/default.vue', 'src/layouts/Default.vue',
    'src/components/Layout.tsx', 'src/components/Layout.jsx',
    'src/App.tsx', 'src/App.jsx', 'src/App.vue',
  ];
  for (const f of layoutCandidates) { if (totalLen >= MAX_TOTAL) break; await tryRead(f, 1500); if (files.some(x => x.path === f)) break; }

  const compDirs = ['src/components', 'components', 'src/app/components', 'app/components', 'src/ui'];
  let compCount = 0;
  for (const dir of compDirs) {
    if (compCount >= 2 || totalLen >= MAX_TOTAL) break;
    try {
      const entries = await readdir(path.join(cwd, dir), { withFileTypes: true });
      for (const e of entries) {
        if (compCount >= 2 || totalLen >= MAX_TOTAL) break;
        if (e.isFile() && /\.(tsx|jsx|vue|svelte)$/.test(e.name) && !/\.(test|spec)\./.test(e.name)) {
          await tryRead(dir + '/' + e.name, 1200);
          if (files.some(x => x.path === dir + '/' + e.name)) compCount++;
        }
      }
    } catch {}
  }

  const data = { pkgInfo, files };
  _projectCtxCache.set(cwd, { data, expiresAt: Date.now() + 5 * 60 * 1000 });
  return data;
}

export async function generatePrototypeWithClaude(specName, stepId, cwd) {
  const { readFile: rf, writeFile: wf, mkdir: mkd } = await import('node:fs/promises');
  const specDir = path.join(cwd, '.sdd', 'specs', specName);
  const fileMap = {
    requirements: ['requirements.md'],
    design: ['requirements.md', 'design.md'],
    tasks: ['requirements.md', 'design.md', 'tasks.md'],
  };
  const files = fileMap[stepId] || ['requirements.md'];
  const contexts = [];
  const sourceFiles = [];
  for (const f of files) {
    try {
      const content = await rf(path.join(specDir, f), 'utf8');
      if (content.trim().length > 20) {
        sourceFiles.push({ name: f, content });
        const truncated = content.length > 4000 ? content.slice(0, 4000) + '\n...[truncado]' : content;
        contexts.push(`### ${f}\n\`\`\`\n${truncated}\n\`\`\``);
      }
    } catch {}
  }
  if (!contexts.length) throw new Error('Nenhum arquivo de spec encontrado. Execute as etapas anteriores primeiro.');

  const projectCtx = await gatherProjectContext(cwd);
  const hasCtx = projectCtx.files.length > 0;

  const styleSection = hasCtx
    ? `## Estilo visual do projeto — OBRIGATÓRIO replicar
${projectCtx.pkgInfo ? projectCtx.pkgInfo + '\n' : ''}Use os arquivos abaixo como única fonte de verdade visual. Replique EXATAMENTE: cores, tipografia, border-radius, sombras, espaçamentos, padrão de botões, inputs e cards.${projectCtx.files.some(f => f.path.includes('tailwind')) ? '\nO projeto usa Tailwind CSS: aplique as classes utilitárias diretamente no HTML.' : '\nConverta os estilos para CSS inline equivalente no <style>.'}
NÃO use estilos genéricos. NÃO use frameworks externos. O resultado deve parecer pertencer ao mesmo sistema visual.

${projectCtx.files.map(f => `### ${f.path}\n\`\`\`\n${f.content}\n\`\`\``).join('\n\n')}`
    : `## Estilo visual
Use um visual limpo e neutro: fundo #f8f9fa, cards #fff com borda #e5e7eb, accent #4f46e5, tipografia system-ui. Mantenha o protótipo simples — sem inventar identidade visual que não existe nas specs.`;

  return new Promise((resolve, reject) => {
    const prompt = `Você é um desenvolvedor frontend sênior gerando um protótipo de tela para validação com stakeholders.

## Especificações da feature

${contexts.join('\n\n')}

${styleSection}

## REGRAS DE ESCOPO — CRÍTICO
- Prototipe SOMENTE o que está explicitamente descrito nas especificações acima
- NÃO invente fluxos, seções, funcionalidades ou telas não mencionadas
- NÃO adicione navegação global, menus, headers ou footers a menos que estejam nas specs
- Se a spec descreve um formulário, entregue apenas aquele formulário — sem extras

## Implementação
- Use HTML, CSS no <style> e JavaScript vanilla no <script> — tudo inline, sem dependências externas
- Dados fictícios mas realistas
- Inclua apenas os estados descritos nas specs (ex: erro, sucesso, vazio) — não invente outros
- Seja responsivo (max-width: 960px centralizado)
- NÃO use imagens externas, fontes externas nem CDNs

Responda APENAS com o código HTML completo, começando com <!DOCTYPE html> e terminando com </html>.`;

    const proc = spawn(CLAUDE_BIN, ['--print', '--dangerously-skip-permissions', prompt], {
      cwd, stdio: ['ignore', 'pipe', 'pipe'],
    });
    let out = '';
    proc.stdout.on('data', d => { out += d.toString(); });
    proc.on('close', async () => {
      appendUsageRecord('prototype', prompt, out, cwd).catch(() => {});
      const htmlMatch = out.match(/<!DOCTYPE html[\s\S]*<\/html>/i);
      const html = htmlMatch ? htmlMatch[0] : out.trim();
      if (!html || html.length < 200) { reject(new Error('Protótipo não gerado corretamente. Verifique se as specs foram geradas.')); return; }
      try { await mkd(specDir, { recursive: true }); await wf(path.join(specDir, `prototype-${stepId}.html`), html, 'utf8'); } catch {}
      resolve({ html, sourceFiles });
    });
    proc.on('error', (err) => reject(err));
    const t = setTimeout(() => { try { proc.kill(); } catch {} reject(new Error('Timeout ao gerar protótipo')); }, 120000);
    proc.on('close', () => clearTimeout(t));
  });
}

function _buildStepPrompt(scenario, scenarioId, urlList, credentialLines) {
  return `Você é um engenheiro de QA. Converta o critério de aceite abaixo em um plano de teste JSON.

## URLs da aplicação
${urlList}

## Credenciais de teste por ambiente
${credentialLines || 'Nenhuma credencial fornecida. Use valores fictícios como qa@teste.com / Senha@123.'}

## Cenário ${scenarioId}: ${scenario.title}
Critérios de aceite:
${scenario.criteria || scenario.title}

## Instruções
Gere um array de passos para este cenário. Use APENAS estas ações:
- {"action":"navigate","url":"<url>"}
- {"action":"screenshot","label":"<texto>"}
- {"action":"click_text","text":"<texto visível>"}
- {"action":"fill_field","label":"<label/placeholder>","value":"<valor>"}
- {"action":"assert_text","text":"<texto>"}
- {"action":"assert_url","contains":"<trecho>"}
- {"action":"wait_navigation"}
- {"action":"scroll_to","text":"<texto visível>"}
- {"action":"wait","ms":500}
- {"action":"wait_for","text":"<texto>","timeout":8000}
- {"action":"wait_for","selector":"<css>","timeout":8000}
- {"action":"assert_visible","selector":"<css>"}
- {"action":"select_option","selector":"<css>","value":"<opção>"}

REGRAS: Nunca use wait_ms para carregamento de página/API. Use wait_navigation após clicks de navegação. Em SPAs use wait_for antes de assert/click.

Responda APENAS com JSON: [{"scenarioId":${scenarioId},"title":"...","steps":[...]}]`;
}

function _generateStepsForOne(scenario, scenarioId, urlList, credentialLines) {
  return new Promise((resolve) => {
    const fallback = { scenarioId, title: scenario.title, steps: [{ action: 'navigate', url: urlList.split('\n')[0]?.replace(/^- \[.*?\] /, '') || 'about:blank' }, { action: 'screenshot', label: 'inicial' }] };
    const proc = spawn(CLAUDE_BIN, ['--print', '--dangerously-skip-permissions', '--model', 'claude-sonnet-4-6', _buildStepPrompt(scenario, scenarioId, urlList, credentialLines)], {
      cwd: process.cwd(), stdio: ['ignore', 'pipe', 'pipe'],
    });
    let out = '';
    proc.stdout.on('data', d => { out += d.toString(); });
    proc.on('close', () => {
      appendUsageRecord('qa-steps', _buildStepPrompt(scenario, scenarioId, urlList, credentialLines), out, process.cwd(), 'sonnet').catch(() => {});
      if (_timedOut) { resolve(fallback); return; }
      try {
        const m = out.match(/\[[\s\S]*\]/);
        if (m) { const arr = JSON.parse(m[0]); resolve(arr[0] || fallback); return; }
      } catch {}
      resolve(fallback);
    });
    proc.on('error', () => resolve(fallback));
    let _timedOut = false;
    const _killTimer = setTimeout(() => { _timedOut = true; try { proc.kill(); } catch {} }, 90000);
    proc.on('close', () => clearTimeout(_killTimer));
  });
}

export async function generateTestStepsWithClaude(scenarios, urls, credentials = []) {
  const urlList = urls.map(u => `- [${u.env}] ${u.url}`).join('\n');
  const credentialLines = credentials.map(c => `- [${c.env}] login: ${c.username} / senha: ${c.password}`).join('\n');
  const CONCURRENCY = 3;
  const results = new Array(scenarios.length);
  let next = 0;
  await Promise.all(Array.from({ length: Math.min(CONCURRENCY, scenarios.length) }, async () => {
    while (next < scenarios.length) {
      const i = next++;
      results[i] = await _generateStepsForOne(scenarios[i], scenarios[i].scenarioId ?? i, urlList, credentialLines);
    }
  }));
  return results;
}

async function captureAndSaveScreenshot(cdp, sid, scenarioId, label, evidenceDir, emit) {
  try {
    const { data } = await cdp('Page.captureScreenshot', { format: 'png' }, sid);
    if (!data) { emit({ type: 'scenario-log', scenarioId, text: `  [AVISO] screenshot "${label}" retornou vazia\n` }); return; }
    const safeName = label.replace(/[^a-z0-9\-_]/gi, '-').toLowerCase();
    const filename = `${String(scenarioId).padStart(2, '0')}-${safeName}.png`;
    const fullPath = path.join(evidenceDir, filename);
    await writeFile(fullPath, Buffer.from(data, 'base64'));
    emit({ type: 'scenario-screenshot', scenarioId, label, dataUrl: 'data:image/png;base64,' + data, path: fullPath });
  } catch (err) {
    emit({ type: 'scenario-log', scenarioId, text: `  [AVISO] falha ao capturar screenshot "${label}": ${err.message}\n` });
  }
}

function buildTabHelpers(cdp, once, sid, emit, scenarioId, evidenceDir) {
  const sleep = (ms) => new Promise(r => setTimeout(r, ms));
  let mouseX = 400, mouseY = 300;

  const evalJs = (expr) => cdp('Runtime.evaluate', { expression: expr, returnByValue: true, awaitPromise: false }, sid);

  const injectCursor = async () => {
    await evalJs(`(function(){
      if(document.getElementById('__sdd_cur__'))return;
      var d=document.createElement('div');d.id='__sdd_cur__';
      d.style.cssText='position:fixed;left:400px;top:300px;pointer-events:none;z-index:2147483647;filter:drop-shadow(1px 1px 2px rgba(0,0,0,.5));';
      d.innerHTML='<svg width="22" height="26" viewBox="0 0 22 26" xmlns="http://www.w3.org/2000/svg"><path d="M2 2 L2 22 L7 16.5 L10.5 24 L13 23 L9.5 15.5 L17 15.5 Z" fill="white" stroke="#222" stroke-width="1.4" stroke-linejoin="round"/></svg>';
      document.body.appendChild(d);window.__sddCur=d;
    })()`).catch(() => {});
  };

  const navigate = async (url) => {
    const loaded = once('Page.loadEventFired', sid);
    const { errorText } = await cdp('Page.navigate', { url }, sid);
    if (errorText && errorText !== 'net::ERR_ABORTED') {
      throw new Error(`Servidor não acessível em "${url}" — ${errorText}. Verifique se o servidor está rodando.`);
    }
    await Promise.race([loaded, sleep(10000)]);
    const { result: rsUrl } = await evalJs('location.href').catch(() => ({ result: null }));
    const currentUrl = rsUrl?.value || '';
    if (currentUrl.startsWith('chrome-error://')) {
      throw new Error(`Servidor não acessível em "${url}". Verifique se o servidor está rodando.`);
    }
    const SSO_DOMAINS = ['login.microsoftonline.com', 'login.live.com', 'accounts.google.com', 'okta.com', 'auth0.com', 'sso.', '/oauth', '/saml', '/auth/'];
    if (SSO_DOMAINS.some(d => currentUrl.includes(d)) && !url.includes(new URL(currentUrl).hostname)) {
      throw new Error(`Autenticação necessária — a página redirecionou para login (${new URL(currentUrl).hostname}). Faça login no Chrome antes de executar os testes, ou use uma URL pública.`);
    }
    await sleep(400);
    await injectCursor();
  };

  const moveMouse = async (toX, toY, steps = 18, durationMs = 260) => {
    const ease = t => t < 0.5 ? 2*t*t : -1+(4-2*t)*t;
    const fromX = mouseX, fromY = mouseY;
    const interval = durationMs / steps;
    for (let i = 1; i <= steps; i++) {
      const t = ease(i / steps);
      mouseX = Math.round(fromX + (toX - fromX) * t);
      mouseY = Math.round(fromY + (toY - fromY) * t);
      await Promise.all([
        cdp('Input.dispatchMouseEvent', { type: 'mouseMoved', x: mouseX, y: mouseY }, sid),
        evalJs(`window.__sddCur&&(window.__sddCur.style.left='${mouseX}px',window.__sddCur.style.top='${mouseY}px')`).catch(() => {}),
      ]);
      await sleep(interval);
    }
  };

  const click = async (x, y) => {
    await moveMouse(x, y);
    await evalJs(`window.__sddCur&&(window.__sddCur.style.transform='scale(0.75)')`).catch(() => {});
    await cdp('Input.dispatchMouseEvent', { type: 'mousePressed', x, y, button: 'left', clickCount: 1 }, sid);
    await sleep(70);
    await cdp('Input.dispatchMouseEvent', { type: 'mouseReleased', x, y, button: 'left', clickCount: 1 }, sid);
    await evalJs(`window.__sddCur&&(window.__sddCur.style.transform='')`).catch(() => {});
    await sleep(100);
  };

  const typeText = async (text, delayMs = 45) => {
    for (const ch of text) {
      await cdp('Input.dispatchKeyEvent', { type: 'char', text: ch }, sid);
      await sleep(delayMs + Math.random() * 15);
    }
  };

  const findByText = async (text) => {
    const { result } = await evalJs(`(function(){
      var t=${JSON.stringify(text)};
      var els=Array.from(document.querySelectorAll('button,a,[role="button"],input[type="submit"],input[type="button"],[onclick]'));
      for(var e of els){if(e.offsetParent!==null&&e.innerText&&e.innerText.trim().includes(t)){var r=e.getBoundingClientRect();if(r.width>0&&r.height>0)return{x:r.x,y:r.y,w:r.width,h:r.height};}}
      var walker=document.createTreeWalker(document.body,NodeFilter.SHOW_TEXT);var node;
      while(node=walker.nextNode()){if(node.textContent.includes(t)){var p=node.parentElement;if(p&&p.offsetParent!==null){var r=p.getBoundingClientRect();if(r.width>0&&r.height>0)return{x:r.x,y:r.y,w:r.width,h:r.height};}}}
      var byAttr=Array.from(document.querySelectorAll('[aria-label],[title],[alt]'));
      for(var e of byAttr){var v=e.getAttribute('aria-label')||e.getAttribute('title')||e.getAttribute('alt')||'';if(v.includes(t)){var r=e.getBoundingClientRect();if(r.width>0&&r.height>0)return{x:r.x,y:r.y,w:r.width,h:r.height};}}
      return null;
    })()`);
    return result?.value || null;
  };

  const findField = async (label) => {
    const { result } = await evalJs(`(function(){
      var l=${JSON.stringify(label)};
      var labels=document.querySelectorAll('label');
      for(var lbl of labels){if(lbl.innerText&&lbl.innerText.includes(l)){var inp=lbl.control||document.getElementById(lbl.htmlFor)||lbl.querySelector('input,textarea,select');if(inp){var r=inp.getBoundingClientRect();if(r.width>0)return{x:r.x,y:r.y,w:r.width,h:r.height};}}}
      var inputs=document.querySelectorAll('input[placeholder],textarea[placeholder]');
      for(var inp of inputs){if(inp.placeholder&&inp.placeholder.includes(l)){var r=inp.getBoundingClientRect();if(r.width>0)return{x:r.x,y:r.y,w:r.width,h:r.height};}}
      return null;
    })()`);
    return result?.value || null;
  };

  const screenshot = (label) => captureAndSaveScreenshot(cdp, sid, scenarioId, label, evidenceDir, emit);

  return { sleep, evalJs, navigate, moveMouse, click, typeText, findByText, findField, screenshot, injectCursor, once };
}

async function executeStep(step, helpers, cdp, sid, emit, scenarioId) {
  const { sleep, evalJs, navigate, click, typeText, findByText, findField, screenshot, moveMouse, injectCursor, once } = helpers;

  switch (step.action) {
    case 'navigate': {
      emit({ type: 'scenario-log', scenarioId, text: `  [NAVEGANDO] ${step.url}\n` });
      await navigate(step.url);
      break;
    }
    case 'screenshot': {
      emit({ type: 'scenario-log', scenarioId, text: `  [SCREENSHOT] ${step.label}\n` });
      await screenshot(step.label);
      break;
    }
    case 'click_text': {
      emit({ type: 'scenario-log', scenarioId, text: `  [CLICANDO] "${step.text}"\n` });
      const rect = await findByText(step.text);
      if (!rect) throw new Error(`Elemento com texto "${step.text}" não encontrado`);
      await click(Math.round(rect.x + rect.w / 2), Math.round(rect.y + rect.h / 2));
      break;
    }
    case 'fill_field': {
      emit({ type: 'scenario-log', scenarioId, text: `  [PREENCHENDO] "${step.label}" = "${step.value}"\n` });
      const rect = await findField(step.label);
      if (!rect) throw new Error(`Campo "${step.label}" não encontrado`);
      await click(Math.round(rect.x + rect.w / 2), Math.round(rect.y + rect.h / 2));
      await sleep(100);
      await cdp('Input.dispatchKeyEvent', { type: 'keyDown', key: 'a', code: 'KeyA', modifiers: 2 }, sid);
      await cdp('Input.dispatchKeyEvent', { type: 'keyUp', key: 'a', code: 'KeyA', modifiers: 0 }, sid);
      await sleep(50);
      await typeText(step.value);
      break;
    }
    case 'assert_text': {
      emit({ type: 'scenario-log', scenarioId, text: `  [VERIFICANDO] texto "${step.text}"\n` });
      const { result } = await evalJs(`(function(){
        var t=${JSON.stringify(step.text)};
        return document.body.innerText.includes(t)||
          Array.from(document.querySelectorAll('[aria-label],[title]'))
            .some(function(e){return(e.getAttribute('aria-label')||'').includes(t)||(e.getAttribute('title')||'').includes(t);});
      })()`);
      if (!result?.value) throw new Error(`Texto "${step.text}" não encontrado na página`);
      emit({ type: 'scenario-log', scenarioId, text: `  [OK] texto encontrado\n` });
      break;
    }
    case 'assert_url': {
      emit({ type: 'scenario-log', scenarioId, text: `  [VERIFICANDO] URL contém "${step.contains}"\n` });
      const { result } = await evalJs(`window.location.href`);
      const url = result?.value || '';
      if (!url.includes(step.contains)) throw new Error(`URL atual "${url}" não contém "${step.contains}"`);
      emit({ type: 'scenario-log', scenarioId, text: `  [OK] URL: ${url}\n` });
      break;
    }
    case 'wait_navigation': {
      emit({ type: 'scenario-log', scenarioId, text: `  [AGUARDANDO] navegação completar...\n` });
      const { result: rs } = await evalJs('document.readyState').catch(() => ({ result: null }));
      if (rs?.value !== 'complete') {
        const loaded = once('Page.loadEventFired');
        await Promise.race([loaded, sleep(10000)]);
      }
      await sleep(200);
      await injectCursor();
      break;
    }
    case 'scroll_to': {
      emit({ type: 'scenario-log', scenarioId, text: `  [ROLANDO] até "${step.text}"\n` });
      const rect = await findByText(step.text);
      if (rect) {
        await evalJs(`window.scrollTo({top:${Math.max(0, rect.y - 100)},behavior:'smooth'})`);
        await sleep(600);
      }
      break;
    }
    case 'wait': {
      const ms = step.ms || 1000;
      emit({ type: 'scenario-log', scenarioId, text: `  [AGUARDANDO] ${ms}ms\n` });
      await sleep(ms);
      break;
    }
    case 'wait_for': {
      const timeout = step.timeout || 10000;
      const desc = step.selector || step.text || '?';
      emit({ type: 'scenario-log', scenarioId, text: `  [AGUARDANDO] "${desc}" aparecer (max ${timeout}ms)\n` });
      const start = Date.now();
      let found = false;
      while (Date.now() - start < timeout) {
        const { result } = step.selector
          ? await evalJs(`!!document.querySelector(${JSON.stringify(step.selector)})`)
          : await evalJs(`document.body.innerText.includes(${JSON.stringify(step.text || '')})`);
        if (result?.value) { found = true; break; }
        await sleep(500);
      }
      if (!found) emit({ type: 'scenario-log', scenarioId, text: `  [AVISO] "${desc}" não apareceu em ${timeout}ms\n` });
      break;
    }
    case 'assert_visible': {
      emit({ type: 'scenario-log', scenarioId, text: `  [VERIFICANDO] visibilidade de "${step.selector}"\n` });
      const { result } = await evalJs(`(function(){
        var el=document.querySelector(${JSON.stringify(step.selector)});
        if(!el) return false;
        var s=window.getComputedStyle(el);
        return s.display!=='none'&&s.visibility!=='hidden'&&el.offsetParent!==null;
      })()`);
      if (!result?.value) throw new Error(`Elemento "${step.selector}" não está visível`);
      emit({ type: 'scenario-log', scenarioId, text: `  [OK] elemento visível\n` });
      break;
    }
    case 'select_option': {
      emit({ type: 'scenario-log', scenarioId, text: `  [SELECIONANDO] "${step.value}" em "${step.selector}"\n` });
      await evalJs(`(function(){
        var sel=document.querySelector(${JSON.stringify(step.selector)});
        if(!sel) return;
        Array.from(sel.options).forEach(function(o){
          if(o.text.includes(${JSON.stringify(step.value)})||o.value.includes(${JSON.stringify(step.value)})) sel.value=o.value;
        });
        sel.dispatchEvent(new Event('change',{bubbles:true}));
      })()`);
      break;
    }
    default:
      emit({ type: 'scenario-log', scenarioId, text: `  [IGNORADO] ação desconhecida: ${step.action}\n` });
  }
}

const SCENARIO_TIMEOUT_MS = 3 * 60 * 1000;

async function runScenarioInTab(scenario, urls, cdpGlobal, onceGlobal, onEventGlobal, evidenceDir, emit) {
  const timeoutPromise = new Promise((_, reject) =>
    setTimeout(() => reject(new Error(`Cenário excedeu o tempo limite de ${SCENARIO_TIMEOUT_MS / 60000} minutos`)), SCENARIO_TIMEOUT_MS)
  );
  return Promise.race([timeoutPromise, _runScenarioInTabImpl(scenario, urls, cdpGlobal, onceGlobal, onEventGlobal, evidenceDir, emit)]);
}

async function _runScenarioInTabImpl(scenario, urls, cdpGlobal, onceGlobal, onEventGlobal, evidenceDir, emit) {
  const { scenarioId, title, steps } = scenario;
  emit({ type: 'scenario-log', scenarioId, text: `Iniciando cenário: ${title}\n` });

  let targetId;
  try {
    const tab = await cdpGlobal('Target.createTarget', { url: 'about:blank' });
    targetId = tab.targetId;
    const { sessionId: sid } = await cdpGlobal('Target.attachToTarget', { targetId, flatten: true });
    await cdpGlobal('Page.enable', {}, sid);
    await cdpGlobal('Runtime.enable', {}, sid);

    const cdp = (method, params = {}, s = null) => cdpGlobal(method, params, s || sid);
    const once = (method, s) => onceGlobal(method, s || sid);

    let lastFrameEmit = 0;
    const unsubScreencast = onEventGlobal('Page.screencastFrame', sid, ({ data, metadata, sessionId }) => {
      cdpGlobal('Page.screencastFrameAck', { sessionId }, sid).catch(() => {});
      const now = Date.now();
      if (now - lastFrameEmit < 200) return;
      lastFrameEmit = now;
      emit({ type: 'scenario-live-frame', scenarioId, dataUrl: 'data:image/jpeg;base64,' + data });
    });
    await cdpGlobal('Page.startScreencast', { format: 'jpeg', quality: 55, maxWidth: 480, maxHeight: 270, everyNthFrame: 1 }, sid).catch(() => {});

    const helpers = buildTabHelpers(cdp, once, sid, emit, scenarioId, evidenceDir);

    await cdp('Page.addScriptToEvaluateOnNewDocument', { source: 'window.open = () => null;' }, sid).catch(() => {});

    const firstUrl = urls.find(u => u.url?.trim())?.url;
    if (firstUrl && (!steps[0] || steps[0].action !== 'navigate')) {
      await helpers.navigate(firstUrl);
    }

    const RETRIABLE = ['click_text', 'fill_field', 'assert_text', 'assert_visible', 'wait_for'];
    let failedStep = null;
    for (let i = 0; i < steps.length; i++) {
      const maxTries = RETRIABLE.includes(steps[i].action) ? 2 : 1;
      let lastErr;
      for (let attempt = 0; attempt < maxTries; attempt++) {
        try {
          await executeStep(steps[i], helpers, cdp, sid, emit, scenarioId);
          lastErr = null;
          break;
        } catch (err) {
          lastErr = err;
          if (attempt < maxTries - 1) {
            emit({ type: 'scenario-log', scenarioId, text: `  [RETRY] ${err.message} — tentando novamente...\n` });
            await new Promise(r => setTimeout(r, 800));
          }
        }
      }
      if (lastErr) {
        failedStep = { step: i + 1, action: steps[i].action, error: lastErr.message };
        emit({ type: 'scenario-log', scenarioId, text: `  [FALHA] passo ${i + 1} (${steps[i].action}): ${lastErr.message}\n` });
        await captureAndSaveScreenshot(cdp, sid, scenarioId, `falha-passo-${i + 1}`, evidenceDir, emit);
        break;
      }
    }

    await cdpGlobal('Page.stopScreencast', {}, sid).catch(() => {});
    unsubScreencast();
    await cdpGlobal('Target.closeTarget', { targetId }).catch(() => {});

    if (failedStep) {
      emit({ type: 'scenario-done', scenarioId, status: 'fail', evidence: `Falhou no passo ${failedStep.step}: ${failedStep.error}` });
      return { scenarioId, title, status: 'fail', error: failedStep.error };
    } else {
      emit({ type: 'scenario-done', scenarioId, status: 'pass', evidence: 'Todos os passos executados com sucesso' });
      return { scenarioId, title, status: 'pass', error: null };
    }
  } catch (err) {
    if (targetId) {
      await cdpGlobal('Page.stopScreencast', {}, targetId).catch(() => {});
      await cdpGlobal('Target.closeTarget', { targetId }).catch(() => {});
    }
    emit({ type: 'scenario-done', scenarioId, status: 'fail', evidence: err.message });
    return { scenarioId, title, status: 'fail', error: err.message };
  }
}

export function startParallelQaRun(specName, urls, scenarios, opts = {}) {
  const jobId = randomUUID();
  const job = { chunks: [], done: false, code: null, listeners: new Set() };
  jobs.set(jobId, job);

  const emit = (event) => {
    job.chunks.push(event);
    for (const fn of job.listeners) fn(event);
  };

  const QA_RUN_TIMEOUT_MS = 10 * 60 * 1000;
  const globalTimeout = setTimeout(() => {
    emit({ type: 'data', text: '\n❌ Tempo limite global de 10 minutos atingido. Execução interrompida.\n' });
    job.done = true; job.code = 1;
    emit({ type: 'done', code: 1 });
    job.listeners.clear();
  }, QA_RUN_TIMEOUT_MS);

  (async () => {
    const sleep = (ms) => new Promise(r => setTimeout(r, ms));
    const chromePort = opts.port || 9222;
    const chromeHeadless = opts.headless || false;

    if (!(await isChromeDebugReachable(chromePort))) {
      emit({ type: 'data', text: 'Iniciando Chrome com depuração remota...\n' });
      await launchChromeDebug(chromePort, chromeHeadless);
      let ready = false;
      for (let i = 0; i < 30; i++) {
        await sleep(1000);
        if (await isChromeDebugReachable(chromePort)) { ready = true; break; }
      }
      if (!ready) {
        emit({ type: 'data', text: '❌ Chrome não ficou disponível. Verifique se está instalado.\n' });
        job.done = true; job.code = 1;
        emit({ type: 'done', code: 1 });
        job.listeners.clear();
        setTimeout(() => jobs.delete(jobId), 5 * 60_000);
        return;
      }
    }

    const version = await cdpHttpGet('/json/version', chromePort);
    if (!version?.webSocketDebuggerUrl) {
      emit({ type: 'data', text: '❌ Chrome não retornou WebSocket URL\n' });
      job.done = true; job.code = 1;
      emit({ type: 'done', code: 1 });
      job.listeners.clear();
      setTimeout(() => jobs.delete(jobId), 5 * 60_000);
      return;
    }

    const ws = new WebSocket(version.webSocketDebuggerUrl);
    let msgId = 0;
    const pending = new Map();
    const eventListeners = new Map();

    const rejectAllPending = (reason) => {
      for (const { reject } of pending.values()) reject(new Error(reason));
      pending.clear();
    };

    ws.onmessage = (e) => {
      let msg; try { msg = JSON.parse(e.data); } catch { return; }
      if (msg.id !== undefined && pending.has(msg.id)) {
        const { resolve, reject } = pending.get(msg.id);
        pending.delete(msg.id);
        msg.error ? reject(new Error(msg.error.message)) : resolve(msg.result || {});
      }
      if (msg.method) {
        const key = `${msg.sessionId || ''}:${msg.method}`;
        (eventListeners.get(key) || new Set()).forEach(h => h(msg.params || {}));
      }
    };
    ws.onclose = () => rejectAllPending('Chrome desconectou inesperadamente');
    ws.onerror = () => rejectAllPending('Erro na conexão WebSocket com o Chrome');

    await new Promise((resolve, reject) => {
      ws.onopen = resolve;
      ws.onerror = (e) => reject(new Error('WS erro: ' + (e.message || 'conexão recusada')));
      setTimeout(() => reject(new Error('WS timeout — Chrome não respondeu em 5s')), 5000);
    });
    ws.onerror = () => rejectAllPending('Erro na conexão WebSocket com o Chrome');

    const cdpGlobal = (method, params = {}, sid = null) => new Promise((resolve, reject) => {
      const id = ++msgId;
      pending.set(id, { resolve, reject });
      ws.send(JSON.stringify({ id, method, params, ...(sid ? { sessionId: sid } : {}) }));
      setTimeout(() => { if (pending.has(id)) { pending.delete(id); reject(new Error(`CDP timeout: ${method}`)); } }, 15000);
    });

    const onceGlobal = (method, sid) => new Promise(resolve => {
      const key = `${sid || ''}:${method}`;
      if (!eventListeners.has(key)) eventListeners.set(key, new Set());
      const h = (p) => { eventListeners.get(key).delete(h); resolve(p); };
      eventListeners.get(key).add(h);
    });

    const onEventGlobal = (method, sid, handler) => {
      const key = `${sid || ''}:${method}`;
      if (!eventListeners.has(key)) eventListeners.set(key, new Set());
      eventListeners.get(key).add(handler);
      return () => eventListeners.get(key).delete(handler);
    };

    const preflightUrl = urls.find(u => u.url?.trim())?.url;
    if (preflightUrl) {
      emit({ type: 'auth-check' });
      const SSO_DOMAINS = ['login.microsoftonline.com', 'login.live.com', 'login.windows.net', 'accounts.google.com', 'okta.com', 'auth0.com', 'login.salesforce.com'];
      let authTargetId;
      try {
        ({ targetId: authTargetId } = await cdpGlobal('Target.createTarget', { url: 'about:blank' }));
        const { sessionId: authSid } = await cdpGlobal('Target.attachToTarget', { targetId: authTargetId, flatten: true });
        await cdpGlobal('Page.enable', {}, authSid);

        const authLoaded = onceGlobal('Page.loadEventFired', authSid);
        const { errorText: authErr } = await cdpGlobal('Page.navigate', { url: preflightUrl }, authSid);

        if (!authErr || authErr === 'net::ERR_ABORTED') {
          await Promise.race([authLoaded, sleep(10000)]);
          await sleep(1500);

          const { targetInfo } = await cdpGlobal('Target.getTargetInfo', { targetId: authTargetId });
          const currentHref = targetInfo?.url || '';
          const needsAuth = SSO_DOMAINS.some(d => currentHref.includes(d));

          if (needsAuth) {
            let authHostname;
            try { authHostname = new URL(currentHref).hostname; } catch { authHostname = currentHref; }

            const authSignal = new Promise(resolve => authSignals.set(jobId, resolve));
            emit({ type: 'auth-required', hostname: authHostname, jobId });

            let authTimedOut = false;
            await Promise.race([
              authSignal,
              sleep(300_000).then(() => { authTimedOut = true; })
            ]);
            authSignals.delete(jobId);
            if (authTimedOut) {
              emit({ type: 'data', text: '❌ Tempo de espera para login esgotado (5 min). Execução cancelada.\n' });
              throw new Error('Timeout de autenticação — faça login no Chrome e tente novamente.');
            }
            emit({ type: 'auth-complete' });
            await sleep(800);
          } else {
            emit({ type: 'auth-skipped' });
          }
        } else {
          emit({ type: 'auth-skipped' });
        }
      } catch (e) {
        emit({ type: 'data', text: `[preflight] aviso: ${e.message} — pulando verificação de autenticação\n` });
        emit({ type: 'auth-skipped' });
      } finally {
        if (authTargetId) await cdpGlobal('Target.closeTarget', { targetId: authTargetId }).catch(() => {});
      }
    }

    const CONCURRENT_TABS = 9;
    const total = scenarios.length;
    if (total > CONCURRENT_TABS) {
      emit({ type: 'data', text: `${total} cenários selecionados — ${CONCURRENT_TABS} rodam em paralelo, os demais aguardam na fila.\n` });
    }

    const timestamp = new Date().toISOString().replace(/[:.]/g, '-').slice(0, 19);
    const evidenceBase = path.join(process.cwd(), '.sdd', 'qa', 'evidences', specName, timestamp);
    await mkdir(evidenceBase, { recursive: true });

    for (let i = 0; i < scenarios.length; i++) {
      const s = scenarios[i];
      emit({ type: 'scenario-start', scenarioId: s.scenarioId ?? i, title: s.title });
      if (i >= CONCURRENT_TABS) {
        emit({ type: 'scenario-queued', scenarioId: s.scenarioId ?? i, queuePos: i - CONCURRENT_TABS + 1 });
      }
    }
    emit({ type: 'queue-info', total, concurrent: Math.min(CONCURRENT_TABS, total), queued: Math.max(0, total - CONCURRENT_TABS) });

    const validUrls = urls.filter(u => u.url?.trim());
    let scenariosWithSteps;
    const alreadyHaveSteps = scenarios.every(s => Array.isArray(s.steps) && s.steps.length > 0);
    if (alreadyHaveSteps) {
      scenariosWithSteps = scenarios;
      emit({ type: 'data', text: `Usando passos pré-aprovados. Iniciando execução...\n\n` });
    } else {
      emit({ type: 'data', text: `Gerando passos de teste com IA para ${total} cenário(s)...\n` });
      const creds = urls.filter(u => u.username).map(u => ({ env: u.env, username: u.username, password: u.password }));
      try {
        scenariosWithSteps = await generateTestStepsWithClaude(
          scenarios.map((s, i) => ({ ...s, scenarioId: s.scenarioId ?? i })),
          validUrls, creds
        );
        scenariosWithSteps = scenariosWithSteps.map((sw, i) => ({
          ...(scenarios[i] || scenarios[sw.scenarioId] || scenarios[0]),
          ...sw,
          scenarioId: sw.scenarioId ?? i,
        }));
      } catch (err) {
        emit({ type: 'data', text: `Aviso: falha ao gerar passos (${err.message}), usando fallback básico.\n` });
        scenariosWithSteps = scenarios.map((s, i) => ({
          ...s, scenarioId: s.scenarioId ?? i,
          steps: [
            { action: 'navigate', url: validUrls[0]?.url || 'about:blank' },
            { action: 'screenshot', label: 'inicial' },
          ],
        }));
      }
      emit({ type: 'data', text: `Passos gerados. Iniciando execução com fila...\n\n` });
    }

    let nextIdx = 0;
    let completed = 0;
    const allResults = new Array(scenariosWithSteps.length);
    await Promise.all(Array.from({ length: Math.min(CONCURRENT_TABS, scenariosWithSteps.length) }, async () => {
      while (nextIdx < scenariosWithSteps.length) {
        const i = nextIdx++;
        const s = scenariosWithSteps[i];
        const sid = s.scenarioId ?? i;
        if (i >= CONCURRENT_TABS) {
          emit({ type: 'scenario-executing', scenarioId: sid });
        }
        const slug = toSlug(s.title).slice(0, 40);
        const evidenceDir = path.join(evidenceBase, `scenario-${sid}-${slug}`);
        await mkdir(evidenceDir, { recursive: true });
        allResults[i] = await runScenarioInTab(s, validUrls, cdpGlobal, onceGlobal, onEventGlobal, evidenceDir, emit);
        completed++;
        emit({ type: 'queue-progress', completed, total });
      }
    }));

    ws.close();

    const results = allResults.filter(Boolean);
    const pass = results.filter(r => r.status === 'pass').length;
    const fail = results.filter(r => r.status === 'fail').length;
    const summary = {
      executedAt: new Date().toISOString(),
      spec: specName,
      evidencesDir: evidenceBase,
      summary: { total: results.length, pass, fail, skip: 0 },
      scenarios: results.map(r => ({ id: r.scenarioId, title: r.title, status: r.status, evidence: r.error || 'OK', error: r.error || null })),
    };
    emit({ type: 'qa-summary', results: summary });
    emit({ type: 'data', text: `\n===QA_RESULTS_JSON===\n${JSON.stringify(summary, null, 2)}\n===END_QA_RESULTS===\n` });

    try {
      const reportDir = path.join(process.cwd(), '.sdd', 'qa', 'reports');
      await mkdir(reportDir, { recursive: true });
      const reportPath = path.join(reportDir, `${specName}-${timestamp}.json`);
      await writeFile(reportPath, JSON.stringify(summary, null, 2), 'utf8');
      emit({ type: 'data', text: `Relatório salvo em: ${reportPath}\n` });
    } catch {}

    clearTimeout(globalTimeout);
    job.done = true; job.code = fail > 0 ? 1 : 0;
    emit({ type: 'done', code: job.code });
    job.listeners.clear();
    setTimeout(() => jobs.delete(jobId), 5 * 60_000);
  })().catch(err => {
    clearTimeout(globalTimeout);
    emit({ type: 'data', text: `❌ Erro no executor QA: ${err.message}\n` });
    job.done = true; job.code = 1;
    emit({ type: 'done', code: 1 });
    job.listeners.clear();
    setTimeout(() => jobs.delete(jobId), 5 * 60_000);
  });

  return jobId;
}

export async function describeImageWithClaude(base64, mimeType) {
  const apiKey = process.env.ANTHROPIC_API_KEY;
  if (!apiKey) return '(ANTHROPIC_API_KEY não configurada — imagem não descrita)';
  try {
    const r = await fetch('https://api.anthropic.com/v1/messages', {
      method: 'POST',
      headers: { 'x-api-key': apiKey, 'anthropic-version': '2023-06-01', 'content-type': 'application/json' },
      body: JSON.stringify({
        model: 'claude-sonnet-4-6', max_tokens: 1024,
        messages: [{ role: 'user', content: [
          { type: 'image', source: { type: 'base64', media_type: mimeType, data: base64 } },
          { type: 'text', text: 'Descreva esta imagem de protótipo/interface para um analista de requisitos: quais telas, seções, campos, botões, fluxos ou informações visuais ela mostra? Seja específico e completo em português.' }
        ]}]
      })
    });
    const d = await r.json();
    return d.content?.[0]?.text || '(sem descrição)';
  } catch { return '(erro ao descrever imagem)'; }
}
