package process_test

import (
	"context"
	"cursor-api-proxy/internal/process"
	"os"
	"testing"
	"time"
)

// sh 是跨平台 shell 執行小 script 的輔助函式
func sh(t *testing.T, script string, opts process.RunOptions) (process.RunResult, error) {
	t.Helper()
	return process.Run("sh", []string{"-c", script}, opts)
}

func TestRun_StdoutAndStderr(t *testing.T) {
	result, err := sh(t, "echo hello; echo world >&2", process.RunOptions{})
	if err != nil {
		t.Fatal(err)
	}
	if result.Code != 0 {
		t.Errorf("Code = %d, want 0", result.Code)
	}
	if result.Stdout != "hello\n" {
		t.Errorf("Stdout = %q, want %q", result.Stdout, "hello\n")
	}
	if result.Stderr != "world\n" {
		t.Errorf("Stderr = %q, want %q", result.Stderr, "world\n")
	}
}

func TestRun_BasicSpawn(t *testing.T) {
	result, err := sh(t, "printf ok", process.RunOptions{})
	if err != nil {
		t.Fatal(err)
	}
	if result.Code != 0 {
		t.Errorf("Code = %d, want 0", result.Code)
	}
	if result.Stdout != "ok" {
		t.Errorf("Stdout = %q, want ok", result.Stdout)
	}
}

func TestRun_ConfigDir_Propagated(t *testing.T) {
	result, err := process.Run("sh", []string{"-c", `printf "$CURSOR_CONFIG_DIR"`},
		process.RunOptions{ConfigDir: "/test/account/dir"})
	if err != nil {
		t.Fatal(err)
	}
	if result.Stdout != "/test/account/dir" {
		t.Errorf("Stdout = %q, want /test/account/dir", result.Stdout)
	}
}

func TestRun_ConfigDir_Absent(t *testing.T) {
	// 確保沒有殘留的環境變數
	_ = os.Unsetenv("CURSOR_CONFIG_DIR")
	result, err := process.Run("sh", []string{"-c", `printf "${CURSOR_CONFIG_DIR:-unset}"`},
		process.RunOptions{})
	if err != nil {
		t.Fatal(err)
	}
	if result.Stdout != "unset" {
		t.Errorf("Stdout = %q, want unset", result.Stdout)
	}
}

func TestRun_NonZeroExit(t *testing.T) {
	result, err := sh(t, "exit 42", process.RunOptions{})
	if err != nil {
		t.Fatal(err)
	}
	if result.Code != 42 {
		t.Errorf("Code = %d, want 42", result.Code)
	}
}

func TestRun_Timeout(t *testing.T) {
	start := time.Now()
	result, err := sh(t, "sleep 30", process.RunOptions{TimeoutMs: 300})
	elapsed := time.Since(start)
	if err != nil {
		t.Fatal(err)
	}
	if result.Code == 0 {
		t.Error("expected non-zero exit code after timeout")
	}
	if elapsed > 2*time.Second {
		t.Errorf("elapsed %v, want < 2s", elapsed)
	}
}

func TestRunStreaming_OnLine(t *testing.T) {
	var lines []string
	result, err := process.RunStreaming("sh", []string{"-c", "printf 'a\nb\nc\n'"},
		process.RunStreamingOptions{
			OnLine: func(line string) { lines = append(lines, line) },
		})
	if err != nil {
		t.Fatal(err)
	}
	if result.Code != 0 {
		t.Errorf("Code = %d, want 0", result.Code)
	}
	if len(lines) != 3 {
		t.Errorf("got %d lines, want 3: %v", len(lines), lines)
	}
	if lines[0] != "a" || lines[1] != "b" || lines[2] != "c" {
		t.Errorf("lines = %v, want [a b c]", lines)
	}
}

func TestRunStreaming_FlushFinalLine(t *testing.T) {
	var lines []string
	result, err := process.RunStreaming("sh", []string{"-c", "printf tail"},
		process.RunStreamingOptions{
			OnLine: func(line string) { lines = append(lines, line) },
		})
	if err != nil {
		t.Fatal(err)
	}
	if result.Code != 0 {
		t.Errorf("Code = %d, want 0", result.Code)
	}
	if len(lines) != 1 {
		t.Errorf("got %d lines, want 1: %v", len(lines), lines)
	}
	if lines[0] != "tail" {
		t.Errorf("lines[0] = %q, want tail", lines[0])
	}
}

