#!/usr/bin/env node

import { spawn } from "node:child_process";
import {
  chmodSync,
  mkdtempSync,
  readFileSync,
  rmSync,
} from "node:fs";
import { tmpdir } from "node:os";
import { dirname, join } from "node:path";
import { fileURLToPath } from "node:url";

const expected = [
  ["Sharon", "Security"],
  ["Marcus", "OWASP"],
  ["Alejandro", "Accessibility"],
  ["Hiroshi", "WCAG"],
  ["Mia", "Usability"],
  ["Tariq", "Performance"],
  ["Sophia", "Content"],
  ["Richard", "Forms"],
  ["Fatima", "Error UX"],
  ["Mei", "Search"],
  ["Zara", "News / Content"],
  ["Yuki", "Landing Pages"],
  ["Hassan", "Checkout"],
  ["Priya", "Shopping Cart"],
  ["Mateo", "Pricing Pages"],
  ["Pete", "Privacy / PII"],
  ["Zanele", "GDPR"],
  ["Rajesh", "Cookie Consent"],
  ["Sundar", "Legal Policy"],
  ["Jason", "AI Code Review"],
  ["Diego", "AI Chatbot"],
];

const pluginRoot = dirname(dirname(fileURLToPath(import.meta.url)));
const serverPath = join(pluginRoot, "server", "index.js");
const tempHome = mkdtempSync(join(tmpdir(), "carbon-personas-"));
chmodSync(tempHome, 0o700);
const childEnv = { ...process.env, HOME: tempHome };
delete childEnv.TESTERS_AI_EMAIL;
delete childEnv.TESTERS_AI_API_KEY;
delete childEnv.TESTERS_AI_BASE_URL;

const child = spawn(process.execPath, [serverPath], {
  cwd: pluginRoot,
  env: childEnv,
  stdio: ["pipe", "pipe", "pipe"],
});
let stdoutBuffer = "";
let stderrBuffer = "";
let nextId = 1;
const pending = new Map();

function fail(message) {
  throw new Error(`${message}${stderrBuffer.trim() ? `\n${stderrBuffer.trim()}` : ""}`);
}

function request(method, params = {}) {
  const id = nextId++;
  child.stdin.write(`${JSON.stringify({ jsonrpc: "2.0", id, method, params })}\n`);
  return new Promise((resolve, reject) => pending.set(id, { resolve, reject }));
}

function parseTool(result) {
  const text = (result.content || []).map((item) => item.text || "").join("\n");
  if (result.isError) fail(text);
  return JSON.parse(text);
}

function pairList(result) {
  return (result.specialists || []).map(({ name, focus }) => [name, focus]);
}

child.stdout.setEncoding("utf8");
child.stdout.on("data", (chunk) => {
  stdoutBuffer += chunk;
  while (stdoutBuffer.includes("\n")) {
    const newline = stdoutBuffer.indexOf("\n");
    const line = stdoutBuffer.slice(0, newline).trim();
    stdoutBuffer = stdoutBuffer.slice(newline + 1);
    if (!line) continue;
    let message;
    try {
      message = JSON.parse(line);
    } catch {
      for (const waiter of pending.values()) waiter.reject(new Error(`Non-JSON MCP output: ${line}`));
      pending.clear();
      continue;
    }
    const waiter = pending.get(message.id);
    if (!waiter) continue;
    pending.delete(message.id);
    if (message.error) waiter.reject(new Error(message.error.message));
    else waiter.resolve(message.result);
  }
});
child.stderr.setEncoding("utf8");
child.stderr.on("data", (chunk) => { stderrBuffer = `${stderrBuffer}${chunk}`.slice(-4000); });
child.on("error", (error) => {
  for (const waiter of pending.values()) waiter.reject(error);
  pending.clear();
});

const timeout = setTimeout(() => {
  for (const waiter of pending.values()) waiter.reject(new Error("Persona contract test timed out"));
  pending.clear();
  child.kill("SIGTERM");
}, 20_000);

