#!/usr/bin/env bun
import assert from "node:assert/strict";
import { chmod, mkdir, mkdtemp, rm, writeFile } from "node:fs/promises";
import http from "node:http";
import https from "node:https";
import os from "node:os";
import path from "node:path";

import {
  handleCodeAgentChat,
  validateCodeAgentChatSchema
} from "../internal/cloud/code-agent-chat.ts";
import {
  codexAppServerArgs,
  codexAppServerProviderBaseUrl,
  createCodexAppServerJsonLineClient,
  createCodexStdioSessionManager
} from "../internal/cloud/codex-stdio-session.ts";
import { createAppServerTurnState } from "../internal/cloud/codex-stdio-session-turn-state.ts";
import {
  classifyCodexRunnerCapability,
  classifyCodeAgentChatReadiness,
  isHttpNon2xxStatus,
  summarizeCodeAgentPayload
} from "./src/code-agent-response-contract.mjs";

const defaultLiveUrl = "http://74.48.78.17:17667/";
const defaultLiveMessage = "请用一句话回复 HWLAB Code Agent readiness。";
const defaultLiveTimeoutMs = 45000;

const args = parseArgs(process.argv.slice(2));

if (args.mode === "help") {
  process.stdout.write(`${JSON.stringify(usage(), null, 2)}\n`);
} else if (args.mode === "live") {
  const report = await runLiveReadinessSmoke(args);
  process.stdout.write(`${JSON.stringify(report, null, 2)}\n`);
  process.exitCode = report.status === "pass" ? 0 : 2;
} else {
  await runLocalContractSmoke();
}

function logOk(name) {
  process.stdout.write(`[code-agent-chat-smoke] ok ${name}\n`);
}

function runnerTraceLabels(payload) {
  return Array.isArray(payload?.runnerTrace?.events)
    ? payload.runnerTrace.events.map((event) => event?.label ?? event).filter(Boolean)
    : [];
}

async function runJsonLineClientApprovalSmoke() {
  const fakeCodex = await createFakeJsonLineCodexCommand();
  const events = [];
  const client = createCodexAppServerJsonLineClient({
    command: fakeCodex.command,
    args: ["app-server", "--listen", "stdio://"],
    cwd: fakeCodex.root,
    env: { ...process.env, PATH: `${path.dirname(fakeCodex.command)}:${process.env.PATH ?? ""}` },
    onNotification: (message) => events.push(message)
  });
  try {
    await client.initialize(2000);
    const thread = await client.startThread({ model: "gpt-test", cwd: fakeCodex.root, sandbox: "workspace-write" }, 2000);
    assert.equal(thread.threadId, "thread_jsonline_test");
    const turn = await client.startTurn({ threadId: thread.threadId, prompt: "trigger approval", model: "gpt-test", cwd: fakeCodex.root }, 2000);
    assert.equal(turn.turnId, "turn_jsonline_test");
    assert.equal(turn.result.approvalDecision, "approved_for_session");
    assert.equal(turn.result.spawnedFrom, "native");
    assert.ok(events.some((event) => event.method === "client/request/handled" && event.params?.decision === "approved_for_session"));
    assert.ok(events.some((event) => event.method === "item/completed" && event.params?.item?.type === "commandExecution"));
  } finally {
    client.close();
    await rm(fakeCodex.root, { recursive: true, force: true });
  }
  logOk("codex app-server JSON-line client approves command requests and uses native binary directly");
}

function runTurnStateVisibilitySmoke() {
  const events = [];
  const state = createAppServerTurnState({
    traceRecorder: {
      append: (event) => {
        events.push(event);
        return event;
      },
      appendAssistantDelta: (event) => {
        events.push({ ...event, label: "assistant:stream" });
        return event;
      }
    },
    session: { sessionId: "ses_visibility_smoke", status: "busy", turn: 1 }
  });
  const command = "printf ".padEnd(3000, "c");
  const stdout = "x".repeat(5000);
  state.handle({ method: "thread/started", params: { thread: { id: "thread_visibility_smoke" } } });
  state.handle({ method: "turn/started", params: { turn: { id: "turn_visibility_smoke" } } });
  state.handle({ method: "client/request/handled", params: { method: "item/commandExecution/requestApproval", decision: "approved", itemId: "cmd_visibility" } });
  state.handle({
    method: "item/completed",
    params: { item: { id: "cmd_visibility", type: "commandExecution", status: "completed", exitCode: 0, command, stdout } }
  });
  const clientEvent = events.find((event) => event.label === "client/request:approved");
  const commandEvent = events.find((event) => event.label === "item/commandExecution:completed");
  assert.equal(clientEvent?.status, "completed");
  assert.equal(commandEvent?.commandBytes, Buffer.byteLength(command, "utf8"));
  assert.equal(commandEvent?.outputBytes, Buffer.byteLength(stdout, "utf8"));
  assert.equal(commandEvent?.commandTruncated, true);
  assert.ok(commandEvent?.stdoutSummary?.length <= 2100);
  logOk("codex app-server turn trace bounds large command/output while preserving byte counts");
}

