package run

import (
	"context"
	"os"
	"runtime"
	"strings"
	"testing"
	"time"

	"github.com/google/uuid"

	"github.com/yan1h/agent-coding-workflow/internal/infra/errs"
	"github.com/yan1h/agent-coding-workflow/internal/workspace"
)

// ===== fakes =====

// fakeWsRepo 只实现 GetWorkspaceByID；其余方法经嵌入 interface 满足契约，被调用即 panic。
type fakeWsRepo struct {
	workspace.Repository
	ws  *workspace.Workspace
	err error
}

func (f *fakeWsRepo) GetWorkspaceByID(context.Context, uuid.UUID) (*workspace.Workspace, error) {
	if f.err != nil {
		return nil, f.err
	}
	return f.ws, nil
}

// fakeWtSvc 只实现 List。
type fakeWtSvc struct {
	workspace.WorktreeService
	wts []*workspace.Worktree
	err error
}

func (f *fakeWtSvc) List(context.Context, workspace.Caller, uuid.UUID) ([]*workspace.Worktree, error) {
	return f.wts, f.err
}

// fakePA 返回固定的 read/write 决策。
type fakePA struct {
	canRead  bool
	canWrite bool
	err      error
}

func (f *fakePA) Resolve(context.Context, uuid.UUID, bool, string) (uuid.UUID, bool, bool, error) {
	return uuid.Nil, f.canRead, f.canWrite, f.err
}
func (f *fakePA) ResolveByID(context.Context, uuid.UUID, bool, uuid.UUID) (bool, bool, error) {
	return f.canRead, f.canWrite, f.err
}

func newExecSvc(t *testing.T, ws *workspace.Workspace, wts []*workspace.Worktree, pa workspace.ProjectAccess, cfg ExecConfig) *ExecService {
	t.Helper()
	return NewExecService(
		&fakeWsRepo{ws: ws},
		&fakeWtSvc{wts: wts},
		pa,
		nil, // audit recorder: nil → audit no-op
		nil, // encryptor unused
		cfg,
		nil,
	)
}

// hasCode 报告 err 是否携带给定 errs.Code。
func hasCode(err error, code errs.Code) bool {
	ae, ok := errs.As(err)
	return ok && ae.Code == code
}

func shellCmd(script string) (string, []string) {
	if runtime.GOOS == "windows" {
		return "cmd", []string{"/c", script}
	}
	return "sh", []string{"-c", script}
}

func baseCfg() ExecConfig {
	return ExecConfig{
		EnvWhitelist:   []string{"PATH"},
		DefaultTimeout: 10 * time.Second,
		MaxTimeout:     30 * time.Second,
		MaxOutputBytes: 1 << 20,
		KillGrace:      time.Second,
	}
}

// ===== tests =====

func TestExecService_RunCommand_Success(t *testing.T) {
	dir := t.TempDir()
	ws := &workspace.Workspace{ID: uuid.New(), ProjectID: uuid.New(), MainPath: dir}
	svc := newExecSvc(t, ws, nil, &fakePA{canRead: true, canWrite: true}, baseCfg())

	cmd, args := shellCmd("echo hi")
	resp, err := svc.RunCommand(context.Background(), workspace.Caller{UserID: uuid.New()}, ExecCommandReq{
		WorkspaceID: ws.ID.String(),
		Command:     cmd,
		Args:        args,
	})
	if err != nil {
		t.Fatalf("RunCommand: %v", err)
	}
	if resp.ExitCode != 0 {
		t.Fatalf("exit = %d", resp.ExitCode)
	}
	if !strings.Contains(resp.Stdout, "hi") {
		t.Fatalf("stdout = %q", resp.Stdout)
	}
}

func TestExecService_AuthDenied(t *testing.T) {
	ws := &workspace.Workspace{ID: uuid.New(), ProjectID: uuid.New(), MainPath: t.TempDir()}
	svc := newExecSvc(t, ws, nil, &fakePA{canRead: true, canWrite: false}, baseCfg())

	cmd, args := shellCmd("echo hi")
	_, err := svc.RunCommand(context.Background(), workspace.Caller{UserID: uuid.New()}, ExecCommandReq{
		WorkspaceID: ws.ID.String(),
		Command:     cmd,
		Args:        args,
	})
	if !hasCode(err, errs.CodeForbidden) {
		t.Fatalf("err = %v, want forbidden", err)
	}
}

func TestExecService_CommandNotAllowed(t *testing.T) {
	ws := &workspace.Workspace{ID: uuid.New(), ProjectID: uuid.New(), MainPath: t.TempDir()}
	cfg := baseCfg()
	cfg.AllowedCommands = []string{"go", "npm"}
	svc := newExecSvc(t, ws, nil, &fakePA{canRead: true, canWrite: true}, cfg)

	cmd, args := shellCmd("echo hi")
	_, err := svc.RunCommand(context.Background(), workspace.Caller{UserID: uuid.New()}, ExecCommandReq{
		WorkspaceID: ws.ID.String(),
		Command:     cmd,
		Args:        args,
	})
	if !hasCode(err, errs.CodeRunExecCommandNotAllowed) {
		t.Fatalf("err = %v, want run.exec_command_not_allowed", err)
	}
}

