import { spawnSync } from "node:child_process";
import { mkdtempSync, readFileSync, rmSync } from "node:fs";
import { tmpdir } from "node:os";
import path from "node:path";
import { afterEach, describe, expect, it } from "vitest";

const tempDirs: string[] = [];
const scriptPath = "scripts/e2e/lib/plugin-lifecycle-matrix/measure.mjs";
const hasTimeoutCommand =
  process.platform === "linux" &&
  spawnSync("bash", ["-lc", "command -v timeout >/dev/null 2>&1"]).status === 0;

function makeTempDir(): string {
  const dir = mkdtempSync(path.join(tmpdir(), "openclaw-plugin-lifecycle-measure-"));
  tempDirs.push(dir);
  return dir;
}

function pidExists(pid: number): boolean {
  try {
    process.kill(pid, 0);
    return true;
  } catch {
    return false;
  }
}

function waitForPidExit(pid: number, timeoutMs: number): boolean {
  const waitBuffer = new SharedArrayBuffer(4);
  const waitView = new Int32Array(waitBuffer);
  const deadline = Date.now() + timeoutMs;
  while (Date.now() < deadline) {
    if (!pidExists(pid)) {
      return true;
    }
    Atomics.wait(waitView, 0, 0, 25);
  }
  return !pidExists(pid);
}

afterEach(() => {
  for (const dir of tempDirs.splice(0)) {
    rmSync(dir, { recursive: true, force: true });
  }
});