async function runLocalContractSmoke() {
  const echoReadiness = classifyCodeAgentChatReadiness({
    conversationId: "cnv_code-agent-chat-smoke-echo",
    sessionId: "cnv_code-agent-chat-smoke-echo",
    messageId: "msg_code-agent-chat-smoke-echo",
    status: "completed",
    createdAt: "2026-05-22T00:00:00.000Z",
    updatedAt: "2026-05-22T00:00:00.000Z",
    traceId: "trc_code-agent-chat-smoke-echo",
    provider: "echo-mock",
    model: "mock-model",
    backend: "hwlab-cloud-api/echo-mock",
    reply: {
      messageId: "msg_code-agent-chat-smoke-echo",
      role: "assistant",
      content: "mock reply",
      createdAt: "2026-05-22T00:00:00.000Z"
    }
  }, { realDevLive: true, httpStatus: 200 });
  assert.equal(echoReadiness.status, "blocked");
  assert.equal(echoReadiness.level, "BLOCKED/untrusted-completion");
  assert.equal(echoReadiness.blocker, "untrusted-completion");
  assert.equal(echoReadiness.devLiveReplyPass, false);
  assert.match(echoReadiness.reason, /echo\/mock\/stub/u);
  logOk("echo/mock completion is not real DEV-LIVE pass");

  for (const [index, message] of [
    "列出可以使用的skill",
    "pwd",
    "ls .",
    "rg --files .",
    "请用cat读取 package.json"
  ].entries()) {
    const blocked = await handleCodeAgentChat(
      {
        conversationId: `cnv_code-agent-chat-blocked-${index}`,
        traceId: `trc_code-agent-chat-blocked-${index}`,
        message
      },
      {
        now: () => "2026-05-22T00:01:00.000Z",
        env: {
          PATH: "",
          HWLAB_CODE_AGENT_PROVIDER: "codex-cli",
          HWLAB_CODE_AGENT_MODEL: "gpt-test",
          HWLAB_CODE_AGENT_CODEX_COMMAND: "/tmp/hwlab-missing-codex"
        }
      }
    );
    validateCodeAgentChatSchema(blocked);
    assert.equal(blocked.status, "failed");
    assert.equal(blocked.provider, "codex-stdio");
    assert.equal(blocked.backend, "hwlab-cloud-api/codex-app-server-stdio");
    assert.equal(blocked.error.code, "codex_cli_binary_missing");
    assert.equal(Object.hasOwn(blocked, "reply"), false);
    assert.deepEqual(blocked.toolCalls, []);
    assert.ok(blocked.runnerLimitations.includes("no-controlled-readonly-fallback"));
    assert.ok(blocked.runnerLimitations.includes("no-text-fallback"));
    assert.equal(blocked.availability.ready, false);
    assert.equal(blocked.availability.partialReady, false);
    assert.equal(blocked.runnerTrace.traceId, `trc_code-agent-chat-blocked-${index}`);
    assert.ok(runnerTraceLabels(blocked).includes("request:accepted"));
    assert.ok(runnerTraceLabels(blocked).includes("codex-stdio:blocked"));
    assert.equal(blocked.runnerTrace.lastEvent.errorCode, "codex_cli_binary_missing");
    assert.equal(blocked.runnerTrace.lastEvent.waitingFor, "codex-stdio-readiness");
    assert.equal(JSON.stringify(blocked).includes("sk-"), false);
  }
  logOk("skills/pwd/ls/rg/cat prompts fail with Codex stdio blocker instead of local fallback");

  await runJsonLineClientApprovalSmoke();
  await runTurnStateVisibilitySmoke();

  const stdioWorkspaceRoot = await mkdtemp(path.join(os.tmpdir(), `hwlab-code-agent-stdio-smoke-${process.pid}-`));
  const stdioWorkspace = path.join(stdioWorkspaceRoot, "workspace");
  const stdioCodexHome = path.join(stdioWorkspaceRoot, "codex-home");
  const stdioSkillsDir = path.join(stdioWorkspaceRoot, "skills");
  await mkdir(stdioWorkspace, { recursive: true });
  await mkdir(path.join(stdioSkillsDir, "aaa-stdio-smoke-skill"), { recursive: true });
  await mkdir(stdioCodexHome, { recursive: true });
  await writeFile(path.join(stdioWorkspace, "README.md"), "stdio smoke workspace\n");
  await writeFile(path.join(stdioCodexHome, "config.toml"), "model = \"gpt-test\"\n");
  await writeFile(path.join(stdioCodexHome, "auth.json"), "{\"auth\":\"redacted-test-placeholder\"}\n");
  await writeFile(path.join(stdioSkillsDir, "aaa-stdio-smoke-skill", "SKILL.md"), [
    "---",
    "name: aaa-stdio-smoke-skill",
    "description: Stdio smoke skill summary.",
    "---",
    "",
    "# Stdio Smoke"
  ].join("\n"));
  let fakeCodex = null;
  try {
    fakeCodex = await createFakeCodexCommand();
    const rpcCalls = [];
    const stdioManager = createCodexStdioSessionManager({
      idFactory: () => "ses_code_agent_chat_smoke_stdio",
      createRpcClient: async () => createFakeAppServerClient({ rpcCalls })
    });
    const stdioEnv = {
      PATH: process.env.PATH,
      OPENAI_API_KEY: "test-openai-key-material",
      CODEX_HOME: stdioCodexHome,
      HWLAB_CODE_AGENT_PROVIDER: "codex-stdio",
      HWLAB_CODE_AGENT_MODEL: "gpt-test",
      HWLAB_CODE_AGENT_CODEX_COMMAND: fakeCodex.command,
      HWLAB_CODE_AGENT_CODEX_STDIO_ENABLED: "1",
      HWLAB_CODE_AGENT_CODEX_STDIO_SUPERVISOR: "repo-owned",
      HWLAB_CODE_AGENT_CODEX_WORKSPACE: stdioWorkspace,
      HWLAB_CODE_AGENT_CODEX_SANDBOX: "workspace-write",
      HWLAB_CODE_AGENT_SKILLS_DIRS: stdioSkillsDir,
      HWLAB_CODE_AGENT_SKILLS_STRICT: "1",
      HWLAB_CODE_AGENT_OPENAI_BASE_URL: "http://127.0.0.1:65535/v1/responses"
    };
    const stdioPwd = await handleCodeAgentChat(
      {
        conversationId: "cnv_code-agent-chat-stdio",
        traceId: "trc_code-agent-chat-stdio-pwd",
        message: "列出可以使用的skill"
      },
      {
        now: () => "2026-05-22T00:02:10.000Z",
        codexStdioManager: stdioManager,
        env: stdioEnv,
        skillsDirs: [stdioSkillsDir],
        skillsDirsExact: true
      }
    );
    validateCodeAgentChatSchema(stdioPwd);
    assert.equal(stdioPwd.status, "completed");
    assert.equal(stdioPwd.provider, "codex-stdio");
    assert.equal(stdioPwd.backend, "hwlab-cloud-api/codex-app-server-stdio");
    assert.equal(stdioPwd.runner.kind, "codex-app-server-stdio-runner");
    assert.equal(stdioPwd.sessionMode, "codex-app-server-stdio-long-lived");
    assert.equal(stdioPwd.implementationType, "repo-owned-codex-app-server-stdio-session");
    assert.equal(stdioPwd.capabilityLevel, "long-lived-codex-stdio-session");
    assert.equal(stdioPwd.workspace, stdioWorkspace);
    assert.equal(stdioPwd.sandbox, "workspace-write");
    assert.equal(stdioPwd.runner.codexStdio, true);
    assert.equal(stdioPwd.runner.writeCapable, true);
    assert.equal(stdioPwd.runner.durableSession, true);
    assert.equal(stdioPwd.session.longLivedSession, true);
    assert.equal(stdioPwd.session.codexStdio, true);
    assert.equal(stdioPwd.sessionReuse.reused, false);
    assert.equal(stdioPwd.sessionReuse.turn, 1);
    assert.equal(stdioPwd.codexStdioFeasibility.ready, true);
    assert.equal(stdioPwd.codexStdioFeasibility.runtimeContract.stdioProtocol.status, "wired");
    assert.equal(stdioPwd.codexStdioFeasibility.runtimeContract.lifecycleSupervisor.status, "present");
    assert.equal(stdioPwd.codexStdioFeasibility.runtimeContract.workspaceMount.status, "ready");
    assert.equal(stdioPwd.codexStdioFeasibility.runtimeContract.codexHome.status, "ready");
    assert.equal(stdioPwd.longLivedSessionGate.status, "pass");
    assert.equal(stdioPwd.longLivedSessionGate.pass, true);
    assert.equal(stdioPwd.providerTrace.toolName, "codex-app-server.thread/start+turn/start");
    assert.equal(stdioPwd.providerTrace.threadId, "thread_code_agent_chat_smoke_stdio");
    assert.equal(Object.hasOwn(stdioPwd.providerTrace, "sidecarOnly"), false);
    const firstTurnCalls = rpcCalls.filter((call) => call.method !== "initialize");
    assert.equal(firstTurnCalls[0].method, "thread/start");
    assert.equal(firstTurnCalls[1].method, "turn/start");
    assert.match(firstTurnCalls[1].args.prompt, /列出可以使用的skill/u);
    assert.ok(stdioPwd.toolCalls.some((call) => call.name === "codex-app-server.thread/start+turn/start" && call.status === "completed"));
    assert.ok(stdioPwd.toolCalls.some((call) => call.name === "skills.discover" && call.status === "completed"));
    assert.equal(stdioPwd.skills.status, "ready");
    assert.ok(stdioPwd.skills.items.some((skill) => skill.name === "aaa-stdio-smoke-skill"));
    assert.equal(stdioPwd.runnerTrace.runnerKind, "codex-app-server-stdio-runner");
    assert.equal(stdioPwd.runnerTrace.sessionMode, "codex-app-server-stdio-long-lived");
    assert.ok(runnerTraceLabels(stdioPwd).includes("session:created"));
    assert.ok(runnerTraceLabels(stdioPwd).includes("prompt:sent"));
    assert.ok(runnerTraceLabels(stdioPwd).includes("tool:codex-app-server.thread/start+turn/start:started"));
    assert.ok(runnerTraceLabels(stdioPwd).includes("tool:codex-app-server.thread/start+turn/start:output_chunk"));
    assert.ok(runnerTraceLabels(stdioPwd).includes("tool:codex-app-server.thread/start+turn/start:completed"));
    assert.ok(stdioPwd.runnerTrace.assistantStreams.some((stream) => stream.label === "assistant:stream" && stream.chunkCount >= 1));
    assert.ok(runnerTraceLabels(stdioPwd).includes("assistant:completed"));
    assert.equal(classifyCodexRunnerCapability(stdioPwd, { httpStatus: 200 }).capabilityPass, true);

    const stdioSecondTurn = await handleCodeAgentChat(
      {
        conversationId: "cnv_code-agent-chat-stdio",
        traceId: "trc_code-agent-chat-stdio-reuse",
        message: "继续复用同一个 session 并 ls ."
      },
      {
        now: () => "2026-05-22T00:02:11.000Z",
        codexStdioManager: stdioManager,
        env: stdioEnv,
        skillsDirs: [stdioSkillsDir],
        skillsDirsExact: true
      }
    );
    validateCodeAgentChatSchema(stdioSecondTurn);
    assert.equal(stdioSecondTurn.status, "completed");
    assert.equal(stdioSecondTurn.provider, "codex-stdio");
    assert.equal(stdioSecondTurn.sessionId, stdioPwd.sessionId);
    assert.equal(stdioSecondTurn.session.sessionId, stdioPwd.session.sessionId);
    assert.equal(stdioSecondTurn.session.threadId, "thread_code_agent_chat_smoke_stdio");
    assert.equal(stdioSecondTurn.sessionReuse.reused, true);
    assert.equal(stdioSecondTurn.sessionReuse.turn, 2);
    assert.equal(stdioSecondTurn.providerTrace.toolName, "codex-app-server.thread/resume+turn/start");
    assert.equal(Object.hasOwn(stdioSecondTurn.providerTrace, "sidecarOnly"), false);
    assert.ok(stdioSecondTurn.toolCalls.some((call) => call.name === "codex-app-server.thread/resume+turn/start" && call.status === "completed"));
    assert.ok(runnerTraceLabels(stdioSecondTurn).includes("session:reused"));
    assert.ok(runnerTraceLabels(stdioSecondTurn).includes("prompt:sent"));
    assert.ok(runnerTraceLabels(stdioSecondTurn).includes("tool:codex-app-server.thread/resume+turn/start:completed"));
    assert.equal(classifyCodexRunnerCapability(stdioSecondTurn, { httpStatus: 200 }).capabilityPass, true);
    assert.equal(JSON.stringify(stdioSecondTurn).includes("test-openai-key-material"), false);
    logOk("codex stdio runner passes long-lived workspace-write session capability");
  } finally {
    await rm(stdioWorkspaceRoot, { recursive: true, force: true });
    if (fakeCodex) {
      await rm(fakeCodex.root, { recursive: true, force: true });
    }
  }
  const completedCodexPayload = {
    conversationId: "cnv_code-agent-chat-stdio-http",
    sessionId: "ses_code-agent-chat-stdio-http",
    messageId: "msg_code-agent-chat-stdio-http",
    status: "completed",
    createdAt: "2026-05-22T00:05:00.000Z",
    updatedAt: "2026-05-22T00:05:00.000Z",
    traceId: "trc_code-agent-chat-stdio-http",
    provider: "codex-stdio",
    model: "gpt-5.5",
    backend: "hwlab-cloud-api/codex-app-server-stdio",
    runner: { kind: "codex-app-server-stdio-runner", codexStdio: true, writeCapable: true, durableSession: true },
    sessionMode: "codex-app-server-stdio-long-lived",
    implementationType: "repo-owned-codex-app-server-stdio-session",
    capabilityLevel: "long-lived-codex-stdio-session",
    reply: {
      messageId: "msg_code-agent-chat-stdio-http",
      role: "assistant",
      content: "real stdio reply",
      createdAt: "2026-05-22T00:05:00.000Z"
    }
  };
  for (const status of [400, 401, 403, 429, 500, 502, 503, 504]) {
    const readiness = classifyCodeAgentChatReadiness(completedCodexPayload, { realDevLive: true, httpStatus: status });
    assert.equal(readiness.status, "blocked", `HTTP ${status} must block`);
    assert.equal(readiness.blocker, "provider-upstream", `HTTP ${status} blocker`);
    assert.equal(readiness.providerStatus, status, `HTTP ${status} providerStatus`);
    assert.equal(readiness.devLiveReplyPass, false, `HTTP ${status} devLiveReplyPass`);
    assert.match(readiness.reason, /HTTP non-2xx|upstream response/u, `HTTP ${status} reason`);
  }
  logOk("non-2xx completion-looking payloads are blocked");

  const failedProvider401Payload = {
    ...completedCodexPayload,
    status: "failed",
    reply: undefined,
    error: {
      code: "codex_stdio_failed",
      providerStatus: 401,
      layer: "runner",
      message: "provider returned HTTP 401"
    },
    availability: {
      status: "codex-stdio-feasible",
      ready: true
    }
  };
  const provider401Readiness = classifyCodeAgentChatReadiness(failedProvider401Payload, { realDevLive: true, httpStatus: 200 });
  assert.equal(provider401Readiness.status, "blocked");
  assert.equal(provider401Readiness.level, "BLOCKED/provider");
  assert.equal(provider401Readiness.blocker, "provider-upstream");
  assert.equal(provider401Readiness.providerStatus, 401);
  assert.equal(provider401Readiness.devLiveReplyPass, false);
  logOk("providerStatus=401 blocks even when readiness fields look feasible");

  const providerEnv = {
    HWLAB_CODE_AGENT_OPENAI_BASE_URL: "http://127.0.0.1:49280/responses",
    OPENAI_API_KEY: "test-openai-key-material"
  };
  assert.equal(codexAppServerProviderBaseUrl(providerEnv), "http://127.0.0.1:49280");
  const providerArgs = codexAppServerArgs(providerEnv).join(" ");
  assert.match(providerArgs, /model_providers\.OpenAI\.base_url="http:\/\/127\.0\.0\.1:49280"/u);
  assert.match(providerArgs, /model_providers\.OpenAI\.name="OpenAI"/u);
  assert.match(providerArgs, /model_providers\.OpenAI\.wire_api="responses"/u);
  assert.match(providerArgs, /model_providers\.OpenAI\.requires_openai_auth=true/u);
  assert.equal(providerArgs.includes("test-openai-key-material"), false);
  logOk("Codex app-server provider override keeps auth material out of argv");

  process.stdout.write("[code-agent-chat-smoke] passed\n");
}

