#!/usr/bin/env node

import { execFileSync, spawnSync } from "node:child_process"
import { existsSync, mkdirSync, rmSync, writeFileSync } from "node:fs"
import { dirname, join, resolve } from "node:path"

const repoRoot = resolve(import.meta.dirname, "..", "..")
const tmpRoot = join(repoRoot, ".tmp", "workspace-agent-runner-smoke")
const compileDir = join(tmpRoot, "compiled")

let failures = 0

function check(label, condition, detail) {
  if (condition) {
    console.log(`  PASS: ${label}`)
  } else {
    console.error(`  FAIL: ${label}${detail ? ` - ${detail}` : ""}`)
    failures++
  }
}

function emitSummary() {
  console.log("")
  if (failures === 0) {
    console.log("workspace-agent-runner-smoke: ALL PASSED")
  } else {
    console.error(`workspace-agent-runner-smoke: ${failures} FAILURE(S)`)
    process.exit(1)
  }
}

function writePackageFile(packageRoot, relativePath, content) {
  const filePath = join(packageRoot, relativePath)
  mkdirSync(dirname(filePath), { recursive: true })
  writeFileSync(filePath, content.trimStart(), "utf8")
}

function preparePackage(caseRoot, files) {
  const packageRoot = join(caseRoot, "openclaw-package")
  mkdirSync(join(packageRoot, "dist"), { recursive: true })
  mkdirSync(join(packageRoot, "node_modules"), { recursive: true })
  writePackageFile(packageRoot, "package.json", JSON.stringify({ type: "module" }, null, 2))
  for (const [relativePath, content] of Object.entries(files)) {
    writePackageFile(packageRoot, relativePath, content)
  }
  return packageRoot
}

function parseEvents(stdout) {
  return stdout
    .split(/\r?\n/)
    .filter((line) => line.startsWith("QJC_WORKSPACE_EVENT\t"))
    .map((line) => JSON.parse(line.slice("QJC_WORKSPACE_EVENT\t".length)))
}

function defaultAttachments(projectRoot, name) {
  return [{
    id: `${name}-image`,
    kind: "image",
    name: "sample.png",
    projectPath: join(projectRoot, "inputs", "images", "main", "sample.png"),
    relativeProjectPath: "inputs/images/main/sample.png",
    sizeBytes: 12,
    mimeType: "image/png",
  }]
}

function multiKindAttachments(projectRoot, name) {
  return [
    ...defaultAttachments(projectRoot, name),
    {
      id: `${name}-pdf`,
      kind: "pdf",
      name: "brief.pdf",
      projectPath: join(projectRoot, "inputs", "documents", "brief.pdf"),
      relativeProjectPath: "inputs/documents/brief.pdf",
      sizeBytes: 128,
      mimeType: "application/pdf",
    },
    {
      id: `${name}-xlsx`,
      kind: "spreadsheet",
      name: "metrics.xlsx",
      projectPath: join(projectRoot, "inputs", "spreadsheets", "metrics.xlsx"),
      relativeProjectPath: "inputs/spreadsheets/metrics.xlsx",
      sizeBytes: 256,
      mimeType: "application/vnd.openxmlformats-officedocument.spreadsheetml.sheet",
    },
    {
      id: `${name}-ppt`,
      kind: "presentation",
      name: "deck.pptx",
      projectPath: join(projectRoot, "inputs", "presentations", "deck.pptx"),
      relativeProjectPath: "inputs/presentations/deck.pptx",
      sizeBytes: 512,
      mimeType: "application/vnd.openxmlformats-officedocument.presentationml.presentation",
    },
    {
      id: `${name}-mp3`,
      kind: "audio",
      name: "clip.mp3",
      projectPath: join(projectRoot, "inputs", "audio", "clip.mp3"),
      relativeProjectPath: "inputs/audio/clip.mp3",
      sizeBytes: 1024,
      mimeType: "audio/mpeg",
    },
  ]
}

