package workspace

import (
	"context"
	"errors"
	"fmt"
	"os"
	"os/exec"
	"path/filepath"
	"strconv"
	"strings"
	"syscall"
	"testing"
	"time"

	iop "iop/proto/gen/iop"
)

func TestMain(m *testing.M) {
	if handled, exitCode := RunCommandShim(os.Args); handled {
		os.Exit(exitCode)
	}
	os.Exit(m.Run())
}

func TestWorkspaceCommandHelperProcess(t *testing.T) {
	mode := os.Getenv("IOP_WORKSPACE_HELPER")
	if mode == "" {
		return
	}
	switch mode {
	case "success":
		_, _ = fmt.Fprint(os.Stdout, "command-stdout")
		_, _ = fmt.Fprint(os.Stderr, "command-stderr")
	case "nonzero":
		os.Exit(7)
	case "environment":
		_, _ = fmt.Fprintf(os.Stdout, "%s|%s", os.Getenv("IOP_TEST_VALUE"), os.Getenv("IOP_AMBIENT_SECRET"))
	case "output":
		_, _ = fmt.Fprint(os.Stdout, strings.Repeat("o", 128<<10))
		_, _ = fmt.Fprint(os.Stderr, strings.Repeat("e", 128<<10))
	case "cwd":
		identity, err := os.ReadFile("identity.txt")
		if err != nil {
			os.Exit(8)
		}
		cwd, err := os.Getwd()
		if err != nil {
			os.Exit(9)
		}
		_, _ = fmt.Fprintf(os.Stdout, "%s|%s", identity, cwd)
	case "block":
		if err := os.WriteFile(os.Getenv("IOP_START_FILE"), []byte("started"), 0o600); err != nil {
			os.Exit(10)
		}
		for {
			time.Sleep(time.Hour)
		}
	case "group":
		cmd := exec.Command(os.Args[0], "-test.run=^TestWorkspaceCommandGrandchild$")
		cmd.Env = []string{"IOP_WORKSPACE_GRANDCHILD=1"}
		if err := cmd.Start(); err != nil {
			os.Exit(11)
		}
		if err := os.WriteFile(os.Getenv("IOP_CHILD_PID_FILE"), []byte(strconv.Itoa(cmd.Process.Pid)), 0o600); err != nil {
			_ = cmd.Process.Kill()
			os.Exit(12)
		}
		for {
			time.Sleep(time.Hour)
		}
	case "sentinel":
		if err := os.WriteFile(os.Getenv("IOP_SENTINEL_FILE"), []byte("target-started"), 0o600); err != nil {
			os.Exit(13)
		}
	default:
		os.Exit(14)
	}
	os.Exit(0)
}

func TestWorkspaceCommandGrandchild(t *testing.T) {
	if os.Getenv("IOP_WORKSPACE_GRANDCHILD") == "" {
		return
	}
	for {
		time.Sleep(time.Hour)
	}
}

func newCommandRuntime(t *testing.T, root string, outputLimit int64) *Runtime {
	t.Helper()
	executable, err := os.Executable()
	if err != nil {
		t.Fatal(err)
	}
	runtime, err := NewRuntime([]*iop.WorkspaceConfig{{
		Ref: "workspace-command", Platform: "darwin", Root: root,
		Operations: []iop.WorkspaceOperation{iop.WorkspaceOperation_WORKSPACE_OPERATION_COMMAND},
		Commands: []*iop.WorkspaceCommandConfig{{
			Id: "helper", Executable: executable,
			Args: []string{"-test.run=^TestWorkspaceCommandHelperProcess$"},
		}},
		EnvironmentAllowlist: []string{
			"IOP_WORKSPACE_HELPER", "IOP_TEST_VALUE", "IOP_START_FILE",
			"IOP_CHILD_PID_FILE", "IOP_SENTINEL_FILE",
		},
		MaxOutputBytes: outputLimit, MaxCommandTimeoutMs: 3000,
	}}, "darwin", nil)
	if err != nil {
		t.Fatal(err)
	}
	t.Cleanup(func() { _ = runtime.Close() })
	return runtime
}

func openCommandRequest(t *testing.T, runtime *Runtime, requestID string, outputLimit int64) {
	t.Helper()
	_, err := runtime.Open(RequestAuthority{
		RequestID: requestID, WorkspaceRef: "workspace-command",
		Operations: []iop.WorkspaceOperation{iop.WorkspaceOperation_WORKSPACE_OPERATION_COMMAND},
		CommandIDs: []string{"helper"}, MaxOutputBytes: outputLimit, MaxCommandTimeoutMS: 3000,
	})
	if err != nil {
		t.Fatal(err)
	}
}