async function runLiveReadinessSmoke(args) {
  const endpoint = agentChatEndpoint(args.url);
  const traceId = protocolId("trc_code-agent-chat-live");
  const conversationId = protocolId("cnv_code-agent-chat-live");
  let httpStatus = null;
  let payload = null;
  let transportError = null;

  try {
    const response = await postJson(endpoint, {
      headers: {
        "x-trace-id": traceId
      },
      timeoutMs: args.timeoutMs,
      body: {
        conversationId,
        traceId,
        projectId: "prj_hwlab-cloud-workbench",
        message: args.message
      }
    });
    httpStatus = response.status;
    payload = response.json;
  } catch (error) {
    transportError = error instanceof Error ? error.message : String(error);
  }

  const readiness = payload || isHttpNon2xxStatus(httpStatus)
    ? classifyCodeAgentChatReadiness(payload, { realDevLive: true, httpStatus })
    : {
        status: "blocked",
        level: "BLOCKED",
        blocker: "transport",
        devLiveReplyPass: false,
        reason: "real DEV /v1/agent/chat did not return a JSON Code Agent payload"
      };

  if (payload && readiness.status === "pass") {
    try {
      validateCodeAgentChatSchema(payload);
    } catch (error) {
      readiness.status = "blocked";
      readiness.level = "BLOCKED";
      readiness.blocker = "schema";
      readiness.devLiveReplyPass = false;
      readiness.reason = error instanceof Error ? error.message : String(error);
    }
  }

  return {
    status: readiness.status,
    mode: "live-readiness",
    url: endpoint.href,
    httpStatus,
    readiness,
    response: summarizePayload(payload, { httpStatus, traceId }),
    ...(transportError ? { transportError } : {}),
    safety: {
      prodTouched: false,
      servicesRestarted: false,
      readsSecretValues: false,
      printsAssistantReply: false,
      statement: "This smoke posts only to the configured DEV Code Agent chat endpoint and prints reply presence, not reply content or secret values."
    }
  };
}