describe("plugin lifecycle resource sampler", () => {
  it("rejects loose numeric env values instead of parsing prefixes", () => {
    const dir = makeTempDir();
    const summary = path.join(dir, "summary.tsv");
    const result = spawnSync("node", [scriptPath, summary, "invalid-env", "--", "node", "-e", ""], {
      cwd: process.cwd(),
      encoding: "utf8",
      env: {
        ...process.env,
        OPENCLAW_PLUGIN_LIFECYCLE_PHASE_TIMEOUT_MS: "150ms",
      },
      timeout: 5000,
    });

    expect(result.status).not.toBe(0);
    expect(result.stderr).toContain(
      "OPENCLAW_PLUGIN_LIFECYCLE_PHASE_TIMEOUT_MS must be a positive integer; got: 150ms",
    );
  });

  it("rejects zero lifecycle timeouts instead of disabling the guard", () => {
    const dir = makeTempDir();
    const summary = path.join(dir, "summary.tsv");
    const result = spawnSync("node", [scriptPath, summary, "invalid-env", "--", "node", "-e", ""], {
      cwd: process.cwd(),
      encoding: "utf8",
      env: {
        ...process.env,
        OPENCLAW_PLUGIN_LIFECYCLE_PHASE_TIMEOUT_MS: "0",
      },
      timeout: 5000,
    });

    expect(result.status).not.toBe(0);
    expect(result.stderr).toContain(
      "OPENCLAW_PLUGIN_LIFECYCLE_PHASE_TIMEOUT_MS must be a positive integer; got: 0",
    );
  });

  it("configures a phase timeout with process-group cleanup", () => {
    const script = readFileSync(scriptPath, "utf8");

    expect(script).toContain("OPENCLAW_PLUGIN_LIFECYCLE_PHASE_TIMEOUT_MS");
    expect(script).toContain("OPENCLAW_PLUGIN_LIFECYCLE_TIMEOUT_KILL_GRACE_MS");
    expect(script).toContain("detached: true");
    expect(script).toContain("process.kill(-child.pid, signal)");
    expect(script).toContain('const summarySignal = timedOut ? "timeout"');
    expect(script).toContain("process.exit(124)");
  });

  it.runIf(process.platform === "linux")(
    "times out wedged phases and records the timeout signal",
    () => {
      const dir = makeTempDir();
      const summary = path.join(dir, "summary.tsv");
      const result = spawnSync(
        "node",
        [scriptPath, summary, "wedged", "--", "node", "-e", "setInterval(() => {}, 1000)"],
        {
          cwd: process.cwd(),
          encoding: "utf8",
          env: {
            ...process.env,
            OPENCLAW_PLUGIN_LIFECYCLE_PHASE_TIMEOUT_MS: "150",
            OPENCLAW_PLUGIN_LIFECYCLE_TIMEOUT_KILL_GRACE_MS: "50",
          },
          timeout: 5000,
        },
      );

      expect(result.status).toBe(124);
      expect(result.stdout).toContain("signal=timeout");
      expect(readFileSync(summary, "utf8")).toMatch(
        /^wedged\t\d+\t[\d.]+\t\d+\t[\d.]+\ttimeout$/mu,
      );
    },
  );

  it.runIf(process.platform === "linux")(
    "kills stubborn descendants after the timeout grace period",
    () => {
      const dir = makeTempDir();
      const summary = path.join(dir, "summary.tsv");
      const pidFile = path.join(dir, "descendant.pid");
      let descendantPid = 0;

      try {
        const result = spawnSync(
          "node",
          [
            scriptPath,
            summary,
            "stubborn-descendant",
            "--",
            "bash",
            "-lc",
            'bash -c \'trap "" TERM; printf "%s\\n" "$$" >"$PID_FILE"; while :; do sleep 1; done\' & wait',
          ],
          {
            cwd: process.cwd(),
            encoding: "utf8",
            env: {
              ...process.env,
              OPENCLAW_PLUGIN_LIFECYCLE_PHASE_TIMEOUT_MS: "150",
              OPENCLAW_PLUGIN_LIFECYCLE_TIMEOUT_KILL_GRACE_MS: "100",
              PID_FILE: pidFile,
            },
            timeout: 5000,
          },
        );

        descendantPid = Number.parseInt(readFileSync(pidFile, "utf8"), 10);
        expect(result.status).toBe(124);
        expect(result.stdout).toContain("signal=timeout");
        expect(readFileSync(summary, "utf8")).toMatch(
          /^stubborn-descendant\t\d+\t[\d.]+\t\d+\t[\d.]+\ttimeout$/mu,
        );
        expect(waitForPidExit(descendantPid, 1000)).toBe(true);
      } finally {
        if (descendantPid > 0 && pidExists(descendantPid)) {
          process.kill(descendantPid, "SIGKILL");
        }
      }
    },
  );

  it.runIf(hasTimeoutCommand)("forwards external termination to the measured process group", () => {
    const dir = makeTempDir();
    const summary = path.join(dir, "summary.tsv");
    const pidFile = path.join(dir, "descendant.pid");
    let descendantPid = 0;

    try {
      const result = spawnSync(
        "timeout",
        [
          "--kill-after=1s",
          "0.2s",
          "node",
          scriptPath,
          summary,
          "external-stop",
          "--",
          "bash",
          "-lc",
          'bash -c \'trap "" TERM; printf "%s\\n" "$$" >"$PID_FILE"; while :; do sleep 1; done\' & wait',
        ],
        {
          cwd: process.cwd(),
          encoding: "utf8",
          env: {
            ...process.env,
            OPENCLAW_PLUGIN_LIFECYCLE_PHASE_TIMEOUT_MS: "5000",
            OPENCLAW_PLUGIN_LIFECYCLE_TIMEOUT_KILL_GRACE_MS: "100",
            PID_FILE: pidFile,
          },
          timeout: 5000,
        },
      );

      descendantPid = Number.parseInt(readFileSync(pidFile, "utf8"), 10);
      expect(result.status).toBe(124);
      expect(waitForPidExit(descendantPid, 1000)).toBe(true);
    } finally {
      if (descendantPid > 0 && pidExists(descendantPid)) {
        process.kill(descendantPid, "SIGKILL");
      }
    }
  });
});
