//go:build !windows

package proc

import (
	"fmt"
	"os/exec"
	"syscall"
	"testing"
	"time"
)

// TestUnixGroup_KillsChildProcess 验证整树清理：父 shell 派生一个后台 sleep
// 子进程并打印其 PID，TerminateGroup 后该子进程（孙级）也应被 kill(-pgid) 回收，
// 证明终止覆盖整个进程组而非仅组长。
func TestUnixGroup_KillsChildProcess(t *testing.T) {
	g := NewGroup()
	// `sleep 30 &` 后台子进程；`echo $!` 打印其 PID；`wait` 让 shell 阻塞存活。
	cmd := exec.Command("sh", "-c", "sleep 30 & echo $!; wait")
	g.Prepare(cmd)
	stdout, err := cmd.StdoutPipe()
	if err != nil {
		t.Fatalf("stdout pipe: %v", err)
	}
	if err := cmd.Start(); err != nil {
		t.Fatalf("start: %v", err)
	}
	if err := g.Adopt(cmd); err != nil {
		t.Fatalf("adopt: %v", err)
	}
	defer g.Close()

	var childPID int
	if _, err := fmt.Fscan(stdout, &childPID); err != nil {
		t.Fatalf("read child pid: %v", err)
	}
	if childPID <= 0 {
		t.Fatalf("invalid child pid: %d", childPID)
	}

	done := make(chan struct{})
	go func() {
		_ = cmd.Wait()
		close(done)
	}()

	g.TerminateGroup(done, 2*time.Second)
	select {
	case <-done:
	case <-time.After(10 * time.Second):
		t.Fatal("parent not terminated")
	}

	// 给内核一点时间回收子进程，然后用 kill(pid, 0) 探测：返回错误(ESRCH)=已死。
	deadline := time.Now().Add(5 * time.Second)
	for {
		if err := syscall.Kill(childPID, 0); err != nil {
			return // 子进程已死，符合预期。
		}
		if time.Now().After(deadline) {
			t.Fatalf("child process %d still alive after group terminate", childPID)
		}
		time.Sleep(100 * time.Millisecond)
	}
}