try {
  const initialized = await request("initialize", {
    protocolVersion: "2024-11-05",
    capabilities: {},
    clientInfo: { name: "carbon-persona-contract", version: "1.0.0" },
  });
  if (initialized.serverInfo?.version !== "1.32.20") {
    fail(`expected server 1.32.8, found ${initialized.serverInfo?.version}`);
  }

  const tools = (await request("tools/list"))?.tools || [];
  if (!tools.some((tool) => tool.name === "carbon_get_specialists")) {
    fail("carbon_get_specialists was not discovered");
  }

  const all = parseTool(await request("tools/call", {
    name: "carbon_get_specialists",
    arguments: {},
  }));
  if (all.count !== 21 || JSON.stringify(pairList(all)) !== JSON.stringify(expected)) {
    fail(`official roster mismatch: ${JSON.stringify(pairList(all))}`);
  }
  if (!all.distinction.includes("Generated end-user personas are separate")) {
    fail("specialist/end-user persona distinction is missing");
  }

  const accessibility = parseTool(await request("tools/call", {
    name: "carbon_get_specialists",
    arguments: { checks: ["accessibility"] },
  }));
  const accessibilityNames = accessibility.specialists.map((row) => row.name);
  if (JSON.stringify(accessibilityNames) !== JSON.stringify(["Alejandro", "Hiroshi"])) {
    fail(`accessibility routing mismatch: ${accessibilityNames.join(", ")}`);
  }

  const routeContract = [
    ["security", ["Sharon", "Marcus"]],
    ["privacy", ["Pete", "Zanele", "Rajesh", "Sundar"]],
    ["performance", ["Tariq"]],
    ["usability", ["Mia"]],
    ["content", ["Sophia", "Zara"]],
    ["forms", ["Richard"]],
    ["search", ["Mei"]],
    ["publishing", ["Zara"]],
    ["landing_page", ["Yuki"]],
    ["checkout", ["Hassan"]],
    ["cart", ["Priya"]],
    ["pricing", ["Mateo"]],
    ["error_ux", ["Fatima"]],
    ["ai_code", ["Jason"]],
    ["chatbot", ["Diego"]],
  ];
  for (const [check, expectedNames] of routeContract) {
    const routed = parseTool(await request("tools/call", {
      name: "carbon_get_specialists",
      arguments: { checks: [check] },
    }));
    const routedNames = routed.specialists.map((row) => row.name);
    if (JSON.stringify(routedNames) !== JSON.stringify(expectedNames)) {
      fail(`${check} routing mismatch: ${routedNames.join(", ")}`);
    }
  }

  const unknown = parseTool(await request("tools/call", {
    name: "carbon_get_specialists",
    arguments: { checks: ["not-a-real-domain"] },
  }));
  if (unknown.count !== 0 || !unknown.unmatchedChecks.includes("not-a-real-domain")) {
    fail("unknown specialist filter did not fail closed");
  }

  const capabilities = parseTool(await request("tools/call", {
    name: "carbon_get_capabilities",
    arguments: {},
  }));
  if (capabilities.namedSpecialists?.count !== 21) {
    fail("capabilities do not show the complete named roster");
  }

  for (const promptName of ["carbon_accessibility"]) {
    const prompt = await request("prompts/get", { name: promptName });
    const text = (prompt.messages || []).map((message) => message.content?.text || "").join("\n");
    if (!text.includes("carbon_get_testing_domains")) {
      fail(`${promptName} does not load named-specialist-domain guidance`);
    }
  }
  const humanPrompt = await request("prompts/get", { name: "carbon_human" });
  const humanText = (humanPrompt.messages || []).map((message) => message.content?.text || "").join("\n");
  if (!humanText.includes("carbon_human_context")) {
    fail("carbon_human does not load durable human-context guidance");
  }

  const readme = readFileSync(join(pluginRoot, "README.md"), "utf8");
  const specialistReference = readFileSync(join(pluginRoot, "runtime", "carbon", "references", "specialists.json"), "utf8").replace(/\s+/g, " ");
  for (const [name, focus] of expected) {
    if (!specialistReference.includes(name) || !specialistReference.includes(focus)) {
      fail(`specialist reference does not show ${name} · ${focus}`);
    }
  }
  for (const legacy of ["Aisha", "Natasha", "Spider"]) {
    if (readme.includes(legacy)) fail(`README contains unrelated legacy persona ${legacy}`);
  }

  const manifest = JSON.parse(readFileSync(join(pluginRoot, ".codex-plugin", "plugin.json"), "utf8"));
  const claudeManifest = JSON.parse(readFileSync(join(pluginRoot, ".claude-plugin", "plugin.json"), "utf8"));
  const serverPackage = JSON.parse(readFileSync(join(pluginRoot, "server", "package.json"), "utf8"));
  if (
    String(manifest.version || "").split("+")[0] !== "1.32.20" ||
    claudeManifest.version !== "1.32.20" ||
    serverPackage.version !== "1.32.20"
  ) {
    fail("Codex plugin, Claude plugin, server package, and runtime versions are not aligned");
  }

  console.log(
    "PASS: official 21-specialist roster, focus routing, persona distinction, " +
    "capabilities, workflow prompts, documentation, and version alignment verified.",
  );
} catch (error) {
  console.error(`FAIL: ${error.message}`);
  process.exitCode = 1;
} finally {
  clearTimeout(timeout);
  child.kill("SIGTERM");
  rmSync(tempHome, { recursive: true, force: true });
}