func TestRunStreaming_ConfigDir(t *testing.T) {
	var lines []string
	_, err := process.RunStreaming("sh", []string{"-c", `printf "$CURSOR_CONFIG_DIR"`},
		process.RunStreamingOptions{
			RunOptions: process.RunOptions{ConfigDir: "/my/config/dir"},
			OnLine:     func(line string) { lines = append(lines, line) },
		})
	if err != nil {
		t.Fatal(err)
	}
	if len(lines) != 1 || lines[0] != "/my/config/dir" {
		t.Errorf("lines = %v, want [/my/config/dir]", lines)
	}
}

func TestRunStreaming_Stderr(t *testing.T) {
	result, err := process.RunStreaming("sh", []string{"-c", "echo err-output >&2"},
		process.RunStreamingOptions{OnLine: func(string) {}})
	if err != nil {
		t.Fatal(err)
	}
	if result.Stderr == "" {
		t.Error("expected stderr to contain output")
	}
}

func TestRunStreaming_Timeout(t *testing.T) {
	start := time.Now()
	result, err := process.RunStreaming("sh", []string{"-c", "sleep 30"},
		process.RunStreamingOptions{
			RunOptions: process.RunOptions{TimeoutMs: 300},
			OnLine:     func(string) {},
		})
	elapsed := time.Since(start)
	if err != nil {
		t.Fatal(err)
	}
	if result.Code == 0 {
		t.Error("expected non-zero exit code after timeout")
	}
	if elapsed > 2*time.Second {
		t.Errorf("elapsed %v, want < 2s", elapsed)
	}
}

func TestRunStreaming_Concurrent(t *testing.T) {
	var lines1, lines2 []string
	done := make(chan struct{}, 2)

	run := func(label string, target *[]string) {
		process.RunStreaming("sh", []string{"-c", "printf '" + label + "'"},
			process.RunStreamingOptions{
				OnLine: func(line string) { *target = append(*target, line) },
			})
		done <- struct{}{}
	}

	go run("stream1", &lines1)
	go run("stream2", &lines2)

	<-done
	<-done

	if len(lines1) != 1 || lines1[0] != "stream1" {
		t.Errorf("lines1 = %v, want [stream1]", lines1)
	}
	if len(lines2) != 1 || lines2[0] != "stream2" {
		t.Errorf("lines2 = %v, want [stream2]", lines2)
	}
}

func TestRunStreaming_ContextCancel(t *testing.T) {
	ctx, cancel := context.WithCancel(context.Background())
	start := time.Now()
	done := make(chan struct{})

	go func() {
		process.RunStreaming("sh", []string{"-c", "sleep 30"},
			process.RunStreamingOptions{
				RunOptions: process.RunOptions{Ctx: ctx},
				OnLine:     func(string) {},
			})
		close(done)
	}()

	time.AfterFunc(100*time.Millisecond, cancel)
	<-done
	elapsed := time.Since(start)

	if elapsed > 2*time.Second {
		t.Errorf("elapsed %v, want < 2s", elapsed)
	}
}

func TestRun_ContextCancel(t *testing.T) {
	ctx, cancel := context.WithCancel(context.Background())
	start := time.Now()
	done := make(chan process.RunResult, 1)

	go func() {
		r, _ := process.Run("sh", []string{"-c", "sleep 30"}, process.RunOptions{Ctx: ctx})
		done <- r
	}()

	time.AfterFunc(100*time.Millisecond, cancel)
	result := <-done
	elapsed := time.Since(start)

	if result.Code == 0 {
		t.Error("expected non-zero exit code after cancel")
	}
	if elapsed > 2*time.Second {
		t.Errorf("elapsed %v, want < 2s", elapsed)
	}
}

func TestRun_AlreadyCancelledContext(t *testing.T) {
	ctx, cancel := context.WithCancel(context.Background())
	cancel() // 已取消

	start := time.Now()
	result, _ := process.Run("sh", []string{"-c", "sleep 30"}, process.RunOptions{Ctx: ctx})
	elapsed := time.Since(start)

	if result.Code == 0 {
		t.Error("expected non-zero exit code")
	}
	if elapsed > 2*time.Second {
		t.Errorf("elapsed %v, want < 2s", elapsed)
	}
}

func TestKillAllChildProcesses(t *testing.T) {
	done := make(chan process.RunResult, 1)
	go func() {
		r, _ := process.Run("sh", []string{"-c", "sleep 30"}, process.RunOptions{})
		done <- r
	}()

	time.Sleep(80 * time.Millisecond)
	process.KillAllChildProcesses()
	result := <-done

	if result.Code == 0 {
		t.Error("expected non-zero exit code after kill")
	}
	// 再次呼叫不應 panic
	process.KillAllChildProcesses()
}