function summarizePayload(payload, options = {}) {
  const responseSummary = summarizeCodeAgentPayload(payload, options);
  const assistantReply = responseSummary?.hasReply === true && typeof payload?.reply?.content === "string"
    ? payload.reply.content.trim()
    : "";
  return {
    status: responseSummary?.status ?? null,
    provider: responseSummary?.provider ?? null,
    model: responseSummary?.model ?? null,
    backend: responseSummary?.backend ?? null,
    runner: responseSummary?.runner ?? null,
    workspace: responseSummary?.workspace ?? null,
    sandbox: responseSummary?.sandbox ?? null,
    capabilityLevel: responseSummary?.capabilityLevel ?? null,
    sessionMode: responseSummary?.sessionMode ?? null,
    sessionReuse: responseSummary?.sessionReuse ?? null,
    implementationType: responseSummary?.implementationType ?? null,
    runnerLimitations: responseSummary?.runnerLimitations ?? [],
    toolCalls: responseSummary?.toolCalls ?? [],
    skills: responseSummary?.skills ?? null,
    runnerTrace: responseSummary?.runnerTrace ?? null,
    codexStdioFeasibility: responseSummary?.codexStdioFeasibility ?? null,
    session: responseSummary?.session ?? null,
    longLivedSessionGate: responseSummary?.longLivedSessionGate ?? null,
    assistantReplyNonEmpty: responseSummary?.hasReply === true,
    assistantReplyLength: assistantReply.length,
    error: responseSummary?.error ?? null
  };
}

