import { describe, expect, test } from "bun:test"
import { readFile } from "node:fs/promises"

import { runFile, cleanupTmpDir } from "./run.ts"
import type { ShoutFile } from "./parse.ts"

function makeFile(commands: { command: string; expected?: string[] }[]): ShoutFile {
  return {
    path: "test.shout",
    directives: [],
    commands: commands.map((c, i) => ({
      line: i + 1,
      raw: `$ ${c.command}`,
      command: c.command,
      expected: c.expected ?? [],
      exitCode: null,
    })),
  }
}

const defaultOpts = {
  cleanEnv: false,
  timeout: 5000,
  verbose: false,
}

async function isProcessRunning(pid: number): Promise<boolean> {
  try {
    const stat = await readFile(`/proc/${pid}/stat`, "utf-8")
    // Find state after the comm field (which is wrapped in parens and may contain spaces)
    const state = stat.charAt(stat.lastIndexOf(")") + 2)
    return state !== "Z" && state !== "X"
  } catch {
    return false
  }
}

describe("runFile", () => {
  test("basic command", async () => {
    const file = makeFile([{ command: "echo hello" }])
    const result = await runFile(file, defaultOpts)
    try {
      expect(result.results[0]?.actual).toEqual(["hello"])
      expect(result.results[0]?.exitCode).toBe(0)
    } finally {
      await cleanupTmpDir(result.tmpDir)
    }
  })

  test("cleans up backgrounded processes after exit", async () => {
    const file = makeFile([
      { command: "sleep 300 >/dev/null 2>&1 & echo $!" },
    ])
    const result = await runFile(file, defaultOpts)
    try {
      const pid = parseInt(result.results[0]?.actual[0] ?? "", 10)
      expect(pid).toBeGreaterThan(0)
      await new Promise(r => setTimeout(r, 100))
      expect(await isProcessRunning(pid)).toBe(false)
    } finally {
      await cleanupTmpDir(result.tmpDir)
    }
  })

  test("strips ANSI color codes from output", async () => {
    const file = makeFile([
      { command: `printf '\\033[31mred\\033[0m and \\033[1;32mbold green\\033[0m'` },
    ])
    const result = await runFile(file, defaultOpts)
    try {
      expect(result.results[0]?.actual).toEqual(["red and bold green"])
      expect(result.results[0]?.exitCode).toBe(0)
    } finally {
      await cleanupTmpDir(result.tmpDir)
    }
  })

  test("background process output does not leak into subsequent commands", async () => {
    const file = makeFile([
      // Start a background process that writes to stdout after a delay
      { command: "{ sleep 0.1; echo LEAKED; } &" },
      // Wait long enough for the background output to appear
      { command: "sleep 0.3; echo clean" },
    ])
    const result = await runFile(file, defaultOpts)
    try {
      // The background command itself should have no output
      expect(result.results[0]?.actual).toEqual([])
      // The sleep command should only see its own output, not the background "LEAKED"
      expect(result.results[1]?.actual).toEqual(["clean"])
    } finally {
      await cleanupTmpDir(result.tmpDir)
    }
  })

  test("cleans up multiple backgrounded processes", async () => {
    const file = makeFile([
      { command: "sleep 300 >/dev/null 2>&1 & P1=$!; sleep 300 >/dev/null 2>&1 & P2=$!; echo $P1 $P2" },
    ])
    const result = await runFile(file, defaultOpts)
    try {
      const pids = (result.results[0]?.actual[0] ?? "").split(" ").map(Number)
      expect(pids).toHaveLength(2)
      expect(pids[0]).toBeGreaterThan(0)
      expect(pids[1]).toBeGreaterThan(0)
      await new Promise(r => setTimeout(r, 100))
      for (const pid of pids) {
        expect(await isProcessRunning(pid)).toBe(false)
      }
    } finally {
      await cleanupTmpDir(result.tmpDir)
    }
  })
})