func commandInput(requestID, toolCallID, mode string) CommandInput {
	return CommandInput{
		RequestID: requestID, ToolCallID: toolCallID, CommandID: "helper", TimeoutMS: 2000,
		Environment: map[string]string{"IOP_WORKSPACE_HELPER": mode},
	}
}

func TestCommandExecutorSuccessFailureAndEnvironment(t *testing.T) {
	t.Setenv("IOP_AMBIENT_SECRET", "must-not-be-inherited")
	runtime := newCommandRuntime(t, t.TempDir(), 256)
	openCommandRequest(t, runtime, "request-success", 256)

	success := runtime.ExecuteCommand(context.Background(), commandInput("request-success", "tool-success", "success"))
	if success.Status != iop.WorkspaceStatus_WORKSPACE_STATUS_SUCCESS || success.ExitCode != 0 || string(success.Stdout) != "command-stdout" || string(success.Stderr) != "command-stderr" {
		t.Fatalf("success = %+v", success)
	}
	nonzero := runtime.ExecuteCommand(context.Background(), commandInput("request-success", "tool-nonzero", "nonzero"))
	if nonzero.Status != iop.WorkspaceStatus_WORKSPACE_STATUS_ERROR || nonzero.Code != iop.WorkspaceErrorCode_WORKSPACE_ERROR_CODE_INTERNAL || nonzero.ExitCode != 7 {
		t.Fatalf("nonzero = %+v", nonzero)
	}
	environmentInput := commandInput("request-success", "tool-environment", "environment")
	environmentInput.Environment["IOP_TEST_VALUE"] = "approved"
	environment := runtime.ExecuteCommand(context.Background(), environmentInput)
	if environment.Status != iop.WorkspaceStatus_WORKSPACE_STATUS_SUCCESS || string(environment.Stdout) != "approved|" {
		t.Fatalf("environment = %+v", environment)
	}

	unknown := commandInput("request-success", "tool-unknown", "success")
	unknown.CommandID = "not-approved"
	if result := runtime.ExecuteCommand(context.Background(), unknown); result.Code != iop.WorkspaceErrorCode_WORKSPACE_ERROR_CODE_INVALID_REQUEST {
		t.Fatalf("unknown command = %+v", result)
	}
	unapprovedEnvironment := commandInput("request-success", "tool-env-denied", "success")
	unapprovedEnvironment.Environment["HOME"] = "/sensitive"
	if result := runtime.ExecuteCommand(context.Background(), unapprovedEnvironment); result.Code != iop.WorkspaceErrorCode_WORKSPACE_ERROR_CODE_INVALID_REQUEST {
		t.Fatalf("unapproved environment = %+v", result)
	}
	oversizedTimeout := commandInput("request-success", "tool-timeout-denied", "success")
	oversizedTimeout.TimeoutMS = 3001
	if result := runtime.ExecuteCommand(context.Background(), oversizedTimeout); result.Code != iop.WorkspaceErrorCode_WORKSPACE_ERROR_CODE_INVALID_REQUEST {
		t.Fatalf("oversized timeout = %+v", result)
	}
}

func TestCommandExecutorSharedOutputBound(t *testing.T) {
	runtime := newCommandRuntime(t, t.TempDir(), 64)
	openCommandRequest(t, runtime, "request-output", 64)
	result := runtime.ExecuteCommand(context.Background(), commandInput("request-output", "tool-output", "output"))
	if result.Status != iop.WorkspaceStatus_WORKSPACE_STATUS_SUCCESS || !result.Truncated || len(result.Stdout)+len(result.Stderr) > 64 {
		t.Fatalf("bounded output = %+v stdout=%d stderr=%d", result, len(result.Stdout), len(result.Stderr))
	}
}