function parseArgs(argv) {
  const parsed = {
    mode: "local",
    url: defaultLiveUrl,
    message: defaultLiveMessage,
    timeoutMs: defaultLiveTimeoutMs
  };
  for (let index = 0; index < argv.length; index += 1) {
    const arg = argv[index];
    if (arg === "--live") {
      parsed.mode = "live";
    } else if (arg === "--url") {
      index += 1;
      if (!argv[index]) throw new Error("--url requires a value");
      parsed.url = argv[index];
    } else if (arg === "--message") {
      index += 1;
      if (!argv[index]) throw new Error("--message requires a value");
      parsed.message = argv[index];
    } else if (arg === "--timeout-ms") {
      index += 1;
      if (!argv[index]) throw new Error("--timeout-ms requires a value");
      const timeoutMs = Number.parseInt(argv[index], 10);
      if (!Number.isFinite(timeoutMs) || timeoutMs <= 0) {
        throw new Error("--timeout-ms must be a positive integer");
      }
      parsed.timeoutMs = timeoutMs;
    } else if (arg === "--help") {
      return { mode: "help" };
    } else {
      throw new Error(`unknown argument ${arg}`);
    }
  }
  return parsed;
}

function usage() {
  return {
    status: "usage",
    command: "node scripts/code-agent-chat-smoke.mjs [--live --url http://74.48.78.17:17667/ --timeout-ms 45000]",
    notes: [
      "Default mode is local schema/readiness contract only and cannot pass #143 DEV-LIVE.",
      "--live posts a minimal chat prompt to the current G14 DEV public endpoint and passes only on completed plus non-empty assistant reply.",
      "Live chat first token can exceed 10s on healthy Codex stdio cold turns; default timeout is 45000ms and can be overridden with --timeout-ms.",
      "provider_unavailable with missing OPENAI_API_KEY or missing/forbidden HWLAB_CODE_AGENT_OPENAI_BASE_URL is reported as BLOCKED/provider-config."
    ]
  };
}

