// df-mcp E2E 实测脚本:spawn 真实二进制,按行喂 JSON-RPC,断言修复项 + 全链路。
// 用法: node df-mcp-e2e.mjs <exe路径> [--db <临时库>]
import { spawn } from 'node:child_process';
import { createInterface } from 'node:readline';
import { mkdtempSync, rmSync } from 'node:fs';
import { tmpdir } from 'node:os';
import { join } from 'node:path';

const exe = process.argv[2];
if (!exe) { console.error('用法: node df-mcp-e2e.mjs <exe路径>'); process.exit(1); }
const dbArg = process.argv[4] ?? join(mkdtempSync(join(tmpdir(), 'dfmcp-')), 'test.db');

let pass = 0, fail = 0;
const failures = [];
function assert(cond, name, extra) {
  if (cond) { pass++; console.log(`  ✅ ${name}`); }
  else { fail++; failures.push(name); console.log(`  ❌ ${name}${extra ? ` — ${extra}` : ''}`); }
}

function session(dbPath, readOnly = false) {
  const args = ['mcp-server', '--db', dbPath];
  if (readOnly) args.push('--read-only');
  const child = spawn(exe, args, { stdio: ['pipe', 'pipe', 'pipe'] });
  const rl = createInterface({ input: child.stdout });
  const pending = [];
  const waiters = [];
  let buf = '';
  rl.on('line', (line) => {
    const resolved = waiters.shift();
    if (resolved) resolved(line);
  });
  child.stderr.on('data', (d) => process.stderr.write(`  [server] ${d}`));
  let idSeq = 0;
  return {
    async call(req) {
      const payload = { jsonrpc: '2.0', id: ++idSeq, ...req };
      const p = new Promise((res) => waiters.push(res));
      child.stdin.write(JSON.stringify(payload) + '\n');
      const line = await p;
      return JSON.parse(line);
    },
    async raw(line) {
      const p = new Promise((res) => waiters.push(res));
      child.stdin.write(line + '\n');
      const out = await p;
      return JSON.parse(out);
    },
    close() { child.stdin.end(); },
  };
}

// 从 CallToolResult 取文本
function txt(res) {
  try { return res.result.content[0].text; } catch { return JSON.stringify(res); }
}
function json(res) { return JSON.parse(txt(res)); }

