package proc

import (
	"context"
	"fmt"
	"runtime"
	"strings"
	"testing"
	"time"
)

// helperShell 返回执行一段 shell 脚本的跨平台命令前缀（command, args...）。
func helperShell(script string) (string, []string) {
	if runtime.GOOS == "windows" {
		return "cmd", []string{"/c", script}
	}
	return "sh", []string{"-c", script}
}

// TestRunOnce_ExitCodeAndCapture 验证捕获 stdout / stderr 与非零退出码。
func TestRunOnce_ExitCodeAndCapture(t *testing.T) {
	var script string
	if runtime.GOOS == "windows" {
		script = "echo out& echo err 1>&2& exit 3"
	} else {
		script = "echo out; echo err 1>&2; exit 3"
	}
	cmd, args := helperShell(script)
	res, err := RunOnce(context.Background(), ExecSpec{Command: cmd, Args: args, Timeout: 10 * time.Second})
	if err != nil {
		t.Fatalf("RunOnce: %v", err)
	}
	if res.ExitCode != 3 {
		t.Fatalf("exit code = %d, want 3", res.ExitCode)
	}
	if !strings.Contains(res.Stdout, "out") {
		t.Fatalf("stdout = %q, want contains 'out'", res.Stdout)
	}
	if !strings.Contains(res.Stderr, "err") {
		t.Fatalf("stderr = %q, want contains 'err'", res.Stderr)
	}
	if res.TimedOut {
		t.Fatal("TimedOut should be false")
	}
}

// TestRunOnce_Success 验证零退出码路径。
func TestRunOnce_Success(t *testing.T) {
	cmd, args := helperShell("echo hello")
	res, err := RunOnce(context.Background(), ExecSpec{Command: cmd, Args: args, Timeout: 10 * time.Second})
	if err != nil {
		t.Fatalf("RunOnce: %v", err)
	}
	if res.ExitCode != 0 {
		t.Fatalf("exit code = %d, want 0", res.ExitCode)
	}
	if !strings.Contains(res.Stdout, "hello") {
		t.Fatalf("stdout = %q", res.Stdout)
	}
}

// TestRunOnce_Truncation 验证 stdout 超过 MaxOutputBytes 时截断并置标志。
func TestRunOnce_Truncation(t *testing.T) {
	// 打印约 1000 字节，但上限设为 100。
	var cmd string
	var args []string
	if runtime.GOOS == "windows" {
		// PowerShell 直接调用避免 cmd 转义；-NoProfile 加快启动。
		cmd, args = "powershell", []string{"-NoProfile", "-Command", "Write-Output ('a' * 1000)"}
	} else {
		cmd, args = "sh", []string{"-c", "for i in $(seq 1 1000); do printf a; done"}
	}
	res, err := RunOnce(context.Background(), ExecSpec{Command: cmd, Args: args, MaxOutputBytes: 100, Timeout: 15 * time.Second})
	if err != nil {
		t.Fatalf("RunOnce: %v", err)
	}
	if !res.Truncated {
		t.Fatalf("Truncated should be true; stdout len=%d size=%d", len(res.Stdout), res.StdoutSize)
	}
	if len(res.Stdout) > 100 {
		t.Fatalf("captured stdout len=%d exceeds limit 100", len(res.Stdout))
	}
}

// TestRunOnce_Timeout 验证超时触发整树终止并置 TimedOut。
//
// Unix：父 shell trap 掉 TERM 并 sleep，再后台派生一个 grandchild sleep；超时后
// TerminateGroup 必须 SIGKILL 整组，确保父退出（done 被关闭）。
// Windows：直接 timeout 长睡，Job-close 终止。
func TestRunOnce_Timeout(t *testing.T) {
	var cmd string
	var args []string
	if runtime.GOOS == "windows" {
		// ping -n N localhost 是无需控制台的可靠延时（timeout /t 在无 stdin 下立即返回）。
		cmd, args = "cmd", []string{"/c", "ping -n 30 127.0.0.1 >nul"}
	} else {
		// trap '' TERM 忽略 SIGTERM；后台派生 grandchild；wait 阻塞存活。
		cmd, args = "sh", []string{"-c", "trap '' TERM; sleep 30 & sleep 30; wait"}
	}
	start := time.Now()
	res, err := RunOnce(context.Background(), ExecSpec{
		Command:   cmd,
		Args:      args,
		Timeout:   1 * time.Second,
		KillGrace: 500 * time.Millisecond,
	})
	if err != nil {
		t.Fatalf("RunOnce: %v", err)
	}
	if !res.TimedOut {
		t.Fatal("TimedOut should be true")
	}
	// 必须在远小于 30s 内返回（超时 + grace + kill），证明整树被回收而非等自然退出。
	if elapsed := time.Since(start); elapsed > 10*time.Second {
		t.Fatalf("RunOnce blocked %v, expected prompt tree-kill", elapsed)
	}
}

// TestRunOnce_CtxCancel 验证 ctx 取消亦触发树终止并尽快返回。
func TestRunOnce_CtxCancel(t *testing.T) {
	cmd, args := helperShell(fmt.Sprintf("%s", sleepScript(30)))
	ctx, cancel := context.WithCancel(context.Background())
	go func() {
		time.Sleep(500 * time.Millisecond)
		cancel()
	}()
	start := time.Now()
	res, err := RunOnce(ctx, ExecSpec{Command: cmd, Args: args, KillGrace: 500 * time.Millisecond})
	if err != nil {
		t.Fatalf("RunOnce: %v", err)
	}
	if elapsed := time.Since(start); elapsed > 10*time.Second {
		t.Fatalf("RunOnce blocked %v after ctx cancel", elapsed)
	}
	_ = res
}

func sleepScript(sec int) string {
	if runtime.GOOS == "windows" {
		return fmt.Sprintf("ping -n %d 127.0.0.1 >nul", sec)
	}
	return fmt.Sprintf("sleep %d", sec)
}