async function createFakeCodexCommand() {
  const root = await mkdtemp(path.join(os.tmpdir(), "hwlab-fake-codex-"));
  const packageRoot = path.join(root, "node_modules", "@openai", "codex");
  const command = path.join(packageRoot, "bin", "codex.js");
  const native = path.join(root, "node_modules", "@openai", "codex-linux-x64", "vendor", "x86_64-unknown-linux-musl", "codex", "codex");
  await mkdir(path.dirname(command), { recursive: true });
  await mkdir(path.dirname(native), { recursive: true });
  await writeFile(path.join(packageRoot, "package.json"), JSON.stringify({ name: "@openai/codex", version: "0.128.0" }), "utf8");
  await writeFile(command, "#!/usr/bin/env node\nconsole.log('codex 0.128.0');\n", "utf8");
  await writeFile(native, "#!/bin/sh\nexit 0\n", "utf8");
  await chmod(command, 0o755);
  await chmod(native, 0o755);
  return { root, command };
}

async function createFakeJsonLineCodexCommand() {
  const root = await mkdtemp(path.join(os.tmpdir(), "hwlab-fake-jsonline-codex-"));
  const packageRoot = path.join(root, "node_modules", "@openai", "codex");
  const command = path.join(packageRoot, "bin", "codex.js");
  const native = path.join(root, "node_modules", "@openai", "codex-linux-x64", "vendor", "x86_64-unknown-linux-musl", "codex", "codex");
  await mkdir(path.dirname(command), { recursive: true });
  await mkdir(path.dirname(native), { recursive: true });
  await writeFile(path.join(packageRoot, "package.json"), JSON.stringify({ name: "@openai/codex", version: "0.128.0" }), "utf8");
  await writeFile(command, "#!/usr/bin/env node\nconsole.log('node-wrapper-should-not-run');\n", "utf8");
  await writeFile(native, [
    "#!/usr/bin/env node",
    "const readline = require('node:readline');",
    "let turnRequestId = null;",
    "let approvalDecision = null;",
    "const rl = readline.createInterface({ input: process.stdin });",
    "function send(payload) { process.stdout.write(JSON.stringify(payload) + '\\n'); }",
    "rl.on('line', (line) => {",
    "  if (!line.trim()) return;",
    "  const payload = JSON.parse(line);",
    "  if (payload.id === 'approval-test' && !payload.method) {",
    "    approvalDecision = payload.result && payload.result.decision;",
    "    send({ method: 'item/completed', params: { item: { id: 'cmd_jsonline_test', type: 'commandExecution', status: 'completed', exitCode: 0, command: 'echo approval', stdout: 'approval ok' } } });",
    "    send({ id: turnRequestId, result: { turn: { id: 'turn_jsonline_test' }, approvalDecision, spawnedFrom: 'native' } });",
    "    return;",
    "  }",
    "  if (payload.method === 'initialize') { send({ id: payload.id, result: { initialized: true } }); return; }",
    "  if (payload.method === 'initialized') return;",
    "  if (payload.method === 'thread/start') {",
    "    send({ method: 'thread/started', params: { thread: { id: 'thread_jsonline_test' } } });",
    "    send({ id: payload.id, result: { thread: { id: 'thread_jsonline_test' } } });",
    "    return;",
    "  }",
    "  if (payload.method === 'turn/start') {",
    "    turnRequestId = payload.id;",
    "    send({ method: 'turn/started', params: { turn: { id: 'turn_jsonline_test' } } });",
    "    send({ id: 'approval-test', method: 'item/commandExecution/requestApproval', params: { itemId: 'cmd_jsonline_test', availableDecisions: ['approved_for_session', 'decline'] } });",
    "    return;",
    "  }",
    "  send({ id: payload.id, error: { code: -32601, message: 'unknown method' } });",
    "});",
    "setTimeout(() => {}, 30000);"
  ].join("\n"), "utf8");
  await chmod(command, 0o755);
  await chmod(native, 0o755);
  return { root, command };
}