func TestCommandExecutorTimeoutAndContextCancel(t *testing.T) {
	root := t.TempDir()
	runtime := newCommandRuntime(t, root, 64)
	openCommandRequest(t, runtime, "request-timeout", 64)
	timeout := commandInput("request-timeout", "tool-timeout", "block")
	timeout.TimeoutMS = 50
	timeout.Environment["IOP_START_FILE"] = filepath.Join(root, "timeout-started")
	result := runtime.ExecuteCommand(context.Background(), timeout)
	if result.Status != iop.WorkspaceStatus_WORKSPACE_STATUS_TIMEOUT || result.Code != iop.WorkspaceErrorCode_WORKSPACE_ERROR_CODE_TIMEOUT || result.ExitCode != -1 {
		t.Fatalf("timeout = %+v", result)
	}

	// Pre-cancelled context fast-path assertion.
	preCtx, preCancel := context.WithCancel(context.Background())
	preCancel()
	preInput := commandInput("request-timeout", "tool-pre-cancel", "success")
	result = runtime.ExecuteCommand(preCtx, preInput)
	if result.Status != iop.WorkspaceStatus_WORKSPACE_STATUS_CANCELLED || result.Code != iop.WorkspaceErrorCode_WORKSPACE_ERROR_CODE_CANCELLED {
		t.Fatalf("pre-cancelled context = %+v", result)
	}

	// Live active context cancellation and process group termination assertion.
	pidFile := filepath.Join(root, "active-child.pid")
	activeCtx, activeCancel := context.WithCancel(context.Background())
	activeInput := commandInput("request-timeout", "tool-active-context", "group")
	activeInput.Environment["IOP_CHILD_PID_FILE"] = pidFile
	resultCh := make(chan Result, 1)
	go func() {
		resultCh <- runtime.ExecuteCommand(activeCtx, activeInput)
	}()
	waitForFile(t, pidFile)
	pidBytes, err := os.ReadFile(pidFile)
	if err != nil {
		t.Fatal(err)
	}
	pid, err := strconv.Atoi(strings.TrimSpace(string(pidBytes)))
	if err != nil {
		t.Fatal(err)
	}
	activeCancel()
	activeResult := <-resultCh
	if activeResult.Status != iop.WorkspaceStatus_WORKSPACE_STATUS_CANCELLED || activeResult.Code != iop.WorkspaceErrorCode_WORKSPACE_ERROR_CODE_CANCELLED || activeResult.ExitCode != -1 {
		t.Fatalf("live active context cancel result = %+v", activeResult)
	}
	deadline := time.Now().Add(2 * time.Second)
	for processExists(pid) && time.Now().Before(deadline) {
		time.Sleep(10 * time.Millisecond)
	}
	if processExists(pid) {
		t.Fatalf("grandchild process %d survived live active context cancellation", pid)
	}
}

func TestCommandExecutorExplicitCancelAndRequestIsolation(t *testing.T) {
	root := t.TempDir()
	runtime := newCommandRuntime(t, root, 64)
	openCommandRequest(t, runtime, "request-a", 64)
	openCommandRequest(t, runtime, "request-b", 64)
	resultA := make(chan Result, 1)
	resultB := make(chan Result, 1)
	inputA := commandInput("request-a", "tool-shared", "block")
	inputA.Environment["IOP_START_FILE"] = filepath.Join(root, "started-a")
	inputB := commandInput("request-b", "tool-shared", "block")
	inputB.Environment["IOP_START_FILE"] = filepath.Join(root, "started-b")
	go func() { resultA <- runtime.ExecuteCommand(context.Background(), inputA) }()
	go func() { resultB <- runtime.ExecuteCommand(context.Background(), inputB) }()
	waitForFile(t, inputA.Environment["IOP_START_FILE"])
	waitForFile(t, inputB.Environment["IOP_START_FILE"])

	if wrong := runtime.Cancel("request-a", "tool-other"); wrong.Code != iop.WorkspaceErrorCode_WORKSPACE_ERROR_CODE_NOT_FOUND {
		t.Fatalf("wrong cancel = %+v", wrong)
	}
	if cancelled := runtime.Cancel("request-a", "tool-shared"); cancelled.Status != iop.WorkspaceStatus_WORKSPACE_STATUS_CANCELLED {
		t.Fatalf("cancel a = %+v", cancelled)
	}
	if result := <-resultA; result.Status != iop.WorkspaceStatus_WORKSPACE_STATUS_CANCELLED {
		t.Fatalf("result a = %+v", result)
	}
	select {
	case result := <-resultB:
		t.Fatalf("cross-request cancel stopped b: %+v", result)
	case <-time.After(50 * time.Millisecond):
	}
	if cancelled := runtime.Cancel("request-b", "tool-shared"); cancelled.Status != iop.WorkspaceStatus_WORKSPACE_STATUS_CANCELLED {
		t.Fatalf("cancel b = %+v", cancelled)
	}
	if result := <-resultB; result.Status != iop.WorkspaceStatus_WORKSPACE_STATUS_CANCELLED {
		t.Fatalf("result b = %+v", result)
	}
}