function runCase(compiledRunner, name, files, expectedContentPart, options = {}) {
  const caseRoot = join(tmpRoot, name)
  const projectRoot = join(caseRoot, "project")
  const instrumentationDir = join(caseRoot, "instrumented")
  rmSync(caseRoot, { recursive: true, force: true })
  mkdirSync(projectRoot, { recursive: true })

  const vendorPackageDir = preparePackage(caseRoot, files)
  const attachments = options.attachments?.(projectRoot, name) || defaultAttachments(projectRoot, name)
  const expectedAttachmentPaths = options.expectedAttachmentPaths || attachments.map((attachment) => attachment.relativeProjectPath)
  const result = spawnSync(process.execPath, [compiledRunner], {
    cwd: repoRoot,
    encoding: "utf8",
    input: JSON.stringify({
      vendorPackageDir,
      instrumentationDir,
      projectRoot,
      sessionId: `${name}-session`,
      prompt: "Analyze this image",
      attachments,
      runId: `${name}-run`,
    }),
  })

  const events = parseEvents(result.stdout)
  const errorEvent = events.find((event) => event.type === "error")
  const completedEvent = events.find((event) => event.type === "completed")
  const deltaEvents = events.filter((event) => event.type === "delta")

  check(`${name}: runner exited successfully`, result.status === 0, errorEvent?.message || result.stderr || `exit ${result.status}`)
  check(`${name}: emitted delta`, deltaEvents.length > 0, JSON.stringify(events))
  check(`${name}: completed`, Boolean(completedEvent), JSON.stringify(events))
  check(`${name}: reply content`, completedEvent?.content?.includes(expectedContentPart), completedEvent?.content)
  for (const relativePath of expectedAttachmentPaths) {
    check(`${name}: attachment prelude includes ${relativePath}`, completedEvent?.content?.includes(relativePath), completedEvent?.content)
  }
}

async function main() {
  console.log("workspace-agent-runner-smoke: validating OpenClaw runner ingress options")

  rmSync(tmpRoot, { recursive: true, force: true })
  mkdirSync(compileDir, { recursive: true })

  execFileSync("corepack", [
    "pnpm",
    "--dir",
    join(repoRoot, "apps", "desktop"),
    "exec",
    "tsup",
    "src/main/project-workspace-agent-runner.ts",
    "--no-config",
    "--format",
    "cjs",
    "--platform",
    "node",
    "--target",
    "node20",
    "--out-dir",
    compileDir,
  ], {
    cwd: repoRoot,
    stdio: "inherit",
  })

  const compiledRunner = join(compileDir, "project-workspace-agent-runner.js")
  check("compiled workspace agent runner", existsSync(compiledRunner), `not found: ${compiledRunner}`)
  if (!existsSync(compiledRunner)) {
    emitSummary()
    return
  }

  const strictIngressOptionsSource = `
function assertIngressOptions(options) {
  if (typeof options.senderIsOwner !== "boolean") {
    throw new Error("senderIsOwner must be a boolean");
  }
  if (options.senderIsOwner !== true) {
    throw new Error("senderIsOwner must be true");
  }
  if (typeof options.allowModelOverride !== "boolean") {
    throw new Error("allowModelOverride must be a boolean");
  }
  if (options.allowModelOverride !== true) {
    throw new Error("allowModelOverride must be true");
  }
}
`

  runCase(compiledRunner, "ingress-options", {
    "dist/agent-legacy.js": `
import { emitAgentEvent, legacyModelMarker } from "./model-selection-legacy.js";
${strictIngressOptionsSource}
async function t(options) {
  assertIngressOptions(options);
  emitAgentEvent({ runId: options.runId, stream: "assistant", data: { delta: "leg" } });
  return { payloads: [{ text: "legacy:" + legacyModelMarker() + ":" + options.message }] };
}
export { t };
`,
    "dist/model-selection-legacy.js": `
const listeners = new Set();
function emitAgentEvent(event) {
  for (const listener of listeners) {
    listener(event);
  }
}
function onAgentEvent(listener) {
  listeners.add(listener);
  return () => listeners.delete(listener);
}
function legacyModelMarker() {
  return "direct-model-selection";
}
export { emitAgentEvent, legacyModelMarker, onAgentEvent };
`,
  }, "legacy:direct-model-selection")

  runCase(compiledRunner, "multi-attachment-prelude", {
    "dist/agent-multi.js": `
import { emitAgentEvent, legacyModelMarker } from "./model-selection-multi.js";
${strictIngressOptionsSource}
async function t(options) {
  assertIngressOptions(options);
  emitAgentEvent({ runId: options.runId, stream: "assistant", data: { delta: "mul" } });
  return { payloads: [{ text: "multi:" + legacyModelMarker() + ":" + options.message }] };
}
export { t };
`,
    "dist/model-selection-multi.js": `
const listeners = new Set();
function emitAgentEvent(event) {
  for (const listener of listeners) {
    listener(event);
  }
}
function onAgentEvent(listener) {
  listeners.add(listener);
  return () => listeners.delete(listener);
}
function legacyModelMarker() {
  return "direct-model-selection";
}
export { emitAgentEvent, legacyModelMarker, onAgentEvent };
`,
  }, "multi:direct-model-selection", {
    attachments: multiKindAttachments,
    expectedAttachmentPaths: [
      "inputs/images/main/sample.png",
      "inputs/documents/brief.pdf",
      "inputs/spreadsheets/metrics.xlsx",
      "inputs/presentations/deck.pptx",
      "inputs/audio/clip.mp3",
    ],
  })

  rmSync(tmpRoot, { recursive: true, force: true })
  emitSummary()
}

main().catch((error) => {
  console.error(error instanceof Error ? error.stack || error.message : String(error))
  process.exit(1)
})