function createFakeAppServerClient({ rpcCalls }) {
  let notificationHandler = null;
  let turn = 0;
  return {
    async initialize() {
      rpcCalls.push({ method: "initialize" });
      return { initialized: true };
    },
    setNotificationHandler(handler) {
      notificationHandler = typeof handler === "function" ? handler : null;
    },
    async startThread(args) {
      rpcCalls.push({ method: "thread/start", args });
      notificationHandler?.({ method: "thread/started", params: { thread: { id: "thread_code_agent_chat_smoke_stdio" } } });
      return { threadId: "thread_code_agent_chat_smoke_stdio" };
    },
    async resumeThread(args) {
      rpcCalls.push({ method: "thread/resume", args });
      notificationHandler?.({ method: "thread/started", params: { thread: { id: args.threadId } } });
      return { threadId: args.threadId };
    },
    async startTurn(args) {
      rpcCalls.push({ method: "turn/start", args });
      turn += 1;
      const turnId = `turn_code_agent_chat_smoke_${turn}`;
      const text = `codex-app-server fixture reply for ${args.threadId ?? "new-thread"}`;
      notificationHandler?.({ method: "turn/started", params: { turn: { id: turnId } } });
      notificationHandler?.({ method: "item/agentMessage/delta", params: { itemId: `item_${turn}`, delta: text } });
      notificationHandler?.({ method: "item/completed", params: { item: { id: `item_${turn}`, type: "agentMessage", text } } });
      notificationHandler?.({ method: "turn/completed", params: { turn: { id: turnId, status: "completed" } } });
      return { turnId };
    },
    close() {}
  };
}