func TestExecService_EmptyCommand(t *testing.T) {
	ws := &workspace.Workspace{ID: uuid.New(), ProjectID: uuid.New(), MainPath: t.TempDir()}
	svc := newExecSvc(t, ws, nil, &fakePA{canRead: true, canWrite: true}, baseCfg())
	_, err := svc.RunCommand(context.Background(), workspace.Caller{UserID: uuid.New()}, ExecCommandReq{
		WorkspaceID: ws.ID.String(),
		Command:     "   ",
	})
	if !hasCode(err, errs.CodeRunCommandInvalid) {
		t.Fatalf("err = %v, want run.command_invalid", err)
	}
}

// TestExecService_EnvWhitelist 验证子进程环境仅含白名单变量 + 请求覆盖，
// 绝不泄漏 APP_MASTER_KEY 等敏感宿主变量。
func TestExecService_EnvWhitelist(t *testing.T) {
	if runtime.GOOS == "windows" {
		t.Skip("env dump via /bin/sh not portable on windows")
	}
	t.Setenv("APP_MASTER_KEY", "super-secret")
	t.Setenv("PATH", os.Getenv("PATH"))

	ws := &workspace.Workspace{ID: uuid.New(), ProjectID: uuid.New(), MainPath: t.TempDir()}
	svc := newExecSvc(t, ws, nil, &fakePA{canRead: true, canWrite: true}, baseCfg())

	resp, err := svc.RunCommand(context.Background(), workspace.Caller{UserID: uuid.New()}, ExecCommandReq{
		WorkspaceID: ws.ID.String(),
		Command:     "env",
		Env:         map[string]string{"MY_OVERRIDE": "v1"},
	})
	if err != nil {
		t.Fatalf("RunCommand: %v", err)
	}
	if strings.Contains(resp.Stdout, "super-secret") || strings.Contains(resp.Stdout, "APP_MASTER_KEY") {
		t.Fatalf("env leaked master key:\n%s", resp.Stdout)
	}
	if !strings.Contains(resp.Stdout, "MY_OVERRIDE=v1") {
		t.Fatalf("override missing from env:\n%s", resp.Stdout)
	}
	if !strings.Contains(resp.Stdout, "PATH=") {
		t.Fatalf("whitelisted PATH missing from env:\n%s", resp.Stdout)
	}
}

// TestExecService_CwdWorktree 验证 worktree_id 解析到 wt.Path。
func TestExecService_CwdWorktree(t *testing.T) {
	mainDir := t.TempDir()
	wtDir := t.TempDir()
	ws := &workspace.Workspace{ID: uuid.New(), ProjectID: uuid.New(), MainPath: mainDir}
	wtID := uuid.New()
	wts := []*workspace.Worktree{{ID: wtID, WorkspaceID: ws.ID, Path: wtDir}}
	svc := newExecSvc(t, ws, wts, &fakePA{canRead: true, canWrite: true}, baseCfg())

	cmd, args := shellCmd(pwdScript())
	resp, err := svc.RunCommand(context.Background(), workspace.Caller{UserID: uuid.New()}, ExecCommandReq{
		WorkspaceID: ws.ID.String(),
		WorktreeID:  wtID.String(),
		Command:     cmd,
		Args:        args,
	})
	if err != nil {
		t.Fatalf("RunCommand: %v", err)
	}
	// 比较 base name 规避 /private 软链等差异。
	if !strings.Contains(resp.Stdout, lastPathElem(wtDir)) {
		t.Fatalf("cwd stdout %q does not contain worktree dir %q", resp.Stdout, wtDir)
	}
}

// TestExecService_CwdMainPath 验证无 worktree_id 时使用 main_path。
func TestExecService_CwdMainPath(t *testing.T) {
	mainDir := t.TempDir()
	ws := &workspace.Workspace{ID: uuid.New(), ProjectID: uuid.New(), MainPath: mainDir}
	svc := newExecSvc(t, ws, nil, &fakePA{canRead: true, canWrite: true}, baseCfg())

	cmd, args := shellCmd(pwdScript())
	resp, err := svc.RunCommand(context.Background(), workspace.Caller{UserID: uuid.New()}, ExecCommandReq{
		WorkspaceID: ws.ID.String(),
		Command:     cmd,
		Args:        args,
	})
	if err != nil {
		t.Fatalf("RunCommand: %v", err)
	}
	if !strings.Contains(resp.Stdout, lastPathElem(mainDir)) {
		t.Fatalf("cwd stdout %q does not contain main dir %q", resp.Stdout, mainDir)
	}
}

