/**
 * Runs startGmailWatcher and stopGmailWatcher with NO mocks for spawn or
 * killProcessTree. A fake `gog` binary is placed on PATH; it spawns a
 * credential-helper child and deliberately does NOT kill it on SIGTERM,
 * simulating the real bug. The test asserts that stopGmailWatcher removes
 * both the gog process and its descendant via the process-group signal.
 */
import { chmodSync, existsSync, mkdtempSync, readFileSync, rmSync, writeFileSync } from "node:fs";
import { tmpdir } from "node:os";
import { join } from "node:path";
import { afterAll, beforeAll, describe, expect, it } from "vitest";

const describePosix = process.platform === "win32" ? describe.skip : describe;

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

describePosix("gmail-watcher process-tree shutdown (integration)", () => {
  let tmpDir: string;
  let savedPath: string | undefined;
  let gogPid: number | undefined;
  let helperPid: number | undefined;
  let stopWatcher: (() => Promise<void>) | undefined;

  beforeAll(() => {
    tmpDir = mkdtempSync(join(tmpdir(), "openclaw-gog-integration-"));

    // fake gog: handles `watch start` (exits 0) and `watch serve`
    // (spawns a credential-helper that ignores SIGTERM)
    const gogScript = join(tmpDir, "gog");
    writeFileSync(
      gogScript,
      [
        "#!/bin/bash",
        'SCRIPT_DIR="$(cd -- "$(dirname -- "$0")" && pwd)"',
        'case "$*" in',
        '  *"watch start"*) echo "[gog] watch registered"; exit 0 ;;',
        '  *"watch serve"*)',
        '    echo "[gog] serve started pid=$$"',
        '    echo $$ > "$SCRIPT_DIR/gog.pid"',
        `    /bin/bash -c 'trap "" TERM; while true; do sleep 1; done' &`,
        "    HELPER=$!",
        '    echo "[gog] credential-helper spawned pid=$HELPER"',
        '    echo $HELPER > "$SCRIPT_DIR/helper.pid"',
        "    trap 'echo \"[gog] SIGTERM (child NOT killed)\"; exit 0' TERM",
        "    while true; do sleep 0.3; done ;;",
        "esac",
      ].join("\n"),
    );
    chmodSync(gogScript, 0o755);

    savedPath = process.env["PATH"];
    process.env["PATH"] = `${tmpDir}:${savedPath ?? ""}`;
  });

  afterAll(async () => {
    try {
      await stopWatcher?.();
    } catch {
      // Force cleanup below; this test must not leave subprocesses behind on failure.
    }
    if (gogPid !== undefined) {
      try {
        process.kill(-gogPid, "SIGKILL");
      } catch {
        // The process group may already be gone.
      }
    }
    if (helperPid !== undefined) {
      try {
        process.kill(helperPid, "SIGKILL");
      } catch {
        // The helper may already be gone with its process group.
      }
    }
    if (savedPath !== undefined) {
      process.env["PATH"] = savedPath;
    }
    rmSync(tmpDir, { recursive: true, force: true });
  });

  it("stopGmailWatcher removes gog and its credential-helper descendant", async () => {
    const { startGmailWatcher, stopGmailWatcher } = await import("./gmail-watcher.js");
    stopWatcher = stopGmailWatcher;

    const result = await startGmailWatcher({
      hooks: {
        enabled: true,
        token: "integration-token",
        gmail: {
          account: "integration@example.com",
          topic: "projects/integration/topics/gmail",
          pushToken: "integration-push-token",
        },
      },
    } as never);

    expect(result.started).toBe(true);

    // Wait for both pid files
    for (let i = 0; i < 50; i++) {
      if (existsSync(join(tmpDir, "helper.pid")) && existsSync(join(tmpDir, "gog.pid"))) {
        break;
      }
      await new Promise<void>((r) => {
        setTimeout(r, 100);
      });
    }

    gogPid = Number.parseInt(readFileSync(join(tmpDir, "gog.pid"), "utf8").trim(), 10);
    helperPid = Number.parseInt(readFileSync(join(tmpDir, "helper.pid"), "utf8").trim(), 10);

    console.log(`\ngog pid=${gogPid}, credential-helper pid=${helperPid}`);
    expect(alive(gogPid)).toBe(true);
    expect(alive(helperPid)).toBe(true);

    console.log("calling stopGmailWatcher...");
    await stopGmailWatcher();
    await new Promise<void>((r) => {
      setTimeout(r, 400);
    });

    console.log(`gog alive after stop: ${alive(gogPid)}`);
    console.log(`credential-helper alive after stop: ${alive(helperPid)}`);

    expect(alive(gogPid)).toBe(false);
    expect(alive(helperPid)).toBe(false); // descendant must also be gone
  }, 15_000);
});