function agentChatEndpoint(value) {
  const url = new URL(value);
  if (url.pathname.endsWith("/v1/agent/chat")) return url;
  return new URL("/v1/agent/chat", url);
}

function postJson(url, { headers = {}, body, timeoutMs = 10000 }) {
  const client = url.protocol === "https:" ? https : http;
  const payload = JSON.stringify(body);
  return new Promise((resolve, reject) => {
    const request = client.request(
      url,
      {
        method: "POST",
        headers: {
          "content-type": "application/json",
          "content-length": Buffer.byteLength(payload),
          ...headers
        },
        insecureHTTPParser: true,
        timeout: timeoutMs
      },
      (response) => {
        const chunks = [];
        response.on("data", (chunk) => chunks.push(chunk));
        response.on("end", () => {
          const text = Buffer.concat(chunks).toString("utf8");
          resolve({
            status: response.statusCode ?? null,
            body: text,
            json: parseJsonOrNull(text)
          });
        });
      }
    );
    request.on("timeout", () => request.destroy(new Error(`request timed out after ${timeoutMs}ms`)));
    request.on("error", reject);
    request.end(payload);
  });
}

function parseJsonOrNull(value) {
  try {
    return JSON.parse(value);
  } catch {
    return null;
  }
}

function protocolId(prefix) {
  return `${prefix}_${Date.now().toString(36)}`;
}