async function main() {
  console.log(`\n═══ df-mcp E2E 实测  exe=${exe}  db=${dbArg}  ═══`);

  // ── 实例 A:读写模式 ──────────────────────────────
  const s = session(dbArg, false);
  console.log('\n【1】握手与工具清单');

  const init = await s.call({ method: 'initialize', params: {} });
  assert(init.result.serverInfo.name === 'devflow-mcp', 'initialize 返回 serverInfo.name', init.result?.serverInfo?.name);
  assert(init.result.capabilities.tools.listChanged === false, 'capabilities.tools 声明');

  const tl = await s.call({ method: 'tools/list', params: {} });
  const names = tl.result.tools.map(t => t.name);
  const low = ['list_projects','get_project','list_tasks','list_ideas','evaluate_idea','list_trash'];
  const med = ['create_project','update_project','bind_directory','create_task','update_task','advance_task','create_idea','update_idea','score_idea','restore_project'];
  const high = ['delete_project','delete_task','delete_idea','run_workflow'];
  assert(low.every(n => names.includes(n)), 'Low 工具全部可见');
  assert(med.every(n => names.includes(n)), 'Medium 工具全部可见');
  assert(!high.some(n => names.includes(n)), 'High 工具默认不可见');
  assert(names.length === low.length + med.length, `工具数 = ${low.length + med.length}`, `实际 ${names.length}`);

  console.log('\n【2】修复项:parse error 带 id:null(修复#1)');
  const perr = await s.raw('{"jsonrpc":"2.0","id":999,"method":"bogus", BAD JSON');
  assert(perr.error && perr.error.code === -32700, '非法 JSON → PARSE_ERROR(-32700)');
  assert(perr.id === null, 'parse error 响应 id 必须为 null', `实际 ${JSON.stringify(perr.id)}`);

  console.log('\n【3】修复项:tools/call 缺 name 明确报错(修复#7)');
  const noname = await s.call({ method: 'tools/call', params: { arguments: {} } });
  assert(noname.error && noname.error.code === -32601, '缺 name → METHOD_NOT_FOUND');
  assert(noname.error.message.includes('name'), '错误消息含 name 提示', noname.error.message);
  assert(!noname.error.message.includes('未知工具: '), '不是空名「未知工具: 」', noname.error.message);

  console.log('\n【4】未知工具');
  const unk = await s.call({ method: 'tools/call', params: { name: 'nope', arguments: {} } });
  assert(txt(unk).includes('未知工具'), '未知工具报错', txt(unk));

  console.log('\n【5】修复项:update_project 非法状态不再静默降级(修复#3)');
  const cp = await s.call({ method: 'tools/call', params: { name: 'create_project', arguments: { name: 'E2E项目', description: '实测' } } });
  assert(!cp.result.isError, 'create_project 成功', txt(cp));
  const proj = json(cp);
  const pid = proj.id;
  const badSt = await s.call({ method: 'tools/call', params: { name: 'update_project', arguments: { id: pid, status: 'bogus_state' } } });
  assert(badSt.result.isError === true, 'update_project 非法 status 报错');
  assert(txt(badSt).includes('非法状态值'), '错误信息含"非法状态值"', txt(badSt));

  console.log('\n【6】update_project 部分更新保留原值');
  const upd = await s.call({ method: 'tools/call', params: { name: 'update_project', arguments: { id: pid, name: 'E2E项目改' } } });
  assert(!upd.result.isError, '部分更新成功');
  const updJ = json(upd);
  assert(updJ.project.name === 'E2E项目改' && updJ.project.status === 'planning', 'name 更新 + status 保留 planning', `status=${updJ.project.status}`);

  console.log('\n【7】create_task 带 parent_id(外部新增能力)');
  const cproj = await s.call({ method: 'tools/call', params: { name: 'list_projects', arguments: {} } });
  const listJ = json(cproj);
  const parentT = await s.call({ method: 'tools/call', params: { name: 'create_task', arguments: { project_id: pid, title: '父任务', description: '' } } });
  const parentId = json(parentT).id;
  const childT = await s.call({ method: 'tools/call', params: { name: 'create_task', arguments: { project_id: pid, title: '子任务', description: '', parent_id: parentId } } });
  assert(!childT.result.isError, '创建子任务成功', txt(childT));
  const grandchild = await s.call({ method: 'tools/call', params: { name: 'create_task', arguments: { project_id: pid, title: '孙任务', description: '', parent_id: json(childT).id } } });
  assert(grandchild.result.isError === true && txt(grandchild).includes('父任务不能是子任务'), '1 级嵌套限制:孙任务被拒', txt(grandchild));
  const badP = await s.call({ method: 'tools/call', params: { name: 'create_task', arguments: { project_id: pid, title: 'x', description: '', parent_id: 'ghost' } } });
  assert(badP.result.isError === true && txt(badP).includes('父任务不存在'), '父任务不存在被拒', txt(badP));

  console.log('\n【8】advance_task 状态机');
  const adv = await s.call({ method: 'tools/call', params: { name: 'advance_task', arguments: { id: parentId, to: 'in_progress' } } });
  assert(!adv.result.isError && json(adv).task.status === 'in_progress', 'todo→in_progress 成功');
  const badAdv = await s.call({ method: 'tools/call', params: { name: 'advance_task', arguments: { id: parentId, to: 'done' } } });
  assert(badAdv.result.isError === true, 'todo→done 非法跳态被拒', txt(badAdv));

  console.log('\n【9】idea 闭环:evaluate(Low只读) → score(Medium写库)');
  const ci = await s.call({ method: 'tools/call', params: { name: 'create_idea', arguments: { title: '核心功能', description: '需要立即重构关键模块以解除阻塞' } } });
  const iid = json(ci).id;
  const ev = await s.call({ method: 'tools/call', params: { name: 'evaluate_idea', arguments: { id: iid } } });
  assert(!ev.result.isError && json(ev).scores.overall >= 0, 'evaluate_idea 返回分数');
  const evJ = json(ev);
  assert(evJ.idea.scores == null, 'evaluate_idea 不写库(scores 仍 null)');
  const sc = await s.call({ method: 'tools/call', params: { name: 'score_idea', arguments: { id: iid } } });
  const scJ = json(sc);
  assert(!sc.result.isError && scJ.idea.scores != null, 'score_idea 写库成功');

  console.log('\n【10】bind_directory:冲突/穿越/不存在路径');
  const bd1 = await s.call({ method: 'tools/call', params: { name: 'bind_directory', arguments: { id: pid, path: 'C:/Temp/nonexist_dir' } } });
  assert(!bd1.result.isError, '绑定不存在目录成功(规范化为绝对路径)');
  const bdConflict = await s.call({ method: 'tools/call', params: { name: 'create_project', arguments: { name: 'E2E项目2', description: '' } } });
  const pid2 = json(bdConflict).id;
  const bd2 = await s.call({ method: 'tools/call', params: { name: 'bind_directory', arguments: { id: pid2, path: 'c:/temp/nonexist_dir' } } });
  assert(bd2.result.isError === true && txt(bd2).includes('路径已被项目'), '路径冲突检测(大小写不敏感)', txt(bd2));
  const bdTraverse = await s.call({ method: 'tools/call', params: { name: 'bind_directory', arguments: { id: pid2, path: 'C:/temp/../Windows' } } });
  assert(bdTraverse.result.isError === true && txt(bdTraverse).includes('..'), '.. 段拒绝', txt(bdTraverse));

  console.log('\n【11】回收站 list_trash / restore_project');
  const lt = await s.call({ method: 'tools/call', params: { name: 'list_trash', arguments: {} } });
  assert(!lt.result.isError, 'list_trash 成功');
  const rest = await s.call({ method: 'tools/call', params: { name: 'restore_project', arguments: { id: 'ghost' } } });
  assert(rest.result.isError === true, '恢复不存在的项目报错', txt(rest));

  console.log('\n【12】跨实体校验');
  const cross = await s.call({ method: 'tools/call', params: { name: 'update_task', arguments: { id: iid, title: 'x' } } });
  assert(cross.result.isError === true && txt(cross).includes('属于 idea'), 'update_task 传 idea id → 跨实体报错', txt(cross));

  console.log('\n【13】High 工具即便绕过 tools/list 也被拒');
  const del = await s.call({ method: 'tools/call', params: { name: 'delete_project', arguments: { id: pid } } });
  assert(del.result.isError === true && txt(del).includes('DevFlow'), 'delete_project 默认拒绝', txt(del));

  s.close();

  // ── 实例 B:read-only 模式 ─────────────────────────
  console.log('\n════ read-only 模式 ════');
  const ro = session(dbArg, true);
  const roList = await ro.call({ method: 'tools/list', params: {} });
  const roNames = roList.result.tools.map(t => t.name);
  assert(roNames.length === low.length, `read-only 仅 Low(${low.length} 个)`, `实际 ${roNames.length}`);
  assert(roNames.every(n => low.includes(n)), 'read-only 只含 Low 工具');
  const roCall = await ro.call({ method: 'tools/call', params: { name: 'create_project', arguments: { name: 'x', description: '' } } });
  assert(roCall.result.isError === true && txt(roCall).includes('只读模式'), 'read-only 拒绝 Medium', txt(roCall));
  const roEval = await ro.call({ method: 'tools/call', params: { name: 'evaluate_idea', arguments: { id: iid } } });
  assert(!roEval.result.isError, 'read-only 放行 evaluate_idea(Low)');
  ro.close();

  try { await new Promise(r => setTimeout(r, 300)); rmSync(join(dbArg, '..'), { recursive: true, force: true }); } catch {}

  console.log(`\n═══ 结果: ${pass} 通过 / ${fail} 失败 ═══`);
  if (failures.length) { console.log('失败项:'); failures.forEach(f => console.log(`  - ${f}`)); process.exit(1); }
  console.log('✅ 全部通过');
}

main().catch(e => { console.error('脚本异常:', e); process.exit(1); });
