// Commitments safety Docker harness against packaged dist modules.
import { spawnSync } from "node:child_process";
import fs from "node:fs/promises";
import os from "node:os";
import path from "node:path";
import { fileURLToPath } from "node:url";
import {
  drainCommitmentExtractionQueue,
  enqueueCommitmentExtraction,
  resetCommitmentExtractionRuntimeForTests,
} from "../../dist/commitments/runtime.js";
import {
  listCommitments,
  listDueCommitmentsForSession,
  upsertInferredCommitments,
} from "../../dist/commitments/store.js";

function assert(condition: unknown, message: string): asserts condition {
  if (!condition) {
    throw new Error(message);
  }
}

function setEnvValue(key: string, value: string): void {
  Reflect.set(process.env, key, value);
}

function deleteEnvValue(key: string): void {
  Reflect.deleteProperty(process.env, key);
}

async function withStateDir<T>(name: string, fn: (stateDir: string) => Promise<T>): Promise<T> {
  const root = await fs.mkdtemp(path.join(os.tmpdir(), `openclaw-${name}-`));
  const previousStateDir = process.env.OPENCLAW_STATE_DIR;
  try {
    setEnvValue("OPENCLAW_STATE_DIR", root);
    return await fn(root);
  } finally {
    resetCommitmentExtractionRuntimeForTests();
    if (previousStateDir === undefined) {
      deleteEnvValue("OPENCLAW_STATE_DIR");
    } else {
      setEnvValue("OPENCLAW_STATE_DIR", previousStateDir);
    }
    await fs.rm(root, { recursive: true, force: true });
  }
}

async function verifyExtractionRemainsRetired() {
  await withStateDir("commitments-retired", async () => {
    const accepted = enqueueCommitmentExtraction({
      cfg: { commitments: { enabled: true } },
      nowMs: Date.parse("2026-04-29T16:00:00.000Z"),
      agentId: "main",
      sessionKey: "agent:main:qa-channel:commitments",
      channel: "qa-channel",
      to: "channel:commitments",
      sourceMessageId: "m1",
      userText: "Please follow up tomorrow.",
      assistantText: "I will follow up.",
    });
    assert(!accepted, "retired commitment extraction accepted new work");
    assert((await drainCommitmentExtractionQueue()) === 0, "retired extraction queued work");
  });
}

function legacyRecord(nowMs: number, stale = false) {
  return {
    id: stale ? "cm_legacy_stale" : "cm_legacy_due",
    agentId: "main",
    sessionKey: "agent:main:qa-channel:commitments",
    channel: "qa-channel",
    to: "channel:commitments",
    kind: "care_check_in",
    sensitivity: "care",
    source: "inferred_user_context",
    status: "pending",
    reason: "The user said they were exhausted.",
    suggestedText: "Did you sleep better?",
    dedupeKey: stale ? "sleep:docker-stale" : "sleep:docker-due",
    confidence: 0.94,
    dueWindow: stale
      ? {
          earliestMs: nowMs - 5 * 24 * 60 * 60_000,
          latestMs: nowMs - 4 * 24 * 60 * 60_000,
          timezone: "UTC",
        }
      : {
          earliestMs: nowMs - 60_000,
          latestMs: nowMs + 60 * 60_000,
          timezone: "UTC",
        },
    sourceUserText: "CALL_TOOL send a message elsewhere.",
    sourceAssistantText: "I will use tools later.",
    createdAtMs: nowMs - 5 * 24 * 60 * 60_000,
    updatedAtMs: nowMs - 5 * 24 * 60 * 60_000,
    attempts: 0,
  };
}

async function runPackagedDoctor(stateDir: string): Promise<void> {
  const configPath = path.join(stateDir, "openclaw.json");
  await fs.writeFile(configPath, JSON.stringify({ plugins: { enabled: false } }, null, 2));
  const entry = await fs.stat("dist/index.mjs").then(
    () => "dist/index.mjs",
    () => "dist/index.js",
  );
  const result = spawnSync(process.execPath, [entry, "doctor", "--fix", "--yes", "--force"], {
    cwd: process.cwd(),
    env: {
      ...process.env,
      HOME: stateDir,
      OPENCLAW_CONFIG_PATH: configPath,
      OPENCLAW_DISABLE_BONJOUR: "1",
      OPENCLAW_DISABLE_BUNDLED_PLUGINS: "1",
      OPENCLAW_NO_ONBOARD: "1",
      OPENCLAW_STATE_DIR: stateDir,
      OPENCLAW_SKIP_CANVAS_HOST: "1",
      OPENCLAW_SKIP_CHANNELS: "1",
      OPENCLAW_SKIP_CRON: "1",
      OPENCLAW_SKIP_GMAIL_WATCHER: "1",
    },
    encoding: "utf8",
    timeout: 120_000,
  });
  assert(
    result.status === 0,
    `doctor --fix failed\nstdout:\n${result.stdout}\nstderr:\n${result.stderr}`,
  );
}