func TestCommandExecutorCancelKillsProcessGroup(t *testing.T) {
	root := t.TempDir()
	runtime := newCommandRuntime(t, root, 64)
	openCommandRequest(t, runtime, "request-group", 64)
	pidFile := filepath.Join(root, "child.pid")
	input := commandInput("request-group", "tool-group", "group")
	input.Environment["IOP_CHILD_PID_FILE"] = pidFile
	resultChannel := make(chan Result, 1)
	go func() { resultChannel <- runtime.ExecuteCommand(context.Background(), input) }()
	waitForFile(t, pidFile)
	pidBytes, err := os.ReadFile(pidFile)
	if err != nil {
		t.Fatal(err)
	}
	pid, err := strconv.Atoi(string(pidBytes))
	if err != nil {
		t.Fatal(err)
	}
	if cancelled := runtime.Cancel("request-group", "tool-group"); cancelled.Status != iop.WorkspaceStatus_WORKSPACE_STATUS_CANCELLED {
		t.Fatalf("cancel = %+v", cancelled)
	}
	if result := <-resultChannel; result.Status != iop.WorkspaceStatus_WORKSPACE_STATUS_CANCELLED {
		t.Fatalf("result = %+v", result)
	}
	deadline := time.Now().Add(2 * time.Second)
	for processExists(pid) && time.Now().Before(deadline) {
		time.Sleep(10 * time.Millisecond)
	}
	if processExists(pid) {
		t.Fatalf("grandchild process %d survived group cancellation", pid)
	}
}

func TestCommandExecutorUsesOpenedRootAfterRenameReplacement(t *testing.T) {
	parent := t.TempDir()
	root := filepath.Join(parent, "workspace")
	if err := os.Mkdir(root, 0o700); err != nil {
		t.Fatal(err)
	}
	if err := os.WriteFile(filepath.Join(root, "identity.txt"), []byte("original"), 0o600); err != nil {
		t.Fatal(err)
	}
	runtime := newCommandRuntime(t, root, 256)
	openCommandRequest(t, runtime, "request-cwd", 256)
	renamed := filepath.Join(parent, "workspace-renamed")
	if err := os.Rename(root, renamed); err != nil {
		t.Fatal(err)
	}
	foreign := filepath.Join(parent, "foreign")
	if err := os.Mkdir(foreign, 0o700); err != nil {
		t.Fatal(err)
	}
	if err := os.WriteFile(filepath.Join(foreign, "identity.txt"), []byte("foreign"), 0o600); err != nil {
		t.Fatal(err)
	}
	if err := os.Symlink(foreign, root); err != nil {
		t.Fatal(err)
	}
	result := runtime.ExecuteCommand(context.Background(), commandInput("request-cwd", "tool-cwd", "cwd"))
	if result.Status != iop.WorkspaceStatus_WORKSPACE_STATUS_SUCCESS || !strings.HasPrefix(string(result.Stdout), "original|") || strings.Contains(string(result.Stdout), "foreign") {
		t.Fatalf("cwd result = %+v", result)
	}
}

func TestCommandExecutorRejectsCorruptRootIdentityBeforeTarget(t *testing.T) {
	rootPath := t.TempDir()
	directory, err := os.Open(rootPath)
	if err != nil {
		t.Fatal(err)
	}
	defer directory.Close()
	info, err := directory.Stat()
	if err != nil {
		t.Fatal(err)
	}
	device, inode, ok := fileIdentity(info)
	if !ok {
		t.Fatal("root identity unavailable")
	}
	executable, err := os.Executable()
	if err != nil {
		t.Fatal(err)
	}
	sentinel := filepath.Join(rootPath, "target-started")
	output := newCommandOutput(64)
	process, err := startCommandProcess(commandLaunchRecord{
		Version: commandLaunchVersion, Executable: executable,
		Args:        []string{"-test.run=^TestWorkspaceCommandHelperProcess$"},
		Environment: []string{"IOP_SENTINEL_FILE=" + sentinel, "IOP_WORKSPACE_HELPER=sentinel"},
		Device:      device, Inode: inode + 1,
	}, directory, output)
	if err != nil {
		t.Fatal(err)
	}
	result := awaitCommand(context.Background(), newCommandExecution(), process, time.Second, time.Now(), output)
	if result.Status != iop.WorkspaceStatus_WORKSPACE_STATUS_ERROR || result.Code != iop.WorkspaceErrorCode_WORKSPACE_ERROR_CODE_INTERNAL {
		t.Fatalf("corrupt identity result = %+v", result)
	}
	if _, err := os.Stat(sentinel); !errors.Is(err, os.ErrNotExist) {
		t.Fatalf("target sentinel exists or stat failed unexpectedly: %v", err)
	}
}

func waitForFile(t *testing.T, path string) {
	t.Helper()
	deadline := time.Now().Add(2 * time.Second)
	for time.Now().Before(deadline) {
		if _, err := os.Stat(path); err == nil {
			return
		}
		time.Sleep(10 * time.Millisecond)
	}
	t.Fatalf("timed out waiting for %s", filepath.Base(path))
}

func processExists(pid int) bool {
	err := syscall.Kill(pid, 0)
	return err == nil || !errors.Is(err, syscall.ESRCH)
}