func TestExecService_BadWorktree(t *testing.T) {
	ws := &workspace.Workspace{ID: uuid.New(), ProjectID: uuid.New(), MainPath: t.TempDir()}
	svc := newExecSvc(t, ws, nil, &fakePA{canRead: true, canWrite: true}, baseCfg())

	cmd, args := shellCmd("echo hi")
	_, err := svc.RunCommand(context.Background(), workspace.Caller{UserID: uuid.New()}, ExecCommandReq{
		WorkspaceID: ws.ID.String(),
		WorktreeID:  uuid.New().String(),
		Command:     cmd,
		Args:        args,
	})
	if !hasCode(err, errs.CodeNotFound) {
		t.Fatalf("err = %v, want not_found", err)
	}
}

// TestExecService_Timeout 验证超时返回 TimedOut。
func TestExecService_Timeout(t *testing.T) {
	ws := &workspace.Workspace{ID: uuid.New(), ProjectID: uuid.New(), MainPath: t.TempDir()}
	svc := newExecSvc(t, ws, nil, &fakePA{canRead: true, canWrite: true}, baseCfg())

	var cmd string
	var args []string
	if runtime.GOOS == "windows" {
		cmd, args = "cmd", []string{"/c", "ping -n 30 127.0.0.1 >nul"}
	} else {
		cmd, args = "sh", []string{"-c", "sleep 30"}
	}
	start := time.Now()
	resp, err := svc.RunCommand(context.Background(), workspace.Caller{UserID: uuid.New()}, ExecCommandReq{
		WorkspaceID: ws.ID.String(),
		Command:     cmd,
		Args:        args,
		TimeoutSec:  1,
	})
	if err != nil {
		t.Fatalf("RunCommand: %v", err)
	}
	if !resp.TimedOut {
		t.Fatal("expected TimedOut")
	}
	if time.Since(start) > 10*time.Second {
		t.Fatalf("timeout took too long: %v", time.Since(start))
	}
}

// TestExecService_RunTests 端到端跑 go test -json 解析。
func TestExecService_RunTests(t *testing.T) {
	if _, err := os.Stat(os.Getenv("GOROOT")); os.Getenv("GOROOT") != "" && err != nil {
		t.Skip("GOROOT not available")
	}
	dir := t.TempDir()
	writeGoModule(t, dir)
	ws := &workspace.Workspace{ID: uuid.New(), ProjectID: uuid.New(), MainPath: dir}
	// 透传完整 PATH 让 go 可被定位。
	cfg := baseCfg()
	cfg.EnvWhitelist = []string{"PATH", "HOME", "GOPATH", "GOROOT", "GOCACHE", "SystemRoot", "TEMP", "TMP", "USERPROFILE", "LOCALAPPDATA"}
	cfg.MaxTimeout = 120 * time.Second
	cfg.DefaultTimeout = 120 * time.Second
	svc := newExecSvc(t, ws, nil, &fakePA{canRead: true, canWrite: true}, cfg)

	resp, err := svc.RunTests(context.Background(), workspace.Caller{UserID: uuid.New()}, ExecTestsReq{
		WorkspaceID: ws.ID.String(),
		Command:     "go",
		Args:        []string{"test", "-json", "./..."},
		Format:      "gotest",
		TimeoutSec:  100,
	})
	if err != nil {
		t.Fatalf("RunTests: %v\nstderr:\n%s", err, resp.Stderr)
	}
	if resp.ParseError != "" {
		t.Fatalf("parse error: %s\nstdout:\n%s", resp.ParseError, resp.Stdout)
	}
	if resp.Summary.Framework != "gotest" {
		t.Fatalf("framework = %q", resp.Summary.Framework)
	}
	if resp.Summary.Passed < 1 || resp.Summary.Failed < 1 {
		t.Fatalf("summary = %+v\nstdout:\n%s", resp.Summary, resp.Stdout)
	}
}

// ===== helpers =====

func pwdScript() string {
	if runtime.GOOS == "windows" {
		return "cd"
	}
	return "pwd"
}

func lastPathElem(p string) string {
	p = strings.TrimRight(p, "/\\")
	if i := strings.LastIndexAny(p, "/\\"); i >= 0 {
		return p[i+1:]
	}
	return p
}

func writeGoModule(t *testing.T, dir string) {
	t.Helper()
	mustWrite(t, dir+"/go.mod", "module selftest\n\ngo 1.21\n")
	src := `package selftest

import "testing"

func TestPasses(t *testing.T) {}

func TestFails(t *testing.T) { t.Fatal("intentional failure") }
`
	mustWrite(t, dir+"/selftest_test.go", src)
}

func mustWrite(t *testing.T, path, content string) {
	t.Helper()
	if err := os.WriteFile(path, []byte(content), 0o644); err != nil {
		t.Fatalf("write %s: %v", path, err)
	}
}