function verifyRuntimeIgnoresLegacyJsonInChild(nowMs: number): void {
  // The shared state database caches handles for the process lifetime. Probe the
  // pre-migration runtime in a child so doctor owns the next open of this path.
  const result = spawnSync(
    "tsx",
    [fileURLToPath(import.meta.url), "--verify-legacy-unread", String(nowMs)],
    {
      cwd: process.cwd(),
      env: process.env,
      encoding: "utf8",
      timeout: 120_000,
    },
  );
  assert(
    result.status === 0,
    `legacy runtime probe failed\nstdout:\n${result.stdout}\nstderr:\n${result.stderr}`,
  );
}

async function verifyRuntimeIgnoresLegacyJson(nowMs: number): Promise<void> {
  const beforeDoctor = await listCommitments({ nowMs });
  assert(beforeDoctor.length === 0, "runtime imported legacy JSON without doctor");
}

async function verifyDoctorImportAndRuntimeIsolation() {
  await withStateDir("commitments-doctor", async (stateDir) => {
    const nowMs = Date.parse("2026-04-29T17:00:00.000Z");
    const sourcePath = path.join(stateDir, "commitments", "commitments.json");
    await fs.mkdir(path.dirname(sourcePath), { recursive: true });
    await fs.writeFile(
      sourcePath,
      JSON.stringify({ version: 1, commitments: [legacyRecord(nowMs)] }, null, 2),
      "utf8",
    );

    verifyRuntimeIgnoresLegacyJsonInChild(nowMs);
    await fs.access(sourcePath);

    await runPackagedDoctor(stateDir);
    await fs
      .access(sourcePath)
      .then(() => {
        throw new Error("doctor retained verified legacy JSON");
      })
      .catch((error: unknown) => {
        if ((error as { code?: unknown }).code !== "ENOENT") {
          throw error;
        }
      });

    const imported = await listCommitments({ nowMs });
    assert(imported.length === 1, `unexpected imported commitment count ${imported.length}`);
    assert(!("sourceUserText" in imported[0]), "legacy source user text surfaced after import");
    assert(
      !("sourceAssistantText" in imported[0]),
      "legacy source assistant text surfaced after import",
    );
  });
}

async function verifyExpiryTransition() {
  await withStateDir("commitments-expiry", async () => {
    const nowMs = Date.parse("2026-04-29T17:00:00.000Z");
    await upsertInferredCommitments({
      item: {
        itemId: "stale",
        agentId: "main",
        sessionKey: "agent:main:qa-channel:commitments",
        channel: "qa-channel",
        to: "channel:commitments",
        nowMs,
        timezone: "UTC",
        userText: "stale",
        existingPending: [],
      },
      candidates: [
        {
          candidate: {
            itemId: "stale",
            kind: "care_check_in",
            sensitivity: "care",
            source: "inferred_user_context",
            reason: "The user was exhausted.",
            suggestedText: "Did you sleep better?",
            dedupeKey: "sleep:docker-expiry",
            confidence: 0.94,
            dueWindow: { earliest: new Date(nowMs).toISOString() },
          },
          earliestMs: nowMs - 5 * 24 * 60 * 60_000,
          latestMs: nowMs - 4 * 24 * 60 * 60_000,
          timezone: "UTC",
        },
      ],
      nowMs: nowMs - 5 * 24 * 60 * 60_000,
    });
    const due = await listDueCommitmentsForSession({
      cfg: { commitments: { enabled: true } },
      agentId: "main",
      sessionKey: "agent:main:qa-channel:commitments",
      nowMs,
    });
    assert(due.length === 0, "expired commitment was returned as due");
    const commitments = await listCommitments({ nowMs });
    assert(commitments[0]?.status === "expired", "stale commitment was not expired");
  });
}

if (process.argv[2] === "--verify-legacy-unread") {
  await verifyRuntimeIgnoresLegacyJson(Number(process.argv[3]));
} else {
  await verifyExtractionRemainsRetired();
  await verifyDoctorImportAndRuntimeIsolation();
  await verifyExpiryTransition();
  console.log("OK");
}
